@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/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 & {
@@ -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);
317
371
  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>;
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);
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
+ };
324
394
  },
325
395
  decrypt: (cipherText: TArg<Uint8Array>, privateKey: TArg<Uint8Array>): TRet<Uint8Array> => {
326
396
  const [u, v] = cipherCoder.decode(cipherText);
@@ -344,7 +414,7 @@ const genKPKE = (opts_: TArg<KyberOpts>) => {
344
414
  * mismatch, and zeroizing the non-returned shared-secret candidate; JS/JIT still provides no
345
415
  * constant-time guarantees for that path.
346
416
  */
347
- function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
417
+ function createKyber(opts: TArg<KyberOpts>): TRet<MLKEM> {
348
418
  const rawOpts = opts as KyberOpts;
349
419
  const KPKE = genKPKE(rawOpts);
350
420
  const { HASH256, HASH512, KDF } = rawOpts;
@@ -352,6 +422,17 @@ function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
352
422
  const secretCoder = splitCoder('secretKey', lengths.secretKey, lengths.publicKey, 32, 32);
353
423
  const msgLen = 32;
354
424
  const seedLen = 64;
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
+ };
355
436
  const kemLengths = Object.freeze({
356
437
  ...lengths,
357
438
  seed: 64,
@@ -381,18 +462,7 @@ function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
381
462
  encapsulate: (publicKey: TArg<Uint8Array>, msg: TArg<Uint8Array> = randomBytes(msgLen)) => {
382
463
  abytes(publicKey, lengths.publicKey, 'publicKey');
383
464
  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');
394
- }
395
- cleanBytes(ek);
465
+ validateModulus(publicKey, 'encapsulate');
396
466
  // derive randomness
397
467
  const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest();
398
468
  const cipherText = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64));
@@ -422,9 +492,60 @@ function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
422
492
  // if ciphertexts do not match, “implicitly reject”
423
493
  const isValid = equalBytes(cipherText, cipherText2);
424
494
  const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
425
- cleanBytes(msg, cipherText2, !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);
426
497
  return (isValid ? Khat : Kbar) as TRet<Uint8Array>;
427
498
  },
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
+ },
428
549
  });
429
550
  }
430
551
 
@@ -458,18 +579,29 @@ const mk = (params: KEMParam) =>
458
579
  /**
459
580
  * ML-KEM-512: Table 2 row `k=2, η1=3, η2=2, du=10, dv=4`; Table 3 sizes `800/1632/768/32`.
460
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
+ * ```
461
593
  */
462
- export const ml_kem512: TRet<KEM> = /* @__PURE__ */ (() => mk(PARAMS[512]))();
594
+ export const ml_kem512: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[512]))();
463
595
  /**
464
596
  * ML-KEM-768: Table 2 row `k=3, η1=2, η2=2, du=10, dv=4`; Table 3 sizes `1184/2400/1088/32`.
465
597
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
466
598
  */
467
- export const ml_kem768: TRet<KEM> = /* @__PURE__ */ (() => mk(PARAMS[768]))();
599
+ export const ml_kem768: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[768]))();
468
600
  /**
469
601
  * ML-KEM-1024: Table 2 row `k=4, η1=2, η2=2, du=11, dv=5`; Table 3 sizes `1568/3168/1568/32`.
470
602
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
471
603
  */
472
- export const ml_kem1024: TRet<KEM> = /* @__PURE__ */ (() => mk(PARAMS[1024]))();
604
+ export const ml_kem1024: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[1024]))();
473
605
 
474
606
  // NOTE: for tests only, don't use. This keeps the exact internal ML-KEM math surfaces available
475
607
  // without re-implementing them in separate test code.
package/src/slh-dsa.ts CHANGED
@@ -28,15 +28,10 @@
28
28
  */
29
29
  /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
30
30
  import { hmac } from '@noble/hashes/hmac.js';
31
+ import { bytesToNumberBE, numberToBytesBE } from '@noble/curves/utils.js';
31
32
  import { sha256, sha512 } from '@noble/hashes/sha2.js';
32
33
  import { shake256 } from '@noble/hashes/sha3.js';
