@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/ml-dsa.js CHANGED
@@ -40,10 +40,16 @@ const GAMMA2_2 = Math.floor((Q - 1) / 32) | 0;
40
40
  * This is only the Table 1 subset used directly here: `BETA = TAU * ETA` is derived later,
41
41
  * while `C_TILDE_BYTES`, `TR_BYTES`, `CRH_BYTES`, and `securityLevel` live in the preset wrappers.
42
42
  */
43
- export const PARAMS = /* @__PURE__ */ (() => ({
44
- 2: { K: 4, L: 4, D, GAMMA1: 2 ** 17, GAMMA2: GAMMA2_1, TAU: 39, ETA: 2, OMEGA: 80 },
45
- 3: { K: 6, L: 5, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 49, ETA: 4, OMEGA: 55 },
46
- 5: { K: 8, L: 7, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 60, ETA: 2, OMEGA: 75 },
43
+ export const PARAMS = /* @__PURE__ */ (() => Object.freeze({
44
+ 2: Object.freeze({
45
+ K: 4, L: 4, D, GAMMA1: 2 ** 17, GAMMA2: GAMMA2_1, TAU: 39, ETA: 2, OMEGA: 80
46
+ }),
47
+ 3: Object.freeze({
48
+ K: 6, L: 5, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 49, ETA: 4, OMEGA: 55
49
+ }),
50
+ 5: Object.freeze({
51
+ K: 8, L: 7, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 60, ETA: 2, OMEGA: 75
52
+ }),
47
53
  }))();
48
54
  const newPoly = (n) => new Int32Array(n);
49
55
  // Shared CRYSTALS helper in the ML-DSA branch: non-Kyber mode, 8-bit bit-reversal,
@@ -66,24 +72,32 @@ const polyCoder = (d, compress = id, verify = id) => crystals.bitsCoder(d, {
66
72
  decode: (i) => verify(compress(i)),
67
73
  });
68
74
  // Mutates `a` in place; callers must pass same-length polynomials.
69
- const polyAdd = (a, b) => {
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.
77
+ const polyAdd = (a_, b_) => {
78
+ const a = a_;
79
+ const b = b_;
70
80
  for (let i = 0; i < a.length; i++)
71
81
  a[i] = crystals.mod(a[i] + b[i]);
72
82
  return a;
73
83
  };
74
84
  // Mutates `a` in place; callers must pass same-length polynomials.
75
- const polySub = (a, b) => {
85
+ const polySub = (a_, b_) => {
86
+ const a = a_;
87
+ const b = b_;
76
88
  for (let i = 0; i < a.length; i++)
77
89
  a[i] = crystals.mod(a[i] - b[i]);
78
90
  return a;
79
91
  };
80
92
  // Mutates `p` in place and assumes it is a decoded `t1`-range polynomial.
81
- const polyShiftl = (p) => {
93
+ const polyShiftl = (p_) => {
94
+ const p = p_;
82
95
  for (let i = 0; i < N; i++)
83
96
  p[i] <<= D;
84
97
  return p;
85
98
  };
86
- const polyChknorm = (p, B) => {
99
+ const polyChknorm = (p_, B) => {
100
+ const p = p_;
87
101
  // FIPS 204 Algorithms 7 and 8 express the same centered-norm check with explicit inequalities.
88
102
  for (let i = 0; i < N; i++)
89
103
  if (Math.abs(crystals.smod(p[i])) >= B)
@@ -91,7 +105,9 @@ const polyChknorm = (p, B) => {
91
105
  return false;
92
106
  };
93
107
  // Both inputs must already be in NTT / `T_q` form.
94
- const MultiplyNTTs = (a, b) => {
108
+ const MultiplyNTTs = (a_, b_) => {
109
+ const a = a_;
110
+ const b = b_;
95
111
  // NOTE: we don't use montgomery reduction in code, since it requires 64 bit ints,
96
112
  // which is not available in JS. mod(a[i] * b[i]) is ok, since Q is 23 bit,
97
113
  // which means a[i] * b[i] is 46 bit, which is safe to use in JS. (number is 53 bits).
@@ -102,7 +118,8 @@ const MultiplyNTTs = (a, b) => {
102
118
  return c;
103
119
  };
104
120
  // Return poly in NTT representation
105
- function RejNTTPoly(xof) {
121
+ function RejNTTPoly(xof_) {
122
+ const xof = xof_;
106
123
  // Samples a polynomial ∈ Tq. xof() must return byte lengths divisible by 3.
107
124
  const r = newPoly(N);
108
125
  // NOTE: we can represent 3xu24 as 4xu32, but it doesn't improve perf :(
@@ -121,7 +138,8 @@ function RejNTTPoly(xof) {
121
138
  }
122
139
  // Instantiate one ML-DSA parameter set from the Table 1 lattice constants plus the
123
140
  // Table 2 byte lengths / hash-width choices used by the public wrappers below.
124
- function getDilithium(opts) {
141
+ function getDilithium(opts_) {
142
+ const opts = opts_;
125
143
  const { K, L, GAMMA1, GAMMA2, TAU, ETA, OMEGA } = opts;
126
144
  const { CRH_BYTES, TR_BYTES, C_TILDE_BYTES, XOF128, XOF256, securityLevel } = opts;
127
145
  if (![2, 4].includes(ETA))
@@ -163,14 +181,16 @@ function getDilithium(opts) {
163
181
  // See dilithium-py README section "Optimising decomposition and making hints".
164
182
  return res0;
165
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));
166
187
  const UseHint = (h, r) => {
167
188
  // Returns the high bits of r adjusted according to hint h
168
- const m = Math.floor((Q - 1) / (2 * GAMMA2));
169
189
  const { r1, r0 } = decompose(r);
170
190
  // 3: if h = 1 and r0 > 0 return (r1 + 1) mod m
171
191
  // 4: if h = 1 and r0 ≤ 0 return (r1 − 1) mod m
172
192
  if (h === 1)
173
- 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;
174
194
  return r1 | 0;
175
195
  };
176
196
  const Power2Round = (r) => {
@@ -181,7 +201,8 @@ function getDilithium(opts) {
181
201
  };
182
202
  const hintCoder = {
183
203
  bytesLen: OMEGA + K,
184
- encode: (h) => {
204
+ encode: (h_) => {
205
+ const h = h_;
185
206
  if (h === false)
186
207
  throw new Error('hint.encode: hint is false'); // should never happen
187
208
  const res = new Uint8Array(OMEGA + K);
@@ -235,7 +256,8 @@ function getDilithium(opts) {
235
256
  // Return poly in ordinary representation.
236
257
  // This helper returns ordinary-form `[-ETA, ETA]` coefficients for ExpandS; callers apply
237
258
  // `NTT.encode()` later when needed.
238
- function RejBoundedPoly(xof) {
259
+ function RejBoundedPoly(xof_) {
260
+ const xof = xof_;
239
261
  // Samples an element a ∈ Rq with coeffcients in [−η, η] computed via rejection sampling from ρ.
240
262
  const r = newPoly(N);
241
263
  for (let j = 0; j < N;) {
@@ -279,7 +301,8 @@ function getDilithium(opts) {
279
301
  }
280
302
  return pre;
281
303
  };
282
- const polyPowerRound = (p) => {
304
+ const polyPowerRound = (p_) => {
305
+ const p = p_;
283
306
  const res0 = newPoly(N);
284
307
  const res1 = newPoly(N);
285
308
  for (let i = 0; i < p.length; i++) {
@@ -289,14 +312,18 @@ function getDilithium(opts) {
289
312
  }
290
313
  return { r0: res0, r1: res1 };
291
314
  };
292
- const polyUseHint = (u, h) => {
315
+ const polyUseHint = (u_, h_) => {
316
+ const u = u_;
317
+ const h = h_;
293
318
  // In-place on `u`: verification only needs the recovered high bits, so reuse the
294
319
  // temporary `wApprox` buffer instead of allocating another polynomial.
295
320
  for (let i = 0; i < N; i++)
296
321
  u[i] = UseHint(h[i], u[i]);
297
322
  return u;
298
323
  };
299
- const polyMakeHint = (a, b) => {
324
+ const polyMakeHint = (a_, b_) => {
325
+ const a = a_;
326
+ const b = b_;
300
327
  const v = newPoly(N);
301
328
  let cnt = 0;
302
329
  for (let i = 0; i < N; i++) {
@@ -309,15 +336,15 @@ function getDilithium(opts) {
309
336
  const signRandBytes = 32;
310
337
  const seedCoder = splitCoder('seed', 32, 64, 32);
311
338
  // API & argument positions are exactly as in FIPS204.
312
- const internal = {
313
- info: { type: 'internal-ml-dsa' },
314
- lengths: {
339
+ const internal = Object.freeze({
340
+ info: Object.freeze({ type: 'internal-ml-dsa' }),
341
+ lengths: Object.freeze({
315
342
  secretKey: secretCoder.bytesLen,
316
343
  publicKey: publicCoder.bytesLen,
317
344
  seed: 32,
318
345
  signature: sigCoder.bytesLen,
319
346
  signRand: signRandBytes,
320
- },
347
+ }),
321
348
  keygen: (seed) => {
322
349
  // H(𝜉||IntegerToBytes(𝑘, 1)||IntegerToBytes(ℓ, 1), 128) 2: ▷ expand seed
323
350
  const seedDst = new Uint8Array(32 + 2);
@@ -367,7 +394,10 @@ function getDilithium(opts) {
367
394
  // DSA44: { calls: 24, xofs: 24 }, DSA65: { calls: 41, xofs: 41 },
368
395
  // DSA87: { calls: 71, xofs: 71 }
369
396
  cleanBytes(rho, rhoPrime, K_, s1, s2, s1Hat, t, t0, t1, tr, seedDst);
370
- return { publicKey, secretKey };
397
+ return {
398
+ publicKey: publicKey,
399
+ secretKey: secretKey,
400
+ };
371
401
  },
372
402
  getPublicKey: (secretKey) => {
373
403
  // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
@@ -395,11 +425,34 @@ function getDilithium(opts) {
395
425
  sign: (msg, secretKey, opts = {}) => {
396
426
  validateSigOpts(opts);
397
427
  validateInternalOpts(opts);
398
- 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');
399
441
  // This part can be pre-cached per secretKey, but there is only minor performance improvement,
400
442
  // since we re-use a lot of variables to computation.
401
443
  // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
402
- 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;
403
456
  // Cache matrix to avoid re-compute later
404
457
  const A = []; // A ← ExpandA(ρ)
405
458
  const xof = XOF128(rho);
@@ -422,19 +475,15 @@ function getDilithium(opts) {
422
475
  : // 6: µ ← H(tr||M, 512)
423
476
  // ▷ Compute message representative µ
424
477
  shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest();
425
- // Compute private random seed
426
- const rnd = random === false
427
- ? new Uint8Array(32)
428
- : random === undefined
429
- ? randomBytes(signRandBytes)
430
- : random;
431
- abytes(rnd, 32, 'extraEntropy');
432
478
  const rhoprime = shake256
433
479
  .create({ dkLen: CRH_BYTES })
434
480
  .update(_K)
435
481
  .update(rnd)
436
482
  .update(mu)
437
483
  .digest(); // ρ′← H(K||rnd||µ, 512)
484
+ // Only wipe entropy we generated; caller-provided extraEntropy stays caller-owned.
485
+ if (ownRnd)
486
+ cleanBytes(rnd);
438
487
  abytes(rhoprime, CRH_BYTES);
439
488
  const x256 = XOF256(rhoprime, ZCoder.bytesLen);
440
489
  // Rejection sampling loop
@@ -506,6 +555,9 @@ function getDilithium(opts) {
506
555
  verify: (sig, msg, publicKey, opts = {}) => {
507
556
  validateInternalOpts(opts);
508
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');
509
561
  // ML-DSA.Verify(pk, M, σ): Verifes a signature σ for a message M.
510
562
  const [rho, t1] = publicCoder.decode(publicKey); // (ρ, t1) ← pkDecode(pk)
511
563
  const tr = shake256(publicKey, { dkLen: TR_BYTES }); // 6: tr ← H(BytesToBits(pk), 512)
@@ -561,9 +613,9 @@ function getDilithium(opts) {
561
613
  return false;
562
614
  return equalBytes(cTilde, c2);
563
615
  },
564
- };
565
- return {
566
- info: { type: 'ml-dsa' },
616
+ });
617
+ return Object.freeze({
618
+ info: Object.freeze({ type: 'ml-dsa' }),
567
619
  internal,
568
620
  securityLevel: securityLevel,
569
621
  keygen: internal.keygen,
@@ -578,32 +630,54 @@ function getDilithium(opts) {
578
630
  },
579
631
  verify: (sig, msg, publicKey, opts = {}) => {
580
632
  validateVerOpts(opts);
633
+ abytes(sig, undefined, 'signature');
581
634
  return internal.verify(sig, getMessage(msg, opts.context), publicKey);
582
635
  },
583
636
  prehash: (hash) => {
584
637
  checkHash(hash, securityLevel);
585
- return {
586
- info: { type: 'hashml-dsa' },
638
+ const rawHash = hash;
639
+ return Object.freeze({
640
+ info: Object.freeze({ type: 'hashml-dsa' }),
587
641
  securityLevel: securityLevel,
588
642
  lengths: internal.lengths,
589
643
  keygen: internal.keygen,
590
644
  getPublicKey: internal.getPublicKey,
591
645
  sign: (msg, secretKey, opts = {}) => {
592
646
  validateSigOpts(opts);
593
- const M = getMessagePrehash(hash, msg, opts.context);
647
+ const M = getMessagePrehash(rawHash, msg, opts.context);
594
648
  const res = internal.sign(M, secretKey, opts);
595
649
  cleanBytes(M);
596
650
  return res;
597
651
  },
598
652
  verify: (sig, msg, publicKey, opts = {}) => {
599
653
  validateVerOpts(opts);
600
- 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);
601
656
  },
602
- };
657
+ });
603
658
  },
604
- };
659
+ });
605
660
  }
606
- /** 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
+ */
607
681
  export const ml_dsa44 = /* @__PURE__ */ (() => getDilithium({
608
682
  ...PARAMS[2],
609
683
  CRH_BYTES: 64,
@@ -633,4 +707,3 @@ export const ml_dsa87 = /* @__PURE__ */ (() => getDilithium({
633
707
  XOF256,
634
708
  securityLevel: 256,
635
709
  }))();
636
- //# sourceMappingURL=ml-dsa.js.map
package/ml-kem.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type KEM } from './utils.ts';
1
+ import { type KEM, type TRet } from './utils.ts';
2
2
  /** FIPS 203: 7. Parameter Sets */
3
3
  /** Public ML-KEM parameter-set description. */
4
4
  export type KEMParam = {
@@ -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: 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: 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: KEM;
85
+ export declare const ml_kem1024: TRet<MLKEM>;
44
86
  export declare const __tests: any;
45
- //# sourceMappingURL=ml-kem.d.ts.map