@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/hybrid.js CHANGED
@@ -79,15 +79,38 @@ import {} from '@noble/curves/abstract/montgomery.js';
79
79
  import {} from '@noble/curves/abstract/weierstrass.js';
80
80
  import { x25519 } from '@noble/curves/ed25519.js';
81
81
  import { p256, p384 } from '@noble/curves/nist.js';
82
- import { asciiToBytes, bytesToNumberBE, bytesToNumberLE, concatBytes, numberToBytesBE, } from '@noble/curves/utils.js';
82
+ import { abool, afunction, asciiToBytes, bytesToNumberBE, bytesToNumberLE, concatBytes, numberToBytesBE, } from '@noble/curves/utils.js';
83
83
  import { expand, extract } from '@noble/hashes/hkdf.js';
84
84
  import { sha256 } from '@noble/hashes/sha2.js';
85
85
  import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
86
- import { abytes, ahash, anumber } from '@noble/hashes/utils.js';
86
+ import { abytes, ahash, anumber, isBytes } from '@noble/hashes/utils.js';
87
87
  import { ml_kem1024, ml_kem768 } from "./ml-kem.js";
88
- import { cleanBytes, copyBytes, randomBytes, splitCoder, validateSigOpts, validateVerOpts, } from "./utils.js";
88
+ import { aobject, astring, cleanBytes, copyBytes, randomBytes, splitCoder, validateSigOpts, validateVerOpts, } from "./utils.js";
89
+ const validateKEM = (kem, title) => {
90
+ const k = aobject(kem, title);
91
+ aobject(k.lengths, `${title}.lengths`);
92
+ afunction(k.keygen, `${title}.keygen`);
93
+ afunction(k.getPublicKey, `${title}.getPublicKey`);
94
+ afunction(k.encapsulate, `${title}.encapsulate`);
95
+ afunction(k.decapsulate, `${title}.decapsulate`);
96
+ return k;
97
+ };
98
+ const validateSigner = (signer, title) => {
99
+ const s = aobject(signer, title);
100
+ aobject(s.lengths, `${title}.lengths`);
101
+ afunction(s.keygen, `${title}.keygen`);
102
+ afunction(s.getPublicKey, `${title}.getPublicKey`);
103
+ afunction(s.sign, `${title}.sign`);
104
+ afunction(s.verify, `${title}.verify`);
105
+ return s;
106
+ };
89
107
  // Can re-use if decide to signatures support, on other hand getSecretKey is specific and ugly
