@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/README.md +191 -104
- package/_crystals.d.ts +8 -3
- package/_crystals.js +38 -10
- package/falcon.d.ts +1 -2
- package/falcon.js +202 -115
- package/hybrid.d.ts +38 -28
- package/hybrid.js +215 -86
- package/index.d.ts +0 -1
- package/index.js +1 -2
- package/ml-dsa.d.ts +31 -5
- package/ml-dsa.js +118 -34
- package/ml-kem.d.ts +45 -4
- package/ml-kem.js +206 -64
- package/package.json +16 -20
- package/slh-dsa.d.ts +23 -3
- package/slh-dsa.js +127 -60
- package/src/_crystals.ts +45 -11
- package/src/falcon.ts +206 -121
- package/src/hybrid.ts +217 -83
- package/src/index.ts +1 -1
- package/src/ml-dsa.ts +140 -43
- package/src/ml-kem.ts +239 -66
- package/src/slh-dsa.ts +149 -69
- package/src/utils.ts +186 -25
- package/src/webcrypto.ts +322 -0
- package/utils.d.ts +54 -4
- package/utils.js +167 -28
- package/webcrypto.d.ts +91 -0
- package/webcrypto.js +213 -0
- package/_crystals.d.ts.map +0 -1
- package/_crystals.js.map +0 -1
- package/falcon.d.ts.map +0 -1
- package/falcon.js.map +0 -1
- package/hybrid.d.ts.map +0 -1
- package/hybrid.js.map +0 -1
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/ml-dsa.d.ts.map +0 -1
- package/ml-dsa.js.map +0 -1
- package/ml-kem.d.ts.map +0 -1
- package/ml-kem.js.map +0 -1
- package/slh-dsa.d.ts.map +0 -1
- package/slh-dsa.js.map +0 -1
- package/utils.d.ts.map +0 -1
- package/utils.js.map +0 -1
package/src/ml-kem.ts
CHANGED
|
@@ -131,23 +131,33 @@ const polyCoder = (d: number) => (d === 12 ? byteCoder(12) : crystals.bitsCoder(
|
|
|
131
131
|
// Poly is mod Q, so 12 bits
|
|
132
132
|
type Poly = Uint16Array;
|
|
133
133
|
|
|
134
|
+
// Coefficients always stay reduced in [0, Q) here (samplers, NTT and coders all reduce),
|
|
135
|
+
// so one conditional correction replaces the generic mod().
|
|
134
136
|
function polyAdd(a_: TArg<Poly>, b_: TArg<Poly>) {
|
|
135
137
|
const a = a_ as Poly;
|
|
136
138
|
const b = b_ as Poly;
|
|
137
139
|
// Mutates `a` in place; callers must pass two N=256 polynomials.
|
|
138
|
-
for (let i = 0; i < N; i++)
|
|
140
|
+
for (let i = 0; i < N; i++) {
|
|
141
|
+
const r = a[i] + b[i]; // a += b
|
|
142
|
+
a[i] = r >= Q ? r - Q : r;
|
|
143
|
+
}
|
|
139
144
|
}
|
|
140
145
|
function polySub(a_: TArg<Poly>, b_: TArg<Poly>) {
|
|
141
146
|
const a = a_ as Poly;
|
|
142
147
|
const b = b_ as Poly;
|
|
143
148
|
// Mutates `a` in place; callers must pass two N=256 polynomials.
|
|
144
|
-
for (let i = 0; i < N; i++)
|
|
149
|
+
for (let i = 0; i < N; i++) {
|
|
150
|
+
const r = a[i] - b[i]; // a -= b
|
|
151
|
+
a[i] = r < 0 ? r + Q : r;
|
|
152
|
+
}
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
// FIPS-203: Computes the product of two degree-one polynomials with respect to a quadratic modulus
|
|
148
156
|
function BaseCaseMultiply(a0: number, a1: number, b0: number, b1: number, zeta: number) {
|
|
149
157
|
// `zeta` here is Algorithm 11's γ = ζ^(2BitRev_7(i)+1).
|
|
150
|
-
|
|
158
|
+
// Reduce a1*b1 before multiplying by zeta: a1*b1*zeta would reach ~2^35, forcing JS engines
|
|
159
|
+
// into slow float fmod; with the extra reduction every intermediate fits int32.
|
|
160
|
+
const c0 = crystals.mod(crystals.mod(a1 * b1) * zeta + a0 * b0);
|
|
151
161
|
const c1 = crystals.mod(a0 * b1 + a1 * b0);
|
|
152
162
|
return { c0, c1 };
|
|
153
163
|
}
|
|
@@ -169,6 +179,33 @@ function MultiplyNTTs(f_: TArg<Poly>, g_: TArg<Poly>): TRet<Poly> {
|
|
|
169
179
|
|
|
170
180
|
type PRF = (l: number, key: Uint8Array, nonce: number) => Uint8Array;
|
|
171
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Prepared (pre-expanded) ML-KEM public key. Experimental prototype.
|
|
184
|
+
* Caches only public data: packed ek, the expanded matrix Â, decoded t̂ and H(ek). No secret
|
|
185
|
+
* material is retained between calls; secret keys passed to `decapsulate` are decoded and wiped
|
|
186
|
+
* per call, exactly like the one-shot API. `clean()` wipes the expanded Â/t̂ cache; the packed
|
|
187
|
+
* public key and H(ek) are public and are not wiped. The object must not be used afterwards.
|
|
188
|
+
*/
|
|
189
|
+
export type KEMPrepared = {
|
|
190
|
+
/**
|
|
191
|
+
* Detached copy of the source public key. Treat as read-only while the prepared object is in use.
|
|
192
|
+
* Callers may wipe it after final use; any mutation invalidates subsequent operations.
|
|
193
|
+
*/
|
|
194
|
+
publicKey: Uint8Array;
|
|
195
|
+
/** Same as `KEM.encapsulate`, minus per-call ek re-validation and  re-expansion. */
|
|
196
|
+
encapsulate: (msg?: Uint8Array) => { cipherText: Uint8Array; sharedSecret: Uint8Array };
|
|
197
|
+
/**
|
|
198
|
+
* Same as `KEM.decapsulate`; throws if `secretKey` does not embed this public key.
|
|
199
|
+
* The embedded-ek byte comparison plus stored-hash comparison is equivalent to the
|
|
200
|
+
* FIPS 203 §7.3 hash input check.
|
|
201
|
+
*/
|
|
202
|
+
decapsulate: (cipherText: Uint8Array, secretKey: Uint8Array) => Uint8Array;
|
|
203
|
+
/** Wipe cached (public) data. */
|
|
204
|
+
clean: () => void;
|
|
205
|
+
};
|
|
206
|
+
/** KEM with prepared-key support. */
|
|
207
|
+
export type MLKEM = KEM & { prepare: (publicKey: Uint8Array) => KEMPrepared };
|
|
208
|
+
|
|
172
209
|
type XofGet = ReturnType<ReturnType<XOF>['get']>;
|
|
173
210
|
|
|
174
211
|
type KyberOpts = KEMParam & {
|
|
@@ -185,7 +222,7 @@ function SampleNTT(xof_: TArg<XofGet>): TRet<Poly> {
|
|
|
185
222
|
// The reader must already bind the Algorithm 7 seed||j||i bytes
|
|
186
223
|
// and return block lengths divisible by 3.
|
|
187
224
|
const r: Poly = new Uint16Array(N);
|
|
188
|
-
for (let j = 0; j < N;
|
|
225
|
+
for (let j = 0; j < N;) {
|
|
189
226
|
const b = xof();
|
|
190
227
|
if (b.length % 3) throw new Error('SampleNTT: unaligned block');
|
|
191
228
|
for (let i = 0; j < N && i + 3 <= b.length; i += 3) {
|
|
@@ -253,6 +290,38 @@ const genKPKE = (opts_: TArg<KyberOpts>) => {
|
|
|
253
290
|
const secretCoder = vecCoder(polyCoder(12), K);
|
|
254
291
|
const cipherCoder = splitCoder('ciphertext', vecCoder(polyU, K), polyV);
|
|
255
292
|
const seedCoder = splitCoder('seed', 32, 32);
|
|
293
|
+
// Algorithm 14 (K-PKE.Encrypt) core, after ek parsing. `tHat` and every poly returned by
|
|
294
|
+
// `getA(i, j)` are treated as disposable scratch: they are mutated in place and wiped/dropped,
|
|
295
|
+
// so callers holding cached copies must pass fresh copies.
|
|
296
|
+
const encryptCore = (
|
|
297
|
+
tHat: TArg<Poly[]>,
|
|
298
|
+
getA: TArg<(i: number, j: number) => Poly>,
|
|
299
|
+
msg: TArg<Uint8Array>,
|
|
300
|
+
seed: TArg<Uint8Array>
|
|
301
|
+
): TRet<Uint8Array> => {
|
|
302
|
+
const rHat = [];
|
|
303
|
+
for (let i = 0; i < K; i++) rHat.push(crystals.NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
|
|
304
|
+
const tmp2 = new Uint16Array(N);
|
|
305
|
+
const u = [];
|
|
306
|
+
for (let i = 0; i < K; i++) {
|
|
307
|
+
const e1 = sampleCBD(PRF, seed, K + i, ETA2);
|
|
308
|
+
const tmp = new Uint16Array(N);
|
|
309
|
+
for (let j = 0; j < K; j++) {
|
|
310
|
+
const aij = getA(i, j); // A[j][i], inplace transpose access
|
|
311
|
+
polyAdd(tmp, MultiplyNTTs(aij, rHat[j])); // t += aij * rHat[j]
|
|
312
|
+
}
|
|
313
|
+
polyAdd(e1, crystals.NTT.decode(tmp)); // e1 += tmp
|
|
314
|
+
u.push(e1);
|
|
315
|
+
polyAdd(tmp2, MultiplyNTTs(tHat[i], rHat[i])); // t2 += tHat[i] * rHat[i]
|
|
316
|
+
cleanBytes(tmp);
|
|
317
|
+
}
|
|
318
|
+
const e2 = sampleCBD(PRF, seed, 2 * K, ETA2);
|
|
319
|
+
polyAdd(e2, crystals.NTT.decode(tmp2)); // e2 += tmp2
|
|
320
|
+
const v = poly1.decode(msg); // encode plaintext m into polynomial v
|
|
321
|
+
polyAdd(v, e2); // v += e2
|
|
322
|
+
cleanBytes(tHat, rHat, tmp2, e2);
|
|
323
|
+
return cipherCoder.encode([u, v]) as TRet<Uint8Array>;
|
|
324
|
+
};
|
|
256
325
|
return {
|
|
257
326
|
secretCoder,
|
|
258
327
|
lengths: {
|
|
@@ -297,30 +366,31 @@ const genKPKE = (opts_: TArg<KyberOpts>) => {
|
|
|
297
366
|
seed: TArg<Uint8Array>
|
|
298
367
|
): TRet<Uint8Array> => {
|
|
299
368
|
const [tHat, rho] = publicCoder.decode(publicKey);
|
|
300
|
-
const rHat = [];
|
|
301
|
-
for (let i = 0; i < K; i++) rHat.push(crystals.NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
|
|
302
369
|
const x = XOF(rho);
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
370
|
+
const res = encryptCore(tHat as Poly[], (i, j) => SampleNTT(x.get(i, j)) as Poly, msg, seed);
|
|
371
|
+
x.clean();
|
|
372
|
+
return res;
|
|
373
|
+
},
|
|
374
|
+
// Expands the full  matrix (public data derived from rho) once, so repeated encryptions
|
|
375
|
+
// against the same ek skip the K² SampleNTT XOF expansions. Cached polys are copied per
|
|
376
|
+
// call because encryptCore mutates its inputs in place.
|
|
377
|
+
prepare: (publicKey: TArg<Uint8Array>) => {
|
|
378
|
+
const [tHat, rho] = publicCoder.decode(publicKey);
|
|
379
|
+
const x = XOF(rho);
|
|
380
|
+
const A: Poly[] = [];
|
|
381
|
+
for (let i = 0; i < K; i++)
|
|
382
|
+
for (let j = 0; j < K; j++) A.push(SampleNTT(x.get(i, j)) as Poly);
|
|
317
383
|
x.clean();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
384
|
+
return {
|
|
385
|
+
encrypt: (msg: TArg<Uint8Array>, seed: TArg<Uint8Array>): TRet<Uint8Array> =>
|
|
386
|
+
encryptCore(
|
|
387
|
+
(tHat as Poly[]).map((p) => p.slice() as Poly),
|
|
388
|
+
(i, j) => A[i * K + j].slice() as Poly,
|
|
389
|
+
msg,
|
|
390
|
+
seed
|
|
391
|
+
),
|
|
392
|
+
clean: () => cleanBytes(tHat as Poly[], A),
|
|
393
|
+
};
|
|
324
394
|
},
|
|
325
395
|
decrypt: (cipherText: TArg<Uint8Array>, privateKey: TArg<Uint8Array>): TRet<Uint8Array> => {
|
|
326
396
|
const [u, v] = cipherCoder.decode(cipherText);
|
|
@@ -329,8 +399,11 @@ const genKPKE = (opts_: TArg<KyberOpts>) => {
|
|
|
329
399
|
// tmp += sk[i] * u[i]
|
|
330
400
|
for (let i = 0; i < K; i++) polyAdd(tmp, MultiplyNTTs(sk[i], crystals.NTT.encode(u[i])));
|
|
331
401
|
polySub(v, crystals.NTT.decode(tmp)); // w = v' - tmp
|
|
332
|
-
|
|
333
|
-
|
|
402
|
+
// `v` now holds w, from which the plaintext is just a 1-bit threshold away, so wipe it too.
|
|
403
|
+
// encode() allocates its own buffer, so the returned bytes do not alias `v`.
|
|
404
|
+
const res = poly1.encode(v) as TRet<Uint8Array>;
|
|
405
|
+
cleanBytes(tmp, sk, u, v);
|
|
406
|
+
return res;
|
|
334
407
|
},
|
|
335
408
|
};
|
|
336
409
|
};
|
|
@@ -339,12 +412,17 @@ const genKPKE = (opts_: TArg<KyberOpts>) => {
|
|
|
339
412
|
* Public ML-KEM wrapper over the internal K-PKE subroutine.
|
|
340
413
|
* `keygen(seed)` and `encapsulate(publicKey, msg)` are deterministic/test-oriented hooks that map
|
|
341
414
|
* more directly to Algorithms 16-17 than to the pure no-input / random-internal Algorithms 19-20.
|
|
415
|
+
* `encapsulate`'s optional `msg` is the 32-byte message randomness `m` of Algorithm 17, the
|
|
416
|
+
* pre-image the shared secret is derived from, NOT a plaintext to encrypt: ML-KEM is a key
|
|
417
|
+
* encapsulation mechanism, not a cipher. Omit it to draw fresh randomness; pass it only to
|
|
418
|
+
* reproduce a known-answer vector, and only as 32 uniformly random bytes, since a low-entropy or
|
|
419
|
+
* reused value makes the shared secret predictable. The same holds for `keygen`'s optional `seed`.
|
|
342
420
|
* decapsulate() tries to follow the Algorithms 18/21 implicit-reject structure as closely as
|
|
343
421
|
* practical here by re-encrypting, comparing ciphertexts, returning `Khat` on match or `Kbar` on
|
|
344
422
|
* mismatch, and zeroizing the non-returned shared-secret candidate; JS/JIT still provides no
|
|
345
423
|
* constant-time guarantees for that path.
|
|
346
424
|
*/
|
|
347
|
-
function createKyber(opts: TArg<KyberOpts>): TRet<
|
|
425
|
+
function createKyber(opts: TArg<KyberOpts>): TRet<MLKEM> {
|
|
348
426
|
const rawOpts = opts as KyberOpts;
|
|
349
427
|
const KPKE = genKPKE(rawOpts);
|
|
350
428
|
const { HASH256, HASH512, KDF } = rawOpts;
|
|
@@ -352,6 +430,17 @@ function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
|
|
|
352
430
|
const secretCoder = splitCoder('secretKey', lengths.secretKey, lengths.publicKey, 32, 32);
|
|
353
431
|
const msgLen = 32;
|
|
354
432
|
const seedLen = 64;
|
|
433
|
+
// FIPS-203 includes additional verification check for modulus
|
|
434
|
+
const validateModulus = (publicKey: TArg<Uint8Array>, fn: string) => {
|
|
435
|
+
const eke = (publicKey as Uint8Array).subarray(0, 384 * rawOpts.K);
|
|
436
|
+
// Copy because of inplace encoding
|
|
437
|
+
const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
|
|
438
|
+
// (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
|
|
439
|
+
// If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
|
|
440
|
+
const ok = equalBytes(ek, eke);
|
|
441
|
+
cleanBytes(ek);
|
|
442
|
+
if (!ok) throw new Error(`ML-KEM.${fn}: wrong publicKey modulus`);
|
|
443
|
+
};
|
|
355
444
|
const kemLengths = Object.freeze({
|
|
356
445
|
...lengths,
|
|
357
446
|
seed: 64,
|
|
@@ -362,45 +451,58 @@ function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
|
|
|
362
451
|
return Object.freeze({
|
|
363
452
|
info: Object.freeze({ type: 'ml-kem' }),
|
|
364
453
|
lengths: kemLengths,
|
|
365
|
-
keygen: (seed
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
454
|
+
keygen: (seed?: TArg<Uint8Array>) => {
|
|
455
|
+
// A generated seed carries z (the implicit-rejection secret) and must be wiped once the
|
|
456
|
+
// secret key holds a copy, matching ml-dsa / slh-dsa / falcon keygen. A caller-supplied
|
|
457
|
+
// seed is the caller's to manage (and the immutability test requires it stay untouched).
|
|
458
|
+
const ownSeed = seed === undefined;
|
|
459
|
+
const s = ownSeed ? randomBytes(seedLen) : (seed as TArg<Uint8Array>);
|
|
460
|
+
let sk: Uint8Array | undefined;
|
|
461
|
+
let publicKeyHash: Uint8Array | undefined;
|
|
462
|
+
try {
|
|
463
|
+
abytes(s, seedLen, 'seed');
|
|
464
|
+
const keys = KPKE.keygen(s.subarray(0, 32));
|
|
465
|
+
const publicKey = keys.publicKey;
|
|
466
|
+
sk = keys.secretKey as Uint8Array;
|
|
467
|
+
publicKeyHash = HASH256(publicKey);
|
|
468
|
+
// (dkPKE||ek||H(ek)||z)
|
|
469
|
+
const secretKey = secretCoder.encode([sk, publicKey, publicKeyHash, s.subarray(32)]);
|
|
470
|
+
return {
|
|
471
|
+
publicKey: publicKey as TRet<Uint8Array>,
|
|
472
|
+
secretKey: secretKey as TRet<Uint8Array>,
|
|
473
|
+
};
|
|
474
|
+
} finally {
|
|
475
|
+
if (sk !== undefined) cleanBytes(sk);
|
|
476
|
+
if (publicKeyHash !== undefined) cleanBytes(publicKeyHash);
|
|
477
|
+
if (ownSeed) cleanBytes(s);
|
|
478
|
+
}
|
|
376
479
|
},
|
|
377
480
|
getPublicKey: (secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
|
|
378
481
|
const [_sk, publicKey, _publicKeyHash, _z] = secretCoder.decode(secretKey);
|
|
379
482
|
return Uint8Array.from(publicKey) as TRet<Uint8Array>;
|
|
380
483
|
},
|
|
381
|
-
encapsulate: (publicKey: TArg<Uint8Array>, msg
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
484
|
+
encapsulate: (publicKey: TArg<Uint8Array>, msg?: TArg<Uint8Array>) => {
|
|
485
|
+
// A generated message is the preimage of the shared secret (K = G(m || H(ek))[0:32]) and
|
|
486
|
+
// must be wiped. A caller-supplied message is the deterministic-randomness hook and the
|
|
487
|
+
// caller's to manage (the immutability test requires it stay untouched).
|
|
488
|
+
const ownMsg = msg === undefined;
|
|
489
|
+
const m = ownMsg ? randomBytes(msgLen) : (msg as TArg<Uint8Array>);
|
|
490
|
+
let kr: Uint8Array | undefined;
|
|
491
|
+
try {
|
|
492
|
+
abytes(publicKey, lengths.publicKey, 'publicKey');
|
|
493
|
+
abytes(m, msgLen, 'message');
|
|
494
|
+
validateModulus(publicKey, 'encapsulate');
|
|
495
|
+
// derive randomness
|
|
496
|
+
kr = HASH512.create().update(m).update(HASH256(publicKey)).digest();
|
|
497
|
+
const cipherText = KPKE.encrypt(publicKey, m, kr.subarray(32, 64));
|
|
498
|
+
return {
|
|
499
|
+
cipherText: cipherText as TRet<Uint8Array>,
|
|
500
|
+
sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
|
|
501
|
+
};
|
|
502
|
+
} finally {
|
|
503
|
+
if (kr !== undefined) cleanBytes(kr.subarray(32));
|
|
504
|
+
if (ownMsg) cleanBytes(m);
|
|
394
505
|
}
|
|
395
|
-
cleanBytes(ek);
|
|
396
|
-
// derive randomness
|
|
397
|
-
const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest();
|
|
398
|
-
const cipherText = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64));
|
|
399
|
-
cleanBytes(kr.subarray(32));
|
|
400
|
-
return {
|
|
401
|
-
cipherText: cipherText as TRet<Uint8Array>,
|
|
402
|
-
sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
|
|
403
|
-
};
|
|
404
506
|
},
|
|
405
507
|
decapsulate: (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
|
|
406
508
|
abytes(secretKey, secretCoder.bytesLen, 'secretKey'); // 768*k + 96
|
|
@@ -422,9 +524,69 @@ function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
|
|
|
422
524
|
// if ciphertexts do not match, “implicitly reject”
|
|
423
525
|
const isValid = equalBytes(cipherText, cipherText2);
|
|
424
526
|
const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
|
|
425
|
-
|
|
527
|
+
// kr[32:64] is the derived K-PKE encryption randomness: wipe it like encapsulate() does.
|
|
528
|
+
cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
|
|
426
529
|
return (isValid ? Khat : Kbar) as TRet<Uint8Array>;
|
|
427
530
|
},
|
|
531
|
+
/**
|
|
532
|
+
* Experimental prototype: pre-expand a public key so repeated encapsulate/decapsulate
|
|
533
|
+
* against the same key skip re-validation, H(ek), t̂ decoding and the K² SampleNTT
|
|
534
|
+
* XOF expansions of Â. Only public data is cached; see {@link KEMPrepared}.
|
|
535
|
+
*/
|
|
536
|
+
prepare: (publicKey: TArg<Uint8Array>): TRet<KEMPrepared> => {
|
|
537
|
+
abytes(publicKey, lengths.publicKey, 'publicKey');
|
|
538
|
+
validateModulus(publicKey, 'prepare');
|
|
539
|
+
const ek = copyBytes(publicKey); // detach from the caller before caching
|
|
540
|
+
const publicKeyHash = HASH256(ek);
|
|
541
|
+
const cached = KPKE.prepare(ek);
|
|
542
|
+
return Object.freeze({
|
|
543
|
+
publicKey: ek as TRet<Uint8Array>,
|
|
544
|
+
encapsulate: (msg?: TArg<Uint8Array>) => {
|
|
545
|
+
// As in the non-prepared encapsulate: a generated message is the shared-secret
|
|
546
|
+
// preimage and is wiped; a caller-supplied one is left untouched.
|
|
547
|
+
const ownMsg = msg === undefined;
|
|
548
|
+
const m = ownMsg ? randomBytes(msgLen) : (msg as TArg<Uint8Array>);
|
|
549
|
+
let kr: Uint8Array | undefined;
|
|
550
|
+
try {
|
|
551
|
+
abytes(m, msgLen, 'message');
|
|
552
|
+
kr = HASH512.create().update(m).update(publicKeyHash).digest();
|
|
553
|
+
const cipherText = cached.encrypt(m, kr.subarray(32, 64));
|
|
554
|
+
return {
|
|
555
|
+
cipherText: cipherText as TRet<Uint8Array>,
|
|
556
|
+
sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
|
|
557
|
+
};
|
|
558
|
+
} finally {
|
|
559
|
+
if (kr !== undefined) cleanBytes(kr.subarray(32));
|
|
560
|
+
if (ownMsg) cleanBytes(m);
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
decapsulate: (
|
|
564
|
+
cipherText: TArg<Uint8Array>,
|
|
565
|
+
secretKey: TArg<Uint8Array>
|
|
566
|
+
): TRet<Uint8Array> => {
|
|
567
|
+
abytes(secretKey, secretCoder.bytesLen, 'secretKey');
|
|
568
|
+
abytes(cipherText, lengths.cipherText, 'cipherText');
|
|
569
|
+
const [sk, ekEmbedded, storedHash, z] = secretCoder.decode(secretKey);
|
|
570
|
+
// Under KEMPrepared's read-only publicKey contract, bind dk to the prepared key.
|
|
571
|
+
// Together with publicKeyHash = H(ek) computed in prepare(), this is equivalent to (and
|
|
572
|
+
// stronger than) FIPS 203 §7.3's `H(dk[384k : 768k+32]) == dk[768k+32 : 768k+64]`.
|
|
573
|
+
if (!equalBytes(ekEmbedded, ek) || !equalBytes(storedHash, publicKeyHash))
|
|
574
|
+
throw new Error('ML-KEM.decapsulate: secretKey does not match prepared publicKey');
|
|
575
|
+
const msg = KPKE.decrypt(cipherText, sk);
|
|
576
|
+
// derive randomness, Khat, rHat = G(mHat || h)
|
|
577
|
+
const kr = HASH512.create().update(msg).update(publicKeyHash).digest();
|
|
578
|
+
const Khat = kr.subarray(0, 32);
|
|
579
|
+
// re-encrypt using the derived randomness and cached Â/t̂
|
|
580
|
+
const cipherText2 = cached.encrypt(msg, kr.subarray(32, 64));
|
|
581
|
+
// if ciphertexts do not match, “implicitly reject”
|
|
582
|
+
const isValid = equalBytes(cipherText, cipherText2);
|
|
583
|
+
const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
|
|
584
|
+
cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
|
|
585
|
+
return (isValid ? Khat : Kbar) as TRet<Uint8Array>;
|
|
586
|
+
},
|
|
587
|
+
clean: cached.clean,
|
|
588
|
+
}) as TRet<KEMPrepared>;
|
|
589
|
+
},
|
|
428
590
|
});
|
|
429
591
|
}
|
|
430
592
|
|
|
@@ -458,18 +620,29 @@ const mk = (params: KEMParam) =>
|
|
|
458
620
|
/**
|
|
459
621
|
* ML-KEM-512: Table 2 row `k=2, η1=3, η2=2, du=10, dv=4`; Table 3 sizes `800/1632/768/32`.
|
|
460
622
|
* The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
|
|
623
|
+
* @example
|
|
624
|
+
* Generate deterministic ML-KEM-512 keys, encapsulate a shared secret, and decapsulate it.
|
|
625
|
+
* ```ts
|
|
626
|
+
* import { ml_kem512 } from '@noble/post-quantum/ml-kem.js';
|
|
627
|
+
* const seed = new Uint8Array(ml_kem512.lengths.seed!);
|
|
628
|
+
* const { secretKey, publicKey } = ml_kem512.keygen(seed);
|
|
629
|
+
* const msg = new Uint8Array(ml_kem512.lengths.msgRand!);
|
|
630
|
+
* const { cipherText, sharedSecret } = ml_kem512.encapsulate(publicKey, msg);
|
|
631
|
+
* const recovered = ml_kem512.decapsulate(cipherText, secretKey);
|
|
632
|
+
* const publicKey2 = ml_kem512.getPublicKey(secretKey);
|
|
633
|
+
* ```
|
|
461
634
|
*/
|
|
462
|
-
export const ml_kem512: TRet<
|
|
635
|
+
export const ml_kem512: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[512]))();
|
|
463
636
|
/**
|
|
464
637
|
* ML-KEM-768: Table 2 row `k=3, η1=2, η2=2, du=10, dv=4`; Table 3 sizes `1184/2400/1088/32`.
|
|
465
638
|
* The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
|
|
466
639
|
*/
|
|
467
|
-
export const ml_kem768: TRet<
|
|
640
|
+
export const ml_kem768: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[768]))();
|
|
468
641
|
/**
|
|
469
642
|
* ML-KEM-1024: Table 2 row `k=4, η1=2, η2=2, du=11, dv=5`; Table 3 sizes `1568/3168/1568/32`.
|
|
470
643
|
* The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
|
|
471
644
|
*/
|
|
472
|
-
export const ml_kem1024: TRet<
|
|
645
|
+
export const ml_kem1024: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[1024]))();
|
|
473
646
|
|
|
474
647
|
// NOTE: for tests only, don't use. This keeps the exact internal ML-KEM math surfaces available
|
|
475
648
|
// without re-implementing them in separate test code.
|