@noble/post-quantum 0.6.0 → 0.7.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 CHANGED
@@ -33,6 +33,8 @@ import {
33
33
  type KEM,
34
34
  randomBytes,
35
35
  splitCoder,
36
+ type TArg,
37
+ type TRet,
36
38
  vecCoder,
37
39
  } from './utils.ts';
38
40
 
@@ -50,7 +52,7 @@ const crystals = /* @__PURE__ */ genCrystals({
50
52
  Q,
51
53
  F,
52
54
  ROOT_OF_UNITY,
53
- newPoly: (n: number): Uint16Array => new Uint16Array(n),
55
+ newPoly: (n: number): TRet<Uint16Array> => new Uint16Array(n) as TRet<Uint16Array>,
54
56
  brvBits: 7,
55
57
  isKyber: true,
56
58
  });
@@ -82,11 +84,12 @@ export type KEMParam = {
82
84
  * `RBGstrength` is Table 2's required randomness-source strength in bits,
83
85
  * not a generic security label.
84
86
  */
85
- export const PARAMS: Record<string, KEMParam> = /* @__PURE__ */ (() => ({
86
- 512: { N, Q, K: 2, ETA1: 3, ETA2: 2, du: 10, dv: 4, RBGstrength: 128 },
87
- 768: { N, Q, K: 3, ETA1: 2, ETA2: 2, du: 10, dv: 4, RBGstrength: 192 },
88
- 1024:{ N, Q, K: 4, ETA1: 2, ETA2: 2, du: 11, dv: 5, RBGstrength: 256 },
89
- } as const))();
87
+ export const PARAMS: Record<string, KEMParam> = /* @__PURE__ */ (() =>
88
+ Object.freeze({
89
+ 512: Object.freeze({ N, Q, K: 2, ETA1: 3, ETA2: 2, du: 10, dv: 4, RBGstrength: 128 }),
90
+ 768: Object.freeze({ N, Q, K: 3, ETA1: 2, ETA2: 2, du: 10, dv: 4, RBGstrength: 192 }),
91
+ 1024: Object.freeze({ N, Q, K: 4, ETA1: 2, ETA2: 2, du: 11, dv: 5, RBGstrength: 256 }),
92
+ } as const))();
90
93
 
91
94
  // FIPS-203: compress/decompress
92
95
  const compress = (d: number): Coder<number, number> => {
@@ -126,28 +129,44 @@ const byteCoder = (d: number) =>
126
129
  const polyCoder = (d: number) => (d === 12 ? byteCoder(12) : crystals.bitsCoder(d, compress(d)));
127
130
 
128
131
  // Poly is mod Q, so 12 bits
129
- type Poly = Uint16Array<any>;
132
+ type Poly = Uint16Array;
130
133
 
131
- function polyAdd(a: Poly, b: Poly) {
134
+ // Coefficients always stay reduced in [0, Q) here (samplers, NTT and coders all reduce),
135
+ // so one conditional correction replaces the generic mod().
136
+ function polyAdd(a_: TArg<Poly>, b_: TArg<Poly>) {
137
+ const a = a_ as Poly;
138
+ const b = b_ as Poly;
132
139
  // Mutates `a` in place; callers must pass two N=256 polynomials.
133
- for (let i = 0; i < N; i++) a[i] = crystals.mod(a[i] + b[i]); // a += b
140
+ for (let i = 0; i < N; i++) {
141
+ const r = a[i] + b[i]; // a += b
142
+ a[i] = r >= Q ? r - Q : r;
143
+ }
134
144
  }
135
- function polySub(a: Poly, b: Poly) {
145
+ function polySub(a_: TArg<Poly>, b_: TArg<Poly>) {
146
+ const a = a_ as Poly;
147
+ const b = b_ as Poly;
136
148
  // Mutates `a` in place; callers must pass two N=256 polynomials.
137
- for (let i = 0; i < N; i++) a[i] = crystals.mod(a[i] - b[i]); // a -= b
149
+ for (let i = 0; i < N; i++) {
150
+ const r = a[i] - b[i]; // a -= b
151
+ a[i] = r < 0 ? r + Q : r;
152
+ }
138
153
  }
139
154
 
140
155
  // FIPS-203: Computes the product of two degree-one polynomials with respect to a quadratic modulus
141
156
  function BaseCaseMultiply(a0: number, a1: number, b0: number, b1: number, zeta: number) {
142
157
  // `zeta` here is Algorithm 11's γ = ζ^(2BitRev_7(i)+1).
143
- const c0 = crystals.mod(a1 * b1 * zeta + a0 * b0);
158
+ // Reduce a1*b1 before multiplying by zeta: a1*b1*zeta would reach ~2^35, forcing JS engines
159
+ // into slow float fmod; with the extra reduction every intermediate fits int32.
160
+ const c0 = crystals.mod(crystals.mod(a1 * b1) * zeta + a0 * b0);
144
161
  const c1 = crystals.mod(a0 * b1 + a1 * b0);
145
162
  return { c0, c1 };
146
163
  }
147
164
 
148
165
  // FIPS-203: Computes the product (in the ring Tq) of two NTT representations.
149
166
  // Works in place on `f`; `g` is read-only and both inputs must already be in NTT form.
150
- function MultiplyNTTs(f: Poly, g: Poly): Poly {
167
+ function MultiplyNTTs(f_: TArg<Poly>, g_: TArg<Poly>): TRet<Poly> {
168
+ const f = f_ as Poly;
169
+ const g = g_ as Poly;
151
170
  for (let i = 0; i < N / 2; i++) {
152
171
  let z = crystals.nttZetas[64 + (i >> 1)];
153
172
  if (i & 1) z = -z;
@@ -155,24 +174,51 @@ function MultiplyNTTs(f: Poly, g: Poly): Poly {
155
174
  f[2 * i + 0] = c0;
156
175
  f[2 * i + 1] = c1;
157
176
  }
158
- return f;
177
+ return f as TRet<Poly>;
159
178
  }
160
179
 
161
180
  type PRF = (l: number, key: Uint8Array, nonce: number) => Uint8Array;
162
181
 
182
+ /**
183
+ * Prepared (pre-expanded) ML-KEM public key. Experimental prototype.
184
+ * Caches only public data: packed ek, the expanded matrix Â, decoded t̂ and H(ek). No secret
185
+ * material is retained between calls; secret keys passed to `decapsulate` are decoded and wiped
186
+ * per call, exactly like the one-shot API. `clean()` wipes the expanded Â/t̂ cache; the packed
187
+ * public key and H(ek) are public and are not wiped. The object must not be used afterwards.
188
+ */
189
+ export type KEMPrepared = {
190
+ /**
191
+ * Detached copy of the source public key. Treat as read-only while the prepared object is in use.
192
+ * Callers may wipe it after final use; any mutation invalidates subsequent operations.
193
+ */
194
+ publicKey: Uint8Array;
195
+ /** Same as `KEM.encapsulate`, minus per-call ek re-validation and  re-expansion. */
196
+ encapsulate: (msg?: Uint8Array) => { cipherText: Uint8Array; sharedSecret: Uint8Array };
197
+ /**
198
+ * Same as `KEM.decapsulate`; throws if `secretKey` does not embed this public key.
199
+ * The embedded-ek byte comparison plus stored-hash comparison is equivalent to the
200
+ * FIPS 203 §7.3 hash input check.
201
+ */
202
+ decapsulate: (cipherText: Uint8Array, secretKey: Uint8Array) => Uint8Array;
203
+ /** Wipe cached (public) data. */
204
+ clean: () => void;
205
+ };
206
+ /** KEM with prepared-key support. */
207
+ export type MLKEM = KEM & { prepare: (publicKey: Uint8Array) => KEMPrepared };
208
+
163
209
  type XofGet = ReturnType<ReturnType<XOF>['get']>;
164
210
 
165
211
  type KyberOpts = KEMParam & {
166
212
  HASH256: CHash;
167
213
  HASH512: CHash;
168
- // KDF: CHash<Keccak, ShakeOpts>;
169
- KDF: any;
214
+ KDF: CHash<any, { dkLen?: number }>;
170
215
  XOF: XOF; // (seed: Uint8Array, len: number, x: number, y: number) => Uint8Array;
171
216
  PRF: PRF;
172
217
  };
173
218
 
174
219
  // Return poly in NTT representation
175
- function SampleNTT(xof: XofGet) {
220
+ function SampleNTT(xof_: TArg<XofGet>): TRet<Poly> {
221
+ const xof = xof_ as XofGet;
176
222
  // The reader must already bind the Algorithm 7 seed||j||i bytes
177
223
  // and return block lengths divisible by 3.
178
224
  const r: Poly = new Uint16Array(N);
@@ -186,13 +232,13 @@ function SampleNTT(xof: XofGet) {
186
232
  if (j < N && d2 < Q) r[j++] = d2;
187
233
  }
188
234
  }
189
- return r;
235
+ return r as TRet<Poly>;
190
236
  }
191
237
 
192
238
  // Sampling from the centered binomial distribution
193
239
  // Returns poly with small coefficients (noise/errors) stored modulo q in ordinary coefficient form.
194
240
  // Current callers only use Table 2 eta values {2,3} and PRF outputs of exactly 64*eta bytes.
195
- const sampleCBDBytes = (buf: Uint8Array, eta: number): Poly => {
241
+ const sampleCBDBytes = (buf: TArg<Uint8Array>, eta: number): TRet<Poly> => {
196
242
  const r: Poly = new Uint16Array(N);
197
243
  // CBD consumes the PRF bitstream in little-endian byte order; normalize the word view on BE,
198
244
  // then swap it back so callers still observe `buf` as read-only.
@@ -217,10 +263,16 @@ const sampleCBDBytes = (buf: Uint8Array, eta: number): Poly => {
217
263
  }
218
264
  swap32IfBE(b32);
219
265
  if (len) throw new Error(`sampleCBD: leftover bits: ${len}`);
220
- return r;
266
+ return r as TRet<Poly>;
221
267
  };
222
268
 
223
- function sampleCBD(PRF: PRF, seed: Uint8Array, nonce: number, eta: number): Poly {
269
+ function sampleCBD(
270
+ PRF_: TArg<PRF>,
271
+ seed: TArg<Uint8Array>,
272
+ nonce: number,
273
+ eta: number
274
+ ): TRet<Poly> {
275
+ const PRF = PRF_ as PRF;
224
276
  return sampleCBDBytes(PRF((eta * N) / 4, seed, nonce), eta);
225
277
  }
226
278
 
@@ -228,7 +280,8 @@ function sampleCBD(PRF: PRF, seed: Uint8Array, nonce: number, eta: number): Poly
228
280
  // Internal ML-KEM subroutine only: exact 32-byte `seed` / `msg` inputs
229
281
  // come from Algorithms 13-15, and the helper mutates decoded temporary
230
282
  // polynomials in place while leaving caller byte arrays unchanged.
231
- const genKPKE = (opts: KyberOpts) => {
283
+ const genKPKE = (opts_: TArg<KyberOpts>) => {
284
+ const opts = opts_ as KyberOpts;
232
285
  const { K, PRF, XOF, HASH512, ETA1, ETA2, du, dv } = opts;
233
286
  const poly1 = polyCoder(1);
234
287
  const polyV = polyCoder(dv);
@@ -237,6 +290,38 @@ const genKPKE = (opts: KyberOpts) => {
237
290
  const secretCoder = vecCoder(polyCoder(12), K);
238
291
  const cipherCoder = splitCoder('ciphertext', vecCoder(polyU, K), polyV);
239
292
  const seedCoder = splitCoder('seed', 32, 32);
293
+ // Algorithm 14 (K-PKE.Encrypt) core, after ek parsing. `tHat` and every poly returned by
294
+ // `getA(i, j)` are treated as disposable scratch: they are mutated in place and wiped/dropped,
295
+ // so callers holding cached copies must pass fresh copies.
296
+ const encryptCore = (
297
+ tHat: TArg<Poly[]>,
298
+ getA: TArg<(i: number, j: number) => Poly>,
299
+ msg: TArg<Uint8Array>,
300
+ seed: TArg<Uint8Array>
301
+ ): TRet<Uint8Array> => {
302
+ const rHat = [];
303
+ for (let i = 0; i < K; i++) rHat.push(crystals.NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
304
+ const tmp2 = new Uint16Array(N);
305
+ const u = [];
306
+ for (let i = 0; i < K; i++) {
307
+ const e1 = sampleCBD(PRF, seed, K + i, ETA2);
308
+ const tmp = new Uint16Array(N);
309
+ for (let j = 0; j < K; j++) {
310
+ const aij = getA(i, j); // A[j][i], inplace transpose access
311
+ polyAdd(tmp, MultiplyNTTs(aij, rHat[j])); // t += aij * rHat[j]
312
+ }
313
+ polyAdd(e1, crystals.NTT.decode(tmp)); // e1 += tmp
314
+ u.push(e1);
315
+ polyAdd(tmp2, MultiplyNTTs(tHat[i], rHat[i])); // t2 += tHat[i] * rHat[i]
316
+ cleanBytes(tmp);
317
+ }
318
+ const e2 = sampleCBD(PRF, seed, 2 * K, ETA2);
319
+ polyAdd(e2, crystals.NTT.decode(tmp2)); // e2 += tmp2
320
+ const v = poly1.decode(msg); // encode plaintext m into polynomial v
321
+ polyAdd(v, e2); // v += e2
322
+ cleanBytes(tHat, rHat, tmp2, e2);
323
+ return cipherCoder.encode([u, v]) as TRet<Uint8Array>;
324
+ };
240
325
  return {
241
326
  secretCoder,
242
327
  lengths: {
@@ -244,7 +329,7 @@ const genKPKE = (opts: KyberOpts) => {
244
329
  publicKey: publicCoder.bytesLen,
245
330
  cipherText: cipherCoder.bytesLen,
246
331
  },
247
- keygen: (seed: Uint8Array) => {
332
+ keygen: (seed: TArg<Uint8Array>) => {
248
333
  abytes(seed, 32, 'seed');
249
334
  const seedDst = new Uint8Array(33);
250
335
  seedDst.set(seed);
@@ -275,34 +360,39 @@ const genKPKE = (opts: KyberOpts) => {
275
360
  cleanBytes(rho, sigma, sHat, tHat, seedDst, seedHash);
276
361
  return res;
277
362
  },
278
- encrypt: (publicKey: Uint8Array, msg: Uint8Array, seed: Uint8Array) => {
363
+ encrypt: (
364
+ publicKey: TArg<Uint8Array>,
365
+ msg: TArg<Uint8Array>,
366
+ seed: TArg<Uint8Array>
367
+ ): TRet<Uint8Array> => {
279
368
  const [tHat, rho] = publicCoder.decode(publicKey);
280
- const rHat = [];
281
- for (let i = 0; i < K; i++) rHat.push(crystals.NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
282
369
  const x = XOF(rho);
283
- const tmp2 = new Uint16Array(N);
284
- const u = [];
285
- for (let i = 0; i < K; i++) {
286
- const e1 = sampleCBD(PRF, seed, K + i, ETA2);
287
- const tmp = new Uint16Array(N);
288
- for (let j = 0; j < K; j++) {
289
- const aij = SampleNTT(x.get(i, j)); // A[j][i], inplace transpose access
290
- polyAdd(tmp, MultiplyNTTs(aij, rHat[j])); // t += aij * rHat[j]
291
- }
292
- polyAdd(e1, crystals.NTT.decode(tmp)); // e1 += tmp
293
- u.push(e1);
294
- polyAdd(tmp2, MultiplyNTTs(tHat[i], rHat[i])); // t2 += tHat[i] * rHat[i]
295
- cleanBytes(tmp);
296
- }
370
+ const res = encryptCore(tHat as Poly[], (i, j) => SampleNTT(x.get(i, j)) as Poly, msg, seed);
297
371
  x.clean();
298
- const e2 = sampleCBD(PRF, seed, 2 * K, ETA2);
299
- polyAdd(e2, crystals.NTT.decode(tmp2)); // e2 += tmp2
300
- const v = poly1.decode(msg); // encode plaintext m into polynomial v
301
- polyAdd(v, e2); // v += e2
302
- cleanBytes(tHat, rHat, tmp2, e2);
303
- return cipherCoder.encode([u, v]);
372
+ return res;
304
373
  },
305
- decrypt: (cipherText: Uint8Array, privateKey: Uint8Array) => {
374
+ // Expands the full  matrix (public data derived from rho) once, so repeated encryptions
375
+ // against the same ek skip the K² SampleNTT XOF expansions. Cached polys are copied per
376
+ // call because encryptCore mutates its inputs in place.
377
+ prepare: (publicKey: TArg<Uint8Array>) => {
378
+ const [tHat, rho] = publicCoder.decode(publicKey);
379
+ const x = XOF(rho);
380
+ const A: Poly[] = [];
381
+ for (let i = 0; i < K; i++)
382
+ for (let j = 0; j < K; j++) A.push(SampleNTT(x.get(i, j)) as Poly);
383
+ x.clean();
384
+ return {
385
+ encrypt: (msg: TArg<Uint8Array>, seed: TArg<Uint8Array>): TRet<Uint8Array> =>
386
+ encryptCore(
387
+ (tHat as Poly[]).map((p) => p.slice() as Poly),
388
+ (i, j) => A[i * K + j].slice() as Poly,
389
+ msg,
390
+ seed
391
+ ),
392
+ clean: () => cleanBytes(tHat as Poly[], A),
393
+ };
394
+ },
395
+ decrypt: (cipherText: TArg<Uint8Array>, privateKey: TArg<Uint8Array>): TRet<Uint8Array> => {
306
396
  const [u, v] = cipherCoder.decode(cipherText);
307
397
  const sk = secretCoder.decode(privateKey); // s ← ByteDecode_12(dkPKE)
308
398
  const tmp = new Uint16Array(N);
@@ -310,7 +400,7 @@ const genKPKE = (opts: KyberOpts) => {
310
400
  for (let i = 0; i < K; i++) polyAdd(tmp, MultiplyNTTs(sk[i], crystals.NTT.encode(u[i])));
311
401
  polySub(v, crystals.NTT.decode(tmp)); // w = v' - tmp
312
402
  cleanBytes(tmp, sk, u);
313
- return poly1.encode(v);
403
+ return poly1.encode(v) as TRet<Uint8Array>;
314
404
  },
315
405
  };
316
406
  };
@@ -324,57 +414,65 @@ const genKPKE = (opts: KyberOpts) => {
324
414
  * mismatch, and zeroizing the non-returned shared-secret candidate; JS/JIT still provides no
325
415
  * constant-time guarantees for that path.
326
416
  */
327
- function createKyber(opts: KyberOpts) {
328
- const KPKE = genKPKE(opts);
329
- const { HASH256, HASH512, KDF } = opts;
417
+ function createKyber(opts: TArg<KyberOpts>): TRet<MLKEM> {
418
+ const rawOpts = opts as KyberOpts;
419
+ const KPKE = genKPKE(rawOpts);
420
+ const { HASH256, HASH512, KDF } = rawOpts;
330
421
  const { secretCoder: KPKESecretCoder, lengths } = KPKE;
331
422
  const secretCoder = splitCoder('secretKey', lengths.secretKey, lengths.publicKey, 32, 32);
332
423
  const msgLen = 32;
333
424
  const seedLen = 64;
334
- return {
335
- info: { type: 'ml-kem' },
336
- lengths: {
337
- ...lengths,
338
- seed: 64,
339
- msg: msgLen,
340
- msgRand: msgLen,
341
- secretKey: secretCoder.bytesLen,
342
- },
343
- keygen: (seed = randomBytes(seedLen)) => {
425
+ // FIPS-203 includes additional verification check for modulus
426
+ const validateModulus = (publicKey: TArg<Uint8Array>, fn: string) => {
427
+ const eke = (publicKey as Uint8Array).subarray(0, 384 * rawOpts.K);
428
+ // Copy because of inplace encoding
429
+ const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
430
+ // (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
431
+ // If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
432
+ const ok = equalBytes(ek, eke);
433
+ cleanBytes(ek);
434
+ if (!ok) throw new Error(`ML-KEM.${fn}: wrong publicKey modulus`);
435
+ };
436
+ const kemLengths = Object.freeze({
437
+ ...lengths,
438
+ seed: 64,
439
+ msg: msgLen,
440
+ msgRand: msgLen,
441
+ secretKey: secretCoder.bytesLen,
442
+ });
443
+ return Object.freeze({
444
+ info: Object.freeze({ type: 'ml-kem' }),
445
+ lengths: kemLengths,
446
+ keygen: (seed: TArg<Uint8Array> = randomBytes(seedLen)) => {
344
447
  abytes(seed, seedLen, 'seed');
345
448
  const { publicKey, secretKey: sk } = KPKE.keygen(seed.subarray(0, 32));
346
449
  const publicKeyHash = HASH256(publicKey);
347
450
  // (dkPKE||ek||H(ek)||z)
348
451
  const secretKey = secretCoder.encode([sk, publicKey, publicKeyHash, seed.subarray(32)]);
349
452
  cleanBytes(sk, publicKeyHash);
350
- return { publicKey, secretKey };
453
+ return {
454
+ publicKey: publicKey as TRet<Uint8Array>,
455
+ secretKey: secretKey as TRet<Uint8Array>,
456
+ };
351
457
  },
352
- getPublicKey: (secretKey: Uint8Array) => {
458
+ getPublicKey: (secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
353
459
  const [_sk, publicKey, _publicKeyHash, _z] = secretCoder.decode(secretKey);
354
- return Uint8Array.from(publicKey);
460
+ return Uint8Array.from(publicKey) as TRet<Uint8Array>;
355
461
  },
356
- encapsulate: (publicKey: Uint8Array, msg = randomBytes(msgLen)) => {
462
+ encapsulate: (publicKey: TArg<Uint8Array>, msg: TArg<Uint8Array> = randomBytes(msgLen)) => {
357
463
  abytes(publicKey, lengths.publicKey, 'publicKey');
358
464
  abytes(msg, msgLen, 'message');
359
-
360
- // FIPS-203 includes additional verification check for modulus
361
- const eke = publicKey.subarray(0, 384 * opts.K);
362
- // Copy because of inplace encoding
363
- const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
364
- // (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
365
- // If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
366
- if (!equalBytes(ek, eke)) {
367
- cleanBytes(ek);
368
- throw new Error('ML-KEM.encapsulate: wrong publicKey modulus');
369
- }
370
- cleanBytes(ek);
465
+ validateModulus(publicKey, 'encapsulate');
371
466
  // derive randomness
372
467
  const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest();
373
468
  const cipherText = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64));
374
469
  cleanBytes(kr.subarray(32));
375
- return { cipherText, sharedSecret: kr.subarray(0, 32) };
470
+ return {
471
+ cipherText: cipherText as TRet<Uint8Array>,
472
+ sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
473
+ };
376
474
  },
377
- decapsulate: (cipherText: Uint8Array, secretKey: Uint8Array) => {
475
+ decapsulate: (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
378
476
  abytes(secretKey, secretCoder.bytesLen, 'secretKey'); // 768*k + 96
379
477
  abytes(cipherText, lengths.cipherText, 'cipherText'); // 32(du*k + dv)
380
478
  // test ← H(dk[384𝑘 ∶ 768𝑘 + 32])) .
@@ -394,20 +492,71 @@ function createKyber(opts: KyberOpts) {
394
492
  // if ciphertexts do not match, “implicitly reject”
395
493
  const isValid = equalBytes(cipherText, cipherText2);
396
494
  const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
397
- cleanBytes(msg, cipherText2, !isValid ? Khat : Kbar);
398
- return isValid ? Khat : Kbar;
495
+ // kr[32:64] is the derived K-PKE encryption randomness: wipe it like encapsulate() does.
496
+ cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
497
+ return (isValid ? Khat : Kbar) as TRet<Uint8Array>;
399
498
  },
400
- };
499
+ /**
500
+ * Experimental prototype: pre-expand a public key so repeated encapsulate/decapsulate
501
+ * against the same key skip re-validation, H(ek), t̂ decoding and the K² SampleNTT
502
+ * XOF expansions of Â. Only public data is cached; see {@link KEMPrepared}.
503
+ */
504
+ prepare: (publicKey: TArg<Uint8Array>): TRet<KEMPrepared> => {
505
+ abytes(publicKey, lengths.publicKey, 'publicKey');
506
+ validateModulus(publicKey, 'prepare');
507
+ const ek = copyBytes(publicKey); // detach from the caller before caching
508
+ const publicKeyHash = HASH256(ek);
509
+ const cached = KPKE.prepare(ek);
510
+ return Object.freeze({
511
+ publicKey: ek as TRet<Uint8Array>,
512
+ encapsulate: (msg: TArg<Uint8Array> = randomBytes(msgLen)) => {
513
+ abytes(msg, msgLen, 'message');
514
+ const kr = HASH512.create().update(msg).update(publicKeyHash).digest();
515
+ const cipherText = cached.encrypt(msg, kr.subarray(32, 64));
516
+ cleanBytes(kr.subarray(32));
517
+ return {
518
+ cipherText: cipherText as TRet<Uint8Array>,
519
+ sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
520
+ };
521
+ },
522
+ decapsulate: (
523
+ cipherText: TArg<Uint8Array>,
524
+ secretKey: TArg<Uint8Array>
525
+ ): TRet<Uint8Array> => {
526
+ abytes(secretKey, secretCoder.bytesLen, 'secretKey');
527
+ abytes(cipherText, lengths.cipherText, 'cipherText');
528
+ const [sk, ekEmbedded, storedHash, z] = secretCoder.decode(secretKey);
529
+ // Under KEMPrepared's read-only publicKey contract, bind dk to the prepared key.
530
+ // Together with publicKeyHash = H(ek) computed in prepare(), this is equivalent to (and
531
+ // stronger than) FIPS 203 §7.3's `H(dk[384k : 768k+32]) == dk[768k+32 : 768k+64]`.
532
+ if (!equalBytes(ekEmbedded, ek) || !equalBytes(storedHash, publicKeyHash))
533
+ throw new Error('ML-KEM.decapsulate: secretKey does not match prepared publicKey');
534
+ const msg = KPKE.decrypt(cipherText, sk);
535
+ // derive randomness, Khat, rHat = G(mHat || h)
536
+ const kr = HASH512.create().update(msg).update(publicKeyHash).digest();
537
+ const Khat = kr.subarray(0, 32);
538
+ // re-encrypt using the derived randomness and cached Â/t̂
539
+ const cipherText2 = cached.encrypt(msg, kr.subarray(32, 64));
540
+ // if ciphertexts do not match, “implicitly reject”
541
+ const isValid = equalBytes(cipherText, cipherText2);
542
+ const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
543
+ cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
544
+ return (isValid ? Khat : Kbar) as TRet<Uint8Array>;
545
+ },
546
+ clean: cached.clean,
547
+ }) as TRet<KEMPrepared>;
548
+ },
549
+ });
401
550
  }
402
551
 
403
552
  // FIPS 203's PRF_eta binding: current callers use only 32-byte keys, one-byte nonces,
404
553
  // and dkLen values {128, 192}; out-of-range nonce numbers still wrap modulo 256 here.
405
- function shakePRF(dkLen: number, key: Uint8Array, nonce: number) {
554
+ function shakePRF(dkLen: number, key: TArg<Uint8Array>, nonce: number): TRet<Uint8Array> {
406
555
  return shake256
407
556
  .create({ dkLen })
408
557
  .update(key)
409
558
  .update(new Uint8Array([nonce]))
410
- .digest();
559
+ .digest() as TRet<Uint8Array>;
411
560
  }
412
561
 
413
562
  // Fixed ML-KEM hash/XOF bindings. `KDF` here is the spec's fixed 32-byte `J` call,
@@ -430,53 +579,65 @@ const mk = (params: KEMParam) =>
430
579
  /**
431
580
  * ML-KEM-512: Table 2 row `k=2, η1=3, η2=2, du=10, dv=4`; Table 3 sizes `800/1632/768/32`.
432
581
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
582
+ * @example
583
+ * Generate deterministic ML-KEM-512 keys, encapsulate a shared secret, and decapsulate it.
584
+ * ```ts
585
+ * import { ml_kem512 } from '@noble/post-quantum/ml-kem.js';
586
+ * const seed = new Uint8Array(ml_kem512.lengths.seed!);
587
+ * const { secretKey, publicKey } = ml_kem512.keygen(seed);
588
+ * const msg = new Uint8Array(ml_kem512.lengths.msgRand!);
589
+ * const { cipherText, sharedSecret } = ml_kem512.encapsulate(publicKey, msg);
590
+ * const recovered = ml_kem512.decapsulate(cipherText, secretKey);
591
+ * const publicKey2 = ml_kem512.getPublicKey(secretKey);
592
+ * ```
433
593
  */
434
- export const ml_kem512: KEM = /* @__PURE__ */ (() => mk(PARAMS[512]))();
594
+ export const ml_kem512: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[512]))();
435
595
  /**
436
596
  * ML-KEM-768: Table 2 row `k=3, η1=2, η2=2, du=10, dv=4`; Table 3 sizes `1184/2400/1088/32`.
437
597
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
438
598
  */
439
- export const ml_kem768: KEM = /* @__PURE__ */ (() => mk(PARAMS[768]))();
599
+ export const ml_kem768: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[768]))();
440
600
  /**
441
601
  * ML-KEM-1024: Table 2 row `k=4, η1=2, η2=2, du=11, dv=5`; Table 3 sizes `1568/3168/1568/32`.
442
602
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
443
603
  */
444
- export const ml_kem1024: KEM = /* @__PURE__ */ (() => mk(PARAMS[1024]))();
604
+ export const ml_kem1024: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[1024]))();
445
605
 
446
606
  // NOTE: for tests only, don't use. This keeps the exact internal ML-KEM math surfaces available
447
607
  // without re-implementing them in separate test code.
448
- export const __tests: any = /* @__PURE__ */ (() => ({
449
- Compress_d: (x: number, d: number) => {
450
- if (d < 1 || d > 11) throw new Error(`Compress_d: expected d in [1..11], got ${d}`);
451
- return compress(d).encode(x) & getMask(d);
452
- },
453
- Decompress_d: (y: number, d: number) => {
454
- if (d < 1 || d > 11) throw new Error(`Decompress_d: expected d in [1..11], got ${d}`);
455
- return compress(d).decode(y);
456
- },
457
- ByteEncode_d: (F: Uint16Array, d: number) => {
458
- if (d < 1 || d > 12) throw new Error(`ByteEncode_d: expected d in [1..12], got ${d}`);
459
- return byteCoder(d).encode(F);
460
- },
461
- ByteDecode_d: (B: Uint8Array, d: number) => {
462
- if (d < 1 || d > 12) throw new Error(`ByteDecode_d: expected d in [1..12], got ${d}`);
463
- return byteCoder(d).decode(B);
464
- },
465
- NTT: (f: Uint16Array) => crystals.NTT.encode(Uint16Array.from(f)),
466
- NTT_inv: (fHat: Uint16Array) => crystals.NTT.decode(Uint16Array.from(fHat)),
467
- MultiplyNTTs: (fHat: Uint16Array, gHat: Uint16Array) =>
468
- MultiplyNTTs(Uint16Array.from(fHat), Uint16Array.from(gHat)),
469
- SamplePolyCBD: (B: Uint8Array, eta: number) => {
470
- abytes(B, 64 * eta, 'B');
471
- return sampleCBDBytes(B, eta);
472
- },
473
- SampleNTT: (B: Uint8Array) => {
474
- abytes(B, 34, 'B');
475
- const xof = XOF128(B.subarray(0, 32));
476
- try {
477
- return SampleNTT(xof.get(B[32], B[33]));
478
- } finally {
479
- xof.clean();
480
- }
481
- },
482
- }))();
608
+ export const __tests: any = /* @__PURE__ */ (() =>
609
+ Object.freeze({
610
+ Compress_d: (x: number, d: number) => {
611
+ if (d < 1 || d > 11) throw new Error(`Compress_d: expected d in [1..11], got ${d}`);
612
+ return compress(d).encode(x) & getMask(d);
613
+ },
614
+ Decompress_d: (y: number, d: number) => {
615
+ if (d < 1 || d > 11) throw new Error(`Decompress_d: expected d in [1..11], got ${d}`);
616
+ return compress(d).decode(y);
617
+ },
618
+ ByteEncode_d: (F: TArg<Uint16Array>, d: number) => {
619
+ if (d < 1 || d > 12) throw new Error(`ByteEncode_d: expected d in [1..12], got ${d}`);
620
+ return byteCoder(d).encode(F as TRet<Uint16Array>);
621
+ },
622
+ ByteDecode_d: (B: TArg<Uint8Array>, d: number) => {
623
+ if (d < 1 || d > 12) throw new Error(`ByteDecode_d: expected d in [1..12], got ${d}`);
624
+ return byteCoder(d).decode(B);
625
+ },
626
+ NTT: (f: TArg<Uint16Array>) => crystals.NTT.encode(Uint16Array.from(f)),
627
+ NTT_inv: (fHat: TArg<Uint16Array>) => crystals.NTT.decode(Uint16Array.from(fHat)),
628
+ MultiplyNTTs: (fHat: TArg<Uint16Array>, gHat: TArg<Uint16Array>) =>
629
+ MultiplyNTTs(Uint16Array.from(fHat), Uint16Array.from(gHat)),
630
+ SamplePolyCBD: (B: TArg<Uint8Array>, eta: number) => {
631
+ abytes(B, 64 * eta, 'B');
632
+ return sampleCBDBytes(B, eta);
633
+ },
634
+ SampleNTT: (B: TArg<Uint8Array>) => {
635
+ abytes(B, 34, 'B');
636
+ const xof = XOF128(B.subarray(0, 32));
637
+ try {
638
+ return SampleNTT(xof.get(B[32], B[33]));
639
+ } finally {
640
+ xof.clean();
641
+ }
642
+ },
643
+ }))();