90
108
  function ecKeygen(curve, allowZeroKey = false) {
109
+ const c = aobject(curve, 'curve');
110
+ aobject(c.lengths, 'curve.lengths');
111
+ afunction(c.keygen, 'curve.keygen');
112
+ afunction(c.getPublicKey, 'curve.getPublicKey');
113
+ abool(allowZeroKey, 'allowZeroKey');
91
114
  const lengths = curve.lengths;
92
115
  let keygen = curve.keygen;
93
116
  if (allowZeroKey) {
@@ -125,6 +148,12 @@ function ecKeygen(curve, allowZeroKey = false) {
125
148
  /**
126
149
  * Wraps an ECDH-capable curve as a KEM.
127
150
  * Shared secrets stay in the wrapped curve's raw ECDH byte format with no built-in KDF.
151
+ *
152
+ * SECURITY: this is a low-level component adapter, not a standalone IND-CCA-secure KEM. It does
153
+ * not bind the encapsulation or recipient public key into the secret, so distinct accepted point
154
+ * encodings can produce the same output. Use it only inside a construction whose specified
155
+ * combiner binds those values, or use a standardized DHKEM with labeled extract-and-expand.
156
+ *
128
157
  * On SEC 1 / Weierstrass curves, that means the compressed shared-point body without the
129
158
  * 1-byte `0x02` / `0x03` prefix.
130
159
  * The X25519 path also leaves RFC 7748's optional all-zero shared-secret check to callers.
@@ -140,15 +169,20 @@ function ecKeygen(curve, allowZeroKey = false) {
140
169
  * Wrap an ECDH-capable curve as a generic KEM.
141
170
  * ```ts
142
171
  * import { x25519 } from '@noble/curves/ed25519.js';
143
- * import { ecdhKem } from '@noble/post-quantum/hybrid.js';
144
- * const kem = ecdhKem(x25519);
172
+ * import { _ecdhKem } from '@noble/post-quantum/hybrid.js';
173
+ * const kem = _ecdhKem(x25519);
145
174
  * const publicKeyLen = kem.lengths.publicKey;
146
175
  * ```
147
176
  */
148
- export function ecdhKem(curve, allowZeroKey = false) {
177
+ export function _ecdhKem(curve, allowZeroKey = false) {
149
178
  const kg = ecKeygen(curve, allowZeroKey);
150
179
  if (!curve.getSharedSecret)
151
180
  throw new Error('wrong curve'); // ed25519 doesn't have one!
181
+ // Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
182
+ const decapsulate = (cipherText, secretKey) => {
183
+ const res = curve.getSharedSecret(secretKey, cipherText);
184
+ return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res);
185
+ };
152
186
  return {
153
187
  lengths: { ...kg.lengths, msg: kg.lengths.seed, cipherText: kg.lengths.publicKey },
154
188
  keygen: kg.keygen,
@@ -159,8 +193,8 @@ export function ecdhKem(curve, allowZeroKey = false) {
159
193
  const seed = copyBytes(rand);
160
194
  let ek = undefined;
161
195
  try {
162
- ek = this.keygen(seed).secretKey;
163
- const sharedSecret = this.decapsulate(publicKey, ek);
196
+ ek = kg.keygen(seed).secretKey;
197
+ const sharedSecret = decapsulate(publicKey, ek);
164
198
  const cipherText = curve.getPublicKey(ek);
165
199
  return { sharedSecret, cipherText };
166
200
  }
@@ -172,10 +206,7 @@ export function ecdhKem(curve, allowZeroKey = false) {
172
206
  cleanBytes(ek);
173
207
  }
174
208
  },
175
- decapsulate(cipherText, secretKey) {
176
- const res = curve.getSharedSecret(secretKey, cipherText);
177
- return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res);
178
- },
209
+ decapsulate,
179
210
  };
180
211
  }
181
212
  /**
@@ -208,7 +239,7 @@ export function ecSigner(curve, allowZeroKey = false) {
208
239
  keygen: kg.keygen,
209
240
  getPublicKey: kg.getPublicKey,
210
241
  sign: (message, secretKey, opts = {}) => {
211
- validateSigOpts(opts);
242
+ opts = validateSigOpts(opts);
212
243
  // This generic wrapper intentionally keeps the Signer contract to message + key only.
213
244
  // Backend-specific knobs like ECDSA extraEntropy or Ed25519ctx context cannot be forwarded
214
245
  // uniformly through combineSigners(), so callers that need them must use the curve directly.
@@ -223,20 +254,28 @@ export function ecSigner(curve, allowZeroKey = false) {
223
254
  * generic opts and lets wrapped-curve malformed-input errors escape unchanged.
224
255
  */
225
256
  verify: (signature, message, publicKey, opts = {}) => {
226
- validateVerOpts(opts);
257
+ opts = validateVerOpts(opts);
227
258
  if (opts.context !== undefined)
228
259
  throw new Error('ecSigner does not support context; use the underlying curve directly');
229
260
  return curve.verify(signature, message, publicKey);
230
261
  },
231
262
  };
232
263
  }