33
- import {
34
- bytesToHex,
35
- concatBytes,
36
- createView,
37
- hexToBytes,
38
- type CHash,
39
- } from '@noble/hashes/utils.js';
34
+ import { concatBytes, createView, type CHash } from '@noble/hashes/utils.js';
40
35
  import {
41
36
  abytes,
42
37
  checkHash,
@@ -123,12 +118,16 @@ const AddressType = {
123
118
  /** Address byte array of size `ADDR_BYTES`. */
124
119
  export type ADRS = Uint8Array;
125
120
 
126
- /** Hash and tweakable-hash callbacks bound to one SLH-DSA keypair context. */
121
+ /** Hash and tweakable-hash callbacks bound to one SLH-DSA keypair context.
122
+ * Buffer-aliasing contract: `PRFaddr`, `thash1` and `thashN` return views into per-context
123
+ * scratch buffers (one per lane), so callers must consume or copy a result before the next
124
+ * call on the same lane. `clean()` wipes the scratch buffers along with the hash states.
125
+ */
127
126
  export type Context = {
128
127
  /**
129
128
  * Derive a PRF output for one address.
130
129
  * @param addr - Address bytes.
131
- * @returns PRF output bytes.
130
+ * @returns PRF output bytes (scratch view; copy to retain).
132
131
  */
133
132
  PRFaddr: (addr: TArg<ADRS>) => TRet<Uint8Array>;
134
133
  /**
@@ -180,21 +179,6 @@ export type GetContext = (
180
179
  opts: SphincsOpts
181
180
  ) => (pub_seed: TArg<Uint8Array>, sk_seed?: TArg<Uint8Array>) => TRet<Context>;
182
181
 
183
- function hexToNumber(hex: string): bigint {
184
- if (typeof hex !== 'string') throw new Error('hex string expected, got ' + typeof hex);
185
- return BigInt(hex === '' ? '0' : '0x' + hex); // Big Endian
186
- }
187
-
188
- // BE: Big Endian, LE: Little Endian. This is the local FIPS 205 `toInt(...)` equivalent.
189
- function bytesToNumberBE(bytes: TArg<Uint8Array>): bigint {
190
- return hexToNumber(bytesToHex(bytes));
191
- }
192
-
193
- // Local in-range FIPS 205 `toByte(x, n)` equivalent; callers must keep `n < 256^len`.
194
- function numberToBytesBE(n: number | bigint, len: number): TRet<Uint8Array> {
195
- return hexToBytes(n.toString(16).padStart(len * 2, '0'));
196
- }
197
-
198
182
  // Local FIPS 205 Algorithm 4 `base_2^b(...)` implementation. Bits are consumed in big-endian
199
183
  // order within each input byte, and callers must provide at least `ceil(outLen * b / 8)` bytes;
200
184
  // short inputs are not rejected and would zero-extend implicitly.
@@ -214,8 +198,12 @@ const base2b = (outLen: number, b: number) => {
214
198
  };
215
199
  };
216
200
 
201
+ const _1n = /* @__PURE__ */ BigInt(1);
202
+ const _8n = /* @__PURE__ */ BigInt(8);
203
+ const _0xffn = /* @__PURE__ */ BigInt(0xff);
204
+
217
205
  function getMaskBig(bits: number) {
218
- return (1n << BigInt(bits)) - 1n; // 4 -> 0b1111
206
+ return (_1n << BigInt(bits)) - _1n; // 4 -> 0b1111
219
207
  }
220
208
 
221
209
  /** Public SLH-DSA signer with prehash customization. */
@@ -284,16 +272,25 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
284
272
  ) => {
285
273
  const { type, height, tree, layer, index, chain, hash, keypair } = opts;
286
274
  const { subtreeAddr, keypairAddr } = opts;
287
- const v = createView(addr);
288
275
 
289
276
  if (height !== undefined) addr[OFFSET_CHAIN_ADDR] = height;
290
277
  if (layer !== undefined) addr[OFFSET_LAYER] = layer;
291
278
  if (type !== undefined) addr[OFFSET_TYPE] = type;
292
279
  if (chain !== undefined) addr[OFFSET_CHAIN_ADDR] = chain;
293
280
  if (hash !== undefined) addr[OFFSET_HASH_ADDR] = hash;
294
- if (index !== undefined) v.setUint32(OFFSET_TREE_INDEX, index, false);
281
+ // Manual big-endian writes: setAddr runs in the innermost WOTS/tree loops, and creating a
282
+ // DataView per call was a measurable share of sign() time.
283
+ if (index !== undefined) {
284
+ addr[OFFSET_TREE_INDEX + 0] = index >>> 24;
285
+ addr[OFFSET_TREE_INDEX + 1] = index >>> 16;
286
+ addr[OFFSET_TREE_INDEX + 2] = index >>> 8;
287
+ addr[OFFSET_TREE_INDEX + 3] = index;
288
+ }
295
289
  if (subtreeAddr) addr.set(subtreeAddr.subarray(0, OFFSET_TREE + 8));
296
- if (tree !== undefined) v.setBigUint64(OFFSET_TREE, tree, false);
290
+ if (tree !== undefined) {
291
+ let t = tree;
292
+ for (let i = 7; i >= 0; i--, t >>= _8n) addr[OFFSET_TREE + i] = Number(t & _0xffn);
293
+ }
297
294
  if (keypair !== undefined) {
298
295
  addr[OFFSET_KP_ADDR1] = keypair;
299
296
  if (TREE_HEIGHT > 8) addr[OFFSET_KP_ADDR2] = keypair >>> 8;
@@ -372,10 +369,12 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
372
369
  const maxIdx = (1 << height) - 1;
373
370
  const stack = new Uint8Array(height * N);
374
371
  const authPath = new Uint8Array(height * N);
372
+ // One node buffer per treehash call (not per leaf): both halves are fully overwritten at
373
+ // each use, and the returned root aliases cur1, which is never reused after return.
374
+ const current = new Uint8Array(2 * N);
375
+ const cur0 = current.subarray(0, N);
376
+ const cur1 = current.subarray(N);
375
377
  for (let idx = 0; ; idx++) {
376
- const current = new Uint8Array(2 * N);
377
- const cur0 = current.subarray(0, N);
378
- const cur1 = current.subarray(N);
379
378
  const addrOffset = idx + idxOffset;
380
379
  cur1.set(leafFn(leafIdx, addrOffset, rawContext, info));
381
380
  let h = 0;
@@ -582,7 +581,9 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
582
581
  },
583
582
  forsTreeAddr
584
583
  );
585
- const prf = context.PRFaddr(forsTreeAddr);
584
+ // Copy: PRFaddr returns a per-context scratch view, and this value is retained in
585
+ // `fors` across the many PRFaddr calls inside forsTreehash below.
586
+ const prf = copyBytes(context.PRFaddr(forsTreeAddr));
586
587
  setAddr({ type: AddressType.FORSTREE }, forsTreeAddr);
587
588
  const { root, authPath } = forsTreehash(
588
589
  context,
@@ -598,7 +599,9 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
598
599
  type: AddressType.FORSPK,
599
600
  keypairAddr: wotsAddr,
600
601
  });
601
- const root = context.thashN(K, concatBytes(...roots), forsPkAddr);
602
+ // Copy: thashN returns a per-context scratch view, and `root` lives across every hash
603
+ // call in the hypertree loop below (it is also mutated via root.set).
604
+ const root = copyBytes(context.thashN(K, concatBytes(...roots), forsPkAddr));
602
605
  // WOTS signatures
603
606
  const treeAddr = setAddr({ type: AddressType.HASHTREE });
604
607
  const wots: [Uint8Array, Uint8Array][] = [];
@@ -622,9 +625,13 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
622
625
  },
623
626
  verify: (sig: TArg<Uint8Array>, msg: TArg<Uint8Array>, publicKey: TArg<Uint8Array>) => {
624
627
  const [pkSeed, pubRoot] = publicCoder.decode(publicKey);
625
- const [random, forsVec, wotsVec] = sigCoder.decode(sig);
626
628
  const pk = publicKey;
629
+ // FIPS 205 Algorithm 20 step 1: wrong-length signatures return false instead of throwing
630
+ // (same as ml-dsa). Must run before sigCoder.decode, which throws on length mismatch.
631
+ // Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
632
+ abytes(sig, undefined, 'signature');
627
633
  if (sig.length !== sigCoder.bytesLen) return false;
634
+ const [random, forsVec, wotsVec] = sigCoder.decode(sig);
628
635
  const context = getContext(pkSeed);
629
636
  let { tree, leafIdx, md } = hashMessage(random, pk, msg, context);
630
637
  const wotsAddr = setAddr({
@@ -644,14 +651,18 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
644
651
  const idxOffset = i << A;
645
652
  setAddr({ height: 0, index: indices[i] + idxOffset }, forsTreeAddr);
646
653
  const leaf = context.thash1(prf, forsTreeAddr);
647
- // Compute inplace, because we need all roots in same byte array
648
- roots.push(computeRoot(leaf, indices[i], idxOffset, authPath, A, context, forsTreeAddr));
654
+ // Copy: computeRoot returns a thashN scratch view, and roots are retained across the
655
+ // remaining FORS iterations (computeRoot itself copies `leaf` before hashing).
656
+ roots.push(
657
+ copyBytes(computeRoot(leaf, indices[i], idxOffset, authPath, A, context, forsTreeAddr))
658
+ );
649
659
  }
650
660
  const forsPkAddr = setAddr({
651
661
  type: AddressType.FORSPK,
652
662
  keypairAddr: wotsAddr,
653
663
  });
654
- let root = context.thashN(K, concatBytes(...roots), forsPkAddr); // root = thash()
664
+ // Copy: `root` must survive the thash1/thashN calls of the WOTS chain loop below.
665
+ let root = copyBytes(context.thashN(K, concatBytes(...roots), forsPkAddr)); // root = thash()
655
666
  // WOTS signature
656
667
  const treeAddr = setAddr({ type: AddressType.HASHTREE });
657
668
  const wotsPkAddr = setAddr({ type: AddressType.WOTSPK });
@@ -674,7 +685,8 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
674
685
  }
675
686
  }
676
687
  const leaf = context.thashN(WOTS_LEN, wotsPk, wotsPkAddr);
677
- root = computeRoot(leaf, leafIdx, 0, sigAuth, TREE_HEIGHT, context, treeAddr);
688
+ // Copy: `root` is read by chainLengths / equalBytes after later hash calls.
689
+ root = copyBytes(computeRoot(leaf, leafIdx, 0, sigAuth, TREE_HEIGHT, context, treeAddr));
678
690
  leafIdx = Number(tree & getMaskBig(TREE_HEIGHT));
679
691
  }
