@noble/post-quantum 0.5.4 → 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
@@ -21,7 +21,7 @@
21
21
  */
22
22
  /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
23
23
  import { sha3_256, sha3_512, shake256 } from '@noble/hashes/sha3.js';
24
- import { type CHash, u32 } from '@noble/hashes/utils.js';
24
+ import { type CHash, swap32IfBE, u32 } from '@noble/hashes/utils.js';
25
25
  import { genCrystals, type XOF, XOF128 } from './_crystals.ts';
26
26
  import {
27
27
  abytes,
@@ -29,9 +29,12 @@ import {
29
29
  type Coder,
30
30
  copyBytes,
31
31
  equalBytes,
32
+ getMask,
32
33
  type KEM,
33
34
  randomBytes,
34
35
  splitCoder,
36
+ type TArg,
37
+ type TRet,
35
38
  vecCoder,
36
39
  } from './utils.ts';
37
40
 
@@ -41,84 +44,127 @@ const N = 256; // Kyber (not FIPS-203) supports different lengths, but all std m
41
44
  const Q = 3329; // 13*(2**8)+1, modulo prime
42
45
  const F = 3303; // 3303 ≡ 128**(−1) mod q (FIPS-203)
43
46
  const ROOT_OF_UNITY = 17; // ζ = 17 ∈ Zq is a primitive 256-th root of unity modulo Q. ζ**128 ≡−1
44
- const { mod, nttZetas, NTT, bitsCoder } = genCrystals({
47
+ // treeshake: keep genCrystals behind the object so PARAMS-only bundles can drop it entirely.
48
+ // Shared CRYSTALS helper in the ML-KEM branch: Kyber mode, 7-bit bit-reversal,
49
+ // and Uint16Array polys because current coefficients stay reduced modulo q.
50
+ const crystals = /* @__PURE__ */ genCrystals({
45
51
  N,
46
52
  Q,
47
53
  F,
48
54
  ROOT_OF_UNITY,
49
- newPoly: (n: number): Uint16Array => new Uint16Array(n),
55
+ newPoly: (n: number): TRet<Uint16Array> => new Uint16Array(n) as TRet<Uint16Array>,
50
56
  brvBits: 7,
51
57
  isKyber: true,
52
58
  });
53
59
 
54
60
  /** FIPS 203: 7. Parameter Sets */
61
+ /** Public ML-KEM parameter-set description. */
55
62
  export type KEMParam = {
63
+ /** Polynomial size. */
56
64
  N: number;
65
+ /** Module rank. */
57
66
  K: number;
67
+ /** Prime modulus. */
58
68
  Q: number;
69
+ /** CBD parameter used for secret-key noise. */
59
70
  ETA1: number;
71
+ /** CBD parameter used for error noise. */
60
72
  ETA2: number;
73
+ /** Compression width for the `u` vector. */
61
74
  du: number;
75
+ /** Compression width for the `v` polynomial. */
62
76
  dv: number;
77
+ /** Required strength of the randomness source in bits. */
63
78
  RBGstrength: number;
64
79
  };
65
80
  /** Internal params of ML-KEM versions */
66
81
  // prettier-ignore
67
- export const PARAMS: Record<string, KEMParam> = {
68
- 512: { N, Q, K: 2, ETA1: 3, ETA2: 2, du: 10, dv: 4, RBGstrength: 128 },
69
- 768: { N, Q, K: 3, ETA1: 2, ETA2: 2, du: 10, dv: 4, RBGstrength: 192 },
70
- 1024:{ N, Q, K: 4, ETA1: 2, ETA2: 2, du: 11, dv: 5, RBGstrength: 256 },
71
- } as const;
82
+ /** Built-in ML-KEM parameter presets keyed by the public export names
83
+ * `ml_kem512` / `ml_kem768` / `ml_kem1024`.
84
+ * `RBGstrength` is Table 2's required randomness-source strength in bits,
85
+ * not a generic security label.
86
+ */
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))();
72
93
 
73
94
  // FIPS-203: compress/decompress
74
95
  const compress = (d: number): Coder<number, number> => {
75
- // Special case, no need to compress, pass as is, but strip high bytes on compression
76
- if (d >= 12) return { encode: (i: number) => i, decode: (i: number) => i };
77
- // NOTE: we don't use float arithmetic (forbidden by FIPS-203 and high chance of bugs).
96
+ // d=12 is the ByteEncode12/ByteDecode12 path, not lossy compression.
97
+ // ByteDecode12 interprets each 12-bit word modulo q; without that reduction the public-key
98
+ // modulus check in encapsulate() becomes a no-op for malformed coefficients like 4095.
99
+ if (d >= 12) return { encode: (i: number) => i, decode: (i: number) => (i >= Q ? i - Q : i) };
78
100
  // Comments map to python implementation in RFC (draft-cfrg-schwabe-kyber)
79
101
  // const round = (i: number) => Math.floor(i + 0.5) | 0;
80
102
  const a = 2 ** (d - 1);
81
103
  return {
82
- // const compress = (i: number) => round((2 ** d / Q) * i) % 2 ** d;
104
+ // This only matches standalone Compress_d after bitsCoder masks the result into Z_(2^d).
83
105
  encode: (i: number) => ((i << d) + Q / 2) / Q,
84
106
  // const decompress = (i: number) => round((Q / 2 ** d) * i);
85
107
  decode: (i: number) => (i * Q + a) >>> d,
86
108
  };
87
109
  };
88
110
 
111
+ // Raw ByteEncode_d / ByteDecode_d from FIPS 203 operate on d-bit words directly.
112
+ // That differs from `polyCoder(d)` for d<12, where noble folds packing together with the lossy
113
+ // ciphertext compression step used by u/v. Tests that exercise the spec's raw packing surface need
114
+ // this exact non-lossy variant instead.
115
+ const byteCoder = (d: number) =>
116
+ crystals.bitsCoder(
117
+ d,
118
+ d === 12
119
+ ? { encode: (i: number) => i, decode: (i: number) => (i >= Q ? i - Q : i) }
120
+ : { encode: (i: number) => i, decode: (i: number) => i }
121
+ );
122
+
89
123
  // NOTE: we merge encoding and compress because it is faster, also both require same d param
90
- // Converts between bytes and d-bits compressed representation. Kinda like convertRadix2 from @scure/base
124
+ // d=12 is the ByteEncode12/ByteDecode12 path rather than compression, and caller-side
125
+ // public-key modulus checks route through this helper's decode/encode roundtrip.
126
+ // Converts between bytes and d-bits compressed representation.
127
+ // Kinda like convertRadix2 from @scure/base.
91
128
  // decode(encode(t)) == t, but there is loss of information on encode(decode(t))
92
- const polyCoder = (d: number) => bitsCoder(d, compress(d));
129
+ const polyCoder = (d: number) => (d === 12 ? byteCoder(12) : crystals.bitsCoder(d, compress(d)));
93
130
 
94
131
  // Poly is mod Q, so 12 bits
95
- type Poly = Uint16Array<any>;
132
+ type Poly = Uint16Array;
96
133
 
97
- function polyAdd(a: Poly, b: Poly) {
98
- for (let i = 0; i < N; i++) a[i] = mod(a[i] + b[i]); // a += b
134
+ function polyAdd(a_: TArg<Poly>, b_: TArg<Poly>) {
135
+ const a = a_ as Poly;
136
+ const b = b_ as Poly;
137
+ // Mutates `a` in place; callers must pass two N=256 polynomials.
138
+ for (let i = 0; i < N; i++) a[i] = crystals.mod(a[i] + b[i]); // a += b
99
139
  }
100
- function polySub(a: Poly, b: Poly) {
101
- for (let i = 0; i < N; i++) a[i] = mod(a[i] - b[i]); // a -= b
140
+ function polySub(a_: TArg<Poly>, b_: TArg<Poly>) {
141
+ const a = a_ as Poly;
142
+ const b = b_ as Poly;
143
+ // Mutates `a` in place; callers must pass two N=256 polynomials.
144
+ for (let i = 0; i < N; i++) a[i] = crystals.mod(a[i] - b[i]); // a -= b
102
145
  }
103
146
 
104
147
  // FIPS-203: Computes the product of two degree-one polynomials with respect to a quadratic modulus
105
148
  function BaseCaseMultiply(a0: number, a1: number, b0: number, b1: number, zeta: number) {
106
- const c0 = mod(a1 * b1 * zeta + a0 * b0);
107
- const c1 = mod(a0 * b1 + a1 * b0);
149
+ // `zeta` here is Algorithm 11's γ = ζ^(2BitRev_7(i)+1).
150
+ const c0 = crystals.mod(a1 * b1 * zeta + a0 * b0);
151
+ const c1 = crystals.mod(a0 * b1 + a1 * b0);
108
152
  return { c0, c1 };
109
153
  }
110
154
 
111
- // FIPS-203: Computes the product (in the ring Tq) of two NTT representations. NOTE: works inplace for f
112
- // NOTE: since multiply defined only for NTT representation, we need to convert to NTT, multiply and convert back
113
- function MultiplyNTTs(f: Poly, g: Poly): Poly {
155
+ // FIPS-203: Computes the product (in the ring Tq) of two NTT representations.
156
+ // Works in place on `f`; `g` is read-only and both inputs must already be in NTT form.
157
+ function MultiplyNTTs(f_: TArg<Poly>, g_: TArg<Poly>): TRet<Poly> {
158
+ const f = f_ as Poly;
159
+ const g = g_ as Poly;
114
160
  for (let i = 0; i < N / 2; i++) {
115
- let z = nttZetas[64 + (i >> 1)];
161
+ let z = crystals.nttZetas[64 + (i >> 1)];
116
162
  if (i & 1) z = -z;
117
163
  const { c0, c1 } = BaseCaseMultiply(f[2 * i + 0], f[2 * i + 1], g[2 * i + 0], g[2 * i + 1], z);
118
164
  f[2 * i + 0] = c0;
119
165
  f[2 * i + 1] = c1;
120
166
  }
121
- return f;
167
+ return f as TRet<Poly>;
122
168
  }
123
169
 
124
170
  type PRF = (l: number, key: Uint8Array, nonce: number) => Uint8Array;
@@ -128,14 +174,16 @@ type XofGet = ReturnType<ReturnType<XOF>['get']>;
128
174
  type KyberOpts = KEMParam & {
129
175
  HASH256: CHash;
130
176
  HASH512: CHash;
131
- // KDF: CHash<Keccak, ShakeOpts>;
132
- KDF: any;
177
+ KDF: CHash<any, { dkLen?: number }>;
133
178
  XOF: XOF; // (seed: Uint8Array, len: number, x: number, y: number) => Uint8Array;
134
179
  PRF: PRF;
135
180
  };
136
181
 
137
182
  // Return poly in NTT representation
138
- function SampleNTT(xof: XofGet) {
183
+ function SampleNTT(xof_: TArg<XofGet>): TRet<Poly> {
184
+ const xof = xof_ as XofGet;
185
+ // The reader must already bind the Algorithm 7 seed||j||i bytes
186
+ // and return block lengths divisible by 3.
139
187
  const r: Poly = new Uint16Array(N);
140
188
  for (let j = 0; j < N; ) {
141
189
  const b = xof();
@@ -147,15 +195,18 @@ function SampleNTT(xof: XofGet) {
147
195
  if (j < N && d2 < Q) r[j++] = d2;
148
196
  }
149
197
  }
150
- return r;
198
+ return r as TRet<Poly>;
151
199
  }
152
200
 
153
201
  // Sampling from the centered binomial distribution
154
- // Returns poly with small coefficients (noise/errors)
155
- function sampleCBD(PRF: PRF, seed: Uint8Array, nonce: number, eta: number): Poly {
156
- const buf = PRF((eta * N) / 4, seed, nonce);
202
+ // Returns poly with small coefficients (noise/errors) stored modulo q in ordinary coefficient form.
203
+ // Current callers only use Table 2 eta values {2,3} and PRF outputs of exactly 64*eta bytes.
204
+ const sampleCBDBytes = (buf: TArg<Uint8Array>, eta: number): TRet<Poly> => {
157
205
  const r: Poly = new Uint16Array(N);
206
+ // CBD consumes the PRF bitstream in little-endian byte order; normalize the word view on BE,
207
+ // then swap it back so callers still observe `buf` as read-only.
158
208
  const b32 = u32(buf);
209
+ swap32IfBE(b32);
159
210
  let len = 0;
160
211
  for (let i = 0, p = 0, bb = 0, t0 = 0; i < b32.length; i++) {
161
212
  let b = b32[i];
@@ -167,19 +218,33 @@ function sampleCBD(PRF: PRF, seed: Uint8Array, nonce: number, eta: number): Poly
167
218
  t0 = bb;
168
219
  bb = 0;
169
220
  } else if (len === 2 * eta) {
170
- r[p++] = mod(t0 - bb);
221
+ r[p++] = crystals.mod(t0 - bb);
171
222
  bb = 0;
172
223
  len = 0;
173
224
  }
174
225
  }
175
226
  }
227
+ swap32IfBE(b32);
176
228
  if (len) throw new Error(`sampleCBD: leftover bits: ${len}`);
177
- return r;
229
+ return r as TRet<Poly>;
230
+ };
231
+
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;
239
+ return sampleCBDBytes(PRF((eta * N) / 4, seed, nonce), eta);
178
240
  }
179
241
 
180
242
  // K-PKE
181
- // As per FIPS-203, it doesn't perform any input validation and can't be used in standalone fashion.
182
- const genKPKE = (opts: KyberOpts) => {
243
+ // Internal ML-KEM subroutine only: exact 32-byte `seed` / `msg` inputs
244
+ // come from Algorithms 13-15, and the helper mutates decoded temporary
245
+ // polynomials in place while leaving caller byte arrays unchanged.
246
+ const genKPKE = (opts_: TArg<KyberOpts>) => {
247
+ const opts = opts_ as KyberOpts;
183
248
  const { K, PRF, XOF, HASH512, ETA1, ETA2, du, dv } = opts;
184
249
  const poly1 = polyCoder(1);
185
250
  const polyV = polyCoder(dv);
@@ -195,22 +260,25 @@ const genKPKE = (opts: KyberOpts) => {
195
260
  publicKey: publicCoder.bytesLen,
196
261
  cipherText: cipherCoder.bytesLen,
197
262
  },
198
- keygen: (seed: Uint8Array) => {
263
+ keygen: (seed: TArg<Uint8Array>) => {
199
264
  abytes(seed, 32, 'seed');
200
265
  const seedDst = new Uint8Array(33);
201
266
  seedDst.set(seed);
267
+ // FIPS 203 Algorithm 13 appends the parameter-set byte `k`
268
+ // before `G(d || k)`, so expanding the same 32-byte seed
269
+ // under a different ML-KEM parameter set yields unrelated keys.
202
270
  seedDst[32] = K;
203
271
  const seedHash = HASH512(seedDst);
204
272
 
205
273
  const [rho, sigma] = seedCoder.decode(seedHash);
206
274
  const sHat: Poly[] = [];
207
275
  const tHat: Poly[] = [];
208
- for (let i = 0; i < K; i++) sHat.push(NTT.encode(sampleCBD(PRF, sigma, i, ETA1)));
276
+ for (let i = 0; i < K; i++) sHat.push(crystals.NTT.encode(sampleCBD(PRF, sigma, i, ETA1)));
209
277
  const x = XOF(rho);
210
278
  for (let i = 0; i < K; i++) {
211
- const e = NTT.encode(sampleCBD(PRF, sigma, K + i, ETA1));
279
+ const e = crystals.NTT.encode(sampleCBD(PRF, sigma, K + i, ETA1));
212
280
  for (let j = 0; j < K; j++) {
213
- const aji = SampleNTT(x.get(j, i)); // A[j][i], inplace
281
+ const aji = SampleNTT(x.get(j, i)); // A[i][j], inplace
214
282
  polyAdd(e, MultiplyNTTs(aji, sHat[j]));
215
283
  }
216
284
  tHat.push(e); // t ← A ◦ s + e
@@ -223,10 +291,14 @@ const genKPKE = (opts: KyberOpts) => {
223
291
  cleanBytes(rho, sigma, sHat, tHat, seedDst, seedHash);
224
292
  return res;
225
293
  },
226
- encrypt: (publicKey: Uint8Array, msg: Uint8Array, seed: Uint8Array) => {
294
+ encrypt: (
295
+ publicKey: TArg<Uint8Array>,
296
+ msg: TArg<Uint8Array>,
297
+ seed: TArg<Uint8Array>
298
+ ): TRet<Uint8Array> => {
227
299
  const [tHat, rho] = publicCoder.decode(publicKey);
228
300
  const rHat = [];
229
- for (let i = 0; i < K; i++) rHat.push(NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
301
+ for (let i = 0; i < K; i++) rHat.push(crystals.NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
230
302
  const x = XOF(rho);
231
303
  const tmp2 = new Uint16Array(N);
232
304
  const u = [];
@@ -234,70 +306,86 @@ const genKPKE = (opts: KyberOpts) => {
234
306
  const e1 = sampleCBD(PRF, seed, K + i, ETA2);
235
307
  const tmp = new Uint16Array(N);
236
308
  for (let j = 0; j < K; j++) {
237
- const aij = SampleNTT(x.get(i, j)); // A[i][j], inplace
309
+ const aij = SampleNTT(x.get(i, j)); // A[j][i], inplace transpose access
238
310
  polyAdd(tmp, MultiplyNTTs(aij, rHat[j])); // t += aij * rHat[j]
239
311
  }
240
- polyAdd(e1, NTT.decode(tmp)); // e1 += tmp
312
+ polyAdd(e1, crystals.NTT.decode(tmp)); // e1 += tmp
241
313
  u.push(e1);
242
314
  polyAdd(tmp2, MultiplyNTTs(tHat[i], rHat[i])); // t2 += tHat[i] * rHat[i]
243
315
  cleanBytes(tmp);
244
316
  }
245
317
  x.clean();
246
318
  const e2 = sampleCBD(PRF, seed, 2 * K, ETA2);
247
- polyAdd(e2, NTT.decode(tmp2)); // e2 += tmp2
319
+ polyAdd(e2, crystals.NTT.decode(tmp2)); // e2 += tmp2
248
320
  const v = poly1.decode(msg); // encode plaintext m into polynomial v
249
321
  polyAdd(v, e2); // v += e2
250
322
  cleanBytes(tHat, rHat, tmp2, e2);
251
- return cipherCoder.encode([u, v]);
323
+ return cipherCoder.encode([u, v]) as TRet<Uint8Array>;
252
324
  },
253
- decrypt: (cipherText: Uint8Array, privateKey: Uint8Array) => {
325
+ decrypt: (cipherText: TArg<Uint8Array>, privateKey: TArg<Uint8Array>): TRet<Uint8Array> => {
254
326
  const [u, v] = cipherCoder.decode(cipherText);
255
327
  const sk = secretCoder.decode(privateKey); // s ← ByteDecode_12(dkPKE)
256
328
  const tmp = new Uint16Array(N);
257
- for (let i = 0; i < K; i++) polyAdd(tmp, MultiplyNTTs(sk[i], NTT.encode(u[i]))); // tmp += sk[i] * u[i]
258
- polySub(v, NTT.decode(tmp)); // v += tmp
329
+ // tmp += sk[i] * u[i]
330
+ for (let i = 0; i < K; i++) polyAdd(tmp, MultiplyNTTs(sk[i], crystals.NTT.encode(u[i])));
331
+ polySub(v, crystals.NTT.decode(tmp)); // w = v' - tmp
259
332
  cleanBytes(tmp, sk, u);
260
- return poly1.encode(v);
333
+ return poly1.encode(v) as TRet<Uint8Array>;
261
334
  },
262
335
  };
263
336
  };
264
337
 
265
- function createKyber(opts: KyberOpts) {
266
- const KPKE = genKPKE(opts);
267
- const { HASH256, HASH512, KDF } = opts;
338
+ /**
339
+ * Public ML-KEM wrapper over the internal K-PKE subroutine.
340
+ * `keygen(seed)` and `encapsulate(publicKey, msg)` are deterministic/test-oriented hooks that map
341
+ * more directly to Algorithms 16-17 than to the pure no-input / random-internal Algorithms 19-20.
342
+ * decapsulate() tries to follow the Algorithms 18/21 implicit-reject structure as closely as
343
+ * practical here by re-encrypting, comparing ciphertexts, returning `Khat` on match or `Kbar` on
344
+ * mismatch, and zeroizing the non-returned shared-secret candidate; JS/JIT still provides no
345
+ * constant-time guarantees for that path.
346
+ */
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;
268
351
  const { secretCoder: KPKESecretCoder, lengths } = KPKE;
269
352
  const secretCoder = splitCoder('secretKey', lengths.secretKey, lengths.publicKey, 32, 32);
270
353
  const msgLen = 32;
271
354
  const seedLen = 64;
272
- return {
273
- info: { type: 'ml-kem' },
274
- lengths: {
275
- ...lengths,
276
- seed: 64,
277
- msg: msgLen,
278
- msgRand: msgLen,
279
- secretKey: secretCoder.bytesLen,
280
- },
281
- 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)) => {
282
366
  abytes(seed, seedLen, 'seed');
283
367
  const { publicKey, secretKey: sk } = KPKE.keygen(seed.subarray(0, 32));
284
368
  const publicKeyHash = HASH256(publicKey);
285
369
  // (dkPKE||ek||H(ek)||z)
286
370
  const secretKey = secretCoder.encode([sk, publicKey, publicKeyHash, seed.subarray(32)]);
287
371
  cleanBytes(sk, publicKeyHash);
288
- return { publicKey, secretKey };
372
+ return {
373
+ publicKey: publicKey as TRet<Uint8Array>,
374
+ secretKey: secretKey as TRet<Uint8Array>,
375
+ };
289
376
  },
290
- getPublicKey: (secretKey: Uint8Array) => {
377
+ getPublicKey: (secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
291
378
  const [_sk, publicKey, _publicKeyHash, _z] = secretCoder.decode(secretKey);
292
- return Uint8Array.from(publicKey);
379
+ return Uint8Array.from(publicKey) as TRet<Uint8Array>;
293
380
  },
294
- encapsulate: (publicKey: Uint8Array, msg = randomBytes(msgLen)) => {
381
+ encapsulate: (publicKey: TArg<Uint8Array>, msg: TArg<Uint8Array> = randomBytes(msgLen)) => {
295
382
  abytes(publicKey, lengths.publicKey, 'publicKey');
296
383
  abytes(msg, msgLen, 'message');
297
384
 
298
385
  // FIPS-203 includes additional verification check for modulus
299
386
  const eke = publicKey.subarray(0, 384 * opts.K);
300
- const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke))); // Copy because of inplace encoding
387
+ // Copy because of inplace encoding
388
+ const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
301
389
  // (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
302
390
  // If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
303
391
  if (!equalBytes(ek, eke)) {
@@ -305,12 +393,16 @@ function createKyber(opts: KyberOpts) {
305
393
  throw new Error('ML-KEM.encapsulate: wrong publicKey modulus');
306
394
  }
307
395
  cleanBytes(ek);
308
- const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest(); // derive randomness
396
+ // derive randomness
397
+ const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest();
309
398
  const cipherText = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64));
310
399
  cleanBytes(kr.subarray(32));
311
- 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
+ };
312
404
  },
313
- decapsulate: (cipherText: Uint8Array, secretKey: Uint8Array) => {
405
+ decapsulate: (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
314
406
  abytes(secretKey, secretCoder.bytesLen, 'secretKey'); // 768*k + 96
315
407
  abytes(cipherText, lengths.cipherText, 'cipherText'); // 32(du*k + dv)
316
408
  // test ← H(dk[384𝑘 ∶ 768𝑘 + 32])) .
@@ -322,47 +414,98 @@ function createKyber(opts: KyberOpts) {
322
414
  throw new Error('invalid secretKey: hash check failed');
323
415
  const [sk, publicKey, publicKeyHash, z] = secretCoder.decode(secretKey);
324
416
  const msg = KPKE.decrypt(cipherText, sk);
325
- const kr = HASH512.create().update(msg).update(publicKeyHash).digest(); // derive randomness, Khat, rHat = G(mHat || h)
417
+ // derive randomness, Khat, rHat = G(mHat || h)
418
+ const kr = HASH512.create().update(msg).update(publicKeyHash).digest();
326
419
  const Khat = kr.subarray(0, 32);
327
- const cipherText2 = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64)); // re-encrypt using the derived randomness
328
- const isValid = equalBytes(cipherText, cipherText2); // if ciphertexts do not match, “implicitly reject”
420
+ // re-encrypt using the derived randomness
421
+ const cipherText2 = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64));
422
+ // if ciphertexts do not match, “implicitly reject”
423
+ const isValid = equalBytes(cipherText, cipherText2);
329
424
  const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
330
425
  cleanBytes(msg, cipherText2, !isValid ? Khat : Kbar);
331
- return isValid ? Khat : Kbar;
426
+ return (isValid ? Khat : Kbar) as TRet<Uint8Array>;
332
427
  },
333
- };
428
+ });
334
429
  }
335
430
 
336
- function shakePRF(dkLen: number, key: Uint8Array, nonce: number) {
431
+ // FIPS 203's PRF_eta binding: current callers use only 32-byte keys, one-byte nonces,
432
+ // and dkLen values {128, 192}; out-of-range nonce numbers still wrap modulo 256 here.
433
+ function shakePRF(dkLen: number, key: TArg<Uint8Array>, nonce: number): TRet<Uint8Array> {
337
434
  return shake256
338
435
  .create({ dkLen })
339
436
  .update(key)
340
437
  .update(new Uint8Array([nonce]))
341
- .digest();
438
+ .digest() as TRet<Uint8Array>;
342
439
  }
343
440
 
344
- const opts = {
441
+ // Fixed ML-KEM hash/XOF bindings. `KDF` here is the spec's fixed 32-byte `J` call,
442
+ // and swapping any field changes the scheme rather than tuning an internal dependency.
443
+ const opts = /* @__PURE__ */ (() => ({
345
444
  HASH256: sha3_256,
346
445
  HASH512: sha3_512,
347
446
  KDF: shake256,
348
447
  XOF: XOF128,
349
448
  PRF: shakePRF,
350
- };
351
-
352
- /** ML-KEM-512 for 128-bit security level. Not recommended after 2030, as per ASD. */
353
- export const ml_kem512: KEM = /* @__PURE__ */ createKyber({
354
- ...opts,
355
- ...PARAMS[512],
356
- });
449
+ }))();
450
+ // Parameter-set instantiation step for the spec's "ML-KEM-x" names; current correctness relies
451
+ // on the internal PARAMS rows rather than local validation of arbitrary KEMParam objects.
452
+ const mk = (params: KEMParam) =>
453
+ createKyber({
454
+ ...opts,
455
+ ...params,
456
+ });
357
457
 
358
- /** ML-KEM-768, for 192-bit security level. Not recommended after 2030, as per ASD. */
359
- export const ml_kem768: KEM = /* @__PURE__ */ createKyber({
360
- ...opts,
361
- ...PARAMS[768],
362
- });
458
+ /**
459
+ * ML-KEM-512: Table 2 row `k=2, η1=3, η2=2, du=10, dv=4`; Table 3 sizes `800/1632/768/32`.
460
+ * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
461
+ */
462
+ export const ml_kem512: TRet<KEM> = /* @__PURE__ */ (() => mk(PARAMS[512]))();
463
+ /**
464
+ * ML-KEM-768: Table 2 row `k=3, η1=2, η2=2, du=10, dv=4`; Table 3 sizes `1184/2400/1088/32`.
465
+ * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
466
+ */
467
+ export const ml_kem768: TRet<KEM> = /* @__PURE__ */ (() => mk(PARAMS[768]))();
468
+ /**
469
+ * ML-KEM-1024: Table 2 row `k=4, η1=2, η2=2, du=11, dv=5`; Table 3 sizes `1568/3168/1568/32`.
470
+ * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
471
+ */
472
+ export const ml_kem1024: TRet<KEM> = /* @__PURE__ */ (() => mk(PARAMS[1024]))();
363
473
 
364
- /** ML-KEM-1024 for 256-bit security level. OK after 2030, as per ASD. */
365
- export const ml_kem1024: KEM = /* @__PURE__ */ createKyber({
366
- ...opts,
367
- ...PARAMS[1024],
368
- });
474
+ // NOTE: for tests only, don't use. This keeps the exact internal ML-KEM math surfaces available
475
+ // without re-implementing them in separate test code.
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
+ }))();