@noble/post-quantum 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/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
+ }