680
692
  return equalBytes(root, pubRoot);
@@ -744,20 +756,27 @@ const genShake =
744
756
  // for each address-bound call instead of reabsorbing the same seed every time.
745
757
  const h0 = shake256.create({}).update(pubSeed);
746
758
  const h0tmp = h0.clone();
759
+ // Per-context output scratch: thash1/thashN/PRFaddr return these buffers directly, so
760
+ // callers must consume or copy a result before the next call on the same lane.
761
+ const thashOut = new Uint8Array(N);
762
+ const prfOut = new Uint8Array(N);
747
763
  const thash = (blocks: number, input: TArg<Uint8Array>, addr: TArg<ADRS>): TRet<Uint8Array> => {
748
764
  stats.thash++;
749
- return h0
750
- ._cloneInto(h0tmp)
765
+ const len = blocks * N;
766
+ h0._cloneInto(h0tmp)
751
767
  .update(addr)
752
- .update(input.subarray(0, blocks * N))
753
- .xof(N) as TRet<Uint8Array>;
768
+ .update(
769
+ input.length === len ? (input as Uint8Array) : (input as Uint8Array).subarray(0, len)
770
+ )
771
+ .xofInto(thashOut);
772
+ return thashOut as TRet<Uint8Array>;
754
773
  };
755
774
  return {
756
775
  PRFaddr: (addr: TArg<ADRS>): TRet<Uint8Array> => {
757
776
  if (!skSeed) throw new Error('no sk seed');
758
777
  stats.prf++;
759
- const res = h0._cloneInto(h0tmp).update(addr).update(skSeed).xof(N);
760
- return res as TRet<Uint8Array>;
778
+ h0._cloneInto(h0tmp).update(addr).update(skSeed).xofInto(prfOut);
779
+ return prfOut as TRet<Uint8Array>;
761
780
  },
762
781
  PRFmsg: (
763
782
  skPRF: TArg<Uint8Array>,
@@ -787,6 +806,7 @@ const genShake =
787
806
  clean: () => {
788
807
  h0.destroy();
789
808
  h0tmp.destroy();
809
+ cleanBytes(thashOut, prfOut);
790
810
  //console.log(stats);
791
811
  },
792
812
  } as TRet<Context>;
@@ -869,6 +889,16 @@ const genSha =
869
889
 
870
890
  const h0tmp = h0ps.clone();
871
891
  const h1tmp = h1ps.clone();
892
+ // Per-context output scratch: thash1/thashN/PRFaddr return views into these buffers, so
893
+ // callers must consume or copy a result before the next call on the same lane (see Context
894
+ // docs). digestInto also skips digest()'s per-call destroy(): the tmp states are fully
895
+ // overwritten by the next _cloneInto and wiped in clean().
896
+ const h0out = new Uint8Array(h0.outputLen);
897
+ const h1out = new Uint8Array(h1.outputLen);
898
+ const prfOut = new Uint8Array(h0.outputLen);
899
+ const h0outN = h0out.subarray(0, N);
900
+ const h1outN = h1out.subarray(0, N);
901
+ const prfOutN = prfOut.subarray(0, N);
872
902
 
873
903
  // https://www.rfc-editor.org/rfc/rfc8017.html#appendix-B.2.1
874
904
  // This local helper is intentionally stricter than generic MGF1 reuse: current SLH-DSA callers
@@ -889,27 +919,28 @@ const genSha =
889
919
  }
