@noble/post-quantum 0.6.0 → 0.6.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/src/ml-kem.ts CHANGED
@@ -33,6 +33,8 @@ import {
33
33
  type KEM,
34
34
  randomBytes,
35
35
  splitCoder,
36
+ type TArg,
37
+ type TRet,
36
38
  vecCoder,
37
39
  } from './utils.ts';
38
40
 
@@ -50,7 +52,7 @@ const crystals = /* @__PURE__ */ genCrystals({
50
52
  Q,
51
53
  F,
52
54
  ROOT_OF_UNITY,
53
- newPoly: (n: number): Uint16Array => new Uint16Array(n),
55
+ newPoly: (n: number): TRet<Uint16Array> => new Uint16Array(n) as TRet<Uint16Array>,
54
56
  brvBits: 7,
55
57
  isKyber: true,
56
58
  });
@@ -82,11 +84,12 @@ export type KEMParam = {
82
84
  * `RBGstrength` is Table 2's required randomness-source strength in bits,
83
85
  * not a generic security label.
84
86
  */
85
- export const PARAMS: Record<string, KEMParam> = /* @__PURE__ */ (() => ({
86
- 512: { N, Q, K: 2, ETA1: 3, ETA2: 2, du: 10, dv: 4, RBGstrength: 128 },
87
- 768: { N, Q, K: 3, ETA1: 2, ETA2: 2, du: 10, dv: 4, RBGstrength: 192 },
88
- 1024:{ N, Q, K: 4, ETA1: 2, ETA2: 2, du: 11, dv: 5, RBGstrength: 256 },
89
- } as const))();
87
+ export const PARAMS: Record<string, KEMParam> = /* @__PURE__ */ (() =>
88
+ Object.freeze({
89
+ 512: Object.freeze({ N, Q, K: 2, ETA1: 3, ETA2: 2, du: 10, dv: 4, RBGstrength: 128 }),
90
+ 768: Object.freeze({ N, Q, K: 3, ETA1: 2, ETA2: 2, du: 10, dv: 4, RBGstrength: 192 }),
91
+ 1024: Object.freeze({ N, Q, K: 4, ETA1: 2, ETA2: 2, du: 11, dv: 5, RBGstrength: 256 }),
92
+ } as const))();
90
93
 
91
94
  // FIPS-203: compress/decompress
92
95
  const compress = (d: number): Coder<number, number> => {
@@ -126,13 +129,17 @@ const byteCoder = (d: number) =>
126
129
  const polyCoder = (d: number) => (d === 12 ? byteCoder(12) : crystals.bitsCoder(d, compress(d)));
127
130
 
128
131
  // Poly is mod Q, so 12 bits
129
- type Poly = Uint16Array<any>;
132
+ type Poly = Uint16Array;
130
133
 
131
- function polyAdd(a: Poly, b: Poly) {
134
+ function polyAdd(a_: TArg<Poly>, b_: TArg<Poly>) {
135
+ const a = a_ as Poly;
136
+ const b = b_ as Poly;
132
137
  // Mutates `a` in place; callers must pass two N=256 polynomials.
133
138
  for (let i = 0; i < N; i++) a[i] = crystals.mod(a[i] + b[i]); // a += b
134
139
  }
135
- function polySub(a: Poly, b: Poly) {
140
+ function polySub(a_: TArg<Poly>, b_: TArg<Poly>) {
141
+ const a = a_ as Poly;
142
+ const b = b_ as Poly;
136
143
  // Mutates `a` in place; callers must pass two N=256 polynomials.
137
144
  for (let i = 0; i < N; i++) a[i] = crystals.mod(a[i] - b[i]); // a -= b
138
145
  }
@@ -147,7 +154,9 @@ function BaseCaseMultiply(a0: number, a1: number, b0: number, b1: number, zeta:
147
154
 
148
155
  // FIPS-203: Computes the product (in the ring Tq) of two NTT representations.
149
156
  // Works in place on `f`; `g` is read-only and both inputs must already be in NTT form.
150
- function MultiplyNTTs(f: Poly, g: Poly): Poly {
157
+ function MultiplyNTTs(f_: TArg<Poly>, g_: TArg<Poly>): TRet<Poly> {
158
+ const f = f_ as Poly;
159
+ const g = g_ as Poly;
151
160
  for (let i = 0; i < N / 2; i++) {
152
161
  let z = crystals.nttZetas[64 + (i >> 1)];
153
162
  if (i & 1) z = -z;
@@ -155,7 +164,7 @@ function MultiplyNTTs(f: Poly, g: Poly): Poly {
155
164
  f[2 * i + 0] = c0;
156
165
  f[2 * i + 1] = c1;
157
166
  }
158
- return f;
167
+ return f as TRet<Poly>;
159
168
  }
160
169
 
161
170
  type PRF = (l: number, key: Uint8Array, nonce: number) => Uint8Array;
@@ -165,14 +174,14 @@ type XofGet = ReturnType<ReturnType<XOF>['get']>;
165
174
  type KyberOpts = KEMParam & {
166
175
  HASH256: CHash;
167
176
  HASH512: CHash;
168
- // KDF: CHash<Keccak, ShakeOpts>;
169
- KDF: any;
177
+ KDF: CHash<any, { dkLen?: number }>;
170
178
  XOF: XOF; // (seed: Uint8Array, len: number, x: number, y: number) => Uint8Array;
171
179
  PRF: PRF;
172
180
  };
173
181
 
174
182
  // Return poly in NTT representation
175
- function SampleNTT(xof: XofGet) {
183
+ function SampleNTT(xof_: TArg<XofGet>): TRet<Poly> {
184
+ const xof = xof_ as XofGet;
176
185
  // The reader must already bind the Algorithm 7 seed||j||i bytes
177
186
  // and return block lengths divisible by 3.
178
187
  const r: Poly = new Uint16Array(N);
@@ -186,13 +195,13 @@ function SampleNTT(xof: XofGet) {
186
195
  if (j < N && d2 < Q) r[j++] = d2;
187
196
  }
188
197
  }
189
- return r;
198
+ return r as TRet<Poly>;
190
199
  }
191
200
 
192
201
  // Sampling from the centered binomial distribution
193
202
  // Returns poly with small coefficients (noise/errors) stored modulo q in ordinary coefficient form.
194
203
  // Current callers only use Table 2 eta values {2,3} and PRF outputs of exactly 64*eta bytes.
195
- const sampleCBDBytes = (buf: Uint8Array, eta: number): Poly => {
204
+ const sampleCBDBytes = (buf: TArg<Uint8Array>, eta: number): TRet<Poly> => {
196
205
  const r: Poly = new Uint16Array(N);
197
206
  // CBD consumes the PRF bitstream in little-endian byte order; normalize the word view on BE,
198
207
  // then swap it back so callers still observe `buf` as read-only.
@@ -217,10 +226,16 @@ const sampleCBDBytes = (buf: Uint8Array, eta: number): Poly => {
217
226
  }
218
227
  swap32IfBE(b32);
219
228
  if (len) throw new Error(`sampleCBD: leftover bits: ${len}`);
220
- return r;
229
+ return r as TRet<Poly>;
221
230
  };
222
231
 
223
- function sampleCBD(PRF: PRF, seed: Uint8Array, nonce: number, eta: number): Poly {
232
+ function sampleCBD(
233
+ PRF_: TArg<PRF>,
234
+ seed: TArg<Uint8Array>,
235
+ nonce: number,
236
+ eta: number
237
+ ): TRet<Poly> {
238
+ const PRF = PRF_ as PRF;
224
239
  return sampleCBDBytes(PRF((eta * N) / 4, seed, nonce), eta);
225
240
  }
226
241
 
@@ -228,7 +243,8 @@ function sampleCBD(PRF: PRF, seed: Uint8Array, nonce: number, eta: number): Poly
228
243
  // Internal ML-KEM subroutine only: exact 32-byte `seed` / `msg` inputs
229
244
  // come from Algorithms 13-15, and the helper mutates decoded temporary
230
245
  // polynomials in place while leaving caller byte arrays unchanged.
231
- const genKPKE = (opts: KyberOpts) => {
246
+ const genKPKE = (opts_: TArg<KyberOpts>) => {
247
+ const opts = opts_ as KyberOpts;
232
248
  const { K, PRF, XOF, HASH512, ETA1, ETA2, du, dv } = opts;
233
249
  const poly1 = polyCoder(1);
234
250
  const polyV = polyCoder(dv);
@@ -244,7 +260,7 @@ const genKPKE = (opts: KyberOpts) => {
244
260
  publicKey: publicCoder.bytesLen,
245
261
  cipherText: cipherCoder.bytesLen,
246
262
  },
247
- keygen: (seed: Uint8Array) => {
263
+ keygen: (seed: TArg<Uint8Array>) => {
248
264
  abytes(seed, 32, 'seed');
249
265
  const seedDst = new Uint8Array(33);
250
266
  seedDst.set(seed);
@@ -275,7 +291,11 @@ const genKPKE = (opts: KyberOpts) => {
275
291
  cleanBytes(rho, sigma, sHat, tHat, seedDst, seedHash);
276
292
  return res;
277
293
  },
278
- encrypt: (publicKey: Uint8Array, msg: Uint8Array, seed: Uint8Array) => {
294
+ encrypt: (
295
+ publicKey: TArg<Uint8Array>,
296
+ msg: TArg<Uint8Array>,
297
+ seed: TArg<Uint8Array>
298
+ ): TRet<Uint8Array> => {
279
299
  const [tHat, rho] = publicCoder.decode(publicKey);
280
300
  const rHat = [];
281
301
  for (let i = 0; i < K; i++) rHat.push(crystals.NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
@@ -300,9 +320,9 @@ const genKPKE = (opts: KyberOpts) => {
300
320
  const v = poly1.decode(msg); // encode plaintext m into polynomial v
301
321
  polyAdd(v, e2); // v += e2
302
322
  cleanBytes(tHat, rHat, tmp2, e2);
303
- return cipherCoder.encode([u, v]);
323
+ return cipherCoder.encode([u, v]) as TRet<Uint8Array>;
304
324
  },
305
- decrypt: (cipherText: Uint8Array, privateKey: Uint8Array) => {
325
+ decrypt: (cipherText: TArg<Uint8Array>, privateKey: TArg<Uint8Array>): TRet<Uint8Array> => {
306
326
  const [u, v] = cipherCoder.decode(cipherText);
307
327
  const sk = secretCoder.decode(privateKey); // s ← ByteDecode_12(dkPKE)
308
328
  const tmp = new Uint16Array(N);
@@ -310,7 +330,7 @@ const genKPKE = (opts: KyberOpts) => {
310
330
  for (let i = 0; i < K; i++) polyAdd(tmp, MultiplyNTTs(sk[i], crystals.NTT.encode(u[i])));
311
331
  polySub(v, crystals.NTT.decode(tmp)); // w = v' - tmp
312
332
  cleanBytes(tmp, sk, u);
313
- return poly1.encode(v);
333
+ return poly1.encode(v) as TRet<Uint8Array>;
314
334
  },
315
335
  };
316
336
  };
@@ -324,36 +344,41 @@ const genKPKE = (opts: KyberOpts) => {
324
344
  * mismatch, and zeroizing the non-returned shared-secret candidate; JS/JIT still provides no
325
345
  * constant-time guarantees for that path.
326
346
  */
327
- function createKyber(opts: KyberOpts) {
328
- const KPKE = genKPKE(opts);
329
- const { HASH256, HASH512, KDF } = opts;
347
+ function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
348
+ const rawOpts = opts as KyberOpts;
349
+ const KPKE = genKPKE(rawOpts);
350
+ const { HASH256, HASH512, KDF } = rawOpts;
330
351
  const { secretCoder: KPKESecretCoder, lengths } = KPKE;
331
352
  const secretCoder = splitCoder('secretKey', lengths.secretKey, lengths.publicKey, 32, 32);
332
353
  const msgLen = 32;
333
354
  const seedLen = 64;
334
- return {
335
- info: { type: 'ml-kem' },
336
- lengths: {
337
- ...lengths,
338
- seed: 64,
339
- msg: msgLen,
340
- msgRand: msgLen,
341
- secretKey: secretCoder.bytesLen,
342
- },
343
- keygen: (seed = randomBytes(seedLen)) => {
355
+ const kemLengths = Object.freeze({
356
+ ...lengths,
357
+ seed: 64,
358
+ msg: msgLen,
359
+ msgRand: msgLen,
360
+ secretKey: secretCoder.bytesLen,
361
+ });
362
+ return Object.freeze({
363
+ info: Object.freeze({ type: 'ml-kem' }),
364
+ lengths: kemLengths,
365
+ keygen: (seed: TArg<Uint8Array> = randomBytes(seedLen)) => {
344
366
  abytes(seed, seedLen, 'seed');
345
367
  const { publicKey, secretKey: sk } = KPKE.keygen(seed.subarray(0, 32));
346
368
  const publicKeyHash = HASH256(publicKey);
347
369
  // (dkPKE||ek||H(ek)||z)
348
370
  const secretKey = secretCoder.encode([sk, publicKey, publicKeyHash, seed.subarray(32)]);
349
371
  cleanBytes(sk, publicKeyHash);
350
- return { publicKey, secretKey };
372
+ return {
373
+ publicKey: publicKey as TRet<Uint8Array>,
374
+ secretKey: secretKey as TRet<Uint8Array>,
375
+ };
351
376
  },
352
- getPublicKey: (secretKey: Uint8Array) => {
377
+ getPublicKey: (secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
353
378
  const [_sk, publicKey, _publicKeyHash, _z] = secretCoder.decode(secretKey);
354
- return Uint8Array.from(publicKey);
379
+ return Uint8Array.from(publicKey) as TRet<Uint8Array>;
355
380
  },
356
- encapsulate: (publicKey: Uint8Array, msg = randomBytes(msgLen)) => {
381
+ encapsulate: (publicKey: TArg<Uint8Array>, msg: TArg<Uint8Array> = randomBytes(msgLen)) => {
357
382
  abytes(publicKey, lengths.publicKey, 'publicKey');
358
383
  abytes(msg, msgLen, 'message');
359
384
 
@@ -372,9 +397,12 @@ function createKyber(opts: KyberOpts) {
372
397
  const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest();
373
398
  const cipherText = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64));
374
399
  cleanBytes(kr.subarray(32));
375
- return { cipherText, sharedSecret: kr.subarray(0, 32) };
400
+ return {
401
+ cipherText: cipherText as TRet<Uint8Array>,
402
+ sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
403
+ };
376
404
  },
377
- decapsulate: (cipherText: Uint8Array, secretKey: Uint8Array) => {
405
+ decapsulate: (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
378
406
  abytes(secretKey, secretCoder.bytesLen, 'secretKey'); // 768*k + 96
379
407
  abytes(cipherText, lengths.cipherText, 'cipherText'); // 32(du*k + dv)
380
408
  // test ← H(dk[384𝑘 ∶ 768𝑘 + 32])) .
@@ -395,19 +423,19 @@ function createKyber(opts: KyberOpts) {
395
423
  const isValid = equalBytes(cipherText, cipherText2);
396
424
  const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
397
425
  cleanBytes(msg, cipherText2, !isValid ? Khat : Kbar);
398
- return isValid ? Khat : Kbar;
426
+ return (isValid ? Khat : Kbar) as TRet<Uint8Array>;
399
427
  },
400
- };
428
+ });
401
429
  }
402
430
 
403
431
  // FIPS 203's PRF_eta binding: current callers use only 32-byte keys, one-byte nonces,
404
432
  // and dkLen values {128, 192}; out-of-range nonce numbers still wrap modulo 256 here.
405
- function shakePRF(dkLen: number, key: Uint8Array, nonce: number) {
433
+ function shakePRF(dkLen: number, key: TArg<Uint8Array>, nonce: number): TRet<Uint8Array> {
406
434
  return shake256
407
435
  .create({ dkLen })
408
436
  .update(key)
409
437
  .update(new Uint8Array([nonce]))
410
- .digest();
438
+ .digest() as TRet<Uint8Array>;
411
439
  }
412
440
 
413
441
  // Fixed ML-KEM hash/XOF bindings. `KDF` here is the spec's fixed 32-byte `J` call,
@@ -431,52 +459,53 @@ const mk = (params: KEMParam) =>
431
459
  * ML-KEM-512: Table 2 row `k=2, η1=3, η2=2, du=10, dv=4`; Table 3 sizes `800/1632/768/32`.
432
460
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
433
461
  */
434
- export const ml_kem512: KEM = /* @__PURE__ */ (() => mk(PARAMS[512]))();
462
+ export const ml_kem512: TRet<KEM> = /* @__PURE__ */ (() => mk(PARAMS[512]))();
435
463
  /**
436
464
  * ML-KEM-768: Table 2 row `k=3, η1=2, η2=2, du=10, dv=4`; Table 3 sizes `1184/2400/1088/32`.
437
465
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
438
466
  */
439
- export const ml_kem768: KEM = /* @__PURE__ */ (() => mk(PARAMS[768]))();
467
+ export const ml_kem768: TRet<KEM> = /* @__PURE__ */ (() => mk(PARAMS[768]))();
440
468
  /**
441
469
  * ML-KEM-1024: Table 2 row `k=4, η1=2, η2=2, du=11, dv=5`; Table 3 sizes `1568/3168/1568/32`.
442
470
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
443
471
  */
444
- export const ml_kem1024: KEM = /* @__PURE__ */ (() => mk(PARAMS[1024]))();
472
+ export const ml_kem1024: TRet<KEM> = /* @__PURE__ */ (() => mk(PARAMS[1024]))();
445
473
 
446
474
  // NOTE: for tests only, don't use. This keeps the exact internal ML-KEM math surfaces available
447
475
  // without re-implementing them in separate test code.
448
- export const __tests: any = /* @__PURE__ */ (() => ({
449
- Compress_d: (x: number, d: number) => {
450
- if (d < 1 || d > 11) throw new Error(`Compress_d: expected d in [1..11], got ${d}`);
451
- return compress(d).encode(x) & getMask(d);
452
- },
453
- Decompress_d: (y: number, d: number) => {
454
- if (d < 1 || d > 11) throw new Error(`Decompress_d: expected d in [1..11], got ${d}`);
455
- return compress(d).decode(y);
456
- },
457
- ByteEncode_d: (F: Uint16Array, d: number) => {
458
- if (d < 1 || d > 12) throw new Error(`ByteEncode_d: expected d in [1..12], got ${d}`);
459
- return byteCoder(d).encode(F);
460
- },
461
- ByteDecode_d: (B: Uint8Array, d: number) => {
462
- if (d < 1 || d > 12) throw new Error(`ByteDecode_d: expected d in [1..12], got ${d}`);
463
- return byteCoder(d).decode(B);
464
- },
465
- NTT: (f: Uint16Array) => crystals.NTT.encode(Uint16Array.from(f)),
466
- NTT_inv: (fHat: Uint16Array) => crystals.NTT.decode(Uint16Array.from(fHat)),
467
- MultiplyNTTs: (fHat: Uint16Array, gHat: Uint16Array) =>
468
- MultiplyNTTs(Uint16Array.from(fHat), Uint16Array.from(gHat)),
469
- SamplePolyCBD: (B: Uint8Array, eta: number) => {
470
- abytes(B, 64 * eta, 'B');
471
- return sampleCBDBytes(B, eta);
472
- },
473
- SampleNTT: (B: Uint8Array) => {
474
- abytes(B, 34, 'B');
475
- const xof = XOF128(B.subarray(0, 32));
476
- try {
477
- return SampleNTT(xof.get(B[32], B[33]));
478
- } finally {
479
- xof.clean();
480
- }
481
- },
482
- }))();
476
+ export const __tests: any = /* @__PURE__ */ (() =>
477
+ Object.freeze({
478
+ Compress_d: (x: number, d: number) => {
479
+ if (d < 1 || d > 11) throw new Error(`Compress_d: expected d in [1..11], got ${d}`);
480
+ return compress(d).encode(x) & getMask(d);
481
+ },
482
+ Decompress_d: (y: number, d: number) => {
483
+ if (d < 1 || d > 11) throw new Error(`Decompress_d: expected d in [1..11], got ${d}`);
484
+ return compress(d).decode(y);
485
+ },
486
+ ByteEncode_d: (F: TArg<Uint16Array>, d: number) => {
487
+ if (d < 1 || d > 12) throw new Error(`ByteEncode_d: expected d in [1..12], got ${d}`);
488
+ return byteCoder(d).encode(F as TRet<Uint16Array>);
489
+ },
490
+ ByteDecode_d: (B: TArg<Uint8Array>, d: number) => {
491
+ if (d < 1 || d > 12) throw new Error(`ByteDecode_d: expected d in [1..12], got ${d}`);
492
+ return byteCoder(d).decode(B);
493
+ },
494
+ NTT: (f: TArg<Uint16Array>) => crystals.NTT.encode(Uint16Array.from(f)),
495
+ NTT_inv: (fHat: TArg<Uint16Array>) => crystals.NTT.decode(Uint16Array.from(fHat)),
496
+ MultiplyNTTs: (fHat: TArg<Uint16Array>, gHat: TArg<Uint16Array>) =>
497
+ MultiplyNTTs(Uint16Array.from(fHat), Uint16Array.from(gHat)),
498
+ SamplePolyCBD: (B: TArg<Uint8Array>, eta: number) => {
499
+ abytes(B, 64 * eta, 'B');
500
+ return sampleCBDBytes(B, eta);
501
+ },
502
+ SampleNTT: (B: TArg<Uint8Array>) => {
503
+ abytes(B, 34, 'B');
504
+ const xof = XOF128(B.subarray(0, 32));
505
+ try {
506
+ return SampleNTT(xof.get(B[32], B[33]));
507
+ } finally {
508
+ xof.clean();
509
+ }
510
+ },
511
+ }))();