threshold-elgamal 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +10 -0
- package/.gitattributes +1 -0
- package/.nvmrc +1 -0
- package/.prettierignore +3 -0
- package/.vscode/extensions.json +7 -0
- package/.vscode/settings.json +50 -0
- package/LICENSE +7 -0
- package/README.md +287 -0
- package/docs/.nojekyll +1 -0
- package/docs/README.md +289 -0
- package/docs/modules.md +381 -0
- package/eslint.config.js +161 -0
- package/package.json +82 -0
- package/src/constants.ts +44 -0
- package/src/elgamal.test.ts +42 -0
- package/src/elgamal.ts +85 -0
- package/src/index.ts +19 -0
- package/src/testUtils.ts +177 -0
- package/src/thresholdElgamal.test.ts +289 -0
- package/src/thresholdElgamal.ts +157 -0
- package/src/types/random-bigint.d.ts +13 -0
- package/src/types.ts +21 -0
- package/src/utils.ts +61 -0
- package/tsconfig.build.json +17 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { GROUPS } from './constants';
|
|
4
|
+
import { generateParameters, encrypt, decrypt } from './elgamal';
|
|
5
|
+
import { multiplyEncryptedValues } from './utils';
|
|
6
|
+
|
|
7
|
+
describe('ElGamal: ', () => {
|
|
8
|
+
Object.entries(GROUPS).forEach(([groupName, groupInfo]) => {
|
|
9
|
+
const { primeBits, prime, generator } = groupInfo;
|
|
10
|
+
const { publicKey, privateKey } = generateParameters(primeBits);
|
|
11
|
+
|
|
12
|
+
it(`${primeBits}-bit encryption and decryption using ${groupName}`, () => {
|
|
13
|
+
const secret = 42;
|
|
14
|
+
const encryptedMessage = encrypt(
|
|
15
|
+
secret,
|
|
16
|
+
prime,
|
|
17
|
+
generator,
|
|
18
|
+
publicKey,
|
|
19
|
+
);
|
|
20
|
+
const decryptedMessage = decrypt(
|
|
21
|
+
encryptedMessage,
|
|
22
|
+
prime,
|
|
23
|
+
privateKey,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(decryptedMessage).toBe(secret);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('homomorphic multiplication', () => {
|
|
30
|
+
const m1 = 12;
|
|
31
|
+
const m2 = 13;
|
|
32
|
+
const m1m2 = m1 * m2;
|
|
33
|
+
|
|
34
|
+
const e1 = encrypt(m1, prime, generator, publicKey);
|
|
35
|
+
const e2 = encrypt(m2, prime, generator, publicKey);
|
|
36
|
+
const e1e2 = multiplyEncryptedValues(e1, e2, prime);
|
|
37
|
+
const decryptedMessage = decrypt(e1e2, prime, privateKey);
|
|
38
|
+
|
|
39
|
+
expect(decryptedMessage).toBe(m1m2);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
package/src/elgamal.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { modPow, modInv } from 'bigint-mod-arith';
|
|
2
|
+
|
|
3
|
+
import { GROUPS } from './constants';
|
|
4
|
+
import type { EncryptedMessage, Parameters } from './types';
|
|
5
|
+
import { getRandomBigIntegerInRange } from './utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generates the parameters for the ElGamal encryption, including the prime, generator,
|
|
9
|
+
* and key pair (public and private keys).
|
|
10
|
+
*
|
|
11
|
+
* @param {2048 | 3072 | 4096} primeBits - The bit length for the prime number. Supports 2048, 3072, or 4096 bits.
|
|
12
|
+
* @returns {Parameters} The generated parameters including the prime, generator, publicKey, and privateKey.
|
|
13
|
+
*/
|
|
14
|
+
export const generateParameters = (
|
|
15
|
+
primeBits: 2048 | 3072 | 4096 = 2048,
|
|
16
|
+
): Parameters => {
|
|
17
|
+
let prime: bigint;
|
|
18
|
+
let generator: bigint;
|
|
19
|
+
|
|
20
|
+
switch (primeBits) {
|
|
21
|
+
case 2048:
|
|
22
|
+
prime = BigInt(GROUPS.ffdhe2048.prime);
|
|
23
|
+
generator = BigInt(GROUPS.ffdhe2048.generator);
|
|
24
|
+
break;
|
|
25
|
+
case 3072:
|
|
26
|
+
prime = BigInt(GROUPS.ffdhe3072.prime);
|
|
27
|
+
generator = BigInt(GROUPS.ffdhe3072.generator);
|
|
28
|
+
break;
|
|
29
|
+
case 4096:
|
|
30
|
+
prime = BigInt(GROUPS.ffdhe4096.prime);
|
|
31
|
+
generator = BigInt(GROUPS.ffdhe4096.generator);
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
throw new Error('Unsupported bit length');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const privateKey = getRandomBigIntegerInRange(2n, prime - 1n);
|
|
38
|
+
const publicKey = modPow(generator, privateKey, prime);
|
|
39
|
+
|
|
40
|
+
return { prime, generator, publicKey, privateKey };
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Encrypts a secret using ElGamal encryption.
|
|
44
|
+
*
|
|
45
|
+
* @param {number} secret - The secret to be encrypted.
|
|
46
|
+
* @param {bigint} prime - The prime number used in the encryption system.
|
|
47
|
+
* @param {bigint} generator - The generator used in the encryption system.
|
|
48
|
+
* @param {bigint} publicKey - The public key used for encryption.
|
|
49
|
+
* @returns {EncryptedMessage} The encrypted secret, consisting of two BigIntegers (c1 and c2).
|
|
50
|
+
*/
|
|
51
|
+
export const encrypt = (
|
|
52
|
+
secret: number,
|
|
53
|
+
prime: bigint,
|
|
54
|
+
generator: bigint,
|
|
55
|
+
publicKey: bigint,
|
|
56
|
+
): EncryptedMessage => {
|
|
57
|
+
if (secret >= Number(prime)) {
|
|
58
|
+
throw new Error('Message is too large for direct encryption');
|
|
59
|
+
}
|
|
60
|
+
const randomNumber = getRandomBigIntegerInRange(1n, prime - 1n);
|
|
61
|
+
|
|
62
|
+
const c1 = modPow(generator, randomNumber, prime);
|
|
63
|
+
const messageBigInt = BigInt(secret);
|
|
64
|
+
const c2 = (modPow(publicKey, randomNumber, prime) * messageBigInt) % prime;
|
|
65
|
+
|
|
66
|
+
return { c1, c2 };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Decrypts an ElGamal encrypted secret.
|
|
71
|
+
*
|
|
72
|
+
* @param {EncryptedMessage} secret - The encrypted secret to decrypt.
|
|
73
|
+
* @param {bigint} prime - The prime number used in the encryption system.
|
|
74
|
+
* @param {bigint} privateKey - The private key used for decryption.
|
|
75
|
+
* @returns {number} The decrypted secret as an integer.
|
|
76
|
+
*/
|
|
77
|
+
export const decrypt = (
|
|
78
|
+
encryptedMessage: EncryptedMessage,
|
|
79
|
+
prime: bigint,
|
|
80
|
+
privateKey: bigint,
|
|
81
|
+
): number => {
|
|
82
|
+
const ax: bigint = modPow(encryptedMessage.c1, privateKey, prime);
|
|
83
|
+
const plaintext: bigint = (modInv(ax, prime) * encryptedMessage.c2) % prime;
|
|
84
|
+
return Number(plaintext);
|
|
85
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { generateParameters, encrypt, decrypt } from './elgamal';
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
generateSingleKeyShare,
|
|
5
|
+
generateKeyShares,
|
|
6
|
+
combinePublicKeys,
|
|
7
|
+
createDecryptionShare,
|
|
8
|
+
combineDecryptionShares,
|
|
9
|
+
thresholdDecrypt,
|
|
10
|
+
} from './thresholdElgamal';
|
|
11
|
+
|
|
12
|
+
export { getRandomBigIntegerInRange, multiplyEncryptedValues } from './utils';
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
EncryptedMessage,
|
|
16
|
+
Parameters,
|
|
17
|
+
KeyPair,
|
|
18
|
+
PartyKeyPair,
|
|
19
|
+
} from './types';
|
package/src/testUtils.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { encrypt } from './elgamal';
|
|
4
|
+
import {
|
|
5
|
+
generateKeyShares,
|
|
6
|
+
combinePublicKeys,
|
|
7
|
+
createDecryptionShare,
|
|
8
|
+
combineDecryptionShares,
|
|
9
|
+
thresholdDecrypt,
|
|
10
|
+
getGroup,
|
|
11
|
+
} from './thresholdElgamal';
|
|
12
|
+
import type { PartyKeyPair } from './types';
|
|
13
|
+
import { multiplyEncryptedValues } from './utils';
|
|
14
|
+
|
|
15
|
+
export const getRandomScore = (min = 1, max = 10): number =>
|
|
16
|
+
Math.floor(Math.random() * (max - min + 1)) + min;
|
|
17
|
+
|
|
18
|
+
export const thresholdSetup = (
|
|
19
|
+
partiesCount: number,
|
|
20
|
+
threshold: number,
|
|
21
|
+
primeBits: 2048 | 3072 | 4096 = 2048,
|
|
22
|
+
): {
|
|
23
|
+
keyShares: PartyKeyPair[];
|
|
24
|
+
combinedPublicKey: bigint;
|
|
25
|
+
prime: bigint;
|
|
26
|
+
generator: bigint;
|
|
27
|
+
} => {
|
|
28
|
+
const { prime, generator } = getGroup(primeBits);
|
|
29
|
+
const keyShares = generateKeyShares(partiesCount, threshold, primeBits);
|
|
30
|
+
const publicKeys = keyShares.map((ks) => ks.partyPublicKey);
|
|
31
|
+
const combinedPublicKey = combinePublicKeys(publicKeys, prime);
|
|
32
|
+
|
|
33
|
+
return { keyShares, combinedPublicKey, prime, generator };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const testSecureEncryptionAndDecryption = (
|
|
37
|
+
participantsCount: number,
|
|
38
|
+
threshold: number,
|
|
39
|
+
secret: number,
|
|
40
|
+
): void => {
|
|
41
|
+
const { keyShares, combinedPublicKey, prime, generator } = thresholdSetup(
|
|
42
|
+
participantsCount,
|
|
43
|
+
threshold,
|
|
44
|
+
);
|
|
45
|
+
const encryptedMessage = encrypt(
|
|
46
|
+
secret,
|
|
47
|
+
prime,
|
|
48
|
+
generator,
|
|
49
|
+
combinedPublicKey,
|
|
50
|
+
);
|
|
51
|
+
const selectedDecryptionShares = keyShares
|
|
52
|
+
.sort(() => Math.random() - 0.5)
|
|
53
|
+
.slice(0, threshold)
|
|
54
|
+
.map((keyShare) =>
|
|
55
|
+
createDecryptionShare(
|
|
56
|
+
encryptedMessage,
|
|
57
|
+
keyShare.partyPrivateKey,
|
|
58
|
+
prime,
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
const combinedDecryptionShares = combineDecryptionShares(
|
|
62
|
+
selectedDecryptionShares,
|
|
63
|
+
prime,
|
|
64
|
+
);
|
|
65
|
+
const decryptedMessage = thresholdDecrypt(
|
|
66
|
+
encryptedMessage,
|
|
67
|
+
combinedDecryptionShares,
|
|
68
|
+
prime,
|
|
69
|
+
);
|
|
70
|
+
expect(decryptedMessage).toBe(secret);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const homomorphicMultiplicationTest = (
|
|
74
|
+
participantsCount: number,
|
|
75
|
+
threshold: number,
|
|
76
|
+
messages: number[],
|
|
77
|
+
): void => {
|
|
78
|
+
const expectedProduct = messages.reduce(
|
|
79
|
+
(product, secret) => product * secret,
|
|
80
|
+
1,
|
|
81
|
+
);
|
|
82
|
+
const { keyShares, combinedPublicKey, prime, generator } = thresholdSetup(
|
|
83
|
+
participantsCount,
|
|
84
|
+
threshold,
|
|
85
|
+
);
|
|
86
|
+
const encryptedMessages = messages.map((secret) =>
|
|
87
|
+
encrypt(secret, prime, generator, combinedPublicKey),
|
|
88
|
+
);
|
|
89
|
+
const encryptedProduct = encryptedMessages.reduce(
|
|
90
|
+
(product, encryptedMessage) =>
|
|
91
|
+
multiplyEncryptedValues(product, encryptedMessage, prime),
|
|
92
|
+
{ c1: 1n, c2: 1n },
|
|
93
|
+
);
|
|
94
|
+
const selectedDecryptionShares = keyShares
|
|
95
|
+
.sort(() => Math.random() - 0.5)
|
|
96
|
+
.slice(0, threshold)
|
|
97
|
+
.map((keyShare) =>
|
|
98
|
+
createDecryptionShare(
|
|
99
|
+
encryptedProduct,
|
|
100
|
+
keyShare.partyPrivateKey,
|
|
101
|
+
prime,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
const combinedDecryptionShares = combineDecryptionShares(
|
|
105
|
+
selectedDecryptionShares,
|
|
106
|
+
prime,
|
|
107
|
+
);
|
|
108
|
+
const decryptedProduct = thresholdDecrypt(
|
|
109
|
+
encryptedProduct,
|
|
110
|
+
combinedDecryptionShares,
|
|
111
|
+
prime,
|
|
112
|
+
);
|
|
113
|
+
expect(decryptedProduct).toBe(expectedProduct);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const votingTest = (
|
|
117
|
+
participantsCount: number,
|
|
118
|
+
threshold: number,
|
|
119
|
+
candidatesCount: number,
|
|
120
|
+
): void => {
|
|
121
|
+
const { keyShares, combinedPublicKey, prime, generator } = thresholdSetup(
|
|
122
|
+
participantsCount,
|
|
123
|
+
threshold,
|
|
124
|
+
);
|
|
125
|
+
const votesMatrix = Array.from({ length: participantsCount }, () =>
|
|
126
|
+
Array.from({ length: candidatesCount }, () => getRandomScore(1, 10)),
|
|
127
|
+
);
|
|
128
|
+
const expectedProducts = Array.from(
|
|
129
|
+
{ length: candidatesCount },
|
|
130
|
+
(_, candidateIndex) =>
|
|
131
|
+
votesMatrix.reduce(
|
|
132
|
+
(product, votes) => product * votes[candidateIndex],
|
|
133
|
+
1,
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
const encryptedVotesMatrix = votesMatrix.map((votes) =>
|
|
137
|
+
votes.map((vote) => encrypt(vote, prime, generator, combinedPublicKey)),
|
|
138
|
+
);
|
|
139
|
+
const encryptedProducts = Array.from(
|
|
140
|
+
{ length: candidatesCount },
|
|
141
|
+
(_, candidateIndex) =>
|
|
142
|
+
encryptedVotesMatrix.reduce(
|
|
143
|
+
(product, encryptedVotes) =>
|
|
144
|
+
multiplyEncryptedValues(
|
|
145
|
+
product,
|
|
146
|
+
encryptedVotes[candidateIndex],
|
|
147
|
+
prime,
|
|
148
|
+
),
|
|
149
|
+
{ c1: 1n, c2: 1n },
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
const partialDecryptionsMatrix = encryptedProducts.map((product) =>
|
|
153
|
+
keyShares
|
|
154
|
+
.slice(0, threshold)
|
|
155
|
+
.map((keyShare) =>
|
|
156
|
+
createDecryptionShare(product, keyShare.partyPrivateKey, prime),
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
const decryptedProducts = partialDecryptionsMatrix.map(
|
|
160
|
+
(decryptionShares) => {
|
|
161
|
+
const combinedDecryptionShares = combineDecryptionShares(
|
|
162
|
+
decryptionShares,
|
|
163
|
+
prime,
|
|
164
|
+
);
|
|
165
|
+
const encryptedProduct =
|
|
166
|
+
encryptedProducts[
|
|
167
|
+
partialDecryptionsMatrix.indexOf(decryptionShares)
|
|
168
|
+
];
|
|
169
|
+
return thresholdDecrypt(
|
|
170
|
+
encryptedProduct,
|
|
171
|
+
combinedDecryptionShares,
|
|
172
|
+
prime,
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
expect(decryptedProducts).toEqual(expectedProducts);
|
|
177
|
+
};
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { encrypt } from './elgamal';
|
|
4
|
+
import {
|
|
5
|
+
homomorphicMultiplicationTest,
|
|
6
|
+
testSecureEncryptionAndDecryption,
|
|
7
|
+
votingTest,
|
|
8
|
+
} from './testUtils';
|
|
9
|
+
import {
|
|
10
|
+
combinePublicKeys,
|
|
11
|
+
createDecryptionShare,
|
|
12
|
+
generateSingleKeyShare,
|
|
13
|
+
getGroup,
|
|
14
|
+
thresholdDecrypt,
|
|
15
|
+
combineDecryptionShares,
|
|
16
|
+
} from './thresholdElgamal';
|
|
17
|
+
import { PartyKeyPair } from './types';
|
|
18
|
+
import { multiplyEncryptedValues } from './utils';
|
|
19
|
+
|
|
20
|
+
// I already have modPow, modInv and getRandomBigIntegerInRange
|
|
21
|
+
describe('Threshold ElGamal', () => {
|
|
22
|
+
describe('in a single secret scheme', () => {
|
|
23
|
+
describe('allows for secure encryption and decryption', () => {
|
|
24
|
+
it('with 2 participants and a threshold of 2', () => {
|
|
25
|
+
testSecureEncryptionAndDecryption(2, 2, 42);
|
|
26
|
+
});
|
|
27
|
+
it('with 20 participants and a threshold of 20', () => {
|
|
28
|
+
testSecureEncryptionAndDecryption(20, 20, 4243);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Failing tests
|
|
32
|
+
// it('(t < n) with 3 participants and a threshold of 2', () => {
|
|
33
|
+
// testSecureEncryptionAndDecryption(3, 2, 123);
|
|
34
|
+
// });
|
|
35
|
+
// it('(t < n) with 5 participants and a threshold of 3', () => {
|
|
36
|
+
// testSecureEncryptionAndDecryption(5, 3, 255);
|
|
37
|
+
// });
|
|
38
|
+
// it('(t < n) with 7 participants and a threshold of 4', () => {
|
|
39
|
+
// testSecureEncryptionAndDecryption(7, 4, 789);
|
|
40
|
+
// });
|
|
41
|
+
});
|
|
42
|
+
it('works for the 3,3 step-by-step README example', () => {
|
|
43
|
+
const primeBits = 2048; // Bit length of the prime modulus
|
|
44
|
+
const threshold = 3; // A scenario for 3 participants with a threshold of 3
|
|
45
|
+
const { prime, generator } = getGroup(2048);
|
|
46
|
+
|
|
47
|
+
// Each participant generates their public key share and private key individually
|
|
48
|
+
const participant1KeyShare: PartyKeyPair = generateSingleKeyShare(
|
|
49
|
+
1,
|
|
50
|
+
threshold,
|
|
51
|
+
primeBits,
|
|
52
|
+
);
|
|
53
|
+
const participant2KeyShare: PartyKeyPair = generateSingleKeyShare(
|
|
54
|
+
2,
|
|
55
|
+
threshold,
|
|
56
|
+
primeBits,
|
|
57
|
+
);
|
|
58
|
+
const participant3KeyShare: PartyKeyPair = generateSingleKeyShare(
|
|
59
|
+
3,
|
|
60
|
+
threshold,
|
|
61
|
+
primeBits,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Combine the public keys to form a single public key
|
|
65
|
+
const combinedPublicKey = combinePublicKeys(
|
|
66
|
+
[
|
|
67
|
+
participant1KeyShare.partyPublicKey,
|
|
68
|
+
participant2KeyShare.partyPublicKey,
|
|
69
|
+
participant3KeyShare.partyPublicKey,
|
|
70
|
+
],
|
|
71
|
+
prime,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Encrypt a message using the combined public key
|
|
75
|
+
const secret = 42;
|
|
76
|
+
const encryptedMessage = encrypt(
|
|
77
|
+
secret,
|
|
78
|
+
prime,
|
|
79
|
+
generator,
|
|
80
|
+
combinedPublicKey,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Decryption shares
|
|
84
|
+
const decryptionShares = [
|
|
85
|
+
createDecryptionShare(
|
|
86
|
+
encryptedMessage,
|
|
87
|
+
participant1KeyShare.partyPrivateKey,
|
|
88
|
+
prime,
|
|
89
|
+
),
|
|
90
|
+
createDecryptionShare(
|
|
91
|
+
encryptedMessage,
|
|
92
|
+
participant2KeyShare.partyPrivateKey,
|
|
93
|
+
prime,
|
|
94
|
+
),
|
|
95
|
+
createDecryptionShare(
|
|
96
|
+
encryptedMessage,
|
|
97
|
+
participant3KeyShare.partyPrivateKey,
|
|
98
|
+
prime,
|
|
99
|
+
),
|
|
100
|
+
];
|
|
101
|
+
// Combining the decryption shares into one, used to decrypt the message
|
|
102
|
+
const combinedDecryptionShares = combineDecryptionShares(
|
|
103
|
+
decryptionShares,
|
|
104
|
+
prime,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Decrypting the message using the combined decryption shares
|
|
108
|
+
const thresholdDecryptedMessage = thresholdDecrypt(
|
|
109
|
+
encryptedMessage,
|
|
110
|
+
combinedDecryptionShares,
|
|
111
|
+
prime,
|
|
112
|
+
);
|
|
113
|
+
console.log(thresholdDecryptedMessage); // 42
|
|
114
|
+
expect(thresholdDecryptedMessage).toBe(secret);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe('in a multiple secrets scheme', () => {
|
|
118
|
+
describe('supports homomorphic multiplication of encrypted messages', () => {
|
|
119
|
+
it('with 2 participants and a threshold of 2', () => {
|
|
120
|
+
homomorphicMultiplicationTest(2, 2, [3, 5]);
|
|
121
|
+
});
|
|
122
|
+
it('with 10 participants and a threshold of 10', () => {
|
|
123
|
+
homomorphicMultiplicationTest(
|
|
124
|
+
10,
|
|
125
|
+
10,
|
|
126
|
+
[13, 24, 35, 46, 5, 6, 7, 8, 9, 10],
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Failing tests
|
|
131
|
+
// it('(t < n) with 3 participants and a threshold of 2', () => {
|
|
132
|
+
// homomorphicMultiplicationTest(3, 2, [2, 3, 4]);
|
|
133
|
+
// });
|
|
134
|
+
// it('(t < n) with 5 participants and a threshold of 3', () => {
|
|
135
|
+
// homomorphicMultiplicationTest(5, 3, [1, 2, 3, 4, 5]);
|
|
136
|
+
// });
|
|
137
|
+
// it('(t < n) with 10 participants and a threshold of 5', () => {
|
|
138
|
+
// homomorphicMultiplicationTest(
|
|
139
|
+
// 10,
|
|
140
|
+
// 5,
|
|
141
|
+
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
142
|
+
// );
|
|
143
|
+
// });
|
|
144
|
+
});
|
|
145
|
+
describe('supports voting', () => {
|
|
146
|
+
it('with 2 participants, threshold of 2 and 2 candidates', () => {
|
|
147
|
+
votingTest(2, 2, 2);
|
|
148
|
+
});
|
|
149
|
+
it('with 5 participants, threshold of 5 and 3 candidates', () => {
|
|
150
|
+
votingTest(5, 5, 3);
|
|
151
|
+
});
|
|
152
|
+
it('with 7 participants, threshold of 7 and 7 candidates', () => {
|
|
153
|
+
votingTest(7, 7, 7);
|
|
154
|
+
});
|
|
155
|
+
it('with 6 participants, threshold of 6 and 8 candidates', () => {
|
|
156
|
+
votingTest(6, 6, 8);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Failing tests
|
|
160
|
+
// it('(t < n) with 3 participants, threshold of 2 and 2 candidates', () => {
|
|
161
|
+
// votingTest(3, 2, 2);
|
|
162
|
+
// });
|
|
163
|
+
// it('(t < n) with 7 participants, threshold of 5 and 3 candidates', () => {
|
|
164
|
+
// votingTest(7, 5, 3);
|
|
165
|
+
// });
|
|
166
|
+
});
|
|
167
|
+
it('works for the 3, 3, 2 step-by-step README example', () => {
|
|
168
|
+
const primeBits = 2048; // Bit length of the prime modulus
|
|
169
|
+
const threshold = 3; // A scenario for 3 participants with a threshold of 3
|
|
170
|
+
const { prime, generator } = getGroup(2048);
|
|
171
|
+
|
|
172
|
+
// Each participant generates their public key share and private key individually
|
|
173
|
+
const participant1KeyShare = generateSingleKeyShare(
|
|
174
|
+
1,
|
|
175
|
+
threshold,
|
|
176
|
+
primeBits,
|
|
177
|
+
);
|
|
178
|
+
const participant2KeyShare = generateSingleKeyShare(
|
|
179
|
+
2,
|
|
180
|
+
threshold,
|
|
181
|
+
primeBits,
|
|
182
|
+
);
|
|
183
|
+
const participant3KeyShare = generateSingleKeyShare(
|
|
184
|
+
3,
|
|
185
|
+
threshold,
|
|
186
|
+
primeBits,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Combine the public keys to form a single public key
|
|
190
|
+
const combinedPublicKey = combinePublicKeys(
|
|
191
|
+
[
|
|
192
|
+
participant1KeyShare.partyPublicKey,
|
|
193
|
+
participant2KeyShare.partyPublicKey,
|
|
194
|
+
participant3KeyShare.partyPublicKey,
|
|
195
|
+
],
|
|
196
|
+
prime,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Participants cast their encrypted votes for two options
|
|
200
|
+
const voteOption1 = [6, 7, 1]; // Votes for option 1 by participants 1, 2, and 3
|
|
201
|
+
const voteOption2 = [10, 7, 4]; // Votes for option 2 by participants 1, 2, and 3
|
|
202
|
+
|
|
203
|
+
// Encrypt votes for both options
|
|
204
|
+
const encryptedVotesOption1 = voteOption1.map((vote) =>
|
|
205
|
+
encrypt(vote, prime, generator, combinedPublicKey),
|
|
206
|
+
);
|
|
207
|
+
const encryptedVotesOption2 = voteOption2.map((vote) =>
|
|
208
|
+
encrypt(vote, prime, generator, combinedPublicKey),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Multiply encrypted votes together to aggregate
|
|
212
|
+
const aggregatedEncryptedVoteOption1 = encryptedVotesOption1.reduce(
|
|
213
|
+
(acc, curr) => multiplyEncryptedValues(acc, curr, prime),
|
|
214
|
+
{ c1: 1n, c2: 1n },
|
|
215
|
+
);
|
|
216
|
+
const aggregatedEncryptedVoteOption2 = encryptedVotesOption2.reduce(
|
|
217
|
+
(acc, curr) => multiplyEncryptedValues(acc, curr, prime),
|
|
218
|
+
{ c1: 1n, c2: 1n },
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Each participant creates a decryption share for both options.
|
|
222
|
+
// Notice that the shares are created for the aggregated, multiplied tally specifically,
|
|
223
|
+
// not the individual votes. This means that they can be used ONLY for decrypting the aggregated votes.
|
|
224
|
+
const decryptionSharesOption1 = [
|
|
225
|
+
createDecryptionShare(
|
|
226
|
+
aggregatedEncryptedVoteOption1,
|
|
227
|
+
participant1KeyShare.partyPrivateKey,
|
|
228
|
+
prime,
|
|
229
|
+
),
|
|
230
|
+
createDecryptionShare(
|
|
231
|
+
aggregatedEncryptedVoteOption1,
|
|
232
|
+
participant2KeyShare.partyPrivateKey,
|
|
233
|
+
prime,
|
|
234
|
+
),
|
|
235
|
+
createDecryptionShare(
|
|
236
|
+
aggregatedEncryptedVoteOption1,
|
|
237
|
+
participant3KeyShare.partyPrivateKey,
|
|
238
|
+
prime,
|
|
239
|
+
),
|
|
240
|
+
];
|
|
241
|
+
const decryptionSharesOption2 = [
|
|
242
|
+
createDecryptionShare(
|
|
243
|
+
aggregatedEncryptedVoteOption2,
|
|
244
|
+
participant1KeyShare.partyPrivateKey,
|
|
245
|
+
prime,
|
|
246
|
+
),
|
|
247
|
+
createDecryptionShare(
|
|
248
|
+
aggregatedEncryptedVoteOption2,
|
|
249
|
+
participant2KeyShare.partyPrivateKey,
|
|
250
|
+
prime,
|
|
251
|
+
),
|
|
252
|
+
createDecryptionShare(
|
|
253
|
+
aggregatedEncryptedVoteOption2,
|
|
254
|
+
participant3KeyShare.partyPrivateKey,
|
|
255
|
+
prime,
|
|
256
|
+
),
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
// Combine decryption shares and decrypt the aggregated votes for both options.
|
|
260
|
+
// Notice that the private keys of the participants never leave their possession.
|
|
261
|
+
// Only the decryption shares are shared with other participants.
|
|
262
|
+
const combinedDecryptionSharesOption1 = combineDecryptionShares(
|
|
263
|
+
decryptionSharesOption1,
|
|
264
|
+
prime,
|
|
265
|
+
);
|
|
266
|
+
const combinedDecryptionSharesOption2 = combineDecryptionShares(
|
|
267
|
+
decryptionSharesOption2,
|
|
268
|
+
prime,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const finalTallyOption1 = thresholdDecrypt(
|
|
272
|
+
aggregatedEncryptedVoteOption1,
|
|
273
|
+
combinedDecryptionSharesOption1,
|
|
274
|
+
prime,
|
|
275
|
+
);
|
|
276
|
+
const finalTallyOption2 = thresholdDecrypt(
|
|
277
|
+
aggregatedEncryptedVoteOption2,
|
|
278
|
+
combinedDecryptionSharesOption2,
|
|
279
|
+
prime,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
console.log(
|
|
283
|
+
`Final tally for Option 1: ${finalTallyOption1}, Option 2: ${finalTallyOption2}`,
|
|
284
|
+
); // 42, 280
|
|
285
|
+
expect(finalTallyOption1).toBe(42);
|
|
286
|
+
expect(finalTallyOption2).toBe(280);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|