@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/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);
272
+ x.clean();
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)));
252
285
  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]);
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);
@@ -265,8 +296,11 @@ const genKPKE = (opts_) => {
265
296
  for (let i = 0; i < K; i++)
266
297
  polyAdd(tmp, MultiplyNTTs(sk[i], crystals.NTT.encode(u[i])));
267
298
  polySub(v, crystals.NTT.decode(tmp)); // w = v' - tmp
268
- cleanBytes(tmp, sk, u);
269
- return poly1.encode(v);
299
+ // `v` now holds w, from which the plaintext is just a 1-bit threshold away, so wipe it too.
300
+ // encode() allocates its own buffer, so the returned bytes do not alias `v`.
301
+ const res = poly1.encode(v);
302
+ cleanBytes(tmp, sk, u, v);
303
+ return res;
270
304
  },
271
305
  };
272
306
  };
@@ -274,6 +308,11 @@ const genKPKE = (opts_) => {
274
308
  * Public ML-KEM wrapper over the internal K-PKE subroutine.
275
309
  * `keygen(seed)` and `encapsulate(publicKey, msg)` are deterministic/test-oriented hooks that map
276
310
  * more directly to Algorithms 16-17 than to the pure no-input / random-internal Algorithms 19-20.
311
+ * `encapsulate`'s optional `msg` is the 32-byte message randomness `m` of Algorithm 17, the
312
+ * pre-image the shared secret is derived from, NOT a plaintext to encrypt: ML-KEM is a key
313
+ * encapsulation mechanism, not a cipher. Omit it to draw fresh randomness; pass it only to
314
+ * reproduce a known-answer vector, and only as 32 uniformly random bytes, since a low-entropy or
315
+ * reused value makes the shared secret predictable. The same holds for `keygen`'s optional `seed`.
277
316
  * decapsulate() tries to follow the Algorithms 18/21 implicit-reject structure as closely as
278
317
  * practical here by re-encrypting, comparing ciphertexts, returning `Khat` on match or `Kbar` on
279
318
  * mismatch, and zeroizing the non-returned shared-secret candidate; JS/JIT still provides no
@@ -287,6 +326,18 @@ function createKyber(opts) {
287
326
  const secretCoder = splitCoder('secretKey', lengths.secretKey, lengths.publicKey, 32, 32);
288
327
  const msgLen = 32;
289
328
  const seedLen = 64;
329
+ // FIPS-203 includes additional verification check for modulus
330
+ const validateModulus = (publicKey, fn) => {
331
+ const eke = publicKey.subarray(0, 384 * rawOpts.K);
332
+ // Copy because of inplace encoding
333
+ const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
334
+ // (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
335
+ // If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
336
+ const ok = equalBytes(ek, eke);
337
+ cleanBytes(ek);
338
+ if (!ok)
339
+ throw new Error(`ML-KEM.${fn}: wrong publicKey modulus`);
340
+ };
290
341
  const kemLengths = Object.freeze({
291
342
  ...lengths,
292
343
  seed: 64,
@@ -297,44 +348,65 @@ function createKyber(opts) {
297
348
  return Object.freeze({
298
349
  info: Object.freeze({ type: 'ml-kem' }),
299
350
  lengths: kemLengths,
300
- keygen: (seed = randomBytes(seedLen)) => {
301
- abytes(seed, seedLen, 'seed');
302
- const { publicKey, secretKey: sk } = KPKE.keygen(seed.subarray(0, 32));
303
- const publicKeyHash = HASH256(publicKey);
304
- // (dkPKE||ek||H(ek)||z)
305
- const secretKey = secretCoder.encode([sk, publicKey, publicKeyHash, seed.subarray(32)]);
306
- cleanBytes(sk, publicKeyHash);
307
- return {
308
- publicKey: publicKey,
309
- secretKey: secretKey,
310
- };
351
+ keygen: (seed) => {
352
+ // A generated seed carries z (the implicit-rejection secret) and must be wiped once the
353
+ // secret key holds a copy, matching ml-dsa / slh-dsa / falcon keygen. A caller-supplied
354
+ // seed is the caller's to manage (and the immutability test requires it stay untouched).
355
+ const ownSeed = seed === undefined;
356
+ const s = ownSeed ? randomBytes(seedLen) : seed;
357
+ let sk;
358
+ let publicKeyHash;
359
+ try {
360
+ abytes(s, seedLen, 'seed');
361
+ const keys = KPKE.keygen(s.subarray(0, 32));
362
+ const publicKey = keys.publicKey;
363
+ sk = keys.secretKey;
364
+ publicKeyHash = HASH256(publicKey);
365
+ // (dkPKE||ek||H(ek)||z)
366
+ const secretKey = secretCoder.encode([sk, publicKey, publicKeyHash, s.subarray(32)]);
367
+ return {
368
+ publicKey: publicKey,
369
+ secretKey: secretKey,
370
+ };
371
+ }
372
+ finally {
373
+ if (sk !== undefined)
374
+ cleanBytes(sk);
375
+ if (publicKeyHash !== undefined)
376
+ cleanBytes(publicKeyHash);
377
+ if (ownSeed)
378
+ cleanBytes(s);
379
+ }
311
380
  },
312
381
  getPublicKey: (secretKey) => {
313
382
  const [_sk, publicKey, _publicKeyHash, _z] = secretCoder.decode(secretKey);
314
383
  return Uint8Array.from(publicKey);
315
384
  },
316
- encapsulate: (publicKey, msg = randomBytes(msgLen)) => {
317
- abytes(publicKey, lengths.publicKey, 'publicKey');
318
- 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');
385
+ encapsulate: (publicKey, msg) => {
386
+ // A generated message is the preimage of the shared secret (K = G(m || H(ek))[0:32]) and
387
+ // must be wiped. A caller-supplied message is the deterministic-randomness hook and the
388
+ // caller's to manage (the immutability test requires it stay untouched).
389
+ const ownMsg = msg === undefined;
390
+ const m = ownMsg ? randomBytes(msgLen) : msg;
391
+ let kr;
392
+ try {
393
+ abytes(publicKey, lengths.publicKey, 'publicKey');
394
+ abytes(m, msgLen, 'message');
395
+ validateModulus(publicKey, 'encapsulate');
396
+ // derive randomness
397
+ kr = HASH512.create().update(m).update(HASH256(publicKey)).digest();
398
+ const cipherText = KPKE.encrypt(publicKey, m, kr.subarray(32, 64));
399
+ return {
400
+ cipherText: cipherText,
401
+ sharedSecret: kr.subarray(0, 32),
402
+ };
403
+ }
404
+ finally {
405
+ if (kr !== undefined)
406
+ cleanBytes(kr.subarray(32));
407
+ if (ownMsg)
408
+ cleanBytes(m);
328
409
  }
329
- cleanBytes(ek);
330
- // derive randomness
331
- const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest();
332
- const cipherText = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64));
333
- cleanBytes(kr.subarray(32));
334
- return {
335
- cipherText: cipherText,
336
- sharedSecret: kr.subarray(0, 32),
337
- };
338
410
  },
339
411
  decapsulate: (cipherText, secretKey) => {
340
412
  abytes(secretKey, secretCoder.bytesLen, 'secretKey'); // 768*k + 96
@@ -356,9 +428,69 @@ function createKyber(opts) {
356
428
  // if ciphertexts do not match, “implicitly reject”
357
429
  const isValid = equalBytes(cipherText, cipherText2);
358
430
  const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
359
- cleanBytes(msg, cipherText2, !isValid ? Khat : Kbar);
431
+ // kr[32:64] is the derived K-PKE encryption randomness: wipe it like encapsulate() does.
432
+ cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
360
433
  return (isValid ? Khat : Kbar);
361
434
  },
435
+ /**
436
+ * Experimental prototype: pre-expand a public key so repeated encapsulate/decapsulate
437
+ * against the same key skip re-validation, H(ek), t̂ decoding and the K² SampleNTT
438
+ * XOF expansions of Â. Only public data is cached; see {@link KEMPrepared}.
439
+ */
440
+ prepare: (publicKey) => {
441
+ abytes(publicKey, lengths.publicKey, 'publicKey');
442
+ validateModulus(publicKey, 'prepare');
443
+ const ek = copyBytes(publicKey); // detach from the caller before caching
444
+ const publicKeyHash = HASH256(ek);
445
+ const cached = KPKE.prepare(ek);
446
+ return Object.freeze({
447
+ publicKey: ek,
448
+ encapsulate: (msg) => {
449
+ // As in the non-prepared encapsulate: a generated message is the shared-secret
450
+ // preimage and is wiped; a caller-supplied one is left untouched.
451
+ const ownMsg = msg === undefined;
452
+ const m = ownMsg ? randomBytes(msgLen) : msg;
453
+ let kr;
454
+ try {
455
+ abytes(m, msgLen, 'message');
456
+ kr = HASH512.create().update(m).update(publicKeyHash).digest();
457
+ const cipherText = cached.encrypt(m, kr.subarray(32, 64));
458
+ return {
459
+ cipherText: cipherText,
460
+ sharedSecret: kr.subarray(0, 32),
461
+ };
462
+ }
463
+ finally {
464
+ if (kr !== undefined)
465
+ cleanBytes(kr.subarray(32));
466
+ if (ownMsg)
467
+ cleanBytes(m);
468
+ }
469
+ },
470
+ decapsulate: (cipherText, secretKey) => {
471
+ abytes(secretKey, secretCoder.bytesLen, 'secretKey');
472
+ abytes(cipherText, lengths.cipherText, 'cipherText');
473
+ const [sk, ekEmbedded, storedHash, z] = secretCoder.decode(secretKey);
474
+ // Under KEMPrepared's read-only publicKey contract, bind dk to the prepared key.
475
+ // Together with publicKeyHash = H(ek) computed in prepare(), this is equivalent to (and
476
+ // stronger than) FIPS 203 §7.3's `H(dk[384k : 768k+32]) == dk[768k+32 : 768k+64]`.
477
+ if (!equalBytes(ekEmbedded, ek) || !equalBytes(storedHash, publicKeyHash))
478
+ throw new Error('ML-KEM.decapsulate: secretKey does not match prepared publicKey');
479
+ const msg = KPKE.decrypt(cipherText, sk);
480
+ // derive randomness, Khat, rHat = G(mHat || h)
481
+ const kr = HASH512.create().update(msg).update(publicKeyHash).digest();
482
+ const Khat = kr.subarray(0, 32);
483
+ // re-encrypt using the derived randomness and cached Â/t̂
484
+ const cipherText2 = cached.encrypt(msg, kr.subarray(32, 64));
485
+ // if ciphertexts do not match, “implicitly reject”
486
+ const isValid = equalBytes(cipherText, cipherText2);
487
+ const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
488
+ cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
489
+ return (isValid ? Khat : Kbar);
490
+ },
491
+ clean: cached.clean,
492
+ });
493
+ },
362
494
  });
363
495
  }
364
496
  // FIPS 203's PRF_eta binding: current callers use only 32-byte keys, one-byte nonces,
@@ -388,6 +520,17 @@ const mk = (params) => createKyber({
388
520
  /**
389
521
  * ML-KEM-512: Table 2 row `k=2, η1=3, η2=2, du=10, dv=4`; Table 3 sizes `800/1632/768/32`.
390
522
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
523
+ * @example
524
+ * Generate deterministic ML-KEM-512 keys, encapsulate a shared secret, and decapsulate it.
525
+ * ```ts
526
+ * import { ml_kem512 } from '@noble/post-quantum/ml-kem.js';
527
+ * const seed = new Uint8Array(ml_kem512.lengths.seed!);
528
+ * const { secretKey, publicKey } = ml_kem512.keygen(seed);
529
+ * const msg = new Uint8Array(ml_kem512.lengths.msgRand!);
530
+ * const { cipherText, sharedSecret } = ml_kem512.encapsulate(publicKey, msg);
531
+ * const recovered = ml_kem512.decapsulate(cipherText, secretKey);
532
+ * const publicKey2 = ml_kem512.getPublicKey(secretKey);
533
+ * ```
391
534
  */
392
535
  export const ml_kem512 = /* @__PURE__ */ (() => mk(PARAMS[512]))();
393
536
  /**
@@ -441,4 +584,3 @@ export const __tests = /* @__PURE__ */ (() => Object.freeze({
441
584
  }
442
585
  },
443
586
  }))();
444
- //# sourceMappingURL=ml-kem.js.map
package/package.json CHANGED
@@ -1,40 +1,35 @@
1
1
  {
2
2
  "name": "@noble/post-quantum",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
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.4.0",
12
+ "@noble/curves": "2.4.0",
13
+ "@noble/hashes": "2.4.0"
16
14
  },
17
15
  "devDependencies": {
18
- "@paulmillr/jsbt": "0.5.0",
19
- "@types/node": "25.3.0",
16
+ "@paulmillr/jsbt": "0.7.1",
17
+ "bismar": "0.1.8",
18
+ "@types/node": "26.2.0",
20
19
  "fast-check": "4.2.0",
21
- "prettier": "3.6.2",
22
- "typescript": "6.0.2"
20
+ "prettier": "3.9.6",
21
+ "typescript": "6.0.3"
23
22
  },
24
23
  "scripts": {
25
- "bench": "node test/benchmark.ts",
24
+ "benchmark": "node benchmark/pq.ts",
25
+ "benchmark:size": "bismar -bsm",
26
26
  "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",
27
+ "check": "jsbt-check",
28
+ "build:clean": "rm *.{js,d.ts} 2> /dev/null",
33
29
  "format": "prettier --write 'src/**/*.{js,ts}' 'test/**/*.{js,ts,mjs}'",
34
- "test": "node test/index.ts",
30
+ "test": "node --no-warnings test/index.ts",
35
31
  "test:bun": "bun test/index.ts",
36
32
  "test:deno": "deno --allow-env --allow-read test/index.ts",
37
- "test:node20": "cd test; npx tsc; node compiled/test/index.js",
38
33
  "test:slow": "SLOW_TESTS=1 node test/index.ts"
39
34
  },
40
35
  "exports": {
@@ -45,7 +40,8 @@
45
40
  "./ml-dsa.js": "./ml-dsa.js",
46
41
  "./ml-kem.js": "./ml-kem.js",
47
42
  "./slh-dsa.js": "./slh-dsa.js",
48
- "./utils.js": "./utils.js"
43
+ "./utils.js": "./utils.js",
44
+ "./webcrypto.js": "./webcrypto.js"
49
45
  },
50
46
  "engines": {
51
47
  "node": ">= 20.19.0"
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