@noble/post-quantum 0.6.1 → 0.7.1

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
@@ -131,23 +131,33 @@ const polyCoder = (d: number) => (d === 12 ? byteCoder(12) : crystals.bitsCoder(
131
131
  // Poly is mod Q, so 12 bits
132
132
  type Poly = Uint16Array;
133
133
 
134
+ // Coefficients always stay reduced in [0, Q) here (samplers, NTT and coders all reduce),
135
+ // so one conditional correction replaces the generic mod().
134
136
  function polyAdd(a_: TArg<Poly>, b_: TArg<Poly>) {
135
137
  const a = a_ as Poly;
136
138
  const b = b_ as Poly;
137
139
  // Mutates `a` in place; callers must pass two N=256 polynomials.
138
- 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
+ }
139
144
  }
140
145
  function polySub(a_: TArg<Poly>, b_: TArg<Poly>) {
141
146
  const a = a_ as Poly;
142
147
  const b = b_ as Poly;
143
148
  // Mutates `a` in place; callers must pass two N=256 polynomials.
144
- 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
+ }
145
153
  }
146
154
 
147
155
  // FIPS-203: Computes the product of two degree-one polynomials with respect to a quadratic modulus
148
156
  function BaseCaseMultiply(a0: number, a1: number, b0: number, b1: number, zeta: number) {
149
157
  // `zeta` here is Algorithm 11's γ = ζ^(2BitRev_7(i)+1).
150
- 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);
151
161
  const c1 = crystals.mod(a0 * b1 + a1 * b0);
152
162
  return { c0, c1 };
153
163
  }
