├── circom ├── test │ ├── decrypt_test.circom │ └── rerandomize_test.circom ├── decrypt.circom └── rerandomize.circom ├── tsconfig.json ├── package.json ├── jest.config.js ├── ts ├── __tests__ │ ├── encoding.test.ts │ ├── encryption.test.ts │ └── circuits.test.ts └── index.ts ├── .gitignore └── README.md /circom/test/decrypt_test.circom: -------------------------------------------------------------------------------- 1 | include "../decrypt.circom"; 2 | 3 | component main = ElGamalDecrypt(); 4 | -------------------------------------------------------------------------------- /circom/test/rerandomize_test.circom: -------------------------------------------------------------------------------- 1 | include "../rerandomize.circom"; 2 | 3 | component main = ElGamalReRandomize(); 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "alwaysStrict": true, 5 | "allowJs": true, 6 | "noImplicitAny": false, 7 | "forceConsistentCasingInFileNames": true, 8 | "noUnusedLocals": false, 9 | "noUnusedParameters": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "sourceMap": true, 13 | "strict": true, 14 | "outDir": "./build", 15 | "lib": [ "es2015", "dom" ] 16 | }, 17 | "exclude": [ 18 | "node_modules/**" 19 | ], 20 | "include": [ 21 | "./ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elgamal-babyjub", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "watch": "tsc --watch", 8 | "build": "tsc", 9 | "test": "jest", 10 | "test-encryption": "jest encryption.test.ts", 11 | "test-encoding": "jest encoding.test.ts", 12 | "test-circuits": "jest circuits.test.ts" 13 | }, 14 | "author": "Koh Wei Jie", 15 | "license": "MIT", 16 | "dependencies": { 17 | "@types/jest": "^26.0.14", 18 | "@types/node": "^14.11.2", 19 | "assert": "^2.0.0", 20 | "circomlib": "0.2.4", 21 | "jest": "^26.4.2", 22 | "maci-crypto": "0.3.2", 23 | "ts-jest": "^26.4.1", 24 | "typescript": "^4.0.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | transform: { 4 | "^.+\\.tsx?$": 'ts-jest' 5 | }, 6 | testPathIgnorePatterns: [ 7 | "/build/", 8 | "/node_modules/", 9 | ], 10 | testRegex: '/__tests__/.*\\.test\\.ts$', 11 | moduleFileExtensions: [ 12 | 'ts', 13 | 'tsx', 14 | 'js', 15 | 'jsx', 16 | 'json', 17 | 'node' 18 | ], 19 | moduleNameMapper: { 20 | "^@libkzg(.*)$": "./$1", 21 | }, 22 | globals: { 23 | 'ts-jest': { 24 | diagnostics: { 25 | // Do not fail on TS compilation errors 26 | // https://kulshekhar.github.io/ts-jest/user/config/diagnostics#do-not-fail-on-first-error 27 | warnOnly: true 28 | } 29 | } 30 | }, 31 | testEnvironment: 'node' 32 | } 33 | -------------------------------------------------------------------------------- /ts/__tests__/encoding.test.ts: -------------------------------------------------------------------------------- 1 | import { encodeToMessage, decodeMessage, Message } from '../' 2 | import { babyJub } from 'circomlib' 3 | import { genRandomSalt } from 'maci-crypto' 4 | 5 | describe('Elliptic curve message encoding and decoding', () => { 6 | const plaintext = genRandomSalt() 7 | const encoded = encodeToMessage(plaintext) 8 | 9 | it('Should convert a value to a valid BabyJub point', () => { 10 | const point = [encoded.point.x, encoded.point.y] 11 | const valid = babyJub.inCurve(point) 12 | expect(valid).toBeTruthy() 13 | 14 | const packedPoint = babyJub.packPoint(point) 15 | const unpackedPoint = babyJub.unpackPoint(packedPoint) 16 | }) 17 | 18 | it('Should convert the BabyJub point back to the same value', () => { 19 | const decoded = decodeMessage(encoded) 20 | expect(decoded.toString()).toEqual(plaintext.toString()) 21 | }) 22 | 23 | it('Stress test', () => { 24 | const MAX = 10 25 | expect.assertions(MAX) 26 | for (let i = 0; i < MAX; i ++) { 27 | const plaintext = genRandomSalt() 28 | const encoded = encodeToMessage(plaintext) 29 | const decoded = decodeMessage(encoded) 30 | expect(decoded.toString()).toEqual(plaintext.toString()) 31 | } 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /circom/decrypt.circom: -------------------------------------------------------------------------------- 1 | include "../node_modules/circomlib/circuits/escalarmulany.circom"; 2 | include "../node_modules/circomlib/circuits/babyjub.circom"; 3 | include "../node_modules/circomlib/circuits/bitify.circom"; 4 | 5 | /* 6 | * Decrypts an ElGamal ciphertext. 7 | * The plaintext is the x-value of the decrypted point minus xIncrement. 8 | * The comments and signal names follow the symbols used here: 9 | * https://ethresear.ch/t/maci-anonymization-using-rerandomizable-encryption/7054 10 | * 11 | * c1, c2: The ciphertext 12 | * xIncrement: Deduct this from the decrypted point's x-value to obtain the 13 | * plaintext 14 | * privKey: The private key 15 | * out: The plaintext 16 | * 17 | * m = ((c1 ** x) ** - 1) * c2 18 | * out = m.x - xIncrement 19 | */ 20 | template ElGamalDecrypt() { 21 | signal input c1[2]; 22 | signal input c2[2]; 23 | signal input xIncrement; 24 | signal private input privKey; 25 | signal output out; 26 | 27 | // Convert the private key to bits 28 | component privKeyBits = Num2Bits(253); 29 | privKeyBits.in <== privKey; 30 | 31 | // c1 ** x 32 | component c1x = EscalarMulAny(253); 33 | for (var i = 0; i < 253; i ++) { 34 | c1x.e[i] <== privKeyBits.out[i]; 35 | } 36 | c1x.p[0] <== c1[0]; 37 | c1x.p[1] <== c1[1]; 38 | 39 | // (c1 ** x) ** -1 40 | signal c1xInverseX; 41 | c1xInverseX <== 0 - c1x.out[0]; 42 | 43 | // ((c1 ** x) ** - 1) * c2 44 | component decryptedPoint = BabyAdd(); 45 | decryptedPoint.x1 <== c1xInverseX; 46 | decryptedPoint.y1 <== c1x.out[1]; 47 | decryptedPoint.x2 <== c2[0]; 48 | decryptedPoint.y2 <== c2[1]; 49 | 50 | out <== decryptedPoint.xout - xIncrement; 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/build/ 2 | node_modules 3 | 4 | .DS_Store 5 | 6 | .vscode/ 7 | 8 | _build/ 9 | 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | 18 | # Diagnostic reports (https://nodejs.org/api/report.html) 19 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 20 | 21 | # Runtime data 22 | pids 23 | *.pid 24 | *.seed 25 | *.pid.lock 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | lib-cov 29 | 30 | # Coverage directory used by tools like istanbul 31 | coverage 32 | *.lcov 33 | 34 | # nyc test coverage 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | .grunt 39 | 40 | # Bower dependency directory (https://bower.io/) 41 | bower_components 42 | 43 | # node-waf configuration 44 | .lock-wscript 45 | 46 | # Compiled binary addons (https://nodejs.org/api/addons.html) 47 | build/Release 48 | 49 | # Dependency directories 50 | jspm_packages/ 51 | 52 | # TypeScript v1 declaration files 53 | typings/ 54 | 55 | # TypeScript cache 56 | *.tsbuildinfo 57 | 58 | # Optional npm cache directory 59 | .npm 60 | 61 | # Optional eslint cache 62 | .eslintcache 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # next.js build output 81 | .next 82 | 83 | # nuxt.js build output 84 | .nuxt 85 | 86 | # vuepress build output 87 | .vuepress/dist 88 | 89 | # Serverless directories 90 | .serverless/ 91 | 92 | # FuseBox cache 93 | .fusebox/ 94 | 95 | # DynamoDB Local files 96 | .dynamodb/ 97 | -------------------------------------------------------------------------------- /ts/__tests__/encryption.test.ts: -------------------------------------------------------------------------------- 1 | import { encrypt, decrypt, rerandomize, ElGamalCiphertext } from '../' 2 | import { genPrivKey, genPubKey, genRandomSalt } from 'maci-crypto' 3 | 4 | describe('ElGamal encryption and decryption', () => { 5 | const plaintext = genRandomSalt() 6 | const privKey = genPrivKey() 7 | const pubKey = genPubKey(privKey) 8 | 9 | let ciphertext: ElGamalCiphertext 10 | 11 | it('Should encrypt and decrypt a plaintext', () => { 12 | ciphertext = encrypt(plaintext, pubKey) 13 | const decrypted = decrypt(privKey, ciphertext) 14 | if (decrypted.toString() !== plaintext.toString()) { 15 | debugger 16 | } 17 | expect(decrypted.toString()).toEqual(plaintext.toString()) 18 | }) 19 | 20 | it('Should decrypt to the same value after rerandomization', () => { 21 | const rerandomized = rerandomize(pubKey, ciphertext) 22 | 23 | expect(rerandomized.xIncrement.toString()).toEqual(ciphertext.xIncrement.toString()) 24 | expect(rerandomized.c1.x.toString()).not.toEqual(ciphertext.c1.x.toString()) 25 | expect(rerandomized.c1.y.toString()).not.toEqual(ciphertext.c1.y.toString()) 26 | expect(rerandomized.c2.x.toString()).not.toEqual(ciphertext.c2.x.toString()) 27 | expect(rerandomized.c2.x.toString()).not.toEqual(ciphertext.c2.x.toString()) 28 | 29 | const decrypted = decrypt(privKey, rerandomized) 30 | expect(decrypted.toString()).toEqual(plaintext.toString()) 31 | }) 32 | 33 | it('Stress test', () => { 34 | const MAX = 10 35 | expect.assertions(MAX) 36 | for (let i = 0; i < MAX; i ++) { 37 | const plaintext = genRandomSalt() 38 | const privKey = genPrivKey() 39 | const pubKey = genPubKey(privKey) 40 | const ciphertext = encrypt(plaintext, pubKey) 41 | const decrypted = decrypt(privKey, ciphertext) 42 | expect(decrypted.toString()).toEqual(plaintext.toString()) 43 | } 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /circom/rerandomize.circom: -------------------------------------------------------------------------------- 1 | include "../node_modules/circomlib/circuits/escalarmulany.circom"; 2 | include "../node_modules/circomlib/circuits/babyjub.circom"; 3 | include "../node_modules/circomlib/circuits/bitify.circom"; 4 | 5 | /* 6 | * Performs rerandomization on an ElGamal ciphertext. 7 | * The comments and signal names follow the symbols used here: 8 | * https://ethresear.ch/t/maci-anonymization-using-rerandomizable-encryption/7054 9 | * 10 | * c1, c2: The existing ciphertext 11 | * d1, d2: The rerandomized ciphertext 12 | * z: The random value (randomVal) 13 | * pubKey: The public key under which the existing ciphertext was encrypted 14 | * g: A generator 15 | * 16 | * d1 = (g ** z) * c1 17 | * d2 = (pk ** z) * c2 18 | */ 19 | template ElGamalReRandomize() { 20 | signal input c1[2]; 21 | signal input c2[2]; 22 | signal input randomVal; 23 | signal input pubKey[2]; 24 | signal output d1[2]; 25 | signal output d2[2]; 26 | 27 | // Convert randomVal to bits 28 | component randomValBits = Num2Bits(253); 29 | randomValBits.in <== randomVal; 30 | 31 | // g ** z 32 | var BASE8[2] = [ 33 | 5299619240641551281634865583518297030282874472190772894086521144482721001553, 34 | 16950150798460657717958625567821834550301663161624707787222815936182638968203 35 | ]; 36 | component gz = EscalarMulFix(253, BASE8); 37 | for (var i = 0; i < 253; i ++) { 38 | gz.e[i] <== randomValBits.out[i]; 39 | } 40 | 41 | // (g ** z) * c1 42 | component d1Adder = BabyAdd(); 43 | d1Adder.x1 <== gz.out[0]; 44 | d1Adder.y1 <== gz.out[1]; 45 | d1Adder.x2 <== c1[0]; 46 | d1Adder.y2 <== c1[1]; 47 | 48 | // pubKey ** z 49 | component pubKeyZ = EscalarMulAny(253); 50 | for (var i = 0; i < 253; i ++) { 51 | pubKeyZ.e[i] <== randomValBits.out[i]; 52 | } 53 | pubKeyZ.p[0] <== pubKey[0]; 54 | pubKeyZ.p[1] <== pubKey[1]; 55 | 56 | // (pubKey ** z) * c2 57 | component d2Adder = BabyAdd(); 58 | d2Adder.x1 <== pubKeyZ.out[0]; 59 | d2Adder.y1 <== pubKeyZ.out[1]; 60 | d2Adder.x2 <== c2[0]; 61 | d2Adder.y2 <== c2[1]; 62 | 63 | // Output the rerandomized ciphertext 64 | d1[0] <== d1Adder.xout; 65 | d1[1] <== d1Adder.yout; 66 | d2[0] <== d2Adder.xout; 67 | d2[1] <== d2Adder.yout; 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ElGamal Decryption and Re-randomization in Typescript and circom 2 | 3 | **Warning**: do not use this in production as it has not been audited. 4 | 5 | This Typescript library implements ElGamal encryption, decryption, and 6 | re-randomization on the BabyJub curve. It also provides 7 | [`circom`](https://github.com/iden3/circom) circuits for decryption and 8 | re-randomization. 9 | 10 | This was written for future implementation of [MACI 11 | anonymization](https://ethresear.ch/t/maci-anonymization-using-rerandomizable-encryption/7054). 12 | 13 | ## Getting started 14 | 15 | Clone this repository, install dependencies, and build the source code: 16 | 17 | ```bash 18 | git clone git@github.com:weijiekoh/elgamal-babyjub.git && 19 | cd elgamal-babyjub && 20 | npm i && 21 | npm run build 22 | ``` 23 | 24 | Run tests: 25 | 26 | ``` 27 | npm run test 28 | ``` 29 | 30 | ## Library functions 31 | 32 | ### `encodeToMessage` 33 | 34 | `encodeToMessage = (original: BigInt): Message` 35 | 36 | This function converts an arbitrary value within the BabyJub finite field into 37 | a BabyJub curve point and an `xIncrement` value. It generates a random curve 38 | point within the BabyJub subgroup and computes the difference between its 39 | x-value and the plaintext. 40 | 41 | ### `encrypt` 42 | 43 | `encrypt = (plaintext: BigInt, pubKey: PubKey, randomVal?: BigInt): ElGamalCiphertext` 44 | 45 | This function encrypts a single `BigInt` plaintext into a ciphertext. Only the 46 | owner of the private key associated with `pubKey` can decrypt it. 47 | 48 | ### `decrypt` 49 | 50 | `decrypt = (privKey: PrivKey, ciphertext: ElGamalCiphertext): BigInt` 51 | 52 | Decrypts a cipertext into the original `BigInt`. 53 | 54 | ### `rerandomize` 55 | 56 | `rerandomize = (pubKey: PubKey, ciphertext: ElGamalCiphertext, randomVal: BigInt = genRandomSalt()): ElGamalCiphertext` 57 | 58 | Re-randomizes a ciphertext such that its value changes but can be decrypted to the same plaintext. 59 | 60 | The `randomVal` should be specified if one wishes to use the 61 | `ElGamalReRandomize` circuit described below. 62 | 63 | ## Zero-knowledge circuits 64 | 65 | ### `ElGamalDecrypt` 66 | 67 | Input signals: 68 | 69 | - `c1[2]`: The x and y-coordinates of the `c1` value of the ciphertext 70 | - `c2[2]`: The x and y-coordinates of the `c2` value of the ciphertext 71 | - `xIncrement`: The x-increment value of the ciphertext 72 | - `privKey` (private): The private key 73 | 74 | Output signals: 75 | 76 | - `out`: The original value 77 | 78 | 79 | ### `ElGamalReRandomize` 80 | 81 | Input signals: 82 | 83 | - `c1[2]`: The x and y-coordinates of the `c1` value of the ciphertext 84 | - `c2[2]`: The x and y-coordinates of the `c2` value of the ciphertext 85 | - `randomVal`: A random value. It must be the same as the one passed to the 86 | above `rerandomize()` function for both the circuit and 87 | Typescript function to output the same rerandomized ciphertext. 88 | - `pubKey`: The public key originally used to encrypt the ciphertext 89 | 90 | Output signals: 91 | 92 | - `d1[2]`: The x and y-coordinates of the `d1` value of the rerandomized ciphertext 93 | - `d2[2]`: The x and y-coordinates of the `d2` value of the rerandomized ciphertext 94 | -------------------------------------------------------------------------------- /ts/__tests__/circuits.test.ts: -------------------------------------------------------------------------------- 1 | jest.setTimeout(90000) 2 | import { 3 | encodeToMessage, 4 | decodeMessage, 5 | Message, 6 | encrypt, 7 | decrypt, 8 | rerandomize, 9 | ElGamalCiphertext, 10 | compileAndLoadCircuit, 11 | executeCircuit, 12 | getSignalByName, 13 | } from '../' 14 | 15 | import { 16 | genPrivKey, 17 | genPubKey, 18 | genRandomSalt, 19 | formatPrivKeyForBabyJub 20 | } from 'maci-crypto' 21 | 22 | import { babyJub } from 'circomlib' 23 | 24 | describe('snark circuits', () => { 25 | const plaintext = genRandomSalt() 26 | const encoded = encodeToMessage(plaintext) 27 | const privKey = genPrivKey() 28 | const pubKey = genPubKey(privKey) 29 | const ciphertext = encrypt(plaintext, pubKey) 30 | 31 | let decryptCircuit 32 | let rerandomizeCircuit 33 | 34 | beforeAll(async () => { 35 | rerandomizeCircuit = await compileAndLoadCircuit('test/rerandomize_test.circom') 36 | decryptCircuit = await compileAndLoadCircuit('test/decrypt_test.circom') 37 | }) 38 | 39 | it('should decrypt a ciphertext', async () => { 40 | const circuitInputs = { 41 | c1: [ciphertext.c1.x, ciphertext.c1.y], 42 | c2: [ciphertext.c2.x, ciphertext.c2.y], 43 | xIncrement: ciphertext.xIncrement, 44 | privKey: formatPrivKeyForBabyJub(privKey), 45 | } 46 | 47 | const witness = await executeCircuit(decryptCircuit, circuitInputs) 48 | const out = getSignalByName(decryptCircuit, witness, 'main.out').toString() 49 | expect(out.toString()).toEqual(plaintext.toString()) 50 | }) 51 | 52 | it('should rerandomize a ciphertext', async () => { 53 | const randomVal = formatPrivKeyForBabyJub(genRandomSalt()) 54 | const rerandomized = rerandomize(pubKey, ciphertext, randomVal) 55 | 56 | const circuitInputs = { 57 | c1: [ciphertext.c1.x, ciphertext.c1.y], 58 | c2: [ciphertext.c2.x, ciphertext.c2.y], 59 | randomVal, 60 | pubKey, 61 | } 62 | const witness = await executeCircuit(rerandomizeCircuit, circuitInputs) 63 | const d1X = getSignalByName(rerandomizeCircuit, witness, 'main.d1[0]').toString() 64 | const d1Y = getSignalByName(rerandomizeCircuit, witness, 'main.d1[1]').toString() 65 | const d2X = getSignalByName(rerandomizeCircuit, witness, 'main.d2[0]').toString() 66 | const d2Y = getSignalByName(rerandomizeCircuit, witness, 'main.d2[1]').toString() 67 | 68 | expect(d1X.toString()).toEqual(rerandomized.c1.x.toString()) 69 | expect(d1Y.toString()).toEqual(rerandomized.c1.y.toString()) 70 | expect(d2X.toString()).toEqual(rerandomized.c2.x.toString()) 71 | expect(d2Y.toString()).toEqual(rerandomized.c2.y.toString()) 72 | 73 | const circuitInputs2 = { 74 | c1: [rerandomized.c1.x, rerandomized.c1.y], 75 | c2: [rerandomized.c2.x, rerandomized.c2.y], 76 | xIncrement: ciphertext.xIncrement, 77 | privKey: formatPrivKeyForBabyJub(privKey), 78 | } 79 | 80 | const witness2 = await executeCircuit(decryptCircuit, circuitInputs2) 81 | const out = getSignalByName(decryptCircuit, witness2, 'main.out').toString() 82 | expect(out.toString()).toEqual(plaintext.toString()) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /ts/index.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import * as path from 'path' 3 | const circom = require('circom') 4 | import { babyJub } from 'circomlib' 5 | import { 6 | genPubKey, 7 | genPrivKey, 8 | formatPrivKeyForBabyJub, 9 | PubKey, 10 | PrivKey, 11 | genRandomSalt, 12 | } from 'maci-crypto' 13 | import { 14 | executeCircuit, 15 | getSignalByName, 16 | compileAndLoadCircuit, 17 | } from 'maci-circuits' 18 | 19 | const F = babyJub.F 20 | 21 | interface BabyJubPoint { 22 | x: BigInt, 23 | y: BigInt, 24 | } 25 | 26 | interface Message { 27 | point: BabyJubPoint, 28 | xIncrement: BigInt, 29 | } 30 | 31 | interface ElGamalCiphertext { 32 | c1: BabyJubPoint; 33 | c2: BabyJubPoint; 34 | xIncrement: BigInt; 35 | } 36 | 37 | /* 38 | * Converts an arbitrary BigInt, which must be less than the BabyJub field 39 | * size, into a Message. Each Message has a BabyJub curve point, and an 40 | * x-increment. 41 | * 42 | * @param original The value to encode. It must be less than the BabyJub field 43 | * size. 44 | */ 45 | const encodeToMessage = ( 46 | original: BigInt 47 | ): Message => { 48 | const randomVal = genPrivKey() 49 | const randomPoint = genPubKey(randomVal) 50 | 51 | assert(babyJub.inSubgroup(randomPoint)) 52 | 53 | const xIncrement = F.e(F.sub(randomPoint[0], original)) 54 | 55 | assert(xIncrement >= BigInt(0)) 56 | 57 | const xVal = randomPoint[0] 58 | const yVal = randomPoint[1] 59 | 60 | const point: BabyJubPoint = { x: xVal, y: yVal } 61 | 62 | return { point, xIncrement } 63 | } 64 | 65 | /* 66 | * Converts a Message into the original value. 67 | * The original value is the x-value of the BabyJub point minus the 68 | * x-increment. 69 | * @param message The message to convert. 70 | */ 71 | const decodeMessage = (message: Message): BigInt => { 72 | const decoded = BigInt( 73 | F.e( 74 | F.sub(message.point.x, message.xIncrement), 75 | ) 76 | ) 77 | assert(decoded >= BigInt(0)) 78 | assert(decoded < babyJub.p) 79 | 80 | return decoded 81 | } 82 | 83 | /* 84 | * Encrypts a plaintext such that only the owner of the specified public key 85 | * may decrypt it. 86 | * @param plaintext An arbitrary value which must be within the BabyJub field 87 | * @param pubKey The recepient's public key 88 | * @param randomVal A random value y used along with the private key to 89 | * generate the ciphertext 90 | */ 91 | const encrypt = ( 92 | plaintext: BigInt, 93 | pubKey: PubKey, 94 | randomVal: BigInt = genRandomSalt(), 95 | ): ElGamalCiphertext => { 96 | const message: Message = encodeToMessage(plaintext) 97 | 98 | const c1Point = babyJub.mulPointEscalar(babyJub.Base8, randomVal) 99 | 100 | const pky = babyJub.mulPointEscalar(pubKey, randomVal) 101 | const c2Point = babyJub.addPoint( 102 | [message.point.x, message.point.y], 103 | pky, 104 | ) 105 | 106 | return { 107 | c1: { x: c1Point[0], y: c1Point[1] }, 108 | c2: { x: c2Point[0], y: c2Point[1] }, 109 | xIncrement: message.xIncrement, 110 | } 111 | } 112 | 113 | /* 114 | * Decrypts a ciphertext using a private key. 115 | * @param privKey The private key 116 | * @param ciphertext The ciphertext to decrypt 117 | */ 118 | const decrypt = (privKey: PrivKey, ciphertext: ElGamalCiphertext): BigInt => { 119 | 120 | const c1x = babyJub.mulPointEscalar( 121 | [ciphertext.c1.x, ciphertext.c1.y], 122 | formatPrivKeyForBabyJub(privKey), 123 | ) 124 | 125 | const c1xInverse = [ 126 | F.e(c1x[0] * BigInt(-1)), 127 | BigInt(c1x[1]), 128 | ] 129 | 130 | const decrypted = babyJub.addPoint( 131 | c1xInverse, 132 | [ciphertext.c2.x, ciphertext.c2.y], 133 | ) 134 | 135 | return decodeMessage( 136 | { 137 | point: { 138 | x: decrypted[0], 139 | y: decrypted[1], 140 | }, 141 | xIncrement: ciphertext.xIncrement, 142 | } 143 | ) 144 | } 145 | 146 | /* 147 | * Randomize a ciphertext such that it is different from the original 148 | * ciphertext but can be decrypted by the same private key. 149 | * @param pubKey The same public key used to encrypt the original plaintext 150 | * @param ciphertext The ciphertext to re-randomize. 151 | * @param randomVal A random value z such that the re-randomized ciphertext 152 | * could have been generated a random value y+z in the first 153 | * place (optional) 154 | */ 155 | const rerandomize = ( 156 | pubKey: PubKey, 157 | ciphertext: ElGamalCiphertext, 158 | randomVal: BigInt = genRandomSalt(), 159 | ): ElGamalCiphertext => { 160 | const d1 = babyJub.addPoint( 161 | babyJub.mulPointEscalar(babyJub.Base8, randomVal), 162 | [ciphertext.c1.x, ciphertext.c1.y], 163 | ) 164 | 165 | const d2 = babyJub.addPoint( 166 | babyJub.mulPointEscalar(pubKey, randomVal), 167 | [ciphertext.c2.x, ciphertext.c2.y], 168 | ) 169 | 170 | return { 171 | c1: { x: d1[0], y: d1[1] }, 172 | c2: { x: d2[0], y: d2[1] }, 173 | xIncrement: ciphertext.xIncrement, 174 | } 175 | } 176 | 177 | /* 178 | * @param circuitPath The subpath to the circuit file (e.g. 179 | * test/batchProcessMessage_test.circom) 180 | */ 181 | const compileAndLoadCircuit = async ( 182 | circuitPath: string 183 | ) => { 184 | 185 | const circuit = await circom.tester( 186 | path.join( 187 | __dirname, 188 | `../circom/${circuitPath}`, 189 | ), 190 | ) 191 | 192 | await circuit.loadSymbols() 193 | 194 | return circuit 195 | } 196 | 197 | const executeCircuit = async ( 198 | circuit: any, 199 | inputs: any, 200 | ) => { 201 | 202 | const witness = await circuit.calculateWitness(inputs, true) 203 | await circuit.checkConstraints(witness) 204 | await circuit.loadSymbols() 205 | 206 | return witness 207 | } 208 | 209 | const getSignalByName = ( 210 | circuit: any, 211 | witness: any, 212 | signal: string, 213 | ) => { 214 | 215 | return witness[circuit.symbols[signal].varIdx] 216 | } 217 | 218 | export { 219 | Message, 220 | BabyJubPoint, 221 | ElGamalCiphertext, 222 | encodeToMessage, 223 | decodeMessage, 224 | encrypt, 225 | decrypt, 226 | rerandomize, 227 | compileAndLoadCircuit, 228 | executeCircuit, 229 | getSignalByName, 230 | } 231 | --------------------------------------------------------------------------------