Skip to content

Commit

Permalink
Ag 50 crypto lib integration (#42)
Browse files Browse the repository at this point in the history
* feat(script): AG-50 import crypto-py

added isntruction for the integration of external libs using symlinks

* feat(scripts): AG-50 integrated crypto-py lib

integrated cripto-py lib in the project and updated smart contracts, scripts and tests

* updated readme

* feat(CI/CD): AG-50 implemented step for python lib

added a step in the CI/CD workflow for the installation of python libs

* yaml fix

* updated workflow

* updated workflow

* updated workflow

* updated workflow

* feat(CI/CD): AG-50 imtegrated python libs in ci/cd

integrated the installation of python libs in the CI/CD workflow
  • Loading branch information
g3k0 authored Apr 21, 2024
1 parent b2a7637 commit aec0591
Show file tree
Hide file tree
Showing 14 changed files with 289 additions and 474 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,17 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: 20.x

- name: Set up Python 3
uses: actions/setup-python@v2
with:
python-version: '3.11'

- name: Install dependencies
run: node ci --function installDeps

- name: install python libs
run: node ci --function installPythonLibs --params "{\"libs\":[\"crypto-py\"]}"

- name: Run linter
run: node ci --function lint
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ node_modules
report

#ignition files
/ignition/deployments
/ignition/deployments

# external libs linked by symlinks
crypto-py
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Agora is a web3 dApp based on the [Hardhat framework](https://hardhat.org/) and
In order to run the application you need the following software installed on you machine:

* [Node.js](https://nodejs.org/en) v20.11.1 or above
* [Python](https://www.python.org/) v3.11.2 or above

You also need an Alchemy account.

Expand All @@ -39,6 +40,18 @@ To setup the application follow these steps:
* `REPORT_GAS` enable or disable the gas report on smart contracts unit tests executions;
* `NODE_ENV` set `development` for your local machine;

### Install crypto-py

Agora uses a Python external library for cryptographic operations called [crypto-py](https://github.com/nova-collective/crypto-py). This library needs to be
manually integrated into the Hardhat framework, follow this steps:

5. from the root folder of application, go to the `lib` folder;
6. clone `crypto-py` inside the lib folder;
6. go inside the crypto-py folder and install the dependencies `pip install -r requirements.txt`;
7. if you decide to set a python environment inside the crypto-py folder, remember to activate the environment;

Alternatively, If you are in a Unix environment, you can clone the library somewhere else and create a symbolic link inside the Agora lib folder.

## How to commit

The `main` and the `develop` branches are protected. It is required to open and review pull requests in order to merge the code.
Expand Down Expand Up @@ -66,7 +79,7 @@ Smart contracts code coverage documentation [here](https://www.npmjs.com/package

| step | library used | theshold | is error blocking |
|------|--------------|----------|-------------------|
| Code Linting | [eslint](https://www.npmjs.com/package/eslint) | No thesholds | yes |
| Code Linting | [eslint](https://www.npmjs.com/package/eslint) | Not applicable | yes |
| Code duplication | [jscpd](https://www.npmjs.com/package/jscpd) | 10% | yes |
| Smart contracts unit tests | [jest](https://www.npmjs.com/package/jest) | all tests must pass | yes |
| TypeScript files unit tests | [jest](https://www.npmjs.com/package/jest) | all tests must pass; 80% code coverage | yes |
Expand Down
7 changes: 7 additions & 0 deletions ci/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
installPythonLibs: {
cryptoPyRepo: "https://github.com/nova-collective/crypto-py",
},
};

module.exports = config;
21 changes: 21 additions & 0 deletions ci/functions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const execSync = require("child_process").execSync;
const fs = require("fs");
const path = require("path");
const config = require("./config");

const execSyncOptions = { stdio: "inherit" };

Expand Down Expand Up @@ -68,6 +70,25 @@ const functions = {
process.exit(1);
}
},
installPythonLibs: function ({ libs }) {
console.log(libs);

try {
if (libs.includes("crypto-py")) {
const libPath = path.resolve("lib");
const repo = config.installPythonLibs.cryptoPyRepo;

execSync(`cd ${libPath} && git clone ${repo}`, execSyncOptions);
execSync(
`cd ${libPath}/crypto-py && pip install -r requirements.txt`,
execSyncOptions,
);
}
} catch (e) {
console.log(e);
process.exit(1);
}
},
};

module.exports = functions;
6 changes: 2 additions & 4 deletions contracts/DEC.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ contract DEC {
Encrypted country;

struct Encrypted {
string iv;
string ephemPublicKey;
string ciphertext;
string mac;
string chiper;
string nonce;
}

constructor(
Expand Down
10 changes: 5 additions & 5 deletions election-scripts/create-dec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ethers } from "hardhat";
import { DEC, Response, result, CreateDECResponse } from "./types";
import { DECMock, VoterEOA } from "./__mocks__";
import { encryptString } from "../lib";
import { Encrypted } from "eth-crypto";
import { Encrypted } from "../lib/types";

/**
* This function encrypt the Voter's DECs data and deploys the smart contract instance.
Expand All @@ -35,10 +35,10 @@ export async function main(
const dec = decsData || DECMock;
const key = privateKey || VoterEOA.privateKey;

const eTaxCode: Encrypted = await encryptString(dec.taxCode, key);
const eMunicipality: Encrypted = await encryptString(dec.municipality, key);
const eRegion: Encrypted = await encryptString(dec.region, key);
const eCountry: Encrypted = await encryptString(dec.country, key);
const eTaxCode: Encrypted = encryptString(dec.taxCode, key);
const eMunicipality: Encrypted = encryptString(dec.municipality, key);
const eRegion: Encrypted = encryptString(dec.region, key);
const eCountry: Encrypted = encryptString(dec.country, key);

const contract = await ContractFactory.deploy(
eTaxCode,
Expand Down
27 changes: 18 additions & 9 deletions lib/crypto-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,30 @@ import { mockEOAs } from "./__mocks__";
describe("Crypto Utils", () => {
const privateKey = mockEOAs[0].privateKey;

it("should encrypt and decrypt a string", async () => {
it("should encrypt and decrypt a string", () => {
const originalString = "Hello, world!";
const encryptedString = await encryptString(originalString, privateKey);
const encryptedString = encryptString(originalString, privateKey);

const decryptedString = await decryptString(encryptedString, privateKey);
expect(decryptedString).toEqual(originalString);
const decryptedString = decryptString(
encryptedString.chiper,
privateKey,
encryptedString.nonce,
);
expect(decryptedString.message).toEqual(originalString);
});

it("should handle decryption with incorrect private key", async () => {
const originalString = "Hello, world!";
const encryptedString = await encryptString(originalString, privateKey);
const encryptedString = encryptString(originalString, privateKey);
const incorrectPrivateKey = "incorrectPrivateKey";

await expect(
decryptString(encryptedString, incorrectPrivateKey),
).rejects.toThrow("Error decrypting string");
try {
decryptString(
encryptedString.chiper,
incorrectPrivateKey,
encryptedString.nonce,
);
} catch (e: any) {
expect(e.message).toBe("Error decrypting string");
}
});
});
83 changes: 64 additions & 19 deletions lib/crypto-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
* is because If we send the data and then encrypt it in solidity, the data will be visible in the transaction that
* in the first place was used to send the data to the contract. Also, solidity doesn't have a function to encrypt.
*/
import * as EthCrypto from "eth-crypto";
import { Encrypted } from "eth-crypto";
import { Encrypted, Decrypted } from "./types";
import { execSync, ExecSyncOptionsWithStringEncoding } from "child_process";
import * as path from "path";

/**
* This function encrypt a string by using a private key possibly from an EOA (Voter's account).
Expand All @@ -16,20 +17,36 @@ import { Encrypted } from "eth-crypto";
*
* @param {string} decryptedString - the string to be encrypted
* @param {string} privateKey - a private key, e.g. from the EOA
* @returns {Promise<Encrypted>} - the encrypted output string
* @returns {Encrypted} - the encrypted output string
*/
export async function encryptString(
export function encryptString(
decryptedString: string,
privateKey: string,
): Promise<Encrypted> {
): Encrypted {
try {
const publicKey = await EthCrypto.publicKeyByPrivateKey(privateKey);
const execSyncOptions = {
stdio: "pipe",
} as ExecSyncOptionsWithStringEncoding;

const encrypted = await EthCrypto.encryptWithPublicKey(
publicKey,
decryptedString,
if (privateKey.startsWith("0x")) {
privateKey = privateKey.substring(2);
}

const cryptoPyPath = getCryptoPyPath();

const encrypted = execSync(
`cd ${cryptoPyPath} && python3 Crypto.py AESGCM_encrypt --key="${privateKey}" --secret="${decryptedString}"`,
execSyncOptions,
);
return encrypted;

const en = encrypted.toString().split("\n");
const chiper = en[0].split(" ")[1];
const nonce = en[1].split(" ")[1];

return {
chiper,
nonce,
};
} catch (e) {
console.error(e);
throw new Error("Error encrypting string");
Expand All @@ -41,20 +58,48 @@ export async function encryptString(
*
* @param {Encrypted} encryptedString - the secret to decrypt
* @param {string} privateKey - A private key possibly from an EOA
* @returns {Promise<string>} - the string decrypted
* @param {string} nonce - The nonce value used for the encryption
* @returns {Decrypted} - the string decrypted
*/
export async function decryptString(
encryptedString: Encrypted,
export function decryptString(
encryptedString: string,
privateKey: string,
): Promise<string> {
nonce: string,
): Decrypted {
try {
const decrypted = await EthCrypto.decryptWithPrivateKey(
privateKey,
encryptedString,
const execSyncOptions = {
stdio: "pipe",
} as ExecSyncOptionsWithStringEncoding;

if (privateKey.startsWith("0x")) {
privateKey = privateKey.substring(2);
}

const cryptoPyPath = getCryptoPyPath();
const decrypted = execSync(
`cd ${cryptoPyPath} && python3 Crypto.py AESGCM_decrypt --key=${privateKey} --nonce=${nonce} --chiper=${encryptedString}`,
execSyncOptions,
);

const de = decrypted.toString().split("\n");
const message = de[0].substring(
de[0].indexOf("message: ") + "message: ".length,
);
return decrypted;

return { message };
} catch (e) {
console.error(e);
throw new Error("Error decrypting string");
}
}

/**
* The crypto-py library should be manually copy-pasted (or git cloned) inside the lib folder.
* Linux and MacOS users can in alternative create a symbolic link.
* After that, activate the python venv in the crypto-py folder, e.g. for linux: source venv/bin/activate
*
* @returns {string} - the absolute path of the crypto-py library
*/
function getCryptoPyPath() {
const cryptoRelativePath = path.join("lib", "crypto-py");
return path.resolve(cryptoRelativePath);
}
8 changes: 8 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type Encrypted = {
chiper: string;
nonce: string;
};

export type Decrypted = {
message: string;
};
Loading

0 comments on commit aec0591

Please sign in to comment.