@noble/post-quantum 0.6.1 → 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/ml-dsa.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { CHash } from '@noble/hashes/utils.js';
1
2
  import { type CryptoKeys, type Signer, type SigOpts, type TArg, type TRet, type VerOpts } from './utils.ts';
2
3
  /** Internal ML-DSA options. */
3
4
  export type DSAInternalOpts = {
@@ -17,6 +18,13 @@ export type DSAInternal = CryptoKeys & {
17
18
  /** Public ML-DSA signer surface. */
18
19
  export type DSA = Signer & {
19
20
  internal: TRet<DSAInternal>;
21
+ securityLevel: number;
22
+ /**
23
+ * HashML-DSA (FIPS 204 §5.4) variant which signs a pre-hashed message.
24
+ * @param hash - Approved hash, checked against the parameter set security level.
25
+ * @returns Signer which pre-hashes `msg` before formatting `M'`.
26
+ */
27
+ prehash: (hash: TArg<CHash>) => TRet<Signer>;
20
28
  };
21
29
  /** Various lattice params. */
22
30
  /** Public ML-DSA parameter-set description. */
@@ -45,10 +53,28 @@ export type DSAParam = {
45
53
  * while `C_TILDE_BYTES`, `TR_BYTES`, `CRH_BYTES`, and `securityLevel` live in the preset wrappers.
46
54
  */
47
55
  export declare const PARAMS: Record<string, DSAParam>;
48
- /** ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD. */
56
+ /**
57
+ * ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD.
58
+ * @example
59
+ * Generate deterministic ML-DSA-44 keys, sign one message, and verify the signature.
60
+ * ```ts
61
+ * import { sha256 } from '@noble/hashes/sha2.js';
62
+ * import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
63
+ * const seed = new Uint8Array(ml_dsa44.lengths.seed!);
64
+ * const { secretKey, publicKey } = ml_dsa44.keygen(seed);
65
+ * const msg = new TextEncoder().encode('hello noble');
66
+ * const sig = ml_dsa44.sign(msg, secretKey);
67
+ * const isValid = ml_dsa44.verify(sig, msg, publicKey);
68
+ * const recovered = ml_dsa44.getPublicKey(secretKey);
69
+ * const context = new Uint8Array([1, 2, 3]);
70
+ * const prehash = ml_dsa44.prehash(sha256);
71
+ * const preSig = prehash.sign(msg, secretKey, { context });
72
+ * const preValid = prehash.verify(preSig, msg, publicKey, { context });
73
+ * const internalSig = ml_dsa44.internal.sign(msg, secretKey);
74
+ * ```
75
+ */
49
76
  export declare const ml_dsa44: TRet<DSA>;
50
77
  /** ML-DSA-65 for 192-bit security level. Not recommended after 2030, as per ASD. */
51
78
  export declare const ml_dsa65: TRet<DSA>;
52
79
  /** ML-DSA-87 for 256-bit security level. OK after 2030, as per ASD. */
53
80
  export declare const ml_dsa87: TRet<DSA>;
54
- //# sourceMappingURL=ml-dsa.d.ts.map
package/ml-dsa.js CHANGED
@@ -72,6 +72,8 @@ const polyCoder = (d, compress = id, verify = id) => crystals.bitsCoder(d, {
72
72
  decode: (i) => verify(compress(i)),
73
73
  });
74
74
  // Mutates `a` in place; callers must pass same-length polynomials.
75
+ // NOTE: conditional-reduction variants (as in ml-kem) were measured performance-neutral here —
76
+ // int32 `%` with 23-bit Q is already cheap — so the simpler mod() form is kept for audit.
75
77
  const polyAdd = (a_, b_) => {
76
78
  const a = a_;
77
79
  const b = b_;
@@ -179,14 +181,16 @@ function getDilithium(opts_) {
179
181
  // See dilithium-py README section "Optimising decomposition and making hints".
180
182
  return res0;
181
183
  };
184
+ // m = (q-1)/(2γ2): 44 for ML-DSA-44, 16 for 65/87. Hoisted out of UseHint, which runs
185
+ // per coefficient during verification.
186
+ const HINT_M = Math.floor((Q - 1) / (2 * GAMMA2));
182
187
  const UseHint = (h, r) => {
183
188
  // Returns the high bits of r adjusted according to hint h
184
- const m = Math.floor((Q - 1) / (2 * GAMMA2));
185
189
  const { r1, r0 } = decompose(r);
186
190
  // 3: if h = 1 and r0 > 0 return (r1 + 1) mod m
187
191
  // 4: if h = 1 and r0 ≤ 0 return (r1 − 1) mod m
188
192
  if (h === 1)
189
- return r0 > 0 ? crystals.mod(r1 + 1, m) | 0 : crystals.mod(r1 - 1, m) | 0;
193
+ return r0 > 0 ? crystals.mod(r1 + 1, HINT_M) | 0 : crystals.mod(r1 - 1, HINT_M) | 0;
190
194
  return r1 | 0;
191
195
  };
192
196
  const Power2Round = (r) => {
@@ -421,11 +425,34 @@ function getDilithium(opts_) {
421
425
  sign: (msg, secretKey, opts = {}) => {
422
426
  validateSigOpts(opts);
423
427
  validateInternalOpts(opts);
424
- let { extraEntropy: random, externalMu = false } = opts;
428
+ const { extraEntropy: random, externalMu = false } = opts;
429
+ // FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
430
+ if (externalMu)
431
+ abytes(msg, CRH_BYTES, 'mu');
432
+ // Prepare entropy before touching decoded secrets: randomBytes() may throw, and an RNG
433
+ // failure must not leave expanded secret-polynomial copies behind.
434
+ const ownRnd = random === false || random === undefined;
435
+ const rnd = random === false
436
+ ? new Uint8Array(32)
437
+ : random === undefined
438
+ ? randomBytes(signRandBytes)
439
+ : random;
440
+ abytes(rnd, 32, 'extraEntropy');
425
441
  // This part can be pre-cached per secretKey, but there is only minor performance improvement,
426
442
  // since we re-use a lot of variables to computation.
427
443
  // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
428
- const [rho, _K, tr, s1, s2, t0] = secretCoder.decode(secretKey);
444
+ const decoded = (() => {
445
+ try {
446
+ return secretCoder.decode(secretKey);
447
+ }
448
+ catch (error) {
449
+ // A malformed key must not strand entropy owned by the library.
450
+ if (ownRnd)
451
+ cleanBytes(rnd);
452
+ throw error;
453
+ }
454
+ })();
455
+ const [rho, _K, tr, s1, s2, t0] = decoded;
429
456
  // Cache matrix to avoid re-compute later
430
457
  const A = []; // A ← ExpandA(ρ)
431
458
  const xof = XOF128(rho);
@@ -448,19 +475,15 @@ function getDilithium(opts_) {
448
475
  : // 6: µ ← H(tr||M, 512)
449
476
  // ▷ Compute message representative µ
450
477
  shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest();
451
- // Compute private random seed
452
- const rnd = random === false
453
- ? new Uint8Array(32)
454
- : random === undefined
455
- ? randomBytes(signRandBytes)
456
- : random;
457
- abytes(rnd, 32, 'extraEntropy');
458
478
  const rhoprime = shake256
459
479
  .create({ dkLen: CRH_BYTES })
460
480
  .update(_K)
461
481
  .update(rnd)
462
482
  .update(mu)
463
483
  .digest(); // ρ′← H(K||rnd||µ, 512)
484
+ // Only wipe entropy we generated; caller-provided extraEntropy stays caller-owned.
485
+ if (ownRnd)
486
+ cleanBytes(rnd);
464
487
  abytes(rhoprime, CRH_BYTES);
465
488
  const x256 = XOF256(rhoprime, ZCoder.bytesLen);
466
489
  // Rejection sampling loop
@@ -532,6 +555,9 @@ function getDilithium(opts_) {
532
555
  verify: (sig, msg, publicKey, opts = {}) => {
533
556
  validateInternalOpts(opts);
534
557
  const { externalMu = false } = opts;
558
+ // FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
559
+ if (externalMu)
560
+ abytes(msg, CRH_BYTES, 'mu');
535
561
  // ML-DSA.Verify(pk, M, σ): Verifes a signature σ for a message M.
536
562
  const [rho, t1] = publicCoder.decode(publicKey); // (ρ, t1) ← pkDecode(pk)
537
563
  const tr = shake256(publicKey, { dkLen: TR_BYTES }); // 6: tr ← H(BytesToBits(pk), 512)
@@ -604,10 +630,12 @@ function getDilithium(opts_) {
604
630
  },
605
631
  verify: (sig, msg, publicKey, opts = {}) => {
606
632
  validateVerOpts(opts);
633
+ abytes(sig, undefined, 'signature');
607
634
  return internal.verify(sig, getMessage(msg, opts.context), publicKey);
608
635
  },
609
636
  prehash: (hash) => {
610
637
  checkHash(hash, securityLevel);
638
+ const rawHash = hash;
611
639
  return Object.freeze({
612
640
  info: Object.freeze({ type: 'hashml-dsa' }),
613
641
  securityLevel: securityLevel,
@@ -616,20 +644,40 @@ function getDilithium(opts_) {
616
644
  getPublicKey: internal.getPublicKey,
617
645
  sign: (msg, secretKey, opts = {}) => {
618
646
  validateSigOpts(opts);
619
- const M = getMessagePrehash(hash, msg, opts.context);
647
+ const M = getMessagePrehash(rawHash, msg, opts.context);
620
648
  const res = internal.sign(M, secretKey, opts);
621
649
  cleanBytes(M);
622
650
  return res;
623
651
  },
624
652
  verify: (sig, msg, publicKey, opts = {}) => {
625
653
  validateVerOpts(opts);
626
- return internal.verify(sig, getMessagePrehash(hash, msg, opts.context), publicKey);
654
+ abytes(sig, undefined, 'signature');
655
+ return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey);
627
656
  },
628
657
  });
629
658
  },
630
659
  });
631
660
  }
632
- /** ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD. */
661
+ /**
662
+ * ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD.
663
+ * @example
664
+ * Generate deterministic ML-DSA-44 keys, sign one message, and verify the signature.
665
+ * ```ts
666
+ * import { sha256 } from '@noble/hashes/sha2.js';
667
+ * import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
668
+ * const seed = new Uint8Array(ml_dsa44.lengths.seed!);
669
+ * const { secretKey, publicKey } = ml_dsa44.keygen(seed);
670
+ * const msg = new TextEncoder().encode('hello noble');
671
+ * const sig = ml_dsa44.sign(msg, secretKey);
672
+ * const isValid = ml_dsa44.verify(sig, msg, publicKey);
673
+ * const recovered = ml_dsa44.getPublicKey(secretKey);
674
+ * const context = new Uint8Array([1, 2, 3]);
675
+ * const prehash = ml_dsa44.prehash(sha256);
676
+ * const preSig = prehash.sign(msg, secretKey, { context });
677
+ * const preValid = prehash.verify(preSig, msg, publicKey, { context });
678
+ * const internalSig = ml_dsa44.internal.sign(msg, secretKey);
679
+ * ```
680
+ */
633
681
  export const ml_dsa44 = /* @__PURE__ */ (() => getDilithium({
634
682
  ...PARAMS[2],
635
683
  CRH_BYTES: 64,
@@ -659,4 +707,3 @@ export const ml_dsa87 = /* @__PURE__ */ (() => getDilithium({
659
707
  XOF256,
660
708
  securityLevel: 256,
661
709
  }))();
662
- //# sourceMappingURL=ml-dsa.js.map
package/ml-kem.d.ts CHANGED
@@ -26,20 +26,61 @@ export type KEMParam = {
26
26
  * not a generic security label.
27
27
  */
28
28
  export declare const PARAMS: Record<string, KEMParam>;
29
+ /**
30
+ * Prepared (pre-expanded) ML-KEM public key. Experimental prototype.
31
+ * Caches only public data: packed ek, the expanded matrix Â, decoded t̂ and H(ek). No secret
32
+ * material is retained between calls; secret keys passed to `decapsulate` are decoded and wiped
33
+ * per call, exactly like the one-shot API. `clean()` wipes the expanded Â/t̂ cache; the packed
34
+ * public key and H(ek) are public and are not wiped. The object must not be used afterwards.
35
+ */
36
+ export type KEMPrepared = {
37
+ /**
38
+ * Detached copy of the source public key. Treat as read-only while the prepared object is in use.
39
+ * Callers may wipe it after final use; any mutation invalidates subsequent operations.
40
+ */
41
+ publicKey: Uint8Array;
42
+ /** Same as `KEM.encapsulate`, minus per-call ek re-validation and  re-expansion. */
43
+ encapsulate: (msg?: Uint8Array) => {
44
+ cipherText: Uint8Array;
45
+ sharedSecret: Uint8Array;
46
+ };
47
+ /**
48
+ * Same as `KEM.decapsulate`; throws if `secretKey` does not embed this public key.
49
+ * The embedded-ek byte comparison plus stored-hash comparison is equivalent to the
50
+ * FIPS 203 §7.3 hash input check.
51
+ */
52
+ decapsulate: (cipherText: Uint8Array, secretKey: Uint8Array) => Uint8Array;
53
+ /** Wipe cached (public) data. */
54
+ clean: () => void;
55
+ };
56
+ /** KEM with prepared-key support. */
57
+ export type MLKEM = KEM & {
58
+ prepare: (publicKey: Uint8Array) => KEMPrepared;
59
+ };
29
60
  /**
30
61
  * ML-KEM-512: Table 2 row `k=2, η1=3, η2=2, du=10, dv=4`; Table 3 sizes `800/1632/768/32`.
31
62
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
63
+ * @example
64
+ * Generate deterministic ML-KEM-512 keys, encapsulate a shared secret, and decapsulate it.
65
+ * ```ts
66
+ * import { ml_kem512 } from '@noble/post-quantum/ml-kem.js';
67
+ * const seed = new Uint8Array(ml_kem512.lengths.seed!);
68
+ * const { secretKey, publicKey } = ml_kem512.keygen(seed);
69
+ * const msg = new Uint8Array(ml_kem512.lengths.msgRand!);
70
+ * const { cipherText, sharedSecret } = ml_kem512.encapsulate(publicKey, msg);
71
+ * const recovered = ml_kem512.decapsulate(cipherText, secretKey);
72
+ * const publicKey2 = ml_kem512.getPublicKey(secretKey);
73
+ * ```
32
74
  */
33
- export declare const ml_kem512: TRet<KEM>;
75
+ export declare const ml_kem512: TRet<MLKEM>;
34
76
  /**
35
77
  * ML-KEM-768: Table 2 row `k=3, η1=2, η2=2, du=10, dv=4`; Table 3 sizes `1184/2400/1088/32`.
36
78
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
37
79
  */
38
- export declare const ml_kem768: TRet<KEM>;
80
+ export declare const ml_kem768: TRet<MLKEM>;
39
81
  /**
40
82
  * ML-KEM-1024: Table 2 row `k=4, η1=2, η2=2, du=11, dv=5`; Table 3 sizes `1568/3168/1568/32`.
41
83
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
42
84
  */
43
- export declare const ml_kem1024: TRet<KEM>;
85
+ export declare const ml_kem1024: TRet<MLKEM>;
44
86
  export declare const __tests: any;
45
- //# sourceMappingURL=ml-kem.d.ts.map
package/ml-kem.js CHANGED
@@ -84,24 +84,32 @@ const byteCoder = (d) => crystals.bitsCoder(d, d === 12
84
84
  // Kinda like convertRadix2 from @scure/base.
85
85
  // decode(encode(t)) == t, but there is loss of information on encode(decode(t))
86
86
  const polyCoder = (d) => (d === 12 ? byteCoder(12) : crystals.bitsCoder(d, compress(d)));
87
+ // Coefficients always stay reduced in [0, Q) here (samplers, NTT and coders all reduce),
88
+ // so one conditional correction replaces the generic mod().
87
89
  function polyAdd(a_, b_) {
88
90
  const a = a_;
89
91
  const b = b_;
90
92
  // Mutates `a` in place; callers must pass two N=256 polynomials.
91
- for (let i = 0; i < N; i++)
92
- a[i] = crystals.mod(a[i] + b[i]); // a += b
93
+ for (let i = 0; i < N; i++) {
94
+ const r = a[i] + b[i]; // a += b
95
+ a[i] = r >= Q ? r - Q : r;
96
+ }
93
97
  }
94
98
  function polySub(a_, b_) {
95
99
  const a = a_;
96
100
  const b = b_;
97
101
  // Mutates `a` in place; callers must pass two N=256 polynomials.
98
- for (let i = 0; i < N; i++)
99
- a[i] = crystals.mod(a[i] - b[i]); // a -= b
102
+ for (let i = 0; i < N; i++) {
103
+ const r = a[i] - b[i]; // a -= b
104
+ a[i] = r < 0 ? r + Q : r;
105
+ }
100
106
  }
101
107
  // FIPS-203: Computes the product of two degree-one polynomials with respect to a quadratic modulus
102
108
  function BaseCaseMultiply(a0, a1, b0, b1, zeta) {
103
109
  // `zeta` here is Algorithm 11's γ = ζ^(2BitRev_7(i)+1).
104
- const c0 = crystals.mod(a1 * b1 * zeta + a0 * b0);
110
+ // Reduce a1*b1 before multiplying by zeta: a1*b1*zeta would reach ~2^35, forcing JS engines
111
+ // into slow float fmod; with the extra reduction every intermediate fits int32.
112
+ const c0 = crystals.mod(crystals.mod(a1 * b1) * zeta + a0 * b0);
105
113
  const c1 = crystals.mod(a0 * b1 + a1 * b0);
106
114
  return { c0, c1 };
107
115
  }
@@ -191,6 +199,34 @@ const genKPKE = (opts_) => {
191
199
  const secretCoder = vecCoder(polyCoder(12), K);
192
200
  const cipherCoder = splitCoder('ciphertext', vecCoder(polyU, K), polyV);
193
201
  const seedCoder = splitCoder('seed', 32, 32);
202
+ // Algorithm 14 (K-PKE.Encrypt) core, after ek parsing. `tHat` and every poly returned by
203
+ // `getA(i, j)` are treated as disposable scratch: they are mutated in place and wiped/dropped,
204
+ // so callers holding cached copies must pass fresh copies.
205
+ const encryptCore = (tHat, getA, msg, seed) => {
206
+ const rHat = [];
207
+ for (let i = 0; i < K; i++)
208
+ rHat.push(crystals.NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
209
+ const tmp2 = new Uint16Array(N);
210
+ const u = [];
211
+ for (let i = 0; i < K; i++) {
212
+ const e1 = sampleCBD(PRF, seed, K + i, ETA2);
213
+ const tmp = new Uint16Array(N);
214
+ for (let j = 0; j < K; j++) {
215
+ const aij = getA(i, j); // A[j][i], inplace transpose access
216
+ polyAdd(tmp, MultiplyNTTs(aij, rHat[j])); // t += aij * rHat[j]
217
+ }
218
+ polyAdd(e1, crystals.NTT.decode(tmp)); // e1 += tmp
219
+ u.push(e1);
220
+ polyAdd(tmp2, MultiplyNTTs(tHat[i], rHat[i])); // t2 += tHat[i] * rHat[i]
221
+ cleanBytes(tmp);
222
+ }
223
+ const e2 = sampleCBD(PRF, seed, 2 * K, ETA2);
224
+ polyAdd(e2, crystals.NTT.decode(tmp2)); // e2 += tmp2
225
+ const v = poly1.decode(msg); // encode plaintext m into polynomial v
226
+ polyAdd(v, e2); // v += e2
227
+ cleanBytes(tHat, rHat, tmp2, e2);
228
+ return cipherCoder.encode([u, v]);
229
+ };
194
230
  return {
195
231
  secretCoder,
196
232
  lengths: {
@@ -231,31 +267,26 @@ const genKPKE = (opts_) => {
231
267
  },
232
268
  encrypt: (publicKey, msg, seed) => {
233
269
  const [tHat, rho] = publicCoder.decode(publicKey);
234
- const rHat = [];
235
- for (let i = 0; i < K; i++)
236
- rHat.push(crystals.NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
237
270
  const x = XOF(rho);
238
- const tmp2 = new Uint16Array(N);
239
- const u = [];
240
- for (let i = 0; i < K; i++) {
241
- const e1 = sampleCBD(PRF, seed, K + i, ETA2);
242
- const tmp = new Uint16Array(N);
243
- for (let j = 0; j < K; j++) {
244
- const aij = SampleNTT(x.get(i, j)); // A[j][i], inplace transpose access
245
- polyAdd(tmp, MultiplyNTTs(aij, rHat[j])); // t += aij * rHat[j]
246
- }
247
- polyAdd(e1, crystals.NTT.decode(tmp)); // e1 += tmp
248
- u.push(e1);
249
- polyAdd(tmp2, MultiplyNTTs(tHat[i], rHat[i])); // t2 += tHat[i] * rHat[i]
250
- cleanBytes(tmp);
251
- }
271
+ const res = encryptCore(tHat, (i, j) => SampleNTT(x.get(i, j)), msg, seed);
252
272
  x.clean();
253
- const e2 = sampleCBD(PRF, seed, 2 * K, ETA2);
254
- polyAdd(e2, crystals.NTT.decode(tmp2)); // e2 += tmp2
255
- const v = poly1.decode(msg); // encode plaintext m into polynomial v
256
- polyAdd(v, e2); // v += e2
257
- cleanBytes(tHat, rHat, tmp2, e2);
258
- return cipherCoder.encode([u, v]);
273
+ return res;
274
+ },
275
+ // Expands the full  matrix (public data derived from rho) once, so repeated encryptions
276
+ // against the same ek skip the K² SampleNTT XOF expansions. Cached polys are copied per
277
+ // call because encryptCore mutates its inputs in place.
278
+ prepare: (publicKey) => {
279
+ const [tHat, rho] = publicCoder.decode(publicKey);
280
+ const x = XOF(rho);
281
+ const A = [];
282
+ for (let i = 0; i < K; i++)
283
+ for (let j = 0; j < K; j++)
284
+ A.push(SampleNTT(x.get(i, j)));
285
+ x.clean();
286
+ return {
287
+ encrypt: (msg, seed) => encryptCore(tHat.map((p) => p.slice()), (i, j) => A[i * K + j].slice(), msg, seed),
288
+ clean: () => cleanBytes(tHat, A),
289
+ };
259
290
  },
260
291
  decrypt: (cipherText, privateKey) => {
261
292
  const [u, v] = cipherCoder.decode(cipherText);
@@ -287,6 +318,18 @@ function createKyber(opts) {
287
318
  const secretCoder = splitCoder('secretKey', lengths.secretKey, lengths.publicKey, 32, 32);
288
319
  const msgLen = 32;
289
320
  const seedLen = 64;
321
+ // FIPS-203 includes additional verification check for modulus
322
+ const validateModulus = (publicKey, fn) => {
323
+ const eke = publicKey.subarray(0, 384 * rawOpts.K);
324
+ // Copy because of inplace encoding
325
+ const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
326
+ // (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
327
+ // If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
328
+ const ok = equalBytes(ek, eke);
329
+ cleanBytes(ek);
330
+ if (!ok)
331
+ throw new Error(`ML-KEM.${fn}: wrong publicKey modulus`);
332
+ };
290
333
  const kemLengths = Object.freeze({
291
334
  ...lengths,
292
335
  seed: 64,
@@ -316,17 +359,7 @@ function createKyber(opts) {
316
359
  encapsulate: (publicKey, msg = randomBytes(msgLen)) => {
317
360
  abytes(publicKey, lengths.publicKey, 'publicKey');
318
361
  abytes(msg, msgLen, 'message');
319
- // FIPS-203 includes additional verification check for modulus
320
- const eke = publicKey.subarray(0, 384 * opts.K);
321
- // Copy because of inplace encoding
322
- const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
323
- // (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
324
- // If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
325
- if (!equalBytes(ek, eke)) {
326
- cleanBytes(ek);
327
- throw new Error('ML-KEM.encapsulate: wrong publicKey modulus');
328
- }
329
- cleanBytes(ek);
362
+ validateModulus(publicKey, 'encapsulate');
330
363
  // derive randomness
331
364
  const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest();
332
365
  const cipherText = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64));
@@ -356,9 +389,57 @@ function createKyber(opts) {
356
389
  // if ciphertexts do not match, “implicitly reject”
357
390
  const isValid = equalBytes(cipherText, cipherText2);
358
391
  const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
359
- cleanBytes(msg, cipherText2, !isValid ? Khat : Kbar);
392
+ // kr[32:64] is the derived K-PKE encryption randomness: wipe it like encapsulate() does.
393
+ cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
360
394
  return (isValid ? Khat : Kbar);
361
395
  },
396
+ /**
397
+ * Experimental prototype: pre-expand a public key so repeated encapsulate/decapsulate
398
+ * against the same key skip re-validation, H(ek), t̂ decoding and the K² SampleNTT
399
+ * XOF expansions of Â. Only public data is cached; see {@link KEMPrepared}.
400
+ */
401
+ prepare: (publicKey) => {
402
+ abytes(publicKey, lengths.publicKey, 'publicKey');
403
+ validateModulus(publicKey, 'prepare');
404
+ const ek = copyBytes(publicKey); // detach from the caller before caching
405
+ const publicKeyHash = HASH256(ek);
406
+ const cached = KPKE.prepare(ek);
407
+ return Object.freeze({
408
+ publicKey: ek,
409
+ encapsulate: (msg = randomBytes(msgLen)) => {
410
+ abytes(msg, msgLen, 'message');
411
+ const kr = HASH512.create().update(msg).update(publicKeyHash).digest();
412
+ const cipherText = cached.encrypt(msg, kr.subarray(32, 64));
413
+ cleanBytes(kr.subarray(32));
414
+ return {
415
+ cipherText: cipherText,
416
+ sharedSecret: kr.subarray(0, 32),
417
+ };
418
+ },
419
+ decapsulate: (cipherText, secretKey) => {
420
+ abytes(secretKey, secretCoder.bytesLen, 'secretKey');
421
+ abytes(cipherText, lengths.cipherText, 'cipherText');
422
+ const [sk, ekEmbedded, storedHash, z] = secretCoder.decode(secretKey);
423
+ // Under KEMPrepared's read-only publicKey contract, bind dk to the prepared key.
424
+ // Together with publicKeyHash = H(ek) computed in prepare(), this is equivalent to (and
425
+ // stronger than) FIPS 203 §7.3's `H(dk[384k : 768k+32]) == dk[768k+32 : 768k+64]`.
426
+ if (!equalBytes(ekEmbedded, ek) || !equalBytes(storedHash, publicKeyHash))
427
+ throw new Error('ML-KEM.decapsulate: secretKey does not match prepared publicKey');
428
+ const msg = KPKE.decrypt(cipherText, sk);
429
+ // derive randomness, Khat, rHat = G(mHat || h)
430
+ const kr = HASH512.create().update(msg).update(publicKeyHash).digest();
431
+ const Khat = kr.subarray(0, 32);
432
+ // re-encrypt using the derived randomness and cached Â/t̂
433
+ const cipherText2 = cached.encrypt(msg, kr.subarray(32, 64));
434
+ // if ciphertexts do not match, “implicitly reject”
435
+ const isValid = equalBytes(cipherText, cipherText2);
436
+ const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
437
+ cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
438
+ return (isValid ? Khat : Kbar);
439
+ },
440
+ clean: cached.clean,
441
+ });
442
+ },
362
443
  });
363
444
  }
364
445
  // FIPS 203's PRF_eta binding: current callers use only 32-byte keys, one-byte nonces,
@@ -388,6 +469,17 @@ const mk = (params) => createKyber({
388
469
  /**
389
470
  * ML-KEM-512: Table 2 row `k=2, η1=3, η2=2, du=10, dv=4`; Table 3 sizes `800/1632/768/32`.
390
471
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
472
+ * @example
473
+ * Generate deterministic ML-KEM-512 keys, encapsulate a shared secret, and decapsulate it.
474
+ * ```ts
475
+ * import { ml_kem512 } from '@noble/post-quantum/ml-kem.js';
476
+ * const seed = new Uint8Array(ml_kem512.lengths.seed!);
477
+ * const { secretKey, publicKey } = ml_kem512.keygen(seed);
478
+ * const msg = new Uint8Array(ml_kem512.lengths.msgRand!);
479
+ * const { cipherText, sharedSecret } = ml_kem512.encapsulate(publicKey, msg);
480
+ * const recovered = ml_kem512.decapsulate(cipherText, secretKey);
481
+ * const publicKey2 = ml_kem512.getPublicKey(secretKey);
482
+ * ```
391
483
  */
392
484
  export const ml_kem512 = /* @__PURE__ */ (() => mk(PARAMS[512]))();
393
485
  /**
@@ -441,4 +533,3 @@ export const __tests = /* @__PURE__ */ (() => Object.freeze({
441
533
  }
442
534
  },
443
535
  }))();
444
- //# sourceMappingURL=ml-kem.js.map
package/package.json CHANGED
@@ -1,40 +1,32 @@
1
1
  {
2
2
  "name": "@noble/post-quantum",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Auditable & minimal JS implementation of post-quantum cryptography: FIPS 203, 204, 205, Falcon",
5
5
  "files": [
6
6
  "*.js",
7
- "*.js.map",
8
7
  "*.d.ts",
9
- "*.d.ts.map",
10
8
  "src"
11
9
  ],
12
10
  "dependencies": {
13
- "@noble/ciphers": "~2.2.0",
14
- "@noble/curves": "~2.2.0",
15
- "@noble/hashes": "~2.2.0"
11
+ "@noble/ciphers": "~2.3.0",
12
+ "@noble/curves": "~2.3.0",
13
+ "@noble/hashes": "~2.3.0"
16
14
  },
17
15
  "devDependencies": {
18
- "@paulmillr/jsbt": "0.5.0",
16
+ "@paulmillr/jsbt": "0.6.5",
19
17
  "@types/node": "25.3.0",
20
18
  "fast-check": "4.2.0",
21
19
  "prettier": "3.6.2",
22
20
  "typescript": "6.0.2"
23
21
  },
24
22
  "scripts": {
25
- "bench": "node test/benchmark.ts",
23
+ "benchmark": "node benchmark/pq.ts",
24
+ "benchmark:size": "npx bismar@0.1 -s",
26
25
  "build": "tsc",
27
- "build:release": "npx --no @paulmillr/jsbt esbuild test/build",
28
- "check": "npm run check:readme && npm run check:treeshake && npm run check:jsdoc",
29
- "check:readme": "npx --no @paulmillr/jsbt readme package.json",
30
- "check:treeshake": "npx --no @paulmillr/jsbt treeshake package.json test/build/out-treeshake",
31
- "check:jsdoc": "npx --no @paulmillr/jsbt tsdoc package.json",
32
- "build:clean": "rm *.{js,js.map,d.ts,d.ts.map} 2> /dev/null",
26
+ "check": "jsbt-check",
27
+ "build:clean": "rm *.{js,d.ts} 2> /dev/null",
33
28
  "format": "prettier --write 'src/**/*.{js,ts}' 'test/**/*.{js,ts,mjs}'",
34
29
  "test": "node test/index.ts",
35
- "test:bun": "bun test/index.ts",
36
- "test:deno": "deno --allow-env --allow-read test/index.ts",
37
- "test:node20": "cd test; npx tsc; node compiled/test/index.js",
38
30
  "test:slow": "SLOW_TESTS=1 node test/index.ts"
39
31
  },
40
32
  "exports": {
package/slh-dsa.d.ts CHANGED
@@ -40,12 +40,16 @@ export type SphincsHashOpts = {
40
40
  export declare const PARAMS: Record<string, SphincsOpts>;
41
41
  /** Address byte array of size `ADDR_BYTES`. */
42
42
  export type ADRS = Uint8Array;
43
- /** Hash and tweakable-hash callbacks bound to one SLH-DSA keypair context. */
43
+ /** Hash and tweakable-hash callbacks bound to one SLH-DSA keypair context.
44
+ * Buffer-aliasing contract: `PRFaddr`, `thash1` and `thashN` return views into per-context
45
+ * scratch buffers (one per lane), so callers must consume or copy a result before the next
46
+ * call on the same lane. `clean()` wipes the scratch buffers along with the hash states.
47
+ */
44
48
  export type Context = {
45
49
  /**
46
50
  * Derive a PRF output for one address.
47
51
  * @param addr - Address bytes.
48
- * @returns PRF output bytes.
52
+ * @returns PRF output bytes (scratch view; copy to retain).
49
53
  */
50
54
  PRFaddr: (addr: TArg<ADRS>) => TRet<Uint8Array>;
51
55
  /**
@@ -131,6 +135,23 @@ export declare const slh_dsa_shake_256s: TRet<SphincsSigner>;
131
135
  * SLH-DSA-SHA2-128f: Table 2 row `n=16, h=66, d=22, h'=3, a=6, k=33, lg w=4, m=34`;
132
136
  * lengths `publicKey=32`, `secretKey=64`, `signature=17088`, `seed=48`, `signRand=16`.
133
137
  * Also exposes `.prehash(...)`.
138
+ * @example
139
+ * Generate deterministic SLH-DSA keys, sign one message, and verify the signature.
140
+ * ```ts
141
+ * import { sha256 } from '@noble/hashes/sha2.js';
142
+ * import { slh_dsa_sha2_128f } from '@noble/post-quantum/slh-dsa.js';
143
+ * const seed = new Uint8Array(slh_dsa_sha2_128f.lengths.seed!);
144
+ * const { secretKey, publicKey } = slh_dsa_sha2_128f.keygen(seed);
145
+ * const msg = new TextEncoder().encode('hello noble');
146
+ * const sig = slh_dsa_sha2_128f.sign(msg, secretKey);
147
+ * const isValid = slh_dsa_sha2_128f.verify(sig, msg, publicKey);
148
+ * const recovered = slh_dsa_sha2_128f.getPublicKey(secretKey);
149
+ * const context = new Uint8Array([1, 2, 3]);
150
+ * const prehash = slh_dsa_sha2_128f.prehash(sha256);
151
+ * const preSig = prehash.sign(msg, secretKey, { context });
152
+ * const preValid = prehash.verify(preSig, msg, publicKey, { context });
153
+ * const internalSig = slh_dsa_sha2_128f.internal.sign(msg, secretKey);
154
+ * ```
134
155
  */
135
156
  export declare const slh_dsa_sha2_128f: TRet<SphincsSigner>;
136
157
  /**
@@ -163,4 +184,3 @@ export declare const slh_dsa_sha2_256f: TRet<SphincsSigner>;
163
184
  * Also exposes `.prehash(...)`.
164
185
  */
165
186
  export declare const slh_dsa_sha2_256s: TRet<SphincsSigner>;
166
- //# sourceMappingURL=slh-dsa.d.ts.map