├── .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 |
--------------------------------------------------------------------------------