890
920
 
891
921
  const thash =
892
- (_: ShaType, h: typeof h0ps, hTmp: typeof h0ps) =>
922
+ (h: typeof h0ps, hTmp: typeof h0ps, out: TArg<Uint8Array>, outN: TArg<Uint8Array>) =>
893
923
  (blocks: number, input: TArg<Uint8Array>, addr: TArg<ADRS>): TRet<Uint8Array> => {
894
924
  stats.thash++;
895
- const d = h
896
- ._cloneInto(hTmp as any)
925
+ const len = blocks * N;
926
+ h._cloneInto(hTmp as any)
897
927
  .update(addr)
898
- .update(input.subarray(0, blocks * N))
899
- .digest();
900
- return d.subarray(0, N) as TRet<Uint8Array>;
928
+ .update(
929
+ input.length === len ? (input as Uint8Array) : (input as Uint8Array).subarray(0, len)
930
+ )
931
+ .digestInto(out);
932
+ return outN as TRet<Uint8Array>;
901
933
  };
902
934
  return {
903
935
  PRFaddr: (addr: TArg<ADRS>): TRet<Uint8Array> => {
904
936
  if (!sk_seed) throw new Error('No sk seed');
905
937
  stats.prf++;
906
- const res = h0ps
938
+ h0ps
907
939
  ._cloneInto(h0tmp as any)
908
940
  .update(addr)
909
941
  .update(sk_seed)
910
- .digest()
911
- .subarray(0, N);
912
- return res as TRet<Uint8Array>;
942
+ .digestInto(prfOut);
943
+ return prfOutN as TRet<Uint8Array>;
913
944
  },
914
945
  PRFmsg: (
915
946
  skPRF: TArg<Uint8Array>,
@@ -938,13 +969,14 @@ const genSha =
938
969
  );
939
970
  return mgf1(seed, outLen, h1);
940
971
  },
