├── .eslintignore ├── report.pdf ├── .gitignore ├── .prettierignore ├── src ├── models.ts ├── ec-elgamal │ ├── curve.ts │ ├── proofs │ │ ├── index.ts │ │ ├── models.ts │ │ ├── keyGeneration.ts │ │ ├── decryption.ts │ │ └── membership.ts │ ├── index.ts │ ├── models.ts │ ├── systemSetup.ts │ ├── curve-config.md │ ├── voting.ts │ ├── helper.ts │ └── encryption.ts ├── ff-elgamal │ ├── proofs │ │ ├── index.ts │ │ ├── models.ts │ │ ├── keyGeneration.ts │ │ ├── decryption.ts │ │ └── membership.ts │ ├── index.ts │ ├── voting.ts │ ├── helper.ts │ ├── models.ts │ ├── systemSetup.ts │ └── encryption.ts ├── index.ts └── helper.ts ├── .prettierrc ├── .lintstagedrc.json ├── .npmignore ├── .eslintrc.json ├── .github └── workflows │ ├── nodejs.yml │ └── publish.yml ├── LICENSE ├── addCurveToEllipticLibrary.sh ├── test ├── ff-elgamal │ ├── proofs │ │ ├── decryption.spec.ts │ │ ├── membership.spec.ts │ │ └── keyGeneration.spec.ts │ ├── systemSetup.spec.ts │ ├── voting.spec.ts │ ├── e2e.spec.ts │ ├── models.spec.ts │ ├── encryption.spec.ts │ └── helper.spec.ts ├── ec-elgamal │ ├── encryption.spec.ts │ ├── voting.spec.ts │ └── proofs │ │ ├── membership.spec.ts │ │ ├── decryption.spec.ts │ │ └── keyGeneration.spec.ts └── helper.spec.ts ├── package.json ├── tsconfig.json ├── curves.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meck93/evote-crypto/HEAD/report.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | node_modules/ 3 | lib/ 4 | .nyc_output 5 | coverage/ 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.nyc_output 2 | /.vscode 3 | /coverage 4 | /lib 5 | /node_modules 6 | -------------------------------------------------------------------------------- /src/models.ts: -------------------------------------------------------------------------------- 1 | export interface Summary { 2 | total: number 3 | yes: number 4 | no: number 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.+(ts)": ["eslint", "git add"], 3 | "*.+(js|ts|json|md)": ["prettier --write", "git add"] 4 | } 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | src 4 | test 5 | .eslintignore 6 | .eslintrc.json 7 | .lintstagedrc.json 8 | .prettierignore 9 | .prettierrc 10 | tsconfig.json 11 | .nyc_output 12 | .github -------------------------------------------------------------------------------- /src/ec-elgamal/curve.ts: -------------------------------------------------------------------------------- 1 | const EC = require('elliptic').ec 2 | const curve25519 = new EC('curve25519-weier') 3 | export const curveDefinition = curve25519 // only used internally 4 | export const curve = curveDefinition.curve // exported from crypto package 5 | -------------------------------------------------------------------------------- /src/ec-elgamal/proofs/index.ts: -------------------------------------------------------------------------------- 1 | import * as Decryption from './decryption' 2 | import * as KeyGeneration from './keyGeneration' 3 | import * as Membership from './membership' 4 | export { Decryption, KeyGeneration, Membership } 5 | 6 | import { DecryptionProof, KeyGenerationProof, MembershipProof } from './models' 7 | export { DecryptionProof, KeyGenerationProof, MembershipProof } 8 | -------------------------------------------------------------------------------- /src/ff-elgamal/proofs/index.ts: -------------------------------------------------------------------------------- 1 | import * as Decryption from './decryption' 2 | import * as KeyGeneration from './keyGeneration' 3 | import * as Membership from './membership' 4 | export { Decryption, KeyGeneration, Membership } 5 | 6 | import { DecryptionProof, KeyGenerationProof, MembershipProof } from './models' 7 | export { DecryptionProof, KeyGenerationProof, MembershipProof } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // global helpers and models 2 | import * as GlobalHelper from './helper' 3 | import { Summary } from './models' 4 | export { GlobalHelper, Summary } 5 | 6 | // finite field elgamal 7 | import * as FFelGamal from './ff-elgamal' 8 | export { FFelGamal } 9 | 10 | // elliptic curve elgamal 11 | import * as ECelGamal from './ec-elgamal' 12 | export { ECelGamal } 13 | -------------------------------------------------------------------------------- /src/ff-elgamal/proofs/models.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | 3 | export interface KeyGenerationProof { 4 | c: BN 5 | d: BN 6 | } 7 | 8 | export interface MembershipProof { 9 | a0: BN 10 | a1: BN 11 | b0: BN 12 | b1: BN 13 | c0: BN 14 | c1: BN 15 | f0: BN 16 | f1: BN 17 | } 18 | 19 | export interface DecryptionProof { 20 | a1: BN 21 | b1: BN 22 | f: BN 23 | d: BN 24 | } 25 | -------------------------------------------------------------------------------- /src/ec-elgamal/proofs/models.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | import { CurvePoint } from '../index' 3 | 4 | export interface KeyGenerationProof { 5 | c: BN 6 | d: BN 7 | } 8 | 9 | export interface MembershipProof { 10 | a0: CurvePoint 11 | a1: CurvePoint 12 | b0: CurvePoint 13 | b1: CurvePoint 14 | c0: BN 15 | c1: BN 16 | f0: BN 17 | f1: BN 18 | } 19 | 20 | export interface DecryptionProof { 21 | a1: CurvePoint 22 | b1: CurvePoint 23 | f: BN 24 | d: CurvePoint 25 | } 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint", "prettier"], 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:prettier/recommended", 8 | "prettier/@typescript-eslint" 9 | ], 10 | "env": { 11 | "jasmine": true, 12 | "mocha": true, 13 | "node": true 14 | }, 15 | "rules": { 16 | "@typescript-eslint/explicit-function-return-type": 2, 17 | "@typescript-eslint/no-var-requires": 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ff-elgamal/index.ts: -------------------------------------------------------------------------------- 1 | import * as Encryption from './encryption' 2 | import * as Helper from './helper' 3 | import * as Proof from './proofs' 4 | import * as SystemSetup from './systemSetup' 5 | import * as Voting from './voting' 6 | export { Encryption, Helper, Proof, SystemSetup, Voting } 7 | 8 | import { 9 | Cipher, 10 | KeyPair, 11 | SystemParameters, 12 | isCipher, 13 | isKeyPair, 14 | isSystemParameters, 15 | } from './models' 16 | export { Cipher, KeyPair, SystemParameters, isCipher, isKeyPair, isSystemParameters } 17 | -------------------------------------------------------------------------------- /src/ec-elgamal/index.ts: -------------------------------------------------------------------------------- 1 | import { curve as Curve } from './curve' 2 | export { Curve } 3 | 4 | import * as Encryption from './encryption' 5 | import * as Helper from './helper' 6 | import * as Proof from './proofs' 7 | import * as SystemSetup from './systemSetup' 8 | import * as Voting from './voting' 9 | export { Encryption, Helper, Proof, SystemSetup, Voting } 10 | 11 | import { Cipher, CurvePoint, KeyPair, SystemParameters, SystemParametersSerialized } from './models' 12 | export { Cipher, CurvePoint, KeyPair, SystemParameters, SystemParametersSerialized } 13 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [12.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - name: npm install, build, and test 22 | run: | 23 | npm ci 24 | npm run build 25 | npm run test:timeout 26 | env: 27 | CI: true 28 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Node.JS Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | - run: npm ci 16 | - run: npm test 17 | 18 | publish-gpr: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v1 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: 12 26 | registry-url: https://npm.pkg.github.com 27 | scope: '@meck93' 28 | - run: npm ci 29 | - run: npm publish 30 | env: 31 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /src/ff-elgamal/voting.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | import { GlobalHelper, Summary } from '../index' 3 | import { Cipher, Encryption, SystemParameters } from './index' 4 | 5 | export const generateYesVote = (sp: SystemParameters, pk: BN): Cipher => 6 | Encryption.encrypt(1, sp, pk) 7 | export const generateNoVote = (sp: SystemParameters, pk: BN): Cipher => 8 | Encryption.encrypt(0, sp, pk) 9 | export const generateBaseVote = (): Cipher => { 10 | return { a: GlobalHelper.newBN(1), b: GlobalHelper.newBN(1) } 11 | } // encrypt with m=0, r=0 12 | 13 | export const addVotes = (votes: Cipher[], sp: SystemParameters): Cipher => { 14 | return votes.reduce( 15 | (previous, current) => Encryption.add(previous, current, sp), 16 | generateBaseVote() 17 | ) 18 | } 19 | 20 | export const tallyVotes = (sp: SystemParameters, sk: BN, votes: Cipher[]): number => { 21 | return Encryption.decrypt1(addVotes(votes, sp), sk, sp).toNumber() 22 | } 23 | 24 | export const getSummary = (total: number, tallyResult: number): Summary => { 25 | const yes = tallyResult - 0 26 | const no = total - yes 27 | return { total, yes, no } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Moritz Eck, Alex Scheitlin, Nik Zaugg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/ec-elgamal/models.ts: -------------------------------------------------------------------------------- 1 | import { curve } from 'elliptic' 2 | import BN = require('bn.js') 3 | 4 | //eslint-disable-next-line @typescript-eslint/no-empty-interface 5 | export interface CurvePoint extends curve.short.ShortPoint {} 6 | 7 | export interface SystemParameters { 8 | p: BN // prime 9 | n: BN // prime factor: p = 2*n+1 10 | g: CurvePoint // generator 11 | } 12 | 13 | export interface SystemParametersSerialized { 14 | p: string 15 | n: string 16 | g: string 17 | } 18 | 19 | export interface KeyPair { 20 | h: CurvePoint 21 | sk: BN 22 | } 23 | 24 | export interface Cipher { 25 | a: CurvePoint 26 | b: CurvePoint 27 | r?: BN 28 | } 29 | 30 | // TODO: test me 31 | export const instanceOfSystemParametersSerialized = ( 32 | object: any 33 | ): object is SystemParametersSerialized => { 34 | /*const test = (field: string, type: string): boolean => { 35 | return field in object && typeof object[field] === type 36 | }*/ 37 | return ( 38 | 'p' in object && 39 | typeof object.p === 'string' && 40 | 'n' in object && 41 | typeof object.n === 'string' && 42 | 'g' in object && 43 | typeof object.g === 'string' 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /src/ec-elgamal/systemSetup.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | import { ec as EC } from 'elliptic' 3 | 4 | import { GlobalHelper } from '../index' 5 | import { Curve, CurvePoint, Helper, KeyPair, SystemParameters } from './index' 6 | import { curveDefinition } from './curve' 7 | 8 | export const generateSystemParameters = (): SystemParameters => { 9 | return { p: Curve.p, n: Curve.n, g: Curve.g } 10 | } 11 | 12 | export const generateKeyPair = (): KeyPair => { 13 | const keyPair: EC.KeyPair = curveDefinition.genKeyPair() 14 | const sk: BN = keyPair.getPrivate() 15 | const h: CurvePoint = keyPair.getPublic() as CurvePoint 16 | return { h, sk } 17 | } 18 | 19 | export const combinePublicKeys = (publicKeyShares: CurvePoint[]): CurvePoint => { 20 | return publicKeyShares.reduce((product, share) => Helper.ECmul(product, share)) 21 | } 22 | 23 | // combines multiple private key shares to one private key 24 | // NOTE: this should not be used as the distributed secret keys will become "useless" 25 | // it is only used for testing purpose 26 | export const combinePrivateKeys = (params: SystemParameters, privateKeyShares: BN[]): BN => { 27 | return privateKeyShares.reduce((sum, share) => GlobalHelper.addBN(sum, share, params.n)) 28 | } 29 | -------------------------------------------------------------------------------- /src/ec-elgamal/curve-config.md: -------------------------------------------------------------------------------- 1 | # curve25519 (in Weierstrass form) 2 | 3 | For more details about this curve, please see the `README` in the root folder. 4 | 5 | In any case, this curve can be used like any other curve in the elliptic package. See the examples below. 6 | 7 | ```javascript 8 | const curve = new EC('curve25519-weier') 9 | ``` 10 | 11 | We define the `curve` inside `src/ec-elgamal/curve.ts`. 12 | 13 | ```javascript 14 | // activeCurve.ts 15 | const EC = require('elliptic').ec 16 | const curve25519 = new EC('curve25519-weier') 17 | export const curveDefinition = curve25519 18 | export const curve = curveDefinition.curve 19 | ``` 20 | 21 | ## Curve Configuration 22 | 23 | ```javascript 24 | defineCurve('curve25519-weier', { 25 | type: 'short', 26 | prime: 'p25519', 27 | p: '7fffffffffffffff ffffffffffffffff ffffffffffffffff ffffffffffffffed', 28 | a: '2aaaaaaaaaaaaaaa aaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaa aaaaaa984914a144', 29 | b: '7b425ed097b425ed 097b425ed097b425 ed097b425ed097b4 260b5e9c7710c864', 30 | n: '1000000000000000 0000000000000000 14def9dea2f79cd6 5812631a5cf5d3ed', 31 | hash: hash.sha256, 32 | gRed: false, 33 | g: [ 34 | '2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad245a', 35 | '20ae19a1b8a086b4e01edd2c7748d14c923d4d7e6d7c61b229e9c5a27eced3d9', 36 | ], 37 | }) 38 | ``` 39 | -------------------------------------------------------------------------------- /addCurveToEllipticLibrary.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # this script is run after each npm install 4 | # please see package.json -> scripts -> postinstall 5 | 6 | # get relative paths, so that this also works when called from 7 | # outisde this directory 8 | 9 | readonly name=$(basename $0) 10 | readonly dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 11 | readonly parentDir="$(dirname "$dir")" 12 | readonly parentParentDir="$(dirname "$parentDir")" 13 | 14 | # replace curve file inside elliptic package 15 | # this adds the elliptic curve. curve25519 in weierstrass form to the elliptic library 16 | # since this pull request is not merged yet: https://github.com/indutny/elliptic/pull/113 17 | fileToCopy=$dir/curves.js 18 | 19 | # top level node_modules (e.g., node_modules/elliptic) 20 | topLevelDest=$parentParentDir/elliptic/lib/elliptic/curves.js 21 | 22 | # node_modules inside @meck93/evote-crypto -> bundled dependencies (e.g., node_modules/@meck93/evote-crypto/node_modules/elliptic) 23 | dependencyDest=$dir/node_modules/elliptic/lib/elliptic/curves.js 24 | 25 | # check if the top level destination exist -> if yes, copy the custom curve file 26 | if [ -f "$topLevelDest" ]; then 27 | echo "$topLevelDest exist." 28 | cp -f $fileToCopy $topLevelDest 29 | echo "$fileToCopy patched." 30 | fi 31 | 32 | # check if the top level destination exist -> if yes, copy the custom curve file 33 | if [ -f "$dependencyDest" ]; then 34 | echo "$dependencyDest exist." 35 | cp -f $fileToCopy $dependencyDest 36 | echo "$fileToCopy patched." 37 | fi 38 | -------------------------------------------------------------------------------- /test/ff-elgamal/proofs/decryption.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { GlobalHelper, FFelGamal } from '../../../src/index' 3 | 4 | describe('ElGamal Finite Field NIZKP for Decryption', () => { 5 | it('create and verify sum proof', () => { 6 | const uniqueID = '0xAd4E7D8f03904b175a1F8AE0D88154f329ac9329' 7 | 8 | // generate and verify 10 proofs (with different random variables and different random messages) 9 | const test = (p: number, g: number): void => { 10 | for (let i = 0; i < 10; i++) { 11 | const log = false 12 | let sp, pk, sk 13 | try { 14 | ;[sp, { h: pk, sk }] = FFelGamal.SystemSetup.generateSystemParametersAndKeys(p, g) 15 | log && console.log('p:', sp.p, 'q:', sp.q, 'g:', sp.g) 16 | } catch (error) { 17 | console.error(error) 18 | break 19 | } 20 | 21 | // sum 22 | const sum = GlobalHelper.getSecureRandomValue(sp.q) 23 | log && console.log(`Sum Proof for Message: ${sum}`) 24 | 25 | const sumEnc = FFelGamal.Encryption.encrypt(sum, sp, pk, log) 26 | const proof = FFelGamal.Proof.Decryption.generate(sumEnc, sp, sk, uniqueID) 27 | 28 | const verifiedSumProof = FFelGamal.Proof.Decryption.verify(sumEnc, proof, sp, pk, uniqueID) 29 | expect(verifiedSumProof).to.be.true 30 | 31 | const decSum = FFelGamal.Encryption.decrypt1(sumEnc, sk, sp, log) 32 | expect(decSum.eq(sum)).to.be.true 33 | } 34 | } 35 | 36 | // p = 23, q = 11 -> only generators that satisfy g^q mod p == 1 37 | test(23, 2) 38 | test(23, 6) 39 | test(23, 8) 40 | 41 | test(107, 3) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/ec-elgamal/encryption.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import { ECelGamal } from '../../src/index' 3 | 4 | // Fixed values for testing purposes 5 | // NO Vote: mapped to integer 3 6 | // YES Vote: mapped to integer 6 7 | const noVoteInt = 3 8 | const yesVoteInt = 6 9 | 10 | // Map/encode votes to points on the elliptic curve 11 | const noVoteOnCurve = ECelGamal.Curve.pointFromX(noVoteInt) 12 | const yesVoteOnCurve = ECelGamal.Curve.pointFromX(yesVoteInt) 13 | 14 | describe('Elliptic Curve ElGamal Encryption', () => { 15 | it('Points that encode the plaintexts should lie on the curve', () => { 16 | assert(ECelGamal.Curve.validate(yesVoteOnCurve) && ECelGamal.Curve.validate(noVoteOnCurve)) 17 | }) 18 | 19 | it('Decrypted value is the same as the original message', () => { 20 | const { h: publicKey, sk: privateKey } = ECelGamal.SystemSetup.generateKeyPair() 21 | 22 | const messageToEncrypt = noVoteOnCurve 23 | const cipherText = ECelGamal.Encryption.encrypt(noVoteOnCurve, publicKey) 24 | const decryptedCipherText = ECelGamal.Encryption.decrypt(cipherText, privateKey) 25 | 26 | assert(decryptedCipherText.eq(messageToEncrypt)) 27 | }) 28 | 29 | it('Two added ciphertexts should be the same as adding two plain texts', () => { 30 | const { h: publicKey, sk: privateKey } = ECelGamal.SystemSetup.generateKeyPair() 31 | 32 | const voteToEncrypt0 = noVoteOnCurve 33 | const voteToEncrypt1 = yesVoteOnCurve 34 | 35 | const cipher0 = ECelGamal.Encryption.encrypt(voteToEncrypt0, publicKey) 36 | const cipher1 = ECelGamal.Encryption.encrypt(voteToEncrypt1, publicKey) 37 | 38 | const cipherHomomorphicSum = ECelGamal.Encryption.homomorphicAdd(cipher0, cipher1) 39 | 40 | const decryptedHomomorphicSum = ECelGamal.Encryption.decrypt(cipherHomomorphicSum, privateKey) 41 | 42 | assert(decryptedHomomorphicSum.eq(noVoteOnCurve.add(yesVoteOnCurve))) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/ff-elgamal/helper.ts: -------------------------------------------------------------------------------- 1 | // check if a given number is prime 2 | export const isPrime = (num: number): boolean => { 3 | for (let i = 2; i <= Math.sqrt(num); i++) { 4 | if (Math.floor(num / i) == num / i) { 5 | return false 6 | } 7 | } 8 | return true 9 | } 10 | 11 | // get all primitive roots of a given number 12 | // only works for prime numbers 13 | // TODO: implement for non-prime numbers 14 | export const getPrimitiveRoots = (n: number): number[] => { 15 | if (!isPrime(n)) { 16 | return [] 17 | } 18 | 19 | // source: https://asecuritysite.com/encryption/pickg 20 | const g: number[] = [] 21 | for (let i = 1; i < n; i++) { 22 | let exp = 1 23 | let next = i % n 24 | 25 | while (next !== 1) { 26 | next = (next * i) % n 27 | exp += 1 28 | } 29 | 30 | if (exp === n - 1) { 31 | g.push(i) 32 | } 33 | } 34 | 35 | return g 36 | } 37 | 38 | // calculate q given p (for p < 2) 39 | // TODO: maybe check if p is actually prime 40 | export const getQofP = (p: number): number => (p > 1 ? (p - 1) / 2 : -1) 41 | 42 | // q is valid if it is prime 43 | export const isQValid = (q: number): boolean => (q > 1 ? isPrime(q) : false) 44 | 45 | // g is valid if: 46 | // - g != 1 47 | // - q != q 48 | // - g^q mod p == 1 49 | export const isGValid = (g: number, p: number): boolean => { 50 | return g !== 1 && g !== getQofP(p) && g ** getQofP(p) % p === 1 51 | } 52 | 53 | // get all primes that have a q = (p-1)/2 that is prime given a list of primes 54 | export const getPCandidates = (primes: number[]): number[] => 55 | primes.reduce((previous: number[], current: number) => { 56 | return isQValid(getQofP(current)) ? [...previous, current] : previous 57 | }, []) 58 | 59 | // get all generators g of q given a prime p 60 | export const getGCandidates = (p: number): number[] => 61 | getPrimitiveRoots(getQofP(p)).reduce((previous: number[], current: number) => { 62 | return isGValid(current, p) ? [...previous, current] : previous 63 | }, []) 64 | -------------------------------------------------------------------------------- /src/ec-elgamal/voting.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | 3 | import { Summary } from '../index' 4 | import { Cipher, Curve, CurvePoint, Encryption, Helper } from './index' 5 | 6 | export const yesVote = Curve.g 7 | export const noVote = Curve.point(null, null) 8 | 9 | export const generateYesVote = (pk: string | CurvePoint): Cipher => { 10 | return Encryption.encrypt(yesVote, Helper.deserializeCurvePoint(pk)) 11 | } 12 | 13 | export const generateNoVote = (pk: string | CurvePoint): Cipher => { 14 | return Encryption.encrypt(noVote, Helper.deserializeCurvePoint(pk)) 15 | } 16 | 17 | export const generateBaseVote = (pk: string | CurvePoint): Cipher => { 18 | return { a: Curve.g, b: Helper.deserializeCurvePoint(pk) } 19 | } // encrypt with m=noVote, r=1 20 | 21 | export const addVotes = (votes: Cipher[], pk: string | CurvePoint): Cipher => { 22 | return votes.reduce( 23 | (previous, current) => Encryption.homomorphicAdd(previous, current), 24 | generateBaseVote(pk) 25 | ) 26 | } 27 | 28 | export const findPoint = (point: CurvePoint): number => { 29 | let sumPoint = noVote 30 | let counter = 0 31 | 32 | while (!point.eq(sumPoint)) { 33 | sumPoint = sumPoint.add(yesVote) 34 | counter += 1 35 | } 36 | 37 | return counter 38 | } 39 | 40 | export const tallyVotes = (pk: string, sk: BN, votes: Cipher[]): number => { 41 | // This function is called in the fronend and did not work with 42 | // passing a CurvePoint directly before. It failed in 43 | // the encrypt function with 'red works only with red numbers'. 44 | 45 | // Fix: Serialize the key in the fronend and extract the public key from the passed hex-string 46 | const publicKey = Helper.deserializeCurvePoint(pk) 47 | 48 | const sum = Encryption.decrypt(addVotes(votes, publicKey), sk) 49 | return findPoint(sum) 50 | } 51 | 52 | export const getSummary = (total: number, tallyResult: number): Summary => { 53 | const yes = tallyResult - 0 54 | const no = total - yes 55 | return { total, yes, no } as Summary 56 | } 57 | -------------------------------------------------------------------------------- /test/ff-elgamal/systemSetup.spec.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | 3 | import { expect } from 'chai' 4 | import { GlobalHelper, FFelGamal } from '../../src/index' 5 | 6 | describe('Finite Field ElGamal System Setup', () => { 7 | it('should generate the system parameters', () => { 8 | const sp: FFelGamal.SystemParameters = FFelGamal.SystemSetup.generateSystemParameters(11, 3) 9 | const expectedSP: FFelGamal.SystemParameters = { 10 | p: new BN(11, 10), 11 | q: new BN(5, 10), 12 | g: new BN(3, 10), 13 | } 14 | 15 | expect(sp.p.eq(expectedSP.p)).to.be.true 16 | expect(sp.q.eq(expectedSP.q)).to.be.true 17 | expect(sp.g.eq(expectedSP.g)).to.be.true 18 | }) 19 | 20 | it('should generate a key pair', () => { 21 | const sp: FFelGamal.SystemParameters = FFelGamal.SystemSetup.generateSystemParameters(11, 3) 22 | 23 | for (let i = 0; i < 100; i++) { 24 | const kp: FFelGamal.KeyPair = FFelGamal.SystemSetup.generateKeyPair(sp) 25 | 26 | // sk: 1 <= sk < q 27 | const skLowerBound = new BN(1, 10) 28 | const skUpperBound = new BN(sp.q, 10).sub(new BN(1, 10)) 29 | expect(kp.sk.gte(skLowerBound)).to.be.true 30 | expect(kp.sk.lte(skUpperBound)).to.be.true 31 | 32 | // h == g^sk mod p 33 | expect(kp.h.eq(sp.g.pow(kp.sk).mod(sp.p))) 34 | } 35 | }) 36 | 37 | it('combine public keys', () => { 38 | const sp: FFelGamal.SystemParameters = FFelGamal.SystemSetup.generateSystemParameters(11, 3) 39 | 40 | let shares = [GlobalHelper.newBN(1)] 41 | let product = 1 42 | expect(FFelGamal.SystemSetup.combinePublicKeys(sp, shares).toNumber()).to.eql(product) 43 | 44 | shares = [GlobalHelper.newBN(4), GlobalHelper.newBN(2)] 45 | product = 8 46 | expect(FFelGamal.SystemSetup.combinePublicKeys(sp, shares).toNumber()).to.eql(product) 47 | 48 | shares = [GlobalHelper.newBN(2), GlobalHelper.newBN(3), GlobalHelper.newBN(4)] 49 | product = 2 50 | expect(FFelGamal.SystemSetup.combinePublicKeys(sp, shares).toNumber()).to.eql(product) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /src/ec-elgamal/proofs/keyGeneration.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | 3 | import { GlobalHelper } from '../../index' 4 | import { CurvePoint, Helper, KeyPair, SystemSetup, SystemParameters } from '../index' 5 | import { KeyGenerationProof } from './index' 6 | 7 | const web3 = require('web3') 8 | 9 | export const generateChallenge = (n: BN, uniqueID: string, h_: CurvePoint, b: CurvePoint): BN => { 10 | const pointsAsString = Helper.curvePointsToString([h_, b]) 11 | const hashString: string = web3.utils.soliditySha3(uniqueID, pointsAsString) 12 | let c: BN = web3.utils.toBN(hashString) 13 | c = c.mod(n) 14 | return c 15 | } 16 | 17 | // 1. generate a "second" key pair (a,b) 18 | // 2. compute challenge 19 | // 3. compute d = a + c*sk 20 | export const generate = ( 21 | params: SystemParameters, 22 | share: KeyPair, 23 | id: string 24 | ): KeyGenerationProof => { 25 | const { n } = params 26 | const { h, sk } = share 27 | 28 | const keyPair: KeyPair = SystemSetup.generateKeyPair() 29 | const a: BN = keyPair.sk 30 | const b: CurvePoint = keyPair.h 31 | 32 | const c: BN = generateChallenge(n, id, h, b) 33 | const d: BN = GlobalHelper.addBN(a, GlobalHelper.mulBN(c, sk, n), n) 34 | 35 | return { c: c, d: d } 36 | } 37 | 38 | // 1. recompute b = g^d / h^c 39 | // 2. recompute the challenge c 40 | // 3. verify that the challenge is correct 41 | // 4. verify that: g^d == b * h^c 42 | export const verify = ( 43 | params: SystemParameters, 44 | proof: KeyGenerationProof, 45 | h_: CurvePoint, 46 | id: string 47 | ): boolean => { 48 | const log = false 49 | const { n, g } = params 50 | const { c, d } = proof 51 | 52 | const b: CurvePoint = Helper.ECdiv(Helper.ECpow(g, d), Helper.ECpow(h_, c)) 53 | 54 | const c_: BN = generateChallenge(n, id, h_, b) 55 | const hashCheck: boolean = c.eq(c_) 56 | 57 | const gPowd: CurvePoint = Helper.ECpow(g, d) 58 | const bhPowC: CurvePoint = Helper.ECmul(b, Helper.ECpow(h_, c)) 59 | const dCheck: boolean = gPowd.eq(bhPowC) 60 | 61 | log && console.log('do the hashes match?\t', hashCheck) 62 | log && console.log('g^d == b * h_^c?\t', dCheck) 63 | log && console.log() 64 | 65 | return hashCheck && dCheck 66 | } 67 | -------------------------------------------------------------------------------- /src/ec-elgamal/helper.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | 3 | import { Curve, CurvePoint, SystemParameters, SystemParametersSerialized } from './index' 4 | import { instanceOfSystemParametersSerialized } from './models' 5 | 6 | export const ECpow = (a: CurvePoint, b: BN): CurvePoint => a.mul(b) as CurvePoint 7 | export const ECmul = (a: CurvePoint, b: CurvePoint): CurvePoint => a.add(b) as CurvePoint 8 | export const ECdiv = (a: CurvePoint, b: CurvePoint): CurvePoint => a.add(b.neg()) as CurvePoint 9 | 10 | export const curvePointToString = (point: CurvePoint): string => { 11 | const pointAsJSON = point.toJSON() 12 | const Px = (pointAsJSON[0] as BN).toString('hex') 13 | const Py = (pointAsJSON[1] as BN).toString('hex') 14 | return Px + Py 15 | } 16 | 17 | export const curvePointsToString = (points: CurvePoint[]): string => { 18 | let asString = '' 19 | for (const point of points) { 20 | asString += curvePointToString(point) 21 | } 22 | return asString 23 | } 24 | 25 | export const serializeBN = (bn: BN): string => { 26 | return bn.toString('hex') 27 | } 28 | 29 | export const deserializeBN = (bn: string): BN => { 30 | return new BN(bn, 'hex') 31 | } 32 | 33 | // https://github.com/indutny/elliptic/blob/71e4e8e2f5b8f0bdbfbe106c72cc9fbc746d3d60/test/curve-test.js#L265 34 | export const serializeCurvePoint = (point: CurvePoint): string => { 35 | return point.encode('hex', false) 36 | } 37 | 38 | export const deserializeCurvePoint = (point: CurvePoint | string): CurvePoint => { 39 | if (typeof point !== 'string') { 40 | return point 41 | } 42 | return Curve.decodePoint(point, 'hex') 43 | } 44 | 45 | export const serializeSystemParameters = (params: SystemParameters): SystemParametersSerialized => { 46 | return { 47 | p: serializeBN(params.p), 48 | n: serializeBN(params.n), 49 | g: serializeCurvePoint(params.g), 50 | } 51 | } 52 | 53 | export const deserializeParams = ( 54 | params: SystemParameters | SystemParametersSerialized 55 | ): SystemParameters => { 56 | if (!instanceOfSystemParametersSerialized(params)) { 57 | return params 58 | } 59 | return { 60 | p: deserializeBN(params.p), // BN 61 | n: deserializeBN(params.n), // BN 62 | g: deserializeCurvePoint(params.g), 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@meck93/evote-crypto", 3 | "version": "0.1.10", 4 | "description": "", 5 | "repository": { 6 | "type": "git", 7 | "url": "ssh://git@github.com:meck93/evote-crypto.git" 8 | }, 9 | "publishConfig": { 10 | "registry": "https://npm.pkg.github.com/" 11 | }, 12 | "main": "lib/index.js", 13 | "types": "lib/index.d.ts", 14 | "scripts": { 15 | "postinstall": "./addCurveToEllipticLibrary.sh", 16 | "prebuild": "rm -rf lib", 17 | "build": "tsc", 18 | "pre-commit": "lint-staged", 19 | "test": "nyc --reporter=text ts-mocha \"./test/**/*.spec.ts\"", 20 | "test:timeout": "nyc --reporter=text ts-mocha \"./test/**/*.spec.ts\" --timeout 100000", 21 | "test:watch": "ts-mocha \"./test/**/*.spec.ts\" -w --watch-extensions ts", 22 | "test:report": "nyc --reporter=html ts-mocha \"./test/**/*.spec.ts\"", 23 | "format": "prettier --write \"**/*.+(js|ts|json|md)\"", 24 | "lint": "eslint . --ext .ts", 25 | "lint:fix": "eslint . --ext .ts --fix", 26 | "ts-mocha": "./node_modules/.bin/ts-mocha", 27 | "ts-node": "./node_modules/.bin/ts-node", 28 | "tsc": "./node_modules/.bin/tsc", 29 | "preversion": "npm run build" 30 | }, 31 | "license": "MIT", 32 | "bundledDependencies": [ 33 | "elliptic" 34 | ], 35 | "dependencies": { 36 | "bn.js": "^5.1.2", 37 | "elliptic": "6.5.4", 38 | "hash.js": "^1.1.7", 39 | "random": "^2.2.0", 40 | "web3": "^1.2.8" 41 | }, 42 | "devDependencies": { 43 | "@types/chai": "^4.2.11", 44 | "@types/elliptic": "^6.4.12", 45 | "@types/mocha": "^7.0.2", 46 | "@typescript-eslint/eslint-plugin": "^3.1.0", 47 | "@typescript-eslint/parser": "^3.1.0", 48 | "chai": "^4.2.0", 49 | "eslint": "^7.2.0", 50 | "eslint-config-prettier": "^6.11.0", 51 | "eslint-import-resolver-typescript": "^2.0.0", 52 | "eslint-plugin-import": "^2.20.2", 53 | "eslint-plugin-json": "^2.1.1", 54 | "eslint-plugin-jsx-a11y": "^6.2.3", 55 | "eslint-plugin-prettier": "^3.1.3", 56 | "lint-staged": "^10.2.9", 57 | "mocha": "^7.2.0", 58 | "nyc": "^15.1.0", 59 | "prettier": "^2.0.5", 60 | "ts-mocha": "^7.0.0", 61 | "ts-node": "^8.10.2", 62 | "typescript": "^3.9.5" 63 | }, 64 | "nyc": { 65 | "extension": [ 66 | ".ts" 67 | ], 68 | "include": [ 69 | "src" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/ff-elgamal/voting.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { FFelGamal } from '../../src/index' 3 | 4 | describe('Finite Field ElGamal Voting', () => { 5 | it('vote', () => { 6 | const vote = (_result: number, _votes: number[]): void => { 7 | const [sp, { h: pk, sk }] = FFelGamal.SystemSetup.generateSystemParametersAndKeys(1319, 2) 8 | 9 | const log = false 10 | 11 | const votes: FFelGamal.Cipher[] = [] 12 | for (const vote of _votes) { 13 | vote === 1 && votes.push(FFelGamal.Voting.generateYesVote(sp, pk)) 14 | vote === 0 && votes.push(FFelGamal.Voting.generateNoVote(sp, pk)) 15 | } 16 | 17 | const result = FFelGamal.Voting.tallyVotes(sp, sk, votes) 18 | const summary = FFelGamal.Voting.getSummary(votes.length, result) 19 | log && 20 | console.log( 21 | _result, 22 | _votes, 23 | result, 24 | 'Total:', 25 | summary.total, 26 | '| Yes:', 27 | summary.yes, 28 | '| No:', 29 | summary.no 30 | ) 31 | 32 | expect(result).to.equal(_result) 33 | expect(summary.yes).to.equal(_votes.filter(v => v === 1).length) 34 | expect(summary.no).to.equal(_votes.filter(v => v === 0).length) 35 | } 36 | 37 | // voters: 0 38 | // results: 2^0 = 1 39 | vote(0, []) 40 | 41 | // voters: 1 42 | // results: 2^1 = 2 43 | vote(0, [0]) 44 | vote(1, [1]) 45 | 46 | // voters: 2 47 | // results: 2^2 = 4 48 | vote(0, [0, 0]) 49 | vote(1, [0, 1]) 50 | vote(1, [1, 0]) 51 | vote(2, [1, 1]) 52 | 53 | // voters: 3 54 | // results: 2^3 = 8 55 | vote(0, [0, 0, 0]) 56 | vote(1, [0, 0, 1]) 57 | vote(1, [0, 1, 0]) 58 | vote(2, [0, 1, 1]) 59 | vote(1, [1, 0, 0]) 60 | vote(2, [1, 0, 1]) 61 | vote(2, [1, 1, 0]) 62 | vote(3, [1, 1, 1]) 63 | 64 | // voters: 4 65 | // results: 2^4 = 16 66 | vote(0, [0, 0, 0, 0]) 67 | vote(1, [0, 0, 0, 1]) 68 | vote(1, [0, 0, 1, 0]) 69 | vote(2, [0, 0, 1, 1]) 70 | vote(1, [0, 1, 0, 0]) 71 | vote(2, [0, 1, 0, 1]) 72 | vote(2, [0, 1, 1, 0]) 73 | vote(3, [0, 1, 1, 1]) 74 | vote(1, [1, 0, 0, 0]) 75 | vote(2, [1, 0, 0, 1]) 76 | vote(2, [1, 0, 1, 0]) 77 | vote(3, [1, 0, 1, 1]) 78 | vote(2, [1, 1, 0, 0]) 79 | vote(3, [1, 1, 0, 1]) 80 | vote(3, [1, 1, 1, 0]) 81 | vote(4, [1, 1, 1, 1]) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /test/ec-elgamal/voting.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import { ECelGamal } from '../../src/index' 3 | 4 | describe('Elliptic Curve ElGamal Voting', () => { 5 | it('Voting works correctly in various scenarii', () => { 6 | const vote = (_result: number, _votes: number[]): void => { 7 | const log = false 8 | 9 | const { h, sk: privateKey } = ECelGamal.SystemSetup.generateKeyPair() 10 | const publicKey = ECelGamal.Helper.serializeCurvePoint(h) 11 | 12 | const votes: ECelGamal.Cipher[] = [] 13 | for (const vote of _votes) { 14 | vote === 1 && votes.push(ECelGamal.Voting.generateYesVote(publicKey)) 15 | vote === 0 && votes.push(ECelGamal.Voting.generateNoVote(publicKey)) 16 | } 17 | 18 | const result = ECelGamal.Voting.tallyVotes(publicKey, privateKey, votes) 19 | const summary = ECelGamal.Voting.getSummary(votes.length, result) 20 | log && 21 | console.log( 22 | _result, 23 | _votes, 24 | result, 25 | 'Total:', 26 | summary.total, 27 | '| Yes:', 28 | summary.yes, 29 | '| No:', 30 | summary.no 31 | ) 32 | 33 | assert(result === _result) 34 | assert(summary.yes === _votes.filter(v => v === 1).length) 35 | assert(summary.no === _votes.filter(v => v === 0).length) 36 | } 37 | 38 | // voters: 0 39 | // results: 2^0 = 1 40 | vote(0, []) 41 | 42 | // voters: 1 43 | // results: 2^1 = 2 44 | vote(0, [0]) 45 | vote(1, [1]) 46 | 47 | // voters: 2 48 | // results: 2^2 = 4 49 | vote(0, [0, 0]) 50 | vote(1, [0, 1]) 51 | vote(1, [1, 0]) 52 | vote(2, [1, 1]) 53 | 54 | // voters: 3 55 | // results: 2^3 = 8 56 | vote(0, [0, 0, 0]) 57 | vote(1, [0, 0, 1]) 58 | vote(1, [0, 1, 0]) 59 | vote(2, [0, 1, 1]) 60 | vote(1, [1, 0, 0]) 61 | vote(2, [1, 0, 1]) 62 | vote(2, [1, 1, 0]) 63 | vote(3, [1, 1, 1]) 64 | 65 | // voters: 4 66 | // results: 2^4 = 16 67 | vote(0, [0, 0, 0, 0]) 68 | vote(1, [0, 0, 0, 1]) 69 | vote(1, [0, 0, 1, 0]) 70 | vote(2, [0, 0, 1, 1]) 71 | vote(1, [0, 1, 0, 0]) 72 | vote(2, [0, 1, 0, 1]) 73 | vote(2, [0, 1, 1, 0]) 74 | vote(3, [0, 1, 1, 1]) 75 | vote(1, [1, 0, 0, 0]) 76 | vote(2, [1, 0, 0, 1]) 77 | vote(2, [1, 0, 1, 0]) 78 | vote(3, [1, 0, 1, 1]) 79 | vote(2, [1, 1, 0, 0]) 80 | vote(3, [1, 1, 0, 1]) 81 | vote(3, [1, 1, 1, 0]) 82 | vote(4, [1, 1, 1, 1]) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /src/ec-elgamal/encryption.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | import { GlobalHelper } from '../index' 3 | import { Cipher, Curve, CurvePoint, Helper } from './index' 4 | 5 | // Elliptic Curve ElGamal Encryption 6 | // 7 | // given: 8 | // - g: generator 9 | // - h: public key (g^privateKey) 10 | // - m: message 11 | // 12 | // steps: 13 | // 1. pick random value r 14 | // 2. compute c1 = g^r (ec-multiplication) 15 | // 3. compute s = h^r (ec-multiplication) 16 | // 4. compute c2 = s*m 17 | export const encrypt = (message: CurvePoint, publicKey: CurvePoint, log = false): Cipher => { 18 | const r = GlobalHelper.getSecureRandomValue(Curve.n) 19 | 20 | const c1 = Curve.g.mul(r) as CurvePoint 21 | const s = publicKey.mul(r) 22 | const c2 = s.add(message) as CurvePoint 23 | 24 | log && console.log('Is c1 on the curve?\t', Curve.validate(c1)) 25 | log && console.log('Is point s on the curve?', Curve.validate(s)) 26 | log && console.log('Is c2 on curve?\t\t', Curve.validate(c2)) 27 | 28 | return { a: c1, b: c2, r: r } 29 | } 30 | 31 | // Elliptic Curve ElGamal Decryption 32 | // 33 | // given: 34 | // - g: generator 35 | // - x: private key 36 | // - c1,c2: cipher 37 | // 38 | // steps: 39 | // 1. compute s = c1^x (ec-multiplication) 40 | // 2. compute s^-1 = multiplicative inverse of s 41 | // 3. compute m = c2 * s^-1 (ec-addition) 42 | export const decrypt = (cipherText: Cipher, privateKey: BN, log = false): CurvePoint => { 43 | const { a: c1, b: c2 } = cipherText 44 | 45 | const s = c1.mul(privateKey) 46 | const sInverse = s.neg() 47 | const m = c2.add(sInverse) 48 | 49 | log && console.log('is s on the curve?', Curve.validate(s)) 50 | log && console.log('is s^-1 on the curve?', Curve.validate(sInverse)) 51 | log && console.log('is m on curve?', Curve.validate(m)) 52 | 53 | return m as CurvePoint 54 | } 55 | 56 | export const homomorphicAdd = (cipher0: Cipher, cipher1: Cipher): Cipher => { 57 | return { 58 | a: cipher0.a.add(cipher1.a) as CurvePoint, 59 | b: cipher0.b.add(cipher1.b) as CurvePoint, 60 | } 61 | } 62 | 63 | // decrypt a cipher text with a private key share 64 | export const decryptShare = (cipher: Cipher, secretKeyShare: BN): CurvePoint => { 65 | return Helper.ECpow(cipher.a, secretKeyShare) 66 | } 67 | 68 | // combine decrypted shares 69 | export const combineDecryptedShares = ( 70 | cipher: Cipher, 71 | decryptedShares: CurvePoint[] 72 | ): CurvePoint => { 73 | const mh = Helper.ECdiv( 74 | cipher.b, 75 | decryptedShares.reduce((product, share) => Helper.ECmul(product, share)) 76 | ) 77 | return mh 78 | } 79 | -------------------------------------------------------------------------------- /test/ff-elgamal/proofs/membership.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { FFelGamal } from '../../../src/index' 3 | 4 | describe('ElGamal Finite Field NIZKP for Plaintext Membership', () => { 5 | it('create and verify proof', () => { 6 | const uniqueID = '0xAd4E7D8f03904b175a1F8AE0D88154f329ac9329' 7 | 8 | // generate and verify 10 proofs (with different random variables) 9 | const test = (p: number, g: number): void => { 10 | for (let i = 0; i < 10; i++) { 11 | const log = false 12 | log && console.log('p:', p, ', g:', g) 13 | let sp, pk 14 | try { 15 | ;[sp, { h: pk }] = FFelGamal.SystemSetup.generateSystemParametersAndKeys(p, g) 16 | } catch (error) { 17 | console.error(error) 18 | break 19 | } 20 | 21 | // yes vote 22 | log && console.log('yes proof') 23 | const yesVote = 1 24 | const yesEnc = FFelGamal.Encryption.encrypt(yesVote, sp, pk, log) 25 | const yesProof = FFelGamal.Proof.Membership.generateYesProof(yesEnc, sp, pk, uniqueID) 26 | 27 | const verifiedYesProof = FFelGamal.Proof.Membership.verify( 28 | yesEnc, 29 | yesProof, 30 | sp, 31 | pk, 32 | uniqueID 33 | ) 34 | expect(verifiedYesProof).to.be.true 35 | 36 | // no vote 37 | log && console.log('no proof') 38 | const noVote = 0 39 | const noEnc = FFelGamal.Encryption.encrypt(noVote, sp, pk, log) 40 | const noProof = FFelGamal.Proof.Membership.generateNoProof(noEnc, sp, pk, uniqueID) 41 | 42 | const verifiedNoProof = FFelGamal.Proof.Membership.verify(noEnc, noProof, sp, pk, uniqueID) 43 | expect(verifiedNoProof).to.be.true 44 | } 45 | } 46 | 47 | // 7 => 2 48 | test(7, 2) 49 | 50 | // 11 => 3 51 | test(11, 3) 52 | 53 | // 23 => 2, 6, 8 54 | test(23, 2) 55 | test(23, 6) 56 | test(23, 8) 57 | 58 | // TODO: adjust test cases below 59 | 60 | // 47 => 2, 3, 4, 6, 8, 9, 12, 16, 18, 24, 32, 36 61 | test(47, 2) 62 | test(47, 3) 63 | test(47, 4) 64 | test(47, 6) 65 | test(47, 8) 66 | test(47, 9) 67 | test(47, 12) 68 | test(47, 16) 69 | test(47, 18) 70 | test(47, 24) 71 | test(47, 32) 72 | test(47, 36) 73 | 74 | // 59 => 3, 4, 12, 16, 48 75 | test(59, 3) 76 | test(59, 4) 77 | test(59, 12) 78 | test(59, 16) 79 | test(59, 48) 80 | 81 | // 83 => 4, 16, 64 82 | test(83, 4) 83 | test(83, 16) 84 | test(83, 64) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /test/ec-elgamal/proofs/membership.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from 'chai' 2 | import { ECelGamal } from '../../../src/index' 3 | 4 | const log = false 5 | 6 | describe('Elliptic Curve ElGamal Vote ZKP', () => { 7 | it('Points that encode the plaintexts should lie on the curve', () => { 8 | assert(ECelGamal.Curve.validate(ECelGamal.Voting.yesVote)) 9 | assert(ECelGamal.Curve.validate(ECelGamal.Voting.noVote)) 10 | }) 11 | 12 | it('Should generate an elliptic curve valid YES vote proof', () => { 13 | const { h: publicKey } = ECelGamal.SystemSetup.generateKeyPair() 14 | 15 | const params: ECelGamal.SystemParameters = { 16 | p: ECelGamal.Curve.p, 17 | g: ECelGamal.Curve.g, 18 | n: ECelGamal.Curve.n, 19 | } 20 | const uniqueID = '0xAd4E7D8f03904b175a1F8AE0D88154f329ac9329' 21 | 22 | // encrypted yes vote + generate proof 23 | log && console.log('YES PROOF\n') 24 | const encryptedYesVote: ECelGamal.Cipher = ECelGamal.Encryption.encrypt( 25 | ECelGamal.Voting.yesVote, 26 | publicKey 27 | ) 28 | const yesProof: ECelGamal.Proof.MembershipProof = ECelGamal.Proof.Membership.generateYesProof( 29 | encryptedYesVote, 30 | params, 31 | publicKey, 32 | uniqueID 33 | ) 34 | 35 | // verify yes vote proof 36 | const verifiedYesProof: boolean = ECelGamal.Proof.Membership.verify( 37 | encryptedYesVote, 38 | yesProof, 39 | params, 40 | publicKey, 41 | uniqueID 42 | ) 43 | expect(verifiedYesProof).to.be.true 44 | }) 45 | 46 | it('Should generate an elliptic curve valid NO vote proof (FIXME)', () => { 47 | const { h: publicKey } = ECelGamal.SystemSetup.generateKeyPair() 48 | 49 | const params: ECelGamal.SystemParameters = { 50 | p: ECelGamal.Curve.p, 51 | g: ECelGamal.Curve.g, 52 | n: ECelGamal.Curve.n, 53 | } 54 | const uniqueID = '0xAd4E7D8f03904b175a1F8AE0D88154f329ac9329' 55 | 56 | // encrypt no vote + generate proof 57 | log && console.log('NO PROOF\n') 58 | const encryptedNoVote: ECelGamal.Cipher = ECelGamal.Encryption.encrypt( 59 | ECelGamal.Voting.noVote, 60 | publicKey 61 | ) 62 | const noProof = ECelGamal.Proof.Membership.generateNoProof( 63 | encryptedNoVote, 64 | params, 65 | publicKey, 66 | uniqueID 67 | ) 68 | 69 | // verify no vote proof 70 | const verifiedNoProof: boolean = ECelGamal.Proof.Membership.verify( 71 | encryptedNoVote, 72 | noProof, 73 | params, 74 | publicKey, 75 | uniqueID 76 | ) 77 | expect(verifiedNoProof).to.be.true 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /src/ff-elgamal/proofs/keyGeneration.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Key Generation Proof 3 | * 4 | * ElGamal Finite Field Non-Interactive Zero-Knowledge Proof for Key Generation 5 | * Using the Schnorr Proof 6 | * 7 | * Proof that one has knowledge of the private key x to the public key h=g^x. 8 | * Proof of knowledge of a discrete logarithm of x = log_g(g^x) 9 | * 10 | * - generate and verify proofs 11 | */ 12 | 13 | import BN = require('bn.js') 14 | import { GlobalHelper } from '../../index' 15 | import { KeyPair, SystemParameters, isKeyPair, isSystemParameters } from '../index' 16 | import { KeyGenerationProof } from './index' 17 | 18 | const web3 = require('web3') 19 | const log = false 20 | 21 | const generateChallenge = (q: BN, uniqueID: string, h_: BN, b: BN): BN => { 22 | let c = web3.utils.soliditySha3(uniqueID, h_, b) 23 | c = web3.utils.toBN(c) 24 | c = c.mod(q) 25 | return c 26 | } 27 | 28 | // 1. generate a "second" key pair (a,b) = (random value from Z_q, g^a mod p) 29 | // 2. compute challenge 30 | // 3. compute d = a + c*sk 31 | export const generate = ( 32 | params: SystemParameters, 33 | keyPair: KeyPair, // share 34 | id: string 35 | ): KeyGenerationProof => { 36 | isSystemParameters(params) 37 | isKeyPair(keyPair) 38 | 39 | const { p, q, g } = params 40 | const { h, sk } = keyPair 41 | 42 | const a: BN = GlobalHelper.getSecureRandomValue(q) 43 | const b: BN = GlobalHelper.powBN(g, a, p) // commitment 44 | 45 | const c: BN = generateChallenge(q, id, h, b) // challenge 46 | const d: BN = GlobalHelper.addBN(a, GlobalHelper.mulBN(c, sk, q), q) // response 47 | 48 | return { c, d } 49 | } 50 | 51 | // 1. recompute b = g^d/h^c 52 | // 2. recompute the challenge c 53 | // 3. verify that the challenge is correct 54 | // 4. verify that: g^d == b * h^c 55 | export const verify = ( 56 | params: SystemParameters, 57 | proof: KeyGenerationProof, 58 | h: BN, 59 | id: string 60 | ): boolean => { 61 | isSystemParameters(params) 62 | 63 | const { p, q, g } = params 64 | const { c, d } = proof 65 | 66 | const b: BN = GlobalHelper.divBN(GlobalHelper.powBN(g, d, p), GlobalHelper.powBN(h, c, p), p) 67 | 68 | const c_: BN = generateChallenge(q, id, h, b) 69 | const hashCheck: boolean = GlobalHelper.timingSafeEqualBN(c, c_) 70 | 71 | const gPowD: BN = GlobalHelper.powBN(g, d, p) 72 | const bhPowC: BN = GlobalHelper.mulBN(b, GlobalHelper.powBN(h, c, p), p) 73 | const dCheck: boolean = GlobalHelper.timingSafeEqualBN(gPowD, bhPowC) 74 | 75 | log && console.log('do the hashes match?\t', hashCheck) 76 | log && console.log('g^d == b * h^c?\t', dCheck) 77 | log && console.log() 78 | 79 | return hashCheck && dCheck 80 | } 81 | -------------------------------------------------------------------------------- /test/ec-elgamal/proofs/decryption.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from 'chai' 2 | import { ECelGamal } from '../../../src/index' 3 | 4 | describe('Elliptic Curve ElGamal Sum ZKP', () => { 5 | it('Points that encode the plaintexts should lie on the curve', () => { 6 | assert(ECelGamal.Curve.validate(ECelGamal.Voting.yesVote)) 7 | assert(ECelGamal.Curve.validate(ECelGamal.Voting.noVote)) 8 | }) 9 | 10 | it('Should generate a valid sum proof for a number of votes', () => { 11 | const log = false 12 | const { h: publicKey, sk: privateKey } = ECelGamal.SystemSetup.generateKeyPair() 13 | 14 | const params: ECelGamal.SystemParameters = { 15 | p: ECelGamal.Curve.p, 16 | g: ECelGamal.Curve.g, 17 | n: ECelGamal.Curve.n, 18 | } 19 | const uniqueID = '0xAd4E7D8f03904b175a1F8AE0D88154f329ac9329' 20 | 21 | const generateAndVerifySum = (_votes: number[], _result: number): void => { 22 | const votes: ECelGamal.Cipher[] = [] 23 | 24 | for (const vote of _votes) { 25 | vote === 1 && votes.push(ECelGamal.Encryption.encrypt(ECelGamal.Voting.yesVote, publicKey)) 26 | vote === 0 && votes.push(ECelGamal.Encryption.encrypt(ECelGamal.Voting.noVote, publicKey)) 27 | } 28 | 29 | // homomorphically add votes + generate sum proof 30 | const encryptedSum: ECelGamal.Cipher = ECelGamal.Voting.addVotes(votes, publicKey) 31 | const sumProof: ECelGamal.Proof.DecryptionProof = ECelGamal.Proof.Decryption.generate( 32 | encryptedSum, 33 | params, 34 | privateKey, 35 | uniqueID 36 | ) 37 | 38 | // verify proof 39 | const verifiedSumProof: boolean = ECelGamal.Proof.Decryption.verify( 40 | encryptedSum, 41 | sumProof, 42 | params, 43 | publicKey, 44 | uniqueID 45 | ) 46 | expect(verifiedSumProof).to.be.true 47 | 48 | // decrypt sum 49 | const decryptedSum: ECelGamal.CurvePoint = ECelGamal.Encryption.decrypt( 50 | encryptedSum, 51 | privateKey 52 | ) 53 | const result = ECelGamal.Voting.findPoint(decryptedSum) 54 | 55 | const summary = ECelGamal.Voting.getSummary(votes.length, result) 56 | log && 57 | console.log( 58 | _result, 59 | _votes, 60 | result, 61 | 'Total:', 62 | summary.total, 63 | '| Yes:', 64 | summary.yes, 65 | '| No:', 66 | summary.no 67 | ) 68 | 69 | expect(result).to.equal(_result) 70 | expect(summary.yes).to.equal(_votes.filter(v => v === 1).length) 71 | expect(summary.no).to.equal(_votes.filter(v => v === 0).length) 72 | } 73 | 74 | // Yes: 3, No: 2 -> Result: 3 75 | generateAndVerifySum([1, 1, 1, 0, 0], 3) 76 | 77 | // Yes: 8, No: 10 -> Result: 8 78 | generateAndVerifySum([0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], 8) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /src/ff-elgamal/models.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | 3 | export interface SystemParameters { 4 | p: BN // prime 5 | q: BN // prime factor: p = 2*q+1 6 | g: BN // generator 7 | } 8 | 9 | // we ignore the ts-rule: no-explicit-any since we want to be able to check any kind of input 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | export function isSystemParameters(object: any): object is SystemParameters { 12 | const check1: boolean = object.p !== undefined && object.p instanceof BN 13 | const check2: boolean = object.q !== undefined && object.q instanceof BN 14 | const check3: boolean = object.g !== undefined && object.g instanceof BN 15 | 16 | if (!(check1 && check2 && check3)) { 17 | throw new TypeError( 18 | `The provided input for type: SystemParameters is not of the required type. Given: ${JSON.stringify( 19 | object 20 | )}, Required: {p: BN, q: BN, g: BN}` 21 | ) 22 | } 23 | 24 | return check1 && check2 && check3 25 | } 26 | 27 | export interface KeyPair { 28 | h: BN 29 | sk: BN 30 | } 31 | 32 | // we ignore the ts-rule: no-explicit-any since we want to be able to check any kind of input 33 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 34 | export function isKeyPair(object: any): object is KeyPair { 35 | const check1: boolean = object.h !== undefined && object.h instanceof BN 36 | const check2: boolean = object.sk !== undefined && object.sk instanceof BN 37 | 38 | if (!(check1 && check2)) { 39 | throw new TypeError( 40 | `The provided input for type: KeyPair is not of the required type. Given: ${JSON.stringify( 41 | object 42 | )}, Required: {h: BN, sk: BN}` 43 | ) 44 | } 45 | 46 | return check1 && check2 47 | } 48 | 49 | export interface Cipher { 50 | a: BN 51 | b: BN 52 | r?: BN 53 | } 54 | 55 | // we ignore the ts-rule: no-explicit-any since we want to be able to check any kind of input 56 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 57 | export function isCipher(object: any): object is Cipher { 58 | const check1: boolean = object.a !== undefined && object.a instanceof BN 59 | const check2: boolean = object.b !== undefined && object.b instanceof BN 60 | const rPresent: boolean = object.r !== undefined 61 | // if r is not present -> use true as default value for check3 62 | // if r is present -> check if istanceof type BN 63 | const check3: boolean = rPresent ? object.r instanceof BN : true 64 | 65 | if (!(check1 && check2)) { 66 | throw new TypeError( 67 | `The provided input for type: Cipher is not of the required type. Given: ${JSON.stringify( 68 | object 69 | )}, Required: {a: BN, b: BN}` 70 | ) 71 | } else if (rPresent && !check3) { 72 | throw new TypeError( 73 | `The provided input for type: Cipher is not of the required type. Given: ${JSON.stringify( 74 | object 75 | )}, Required: {a: BN, b: BN, r?: BN}` 76 | ) 77 | } 78 | 79 | return check1 && check2 && check3 80 | } 81 | -------------------------------------------------------------------------------- /src/helper.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | import crypto = require('crypto') 3 | 4 | export const newBN = (n: number, base = 10): BN => new BN(n, base) 5 | export const invmBN = (a: BN, modulus: BN): BN => a.invm(modulus) 6 | export const addBN = (a: BN, b: BN, modulus: BN): BN => a.add(b).mod(modulus) 7 | export const subBN = (a: BN, b: BN, modulus: BN): BN => a.sub(b).mod(modulus) 8 | export const mulBN = (a: BN, b: BN, modulus: BN): BN => a.mul(b).mod(modulus) 9 | export const divBN = (a: BN, b: BN, modulus: BN): BN => mulBN(a, invmBN(b, modulus), modulus) 10 | export const powBN = (a: BN, b: BN, modulus: BN): BN => a.pow(b).mod(modulus) 11 | 12 | // compute the required number of bytes to store a decimal 13 | export const getByteSizeForDecimalNumber = (n: BN): BN => { 14 | const modulus: BN = n.mod(new BN(256, 10)) 15 | const smallerHalf: boolean = modulus.lt(new BN(128, 10)) 16 | const result: BN = n.divRound(new BN(256, 10)) 17 | return smallerHalf ? result.add(new BN(1, 10)) : result 18 | } 19 | 20 | // get a secure random value x: 0 < x < n 21 | export const getSecureRandomValue = (n: BN): BN => { 22 | const ONE: BN = new BN(1, 10) 23 | const UPPER_BOUND_RANDOM: BN = n.sub(ONE) 24 | const BYTE_SIZE: BN = getByteSizeForDecimalNumber(n) 25 | 26 | let byteSize: number 27 | try { 28 | byteSize = BYTE_SIZE.toNumber() 29 | } catch { 30 | // https://www.ecma-international.org/ecma-262/5.1/#sec-8.5 31 | // used for large numbers from EC 32 | byteSize = 32 33 | } 34 | 35 | let randomBytes: Buffer = crypto.randomBytes(byteSize) 36 | let randomValue: BN = new BN(randomBytes) 37 | 38 | // ensure that the random value is in range [1, n-1] 39 | while (!(randomValue.lte(UPPER_BOUND_RANDOM) && randomValue.gte(ONE))) { 40 | randomBytes = crypto.randomBytes(byteSize) 41 | randomValue = new BN(randomBytes) 42 | } 43 | return randomValue 44 | } 45 | 46 | export const timingSafeEqual = (a: Buffer, b: Buffer): boolean => { 47 | if (!Buffer.isBuffer(a)) { 48 | throw new TypeError('First argument must be a buffer') 49 | } 50 | if (!Buffer.isBuffer(b)) { 51 | throw new TypeError('Second argument must be a buffer') 52 | } 53 | 54 | // check if both buffers have the same length but don't leak any information about it 55 | // if buffers don't have the same length -> therefore, compare buffer a with itself and always return false 56 | // if buffers have the same length -> XOR every position in the Buffer 57 | let mismatch = a.length === b.length ? 0 : 1 58 | 59 | if (mismatch) { 60 | b = a 61 | } 62 | 63 | for (let i = 0, len = a.length; i < len; i++) { 64 | mismatch |= a[i] ^ b[i] 65 | } 66 | return mismatch === 0 67 | } 68 | 69 | export const timingSafeEqualBN = (a: BN, b: BN): boolean => { 70 | if (!BN.isBN(a)) { 71 | throw new TypeError('First argument must be of type: BN') 72 | } 73 | if (!BN.isBN(b)) { 74 | throw new TypeError('Second argument must be of type: BN') 75 | } 76 | const a_ = new Buffer(a.toArray()) 77 | const b_ = new Buffer(b.toArray()) 78 | return timingSafeEqual(a_, b_) 79 | } 80 | -------------------------------------------------------------------------------- /test/ff-elgamal/e2e.spec.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | import { expect } from 'chai' 3 | import { FFelGamal } from '../../src/index' 4 | 5 | const random = require('random') 6 | const web3 = require('web3') 7 | 8 | describe('ElGamal Finite Field E2E Test', () => { 9 | it('complete vote', () => { 10 | const vote = (p: number, g: number, _result: number, _votes: number[]): void => { 11 | const govID = 'GOV_ID_SUPER_SECURE_:-)' 12 | 13 | const log = false 14 | 15 | let sp: FFelGamal.SystemParameters 16 | let pk: BN 17 | let sk: BN 18 | 19 | try { 20 | ;[sp, { h: pk, sk }] = FFelGamal.SystemSetup.generateSystemParametersAndKeys(p, g) 21 | log && console.log('p:', sp.p, 'q:', sp.q, 'g:', sp.g, 'pk:', pk, 'sk:', sk) 22 | } catch (error) { 23 | console.error(error) 24 | } 25 | 26 | const votes: FFelGamal.Cipher[] = [] 27 | 28 | // generate votes and proofs with random IDs 29 | for (const vote of _votes) { 30 | const id = web3.utils.soliditySha3(random.int(1, Math.pow(2, 16))) 31 | 32 | if (vote === 1) { 33 | const encYesVote = FFelGamal.Voting.generateYesVote(sp, pk) 34 | votes.push(encYesVote) 35 | 36 | const encYesProof = FFelGamal.Proof.Membership.generateYesProof(encYesVote, sp, pk, id) 37 | 38 | const validVote = FFelGamal.Proof.Membership.verify(encYesVote, encYesProof, sp, pk, id) 39 | expect(validVote).to.be.true 40 | } else { 41 | const encNoVote = FFelGamal.Voting.generateNoVote(sp, pk) 42 | votes.push(encNoVote) 43 | 44 | const encNoProof = FFelGamal.Proof.Membership.generateNoProof(encNoVote, sp, pk, id) 45 | 46 | const validVote = FFelGamal.Proof.Membership.verify(encNoVote, encNoProof, sp, pk, id) 47 | expect(validVote).to.be.true 48 | } 49 | } 50 | 51 | // homomorphically add all votes and create sum proof 52 | const encryptedSum = FFelGamal.Voting.addVotes(votes, sp) 53 | const sumProof = FFelGamal.Proof.Decryption.generate(encryptedSum, sp, sk, govID) 54 | 55 | // verifiy the sum proof 56 | const validSum = FFelGamal.Proof.Decryption.verify(encryptedSum, sumProof, sp, pk, govID) 57 | expect(validSum).to.be.true 58 | 59 | // decrypt the sum 60 | const decryptedSum = FFelGamal.Encryption.decrypt1(encryptedSum, sk, sp) 61 | const summary = FFelGamal.Voting.getSummary(votes.length, decryptedSum.toNumber()) 62 | log && 63 | console.log( 64 | _result, 65 | _votes, 66 | 'Total:', 67 | summary.total, 68 | '| Yes:', 69 | summary.yes, 70 | '| No:', 71 | summary.no 72 | ) 73 | 74 | expect(decryptedSum.toNumber()).to.equal(_result) 75 | expect(summary.yes).to.equal(_votes.filter(v => v === 1).length) 76 | expect(summary.no).to.equal(_votes.filter(v => v === 0).length) 77 | } 78 | 79 | // voters: 3 80 | // result: 2 yes, 1 no 81 | // p = 23, q = 11, g = 2 82 | vote(23, 2, 2, [1, 1, 0]) 83 | 84 | // voters: 26 85 | // result: 17 yes, 9 no 86 | // p = 107, q = 53, g = 3 87 | vote(107, 3, 17, [1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1]) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /test/ff-elgamal/models.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import * as Models from '../../src/ff-elgamal/models' 3 | import BN from 'bn.js' 4 | 5 | describe('Model Typechecking Test', () => { 6 | it('SystemParameters Type Test', () => { 7 | // invalid systemsparameter object: incorrect variables, correct types 8 | const testObject0 = { a: new BN(23), b: new BN(11), c: new BN(3) } 9 | expect(() => Models.isSystemParameters(testObject0)).to.throw( 10 | TypeError, 11 | `The provided input for type: SystemParameters is not of the required type.` 12 | ) 13 | 14 | // invalid systemsparameter object: correct variables, incorrect types 15 | const testObject1 = { p: 23, q: 11, g: 3 } 16 | expect(() => Models.isSystemParameters(testObject1)).to.throw( 17 | TypeError, 18 | `The provided input for type: SystemParameters is not of the required type.` 19 | ) 20 | 21 | // valid creation of systems parameter object 22 | const testObject2 = { p: new BN(23), q: new BN(11), g: new BN(3) } 23 | const check2 = Models.isSystemParameters(testObject2) 24 | expect(check2).to.be.true 25 | }) 26 | 27 | it('KeyPair Type Test', () => { 28 | // invalid keypair object: correct variables, wrong type 29 | const testObject1 = { h: new BN(3), sk: 2 } 30 | expect(() => Models.isKeyPair(testObject1)).to.throw( 31 | TypeError, 32 | `The provided input for type: KeyPair is not of the required type.` 33 | ) 34 | 35 | // invalid keypair object: incorrect variables, correct type 36 | const testObject2 = { h: new BN(3), p: new BN(2) } 37 | expect(() => Models.isKeyPair(testObject2)).to.throw( 38 | TypeError, 39 | `The provided input for type: KeyPair is not of the required type.` 40 | ) 41 | 42 | // valid creation of keypair object 43 | const testObject3 = { h: new BN(3), sk: new BN(2) } 44 | const check3 = Models.isKeyPair(testObject3) 45 | expect(check3).to.be.true 46 | }) 47 | 48 | it('Cipher Type Test', () => { 49 | // invalid cipher object: correct variables, wrong type 50 | const testObject1 = { a: 3, b: 2 } 51 | expect(() => Models.isCipher(testObject1)).to.throw( 52 | TypeError, 53 | `The provided input for type: Cipher is not of the required type.` 54 | ) 55 | 56 | // invalid cipher object: incorrect variables, correct type 57 | const testObject2 = { h: new BN(3), p: new BN(2) } 58 | expect(() => Models.isCipher(testObject2)).to.throw( 59 | TypeError, 60 | `The provided input for type: Cipher is not of the required type.` 61 | ) 62 | 63 | // valid creation of cipher object 64 | const testObject3 = { a: new BN(3), b: new BN(2) } 65 | const check3 = Models.isCipher(testObject3) 66 | expect(check3).to.be.true 67 | 68 | // valid creation of cipher object: including r 69 | const testObject4 = { a: new BN(3), b: new BN(2), r: new BN(1) } 70 | const check4 = Models.isCipher(testObject4) 71 | expect(check4).to.be.true 72 | 73 | // invalid cipher object: a,b correct -> r wrong 74 | const testObject5 = { a: new BN(3), b: new BN(2), r: 1 } 75 | expect(() => Models.isCipher(testObject5)).to.throw( 76 | TypeError, 77 | `The provided input for type: Cipher is not of the required type.` 78 | ) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /src/ec-elgamal/proofs/decryption.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | 3 | import { GlobalHelper } from '../../index' 4 | import { 5 | Cipher, 6 | Curve, 7 | CurvePoint, 8 | Helper, 9 | SystemParameters, 10 | SystemParametersSerialized, 11 | } from '../index' 12 | import { DecryptionProof } from './models' 13 | import { curveDefinition } from '../curve' 14 | 15 | export const generateChallenge = ( 16 | n: BN, 17 | id: string, 18 | a: CurvePoint, 19 | b: CurvePoint, 20 | a1: CurvePoint, 21 | b1: CurvePoint 22 | ): BN => { 23 | const pointsAsString: string = Helper.curvePointsToString([a, b, a1, b1]) 24 | const input = id + pointsAsString 25 | 26 | let c = curveDefinition 27 | .hash() 28 | .update(input) 29 | .digest('hex') 30 | 31 | c = new BN(c, 'hex') 32 | c = c.mod(n) 33 | 34 | return c 35 | } 36 | 37 | // generate a proof for the decryption 38 | // steps: 39 | // 1. generate random value x 40 | // 2. compute (a1, b1) = (a^x, g^x) 41 | // 3. generate the challenge 42 | // 4. compute f = x + c * sk (NOTE: mod q!) 43 | // 5. compute the decryption factor d = a^r 44 | export const generate = ( 45 | cipher: Cipher, 46 | params: SystemParameters | SystemParametersSerialized, 47 | sk: BN, 48 | id: string, 49 | log = false 50 | ): DecryptionProof => { 51 | const { a, b } = cipher 52 | const { g, n } = Helper.deserializeParams(params) 53 | 54 | const x: BN = GlobalHelper.getSecureRandomValue(n) 55 | 56 | const a1 = Helper.ECpow(a, x) 57 | const b1 = Helper.ECpow(g, x) 58 | 59 | const c = generateChallenge(n, id, a, b, a1, b1) 60 | const f = GlobalHelper.addBN(x, GlobalHelper.mulBN(c, sk, n), n) 61 | const d = Helper.ECpow(a, sk) 62 | 63 | log && console.log('a1 is on the curve?\t', Curve.validate(a1)) 64 | log && console.log('b1 is on the curve?\t', Curve.validate(b1)) 65 | log && console.log('d is on the curve?\t', Curve.validate(d)) 66 | 67 | log && console.log('x\t\t\t', x) 68 | log && console.log('a1\t\t\t', a1) 69 | log && console.log('b1\t\t\t', b1) 70 | log && console.log('c\t\t\t', c) 71 | log && console.log('f = x + c*r\t\t', f) 72 | log && console.log() 73 | 74 | return { a1, b1, f, d } 75 | } 76 | 77 | // verify a proof for the decryption 78 | // 1. recompute the challenge 79 | // 2. verification a^f == a1 * d^c 80 | // 3. verification g^f == b1 * h^c 81 | export const verify = ( 82 | encryptedSum: Cipher, 83 | proof: DecryptionProof, 84 | params: SystemParameters | SystemParametersSerialized, 85 | pk: CurvePoint | string, 86 | id: string, 87 | log = false 88 | ): boolean => { 89 | const { a, b } = encryptedSum 90 | const { g, n } = Helper.deserializeParams(params) 91 | pk = Helper.deserializeCurvePoint(pk) 92 | const { a1, b1, f, d } = proof 93 | 94 | const c = generateChallenge(n, id, a, b, a1, b1) 95 | 96 | const l1 = Helper.ECpow(a, f) 97 | const r1 = Helper.ECmul(a1, Helper.ECpow(d, c)) 98 | const v1 = l1.eq(r1) 99 | 100 | const l2 = Helper.ECpow(g, f) 101 | const r2 = Helper.ECmul(b1, Helper.ECpow(pk, c)) 102 | const v2 = l2.eq(r2) 103 | 104 | log && console.log('a^f == a1*d^c:\t\t', v1) 105 | log && console.log('g^f == b1*h^c\t\t', v2) 106 | log && console.log() 107 | 108 | return v1 && v2 109 | } 110 | -------------------------------------------------------------------------------- /src/ff-elgamal/systemSetup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * System Setup 3 | * 4 | * - generate system parameters p,q,g 5 | * - generate key pairs h,sk 6 | * - combine public/private key shares 7 | */ 8 | 9 | import BN = require('bn.js') 10 | import { GlobalHelper } from '../index' 11 | import { Helper, KeyPair, SystemParameters, isSystemParameters } from './index' 12 | 13 | // generate system parameters p,q,g given p,g 14 | export const generateSystemParameters = (p: number, g: number): SystemParameters => { 15 | return { 16 | p: GlobalHelper.newBN(p), 17 | q: GlobalHelper.newBN(Helper.getQofP(p)), 18 | g: GlobalHelper.newBN(g), 19 | } 20 | } 21 | 22 | // randomly generate a key pair h,sk given the system parameters p,q,g 23 | export const generateKeyPair = (sp: SystemParameters): KeyPair => { 24 | isSystemParameters(sp) 25 | const sk = GlobalHelper.getSecureRandomValue(sp.q) // pick a random value in Z_q 26 | const h = GlobalHelper.powBN(sp.g, sk, sp.p) // compute public key h: g^sk mod p 27 | return { h, sk } 28 | } 29 | 30 | // generate system parameters p,q,g and a key pair h,sk given p,g 31 | export const generateSystemParametersAndKeys = ( 32 | p: number, 33 | g: number 34 | ): [SystemParameters, KeyPair] => { 35 | const sysParams = generateSystemParameters(p, g) 36 | const keyPair = generateKeyPair(sysParams) 37 | return [sysParams, keyPair] 38 | } 39 | 40 | // generate system parameters p,q,g and a key pair h,sk given p,g 41 | // these parameters can be used for zero-knowledge proofs 42 | export const generateSystemParametersAndKeysZKP = ( 43 | p: number, 44 | g: number 45 | ): [SystemParameters, KeyPair] => { 46 | const sysParams = generateSystemParameters(p, g) 47 | const keyPair = generateKeyPair(sysParams) 48 | 49 | // verify that g^q mod p == 1 (this means: gcd(q,p) == 1) 50 | const test1 = GlobalHelper.powBN(sysParams.g, sysParams.q, sysParams.p) 51 | if (!test1.eq(GlobalHelper.newBN(1))) { 52 | throw new Error( 53 | `g^q mod p != 1 (== ${test1.toNumber()}. for p: ${p}, q: ${sysParams.q.toNumber()} and g: ${g}` 54 | ) 55 | } 56 | 57 | // verify that h^q mod p == 1 (this means: gcd(h,p) == 1) 58 | const test2 = GlobalHelper.powBN(keyPair.h, sysParams.q, sysParams.p) 59 | if (!test2.eq(GlobalHelper.newBN(1))) { 60 | throw new Error( 61 | `h^q mod p != 1 (== ${test2.toNumber()}. for p: ${p}, q: ${sysParams.q.toNumber()} and g: ${g}` 62 | ) 63 | } 64 | 65 | // verify that the public key h is not 1 66 | const test3 = keyPair.h.mod(sysParams.p) 67 | if (test3.eq(GlobalHelper.newBN(1))) { 68 | throw new Error(`h mod p == 1. for p: ${p}, q: ${sysParams.q.toNumber()} and g: ${g}`) 69 | } 70 | 71 | return [sysParams, keyPair] 72 | } 73 | 74 | // combines multiple public key shares to one public key 75 | export const combinePublicKeys = (params: SystemParameters, publicKeyShares: BN[]): BN => { 76 | isSystemParameters(params) 77 | return publicKeyShares.reduce((product, share) => GlobalHelper.mulBN(product, share, params.p)) 78 | } 79 | 80 | // combines multiple private key shares to one private key 81 | // NOTE: this should not be used as the distributed secret keys will become "useless" 82 | // it is only used for testing purpose 83 | export const combinePrivateKeys = (params: SystemParameters, privateKeyShares: BN[]): BN => { 84 | isSystemParameters(params) 85 | return privateKeyShares.reduce((sum, share) => GlobalHelper.addBN(sum, share, params.q)) 86 | } 87 | -------------------------------------------------------------------------------- /src/ff-elgamal/proofs/decryption.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Decryption Proof 3 | * 4 | * ElGamal Finite Field Non-Interactive Zero-Knowledge Proof for Decryption 5 | * Using the Chaum-Pedersen Proof 6 | * 7 | * Proving that the decryption is done using the corresponding private key to the 8 | * public key used for the encryption. 9 | * 10 | * - generate and verify proofs 11 | */ 12 | 13 | import BN = require('bn.js') 14 | import { GlobalHelper } from '../../index' 15 | import { Cipher, SystemParameters, isCipher, isSystemParameters } from '../index' 16 | import { DecryptionProof } from './models' 17 | 18 | const web3 = require('web3') 19 | const printConsole = false 20 | 21 | // modulo operations 22 | const add = (a: BN, b: BN, sp: SystemParameters): BN => GlobalHelper.addBN(a, b, sp.q) 23 | const mul = (a: BN, b: BN, sp: SystemParameters): BN => GlobalHelper.mulBN(a, b, sp.p) 24 | const pow = (a: BN, b: BN, sp: SystemParameters): BN => GlobalHelper.powBN(a, b, sp.p) 25 | 26 | // TODO: check paper https://eprint.iacr.org/2016/771.pdf why we should not hash a and b 27 | const generateChallenge = (q: BN, uniqueID: string, a: BN, b: BN, a1: BN, b1: BN): BN => { 28 | let c = web3.utils.soliditySha3(uniqueID, a, b, a1, b1) 29 | c = web3.utils.toBN(c) 30 | c = c.mod(q) 31 | return c 32 | } 33 | 34 | // generate a proof for the decryption 35 | // given: 36 | // - a,b: cipher (a = g^r, b = h^r*g^m) 37 | // steps: 38 | // 1. generate random value x 39 | // 2. compute (a1, b1) = (a^x, g^x) 40 | // 3. generate the challenge 41 | // 3. compute f = x + c * sk (NOTE: mod q!) 42 | // 4. compute the decryption factor d = a^r 43 | export const generate = ( 44 | cipher: Cipher, 45 | sp: SystemParameters, 46 | sk: BN, 47 | uniqueID: string 48 | ): DecryptionProof => { 49 | isCipher(cipher) 50 | isSystemParameters(sp) 51 | 52 | const { a, b }: Cipher = cipher 53 | 54 | const x: BN = GlobalHelper.getSecureRandomValue(sp.q) 55 | 56 | const a1: BN = pow(a, x, sp) 57 | const b1: BN = pow(sp.g, x, sp) 58 | 59 | const c: BN = generateChallenge(sp.q, uniqueID, a, b, a1, b1) 60 | const f: BN = add(x, c.mul(sk).mod(sp.q), sp) 61 | const d: BN = pow(a, sk, sp) 62 | 63 | printConsole && console.log('x\t\t\t', x.toNumber()) 64 | printConsole && console.log('a1\t\t\t', a1.toNumber()) 65 | printConsole && console.log('b1\t\t\t', b1.toNumber()) 66 | printConsole && console.log('c\t\t\t', c.toNumber()) 67 | printConsole && console.log('f = x + c*r\t\t', f.toNumber()) 68 | printConsole && console.log() 69 | 70 | return { a1, b1, f, d } as DecryptionProof 71 | } 72 | 73 | // verify a proof for the decryption 74 | // 1. recompute the challenge 75 | // 2. verification a^f == a1 * d^c 76 | // 3. verification g^f == b1 * h^c 77 | export const verify = ( 78 | cipher: Cipher, 79 | proof: DecryptionProof, 80 | sp: SystemParameters, 81 | pk: BN, 82 | uniqueID: string 83 | ): boolean => { 84 | isCipher(cipher) 85 | isSystemParameters(sp) 86 | 87 | const { a, b }: Cipher = cipher 88 | const { a1, b1, f, d }: DecryptionProof = proof 89 | 90 | const c: BN = generateChallenge(sp.q, uniqueID, a, b, a1, b1) 91 | 92 | const l1: BN = pow(a, f, sp) 93 | const r1: BN = mul(a1, pow(d, c, sp), sp) 94 | const v1: boolean = GlobalHelper.timingSafeEqualBN(l1, r1) 95 | 96 | const l2: BN = pow(sp.g, f, sp) 97 | const r2: BN = mul(b1, pow(pk, c, sp), sp) 98 | const v2: boolean = GlobalHelper.timingSafeEqualBN(l2, r2) 99 | 100 | printConsole && console.log('a^f == a1*d^c:\t\t', v1, l1.toNumber(), r1.toNumber()) 101 | printConsole && console.log('g^f == b1*h^c\t\t', v2, l2.toNumber(), r2.toNumber()) 102 | printConsole && console.log() 103 | 104 | return v1 && v2 105 | } 106 | -------------------------------------------------------------------------------- /test/ff-elgamal/encryption.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { GlobalHelper, FFelGamal } from '../../src/index' 3 | 4 | describe('Finite Field ElGamal Encryption', () => { 5 | it('should encode a message', () => { 6 | const sp = FFelGamal.SystemSetup.generateSystemParameters(11, 3) 7 | 8 | const values = [ 9 | { decoded: 0, encoded: 1 }, 10 | { decoded: 1, encoded: 3 }, 11 | { decoded: 2, encoded: 9 }, 12 | { decoded: 3, encoded: 5 }, 13 | { decoded: 4, encoded: 4 }, 14 | { decoded: 5, encoded: 1 }, 15 | { decoded: 6, encoded: 3 }, 16 | { decoded: 7, encoded: 9 }, 17 | { decoded: 8, encoded: 5 }, 18 | { decoded: 9, encoded: 4 }, 19 | { decoded: 10, encoded: 1 }, 20 | { decoded: 11, encoded: 3 }, 21 | ] 22 | 23 | for (const value of values) { 24 | expect(FFelGamal.Encryption.encodeMessage(value.decoded, sp).toNumber()).to.equal( 25 | value.encoded 26 | ) 27 | } 28 | }) 29 | 30 | it('should decode an encoded message', () => { 31 | const sp = FFelGamal.SystemSetup.generateSystemParameters(11, 3) 32 | 33 | const values = [ 34 | { decoded: 0, encoded: 1 }, 35 | { decoded: 1, encoded: 3 }, 36 | { decoded: 2, encoded: 9 }, 37 | { decoded: 3, encoded: 5 }, 38 | { decoded: 4, encoded: 4 }, 39 | // TODO: define constraints for message space 40 | /*{ decoded: 5, encoded: 1 }, 41 | { decoded: 6, encoded: 3 }, 42 | { decoded: 7, encoded: 9 }, 43 | { decoded: 8, encoded: 5 }, 44 | { decoded: 9, encoded: 4 }, 45 | { decoded: 10, encoded: 1 }, 46 | { decoded: 11, encoded: 3 },*/ 47 | ] 48 | 49 | for (const value of values) { 50 | expect(FFelGamal.Encryption.decodeMessage(value.encoded, sp).toNumber()).to.equal( 51 | value.decoded 52 | ) 53 | } 54 | }) 55 | 56 | it('compare decryption implementations', () => { 57 | const log = false 58 | const [sp, { h: pk, sk }] = FFelGamal.SystemSetup.generateSystemParametersAndKeys(1319, 2) 59 | 60 | for (let i = 0; i < 10; i++) { 61 | // generate random messages of max size 10 62 | const message = GlobalHelper.getSecureRandomValue(GlobalHelper.newBN(10, 10)) 63 | 64 | log && console.log(i) 65 | log && console.log('prime (p)\t', sp.p) 66 | log && console.log('generator (g)\t', sp.g) 67 | log && console.log('dec secret (x)\t', sk) 68 | log && console.log(' (h)\t', pk) 69 | log && console.log('plaintext (m)', message) 70 | log && console.log('------------------------') 71 | 72 | const mEnc = FFelGamal.Encryption.encrypt(message, sp, pk, log) 73 | const mD1 = FFelGamal.Encryption.decrypt1(mEnc, sk, sp, log) 74 | const mD2 = FFelGamal.Encryption.decrypt2(mEnc, sk, sp, log) 75 | 76 | expect(mD1.eq(message)).to.be.true 77 | expect(mD2.eq(message)).to.be.true 78 | expect(mD1.eq(mD2)).to.be.true 79 | } 80 | }) 81 | 82 | it('homomorphic addition', () => { 83 | const log = false 84 | const [sp, { h: pk, sk }] = FFelGamal.SystemSetup.generateSystemParametersAndKeys(1319, 2) 85 | 86 | for (let i = 0; i < 10; i++) { 87 | // generate random messages of max size 10 88 | const m1 = GlobalHelper.getSecureRandomValue(GlobalHelper.newBN(10, 10)) 89 | const m2 = GlobalHelper.getSecureRandomValue(GlobalHelper.newBN(10, 10)) 90 | 91 | const eM1 = FFelGamal.Encryption.encrypt(m1, sp, pk, log) 92 | const eM2 = FFelGamal.Encryption.encrypt(m2, sp, pk, log) 93 | 94 | const dSum = FFelGamal.Encryption.decrypt1( 95 | FFelGamal.Encryption.add(eM1, eM2, sp), 96 | sk, 97 | sp, 98 | log 99 | ) 100 | 101 | expect(dSum.eq(m1.add(m2))).to.be.true 102 | } 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /test/ff-elgamal/proofs/keyGeneration.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { FFelGamal, GlobalHelper } from '../../../src/index' 3 | 4 | describe('ElGamal Finite Field NIZKP for Key Generation', () => { 5 | it('generate and verify (distributed) key share', () => { 6 | for (let i = 0; i < 10; i++) { 7 | const log = false 8 | 9 | // generate the system parameters: P, Q, G 10 | const sp: FFelGamal.SystemParameters = FFelGamal.SystemSetup.generateSystemParameters(11, 3) 11 | 12 | // generate the public and private key share: H_, SK_ 13 | const share: FFelGamal.KeyPair = FFelGamal.SystemSetup.generateKeyPair(sp) 14 | 15 | expect(share.h).to.eql(sp.g.pow(share.sk).mod(sp.p)) 16 | 17 | log && console.log('Key Parts') 18 | log && console.log('h:\t', share.h.toString()) 19 | log && console.log('sk:\t', share.sk.toString()) 20 | log && console.log() 21 | 22 | // generate the key share generation proof 23 | const uniqueId = 'IamReallyUnique;-)' 24 | const proof: FFelGamal.Proof.KeyGenerationProof = FFelGamal.Proof.KeyGeneration.generate( 25 | sp, 26 | share, 27 | uniqueId 28 | ) 29 | const { c: c1, d: d1 } = proof 30 | 31 | log && console.log('Proof Parts') 32 | log && console.log('c:\t', c1.toString()) 33 | log && console.log('d1:\t', d1.toString()) 34 | log && console.log() 35 | 36 | // verify that the key share has been generated truthfully 37 | const verifiedProof: boolean = FFelGamal.Proof.KeyGeneration.verify( 38 | sp, 39 | proof, 40 | share.h, 41 | uniqueId 42 | ) 43 | 44 | expect(verifiedProof).to.be.true 45 | } 46 | }) 47 | 48 | it('perform distributed key generation', () => { 49 | const log = false 50 | 51 | const sp: FFelGamal.SystemParameters = FFelGamal.SystemSetup.generateSystemParameters(11, 3) 52 | 53 | // first authority 54 | // generate the public and private key share and the key generation proof 55 | const share1: FFelGamal.KeyPair = FFelGamal.SystemSetup.generateKeyPair(sp) 56 | const uniqueId1 = 'IamReallyUnique;-)' 57 | const proof1: FFelGamal.Proof.KeyGenerationProof = FFelGamal.Proof.KeyGeneration.generate( 58 | sp, 59 | share1, 60 | uniqueId1 61 | ) 62 | expect(FFelGamal.Proof.KeyGeneration.verify(sp, proof1, share1.h, uniqueId1)).to.be.true 63 | 64 | // second authority 65 | // generate the public and private key share and the key generation proof 66 | const share2: FFelGamal.KeyPair = FFelGamal.SystemSetup.generateKeyPair(sp) 67 | const uniqueId2 = 'IamMuchMoreUnique_o.o' 68 | const proof2: FFelGamal.Proof.KeyGenerationProof = FFelGamal.Proof.KeyGeneration.generate( 69 | sp, 70 | share2, 71 | uniqueId2 72 | ) 73 | expect(FFelGamal.Proof.KeyGeneration.verify(sp, proof2, share2.h, uniqueId2)).to.be.true 74 | 75 | log && console.log('1: pk, sk', share1.h.toNumber(), share1.sk.toNumber()) 76 | log && console.log('2: pk, sk', share2.h.toNumber(), share2.sk.toNumber()) 77 | 78 | // combined keys 79 | const publicKey = FFelGamal.SystemSetup.combinePublicKeys(sp, [share1.h, share2.h]) 80 | const privateKey = FFelGamal.SystemSetup.combinePrivateKeys(sp, [share1.sk, share2.sk]) 81 | 82 | log && console.log('pk', publicKey.toNumber()) 83 | log && console.log('sk', privateKey.toNumber()) 84 | 85 | // encrypt some message 86 | const plaintext = GlobalHelper.newBN(3) 87 | const cipherText = FFelGamal.Encryption.encrypt(plaintext, sp, publicKey) 88 | 89 | log && console.log('plaintext', plaintext.toNumber()) 90 | log && console.log('cipherText (a,b)', cipherText.a.toNumber(), cipherText.b.toNumber()) 91 | 92 | // decrypt shares 93 | const decShare1 = FFelGamal.Encryption.decryptShare(sp, cipherText, share1.sk) 94 | const decShare2 = FFelGamal.Encryption.decryptShare(sp, cipherText, share2.sk) 95 | 96 | log && console.log('ds1', decShare1.toNumber()) 97 | log && console.log('ds2', decShare2.toNumber()) 98 | 99 | // create proofs 100 | const decryptionProof1 = FFelGamal.Proof.Decryption.generate( 101 | cipherText, 102 | sp, 103 | share1.sk, 104 | uniqueId1 105 | ) 106 | 107 | const decryptionProof2 = FFelGamal.Proof.Decryption.generate( 108 | cipherText, 109 | sp, 110 | share2.sk, 111 | uniqueId2 112 | ) 113 | 114 | // verify proofs 115 | const verifiedProof1 = FFelGamal.Proof.Decryption.verify( 116 | cipherText, 117 | decryptionProof1, 118 | sp, 119 | share1.h, 120 | uniqueId1 121 | ) 122 | const verifiedProof2 = FFelGamal.Proof.Decryption.verify( 123 | cipherText, 124 | decryptionProof2, 125 | sp, 126 | share2.h, 127 | uniqueId2 128 | ) 129 | expect(verifiedProof1).to.be.true 130 | expect(verifiedProof2).to.be.true 131 | 132 | // finish decryption by combining decrypted shares 133 | const decFinal = FFelGamal.Encryption.combineDecryptedShares(sp, cipherText, [ 134 | decShare1, 135 | decShare2, 136 | ]) 137 | 138 | // decrypt with combined private key 139 | const d2 = FFelGamal.Encryption.decrypt2(cipherText, privateKey, sp) 140 | 141 | log && console.log('d', decFinal.toNumber()) 142 | log && console.log('d2', d2.toNumber()) 143 | 144 | expect(decFinal.toNumber()).to.eql(d2.toNumber()) 145 | expect(decFinal.toNumber()).to.eql(plaintext.toNumber()) 146 | }) 147 | }) 148 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 7 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 8 | // "lib": [], /* Specify library files to be included in the compilation. */ 9 | // "allowJs": true, /* Allow javascript files to be compiled. */ 10 | // "checkJs": true /* Report errors in .js files. */, 11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 12 | "declaration": true /* Generates corresponding '.d.ts' file. */, 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "./lib" /* Redirect output structure to the directory. */, 17 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 18 | // "composite": true, /* Enable project compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | "removeComments": true /* Do not emit comments to output. */, 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | "preserveConstEnums": true, 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 30 | "strictNullChecks": true /* Enable strict null checks. */, 31 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 32 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 33 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 34 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 35 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 36 | 37 | /* Additional Checks */ 38 | "noUnusedLocals": true /* Report errors on unused locals. */, 39 | "noUnusedParameters": true /* Report errors on unused parameters. */, 40 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 41 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, 51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 67 | }, 68 | "include": ["./src"] 69 | } 70 | -------------------------------------------------------------------------------- /src/ff-elgamal/encryption.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Encryption 3 | * 4 | * ElGamal Finite Field Encryption 5 | * - encode and decode messages 6 | * - encrypt and decrypt messages 7 | * - homomorphically add encrypted messages 8 | * - decrypt cipher texts with a private key share 9 | * - combine decrypted shares 10 | */ 11 | 12 | import BN = require('bn.js') 13 | import { GlobalHelper } from '../index' 14 | import { Cipher, SystemParameters, isCipher, isSystemParameters } from './index' 15 | 16 | // encode a message m to g^m 17 | export const encodeMessage = (m: number | BN, sysParams: SystemParameters): BN => { 18 | isSystemParameters(sysParams) 19 | m = typeof m === 'number' ? GlobalHelper.newBN(m) : m 20 | return GlobalHelper.powBN(sysParams.g, m, sysParams.p) 21 | } 22 | 23 | // decode a message g^m to m 24 | // TODO: use baby-step giant-step instead of brute force 25 | export const decodeMessage = (mh: number | BN, sysParams: SystemParameters): BN => { 26 | isSystemParameters(sysParams) 27 | mh = typeof mh === 'number' ? GlobalHelper.newBN(mh) : mh 28 | 29 | let m = GlobalHelper.newBN(0) 30 | while (!GlobalHelper.timingSafeEqualBN(encodeMessage(m, sysParams), mh)) { 31 | m = m.add(GlobalHelper.newBN(1)) 32 | } 33 | return m 34 | } 35 | 36 | // TODO: test encryption and both decryption for the whole message range 37 | // (to verify the correct implementation and usage of decodeMessage) 38 | 39 | // Finite Field ElGamal Encryption 40 | // 41 | // given: 42 | // - p: prime number 43 | // - g: generator 44 | // - h: public key (g^privateKey) 45 | // - m: message 46 | // 47 | // steps: 48 | // 1. pick random value r: 0 < r < q 49 | // 2. compute c1 = g^r 50 | // 3. compute s = h^r 51 | // 4. compute mh = g^message (encode it to make it "homomorphic") 52 | // 5. compute c2 = s*mh 53 | export const encrypt = ( 54 | message: number | BN, 55 | sysParams: SystemParameters, 56 | publicKey: BN, 57 | log = false 58 | ): Cipher => { 59 | isSystemParameters(sysParams) 60 | const m = typeof message === 'number' ? GlobalHelper.newBN(message) : message 61 | 62 | const r = GlobalHelper.getSecureRandomValue(sysParams.q) 63 | const c1 = GlobalHelper.powBN(sysParams.g, r, sysParams.p) 64 | const s = GlobalHelper.powBN(publicKey, r, sysParams.p) 65 | const mh = encodeMessage(m, sysParams) 66 | const c2 = GlobalHelper.mulBN(s, mh, sysParams.p) 67 | 68 | log && console.log('enc secret (r)', r) 69 | log && console.log('a\t\t', c1) 70 | log && console.log('h^r\t\t', s) 71 | log && console.log('g^m\t\t', mh) 72 | log && console.log('b\t\t', c2) 73 | log && console.log('------------------------') 74 | 75 | return { a: c1, b: c2, r } 76 | } 77 | 78 | // Finite Field ElGamal Decryption 79 | // 80 | // given: 81 | // - p: prime number 82 | // - g: generator 83 | // - x: private key 84 | // - c1,c2: cipher 85 | // 86 | // steps: 87 | // 1. compute s = c1^x 88 | // 2. compute s^-1 = multiplicative inverse of s 89 | // 3. compute mh = c2 * s^-1 90 | // 4. compute m (decode mh using brute force) 91 | export const decrypt1 = ( 92 | cipherText: Cipher, 93 | sk: BN, 94 | sysParams: SystemParameters, 95 | log = false 96 | ): BN => { 97 | isCipher(cipherText) 98 | isSystemParameters(sysParams) 99 | 100 | const { a: c1, b: c2 } = cipherText 101 | 102 | const s = GlobalHelper.powBN(c1, sk, sysParams.p) 103 | const sInverse = GlobalHelper.invmBN(s, sysParams.p) 104 | const mh = GlobalHelper.mulBN(c2, sInverse, sysParams.p) 105 | const m = decodeMessage(mh, sysParams) 106 | 107 | log && console.log('s\t\t', s) 108 | log && console.log('s^-1\t\t', sInverse) 109 | log && console.log('mh\t\t', mh) 110 | log && console.log('plaintext d1\t', m) 111 | log && console.log('------------------------') 112 | 113 | return m 114 | } 115 | 116 | // Finite Field ElGamal Decryption Alternative (using Euler's Theorem) 117 | // 118 | // given: 119 | // - p: prime number 120 | // - g: generator 121 | // - x: private key 122 | // - c1,c2: cipher 123 | // 124 | // steps: 125 | // 1. compute s = c1^x 126 | // 2. compute s^-1 = multiplicative inverse of s 127 | // 3. compute s^(p-2) 128 | // 4. compute mh = c2 * s^(p-2) 129 | // 5. compute m (decode mh using brute force) 130 | export const decrypt2 = ( 131 | cipherText: Cipher, 132 | sk: BN, 133 | sysParams: SystemParameters, 134 | log = false 135 | ): BN => { 136 | isCipher(cipherText) 137 | isSystemParameters(sysParams) 138 | 139 | const { a: c1, b: c2 } = cipherText 140 | 141 | const s = GlobalHelper.powBN(c1, sk, sysParams.p) 142 | const sPowPMinus2 = GlobalHelper.powBN(s, sysParams.p.sub(GlobalHelper.newBN(2)), sysParams.p) 143 | const mh = GlobalHelper.mulBN(c2, sPowPMinus2, sysParams.p) 144 | const m = decodeMessage(mh, sysParams) 145 | 146 | log && console.log('s\t\t', s) 147 | log && console.log('s^(p-2)\t\t', sPowPMinus2) 148 | log && console.log('mh\t', mh) 149 | log && console.log('plaintext d2\t', m) 150 | log && console.log('------------------------') 151 | 152 | return m 153 | } 154 | 155 | // homomorphic addition 156 | export const add = (em1: Cipher, em2: Cipher, sysParams: SystemParameters): Cipher => { 157 | isCipher(em1) 158 | isCipher(em2) 159 | isSystemParameters(sysParams) 160 | return { 161 | a: GlobalHelper.mulBN(em1.a, em2.a, sysParams.p), 162 | b: GlobalHelper.mulBN(em1.b, em2.b, sysParams.p), 163 | } 164 | } 165 | 166 | // decrypt a cipher text with a private key share 167 | export const decryptShare = (params: SystemParameters, cipher: Cipher, secretKeyShare: BN): BN => { 168 | isSystemParameters(params) 169 | isCipher(cipher) 170 | return GlobalHelper.powBN(cipher.a, secretKeyShare, params.p) 171 | } 172 | 173 | // combine decrypted shares 174 | export const combineDecryptedShares = ( 175 | params: SystemParameters, 176 | cipher: Cipher, 177 | decryptedShares: BN[] 178 | ): BN => { 179 | isSystemParameters(params) 180 | isCipher(cipher) 181 | const mh = GlobalHelper.divBN( 182 | cipher.b, 183 | decryptedShares.reduce((product, share) => GlobalHelper.mulBN(product, share, params.p)), 184 | params.p 185 | ) 186 | 187 | return decodeMessage(mh, params) 188 | } 189 | -------------------------------------------------------------------------------- /test/ec-elgamal/proofs/keyGeneration.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { ECelGamal } from '../../../src/index' 3 | 4 | const generateKeyPairs = (n: number): ECelGamal.KeyPair[] => { 5 | const res: ECelGamal.KeyPair[] = [] 6 | for (let i = 0; i < n; i++) { 7 | res.push(ECelGamal.SystemSetup.generateKeyPair()) 8 | } 9 | return res 10 | } 11 | 12 | describe('Elliptic Curve ElGamal Distributed Key Generation', () => { 13 | it('generate and verify (distributed) key share', () => { 14 | for (let i = 0; i < 10; i++) { 15 | const log = false 16 | 17 | // generate the system parameters: P, Q, G 18 | const params: ECelGamal.SystemParameters = ECelGamal.SystemSetup.generateSystemParameters() 19 | const { g } = params 20 | 21 | // generate the public and private key share: H_, SK_ 22 | const share: ECelGamal.KeyPair = ECelGamal.SystemSetup.generateKeyPair() 23 | const { h: h1, sk: sk1 } = share 24 | 25 | expect(h1).to.eql(ECelGamal.Helper.ECpow(g, sk1)) 26 | 27 | log && console.log('Key Parts') 28 | log && console.log('h_:\t', h1.toString()) 29 | log && console.log('sk_:\t', sk1.toString()) 30 | log && console.log() 31 | 32 | // generate the key share generation proof 33 | const uniqueId = 'IamReallyUnique;-)' 34 | const proof: ECelGamal.Proof.KeyGenerationProof = ECelGamal.Proof.KeyGeneration.generate( 35 | params, 36 | share, 37 | uniqueId 38 | ) 39 | const { c: c1, d: d1 } = proof 40 | 41 | log && console.log('Proof Parts') 42 | log && console.log('c:\t', c1.toString()) 43 | log && console.log('d1:\t', d1.toString()) 44 | log && console.log() 45 | 46 | // verify that the key share has been generated truthfully 47 | const verifiedProof: boolean = ECelGamal.Proof.KeyGeneration.verify( 48 | params, 49 | proof, 50 | h1, 51 | uniqueId 52 | ) 53 | 54 | expect(verifiedProof).to.be.true 55 | } 56 | }) 57 | 58 | it('combine public keys', () => { 59 | // generate one share 60 | let keyPairs: ECelGamal.KeyPair[] = generateKeyPairs(1) 61 | let shares: ECelGamal.CurvePoint[] = [keyPairs[0].h] 62 | let product: ECelGamal.CurvePoint = shares[0] 63 | expect(ECelGamal.SystemSetup.combinePublicKeys(shares)).to.eql(product) 64 | 65 | // generate two shares + combine them 66 | keyPairs = generateKeyPairs(2) 67 | shares = [keyPairs[0].h, keyPairs[1].h] 68 | product = ECelGamal.Helper.ECmul(shares[0], shares[1]) 69 | expect(ECelGamal.SystemSetup.combinePublicKeys(shares)).to.eql(product) 70 | 71 | // generate three shares + combine them 72 | keyPairs = generateKeyPairs(3) 73 | shares = [keyPairs[0].h, keyPairs[1].h, keyPairs[2].h] 74 | product = ECelGamal.Helper.ECmul(ECelGamal.Helper.ECmul(shares[0], shares[1]), shares[2]) 75 | expect(ECelGamal.SystemSetup.combinePublicKeys(shares)).to.eql(product) 76 | }) 77 | 78 | it('perform distributed key generation', () => { 79 | const log = false 80 | 81 | const params: ECelGamal.SystemParameters = ECelGamal.SystemSetup.generateSystemParameters() 82 | 83 | // first authority 84 | // generate the public and private key share and the key generation proof 85 | const share1: ECelGamal.KeyPair = ECelGamal.SystemSetup.generateKeyPair() 86 | const uniqueId1 = 'IamReallyUnique;-)' 87 | const proof1: ECelGamal.Proof.KeyGenerationProof = ECelGamal.Proof.KeyGeneration.generate( 88 | params, 89 | share1, 90 | uniqueId1 91 | ) 92 | const verified1: boolean = ECelGamal.Proof.KeyGeneration.verify( 93 | params, 94 | proof1, 95 | share1.h, 96 | uniqueId1 97 | ) 98 | expect(verified1).to.be.true 99 | 100 | // second authority 101 | // generate the public and private key share and the key generation proof 102 | const share2: ECelGamal.KeyPair = ECelGamal.SystemSetup.generateKeyPair() 103 | const uniqueId2 = 'IamMuchMoreUnique_o.o' 104 | const proof2: ECelGamal.Proof.KeyGenerationProof = ECelGamal.Proof.KeyGeneration.generate( 105 | params, 106 | share2, 107 | uniqueId2 108 | ) 109 | const verified2: boolean = ECelGamal.Proof.KeyGeneration.verify( 110 | params, 111 | proof2, 112 | share2.h, 113 | uniqueId2 114 | ) 115 | expect(verified2).to.be.true 116 | 117 | log && console.log('1: pk, sk', share1.h, share1.sk) 118 | log && console.log('2: pk, sk', share2.h, share2.sk) 119 | 120 | // combined keys 121 | const publicKey = ECelGamal.SystemSetup.combinePublicKeys([share1.h, share2.h]) 122 | const privateKey = ECelGamal.SystemSetup.combinePrivateKeys(params, [share1.sk, share2.sk]) 123 | 124 | log && console.log('pk', publicKey) 125 | log && console.log('sk', privateKey) 126 | 127 | // encrypt a single yes vote -> we use the generator 128 | const plaintext = ECelGamal.Curve.g 129 | const cipherText = ECelGamal.Encryption.encrypt(plaintext, publicKey) 130 | 131 | log && console.log('plaintext', plaintext) 132 | log && console.log('cipherText (a,b)', cipherText.a, cipherText.b) 133 | 134 | // decrypt shares 135 | const share1Decrypted = ECelGamal.Encryption.decryptShare(cipherText, share1.sk) 136 | const share2Decrypted = ECelGamal.Encryption.decryptShare(cipherText, share2.sk) 137 | 138 | log && console.log('share 1 - decrypted\t', share1Decrypted) 139 | log && console.log('share 2 - decrypted\t', share2Decrypted) 140 | 141 | // finish decryption by combining decrypted shares 142 | const distributedDecrypted = ECelGamal.Encryption.combineDecryptedShares(cipherText, [ 143 | share1Decrypted, 144 | share2Decrypted, 145 | ]) 146 | const result1 = ECelGamal.Voting.findPoint(distributedDecrypted) 147 | 148 | // decrypt with combined private key 149 | const combinedDecryption = ECelGamal.Encryption.decrypt(cipherText, privateKey) 150 | const result2 = ECelGamal.Voting.findPoint(combinedDecryption) 151 | 152 | log && console.log('distributed decryption\t', distributedDecrypted.toString()) 153 | log && console.log('combined decryption\t', combinedDecryption.toString()) 154 | 155 | // check that decrypting both ways results in the same result 156 | expect(distributedDecrypted).to.eql(combinedDecryption) 157 | expect(result1).to.equal(result2) 158 | }) 159 | }) 160 | -------------------------------------------------------------------------------- /test/ff-elgamal/helper.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { FFelGamal } from '../../src/index' 3 | //import { primes as primes2 } from './primes.spec' 4 | 5 | const Helper = FFelGamal.Helper 6 | 7 | // all prime numbers up to 100 8 | // prettier-ignore 9 | const primes = [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 10 | 67, 71, 73, 79, 83, 89, 97 ] 11 | 12 | describe('Finite Field ElGamal Helper', () => { 13 | it('should check if a given number is prime', () => { 14 | for (const prime of primes) { 15 | expect(Helper.isPrime(prime)).to.be.true 16 | } 17 | 18 | // print all primes up to 100'000 19 | /* 20 | let p = '' 21 | for (let i = 2; i <= 100000; i++) { 22 | if(Helper.isPrime(i)) { 23 | p += (i + ',') 24 | } 25 | } 26 | console.log(p) 27 | */ 28 | }) 29 | 30 | it('should get all primitive roots given n', () => { 31 | // values from https://en.wikipedia.org/wiki/Primitive_root_modulo_n 32 | // calculate them here www.bluetulip.org/2014/programs/primitive.html 33 | // prettier-ignore 34 | const values = [ 35 | //{ n: 2, roots: [1] }, 36 | { n: 3, roots: [2] }, 37 | //{ n: 4, roots: [3] }, 38 | { n: 5, roots: [2, 3] }, 39 | //{ n: 6, roots: [5] }, 40 | { n: 7, roots: [3, 5] }, 41 | //{ n: 8, roots: [] }, 42 | //{ n: 9, roots: [2, 5] }, 43 | //{ n: 10, roots: [3, 7] }, 44 | { n: 11, roots: [2, 6, 7, 8] }, 45 | //{ n: 12, roots: [] }, 46 | { n: 13, roots: [2, 6, 7, 11] }, 47 | //{ n: 14, roots: [3, 5] }, 48 | //{ n: 15, roots: [] }, 49 | //{ n: 16, roots: [] }, 50 | { n: 17, roots: [3, 5, 6, 7, 10, 11, 12, 14] }, 51 | //{ n: 18, roots: [5 ,11] }, 52 | { n: 19, roots: [2, 3, 10, 13, 14, 15] }, 53 | //{ n: 20, roots: [] }, 54 | //{ n: 21, roots: [] }, 55 | //{ n: 22, roots: [7, 13, 17, 19] }, 56 | { n: 23, roots: [5, 7, 10, 11, 14, 15, 17, 19, 20, 21] }, 57 | { n: 29, roots: [2, 3, 8, 10, 11, 14, 15, 18, 19, 21, 26, 27] }, 58 | { n: 31, roots: [3, 11, 12, 13, 17, 21, 22, 24] }, 59 | { n: 37, roots: [2, 5, 13, 15, 17, 18, 19, 20, 22, 24, 32, 35] }, 60 | { n: 41, roots: [6, 7, 11, 12, 13, 15, 17, 19, 22, 24, 26, 28, 29, 30, 34, 35] }, 61 | { n: 43, roots: [3, 5, 12, 18, 19, 20, 26, 28, 29, 30, 33, 34] }, 62 | { n: 47, roots: [5, 10, 11, 13, 15, 19, 20, 22, 23, 26, 29, 30, 31, 33, 35, 38, 63 | 39, 40, 41, 43, 44, 45 ] }, 64 | { n: 53, roots: [2, 3, 5, 8, 12, 14, 18, 19, 20, 21, 22, 26, 27, 31, 32, 33, 34, 65 | 35, 39, 41, 45, 48, 50, 51 ] }, 66 | { n: 59, roots: [2, 6, 8, 10, 11, 13, 14, 18, 23, 24, 30, 31, 32, 33, 34, 37, 38, 67 | 39, 40, 42, 43, 44, 47, 50, 52, 54, 55, 56 ] }, 68 | { n: 61, roots: [2, 6, 7, 10, 17, 18, 26, 30, 31, 35, 43, 44, 51, 54, 55, 59] }, 69 | { n: 67, roots: [2, 7, 11, 12, 13, 18, 20, 28, 31, 32, 34, 41, 44, 46, 48, 50, 51, 70 | 57, 61, 63] }, 71 | { n: 71, roots: [7, 11, 13, 21, 22, 28, 31, 33, 35, 42, 44, 47, 52, 53, 55, 56, 59, 72 | 61, 62, 63, 65, 67, 68, 69 ] }, 73 | { n: 73, roots: [5, 11, 13, 14, 15, 20, 26, 28, 29, 31, 33, 34, 39, 40, 42, 44, 45, 74 | 47, 53, 58, 59, 60, 62, 68 ] }, 75 | { n: 79, roots: [3, 6, 7, 28, 29, 30, 34, 35, 37, 39, 43, 47, 48, 53, 54, 59, 60, 63, 76 | 66, 68, 70, 74, 75, 77 ] } 77 | ] 78 | 79 | for (const value of values) { 80 | expect(Helper.getPrimitiveRoots(value.n)).to.eql(value.roots) 81 | } 82 | }) 83 | 84 | it('should calculate q given p', () => { 85 | expect(Helper.getQofP(-2)).to.eql(-1) 86 | expect(Helper.getQofP(0)).to.eql(-1) 87 | expect(Helper.getQofP(-1)).to.eql(-1) 88 | expect(Helper.getQofP(1)).to.eql(-1) 89 | expect(Helper.getQofP(2)).to.eql(0.5) 90 | expect(Helper.getQofP(3)).to.eql(1) 91 | expect(Helper.getQofP(4)).to.eql(1.5) 92 | expect(Helper.getQofP(5)).to.eql(2) 93 | expect(Helper.getQofP(6)).to.eql(2.5) 94 | expect(Helper.getQofP(7)).to.eql(3) 95 | expect(Helper.getQofP(8)).to.eql(3.5) 96 | expect(Helper.getQofP(9)).to.eql(4) 97 | expect(Helper.getQofP(10)).to.eql(4.5) 98 | expect(Helper.getQofP(11)).to.eql(5) 99 | expect(Helper.getQofP(13)).to.eql(6) 100 | expect(Helper.getQofP(17)).to.eql(8) 101 | expect(Helper.getQofP(19)).to.eql(9) 102 | }) 103 | 104 | it('should validate q', () => { 105 | expect(Helper.isQValid(-1)).to.false 106 | expect(Helper.isQValid(0)).to.false 107 | expect(Helper.isQValid(1)).to.false 108 | expect(Helper.isQValid(2)).to.true 109 | expect(Helper.isQValid(3)).to.true 110 | expect(Helper.isQValid(4)).to.false 111 | expect(Helper.isQValid(5)).to.true 112 | }) 113 | 114 | it('should validate g given p', () => { 115 | const values = [ 116 | { p: 7, gs: [2] }, 117 | { p: 11, gs: [3] }, 118 | { p: 23, gs: [2, 6, 8] }, 119 | { p: 59, gs: [3] }, 120 | { p: 167, gs: [2, 8, 32] }, 121 | { p: 263, gs: [2, 8, 128] }, 122 | { p: 347, gs: [11, 44] }, 123 | { p: 359, gs: [2, 8, 32] }, 124 | { p: 839, gs: [2] }, 125 | { p: 887, gs: [2] }, 126 | { p: 983, gs: [2] }, 127 | { p: 1319, gs: [2] }, 128 | { p: 2039, gs: [2] }, 129 | ] 130 | 131 | for (const value of values) { 132 | for (const g of value.gs) { 133 | expect(Helper.isGValid(g, value.p)).to.true 134 | } 135 | } 136 | 137 | expect(Helper.isGValid(3, 7)).to.false 138 | }) 139 | 140 | it('should get candidate values for p (having a q that is prime)', () => { 141 | expect(Helper.getPCandidates(primes)).to.eql([5, 7, 11, 23, 47, 59, 83]) 142 | }) 143 | 144 | it('should get candidate values for g given p', () => { 145 | const values = [ 146 | { p: 7, gs: [2] }, 147 | { p: 11, gs: [3] }, 148 | { p: 23, gs: [2, 6, 8] }, 149 | { p: 59, gs: [3] }, // next ones over 100: 167, 263, ... 150 | ] 151 | 152 | for (const value of values) { 153 | expect(Helper.getGCandidates(value.p)).to.eql(value.gs) 154 | } 155 | 156 | /* 157 | // get candidate gs for all primes up to 10'000 158 | let index = 0; 159 | for (const prime of primes2) { 160 | const gs = Helper.getGCandidates(prime) 161 | gs.length > 0 && console.log(prime, gs) 162 | index++ % 100 === 0 && console.log(prime, gs) 163 | } 164 | */ 165 | }) 166 | 167 | xit('should print suitable values', () => { 168 | const prime = 11 169 | console.log('p candidates', Helper.getPCandidates(primes).toString()) 170 | console.log() 171 | console.log('p', prime) 172 | console.log('g candidates', Helper.getGCandidates(prime).toString()) 173 | }) 174 | }) 175 | -------------------------------------------------------------------------------- /src/ec-elgamal/proofs/membership.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | 3 | import { GlobalHelper } from '../../index' 4 | import { 5 | Cipher, 6 | Curve, 7 | CurvePoint, 8 | Helper, 9 | SystemParameters, 10 | SystemParametersSerialized, 11 | } from '../index' 12 | import { MembershipProof } from './models' 13 | import { curveDefinition } from '../curve' 14 | 15 | const printConsole = false 16 | 17 | export const generateChallenge = ( 18 | n: BN, 19 | id: string, 20 | c1: CurvePoint, 21 | c2: CurvePoint, 22 | a1: CurvePoint, 23 | a2: CurvePoint, 24 | b1: CurvePoint, 25 | b2: CurvePoint 26 | ): BN => { 27 | const pointsAsString = Helper.curvePointsToString([c1, c2, a1, a2, b1, b2]) 28 | const input = id + pointsAsString 29 | 30 | let c = curveDefinition 31 | .hash() 32 | .update(input) 33 | .digest('hex') 34 | 35 | c = new BN(c, 'hex') 36 | c = c.mod(n) 37 | 38 | return c 39 | } 40 | 41 | // generate a proof for an encrypted yes vote 42 | // steps: 43 | // 1. generate fake values c0,f0 for m=0 (random values in Z_q = Z_n) 44 | // 2. compute fake (a0,b0) = (g^f0 / a^c0, h^f0 / b^c0) 45 | // 3. generate proof for m=1 46 | // 3.1 generate a random value x in Z_q = Z_n 47 | // 3.2 compute (a1,b1) = (g^x, h^x) 48 | // 4. generate the challenge c 49 | // 5. compute c1 = c - c0 50 | // 6. compute f1 = x + c1 * r (NOTE: mod q! = mod n!) 51 | export const generateYesProof = ( 52 | encryptedVote: Cipher, 53 | params: SystemParameters | SystemParametersSerialized, 54 | publicKey: CurvePoint | string, 55 | id: string 56 | ): MembershipProof => { 57 | const { a, b, r } = encryptedVote 58 | const { g, n } = Helper.deserializeParams(params) 59 | const h = Helper.deserializeCurvePoint(publicKey) 60 | 61 | if (r === undefined || r === null) { 62 | throw new Error('value r is undefined') 63 | } 64 | 65 | const c0: BN = GlobalHelper.getSecureRandomValue(n) 66 | const f0: BN = GlobalHelper.getSecureRandomValue(n) 67 | 68 | const a0 = Helper.ECdiv(Helper.ECpow(g, f0), Helper.ECpow(a, c0)) 69 | const b0 = Helper.ECdiv(Helper.ECpow(h, f0), Helper.ECpow(b, c0)) 70 | 71 | const x: BN = GlobalHelper.getSecureRandomValue(n) 72 | const a1 = Helper.ECpow(g, x) 73 | const b1 = Helper.ECpow(h, x) 74 | 75 | const c = generateChallenge(n, id, a, b, a0, b0, a1, b1) 76 | const c1 = GlobalHelper.addBN(n, GlobalHelper.subBN(c, c0, n), n) 77 | 78 | const f1 = GlobalHelper.addBN(x, GlobalHelper.mulBN(c1, r, n), n) 79 | 80 | printConsole && console.log('a0 is on the curve?\t', Curve.validate(a0)) 81 | printConsole && console.log('b0 is on the curve?\t', Curve.validate(b0)) 82 | printConsole && console.log('a1 is on the curve?\t', Curve.validate(a1)) 83 | printConsole && console.log('b1 is on the curve?\t', Curve.validate(b1)) 84 | 85 | printConsole && console.log('c0\t\t\t\t', c0.toString('hex')) 86 | printConsole && console.log('f0\t\t\t\t', f0.toString('hex')) 87 | printConsole && console.log('x\t\t\t\t', x.toString('hex')) 88 | printConsole && console.log('c\t\t\t\t', c.toString('hex')) 89 | printConsole && console.log('c1 = (q + (c - c0) % q) % q\t', c1.toString('hex')) 90 | printConsole && console.log('f1 = x + c1*r\t\t\t', f1.toString('hex')) 91 | printConsole && console.log() 92 | 93 | return { a0, a1, b0, b1, c0, c1, f0, f1 } 94 | } 95 | 96 | // generate a proof for an encrypted no vote 97 | // steps: 98 | // 1. generate fake values c1,f1 for m=1 (random values in Z_q = Z_n) 99 | // 2. compute fake b_ = b/g 100 | // 3. compute fake (a1,b1) = (g^f1 / a^c1, h^f1 / (b/g)^c1) 101 | // 4. generate proof for m=0 102 | // 4.1 generate a random value x in Z_q = Z_n 103 | // 4.2 compute (a0,b0) = (g^x, h^x) 104 | // 5. generate the challenge c 105 | // 6. compute c0 = c - c1 106 | // 7. compute f0 = x + c0 * r (NOTE: mod q!) 107 | export const generateNoProof = ( 108 | encryptedVote: Cipher, 109 | params: SystemParameters | SystemParametersSerialized, 110 | publicKey: CurvePoint | string, 111 | id: string 112 | ): MembershipProof => { 113 | const { a, b, r } = encryptedVote 114 | const { g, n } = Helper.deserializeParams(params) 115 | const h = Helper.deserializeCurvePoint(publicKey) 116 | 117 | if (r === undefined || r === null) { 118 | throw new Error('value r is undefined') 119 | } 120 | 121 | const c1: BN = GlobalHelper.getSecureRandomValue(n) 122 | const f1: BN = GlobalHelper.getSecureRandomValue(n) 123 | 124 | const b_ = Helper.ECdiv(b, g) 125 | const a1 = Helper.ECdiv(Helper.ECpow(g, f1), Helper.ECpow(a, c1)) 126 | const b1 = Helper.ECdiv(Helper.ECpow(h, f1), Helper.ECpow(b_, c1)) 127 | 128 | const x: BN = GlobalHelper.getSecureRandomValue(n) 129 | const a0 = Helper.ECpow(g, x) 130 | const b0 = Helper.ECpow(h, x) 131 | 132 | const c = generateChallenge(n, id, a, b, a0, b0, a1, b1) 133 | const c0 = GlobalHelper.addBN(n, GlobalHelper.subBN(c, c1, n), n) 134 | 135 | const f0 = GlobalHelper.addBN(x, GlobalHelper.mulBN(c0, r, n), n) 136 | 137 | printConsole && console.log('a1 is on the curve?\t', Curve.validate(a1)) 138 | printConsole && console.log('b1 is on the curve?\t', Curve.validate(b1)) 139 | printConsole && console.log('a0 is on the curve?\t', Curve.validate(a0)) 140 | printConsole && console.log('b0 is on the curve?\t', Curve.validate(b0)) 141 | 142 | printConsole && console.log('c1\t\t\t\t', c1.toString('hex')) 143 | printConsole && console.log('f1\t\t\t\t', f1.toString('hex')) 144 | printConsole && console.log('x\t\t\t\t', x.toString('hex')) 145 | printConsole && console.log('c\t\t\t\t', c.toString('hex')) 146 | printConsole && console.log('c0 = (q + (c - c1) % q) % q\t', c0.toString('hex')) 147 | printConsole && console.log('f0 = x + c0*r\t\t\t', f0.toString('hex')) 148 | printConsole && console.log() 149 | 150 | return { a0, a1, b0, b1, c0, c1, f0, f1 } 151 | } 152 | 153 | // verification g^f0 == a0*a^c0 154 | // verification g^f1 == a1*a^c1 155 | // verification h^f0 == b0 * b^c0 156 | // verification h^f1 == b1 * (b/g)^c1 157 | // recompute the hash and verify 158 | export const verify = ( 159 | encryptedVote: Cipher, 160 | proof: MembershipProof, 161 | params: SystemParameters | SystemParametersSerialized, 162 | publicKey: CurvePoint | string, 163 | id: string 164 | ): boolean => { 165 | const { a0, a1, b0, b1, c0, c1, f0, f1 } = proof 166 | const { g, n } = Helper.deserializeParams(params) 167 | const h = Helper.deserializeCurvePoint(publicKey) 168 | const { a, b } = encryptedVote 169 | 170 | const v1 = Helper.ECpow(g, f0).eq(Helper.ECmul(a0, Helper.ECpow(a, c0))) 171 | const v2 = Helper.ECpow(g, f1).eq(Helper.ECmul(a1, Helper.ECpow(a, c1))) 172 | const v3 = Helper.ECpow(h, f0).eq(Helper.ECmul(b0, Helper.ECpow(b, c0))) 173 | const v4 = Helper.ECpow(h, f1).eq(Helper.ECmul(b1, Helper.ECpow(Helper.ECdiv(b, g), c1))) 174 | const v5 = GlobalHelper.addBN(c0, c1, n).eq(generateChallenge(n, id, a, b, a0, b0, a1, b1)) 175 | 176 | printConsole && console.log('g^f0 == a0*a^c0:\t', v1) 177 | printConsole && console.log('g^f1 == a1*a^c1\t\t', v2) 178 | printConsole && console.log('h^f0 == b0*b^c0\t\t', v3) 179 | printConsole && console.log('h^f1 == b1*(b/g)^c1\t', v4) 180 | printConsole && console.log('c == c1 + c0\t\t', v5) 181 | printConsole && console.log() 182 | 183 | return v1 && v2 && v3 && v4 && v5 184 | } 185 | -------------------------------------------------------------------------------- /src/ff-elgamal/proofs/membership.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Membership Proof 3 | * 4 | * ElGamal Finite Field Non-Interactive Zero-Knowledge Proof for Plaintext Membership 5 | * Using the Disjunctive Chaum-Pedersen Proof 6 | * 7 | * Proof that one of two statements is true without revealing which one. 8 | * 9 | * - generate and verify proofs 10 | */ 11 | 12 | import BN = require('bn.js') 13 | import { GlobalHelper } from '../../index' 14 | import { Cipher, SystemParameters, isCipher, isSystemParameters } from '../index' 15 | import { MembershipProof } from './models' 16 | 17 | const web3 = require('web3') 18 | const printConsole = false 19 | 20 | // modulo operations 21 | const add = (a: BN, b: BN, sp: SystemParameters): BN => GlobalHelper.addBN(a, b, sp.q) 22 | const sub = (a: BN, b: BN, sp: SystemParameters): BN => GlobalHelper.subBN(a, b, sp.q) 23 | const mul = (a: BN, b: BN, sp: SystemParameters): BN => GlobalHelper.mulBN(a, b, sp.p) 24 | const div = (a: BN, b: BN, sp: SystemParameters): BN => GlobalHelper.divBN(a, b, sp.p) 25 | const pow = (a: BN, b: BN, sp: SystemParameters): BN => GlobalHelper.powBN(a, b, sp.p) 26 | 27 | // TODO: check paper https://eprint.iacr.org/2016/771.pdf why we should not hash a and b 28 | const generateChallenge = ( 29 | q: BN, 30 | uniqueID: string, 31 | a: BN, 32 | b: BN, 33 | a0: BN, 34 | b0: BN, 35 | a1: BN, 36 | b1: BN 37 | ): BN => { 38 | let c = web3.utils.soliditySha3(uniqueID, a, b, a0, b0, a1, b1) 39 | c = web3.utils.toBN(c) 40 | c = c.mod(q) 41 | 42 | return c 43 | } 44 | 45 | // generate a proof for an encrypted yes vote 46 | // given: 47 | // - cipher (a,b) = (g^r, h^r*g^m) 48 | // - random value r used to encrypt the message 49 | // steps: 50 | // 1. generate fake values c0,f0 for m=0 (random values in Z_q) 51 | // 2. compute fake (a0,b0) = (g^f0 / a^c0, h^f0 / b^c0) 52 | // 3. generate proof for m=1 53 | // 3.1 generate a random value x in Z_q 54 | // 3.2 compute (a1,b1) = (g^x, h^x) 55 | // 4. generate the challenge c 56 | // 5. compute c1 = c - c0 57 | // 6. compute f1 = x + c1 * r (NOTE: mod q!) 58 | export const generateYesProof = ( 59 | cipher: Cipher, 60 | sp: SystemParameters, 61 | pk: BN, 62 | uniqueID: string 63 | ): MembershipProof => { 64 | isCipher(cipher) 65 | isSystemParameters(sp) 66 | 67 | const { a, b, r } = cipher 68 | 69 | const c0 = GlobalHelper.getSecureRandomValue(sp.q) 70 | const f0 = GlobalHelper.getSecureRandomValue(sp.q) 71 | 72 | const a0 = div(pow(sp.g, f0, sp), pow(a, c0, sp), sp) 73 | const b0 = div(pow(pk, f0, sp), pow(b, c0, sp), sp) 74 | 75 | const x = GlobalHelper.getSecureRandomValue(sp.q) 76 | const a1 = pow(sp.g, x, sp) 77 | const b1 = pow(pk, x, sp) 78 | 79 | const c = generateChallenge(sp.q, uniqueID, a, b, a0, b0, a1, b1) 80 | const c1 = add(sp.q, sub(c, c0, sp), sp) 81 | 82 | const f1 = add(x, c1.mul(r as BN).mod(sp.q), sp) 83 | 84 | printConsole && console.log('c0\t\t\t', c0.toNumber()) 85 | printConsole && console.log('f0\t\t\t', f0.toNumber()) 86 | printConsole && console.log('a0\t\t\t', a0.toNumber()) 87 | printConsole && console.log('b0\t\t\t', b0.toNumber()) 88 | printConsole && console.log('x\t\t\t', x.toNumber()) 89 | printConsole && console.log('a1\t\t\t', a1.toNumber()) 90 | printConsole && console.log('b1\t\t\t', b1.toNumber()) 91 | printConsole && console.log('c\t\t', c.toNumber()) 92 | printConsole && console.log('c1 = (q + (c - c0) % q) % q\t', c1.toNumber()) 93 | printConsole && console.log('f1 = x + c1*r\t\t', f1.toNumber()) 94 | printConsole && console.log() 95 | 96 | // TODO: proof is only c0,c1 f0,f1 (recompute a0,a1 and b0,b1 during the verification) 97 | return { a0, a1, b0, b1, c0, c1, f0, f1 } 98 | } 99 | 100 | // generate a proof for an encrypted no vote 101 | // given: 102 | // - cipher (a,b) = (g^r, h^r*g^m) 103 | // - random value r used to encrypt the message 104 | // steps: 105 | // 1. generate fake values c1,f1 for m=1 (random values in Z_q) 106 | // 2. compute fake b_ = b/g 107 | // 3. compute fake (a1,b1) = (g^f1 / a^c1, h^f1 / (b/g)^c1) 108 | // 4. generate proof for m=0 109 | // 4.1 generate a random value x in Z_q 110 | // 4.2 compute (a0,b0) = (g^x, h^x) 111 | // 5. generate the challenge c 112 | // 6. compute c0 = c - c1 113 | // 7. compute f0 = x + c0 * r (NOTE: mod q! = mod n!) 114 | export const generateNoProof = ( 115 | cipher: Cipher, 116 | sp: SystemParameters, 117 | pk: BN, 118 | uniqueID: string 119 | ): MembershipProof => { 120 | isCipher(cipher) 121 | isSystemParameters(sp) 122 | 123 | const { a, b, r } = cipher 124 | 125 | const c1 = GlobalHelper.getSecureRandomValue(sp.q) 126 | const f1 = GlobalHelper.getSecureRandomValue(sp.q) 127 | 128 | const b_ = div(b, sp.g, sp) 129 | 130 | const a1 = div(pow(sp.g, f1, sp), pow(a, c1, sp), sp) 131 | const b1 = div(pow(pk, f1, sp), pow(b_, c1, sp), sp) 132 | 133 | const x = GlobalHelper.getSecureRandomValue(sp.q) 134 | const a0 = pow(sp.g, x, sp) 135 | const b0 = pow(pk, x, sp) 136 | 137 | const c = generateChallenge(sp.q, uniqueID, a, b, a0, b0, a1, b1) 138 | const c0 = add(sp.q, sub(c, c1, sp), sp) 139 | 140 | const f0 = add(x, c0.mul(r as BN).mod(sp.q), sp) 141 | 142 | printConsole && console.log('c0\t\t\t', c0.toNumber()) 143 | printConsole && console.log('f0\t\t\t', f0.toNumber()) 144 | printConsole && console.log('a0\t\t\t', a0.toNumber()) 145 | printConsole && console.log('b0\t\t\t', b0.toNumber()) 146 | printConsole && console.log('x\t\t\t', x.toNumber()) 147 | printConsole && console.log('a1\t\t\t', a1.toNumber()) 148 | printConsole && console.log('b1\t\t\t', b1.toNumber()) 149 | printConsole && console.log('c\t\t', c.toNumber()) 150 | printConsole && console.log('c1 = (q + (c - c0) % q) % q\t', c1.toNumber()) 151 | printConsole && console.log('f1 = x + c1*r\t\t', f1.toNumber()) 152 | printConsole && console.log() 153 | 154 | return { a0, a1, b0, b1, c0, c1, f0, f1 } 155 | } 156 | 157 | // verification g^f0 == a0*a^c0 158 | // verification g^f1 == a1*a^c1 159 | // verification h^f0 == b0 * b^c0 160 | // verification h^f1 == b1 * (b/g)^c1 161 | // recompute the hash and verify 162 | export const verify = ( 163 | cipher: Cipher, 164 | proof: MembershipProof, 165 | sp: SystemParameters, 166 | pk: BN, 167 | uniqueID: string 168 | ): boolean => { 169 | isCipher(cipher) 170 | isSystemParameters(sp) 171 | 172 | const { a, b } = cipher 173 | const { a0, a1, b0, b1, c0, c1, f0, f1 } = proof 174 | 175 | const v1 = GlobalHelper.timingSafeEqualBN(pow(sp.g, f0, sp), mul(a0, pow(a, c0, sp), sp)) 176 | const v2 = GlobalHelper.timingSafeEqualBN(pow(sp.g, f1, sp), mul(a1, pow(a, c1, sp), sp)) 177 | const v3 = GlobalHelper.timingSafeEqualBN(pow(pk, f0, sp), mul(b0, pow(b, c0, sp), sp)) 178 | const v4 = GlobalHelper.timingSafeEqualBN( 179 | pow(pk, f1, sp), 180 | mul(b1, pow(div(b, sp.g, sp), c1, sp), sp) 181 | ) 182 | const v5 = GlobalHelper.timingSafeEqualBN( 183 | c1.add(c0).mod(sp.q), 184 | generateChallenge(sp.q, uniqueID, a, b, a0, b0, a1, b1) 185 | ) 186 | 187 | printConsole && console.log('g^f0 == a0*a^c0:\t', v1) 188 | printConsole && console.log('g^f1 == a1*a^c1\t', v2) 189 | printConsole && console.log('h^f0 == b0*b^c0\t\t', v3) 190 | printConsole && console.log('h^f1 == b1*(b/g)^c1\t', v4) 191 | printConsole && console.log('c == c1 + c0\t\t', v5) 192 | printConsole && console.log() 193 | 194 | return v1 && v2 && v3 && v4 && v5 195 | } 196 | -------------------------------------------------------------------------------- /curves.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let curves = exports 4 | 5 | let hash = require('hash.js') 6 | let curve = require('./curve') 7 | let utils = require('./utils') 8 | 9 | let assert = utils.assert 10 | 11 | function PresetCurve(options) { 12 | if (options.type === 'short') this.curve = new curve.short(options) 13 | else if (options.type === 'edwards') this.curve = new curve.edwards(options) 14 | else this.curve = new curve.mont(options) 15 | this.g = this.curve.g 16 | this.n = this.curve.n 17 | this.hash = options.hash 18 | 19 | assert(this.g.validate(), 'Invalid curve') 20 | assert(this.g.mul(this.n).isInfinity(), 'Invalid curve, G*N != O') 21 | } 22 | curves.PresetCurve = PresetCurve 23 | 24 | function defineCurve(name, options) { 25 | Object.defineProperty(curves, name, { 26 | configurable: true, 27 | enumerable: true, 28 | get: function() { 29 | let curve = new PresetCurve(options) 30 | Object.defineProperty(curves, name, { 31 | configurable: true, 32 | enumerable: true, 33 | value: curve, 34 | }) 35 | return curve 36 | }, 37 | }) 38 | } 39 | 40 | defineCurve('p192', { 41 | type: 'short', 42 | prime: 'p192', 43 | p: 'ffffffff ffffffff ffffffff fffffffe ffffffff ffffffff', 44 | a: 'ffffffff ffffffff ffffffff fffffffe ffffffff fffffffc', 45 | b: '64210519 e59c80e7 0fa7e9ab 72243049 feb8deec c146b9b1', 46 | n: 'ffffffff ffffffff ffffffff 99def836 146bc9b1 b4d22831', 47 | hash: hash.sha256, 48 | gRed: false, 49 | g: [ 50 | '188da80e b03090f6 7cbf20eb 43a18800 f4ff0afd 82ff1012', 51 | '07192b95 ffc8da78 631011ed 6b24cdd5 73f977a1 1e794811', 52 | ], 53 | }) 54 | 55 | defineCurve('p224', { 56 | type: 'short', 57 | prime: 'p224', 58 | p: 'ffffffff ffffffff ffffffff ffffffff 00000000 00000000 00000001', 59 | a: 'ffffffff ffffffff ffffffff fffffffe ffffffff ffffffff fffffffe', 60 | b: 'b4050a85 0c04b3ab f5413256 5044b0b7 d7bfd8ba 270b3943 2355ffb4', 61 | n: 'ffffffff ffffffff ffffffff ffff16a2 e0b8f03e 13dd2945 5c5c2a3d', 62 | hash: hash.sha256, 63 | gRed: false, 64 | g: [ 65 | 'b70e0cbd 6bb4bf7f 321390b9 4a03c1d3 56c21122 343280d6 115c1d21', 66 | 'bd376388 b5f723fb 4c22dfe6 cd4375a0 5a074764 44d58199 85007e34', 67 | ], 68 | }) 69 | 70 | defineCurve('p256', { 71 | type: 'short', 72 | prime: null, 73 | p: 'ffffffff 00000001 00000000 00000000 00000000 ffffffff ffffffff ffffffff', 74 | a: 'ffffffff 00000001 00000000 00000000 00000000 ffffffff ffffffff fffffffc', 75 | b: '5ac635d8 aa3a93e7 b3ebbd55 769886bc 651d06b0 cc53b0f6 3bce3c3e 27d2604b', 76 | n: 'ffffffff 00000000 ffffffff ffffffff bce6faad a7179e84 f3b9cac2 fc632551', 77 | hash: hash.sha256, 78 | gRed: false, 79 | g: [ 80 | '6b17d1f2 e12c4247 f8bce6e5 63a440f2 77037d81 2deb33a0 f4a13945 d898c296', 81 | '4fe342e2 fe1a7f9b 8ee7eb4a 7c0f9e16 2bce3357 6b315ece cbb64068 37bf51f5', 82 | ], 83 | }) 84 | 85 | defineCurve('p384', { 86 | type: 'short', 87 | prime: null, 88 | p: 89 | 'ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ' + 90 | 'fffffffe ffffffff 00000000 00000000 ffffffff', 91 | a: 92 | 'ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ' + 93 | 'fffffffe ffffffff 00000000 00000000 fffffffc', 94 | b: 95 | 'b3312fa7 e23ee7e4 988e056b e3f82d19 181d9c6e fe814112 0314088f ' + 96 | '5013875a c656398d 8a2ed19d 2a85c8ed d3ec2aef', 97 | n: 98 | 'ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff c7634d81 ' + 99 | 'f4372ddf 581a0db2 48b0a77a ecec196a ccc52973', 100 | hash: hash.sha384, 101 | gRed: false, 102 | g: [ 103 | 'aa87ca22 be8b0537 8eb1c71e f320ad74 6e1d3b62 8ba79b98 59f741e0 82542a38 ' + 104 | '5502f25d bf55296c 3a545e38 72760ab7', 105 | '3617de4a 96262c6f 5d9e98bf 9292dc29 f8f41dbd 289a147c e9da3113 b5f0b8c0 ' + 106 | '0a60b1ce 1d7e819d 7a431d7c 90ea0e5f', 107 | ], 108 | }) 109 | 110 | defineCurve('p521', { 111 | type: 'short', 112 | prime: null, 113 | p: 114 | '000001ff ffffffff ffffffff ffffffff ffffffff ffffffff ' + 115 | 'ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ' + 116 | 'ffffffff ffffffff ffffffff ffffffff ffffffff', 117 | a: 118 | '000001ff ffffffff ffffffff ffffffff ffffffff ffffffff ' + 119 | 'ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ' + 120 | 'ffffffff ffffffff ffffffff ffffffff fffffffc', 121 | b: 122 | '00000051 953eb961 8e1c9a1f 929a21a0 b68540ee a2da725b ' + 123 | '99b315f3 b8b48991 8ef109e1 56193951 ec7e937b 1652c0bd ' + 124 | '3bb1bf07 3573df88 3d2c34f1 ef451fd4 6b503f00', 125 | n: 126 | '000001ff ffffffff ffffffff ffffffff ffffffff ffffffff ' + 127 | 'ffffffff ffffffff fffffffa 51868783 bf2f966b 7fcc0148 ' + 128 | 'f709a5d0 3bb5c9b8 899c47ae bb6fb71e 91386409', 129 | hash: hash.sha512, 130 | gRed: false, 131 | g: [ 132 | '000000c6 858e06b7 0404e9cd 9e3ecb66 2395b442 9c648139 ' + 133 | '053fb521 f828af60 6b4d3dba a14b5e77 efe75928 fe1dc127 ' + 134 | 'a2ffa8de 3348b3c1 856a429b f97e7e31 c2e5bd66', 135 | '00000118 39296a78 9a3bc004 5c8a5fb4 2c7d1bd9 98f54449 ' + 136 | '579b4468 17afbd17 273e662c 97ee7299 5ef42640 c550b901 ' + 137 | '3fad0761 353c7086 a272c240 88be9476 9fd16650', 138 | ], 139 | }) 140 | 141 | defineCurve('curve25519', { 142 | type: 'mont', 143 | prime: 'p25519', 144 | p: '7fffffffffffffff ffffffffffffffff ffffffffffffffff ffffffffffffffed', 145 | a: '76d06', 146 | b: '1', 147 | n: '1000000000000000 0000000000000000 14def9dea2f79cd6 5812631a5cf5d3ed', 148 | hash: hash.sha256, 149 | gRed: false, 150 | g: ['9'], 151 | }) 152 | 153 | defineCurve('ed25519', { 154 | type: 'edwards', 155 | prime: 'p25519', 156 | p: '7fffffffffffffff ffffffffffffffff ffffffffffffffff ffffffffffffffed', 157 | a: '-1', 158 | c: '1', 159 | // -121665 * (121666^(-1)) (mod P) 160 | d: '52036cee2b6ffe73 8cc740797779e898 00700a4d4141d8ab 75eb4dca135978a3', 161 | n: '1000000000000000 0000000000000000 14def9dea2f79cd6 5812631a5cf5d3ed', 162 | hash: hash.sha256, 163 | gRed: false, 164 | g: [ 165 | '216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51a', 166 | 167 | // 4/5 168 | '6666666666666666666666666666666666666666666666666666666666666658', 169 | ], 170 | }) 171 | 172 | let pre 173 | try { 174 | pre = require('./precomputed/secp256k1') 175 | } catch (e) { 176 | pre = undefined 177 | } 178 | 179 | defineCurve('secp256k1', { 180 | type: 'short', 181 | prime: 'k256', 182 | p: 'ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffe fffffc2f', 183 | a: '0', 184 | b: '7', 185 | n: 'ffffffff ffffffff ffffffff fffffffe baaedce6 af48a03b bfd25e8c d0364141', 186 | h: '1', 187 | hash: hash.sha256, 188 | 189 | // Precomputed endomorphism 190 | beta: '7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee', 191 | lambda: '5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd72', 192 | basis: [ 193 | { 194 | a: '3086d221a7d46bcde86c90e49284eb15', 195 | b: '-e4437ed6010e88286f547fa90abfe4c3', 196 | }, 197 | { 198 | a: '114ca50f7a8e2f3f657c1108d9d44cfd8', 199 | b: '3086d221a7d46bcde86c90e49284eb15', 200 | }, 201 | ], 202 | 203 | gRed: false, 204 | g: [ 205 | '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', 206 | '483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8', 207 | pre, 208 | ], 209 | }) 210 | 211 | /** 212 | * This is the new curve definition needed for the master project "Blockchain Based eVoting" 213 | */ 214 | defineCurve('curve25519-weier', { 215 | type: 'short', 216 | prime: 'p25519', 217 | p: '7fffffffffffffff ffffffffffffffff ffffffffffffffff ffffffffffffffed', 218 | a: '2aaaaaaaaaaaaaaa aaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaa aaaaaa984914a144', 219 | b: '7b425ed097b425ed 097b425ed097b425 ed097b425ed097b4 260b5e9c7710c864', 220 | n: '1000000000000000 0000000000000000 14def9dea2f79cd6 5812631a5cf5d3ed', 221 | hash: hash.sha256, 222 | gRed: false, 223 | g: [ 224 | '2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad245a', 225 | '20ae19a1b8a086b4e01edd2c7748d14c923d4d7e6d7c61b229e9c5a27eced3d9', 226 | ], 227 | }) 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eVoting Cryptography 2 | 3 | > _A cryptographic library developed for electronic voting using TypeScript. It includes an implementation of the ElGamal cryptosystem using finite fields and elliptic curves combined with non-interactive zero-knowledge proofs of knowledge and distributed key generation._ 4 | > 5 | > _**This material is provided "as is", with absolutely no warranty expressed or implied. Any use is at your own risk.**_ 6 | 7 | ## Overview 8 | 9 | The library uses the following concepts to achieve the listed system properties: 10 | 11 | - **ElGamal Cryptosystem**: Encryption and decryption of votes 12 | - **Homomorphic Encryption**: Counting encrypted votes 13 | - **Distributed Key Generation**: Multi-party asymmetric key generation 14 | - **Non-Interactive Zero-Knowledge Proofs of Knowledge**: Proof of secret key knowledge and vote and decryption correctness 15 | 16 | The library is divided into two parts: [Finite Fields](src/ff-elgamal) and [Elliptic Curves](src/ec-elgamal). Both parts mainly consist of the following modules: 17 | 18 | - `SystemSetup`: 19 | - Key pair generation 20 | - Public key share combination 21 | - `Encryption`: 22 | - Encryption/decryption of votes, 23 | - Homomorphic addition of encrypted votes 24 | - Vote sum decryption with private key shares 25 | - Combining decrypted vote sum shares 26 | - `Voting`: 27 | - Generating yes and no votes 28 | - Tallying (= add and decrypt) votes 29 | - `Proof`: 30 | - `KeyGeneration`: Proof of secret key knowledge 31 | - `Membership`: Proof of membership of a vote (0 or 1) 32 | - `Decryption`: Proof of correct decryption of the sum 33 | 34 | If you'd like to have a more detailed explanation of the cryptography (encryption & proofs) used in this library (project), please have a look at our [report](report.pdf). 35 | 36 | ## Cryptographic Building Blocks [1] 37 | 38 | ### ElGamal 39 | 40 | The ElGamal cryptosystem [2] is a public key cryptosystem defined over cyclic groups and is based on the difficulty of finding the discrete logarithm. This library uses cyclic groups and their modulo operations. To make the system safe, it uses a multiplicative prime-order group ZP* where `p = 2*q + 1` and `q` are both prime numbers and `p` needs to be chosen very large. 41 | 42 | The following values are used within the system: 43 | 44 | - `SystemParameters`: 45 | - a prime number `p` 46 | - a prime number `q = (p - 1) / 2` (order of the group) 47 | - a group's generator `g` 48 | - `KeyPair`: 49 | - a secret key `sk`: `0 < r < q` 50 | - a public key `h = g^sk mod p` 51 | - `Cipher`: formed of `a` and `b` 52 | 53 | #### Encryption 54 | 55 | 1. Pick a random value `r`: `0 < r < q` 56 | 2. Compute `a = g^r mod p` 57 | 3. Compute `s = h^r mod p` 58 | 4. Encode the message `m`: `mh = g^m mod p` 59 | 5. Compute `b = s*mh mod p` 60 | 61 | => Cipher (`a`, `b`) 62 | 63 | #### Decryption 64 | 65 | 1. Compute `s = a^sk mod p` 66 | 2. Compute `s^(-1)` (multiplicative inverse of s) 67 | 3. Compute `mh = b * s^(-1)` 68 | 4. Decode `mh` using brute force 69 | 70 | => Plaintext `m` 71 | 72 | ### Homomorphic Encryption 73 | 74 | Homomorphic encryption is used to perform computations on ciphertexts (here for addition) as if the computation was executed on plaintexts. This is the reason why `g^m` is used instead of just `m` during the encryption (encoding step). 75 | 76 | The encrypted message is created as before (see 1). Due to 2, encrypted messages can be added as show in 3. 77 | 78 | 1. `E(m) = (a, b) = (g^r, h^r * g^m)` 79 | 2. `E(m1) * E(m2) = (g^(r1+r2), h^(r1+r2) * g^(m1+m2)) = E(m1 + m2)` 80 | 3. `E(m3) = E(m1 + m2) = (E(m1)_a * E(m2)_a, E(m1)_b * E(m2)_b)` 81 | 82 | ### Distributed Key Generation/Decryption 83 | 84 | As shown above, the library can be used with a single key pair `(sk, h)`. To use the system with not only one but multiple parties (each party `i` having its own key pair `(sk_i, h_i)`), the key generation and decryption algorithms need to be adjusted. The encryption step however stays the same. 85 | 86 | This library uses a setting where `n` out of `n` keys need to be present for decrypting a cipher [9]. To create a key pair, each party `1 <= i <= n` individually picks a secret key and generates the public key as shown before (see 1). All the public keys are then combined to one public key used for encryption (see 2). To decrypt a cipher, two steps are needed. First, each party uses its private key `sk_i` to create a decrypted share `d_i` (see 3). Then, these shares are combined to get the plaintext `m` (see 4). 87 | 88 | 1. `(sk_i, h_i = g^sk_i mod p)` 89 | 2. `h = h_1 * ... * h_i * ... * h_n mod p` 90 | 3. `d_i = a^sk_i mod p` with cipher `(a, b)` 91 | 4. `m = b / (d_1 * ... * d_i * ... * d_n) mod p` (where `x/y = x*y^-1` with `y^-1` being the multiplicative inverse of `y`) 92 | 93 | ### Non-Interactive Zero-Knowledge Proofs of Knowledge 94 | 95 | To convince a verifier that a prover knows some secret without revealing the actual secret, a zero-knowledge proof of knowledge can be used. This requires to follow special sigma protocols which include some (three-move) interactions between the verifier and the prover where the prover makes a commitment and the verifier answers with some random challenge the prover needs to respond to. 96 | 97 | Such interactive proof systems can be made non-interactive by applying the Fiat-Shamir heuristic [3] by using a cryptographic hash function as a random oracle for computing the challenge of the verifier. According to [4], the Fiat-Shamir transformation is "weak" (e.g., under certain circumstances, a proof might be verified correctly even the initial commitment was tapered with) when only the commitment is hashed (as described in [5]). However, it is considered "strong" [4], if the statement to be proved is also hashed (as suggested in [5, 6]). Thus, this library hashes both the statement to be proved and the commitment when generating the challenge. 98 | 99 | The Fiat-Shamir transformation is applied to the Schnorr [6], Chaum Pedersen [7], and Disjuntive Chaum-Pedersen protocols as depicted in the following sections. 100 | 101 | #### Key Generation: Schnorr Proof 102 | 103 | After the (distributed) key generation as described above, the Schnorr Proof [8] is used to prove that a party knows the corresponding secret key `sk` to the published public key `h = g^sk`. It is a proof of knowledge of a discrete logarithm of `sk = log_g(g^sk)`. 104 | 105 | #### Decryption: Chaum-Pedersen Proof 106 | 107 | The Chaum-Pedersen Proof [7] is used for proving that the decryption (`m = (a^sk)^-1 * b` with cipher (`a`, `b`)) was done using the corresponding private key `sk` to the public key `h = g^sk` used for the encryption. It is a proof of discrete logarithm equality of `log_g(g^sk) = log_h(h^r)`. 108 | 109 | #### Membership: Disjunctive Chaum-Pedersen Proof 110 | 111 | The Disjunctive Chaum-Pedersen Proof is used for proving that one out of two statements is true without revealing which one is correct. Here, this proof is used to prove that an encrypted vote (0 or 1) is either 0 or 1 while not revealing the vote's actual value. 112 | 113 | ## Implementation 114 | 115 | The respective implementations of the homomorphic ElGamal cryptosystem using distributed keys and non-interactive zero-knowledge proofs of knowledge can be found here: [Finite Fields](src/ff-elgamal) and [Elliptic Curves](src/ec-elgamal) 116 | 117 | **Important for the Elliptic Curve Implementation**: 118 | 119 | `src/ec-elgamal` uses the **curve25519** in Weierstrass form, which is not yet supported by the `elliptic` package used in this project to operate on elliptic curves. Since the required pull request has not been merged yet, this curve is manually added to the elliptic library. 120 | 121 | This is done via the script `copyCustomCurve.sh`. **You should not have to run this manually**. 122 | 123 | `npm install` will automatically run `copyCustomCurve.sh` in it's `"postinstall"` task. 124 | 125 | ## Publishing the Library 126 | 127 | ### NPM & GitHub Packages 128 | 129 | #### Authenticating to GitHub Packages 130 | 131 | 1. You need a personal access token to publish, install, and delete packages in GitHub Packages. The personal access token requires the following scopes: `read:packages, write:packages`. 132 | 133 | 2. You can authenticate to GitHub Packages with npm by either editing your per-user `~/.npmrc` file to include your personal access token or by logging in to npm on the command line using your username and personal access token. 134 | 135 | 2.1. To authenticate with your personal access token include the following line in your `~/.npmrc` file: 136 | `//npm.pkg.github.com/:\_authToken=TOKEN` 137 | 138 | 2.2. To authenticate by logging in to npm, use the npm login command: 139 | 140 | `npm login --registry=https://npm.pkg.github.com` 141 | 142 | #### Publishing a Package 143 | 144 | By default, GitHub Packages publishes a package in the GitHub repository you specify in the name field of the `package.json` file. For example, you would publish this package named `@meck93/evote-crypto` to the meck93/evote-crypto GitHub repository. 145 | 146 | 1. Make sure the name in the `package.json` is the same as on Github `@OWNER/repo` 147 | 2. Add the following line to your `package.json`. 148 | 149 | 2.1. HTTPS: `"repository": "git@github.com:meck93/evote-crypto.git"` 150 | 151 | 2.2. SSH: `"repository": "https://github.com/meck93/evote-crypto.git"` 152 | 153 | 3. Create a new version using `npm version [ | major | minor | patch] -m "Upgarde to %s for...` 154 | 155 | - The `%s` will be automatically replace with the new version. 156 | - A git commit and tag for the new version will be created automatically. 157 | - **Note.** The command will only work if the repository is clean. No uncommited changes. 158 | 159 | 4. Publish the package: `npm publish` 160 | 161 | ## References 162 | 163 | [1] Ronald Cramer, Rosario Gennaro, Berry Schoenmakers: **A secure and optimally efficient multi-authority election scheme.** European Transactions on Telecommunications 8(5): 481-490 (1997) - [PDF](https://www.win.tue.nl/~berry/papers/euro97.pdf), [dblp](https://dblp.org/rec/journals/ett/CramerGS97.html) 164 | 165 | [2] Taher El Gamal: **A public key cryptosystem and a signature scheme based on discrete logarithms.** IEEE Trans. Information Theory 31(4): 469-472 (1985) - [PDF](https://caislab.kaist.ac.kr/lecture/2010/spring/cs548/basic/B02.pdf), [dblp](https://dblp.uni-trier.de/rec/html/journals/tit/Elgamal85) 166 | 167 | [3] Amos Fiat, Adi Shamir: **How to Prove Yourself: Practical Solutions to Identification and Signature Problems.** CRYPTO 1986: 186-194 - [PDF](https://link.springer.com/content/pdf/10.1007%2F3-540-47721-7_12.pdf), [dblp](https://dblp.uni-trier.de/rec/html/conf/crypto/FiatS86) 168 | 169 | [4] David Bernhard, Olivier Pereira, Bogdan Warinschi: **How not to Prove Yourself: Pitfalls of the Fiat-Shamir Heuristic and Applications to Helios.** IACR Cryptology ePrint Archive 2016: 771 (2016) - [PDF](https://eprint.iacr.org/2016/771.pdf), [dblp](https://dblp.org/rec/journals/iacr/BernhardPW16) 170 | 171 | [5] Mihir Bellare, Phillip Rogaway: **Random Oracles are Practical: A Paradigm for Designing Efficient Protocols.** ACM Conference on Computer and Communications Security 1993: 62-73 - [PDF](https://cseweb.ucsd.edu/~mihir/papers/ro.pdf), [dblp]( https://dblp.org/rec/conf/ccs/BellareR93) 172 | 173 | [6] Claus-Peter Schnorr: **Efficient Signature Generation by Smart Cards.** J. Cryptology 4(3): 161-174 (1991) - [PDF](https://www.researchgate.net/profile/Claus_Schnorr/publication/227088517_Efficient_signature_generation_by_smart_cards/links/0046353849579ce09c000000/Efficient-signature-generation-by-smart-cards.pdf), [dblp](ttps://dblp.org/rec/journals/joc/Schnorr91) 174 | 175 | [7] David Chaum, Torben P. Pedersen: **Wallet Databases with Observers.** CRYPTO 1992: 89-105 - [PDF](https://www.chaum.com/publications/Wallet_Databases.pdf), [dblp](https://dblp.org/rec/conf/crypto/ChaumP92) 176 | 177 | [8] Feng Hao: **Schnorr Non-interactive Zero-Knowledge Proof.** RFC 8235: 1-13 (2017) - [RFC](https://tools.ietf.org/html/rfc8235), [PDF](https://tools.ietf.org/pdf/rfc8235.pdf), [dblp](https://dblp.org/rec/journals/rfc/rfc8235) 178 | 179 | [9] David Bernhard, Bogdan Warinschi: **Cryptographic Voting - A Gentle Introduction.** IACR Cryptology ePrint Archive 2016: 765 (2016) - [PDF](https://eprint.iacr.org/2016/765.pdf), [dblp](https://dblp.org/rec/journals/iacr/BernhardW16) 180 | 181 | ## Authors 182 | 183 | - **Moritz Eck** - [meck93](https://github.com/meck93) 184 | - **Alex Scheitlin** - [alexscheitlin](https://github.com/alexscheitlin) 185 | - **Nik Zaugg** - [nikzaugg](https://github.com/nikzaugg) 186 | 187 | ## License 188 | 189 | This project is licensed under the [MIT License](LICENSE). 190 | -------------------------------------------------------------------------------- /test/helper.spec.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | import { expect } from 'chai' 3 | 4 | import { GlobalHelper } from '../src/index' 5 | 6 | describe('Global Helper Test', () => { 7 | it('should create a new BN', () => { 8 | expect(GlobalHelper.newBN(5, 2).eq(new BN(5, 2))).to.be.true 9 | expect(GlobalHelper.newBN(5, 10).eq(new BN(5, 10))).to.be.true 10 | expect(GlobalHelper.newBN(5).eq(new BN(5, 10))).to.be.true 11 | }) 12 | 13 | it('should invert BNs', () => { 14 | const base = 10 15 | const modulus = 4 16 | 17 | // inverse a mod modulus = c 18 | const tests = [ 19 | { a: 0, c: 0 }, // none 20 | { a: 1, c: 1 }, 21 | { a: 2, c: 1 }, // none 22 | { a: 3, c: 3 }, 23 | { a: 4, c: 0 }, // none 24 | { a: 5, c: 1 }, 25 | { a: 6, c: 1 }, // none 26 | { a: 7, c: 3 }, 27 | { a: 8, c: 0 }, // none 28 | { a: 9, c: 1 }, 29 | ] 30 | 31 | for (const test of tests) { 32 | const a = new BN(test.a, base) 33 | const c = new BN(test.c, base) 34 | const result = GlobalHelper.invmBN(a, new BN(modulus, base)) 35 | 36 | const log = false 37 | log && console.log('a:', a.toNumber(), ', c:', c.toNumber(), 'res:', result.toNumber()) 38 | expect(result.eq(c)).to.be.true 39 | } 40 | }) 41 | 42 | it('should add BNs', () => { 43 | const base = 10 44 | const modulus = 4 45 | 46 | // a + b = c 47 | const tests = [ 48 | { a: 0, b: 0, c: 0 }, 49 | 50 | { a: 1, b: 0, c: 1 }, 51 | { a: 1, b: 1, c: 2 }, 52 | 53 | { a: 2, b: 0, c: 2 }, 54 | { a: 2, b: 1, c: 3 }, 55 | { a: 2, b: 2, c: 0 }, 56 | 57 | { a: 3, b: 0, c: 3 }, 58 | { a: 3, b: 1, c: 0 }, 59 | { a: 3, b: 2, c: 1 }, 60 | { a: 3, b: 3, c: 2 }, 61 | 62 | { a: 4, b: 0, c: 0 }, 63 | { a: 4, b: 1, c: 1 }, 64 | { a: 4, b: 2, c: 2 }, 65 | { a: 4, b: 3, c: 3 }, 66 | { a: 4, b: 4, c: 0 }, 67 | ] 68 | 69 | for (const test of tests) { 70 | const a = new BN(test.a, base) 71 | const b = new BN(test.b, base) 72 | const c = new BN(test.c, base) 73 | const result = GlobalHelper.addBN(a, b, new BN(modulus, base)) 74 | 75 | const log = false 76 | log && 77 | console.log( 78 | 'a:', 79 | a.toNumber(), 80 | ', b:', 81 | b.toNumber(), 82 | ', c:', 83 | c.toNumber(), 84 | 'res:', 85 | result.toNumber() 86 | ) 87 | expect(result.eq(c)).to.be.true 88 | } 89 | }) 90 | 91 | it('should subtract BNs', () => { 92 | const base = 10 93 | const modulus = 4 94 | 95 | // a - b = c 96 | const tests = [ 97 | { a: 0, b: 0, c: 0 }, 98 | 99 | { a: 1, b: 0, c: 1 }, 100 | { a: 1, b: 1, c: 0 }, 101 | 102 | { a: 2, b: 0, c: 2 }, 103 | { a: 2, b: 1, c: 1 }, 104 | { a: 2, b: 2, c: 0 }, 105 | 106 | { a: 3, b: 0, c: 3 }, 107 | { a: 3, b: 1, c: 2 }, 108 | { a: 3, b: 2, c: 1 }, 109 | { a: 3, b: 3, c: 0 }, 110 | 111 | { a: 4, b: 0, c: 0 }, 112 | { a: 4, b: 1, c: 3 }, 113 | { a: 4, b: 2, c: 2 }, 114 | { a: 4, b: 3, c: 1 }, 115 | { a: 4, b: 4, c: 0 }, 116 | ] 117 | 118 | for (const test of tests) { 119 | const a = new BN(test.a, base) 120 | const b = new BN(test.b, base) 121 | const c = new BN(test.c, base) 122 | const result = GlobalHelper.subBN(a, b, new BN(modulus, base)) 123 | 124 | const log = false 125 | log && 126 | console.log( 127 | 'a:', 128 | a.toNumber(), 129 | ', b:', 130 | b.toNumber(), 131 | ', c:', 132 | c.toNumber(), 133 | 'res:', 134 | result.toNumber() 135 | ) 136 | expect(result.eq(c)).to.be.true 137 | } 138 | }) 139 | 140 | it('should multiply BNs', () => { 141 | const base = 10 142 | const modulus = 4 143 | 144 | // a * b = c 145 | const tests = [ 146 | { a: 0, b: 0, c: 0 }, 147 | 148 | { a: 1, b: 0, c: 0 }, 149 | { a: 1, b: 1, c: 1 }, 150 | 151 | { a: 2, b: 0, c: 0 }, 152 | { a: 2, b: 1, c: 2 }, 153 | { a: 2, b: 2, c: 0 }, 154 | 155 | { a: 3, b: 0, c: 0 }, 156 | { a: 3, b: 1, c: 3 }, 157 | { a: 3, b: 2, c: 2 }, 158 | { a: 3, b: 3, c: 1 }, 159 | 160 | { a: 4, b: 0, c: 0 }, 161 | { a: 4, b: 1, c: 0 }, 162 | { a: 4, b: 2, c: 0 }, 163 | { a: 4, b: 3, c: 0 }, 164 | { a: 4, b: 4, c: 0 }, 165 | ] 166 | 167 | for (const test of tests) { 168 | const a = new BN(test.a, base) 169 | const b = new BN(test.b, base) 170 | const c = new BN(test.c, base) 171 | const result = GlobalHelper.mulBN(a, b, new BN(modulus, base)) 172 | 173 | const log = false 174 | log && 175 | console.log( 176 | 'a:', 177 | a.toNumber(), 178 | ', b:', 179 | b.toNumber(), 180 | ', c:', 181 | c.toNumber(), 182 | 'res:', 183 | result.toNumber() 184 | ) 185 | expect(result.eq(c)).to.be.true 186 | } 187 | }) 188 | 189 | it('should divide BNs', () => { 190 | const base = 10 191 | const modulus = 4 192 | 193 | // a / b = c 194 | const tests = [ 195 | { a: 0, b: 0, c: 0 }, // none 196 | 197 | { a: 1, b: 0, c: 0 }, // none 198 | { a: 1, b: 1, c: 1 }, 199 | 200 | { a: 2, b: 0, c: 0 }, // none 201 | { a: 2, b: 1, c: 2 }, 202 | { a: 2, b: 2, c: 2 }, 203 | 204 | { a: 3, b: 0, c: 0 }, // none 205 | { a: 3, b: 1, c: 3 }, 206 | { a: 3, b: 2, c: 3 }, 207 | { a: 3, b: 3, c: 1 }, 208 | 209 | { a: 4, b: 0, c: 0 }, // none 210 | { a: 4, b: 1, c: 0 }, // none 211 | { a: 4, b: 2, c: 0 }, // none 212 | { a: 4, b: 3, c: 0 }, // none 213 | { a: 4, b: 4, c: 0 }, // none 214 | ] 215 | 216 | for (const test of tests) { 217 | const a = new BN(test.a, base) 218 | const b = new BN(test.b, base) 219 | const c = new BN(test.c, base) 220 | const result = GlobalHelper.divBN(a, b, new BN(modulus, base)) 221 | 222 | const log = false 223 | log && 224 | console.log( 225 | 'a:', 226 | a.toNumber(), 227 | ', b:', 228 | b.toNumber(), 229 | ', c:', 230 | c.toNumber(), 231 | 'res:', 232 | result.toNumber() 233 | ) 234 | expect(result.eq(c)).to.be.true 235 | } 236 | }) 237 | 238 | it('should exponentiate BNs', () => { 239 | const base = 10 240 | const modulus = 4 241 | 242 | // a^b = c 243 | const tests = [ 244 | { a: 0, b: 0, c: 1 }, 245 | 246 | { a: 1, b: 0, c: 1 }, 247 | { a: 1, b: 1, c: 1 }, 248 | 249 | { a: 2, b: 0, c: 1 }, 250 | { a: 2, b: 1, c: 2 }, 251 | { a: 2, b: 2, c: 0 }, 252 | 253 | { a: 3, b: 0, c: 1 }, 254 | { a: 3, b: 1, c: 3 }, 255 | { a: 3, b: 2, c: 1 }, 256 | { a: 3, b: 3, c: 3 }, 257 | 258 | { a: 4, b: 0, c: 1 }, 259 | { a: 4, b: 1, c: 0 }, 260 | { a: 4, b: 2, c: 0 }, 261 | { a: 4, b: 3, c: 0 }, 262 | { a: 4, b: 4, c: 0 }, 263 | ] 264 | 265 | for (const test of tests) { 266 | const a = new BN(test.a, base) 267 | const b = new BN(test.b, base) 268 | const c = new BN(test.c, base) 269 | const result = GlobalHelper.powBN(a, b, new BN(modulus, base)) 270 | 271 | const log = false 272 | log && 273 | console.log( 274 | 'a:', 275 | a.toNumber(), 276 | ', b:', 277 | b.toNumber(), 278 | ', c:', 279 | c.toNumber(), 280 | 'res:', 281 | result.toNumber() 282 | ) 283 | expect(result.eq(c)).to.be.true 284 | } 285 | }) 286 | 287 | it('should compute the number of bytes needed to store a number', () => { 288 | const numbers: number[] = [31, 32, 254, 255, 256, 511, 512] 289 | const bytes: number[] = [1, 1, 1, 1, 2, 2, 3] 290 | 291 | numbers.map((nr, idx) => { 292 | const numberOfBytes = GlobalHelper.getByteSizeForDecimalNumber(new BN(nr, 10)) 293 | expect(numberOfBytes.eq(new BN(bytes[idx], 10))).to.be.true 294 | }) 295 | }) 296 | 297 | it('should generate random values', () => { 298 | for (let a = 0; a < 100; a++) { 299 | const rnd = GlobalHelper.getSecureRandomValue(new BN(5, 10)).toNumber() 300 | expect(rnd).to.be.at.least(1) 301 | expect(rnd).to.be.at.most(4) 302 | } 303 | }) 304 | 305 | // ------------------------------------------------- 306 | // timing safe equality 307 | // ------------------------------------------------- 308 | const timingLog = false 309 | 310 | // Returns the mean of an array 311 | const mean = (array: number[]): number => { 312 | return array.reduce((sum, val) => sum + val, 0) / array.length 313 | } 314 | 315 | // Returns the sample standard deviation of an array 316 | const standardDeviation = (array: number[]): number => { 317 | const arrMean = mean(array) 318 | const total = array.reduce((sum, val) => sum + Math.pow(val - arrMean, 2), 0) 319 | return Math.sqrt(total / (array.length - 1)) 320 | } 321 | 322 | // Returns the common standard deviation of two arrays 323 | const combinedStandardDeviation = (arr1: number[], arr2: number[]): number => { 324 | const sum1 = Math.pow(standardDeviation(arr1), 2) * (arr1.length - 1) 325 | const sum2 = Math.pow(standardDeviation(arr2), 2) * (arr2.length - 1) 326 | return Math.sqrt((sum1 + sum2) / (arr1.length + arr2.length - 2)) 327 | } 328 | 329 | // Filter large outliers from an array. A 'large outlier' is a value that is at 330 | // least 50 times larger than the mean. This prevents the tests from failing 331 | // due to the standard deviation increase when a function unexpectedly takes 332 | // a very long time to execute. 333 | const filterOutliers = (array: number[]): number[] => { 334 | const arrMean = mean(array) 335 | return array.filter(value => value / arrMean < 50) 336 | } 337 | 338 | const safeEqualityCheck = (a: Buffer, b: Buffer, equalInputs: boolean): number => { 339 | const startTime = process.hrtime() 340 | const equalityCheck: boolean = GlobalHelper.timingSafeEqual(a, b) 341 | const endTime = process.hrtime(startTime) 342 | 343 | if (equalInputs) { 344 | expect(equalityCheck).to.be.true 345 | } else { 346 | expect(equalityCheck).to.be.false 347 | } 348 | 349 | const diff = (1e9 * endTime[0] + endTime[1]) / 1e9 350 | //console.log('execution A. time:', diff) 351 | return diff 352 | } 353 | 354 | const unsafeEqualityCheck = (a: Buffer, b: Buffer, equalInputs: boolean): number => { 355 | const startTime = process.hrtime() 356 | const equalityCheck: boolean = a.equals(b) 357 | const endTime = process.hrtime(startTime) 358 | 359 | if (equalInputs) { 360 | expect(equalityCheck).to.be.true 361 | } else { 362 | expect(equalityCheck).to.be.false 363 | } 364 | 365 | const diff = (1e9 * endTime[0] + endTime[1]) / 1e9 366 | //console.log('execution A. time:', diff) 367 | return diff 368 | } 369 | 370 | const benchmark = ( 371 | equalityCheck: (a: Buffer, b: Buffer, equalInputs: boolean) => number 372 | ): number => { 373 | const numberOfTrials = 10000 374 | const bufferSize = 64 375 | 376 | const equalResults: number[] = Array(numberOfTrials) 377 | const unequalResults: number[] = Array(numberOfTrials) 378 | 379 | for (let i = 0; i < numberOfTrials; i++) { 380 | if (i % 2 == 0) { 381 | const bufferA1: Buffer = Buffer.alloc(bufferSize, 'A', 'utf8') 382 | const bufferB: Buffer = Buffer.alloc(bufferSize, 'B', 'utf8') 383 | const bufferA2: Buffer = Buffer.alloc(bufferSize, 'A', 'utf8') 384 | const bufferC: Buffer = Buffer.alloc(bufferSize, 'C', 'utf8') 385 | 386 | equalResults[i] = equalityCheck(bufferA1, bufferA2, true) 387 | unequalResults[i] = equalityCheck(bufferB, bufferC, false) 388 | } else { 389 | // Swap the order of the benchmarks every second iteration, to avoid any patterns caused by memory usage. 390 | const bufferA2: Buffer = Buffer.alloc(bufferSize, 'A', 'utf8') 391 | const bufferC: Buffer = Buffer.alloc(bufferSize, 'C', 'utf8') 392 | const bufferA1: Buffer = Buffer.alloc(bufferSize, 'A', 'utf8') 393 | const bufferB: Buffer = Buffer.alloc(bufferSize, 'B', 'utf8') 394 | 395 | equalResults[i] = equalityCheck(bufferA1, bufferA2, true) 396 | unequalResults[i] = equalityCheck(bufferB, bufferC, false) 397 | } 398 | } 399 | 400 | const equalBenches = filterOutliers(equalResults) 401 | const unequalBenches = filterOutliers(unequalResults) 402 | 403 | // Use a two-sample t-test to determine whether the timing difference between 404 | // the benchmarks is statistically significant. 405 | // https://wikipedia.org/wiki/Student%27s_t-test#Independent_two-sample_t-test 406 | 407 | const equalMean = mean(equalBenches) 408 | const unequalMean = mean(unequalBenches) 409 | 410 | const equalLen = equalBenches.length 411 | const unequalLen = unequalBenches.length 412 | 413 | const combinedStd = combinedStandardDeviation(equalBenches, unequalBenches) 414 | const standardErr = combinedStd * Math.sqrt(1 / equalLen + 1 / unequalLen) 415 | 416 | return (equalMean - unequalMean) / standardErr 417 | } 418 | 419 | it('should perform timing safe equality checks - benchmark', () => { 420 | // t_(0.99995, ∞) 421 | // i.e. If a given comparison function is indeed timing-safe, the t-test result 422 | // has a 99.99% chance to be below this threshold. Unfortunately, this means 423 | // that this test will be a bit flakey and will fail 0.01% of the time even if 424 | // crypto.timingSafeEqual is working properly. 425 | // t-table ref: http://www.sjsu.edu/faculty/gerstman/StatPrimer/t-table.pdf 426 | // Note that in reality there are roughly `2 * numTrials - 2` degrees of 427 | // freedom, not ∞. However, assuming `numTrials` is large, this doesn't 428 | // significantly affect the threshold. 429 | const T_THRESHOLD = 3.892 430 | 431 | const tValueSafe = benchmark(safeEqualityCheck) 432 | timingLog && console.log('Safe T-Value:\t', Math.abs(tValueSafe)) 433 | expect(Math.abs(tValueSafe) < T_THRESHOLD).to.be.true 434 | 435 | // As a sanity check to make sure the statistical tests are working, run the 436 | // same benchmarks again, this time with an unsafe comparison function. In this 437 | // case the t-value should be above the threshold. 438 | // const unsafeCompare = (bufA, bufB) => bufA.equals(bufB) 439 | // const t2 = getTValue(unsafeCompare) 440 | // t.ok( 441 | // Math.abs(t2) > T_THRESHOLD, 442 | // `Buffer#equals should leak information from its execution time (t=${t2})` 443 | // ) 444 | const tValueUnsafe = benchmark(unsafeEqualityCheck) 445 | timingLog && console.log('Unsafe T-Value:\t', Math.abs(tValueUnsafe)) 446 | }) 447 | 448 | it('should perform timing safe BN equality checks', () => { 449 | // check that it is the same 450 | const a1 = new BN('abcd12345abcdabcd12345abcdabcd12345abcd', 'hex') 451 | const b1 = new BN('abcd12345abcdabcd12345abcdabcd12345abcd', 'hex') 452 | const isEqual1 = GlobalHelper.timingSafeEqualBN(a1, b1) 453 | expect(isEqual1).to.be.true 454 | 455 | // check that it is not the same 456 | const a2 = new BN('abcd12345abcdabcd12345abcdabcd12345abcd', 'hex') 457 | const b2 = new BN('abcd12345abcdabcd12345abcdabcd12345abc1', 'hex') 458 | const isEqual2 = GlobalHelper.timingSafeEqualBN(a2, b2) 459 | expect(isEqual2).to.be.false 460 | }) 461 | }) 462 | --------------------------------------------------------------------------------