264
+ function positiveLength(value, title) {
265
+ const length = anumber(value, title);
266
+ if (length === 0)
267
+ throw new RangeError(`"${title}" expected integer greater than 0, got 0`);
268
+ return length;
269
+ }
233
270
  function splitLengths(lst, name) {
234
271
  // Preserve caller order exactly; raw numeric fields still decode as splitCoder() subarray views.
235
- return splitCoder(name, ...lst.map((i) => {
272
+ const coder = splitCoder(name, ...lst.map((i) => {
236
273
  if (typeof i.lengths[name] !== 'number')
237
274
  throw new Error('wrong length: ' + name);
238
- return i.lengths[name];
275
+ return positiveLength(i.lengths[name], name);
239
276
  }));
277
+ positiveLength(coder.bytesLen, name);
278
+ return coder;
240
279
  }
241
280
  // It is XOF for most cases, but can be more complex!
242
281
  /**
@@ -265,11 +304,9 @@ expandSeed_, ...ck_) {
265
304
  const seedCoder = splitLengths(ck, 'seed');
266
305
  const pkCoder = splitLengths(ck, 'publicKey');
267
306
  // Allows to use identity functions for combiner/expandSeed
268
- if (realSeedLen === undefined)
269
- realSeedLen = seedCoder.bytesLen;
270
- anumber(realSeedLen);
307
+ const rootSeedLen = positiveLength(realSeedLen === undefined ? seedCoder.bytesLen : realSeedLen, 'realSeedLen');
271
308
  function expandDecapsulationKey(seed) {
272
- abytes(seed, realSeedLen);
309
+ abytes(seed, rootSeedLen);
273
310
  const expandedRaw = expandSeed(seed, seedCoder.bytesLen);
274
311
  // Identity/subarray expanders can hand back caller-owned seed storage. Detach those outputs so
275
312
  // later cleanup can wipe the expanded schedule without mutating the caller's root seed bytes.
@@ -303,39 +340,62 @@ expandSeed_, ...ck_) {
303
340
  cleanBytes(secretKey);
304
341
  }
305
342
  }
306
- return {
307
- info: { lengths: { seed: realSeedLen, publicKey: pkCoder.bytesLen, secretKey: realSeedLen } },
308
- getPublicKey(secretKey) {
309
- // Composite secret keys are root seeds, so public-key derivation reruns key expansion from
310
- // that seed instead of decoding a packed child-secret-key structure.
311
- return this.keygen(secretKey).publicKey;
312
- },
313
- keygen(seed = randomBytes(realSeedLen)) {
314
- const { publicKey: pk, secretKey } = expandDecapsulationKey(seed);
343
+ // Standalone (not a method) so getPublicKey / destructured usage never depends on `this`.
344
+ const keygen = (seed) => {
345
+ // Detach the root: the exported secretKey must not alias caller-owned seed bytes, so later
346
+ // caller mutation of the seed cannot silently change the secret key (and vice versa).
347
+ const root = seed === undefined ? randomBytes(rootSeedLen) : copyBytes(seed);
348
+ let res;
349
+ try {
350
+ const { publicKey: pk, secretKey } = expandDecapsulationKey(root);
315
351
  try {
316
- const publicKey = pkCoder.encode(pk);
317
- return { secretKey: seed, publicKey };
352
+ res = {
353
+ secretKey: root,
354
+ publicKey: pkCoder.encode(pk),
355
+ };
318
356
  }
319
357
  finally {
320
- cleanBytes(pk);
321
- // The exported secretKey is the caller/root seed itself; child secret keys are internal
358
+ // The exported secretKey is the (detached) root seed; child secret keys are internal
322
359
  // expansion outputs that are cleaned whether encoding succeeds or throws.
323
- cleanBytes(secretKey);
360
+ cleanBytes(pk, secretKey);
324
361
  }
362
+ return res;
363
+ }
364
+ finally {
365
+ if (!res)
366
+ cleanBytes(root);
367
+ }
368
+ };
369
+ return {
370
+ info: { lengths: { seed: rootSeedLen, publicKey: pkCoder.bytesLen, secretKey: rootSeedLen } },
371
+ // Composite secret keys are root seeds, so public-key derivation reruns key expansion from
372
+ // that seed instead of decoding a packed child-secret-key structure.
373
+ getPublicKey: (secretKey) => {
374
+ const keys = keygen(secretKey);
375
+ // keygen detaches its exported root; getPublicKey discards that half of the result.
376
+ cleanBytes(keys.secretKey);
377
+ return keys.publicKey;
325
378
  },
379
+ keygen,
326
380
  expandDecapsulationKey,
327
- realSeedLen,
381
+ realSeedLen: rootSeedLen,
328
382
  };
329
383
  }
330
384
  // This generic function that combines multiple KEMs into single one
331
385
  /**
332
386
  * Combines multiple KEMs into one composite KEM.
333
- * @param realSeedLen - Input seed length expected by `expandSeed`.
334
- * @param realMsgLen - Shared-secret length returned by `combiner`.
387
+ * @param realSeedLen - Positive input seed length expected by `expandSeed`, or `undefined` to use
388
+ * the sum of component seed lengths. Callers remain responsible for choosing a security-appropriate
389
+ * size.
390
+ * @param realMsgLen - Positive shared-secret length returned by `combiner`, or `undefined` to use
391
+ * the sum of component message lengths.
335
392
  * @param expandSeed - Seed expander used to derive per-KEM seeds.
336
393
  * @param combiner - Combines the per-KEM outputs into one shared secret.
337
- * @param kems - KEM implementations to combine.
394
+ * @param kems - At least one KEM implementation. A construction advertised as hybrid normally
395
+ * supplies two or more.
338
396
  * @returns Composite KEM.
397
+ * @throws On wrong argument types. {@link TypeError}
398
+ * @throws If there are no components or any required length resolves to zero. {@link RangeError}
339
399
  * @example
340
400
  * Combine multiple KEMs into one composite KEM.
341
401
  * ```ts
@@ -356,21 +416,53 @@ expandSeed_, ...ck_) {
356
416
  export function combineKEMS(realSeedLen, // how much bytes expandSeed expects
357
417
  realMsgLen, // how much bytes combiner returns
358
418
  expandSeed, combiner, ...kems) {
419
+ if (realSeedLen !== undefined)
420
+ positiveLength(realSeedLen, 'realSeedLen');
421
+ if (realMsgLen !== undefined)
422
+ positiveLength(realMsgLen, 'realMsgLen');
423
+ if (typeof expandSeed !== 'function')
424
+ throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
425
+ if (typeof combiner !== 'function')
426
+ throw new TypeError('"combiner" expected function, got type=' + typeof combiner);
359
427
  const rawCombiner = combiner;
360
428
  const rawKems = kems;
429
+ if (rawKems.length === 0)
430
+ throw new RangeError('combineKEMS requires at least one KEM');
431
+ for (let i = 0; i < rawKems.length; i++)
432
+ validateKEM(rawKems[i], `kems[${i}]`);
361
433
  const keys = combineKeys(realSeedLen, expandSeed, ...rawKems);
362
434
  const ctCoder = splitLengths(rawKems, 'cipherText');
363
435
  const pkCoder = splitLengths(rawKems, 'publicKey');
364
436
  const msgCoder = splitLengths(rawKems, 'msg');
365
- if (realMsgLen === undefined)
366
- realMsgLen = msgCoder.bytesLen;
367
- anumber(realMsgLen);
437
+ const sharedSecretLen = positiveLength(realMsgLen === undefined ? msgCoder.bytesLen : realMsgLen, 'realMsgLen');
368
438
  const lengths = Object.freeze({
369
439
  ...keys.info.lengths,
370
- msg: realMsgLen,
440
+ msg: sharedSecretLen,
371
441
  msgRand: msgCoder.bytesLen,
372
442
  cipherText: ctCoder.bytesLen,
373
443
  });
444
+ const combine = (publicKeys, cipherTexts, sharedSecrets) => {
445
+ const combined = rawCombiner(publicKeys, cipherTexts, sharedSecrets);
446
+ try {
447
+ return copyBytes(abytes(combined, sharedSecretLen, 'sharedSecret'));
448
+ }
449
+ catch (error) {
450
+ if (isBytes(combined)) {
451
+ // A combiner may return any callback argument. Public keys during encapsulation and
452
+ // ciphertexts during decapsulation are views into caller-owned inputs, so wipe an invalid
453
+ // byte result only when its range does not overlap either public argument vector. Child
454
+ // shared-secret aliases are already wiped by the operation's outer finally block.
455
+ const overlaps = (value) => combined.buffer === value.buffer &&
456
+ combined.byteOffset < value.byteOffset + value.byteLength &&
457
+ value.byteOffset < combined.byteOffset + combined.byteLength;
458
+ const aliasesPublicInput = publicKeys.some(overlaps) ||
459
+ cipherTexts.some(overlaps);
460
+ if (!aliasesPublicInput)
461
+ cleanBytes(combined);
462
+ }
463
+ throw error;
464
+ }
465
+ };
374
466
  return Object.freeze({
375
467
  lengths,
376
468
  getPublicKey: keys.getPublicKey,
@@ -386,11 +478,14 @@ expandSeed, combiner, ...kems) {
386
478
  sharedSecret.push(enc.sharedSecret);
387
479
  cipherText.push(enc.cipherText);
388
480
  }
481
+ // Validate and detach public ciphertexts before deriving a final secret from them. This
482
+ // also ensures a malformed child cannot make us allocate and then strand a combined key.
483
+ const encodedCipherText = ctCoder.encode(cipherText);
389
484
  return {
390
485
  // Detach the combiner result before cleanup: a caller-provided combiner may alias one of
391
486
  // the child sharedSecret buffers, and those child buffers are zeroized immediately below.
392
- sharedSecret: copyBytes(rawCombiner(pks, cipherText, sharedSecret)),
393
- cipherText: ctCoder.encode(cipherText),
487
+ sharedSecret: combine(pks, cipherText, sharedSecret),
488
+ cipherText: encodedCipherText,
394
489
  };
395
490
  }
396
491
  finally {
@@ -402,11 +497,16 @@ expandSeed, combiner, ...kems) {
402
497
  decapsulate(ct, seed) {
403
498
  const cts = ctCoder.decode(ct);
404
499
  const { publicKey, secretKey } = keys.expandDecapsulationKey(seed);
405
- const sharedSecret = rawKems.map((i, j) => i.decapsulate(cts[j], secretKey[j]));
500
+ const sharedSecret = [];
406
501
  try {
502
+ // Child decapsulate() is inside the try: it can throw on an attacker-supplied ciphertext
503
+ // (e.g. a low-order X25519 point), and by then the expanded child secret keys — plus any
504
+ // child shared secrets already produced — are live and must still be wiped.
505
+ for (let i = 0; i < rawKems.length; i++)
506
+ sharedSecret.push(rawKems[i].decapsulate(cts[i], secretKey[i]));
407
507
  // Detach the decapsulation result before cleanup: the combiner may hand back one of the
408
508
  // child shared-secret buffers, and those temporary buffers are zeroized below.
409
- return copyBytes(rawCombiner(publicKey, cts, sharedSecret));
509
+ return combine(publicKey, cts, sharedSecret);
410
510
  }
411
511
  finally {
412
512
  // Decapsulation only needs the expanded child secret keys and child shared secrets for this
@@ -420,10 +520,15 @@ expandSeed, combiner, ...kems) {
420
520
  // realSeedLen: how much bytes expandSeed expects.
421
521
  /**
422
522
  * Combines multiple signers into one composite signer.
423
- * @param realSeedLen - Input seed length expected by `expandSeed`.
523
+ * @param realSeedLen - Positive input seed length expected by `expandSeed`, or `undefined` to use
524
+ * the sum of component seed lengths. Callers remain responsible for choosing a security-appropriate
525
+ * size.
424
526
  * @param expandSeed - Seed expander used to derive per-signer seeds.
425
- * @param signers - Signers to combine.
527
+ * @param signers - At least one signer. A construction advertised as hybrid normally supplies two
528
+ * or more.
426
529
  * @returns Composite signer.
530
+ * @throws On wrong argument types. {@link TypeError}
531
+ * @throws If there are no components or any required length resolves to zero. {@link RangeError}
427
532
  * @example
428
533
  * Combine multiple signers into one composite signer.
429
534
  * ```ts
@@ -431,11 +536,23 @@ expandSeed, combiner, ...kems) {
431
536
  * import { combineSigners, expandSeedXof } from '@noble/post-quantum/hybrid.js';
432
537
  * import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
433
538
  * const hybrid = combineSigners(32, expandSeedXof(shake256), ml_dsa44, ml_dsa44);
434
- * const { publicKey } = hybrid.keygen();
539
+ * const seed = new Uint8Array(hybrid.lengths.seed!).fill(1);
540
+ * const { secretKey, publicKey } = hybrid.keygen(seed);
541
+ * const msg = new TextEncoder().encode('hello noble');
542
+ * const sig = hybrid.sign(msg, secretKey);
543
+ * const isValid = hybrid.verify(sig, msg, publicKey);
435
544
  * ```
436
545
  */
437
546
  export function combineSigners(realSeedLen, expandSeed, ...signers) {
547
+ if (realSeedLen !== undefined)
548
+ positiveLength(realSeedLen, 'realSeedLen');
549
+ if (typeof expandSeed !== 'function')
550
+ throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
438
551
  const rawSigners = signers;
552
+ if (rawSigners.length === 0)
553
+ throw new RangeError('combineSigners requires at least one signer');
554
+ for (let i = 0; i < rawSigners.length; i++)
555
+ validateSigner(rawSigners[i], `signers[${i}]`);
439
556
  const keys = combineKeys(realSeedLen, expandSeed, ...rawSigners);
440
557
  const sigCoder = splitLengths(rawSigners, 'signature');
441
558
  const pkCoder = splitLengths(rawSigners, 'publicKey');
@@ -444,7 +561,7 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
444
561
  getPublicKey: keys.getPublicKey,
445
562
  keygen: keys.keygen,
446
563
  sign(message, seed, opts = {}) {
447
- validateSigOpts(opts);
564
+ opts = validateSigOpts(opts);
448
565
  // This generic wrapper intentionally keeps the composite signer contract to message + root
449
566
  // seed only. Per-signer opts like context or extraEntropy cannot be preserved uniformly
450
567
  // across mixed backends, so callers that need them must use the underlying signer directly.
@@ -464,14 +581,21 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
464
581
  }
465
582
  },
466
583
  /** Verify one combined signature.
467
- * Returns `false` when the aggregate signature/publicKey decode succeeds but any child verify
468
- * check fails. Throws on unsupported generic opts or malformed aggregate encodings.
584
+ * Wrong-length aggregate signatures return `false` (matching ml-dsa / slh-dsa behavior), as
585
+ * does any failing child verify. Throws on unsupported generic opts or malformed publicKey.
469
586
  */
470
587
  verify: (signature, message, publicKey, opts = {}) => {
471
- validateVerOpts(opts);
588
+ opts = validateVerOpts(opts);
472
589
  if (opts.context !== undefined)
473
590
  throw new Error('combineSigners does not support context; use the underlying signer directly');
591
+ // Malformed signature *length* is a verification failure, not a thrown type error —
592
+ // consistent with ml-dsa / slh-dsa. Must run before sigCoder.decode, which throws.
593
+ // Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
594
+ abytes(signature, undefined, 'signature');
595
+ // A signature failure must not hide malformed aggregate public-key bytes.
474
596
  const pks = pkCoder.decode(publicKey);
597
+ if (signature.length !== sigCoder.bytesLen)
598
+ return false;
475
599
  const sigs = sigCoder.decode(signature);
476
600
  for (let i = 0; i < rawSigners.length; i++) {
477
601
  if (!rawSigners[i].verify(sigs[i], message, pks[i]))
@@ -494,26 +618,35 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
494
618
  * @param xof - XOF used for seed expansion.
495
619
  * @param kdf - Hash used for the final combiner.
496
620
  * @returns Hybrid KEM.
621
+ * @throws On wrong argument types. {@link TypeError}
622
+ * @throws On wrong argument ranges or values. {@link RangeError}
497
623
  * @example
498
624
  * Build a QSF hybrid KEM preset from a PQ KEM and an elliptic-curve KEM.
499
625
  * ```ts
500
626
  * import { p256 } from '@noble/curves/nist.js';
501
627
  * import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
502
- * import { QSF, ecdhKem } from '@noble/post-quantum/hybrid.js';
628
+ * import { QSF, _ecdhKem } from '@noble/post-quantum/hybrid.js';
503
629
  * import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
504
- * const kem = QSF('example', ml_kem768, ecdhKem(p256, true), shake256, sha3_256);
630
+ * const kem = QSF('example', ml_kem768, _ecdhKem(p256, true), shake256, sha3_256);
505
631
  * const publicKeyLen = kem.lengths.publicKey;
506
632
  * ```
507
633
  */
508
634
  export function QSF(label, pqc, curveKEM, xof, kdf) {
635
+ astring(label, 'label');
636
+ validateKEM(pqc, 'pqc');
637
+ validateKEM(curveKEM, 'curveKEM');
638
+ if (typeof xof !== 'function' || typeof xof.create !== 'function')
639
+ throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
509
640
  ahash(xof);
641
+ if (typeof kdf !== 'function' || typeof kdf.create !== 'function')
642
+ throw new TypeError('"kdf" expected hash function, got type=' + typeof kdf);
510
643
  ahash(kdf);
511
644
  return combineKEMS(32, kdf.outputLen, expandSeedXof(xof), (pk, ct, ss) => kdf(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))), pqc, curveKEM);
512
645
  }
513
646
  /** QSF preset combining ML-KEM-768 with P-256. */
514
- export const QSF_ml_kem768_p256 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-768,P-256)-XOF(SHAKE256)-KDF(SHA3-256)', ml_kem768, ecdhKem(p256, true), shake256, sha3_256))();
647
+ export const QSF_ml_kem768_p256 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-768,P-256)-XOF(SHAKE256)-KDF(SHA3-256)', ml_kem768, _ecdhKem(p256, true), shake256, sha3_256))();
515
648
  /** QSF preset combining ML-KEM-1024 with P-384. */
516
- export const QSF_ml_kem1024_p384 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-1024,P-384)-XOF(SHAKE256)-KDF(SHA3-256)', ml_kem1024, ecdhKem(p384, true), shake256, sha3_256))();
649
+ export const QSF_ml_kem1024_p384 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-1024,P-384)-XOF(SHAKE256)-KDF(SHA3-256)', ml_kem1024, _ecdhKem(p384, true), shake256, sha3_256))();
517
650
  /**
518
651
  * Builds the "KitchenSink" hybrid KEM combiner.
519
652
  * The current builder always derives a fixed 32-byte output,
@@ -528,20 +661,29 @@ export const QSF_ml_kem1024_p384 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-10
528
661
  * @param xof - XOF used for seed expansion.
529
662
  * @param hash - Hash used for HKDF extraction and expansion.
530
663
  * @returns Hybrid KEM.
664
+ * @throws On wrong argument types. {@link TypeError}
665
+ * @throws On wrong argument ranges or values. {@link RangeError}
531
666
  * @example
532
667
  * Build the "KitchenSink" hybrid KEM combiner.
533
668
  * ```ts
534
669
  * import { sha256 } from '@noble/hashes/sha2.js';
535
670
  * import { shake256 } from '@noble/hashes/sha3.js';
536
- * import { createKitchenSink, ecdhKem } from '@noble/post-quantum/hybrid.js';
671
+ * import { createKitchenSink, _ecdhKem } from '@noble/post-quantum/hybrid.js';
537
672
  * import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
538
673
  * import { x25519 } from '@noble/curves/ed25519.js';
539
- * const kem = createKitchenSink('example', ml_kem768, ecdhKem(x25519), shake256, sha256);
674
+ * const kem = createKitchenSink('example', ml_kem768, _ecdhKem(x25519), shake256, sha256);
540
675
  * const publicKeyLen = kem.lengths.publicKey;
541
676
  * ```
542
677
  */
543
678
  export function createKitchenSink(label, pqc, curveKEM, xof, hash) {
679
+ astring(label, 'label');
680
+ validateKEM(pqc, 'pqc');
681
+ validateKEM(curveKEM, 'curveKEM');
682
+ if (typeof xof !== 'function' || typeof xof.create !== 'function')
683
+ throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
544
684
  ahash(xof);
685
+ if (typeof hash !== 'function' || typeof hash.create !== 'function')
686
+ throw new TypeError('"hash" expected hash function, got type=' + typeof hash);
545
687
  ahash(hash);
546
688
  return combineKEMS(32, 32, expandSeedXof(xof), (pk, ct, ss) => {
547
689
  const preimage = concatBytes(ss[0], ss[1], ct[0], pk[0], ct[1], pk[1], asciiToBytes(label));
@@ -554,9 +696,9 @@ export function createKitchenSink(label, pqc, curveKEM, xof, hash) {
554
696
  return res;
555
697
  }, pqc, curveKEM);
556
698
  }
557
- // Internal alias only: this stays exactly `ecdhKem(x25519)`
699
+ // Internal alias only: this stays exactly `_ecdhKem(x25519)`
558
700
  // and inherits that wrapper's mutation/oracle behavior.
559
- const x25519kem = /* @__PURE__ */ ecdhKem(x25519);
701
+ const x25519kem = /* @__PURE__ */ _ecdhKem(x25519);
560
702
  /** KitchenSink preset combining ML-KEM-768 with X25519.
561
703
  * Caller randomness splits into 32 ML-KEM coins plus a 32-byte X25519 ephemeral-secret seed.
562
704
  */
@@ -596,6 +738,11 @@ function nistCurveKem(curve, scalarLen, elemLen, nseed) {
596
738
  const publicKey = curve.getPublicKey(secretKey, false);
597
739
  return { secretKey, publicKey };
598
740
  }
741
+ // Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
742
+ const decapsulate = (cipherText, secretKey) => {
743
+ const full = curve.getSharedSecret(secretKey, cipherText);
744
+ return full.subarray(1);
745
+ };
599
746
  return {
600
747
  lengths: {
601
748
  secretKey: scalarLen,
@@ -616,7 +763,7 @@ function nistCurveKem(curve, scalarLen, elemLen, nseed) {
616
763
  let ek = undefined;
617
764
  try {
618
765
  ek = rejectionSampling(rand).secretKey;
619
- const sharedSecret = this.decapsulate(publicKey, ek);
766
+ const sharedSecret = decapsulate(publicKey, ek);
620
767
  const cipherText = curve.getPublicKey(ek, false);
621
768
  return { sharedSecret, cipherText };
622
769
  }
@@ -627,10 +774,7 @@ function nistCurveKem(curve, scalarLen, elemLen, nseed) {
627
774
  cleanBytes(ek);
628
775
  }
629
776
  },
630
- decapsulate(cipherText, secretKey) {
631
- const full = curve.getSharedSecret(secretKey, cipherText);
632
- return full.subarray(1);
633
- },
777
+ decapsulate,
634
778
  };
635
779
  }
636
780
  /**
@@ -650,29 +794,14 @@ function concreteHybridKem(label, mlkem, curve, nseed) {
650
794
  const totalSeedLen = mlkemSeedLen + nseed;
651
795
  return combineKEMS(32, 32, (seed) => {
652
796
  abytes(seed, 32);
653
- const expanded = shake256(seed, { dkLen: totalSeedLen });
654
- const mlkemSeed = expanded.subarray(0, mlkemSeedLen);
655
- const curveSeed = expanded.subarray(mlkemSeedLen, totalSeedLen);
656
- return concatBytes(mlkemSeed, curveSeed);
797
+ // One SHAKE256 stream split by the seed coder as mlkemSeed (64) || curveSeed (nseed).
798
+ // Returned directly: the previous concatBytes of two adjacent subarrays produced an
799
+ // identical copy while leaving this original buffer unwiped; expandDecapsulationKey
800
+ // wipes the returned buffer after the child seeds are copied out.
801
+ return shake256(seed, { dkLen: totalSeedLen });
657
802
  }, (pk, ct, ss) => sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))), mlkem, curveKem);
658
803
  }
659
804
  /** P-256 + ML-KEM-768 hybrid preset. */
660
805
  export const ml_kem768_p256 = /* @__PURE__ */ (() => concreteHybridKem('MLKEM768-P256', ml_kem768, p256, 128))();
661
806
  /** P-384 + ML-KEM-1024 hybrid preset. */
662
807
  export const ml_kem1024_p384 = /* @__PURE__ */ (() => concreteHybridKem('MLKEM1024-P384', ml_kem1024, p384, 48))();
663
- // Legacy aliases
664
- /** Legacy alias for `ml_kem768_x25519`. */
665
- export const XWing = /* @__PURE__ */ (() => ml_kem768_x25519)();
666
- /** Legacy alias for `ml_kem768_x25519`. */
667
- export const MLKEM768X25519 = /* @__PURE__ */ (() => ml_kem768_x25519)();
668
- /** Legacy alias for `ml_kem768_p256`. */
669
- export const MLKEM768P256 = /* @__PURE__ */ (() => ml_kem768_p256)();
670
- /** Legacy alias for `ml_kem1024_p384`. */
671
- export const MLKEM1024P384 = /* @__PURE__ */ (() => ml_kem1024_p384)();
672
- /** Legacy alias for `QSF_ml_kem768_p256`. */
673
- export const QSFMLKEM768P256 = /* @__PURE__ */ (() => QSF_ml_kem768_p256)();
674
- /** Legacy alias for `QSF_ml_kem1024_p384`. */
675
- export const QSFMLKEM1024P384 = /* @__PURE__ */ (() => QSF_ml_kem1024_p384)();
676
- /** Legacy alias for `KitchenSink_ml_kem768_x25519`. */
677
- export const KitchenSinkMLKEM768X25519 = /* @__PURE__ */ (() => KitchenSink_ml_kem768_x25519)();
678
- //# sourceMappingURL=hybrid.js.map
package/index.d.ts CHANGED
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -19,11 +19,10 @@ import {
19
19
  } from '@noble/post-quantum/falcon.js';
20
20
  import {
21
21
  ml_kem768_x25519, ml_kem768_p256, ml_kem1024_p384,
22
- KitchenSink_ml_kem768_x25519, XWing,
22
+ KitchenSink_ml_kem768_x25519,
23
23
  QSF_ml_kem768_p256, QSF_ml_kem1024_p384,
24
24
  } from '@noble/post-quantum/hybrid.js';
25
25
  ```
26
26
  */
27
27
  throw new Error('root module cannot be imported: import submodules instead. Check out README');
28
28
  export {};
29
- //# sourceMappingURL=index.js.map
package/ml-dsa.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { type CryptoKeys, type Signer, type SigOpts, type TArg, type TRet, type VerOpts } from './utils.ts';
1
+ import type { CHash } from '@noble/hashes/utils.js';
2
+ import { type CryptoKeys, type Signer, type SigOpts, type TArg, type TRet } from './utils.ts';
2
3
  /** Internal ML-DSA options. */
3
4
  export type DSAInternalOpts = {
4
5
  /**
@@ -11,12 +12,19 @@ export type DSAInternalOpts = {
11
12
  /** ML-DSA signer surface with access to the internal message formatting mode. */
12
13
  export type DSAInternal = CryptoKeys & {
13
14
  lengths: Signer['lengths'];
14
- sign: (msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts?: TArg<SigOpts & DSAInternalOpts>) => TRet<Uint8Array>;
15
- verify: (sig: TArg<Uint8Array>, msg: TArg<Uint8Array>, pubKey: TArg<Uint8Array>, opts?: TArg<VerOpts & DSAInternalOpts>) => boolean;
15
+ sign: (msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts?: TArg<Omit<SigOpts, 'context'> & DSAInternalOpts>) => TRet<Uint8Array>;
16
+ verify: (sig: TArg<Uint8Array>, msg: TArg<Uint8Array>, pubKey: TArg<Uint8Array>, opts?: TArg<DSAInternalOpts>) => boolean;
16
17
  };
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