@noble/post-quantum 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
package/src/ml-kem.ts ADDED
@@ -0,0 +1,403 @@
1
+ /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
2
+ import { ctr } from '@noble/ciphers/aes';
3
+ import { sha256, sha512 } from '@noble/hashes/sha2';
4
+ import { sha3_256, sha3_512, shake256 } from '@noble/hashes/sha3';
5
+ import { u32, wrapConstructor, wrapConstructorWithOpts } from '@noble/hashes/utils';
6
+ import { genCrystals, XOF, XOF_AES, XOF128 } from './_crystals.js';
7
+ import {
8
+ Coder,
9
+ cleanBytes,
10
+ ensureBytes,
11
+ equalBytes,
12
+ randomBytes,
13
+ splitCoder,
14
+ vecCoder,
15
+ } from './utils.js';
16
+
17
+ /*
18
+ Lattice-based key encapsulation mechanism.
19
+ See [official site](https://www.pq-crystals.org/kyber/resources.shtml),
20
+ [repo](https://github.com/pq-crystals/kyber),
21
+ [spec](https://datatracker.ietf.org/doc/draft-cfrg-schwabe-kyber/).
22
+
23
+ Key encapsulation is similar to DH / ECDH (think X25519), with important differences:
24
+
25
+ - We can't verify if it was "Bob" who've sent the shared secret.
26
+ In ECDH, it's always verified
27
+ - Kyber is probabalistic and relies on quality of randomness (CSPRNG).
28
+ ECDH doesn't (to this extent).
29
+ - Kyber decapsulation never throws an error, even when shared secret was
30
+ encrypted by a different public key. It will just return a different
31
+ shared secret
32
+
33
+ There are some concerns with regards to security: see
34
+ [djb blog](https://blog.cr.yp.to/20231003-countcorrectly.html) and
35
+ [mailing list](https://groups.google.com/a/list.nist.gov/g/pqc-forum/c/W2VOzy0wz_E).
36
+
37
+ Three versions are provided:
38
+
39
+ 1. Kyber
40
+ 2. Kyber-90s, using algorithms from 1990s
41
+ 3. ML-KEM aka [FIPS-203](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.ipd.pdf)
42
+ */
43
+
44
+ const N = 256; // Kyber (not FIPS-203) supports different lengths, but all std modes were using 256
45
+ const Q = 3329; // 13*(2**8)+1, modulo prime
46
+ const F = 3303; // 3303 ≡ 128−1 mod q (FIPS-203)
47
+ const ROOT_OF_UNITY = 17; // ζ = 17 ∈ Zq is a primitive 256-th root of unity modulo Q. ζ**128 ≡−1
48
+ const { mod, nttZetas, NTT, bitsCoder } = genCrystals({
49
+ N,
50
+ Q,
51
+ F,
52
+ ROOT_OF_UNITY,
53
+ newPoly: (n: number) => new Uint16Array(n),
54
+ brvBits: 7,
55
+ isKyber: true,
56
+ });
57
+
58
+ // FIPS 203: 7. Parameter Sets
59
+ type ParameterSet = {
60
+ N: number;
61
+ K: number;
62
+ Q: number;
63
+ ETA1: number;
64
+ ETA2: number;
65
+ du: number;
66
+ dv: number;
67
+ RBGstrength: number;
68
+ };
69
+ // prettier-ignore
70
+ export const PARAMS: Record<string, ParameterSet> = {
71
+ 512: { N, Q, K: 2, ETA1: 3, ETA2: 2, du: 10, dv: 4, RBGstrength: 128 },
72
+ 768: { N, Q, K: 3, ETA1: 2, ETA2: 2, du: 10, dv: 4, RBGstrength: 192 },
73
+ 1024:{ N, Q, K: 4, ETA1: 2, ETA2: 2, du: 11, dv: 5, RBGstrength: 256 },
74
+ } as const;
75
+
76
+ // FIPS-203: compress/decompress
77
+ const compress = (d: number): Coder<number, number> => {
78
+ // Special case, no need to compress, pass as is, but strip high bytes on compression
79
+ if (d >= 12) return { encode: (i: number) => i, decode: (i: number) => i };
80
+ // NOTE: we don't use float arithmetic (forbidden by FIPS-203 and high chance of bugs).
81
+ // Comments map to python implementation in RFC (draft-cfrg-schwabe-kyber)
82
+ // const round = (i: number) => Math.floor(i + 0.5) | 0;
83
+ const a = 2 ** (d - 1);
84
+ return {
85
+ // const compress = (i: number) => round((2 ** d / Q) * i) % 2 ** d;
86
+ encode: (i: number) => ((i << d) + Q / 2) / Q,
87
+ // const decompress = (i: number) => round((Q / 2 ** d) * i);
88
+ decode: (i: number) => (i * Q + a) >>> d,
89
+ };
90
+ };
91
+
92
+ // NOTE: we merge encoding and compress because it is faster, also both require same d param
93
+ // Converts between bytes and d-bits compressed representation. Kinda like convertRadix2 from @scure/base
94
+ // decode(encode(t)) == t, but there is loss of information on encode(decode(t))
95
+ const polyCoder = (d: number) => bitsCoder(d, compress(d));
96
+
97
+ // Poly is mod Q, so 12 bits
98
+ type Poly = Uint16Array;
99
+
100
+ function polyAdd(a: Poly, b: Poly) {
101
+ for (let i = 0; i < N; i++) a[i] = mod(a[i] + b[i]); // a += b
102
+ }
103
+ function polySub(a: Poly, b: Poly) {
104
+ for (let i = 0; i < N; i++) a[i] = mod(a[i] - b[i]); // a -= b
105
+ }
106
+
107
+ // FIPS-203: Computes the product of two degree-one polynomials with respect to a quadratic modulus
108
+ function BaseCaseMultiply(a0: number, a1: number, b0: number, b1: number, zeta: number) {
109
+ const c0 = mod(a1 * b1 * zeta + a0 * b0);
110
+ const c1 = mod(a0 * b1 + a1 * b0);
111
+ return { c0, c1 };
112
+ }
113
+
114
+ // FIPS-203: Computes the product (in the ring Tq) of two NTT representations. NOTE: works inplace for f
115
+ // NOTE: since multiply defined only for NTT representation, we need to convert to NTT, multiply and convert back
116
+ function MultiplyNTTs(f: Poly, g: Poly): Poly {
117
+ for (let i = 0; i < N / 2; i++) {
118
+ let z = nttZetas[64 + (i >> 1)];
119
+ if (i & 1) z = -z;
120
+ const { c0, c1 } = BaseCaseMultiply(f[2 * i + 0], f[2 * i + 1], g[2 * i + 0], g[2 * i + 1], z);
121
+ f[2 * i + 0] = c0;
122
+ f[2 * i + 1] = c1;
123
+ }
124
+ return f;
125
+ }
126
+
127
+ type PRF = (l: number, key: Uint8Array, nonce: number) => Uint8Array;
128
+
129
+ type Hash = ReturnType<typeof wrapConstructor>;
130
+ type HashWOpts = ReturnType<typeof wrapConstructorWithOpts>;
131
+ type XofGet = ReturnType<ReturnType<XOF>['get']>;
132
+
133
+ type KyberOpts = ParameterSet & {
134
+ HASH256: Hash;
135
+ HASH512: Hash;
136
+ KDF: Hash | HashWOpts;
137
+ XOF: XOF; // (seed: Uint8Array, len: number, x: number, y: number) => Uint8Array;
138
+ PRF: PRF;
139
+ FIPS203?: boolean;
140
+ };
141
+
142
+ // Return poly in NTT representation
143
+ function SampleNTT(xof: XofGet) {
144
+ const r: Poly = new Uint16Array(N);
145
+ for (let j = 0; j < N; ) {
146
+ const b = xof();
147
+ if (b.length % 3) throw new Error('SampleNTT: unaligned block');
148
+ for (let i = 0; j < N && i + 3 <= b.length; i += 3) {
149
+ const d1 = ((b[i + 0] >> 0) | (b[i + 1] << 8)) & 0xfff;
150
+ const d2 = ((b[i + 1] >> 4) | (b[i + 2] << 4)) & 0xfff;
151
+ if (d1 < Q) r[j++] = d1;
152
+ if (j < N && d2 < Q) r[j++] = d2;
153
+ }
154
+ }
155
+ return r;
156
+ }
157
+
158
+ // Sampling from the centered binomial distribution
159
+ // Returns poly with small coefficients (noise/errors)
160
+ function sampleCBD(PRF: PRF, seed: Uint8Array, nonce: number, eta: number): Poly {
161
+ const buf = PRF((eta * N) / 4, seed, nonce);
162
+ const r: Poly = new Uint16Array(N);
163
+ const b32 = u32(buf);
164
+ let len = 0;
165
+ for (let i = 0, p = 0, bb = 0, t0 = 0; i < b32.length; i++) {
166
+ let b = b32[i];
167
+ for (let j = 0; j < 32; j++) {
168
+ bb += b & 1;
169
+ b >>= 1;
170
+ len += 1;
171
+ if (len === eta) {
172
+ t0 = bb;
173
+ bb = 0;
174
+ } else if (len === 2 * eta) {
175
+ r[p++] = mod(t0 - bb);
176
+ bb = 0;
177
+ len = 0;
178
+ }
179
+ }
180
+ }
181
+ if (len) throw new Error(`sampleCBD: leftover bits: ${len}`);
182
+ return r;
183
+ }
184
+
185
+ // K-PKE
186
+ // As per FIPS-203, it doesn't perform any input validation and can't be used in standalone fashion.
187
+ const genKPKE = (opts: KyberOpts) => {
188
+ const { K, PRF, XOF, HASH512, ETA1, ETA2, du, dv, FIPS203 } = opts;
189
+ const poly1 = polyCoder(1);
190
+ const polyV = polyCoder(dv);
191
+ const polyU = polyCoder(du);
192
+ const publicCoder = splitCoder(vecCoder(polyCoder(12), K), 32);
193
+ const secretCoder = vecCoder(polyCoder(12), K);
194
+ const cipherCoder = splitCoder(vecCoder(polyU, K), polyV);
195
+ const seedCoder = splitCoder(32, 32);
196
+ return {
197
+ secretCoder,
198
+ secretKeyLen: secretCoder.bytesLen,
199
+ publicKeyLen: publicCoder.bytesLen,
200
+ cipherTextLen: cipherCoder.bytesLen,
201
+ keygen: (seed: Uint8Array) => {
202
+ const [rho, sigma] = seedCoder.decode(HASH512(seed));
203
+ const sHat: Poly[] = [];
204
+ const tHat: Poly[] = [];
205
+ for (let i = 0; i < K; i++) sHat.push(NTT.encode(sampleCBD(PRF, sigma, i, ETA1)));
206
+ const x = XOF(rho);
207
+ for (let i = 0; i < K; i++) {
208
+ const e = NTT.encode(sampleCBD(PRF, sigma, K + i, ETA1));
209
+ for (let j = 0; j < K; j++) {
210
+ const aji = SampleNTT(FIPS203 ? x.get(i, j) : x.get(j, i)); // A[j][i], inplace
211
+ polyAdd(e, MultiplyNTTs(aji, sHat[j]));
212
+ }
213
+ tHat.push(e); // t ← A ◦ s + e
214
+ }
215
+ x.clean();
216
+ const res = {
217
+ publicKey: publicCoder.encode([tHat, rho]),
218
+ secretKey: secretCoder.encode(sHat),
219
+ };
220
+ cleanBytes(rho, sigma, sHat, tHat);
221
+ return res;
222
+ },
223
+ encrypt: (publicKey: Uint8Array, msg: Uint8Array, seed: Uint8Array) => {
224
+ const [tHat, rho] = publicCoder.decode(publicKey);
225
+ const rHat = [];
226
+ for (let i = 0; i < K; i++) rHat.push(NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
227
+ const x = XOF(rho);
228
+ const tmp2 = new Uint16Array(N);
229
+ const u = [];
230
+ for (let i = 0; i < K; i++) {
231
+ const e1 = sampleCBD(PRF, seed, K + i, ETA2);
232
+ const tmp = new Uint16Array(N);
233
+ for (let j = 0; j < K; j++) {
234
+ const aij = SampleNTT(FIPS203 ? x.get(j, i) : x.get(i, j)); // A[i][j], inplace
235
+ polyAdd(tmp, MultiplyNTTs(aij, rHat[j])); // t += aij * rHat[j]
236
+ }
237
+ polyAdd(e1, NTT.decode(tmp)); // e1 += tmp
238
+ u.push(e1);
239
+ polyAdd(tmp2, MultiplyNTTs(tHat[i], rHat[i])); // t2 += tHat[i] * rHat[i]
240
+ tmp.fill(0);
241
+ }
242
+ x.clean();
243
+ const e2 = sampleCBD(PRF, seed, 2 * K, ETA2);
244
+ polyAdd(e2, NTT.decode(tmp2)); // e2 += tmp2
245
+ const v = poly1.decode(msg); // encode plaintext m into polynomial v
246
+ polyAdd(v, e2); // v += e2
247
+ cleanBytes(tHat, rHat, tmp2, e2);
248
+ return cipherCoder.encode([u, v]);
249
+ },
250
+ decrypt: (cipherText: Uint8Array, privateKey: Uint8Array) => {
251
+ const [u, v] = cipherCoder.decode(cipherText);
252
+ const sk = secretCoder.decode(privateKey); // s ← ByteDecode_12(dkPKE)
253
+ const tmp = new Uint16Array(N);
254
+ for (let i = 0; i < K; i++) polyAdd(tmp, MultiplyNTTs(sk[i], NTT.encode(u[i]))); // tmp += sk[i] * u[i]
255
+ polySub(v, NTT.decode(tmp)); // v += tmp
256
+ cleanBytes(tmp, sk, u);
257
+ return poly1.encode(v);
258
+ },
259
+ };
260
+ };
261
+
262
+ function createKyber(opts: KyberOpts) {
263
+ const KPKE = genKPKE(opts);
264
+ const { HASH256, HASH512, KDF, FIPS203 } = opts;
265
+ const { secretCoder: KPKESecretCoder, cipherTextLen } = KPKE;
266
+ const publicKeyLen = KPKE.publicKeyLen; // 384*K+32
267
+ const secretCoder = splitCoder(KPKE.secretKeyLen, KPKE.publicKeyLen, 32, 32);
268
+ const secretKeyLen = secretCoder.bytesLen;
269
+ const msgLen = 32;
270
+ return {
271
+ publicKeyLen,
272
+ msgLen,
273
+ keygen: (seed = randomBytes(64)) => {
274
+ ensureBytes(seed, 64);
275
+ const { publicKey, secretKey: sk } = KPKE.keygen(seed.subarray(0, 32));
276
+ const publicKeyHash = HASH256(publicKey);
277
+ // (dkPKE||ek||H(ek)||z)
278
+ const secretKey = secretCoder.encode([sk, publicKey, publicKeyHash, seed.subarray(32)]);
279
+ cleanBytes(sk, publicKeyHash);
280
+ return { publicKey, secretKey };
281
+ },
282
+ encapsulate: (publicKey: Uint8Array, msg = randomBytes(32)) => {
283
+ ensureBytes(publicKey, publicKeyLen);
284
+ ensureBytes(msg, msgLen);
285
+ if (!FIPS203) msg = HASH256(msg); // NOTE: ML-KEM doesn't have this step!
286
+ else {
287
+ // FIPS-203 includes additional verification check for modulus
288
+ const eke = publicKey.subarray(0, 384 * opts.K);
289
+ const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(eke.slice())); // Copy because of inplace encoding
290
+ // (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
291
+ // If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
292
+ if (!equalBytes(ek, eke)) {
293
+ cleanBytes(ek);
294
+ throw new Error('ML-KEM.encapsulate: wrong publicKey modulus');
295
+ }
296
+ cleanBytes(ek);
297
+ }
298
+ const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest(); // derive randomness
299
+ const cipherText = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64));
300
+ if (FIPS203) return { cipherText, sharedSecret: kr.subarray(0, 32) };
301
+ const cipherTextHash = HASH256(cipherText);
302
+ const sharedSecret = KDF.create({})
303
+ .update(kr.subarray(0, 32))
304
+ .update(cipherTextHash)
305
+ .digest();
306
+ cleanBytes(kr, cipherTextHash);
307
+ return { cipherText, sharedSecret };
308
+ },
309
+ decapsulate: (cipherText: Uint8Array, secretKey: Uint8Array) => {
310
+ ensureBytes(secretKey, secretKeyLen); // 768*k + 96
311
+ ensureBytes(cipherText, cipherTextLen); // 32(du*k + dv)
312
+ const [sk, publicKey, publicKeyHash, z] = secretCoder.decode(secretKey);
313
+ const msg = KPKE.decrypt(cipherText, sk);
314
+ const kr = HASH512.create().update(msg).update(publicKeyHash).digest(); // derive randomness, Khat, rHat = G(mHat || h)
315
+ const Khat = kr.subarray(0, 32);
316
+ const cipherText2 = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64)); // re-encrypt using the derived randomness
317
+ const isValid = equalBytes(cipherText, cipherText2); // if ciphertexts do not match, “implicitly reject”
318
+ if (FIPS203) {
319
+ const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
320
+ cleanBytes(msg, cipherText2, !isValid ? Khat : Kbar);
321
+ return isValid ? Khat : Kbar;
322
+ }
323
+ const cipherTextHash = HASH256(cipherText);
324
+ const sharedSecret = KDF.create({ dkLen: 32 })
325
+ .update(isValid ? Khat : z)
326
+ .update(cipherTextHash)
327
+ .digest();
328
+ cleanBytes(msg, cipherTextHash, cipherText2, Khat, z);
329
+ return sharedSecret;
330
+ },
331
+ };
332
+ }
333
+
334
+ function PRF(l: number, key: Uint8Array, nonce: number) {
335
+ const _nonce = new Uint8Array(16);
336
+ _nonce[0] = nonce;
337
+ return ctr(key, _nonce).encrypt(new Uint8Array(l));
338
+ }
339
+
340
+ const opts90s = { HASH256: sha256, HASH512: sha512, KDF: sha256, XOF: XOF_AES, PRF };
341
+
342
+ export const kyber512_90s = /* @__PURE__ */ createKyber({
343
+ ...opts90s,
344
+ ...PARAMS[512],
345
+ });
346
+ export const kyber768_90s = /* @__PURE__ */ createKyber({
347
+ ...opts90s,
348
+ ...PARAMS[768],
349
+ });
350
+ export const kyber1024_90s = /* @__PURE__ */ createKyber({
351
+ ...opts90s,
352
+ ...PARAMS[1024],
353
+ });
354
+
355
+ function shakePRF(dkLen: number, key: Uint8Array, nonce: number) {
356
+ return shake256
357
+ .create({ dkLen })
358
+ .update(key)
359
+ .update(new Uint8Array([nonce]))
360
+ .digest();
361
+ }
362
+
363
+ const opts = {
364
+ HASH256: sha3_256,
365
+ HASH512: sha3_512,
366
+ KDF: shake256,
367
+ XOF: XOF128,
368
+ PRF: shakePRF,
369
+ };
370
+
371
+ export const kyber512 = /* @__PURE__ */ createKyber({
372
+ ...opts,
373
+ ...PARAMS[512],
374
+ });
375
+ export const kyber768 = /* @__PURE__ */ createKyber({
376
+ ...opts,
377
+ ...PARAMS[768],
378
+ });
379
+ export const kyber1024 = /* @__PURE__ */ createKyber({
380
+ ...opts,
381
+ ...PARAMS[1024],
382
+ });
383
+
384
+ /**
385
+ * FIPS-203 (draft) ML-KEM.
386
+ * Unsafe: we can't cross-verify, because there are no test vectors or other implementations.
387
+ */
388
+
389
+ export const ml_kem512 = /* @__PURE__ */ createKyber({
390
+ ...opts,
391
+ ...PARAMS[512],
392
+ FIPS203: true,
393
+ });
394
+ export const ml_kem768 = /* @__PURE__ */ createKyber({
395
+ ...opts,
396
+ ...PARAMS[768],
397
+ FIPS203: true,
398
+ });
399
+ export const ml_kem1024 = /* @__PURE__ */ createKyber({
400
+ ...opts,
401
+ ...PARAMS[1024],
402
+ FIPS203: true,
403
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }