@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/falcon.js CHANGED
@@ -403,6 +403,17 @@ const Q = 12289; // 12 * 1024 + 1
403
403
  // Falcon's midpoint floor(q/2); the only live use is the mirrored G-reconstruction reduction below.
404
404
  const Qhalf = Q >> 1;
405
405
  const QBig = BigInt(Q);
406
+ const _0n = /* @__PURE__ */ BigInt(0);
407
+ const _1n = /* @__PURE__ */ BigInt(1);
408
+ const _10n = /* @__PURE__ */ BigInt(10);
409
+ const _25n = /* @__PURE__ */ BigInt(25);
410
+ const _31n = /* @__PURE__ */ BigInt(31);
411
+ const _32n = /* @__PURE__ */ BigInt(32);
412
+ const _63n = /* @__PURE__ */ BigInt(63);
413
+ const _64n = /* @__PURE__ */ BigInt(64);
414
+ // Low 32 bits and low 63 bits of a bigint, used by the chacha20 counter and gaussian sampler.
415
+ const MASK_32n = /* @__PURE__ */ BigInt('0xffffffff');
416
+ const MASK_63n = /* @__PURE__ */ BigInt('0x7fffffffffffffff');
406
417
  //const R = 4091; // 2^16 mod q
407
418
  // This 16-bit Montgomery kernel uses R = 2^16, so mul(x, R2) converts x into Montgomery form.
408
419
  const R2 = 10952; // 2^32 mod q
