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.
@@ -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';
@@ -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
+ });