@@ -169,6 +179,33 @@ function MultiplyNTTs(f_: TArg<Poly>, g_: TArg<Poly>): TRet<Poly> {
169
179
 
170
180
  type PRF = (l: number, key: Uint8Array, nonce: number) => Uint8Array;
171
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
+
172
209
  type XofGet = ReturnType<ReturnType<XOF>['get']>;
173
210
 
174
211
  type KyberOpts = KEMParam & {
@@ -185,7 +222,7 @@ function SampleNTT(xof_: TArg<XofGet>): TRet<Poly> {
185
222
  // The reader must already bind the Algorithm 7 seed||j||i bytes
186
223
  // and return block lengths divisible by 3.
187
224
  const r: Poly = new Uint16Array(N);
188
- for (let j = 0; j < N; ) {
225
+ for (let j = 0; j < N;) {
189
226
  const b = xof();
190
227
  if (b.length % 3) throw new Error('SampleNTT: unaligned block');
191
228
  for (let i = 0; j < N && i + 3 <= b.length; i += 3) {
@@ -253,6 +290,38 @@ const genKPKE = (opts_: TArg<KyberOpts>) => {
253
290
  const secretCoder = vecCoder(polyCoder(12), K);
254
291
  const cipherCoder = splitCoder('ciphertext', vecCoder(polyU, K), polyV);
255
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
+ };
256
325
  return {
257
326
  secretCoder,
258
327
  lengths: {
@@ -297,30 +366,31 @@ const genKPKE = (opts_: TArg<KyberOpts>) => {
297
366
  seed: TArg<Uint8Array>
298
367
  ): TRet<Uint8Array> => {
299
368
  const [tHat, rho] = publicCoder.decode(publicKey);
300
- const rHat = [];
301
- for (let i = 0; i < K; i++) rHat.push(crystals.NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
302
369
  const x = XOF(rho);
303
- const tmp2 = new Uint16Array(N);
304
- const u = [];
305
- for (let i = 0; i < K; i++) {
306
- const e1 = sampleCBD(PRF, seed, K + i, ETA2);
307
- const tmp = new Uint16Array(N);
308
- for (let j = 0; j < K; j++) {
309
- const aij = SampleNTT(x.get(i, j)); // A[j][i], inplace transpose access
310
- polyAdd(tmp, MultiplyNTTs(aij, rHat[j])); // t += aij * rHat[j]
311
- }
312
- polyAdd(e1, crystals.NTT.decode(tmp)); // e1 += tmp
313
- u.push(e1);
314
- polyAdd(tmp2, MultiplyNTTs(tHat[i], rHat[i])); // t2 += tHat[i] * rHat[i]
315
- cleanBytes(tmp);
316
- }
370
+ const res = encryptCore(tHat as Poly[], (i, j) => SampleNTT(x.get(i, j)) as Poly, msg, seed);
371
+ x.clean();
372
+ return res;
373
+ },
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);
317
383
  x.clean();
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>;
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
+ };
324
394
  },
325
395
  decrypt: (cipherText: TArg<Uint8Array>, privateKey: TArg<Uint8Array>): TRet<Uint8Array> => {
326
396
  const [u, v] = cipherCoder.decode(cipherText);
@@ -329,8 +399,11 @@ const genKPKE = (opts_: TArg<KyberOpts>) => {
329
399
  // tmp += sk[i] * u[i]
330
400
  for (let i = 0; i < K; i++) polyAdd(tmp, MultiplyNTTs(sk[i], crystals.NTT.encode(u[i])));
331
401
  polySub(v, crystals.NTT.decode(tmp)); // w = v' - tmp
332
- cleanBytes(tmp, sk, u);
333
- return poly1.encode(v) as TRet<Uint8Array>;
402
+ // `v` now holds w, from which the plaintext is just a 1-bit threshold away, so wipe it too.
403
+ // encode() allocates its own buffer, so the returned bytes do not alias `v`.
404
+ const res = poly1.encode(v) as TRet<Uint8Array>;
405
+ cleanBytes(tmp, sk, u, v);
406
+ return res;
334
407
  },
335
408
  };
336
409
  };
@@ -339,12 +412,17 @@ const genKPKE = (opts_: TArg<KyberOpts>) => {
339
412
  * Public ML-KEM wrapper over the internal K-PKE subroutine.
340
413
  * `keygen(seed)` and `encapsulate(publicKey, msg)` are deterministic/test-oriented hooks that map
341
414
  * more directly to Algorithms 16-17 than to the pure no-input / random-internal Algorithms 19-20.
415
+ * `encapsulate`'s optional `msg` is the 32-byte message randomness `m` of Algorithm 17, the
416
+ * pre-image the shared secret is derived from, NOT a plaintext to encrypt: ML-KEM is a key
417
+ * encapsulation mechanism, not a cipher. Omit it to draw fresh randomness; pass it only to
418
+ * reproduce a known-answer vector, and only as 32 uniformly random bytes, since a low-entropy or
419
+ * reused value makes the shared secret predictable. The same holds for `keygen`'s optional `seed`.
342
420
  * decapsulate() tries to follow the Algorithms 18/21 implicit-reject structure as closely as
343
421
  * practical here by re-encrypting, comparing ciphertexts, returning `Khat` on match or `Kbar` on
344
422
  * mismatch, and zeroizing the non-returned shared-secret candidate; JS/JIT still provides no
345
423
  * constant-time guarantees for that path.
346
424
  */
347
- function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
425
+ function createKyber(opts: TArg<KyberOpts>): TRet<MLKEM> {
348
426
  const rawOpts = opts as KyberOpts;
349
427
  const KPKE = genKPKE(rawOpts);
350
428
  const { HASH256, HASH512, KDF } = rawOpts;
@@ -352,6 +430,17 @@ function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
352
430
  const secretCoder = splitCoder('secretKey', lengths.secretKey, lengths.publicKey, 32, 32);
353
431
  const msgLen = 32;
354
432
  const seedLen = 64;
433
+ // FIPS-203 includes additional verification check for modulus
434
+ const validateModulus = (publicKey: TArg<Uint8Array>, fn: string) => {
435
+ const eke = (publicKey as Uint8Array).subarray(0, 384 * rawOpts.K);
436
+ // Copy because of inplace encoding
437
+ const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
438
+ // (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
439
+ // If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
440
+ const ok = equalBytes(ek, eke);
441
+ cleanBytes(ek);
442
+ if (!ok) throw new Error(`ML-KEM.${fn}: wrong publicKey modulus`);
443
+ };
355
444
  const kemLengths = Object.freeze({
356
445
  ...lengths,
357
446
  seed: 64,
@@ -362,45 +451,58 @@ function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
362
451
  return Object.freeze({
363
452
  info: Object.freeze({ type: 'ml-kem' }),
364
453
  lengths: kemLengths,
365
- keygen: (seed: TArg<Uint8Array> = randomBytes(seedLen)) => {
366
- abytes(seed, seedLen, 'seed');
367
- const { publicKey, secretKey: sk } = KPKE.keygen(seed.subarray(0, 32));
368
- const publicKeyHash = HASH256(publicKey);
369
- // (dkPKE||ek||H(ek)||z)
370
- const secretKey = secretCoder.encode([sk, publicKey, publicKeyHash, seed.subarray(32)]);
371
- cleanBytes(sk, publicKeyHash);
372
- return {
373
- publicKey: publicKey as TRet<Uint8Array>,
374
- secretKey: secretKey as TRet<Uint8Array>,
375
- };
454
+ keygen: (seed?: TArg<Uint8Array>) => {
455
+ // A generated seed carries z (the implicit-rejection secret) and must be wiped once the
456
+ // secret key holds a copy, matching ml-dsa / slh-dsa / falcon keygen. A caller-supplied
457
+ // seed is the caller's to manage (and the immutability test requires it stay untouched).
458
+ const ownSeed = seed === undefined;
459
+ const s = ownSeed ? randomBytes(seedLen) : (seed as TArg<Uint8Array>);
460
+ let sk: Uint8Array | undefined;
461
+ let publicKeyHash: Uint8Array | undefined;
462
+ try {
463
+ abytes(s, seedLen, 'seed');
464
+ const keys = KPKE.keygen(s.subarray(0, 32));
465
+ const publicKey = keys.publicKey;
466
+ sk = keys.secretKey as Uint8Array;
467
+ publicKeyHash = HASH256(publicKey);
468
+ // (dkPKE||ek||H(ek)||z)
469
+ const secretKey = secretCoder.encode([sk, publicKey, publicKeyHash, s.subarray(32)]);
470
+ return {
471
+ publicKey: publicKey as TRet<Uint8Array>,
472
+ secretKey: secretKey as TRet<Uint8Array>,
473
+ };
474
+ } finally {
475
+ if (sk !== undefined) cleanBytes(sk);
476
+ if (publicKeyHash !== undefined) cleanBytes(publicKeyHash);
477
+ if (ownSeed) cleanBytes(s);
478
+ }
376
479
  },
377
480
  getPublicKey: (secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
378
481
  const [_sk, publicKey, _publicKeyHash, _z] = secretCoder.decode(secretKey);
379
482
  return Uint8Array.from(publicKey) as TRet<Uint8Array>;
380
483
  },
381
- encapsulate: (publicKey: TArg<Uint8Array>, msg: TArg<Uint8Array> = randomBytes(msgLen)) => {
382
- abytes(publicKey, lengths.publicKey, 'publicKey');
383
- abytes(msg, msgLen, 'message');
384
-
385
- // FIPS-203 includes additional verification check for modulus
386
- const eke = publicKey.subarray(0, 384 * opts.K);
387
- // Copy because of inplace encoding
388
- const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
389
- // (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
390
- // If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
391
- if (!equalBytes(ek, eke)) {
392
- cleanBytes(ek);
393
- throw new Error('ML-KEM.encapsulate: wrong publicKey modulus');
484
+ encapsulate: (publicKey: TArg<Uint8Array>, msg?: TArg<Uint8Array>) => {
485
+ // A generated message is the preimage of the shared secret (K = G(m || H(ek))[0:32]) and
486
+ // must be wiped. A caller-supplied message is the deterministic-randomness hook and the
487
+ // caller's to manage (the immutability test requires it stay untouched).
488
+ const ownMsg = msg === undefined;
489
+ const m = ownMsg ? randomBytes(msgLen) : (msg as TArg<Uint8Array>);
490
+ let kr: Uint8Array | undefined;
491
+ try {
492
+ abytes(publicKey, lengths.publicKey, 'publicKey');
493
+ abytes(m, msgLen, 'message');
494
+ validateModulus(publicKey, 'encapsulate');
495
+ // derive randomness
496
+ kr = HASH512.create().update(m).update(HASH256(publicKey)).digest();
497
+ const cipherText = KPKE.encrypt(publicKey, m, kr.subarray(32, 64));
498
+ return {
499
+ cipherText: cipherText as TRet<Uint8Array>,
500
+ sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
501
+ };
502
+ } finally {
503
+ if (kr !== undefined) cleanBytes(kr.subarray(32));
504
+ if (ownMsg) cleanBytes(m);
394
505
  }
395
- cleanBytes(ek);
396
- // derive randomness
397
- const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest();
398
- const cipherText = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64));
399
- cleanBytes(kr.subarray(32));
400
- return {
401
- cipherText: cipherText as TRet<Uint8Array>,
402
- sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
403
- };
404
506
  },
405
507
  decapsulate: (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
406
508
  abytes(secretKey, secretCoder.bytesLen, 'secretKey'); // 768*k + 96
@@ -422,9 +524,69 @@ function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
422
524
  // if ciphertexts do not match, “implicitly reject”
423
525
  const isValid = equalBytes(cipherText, cipherText2);
424
526
  const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
425
- cleanBytes(msg, cipherText2, !isValid ? Khat : Kbar);
527
+ // kr[32:64] is the derived K-PKE encryption randomness: wipe it like encapsulate() does.
528
+ cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
426
529
  return (isValid ? Khat : Kbar) as TRet<Uint8Array>;
427
530
  },
531
+ /**
532
+ * Experimental prototype: pre-expand a public key so repeated encapsulate/decapsulate
533
+ * against the same key skip re-validation, H(ek), t̂ decoding and the K² SampleNTT
534
+ * XOF expansions of Â. Only public data is cached; see {@link KEMPrepared}.
535
+ */
536
+ prepare: (publicKey: TArg<Uint8Array>): TRet<KEMPrepared> => {
537
+ abytes(publicKey, lengths.publicKey, 'publicKey');
538
+ validateModulus(publicKey, 'prepare');
539
+ const ek = copyBytes(publicKey); // detach from the caller before caching
540
+ const publicKeyHash = HASH256(ek);
541
+ const cached = KPKE.prepare(ek);
542
+ return Object.freeze({
543
+ publicKey: ek as TRet<Uint8Array>,
544
+ encapsulate: (msg?: TArg<Uint8Array>) => {
545
+ // As in the non-prepared encapsulate: a generated message is the shared-secret
546
+ // preimage and is wiped; a caller-supplied one is left untouched.
547
+ const ownMsg = msg === undefined;
548
+ const m = ownMsg ? randomBytes(msgLen) : (msg as TArg<Uint8Array>);
549
+ let kr: Uint8Array | undefined;
550
+ try {
551
+ abytes(m, msgLen, 'message');
552
+ kr = HASH512.create().update(m).update(publicKeyHash).digest();
553
+ const cipherText = cached.encrypt(m, kr.subarray(32, 64));
554
+ return {
555
+ cipherText: cipherText as TRet<Uint8Array>,
556
+ sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
557
+ };
558
+ } finally {
559
+ if (kr !== undefined) cleanBytes(kr.subarray(32));
560
+ if (ownMsg) cleanBytes(m);
561
+ }
562
+ },
563
+ decapsulate: (
564
+ cipherText: TArg<Uint8Array>,
565
+ secretKey: TArg<Uint8Array>
566
+ ): TRet<Uint8Array> => {
567
+ abytes(secretKey, secretCoder.bytesLen, 'secretKey');
568
+ abytes(cipherText, lengths.cipherText, 'cipherText');
569
+ const [sk, ekEmbedded, storedHash, z] = secretCoder.decode(secretKey);
570
+ // Under KEMPrepared's read-only publicKey contract, bind dk to the prepared key.
571
+ // Together with publicKeyHash = H(ek) computed in prepare(), this is equivalent to (and
572
+ // stronger than) FIPS 203 §7.3's `H(dk[384k : 768k+32]) == dk[768k+32 : 768k+64]`.
573
+ if (!equalBytes(ekEmbedded, ek) || !equalBytes(storedHash, publicKeyHash))
574
+ throw new Error('ML-KEM.decapsulate: secretKey does not match prepared publicKey');
575
+ const msg = KPKE.decrypt(cipherText, sk);
576
+ // derive randomness, Khat, rHat = G(mHat || h)
577
+ const kr = HASH512.create().update(msg).update(publicKeyHash).digest();
578
+ const Khat = kr.subarray(0, 32);
579
+ // re-encrypt using the derived randomness and cached Â/t̂
580
+ const cipherText2 = cached.encrypt(msg, kr.subarray(32, 64));
581
+ // if ciphertexts do not match, “implicitly reject”
582
+ const isValid = equalBytes(cipherText, cipherText2);
583
+ const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
584
+ cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
585
+ return (isValid ? Khat : Kbar) as TRet<Uint8Array>;
586
+ },
587
+ clean: cached.clean,
588
+ }) as TRet<KEMPrepared>;
589
+ },
428
590
  });
429
591
  }
430
592
 
@@ -458,18 +620,29 @@ const mk = (params: KEMParam) =>
458
620
  /**
459
621
  * ML-KEM-512: Table 2 row `k=2, η1=3, η2=2, du=10, dv=4`; Table 3 sizes `800/1632/768/32`.
460
622
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
623
+ * @example
624
+ * Generate deterministic ML-KEM-512 keys, encapsulate a shared secret, and decapsulate it.
625
+ * ```ts
626
+ * import { ml_kem512 } from '@noble/post-quantum/ml-kem.js';
627
+ * const seed = new Uint8Array(ml_kem512.lengths.seed!);
628
+ * const { secretKey, publicKey } = ml_kem512.keygen(seed);
629
+ * const msg = new Uint8Array(ml_kem512.lengths.msgRand!);
630
+ * const { cipherText, sharedSecret } = ml_kem512.encapsulate(publicKey, msg);
631
+ * const recovered = ml_kem512.decapsulate(cipherText, secretKey);
632
+ * const publicKey2 = ml_kem512.getPublicKey(secretKey);
633
+ * ```
461
634
  */
462
- export const ml_kem512: TRet<KEM> = /* @__PURE__ */ (() => mk(PARAMS[512]))();
635
+ export const ml_kem512: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[512]))();
463
636
  /**
464
637
  * ML-KEM-768: Table 2 row `k=3, η1=2, η2=2, du=10, dv=4`; Table 3 sizes `1184/2400/1088/32`.
465
638
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
466
639
  */
467
- export const ml_kem768: TRet<KEM> = /* @__PURE__ */ (() => mk(PARAMS[768]))();
640
+ export const ml_kem768: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[768]))();
468
641
  /**
469
642
  * ML-KEM-1024: Table 2 row `k=4, η1=2, η2=2, du=11, dv=5`; Table 3 sizes `1568/3168/1568/32`.
470
643
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
471
644
  */
472
- export const ml_kem1024: TRet<KEM> = /* @__PURE__ */ (() => mk(PARAMS[1024]))();
645
+ export const ml_kem1024: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[1024]))();
473
646
 
474
647
  // NOTE: for tests only, don't use. This keeps the exact internal ML-KEM math surfaces available
475
648
  // without re-implementing them in separate test code.