941
- thash1: thash(h0, h0ps, h0tmp).bind(null, 1),
942
- thashN: thash(h1, h1ps, h1tmp),
972
+ thash1: thash(h0ps, h0tmp, h0out, h0outN).bind(null, 1),
973
+ thashN: thash(h1ps, h1tmp, h1out, h1outN),
943
974
  clean: () => {
944
975
  h0ps.destroy();
945
976
  h1ps.destroy();
946
977
  h0tmp.destroy();
947
978
  h1tmp.destroy();
979
+ cleanBytes(h0out, h1out, prfOut);
948
980
  //console.log(stats);
949
981
  },
950
982
  } as TRet<Context>;
@@ -963,6 +995,23 @@ const SHA512_SIMPLE = /* @__PURE__ */ (() => ({
963
995
  * SLH-DSA-SHA2-128f: Table 2 row `n=16, h=66, d=22, h'=3, a=6, k=33, lg w=4, m=34`;
964
996
  * lengths `publicKey=32`, `secretKey=64`, `signature=17088`, `seed=48`, `signRand=16`.
965
997
  * Also exposes `.prehash(...)`.
998
+ * @example
999
+ * Generate deterministic SLH-DSA keys, sign one message, and verify the signature.
1000
+ * ```ts
1001
+ * import { sha256 } from '@noble/hashes/sha2.js';
1002
+ * import { slh_dsa_sha2_128f } from '@noble/post-quantum/slh-dsa.js';
1003
+ * const seed = new Uint8Array(slh_dsa_sha2_128f.lengths.seed!);
1004
+ * const { secretKey, publicKey } = slh_dsa_sha2_128f.keygen(seed);
1005
+ * const msg = new TextEncoder().encode('hello noble');
1006
+ * const sig = slh_dsa_sha2_128f.sign(msg, secretKey);
1007
+ * const isValid = slh_dsa_sha2_128f.verify(sig, msg, publicKey);
1008
+ * const recovered = slh_dsa_sha2_128f.getPublicKey(secretKey);
1009
+ * const context = new Uint8Array([1, 2, 3]);
1010
+ * const prehash = slh_dsa_sha2_128f.prehash(sha256);
1011
+ * const preSig = prehash.sign(msg, secretKey, { context });
1012
+ * const preValid = prehash.verify(preSig, msg, publicKey, { context });
1013
+ * const internalSig = slh_dsa_sha2_128f.internal.sign(msg, secretKey);
1014
+ * ```
966
1015
  */
967
1016
  export const slh_dsa_sha2_128f: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
968
1017
  gen(PARAMS['128f'], SHA256_SIMPLE))();