@@ -439,35 +450,35 @@ const BITLENGTH = [
439
450
  // Smaller Falcon dimensions reuse the N = 1024, q = 12289 table by summing 2^(10-logn) draws.
440
451
  // The trailing 0 sentinel guarantees gaussSingle()
441
452
  // always selects a tail bucket when x = 0 is missed.
442
- const gauss_1024_12289 = [
443
- 1283868770400643928n,
444
- 6416574995475331444n,
445
- 4078260278032692663n,
446
- 2353523259288686585n,
447
- 1227179971273316331n,
448
- 575931623374121527n,
449
- 242543240509105209n,
450
- 91437049221049666n,
451
- 30799446349977173n,
452
- 9255276791179340n,
453
- 2478152334826140n,
454
- 590642893610164n,
455
- 125206034929641n,
456
- 23590435911403n,
457
- 3948334035941n,
458
- 586753615614n,
459
- 77391054539n,
460
- 9056793210n,
461
- 940121950n,
462
- 86539696n,
463
- 7062824n,
464
- 510971n,
465
- 32764n,
466
- 1862n,
467
- 94n,
468
- 4n,
469
- 0n,
470
- ];
453
+ const gauss_1024_12289 = /* @__PURE__ */ [
454
+ '1283868770400643928',
455
+ '6416574995475331444',
456
+ '4078260278032692663',
457
+ '2353523259288686585',
458
+ '1227179971273316331',
459
+ '575931623374121527',
460
+ '242543240509105209',
461
+ '91437049221049666',
462
+ '30799446349977173',
463
+ '9255276791179340',
464
+ '2478152334826140',
465
+ '590642893610164',
466
+ '125206034929641',
467
+ '23590435911403',
468
+ '3948334035941',
469
+ '586753615614',
470
+ '77391054539',
471
+ '9056793210',
472
+ '940121950',
473
+ '86539696',
474
+ '7062824',
475
+ '510971',
476
+ '32764',
477
+ '1862',
478
+ '94',
479
+ '4',
480
+ '0',
481
+ ].map(BigInt);
471
482
  // Exact binary64 1/sigma payloads from round-3 fpr.h. Nearby decimal spellings round 1 ULP low in
472
483
  // JS, so keep these as decoded bit patterns and recheck the raw payloads after edits.
473
484
  const INV_SIGMA = /* @__PURE__ */ Object.freeze([
@@ -1147,9 +1158,9 @@ function genFalcon(opts) {
1147
1158
  let val = 0;
1148
1159
  for (let i = 0; i < g; i++) {
1149
1160
  const r128 = bytesToNumberLE(this.shake.xof(16));
1150
- const r1 = r128 & 0x7fffffffffffffffn;
1151
- const r2 = (r128 >> 64n) & 0x7fffffffffffffffn;
1152
- const sign = Number((r128 >> 63n) & 1n);
1161
+ const r1 = r128 & MASK_63n;
1162
+ const r2 = (r128 >> _64n) & MASK_63n;
1163
+ const sign = Number((r128 >> _63n) & _1n);
1153
1164
  let f = r1 < gauss_1024_12289[0] ? 1 : 0;
1154
1165
  let v = 0;
1155
1166
  for (let k = 1; k < gauss_1024_12289.length; k++) {
@@ -1186,7 +1197,7 @@ function genFalcon(opts) {
1186
1197
  const n = 1 << logn;
1187
1198
  const d = new Array(n >> 1);
1188
1199
  for (let k = 0; k < n; k += 2) {
1189
- let s = 0n;
1200
+ let s = _0n;
1190
1201
  for (let i = 0; i <= k; i += 2)
1191
1202
  s += a[i] * a[k - i];
1192
1203
  for (let i = k + 2; i < n; i += 2)
@@ -1194,7 +1205,7 @@ function genFalcon(opts) {
1194
1205
  d[k >>> 1] = s;
1195
1206
  }
1196
1207
  for (let k = 0; k < n; k += 2) {
1197
- let s = 0n;
1208
+ let s = _0n;
1198
1209
  for (let i = 1; i < k; i += 2)
1199
1210
  s += a[i] * a[k - i];
1200
1211
  for (let i = k + 1; i < n; i += 2)
@@ -1206,7 +1217,7 @@ function genFalcon(opts) {
1206
1217
  mulConjD(logn, d, a, b) {
1207
1218
  const n = 1 << logn;
1208
1219
  for (let k = 0; k < n; k++) {
1209
- let s = 0n;
1220
+ let s = _0n;
1210
1221
  for (let i = 0; i <= k; i += 2)
1211
1222
  s += b[i >>> 1] * a[k - i];
1212
1223
  for (let i = k + 2 - (k & 1); i < n; i += 2)
@@ -1221,7 +1232,7 @@ function genFalcon(opts) {
1221
1232
  subMul(logn, a, b, c, e) {
1222
1233
  const n = 1 << logn;
1223
1234
  for (let k = 0; k < n; k++) {
1224
- let s = 0n;
1235
+ let s = _0n;
1225
1236
  for (let i = 0; i <= k; i++)
1226
1237
  s += b[i] * c[k - i];
1227
1238
  for (let i = k + 1; i < n; i++)
@@ -1265,7 +1276,7 @@ function genFalcon(opts) {
1265
1276
  const Gx = new Float64Array(n);
1266
1277
  const k = new Array(n);
1267
1278
  while (true) {
1268
- let scaleFG = 31n * (FGlen - 10n);
1279
+ let scaleFG = _31n * (FGlen - _10n);
1269
1280
  for (let i = 0; i < n; i++) {
1270
1281
  Fx[i] = Number(F[i] >> scaleFG);
1271
1282
  Gx[i] = Number(G[i] >> scaleFG);
@@ -1284,16 +1295,16 @@ function genFalcon(opts) {
1284
1295
  }
1285
1296
  F = this.subMul(logn, F, f, k, scaleK); // 3: F ← F - kf
1286
1297
  G = this.subMul(logn, G, g, k, scaleK); // 4: G ← G - kg
1287
- const maxfgNew = scaleK + BigInt(Math.round(fgMaxBits)) + 10n;
1298
+ const maxfgNew = scaleK + BigInt(Math.round(fgMaxBits)) + _10n;
1288
1299
  if (maxfgNew < maxFGBits)
1289
1300
  maxFGBits = maxfgNew;
1290
- if (FGlen > 1n && FGlen * 31n >= maxFGBits + 31n)
1301
+ if (FGlen > _1n && FGlen * _31n >= maxFGBits + _31n)
1291
1302
  FGlen--;
1292
- if (scaleK <= 0n)
1303
+ if (scaleK <= _0n)
1293
1304
  break;
1294
- scaleK -= 25n;
1295
- if (scaleK < 0n)
1296
- scaleK = 0n;
1305
+ scaleK -= _25n;
1306
+ if (scaleK < _0n)
1307
+ scaleK = _0n;
1297
1308
  }
1298
1309
  return true;
1299
1310
  }
@@ -1321,11 +1332,11 @@ function genFalcon(opts) {
1321
1332
  const xf = f[0];
1322
1333
  const xg = g[0];
1323
1334
  // We can rely on 'invert' to throw if they are not coprime.
1324
- if (xf <= 0n || xg <= 0n)
1335
+ if (xf <= _0n || xg <= _0n)
1325
1336
  return false;
1326
1337
  try {
1327
1338
  const u1 = invert(xf, xg); // if gcd(f, g) ≠ 1 then
1328
- const v1 = (1n - u1 * xf) / xg;
1339
+ const v1 = (_1n - u1 * xf) / xg;
1329
1340
  F[0] = -v1 * QBig; // 5: (F, G) ← (vq, uq)
1330
1341
  G[0] = u1 * QBig;
1331
1342
  return true;
@@ -1607,7 +1618,14 @@ function genFalcon(opts) {
1607
1618
  return headerCoder(0x30 + logn, splitCoder('falcon.signature', NONCELEN, sigLen, msg.length)).encode([nonce, pad(sigLen).encode(s2), msg]);
1608
1619
  },
1609
1620
  decode(data) {
1621
+ // Keep API misuse on the coder's TypeError path before reading the container length.
1622
+ abytes(data, undefined, 'signature');
1623
+ // The compressed-signature field is fixed-width here; only the message is variable. A
1624
+ // container shorter than the fixed part would make the s2 field borrow bytes from nowhere
1625
+ // and let a truncated encoding open to the same message.
1610
1626
  const msgLen = data.length - NONCELEN - sigLen - 1;
1627
+ if (msgLen < 0)
1628
+ throw new Error('signature coder: wrong length');
1611
1629
  const [nonce, s2, msg] = headerCoder(0x30 + logn, splitCoder('falcon.signature', NONCELEN, sigLen, msgLen)).decode(data);
1612
1630
  return { nonce, s2: decodeSig(s2), msg };
1613
1631
  },
@@ -1622,7 +1640,13 @@ function genFalcon(opts) {
1622
1640
  return headerCoder(0x30 + logn, splitCoder('falcon.detachedSignature', NONCELEN, getSigLen(s2))).encode([nonce, opts.padded ? pad(sigLen).encode(s2) : s2]);
1623
1641
  },
1624
1642
  decode(data) {
1625
- const [nonce, raw] = headerCoder(0x30 + logn, splitCoder('falcon.detachedSignature', NONCELEN, data.length - NONCELEN - 1)).decode(data);
1643
+ // Padded detached signatures are fixed-length (`lengths.signature`), so the payload width
1644
+ // must come from the parameter set, not from the input: deriving it would accept appended
1645
+ // zero bytes and truncated padding as extra valid encodings of the same signature.
1646
+ // Unpadded signatures are variable-length; decodeUnpaddedSig() enforces the exact canonical
1647
+ // bitlength of whatever remains.
1648
+ const payloadLen = opts.padded ? sigLen : data.length - NONCELEN - 1;
1649
+ const [nonce, raw] = headerCoder(0x30 + logn, splitCoder('falcon.detachedSignature', NONCELEN, payloadLen)).decode(data);
1626
1650
  const s2 = decodeSig(raw);
1627
1651
  return { nonce, s2 };
1628
1652
  },
@@ -1710,7 +1734,7 @@ function genFalcon(opts) {
1710
1734
  shakeBuf;
1711
1735
  ctrView;
1712
1736
  // ChaCha
1713
- ctr = 0n;
1737
+ ctr = _0n;
1714
1738
  buf;
1715
1739
  buf32;
1716
1740
  pos;
@@ -1760,8 +1784,8 @@ function genFalcon(opts) {
1760
1784
  const out32 = swap32IfBE(this.buf32);
1761
1785
  for (let i = 0; i < 8; i++, this.ctr++) {
1762
1786
  const n = swap32IfBE(this.nonce32.slice()); // [n0, n1, n2, n3]
1763
- n[2] ^= Number(this.ctr & 0xffffffffn);
1764
- n[3] ^= Number(this.ctr >> 32n);
1787
+ n[2] ^= Number(this.ctr & MASK_32n);
1788
+ n[3] ^= Number(this.ctr >> _32n);
1765
1789
  // chacha20() takes raw nonce bytes; on BE the word-normalized temp must be swapped back.
1766
1790
  swap32IfBE(n.subarray(1));
1767
1791
  chacha20(this.key, u8(n.subarray(1)), EMPTY_CHACHA20_BLOCK, this.curBlock, n[0]);
@@ -2078,7 +2102,8 @@ function genFalcon(opts) {
2078
2102
  // ▷ Remove 1 byte for the header, and 40 bytes for r
2079
2103
  // 11: while (s = ⊥)
2080
2104
  // 12: return sig = (r, s)
2081
- abytes(msg);
2105
+ abytes(msg, undefined, 'msg');
2106
+ abytes(sk, secretKeyCoder.bytesLen, 'secretKey');
2082
2107
  // One RNG stream drives both the public 40-byte nonce and the 48-byte sampler seed, so
2083
2108
  // deterministic rnd hooks make signatures deterministic for fixed secretKey/message inputs.
2084
2109
  const nonce = rnd(40);
@@ -2184,6 +2209,8 @@ function genFalcon(opts) {
2184
2209
  validateSigOpts(opts);
2185
2210
  if (opts.context !== undefined)
2186
2211
  throw new Error('context is not supported');
2212
+ if (opts.random !== undefined && typeof opts.random !== 'function')
2213
+ throw new TypeError('"opts.random" expected function, got type=' + typeof opts.random);
2187
2214
  if (opts.random !== undefined)
2188
2215
  return opts.random;
2189
2216
  if (opts.extraEntropy === undefined)
@@ -2223,6 +2250,7 @@ function genFalcon(opts) {
2223
2250
  return { publicKey: pk, secretKey: sk };
2224
2251
  };
2225
2252
  const getPublicKey = (sk) => {
2253
+ abytes(sk, secretKeyCoder.bytesLen, 'secretKey');
2226
2254
  const [f, g, F] = secretKeyCoder.decode(sk);
2227
2255
  try {
2228
2256
  const h = computePublic(f, g);
@@ -2245,9 +2273,10 @@ function genFalcon(opts) {
2245
2273
  */
2246
2274
  const verify = (sig, msg, pk, verOpts = {}) => {
2247
2275
  checkVerOpts(verOpts);
2248
- abytes(sig);
2249
- abytes(msg);
2250
- abytes(pk);
2276
+ abytes(sig, undefined, 'signature');
2277
+ abytes(msg, undefined, 'msg');
2278
+ // Length/canonical public-key failures are decoded below and return false; only type is fatal.
2279
+ abytes(pk, undefined, 'publicKey');
2251
2280
  try {
2252
2281
  const { s2, nonce } = SignatureCoderDetached(logn).decode(sig);
2253
2282
  return verifyRaw(pk, s2, nonce, msg);
@@ -2382,4 +2411,3 @@ export const __tests = /* @__PURE__ */ (() => Object.freeze({
2382
2411
  falcon1024: falcon1024.__test,
2383
2412
  falcon1024padded: falcon1024padded.__test,
2384
2413
  }))();
2385
- //# sourceMappingURL=falcon.js.map
package/hybrid.d.ts CHANGED
@@ -190,7 +190,11 @@ expandSeed: TArg<ExpandSeed>, combiner: TArg<Combiner>, ...kems: TArg<KEM[]>): T
190
190
  * import { combineSigners, expandSeedXof } from '@noble/post-quantum/hybrid.js';
191
191
  * import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
192
192
  * const hybrid = combineSigners(32, expandSeedXof(shake256), ml_dsa44, ml_dsa44);
193
- * const { publicKey } = hybrid.keygen();
193
+ * const seed = new Uint8Array(hybrid.lengths.seed!).fill(1);
194
+ * const { secretKey, publicKey } = hybrid.keygen(seed);
195
+ * const msg = new TextEncoder().encode('hello noble');
196
+ * const sig = hybrid.sign(msg, secretKey);
197
+ * const isValid = hybrid.verify(sig, msg, publicKey);
194
198
  * ```
195
199
  */
196
200
  export declare function combineSigners(realSeedLen: number | undefined, expandSeed: TArg<ExpandSeed>, ...signers: TArg<Signer[]>): TRet<Signer>;
@@ -263,19 +267,4 @@ export declare const ml_kem768_x25519: TRet<KEM>;
263
267
  export declare const ml_kem768_p256: TRet<KEM>;
264
268
  /** P-384 + ML-KEM-1024 hybrid preset. */
265
269
  export declare const ml_kem1024_p384: TRet<KEM>;
266
- /** Legacy alias for `ml_kem768_x25519`. */
267
- export declare const XWing: TRet<KEM>;
268
- /** Legacy alias for `ml_kem768_x25519`. */
269
- export declare const MLKEM768X25519: TRet<KEM>;
270
- /** Legacy alias for `ml_kem768_p256`. */
271
- export declare const MLKEM768P256: TRet<KEM>;
272
- /** Legacy alias for `ml_kem1024_p384`. */
273
- export declare const MLKEM1024P384: TRet<KEM>;
274
- /** Legacy alias for `QSF_ml_kem768_p256`. */
275
- export declare const QSFMLKEM768P256: TRet<KEM>;
276
- /** Legacy alias for `QSF_ml_kem1024_p384`. */
277
- export declare const QSFMLKEM1024P384: TRet<KEM>;
278
- /** Legacy alias for `KitchenSink_ml_kem768_x25519`. */
279
- export declare const KitchenSinkMLKEM768X25519: TRet<KEM>;
280
270
  export {};
281
- //# sourceMappingURL=hybrid.d.ts.map
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
86
  import { abytes, ahash, anumber } 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) {
@@ -149,6 +172,11 @@ export function ecdhKem(curve, allowZeroKey = false) {
149
172
  const kg = ecKeygen(curve, allowZeroKey);
150
173
  if (!curve.getSharedSecret)
151
174
  throw new Error('wrong curve'); // ed25519 doesn't have one!
175
+ // Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
176
+ const decapsulate = (cipherText, secretKey) => {
177
+ const res = curve.getSharedSecret(secretKey, cipherText);
178
+ return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res);
179
+ };
152
180
  return {
153
181
  lengths: { ...kg.lengths, msg: kg.lengths.seed, cipherText: kg.lengths.publicKey },
154
182
  keygen: kg.keygen,
@@ -159,8 +187,8 @@ export function ecdhKem(curve, allowZeroKey = false) {
159
187
  const seed = copyBytes(rand);
160
188
  let ek = undefined;
161
189
  try {
162
- ek = this.keygen(seed).secretKey;
163
- const sharedSecret = this.decapsulate(publicKey, ek);
190
+ ek = kg.keygen(seed).secretKey;
191
+ const sharedSecret = decapsulate(publicKey, ek);
164
192
  const cipherText = curve.getPublicKey(ek);
165
193
  return { sharedSecret, cipherText };
166
194
  }
@@ -172,10 +200,7 @@ export function ecdhKem(curve, allowZeroKey = false) {
172
200
  cleanBytes(ek);
173
201
  }
174
202
  },
175
- decapsulate(cipherText, secretKey) {
176
- const res = curve.getSharedSecret(secretKey, cipherText);
177
- return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res);
178
- },
203
+ decapsulate,
179
204
  };
180
205
  }
181
206
  /**
@@ -303,26 +328,43 @@ expandSeed_, ...ck_) {
303
328
  cleanBytes(secretKey);
304
329
  }
305
330
  }
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);
331
+ // Standalone (not a method) so getPublicKey / destructured usage never depends on `this`.
332
+ const keygen = (seed) => {
333
+ // Detach the root: the exported secretKey must not alias caller-owned seed bytes, so later
334
+ // caller mutation of the seed cannot silently change the secret key (and vice versa).
335
+ const root = seed === undefined ? randomBytes(realSeedLen) : copyBytes(seed);
336
+ let res;
337
+ try {
338
+ const { publicKey: pk, secretKey } = expandDecapsulationKey(root);
315
339
  try {
316
- const publicKey = pkCoder.encode(pk);
317
- return { secretKey: seed, publicKey };
340
+ res = {
341
+ secretKey: root,
342
+ publicKey: pkCoder.encode(pk),
343
+ };
318
344
  }
319
345
  finally {
320
- cleanBytes(pk);
321
- // The exported secretKey is the caller/root seed itself; child secret keys are internal
346
+ // The exported secretKey is the (detached) root seed; child secret keys are internal
322
347
  // expansion outputs that are cleaned whether encoding succeeds or throws.
323
- cleanBytes(secretKey);
348
+ cleanBytes(pk, secretKey);
324
349
  }
350
+ return res;
351
+ }
352
+ finally {
353
+ if (!res)
354
+ cleanBytes(root);
355
+ }
356
+ };
357
+ return {
358
+ info: { lengths: { seed: realSeedLen, publicKey: pkCoder.bytesLen, secretKey: realSeedLen } },
359
+ // Composite secret keys are root seeds, so public-key derivation reruns key expansion from
360
+ // that seed instead of decoding a packed child-secret-key structure.
361
+ getPublicKey: (secretKey) => {
362
+ const keys = keygen(secretKey);
363
+ // keygen detaches its exported root; getPublicKey discards that half of the result.
364
+ cleanBytes(keys.secretKey);
365
+ return keys.publicKey;
325
366
  },
367
+ keygen,
326
368
  expandDecapsulationKey,
327
369
  realSeedLen,
328
370
  };
@@ -356,15 +398,25 @@ expandSeed_, ...ck_) {
356
398
  export function combineKEMS(realSeedLen, // how much bytes expandSeed expects
357
399
  realMsgLen, // how much bytes combiner returns
358
400
  expandSeed, combiner, ...kems) {
401
+ if (realSeedLen !== undefined)
402
+ anumber(realSeedLen, 'realSeedLen');
403
+ if (realMsgLen !== undefined)
404
+ anumber(realMsgLen, 'realMsgLen');
405
+ if (typeof expandSeed !== 'function')
406
+ throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
407
+ if (typeof combiner !== 'function')
408
+ throw new TypeError('"combiner" expected function, got type=' + typeof combiner);
359
409
  const rawCombiner = combiner;
360
410
  const rawKems = kems;
411
+ for (let i = 0; i < rawKems.length; i++)
412
+ validateKEM(rawKems[i], `kems[${i}]`);
361
413
  const keys = combineKeys(realSeedLen, expandSeed, ...rawKems);
362
414
  const ctCoder = splitLengths(rawKems, 'cipherText');
363
415
  const pkCoder = splitLengths(rawKems, 'publicKey');
364
416
  const msgCoder = splitLengths(rawKems, 'msg');
365
417
  if (realMsgLen === undefined)
366
418
  realMsgLen = msgCoder.bytesLen;
367
- anumber(realMsgLen);
419
+ anumber(realMsgLen, 'realMsgLen');
368
420
  const lengths = Object.freeze({
369
421
  ...keys.info.lengths,
370
422
  msg: realMsgLen,
@@ -431,11 +483,21 @@ expandSeed, combiner, ...kems) {
431
483
  * import { combineSigners, expandSeedXof } from '@noble/post-quantum/hybrid.js';
432
484
  * import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
433
485
  * const hybrid = combineSigners(32, expandSeedXof(shake256), ml_dsa44, ml_dsa44);
434
- * const { publicKey } = hybrid.keygen();
486
+ * const seed = new Uint8Array(hybrid.lengths.seed!).fill(1);
487
+ * const { secretKey, publicKey } = hybrid.keygen(seed);
488
+ * const msg = new TextEncoder().encode('hello noble');
489
+ * const sig = hybrid.sign(msg, secretKey);
490
+ * const isValid = hybrid.verify(sig, msg, publicKey);
435
491
  * ```
436
492
  */
437
493
  export function combineSigners(realSeedLen, expandSeed, ...signers) {
494
+ if (realSeedLen !== undefined)
495
+ anumber(realSeedLen, 'realSeedLen');
496
+ if (typeof expandSeed !== 'function')
497
+ throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
438
498
  const rawSigners = signers;
499
+ for (let i = 0; i < rawSigners.length; i++)
500
+ validateSigner(rawSigners[i], `signers[${i}]`);
439
501
  const keys = combineKeys(realSeedLen, expandSeed, ...rawSigners);
440
502
  const sigCoder = splitLengths(rawSigners, 'signature');
441
503
  const pkCoder = splitLengths(rawSigners, 'publicKey');
@@ -464,14 +526,21 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
464
526
  }
465
527
  },
466
528
  /** 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.
529
+ * Wrong-length aggregate signatures return `false` (matching ml-dsa / slh-dsa behavior), as
530
+ * does any failing child verify. Throws on unsupported generic opts or malformed publicKey.
469
531
  */
470
532
  verify: (signature, message, publicKey, opts = {}) => {
471
533
  validateVerOpts(opts);
472
534
  if (opts.context !== undefined)
473
535
  throw new Error('combineSigners does not support context; use the underlying signer directly');
536
+ // Malformed signature *length* is a verification failure, not a thrown type error —
537
+ // consistent with ml-dsa / slh-dsa. Must run before sigCoder.decode, which throws.
538
+ // Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
539
+ abytes(signature, undefined, 'signature');
540
+ // A signature failure must not hide malformed aggregate public-key bytes.
474
541
  const pks = pkCoder.decode(publicKey);
542
+ if (signature.length !== sigCoder.bytesLen)
543
+ return false;
475
544
  const sigs = sigCoder.decode(signature);
476
545
  for (let i = 0; i < rawSigners.length; i++) {
477
546
  if (!rawSigners[i].verify(sigs[i], message, pks[i]))
@@ -506,7 +575,14 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
506
575
  * ```
507
576
  */
508
577
  export function QSF(label, pqc, curveKEM, xof, kdf) {
578
+ astring(label, 'label');
579
+ validateKEM(pqc, 'pqc');
580
+ validateKEM(curveKEM, 'curveKEM');
581
+ if (typeof xof !== 'function' || typeof xof.create !== 'function')
582
+ throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
509
583
  ahash(xof);
584
+ if (typeof kdf !== 'function' || typeof kdf.create !== 'function')
585
+ throw new TypeError('"kdf" expected hash function, got type=' + typeof kdf);
510
586
  ahash(kdf);
511
587
  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
588
  }
@@ -541,7 +617,14 @@ export const QSF_ml_kem1024_p384 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-10
541
617
  * ```
542
618
  */
543
619
  export function createKitchenSink(label, pqc, curveKEM, xof, hash) {
620
+ astring(label, 'label');
621
+ validateKEM(pqc, 'pqc');
622
+ validateKEM(curveKEM, 'curveKEM');
623
+ if (typeof xof !== 'function' || typeof xof.create !== 'function')
624
+ throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
544
625
  ahash(xof);
626
+ if (typeof hash !== 'function' || typeof hash.create !== 'function')
627
+ throw new TypeError('"hash" expected hash function, got type=' + typeof hash);
545
628
  ahash(hash);
546
629
  return combineKEMS(32, 32, expandSeedXof(xof), (pk, ct, ss) => {
547
630
  const preimage = concatBytes(ss[0], ss[1], ct[0], pk[0], ct[1], pk[1], asciiToBytes(label));
@@ -596,6 +679,11 @@ function nistCurveKem(curve, scalarLen, elemLen, nseed) {
596
679
  const publicKey = curve.getPublicKey(secretKey, false);
597
680
  return { secretKey, publicKey };
598
681
  }
682
+ // Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
683
+ const decapsulate = (cipherText, secretKey) => {
684
+ const full = curve.getSharedSecret(secretKey, cipherText);
685
+ return full.subarray(1);
686
+ };
599
687
  return {
600
688
  lengths: {
601
689
  secretKey: scalarLen,
@@ -616,7 +704,7 @@ function nistCurveKem(curve, scalarLen, elemLen, nseed) {
616
704
  let ek = undefined;
617
705
  try {
618
706
  ek = rejectionSampling(rand).secretKey;
619
- const sharedSecret = this.decapsulate(publicKey, ek);
707
+ const sharedSecret = decapsulate(publicKey, ek);
620
708
  const cipherText = curve.getPublicKey(ek, false);
621
709
  return { sharedSecret, cipherText };
622
710
  }
@@ -627,10 +715,7 @@ function nistCurveKem(curve, scalarLen, elemLen, nseed) {
627
715
  cleanBytes(ek);
628
716
  }
629
717
  },
630
- decapsulate(cipherText, secretKey) {
631
- const full = curve.getSharedSecret(secretKey, cipherText);
632
- return full.subarray(1);
633
- },
718
+ decapsulate,
634
719
  };
635
720
  }
636
721
  /**
@@ -650,29 +735,14 @@ function concreteHybridKem(label, mlkem, curve, nseed) {
650
735
  const totalSeedLen = mlkemSeedLen + nseed;
651
736
  return combineKEMS(32, 32, (seed) => {
652
737
  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);
738
+ // One SHAKE256 stream split by the seed coder as mlkemSeed (64) || curveSeed (nseed).
739
+ // Returned directly: the previous concatBytes of two adjacent subarrays produced an
740
+ // identical copy while leaving this original buffer unwiped; expandDecapsulationKey
741
+ // wipes the returned buffer after the child seeds are copied out.
742
+ return shake256(seed, { dkLen: totalSeedLen });
657
743
  }, (pk, ct, ss) => sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))), mlkem, curveKem);
658
744
  }
659
745
  /** P-256 + ML-KEM-768 hybrid preset. */
660
746
  export const ml_kem768_p256 = /* @__PURE__ */ (() => concreteHybridKem('MLKEM768-P256', ml_kem768, p256, 128))();
661
747
  /** P-384 + ML-KEM-1024 hybrid preset. */
662
748
  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
@@ -26,4 +26,3 @@ import {
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