@noble/post-quantum 0.6.0 → 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/README.md +106 -80
- package/_crystals.d.ts +23 -16
- package/_crystals.js +50 -12
- package/falcon.d.ts +7 -8
- package/falcon.js +109 -74
- package/hybrid.d.ts +21 -32
- package/hybrid.js +157 -78
- package/index.d.ts +0 -1
- package/index.js +8 -1
- package/ml-dsa.d.ts +35 -9
- package/ml-dsa.js +116 -43
- package/ml-kem.d.ts +46 -5
- package/ml-kem.js +175 -67
- package/package.json +10 -18
- package/slh-dsa.d.ts +44 -24
- package/slh-dsa.js +150 -84
- package/src/_crystals.ts +86 -35
- package/src/falcon.ts +230 -169
- package/src/hybrid.ts +243 -129
- package/src/index.ts +8 -0
- package/src/ml-dsa.ts +194 -87
- package/src/ml-kem.ts +283 -122
- package/src/slh-dsa.ts +310 -182
- package/src/utils.ts +244 -48
- package/utils.d.ts +99 -24
- package/utils.js +92 -24
- 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
|
@@ -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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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,28 +129,44 @@ 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
|
|
132
|
+
type Poly = Uint16Array;
|
|
130
133
|
|
|
131
|
-
|
|
134
|
+
// Coefficients always stay reduced in [0, Q) here (samplers, NTT and coders all reduce),
|
|
135
|
+
// so one conditional correction replaces the generic mod().
|
|
136
|
+
function polyAdd(a_: TArg<Poly>, b_: TArg<Poly>) {
|
|
137
|
+
const a = a_ as Poly;
|
|
138
|
+
const b = b_ as Poly;
|
|
132
139
|
// Mutates `a` in place; callers must pass two N=256 polynomials.
|
|
133
|
-
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
|
+
}
|
|
134
144
|
}
|
|
135
|
-
function polySub(
|
|
145
|
+
function polySub(a_: TArg<Poly>, b_: TArg<Poly>) {
|
|
146
|
+
const a = a_ as Poly;
|
|
147
|
+
const b = b_ as Poly;
|
|
136
148
|
// Mutates `a` in place; callers must pass two N=256 polynomials.
|
|
137
|
-
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
|
+
}
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
// FIPS-203: Computes the product of two degree-one polynomials with respect to a quadratic modulus
|
|
141
156
|
function BaseCaseMultiply(a0: number, a1: number, b0: number, b1: number, zeta: number) {
|
|
142
157
|
// `zeta` here is Algorithm 11's γ = ζ^(2BitRev_7(i)+1).
|
|
143
|
-
|
|
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);
|
|
144
161
|
const c1 = crystals.mod(a0 * b1 + a1 * b0);
|
|
145
162
|
return { c0, c1 };
|
|
146
163
|
}
|
|
147
164
|
|
|
148
165
|
// FIPS-203: Computes the product (in the ring Tq) of two NTT representations.
|
|
149
166
|
// Works in place on `f`; `g` is read-only and both inputs must already be in NTT form.
|
|
150
|
-
function MultiplyNTTs(
|
|
167
|
+
function MultiplyNTTs(f_: TArg<Poly>, g_: TArg<Poly>): TRet<Poly> {
|
|
168
|
+
const f = f_ as Poly;
|
|
169
|
+
const g = g_ as Poly;
|
|
151
170
|
for (let i = 0; i < N / 2; i++) {
|
|
152
171
|
let z = crystals.nttZetas[64 + (i >> 1)];
|
|
153
172
|
if (i & 1) z = -z;
|
|
@@ -155,24 +174,51 @@ function MultiplyNTTs(f: Poly, g: Poly): Poly {
|
|
|
155
174
|
f[2 * i + 0] = c0;
|
|
156
175
|
f[2 * i + 1] = c1;
|
|
157
176
|
}
|
|
158
|
-
return f
|
|
177
|
+
return f as TRet<Poly>;
|
|
159
178
|
}
|
|
160
179
|
|
|
161
180
|
type PRF = (l: number, key: Uint8Array, nonce: number) => Uint8Array;
|
|
162
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
|
+
|
|
163
209
|
type XofGet = ReturnType<ReturnType<XOF>['get']>;
|
|
164
210
|
|
|
165
211
|
type KyberOpts = KEMParam & {
|
|
166
212
|
HASH256: CHash;
|
|
167
213
|
HASH512: CHash;
|
|
168
|
-
|
|
169
|
-
KDF: any;
|
|
214
|
+
KDF: CHash<any, { dkLen?: number }>;
|
|
170
215
|
XOF: XOF; // (seed: Uint8Array, len: number, x: number, y: number) => Uint8Array;
|
|
171
216
|
PRF: PRF;
|
|
172
217
|
};
|
|
173
218
|
|
|
174
219
|
// Return poly in NTT representation
|
|
175
|
-
function SampleNTT(
|
|
220
|
+
function SampleNTT(xof_: TArg<XofGet>): TRet<Poly> {
|
|
221
|
+
const xof = xof_ as XofGet;
|
|
176
222
|
// The reader must already bind the Algorithm 7 seed||j||i bytes
|
|
177
223
|
// and return block lengths divisible by 3.
|
|
178
224
|
const r: Poly = new Uint16Array(N);
|
|
@@ -186,13 +232,13 @@ function SampleNTT(xof: XofGet) {
|
|
|
186
232
|
if (j < N && d2 < Q) r[j++] = d2;
|
|
187
233
|
}
|
|
188
234
|
}
|
|
189
|
-
return r
|
|
235
|
+
return r as TRet<Poly>;
|
|
190
236
|
}
|
|
191
237
|
|
|
192
238
|
// Sampling from the centered binomial distribution
|
|
193
239
|
// Returns poly with small coefficients (noise/errors) stored modulo q in ordinary coefficient form.
|
|
194
240
|
// Current callers only use Table 2 eta values {2,3} and PRF outputs of exactly 64*eta bytes.
|
|
195
|
-
const sampleCBDBytes = (buf: Uint8Array
|
|
241
|
+
const sampleCBDBytes = (buf: TArg<Uint8Array>, eta: number): TRet<Poly> => {
|
|
196
242
|
const r: Poly = new Uint16Array(N);
|
|
197
243
|
// CBD consumes the PRF bitstream in little-endian byte order; normalize the word view on BE,
|
|
198
244
|
// then swap it back so callers still observe `buf` as read-only.
|
|
@@ -217,10 +263,16 @@ const sampleCBDBytes = (buf: Uint8Array, eta: number): Poly => {
|
|
|
217
263
|
}
|
|
218
264
|
swap32IfBE(b32);
|
|
219
265
|
if (len) throw new Error(`sampleCBD: leftover bits: ${len}`);
|
|
220
|
-
return r
|
|
266
|
+
return r as TRet<Poly>;
|
|
221
267
|
};
|
|
222
268
|
|
|
223
|
-
function sampleCBD(
|
|
269
|
+
function sampleCBD(
|
|
270
|
+
PRF_: TArg<PRF>,
|
|
271
|
+
seed: TArg<Uint8Array>,
|
|
272
|
+
nonce: number,
|
|
273
|
+
eta: number
|
|
274
|
+
): TRet<Poly> {
|
|
275
|
+
const PRF = PRF_ as PRF;
|
|
224
276
|
return sampleCBDBytes(PRF((eta * N) / 4, seed, nonce), eta);
|
|
225
277
|
}
|
|
226
278
|
|
|
@@ -228,7 +280,8 @@ function sampleCBD(PRF: PRF, seed: Uint8Array, nonce: number, eta: number): Poly
|
|
|
228
280
|
// Internal ML-KEM subroutine only: exact 32-byte `seed` / `msg` inputs
|
|
229
281
|
// come from Algorithms 13-15, and the helper mutates decoded temporary
|
|
230
282
|
// polynomials in place while leaving caller byte arrays unchanged.
|
|
231
|
-
const genKPKE = (
|
|
283
|
+
const genKPKE = (opts_: TArg<KyberOpts>) => {
|
|
284
|
+
const opts = opts_ as KyberOpts;
|
|
232
285
|
const { K, PRF, XOF, HASH512, ETA1, ETA2, du, dv } = opts;
|
|
233
286
|
const poly1 = polyCoder(1);
|
|
234
287
|
const polyV = polyCoder(dv);
|
|
@@ -237,6 +290,38 @@ const genKPKE = (opts: KyberOpts) => {
|
|
|
237
290
|
const secretCoder = vecCoder(polyCoder(12), K);
|
|
238
291
|
const cipherCoder = splitCoder('ciphertext', vecCoder(polyU, K), polyV);
|
|
239
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
|
+
};
|
|
240
325
|
return {
|
|
241
326
|
secretCoder,
|
|
242
327
|
lengths: {
|
|
@@ -244,7 +329,7 @@ const genKPKE = (opts: KyberOpts) => {
|
|
|
244
329
|
publicKey: publicCoder.bytesLen,
|
|
245
330
|
cipherText: cipherCoder.bytesLen,
|
|
246
331
|
},
|
|
247
|
-
keygen: (seed: Uint8Array) => {
|
|
332
|
+
keygen: (seed: TArg<Uint8Array>) => {
|
|
248
333
|
abytes(seed, 32, 'seed');
|
|
249
334
|
const seedDst = new Uint8Array(33);
|
|
250
335
|
seedDst.set(seed);
|
|
@@ -275,34 +360,39 @@ const genKPKE = (opts: KyberOpts) => {
|
|
|
275
360
|
cleanBytes(rho, sigma, sHat, tHat, seedDst, seedHash);
|
|
276
361
|
return res;
|
|
277
362
|
},
|
|
278
|
-
encrypt: (
|
|
363
|
+
encrypt: (
|
|
364
|
+
publicKey: TArg<Uint8Array>,
|
|
365
|
+
msg: TArg<Uint8Array>,
|
|
366
|
+
seed: TArg<Uint8Array>
|
|
367
|
+
): TRet<Uint8Array> => {
|
|
279
368
|
const [tHat, rho] = publicCoder.decode(publicKey);
|
|
280
|
-
const rHat = [];
|
|
281
|
-
for (let i = 0; i < K; i++) rHat.push(crystals.NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
|
|
282
369
|
const x = XOF(rho);
|
|
283
|
-
const
|
|
284
|
-
const u = [];
|
|
285
|
-
for (let i = 0; i < K; i++) {
|
|
286
|
-
const e1 = sampleCBD(PRF, seed, K + i, ETA2);
|
|
287
|
-
const tmp = new Uint16Array(N);
|
|
288
|
-
for (let j = 0; j < K; j++) {
|
|
289
|
-
const aij = SampleNTT(x.get(i, j)); // A[j][i], inplace transpose access
|
|
290
|
-
polyAdd(tmp, MultiplyNTTs(aij, rHat[j])); // t += aij * rHat[j]
|
|
291
|
-
}
|
|
292
|
-
polyAdd(e1, crystals.NTT.decode(tmp)); // e1 += tmp
|
|
293
|
-
u.push(e1);
|
|
294
|
-
polyAdd(tmp2, MultiplyNTTs(tHat[i], rHat[i])); // t2 += tHat[i] * rHat[i]
|
|
295
|
-
cleanBytes(tmp);
|
|
296
|
-
}
|
|
370
|
+
const res = encryptCore(tHat as Poly[], (i, j) => SampleNTT(x.get(i, j)) as Poly, msg, seed);
|
|
297
371
|
x.clean();
|
|
298
|
-
|
|
299
|
-
polyAdd(e2, crystals.NTT.decode(tmp2)); // e2 += tmp2
|
|
300
|
-
const v = poly1.decode(msg); // encode plaintext m into polynomial v
|
|
301
|
-
polyAdd(v, e2); // v += e2
|
|
302
|
-
cleanBytes(tHat, rHat, tmp2, e2);
|
|
303
|
-
return cipherCoder.encode([u, v]);
|
|
372
|
+
return res;
|
|
304
373
|
},
|
|
305
|
-
|
|
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);
|
|
383
|
+
x.clean();
|
|
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
|
+
};
|
|
394
|
+
},
|
|
395
|
+
decrypt: (cipherText: TArg<Uint8Array>, privateKey: TArg<Uint8Array>): TRet<Uint8Array> => {
|
|
306
396
|
const [u, v] = cipherCoder.decode(cipherText);
|
|
307
397
|
const sk = secretCoder.decode(privateKey); // s ← ByteDecode_12(dkPKE)
|
|
308
398
|
const tmp = new Uint16Array(N);
|
|
@@ -310,7 +400,7 @@ const genKPKE = (opts: KyberOpts) => {
|
|
|
310
400
|
for (let i = 0; i < K; i++) polyAdd(tmp, MultiplyNTTs(sk[i], crystals.NTT.encode(u[i])));
|
|
311
401
|
polySub(v, crystals.NTT.decode(tmp)); // w = v' - tmp
|
|
312
402
|
cleanBytes(tmp, sk, u);
|
|
313
|
-
return poly1.encode(v)
|
|
403
|
+
return poly1.encode(v) as TRet<Uint8Array>;
|
|
314
404
|
},
|
|
315
405
|
};
|
|
316
406
|
};
|
|
@@ -324,57 +414,65 @@ const genKPKE = (opts: KyberOpts) => {
|
|
|
324
414
|
* mismatch, and zeroizing the non-returned shared-secret candidate; JS/JIT still provides no
|
|
325
415
|
* constant-time guarantees for that path.
|
|
326
416
|
*/
|
|
327
|
-
function createKyber(opts: KyberOpts) {
|
|
328
|
-
const
|
|
329
|
-
const
|
|
417
|
+
function createKyber(opts: TArg<KyberOpts>): TRet<MLKEM> {
|
|
418
|
+
const rawOpts = opts as KyberOpts;
|
|
419
|
+
const KPKE = genKPKE(rawOpts);
|
|
420
|
+
const { HASH256, HASH512, KDF } = rawOpts;
|
|
330
421
|
const { secretCoder: KPKESecretCoder, lengths } = KPKE;
|
|
331
422
|
const secretCoder = splitCoder('secretKey', lengths.secretKey, lengths.publicKey, 32, 32);
|
|
332
423
|
const msgLen = 32;
|
|
333
424
|
const seedLen = 64;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
425
|
+
// FIPS-203 includes additional verification check for modulus
|
|
426
|
+
const validateModulus = (publicKey: TArg<Uint8Array>, fn: string) => {
|
|
427
|
+
const eke = (publicKey as Uint8Array).subarray(0, 384 * rawOpts.K);
|
|
428
|
+
// Copy because of inplace encoding
|
|
429
|
+
const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
|
|
430
|
+
// (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
|
|
431
|
+
// If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
|
|
432
|
+
const ok = equalBytes(ek, eke);
|
|
433
|
+
cleanBytes(ek);
|
|
434
|
+
if (!ok) throw new Error(`ML-KEM.${fn}: wrong publicKey modulus`);
|
|
435
|
+
};
|
|
436
|
+
const kemLengths = Object.freeze({
|
|
437
|
+
...lengths,
|
|
438
|
+
seed: 64,
|
|
439
|
+
msg: msgLen,
|
|
440
|
+
msgRand: msgLen,
|
|
441
|
+
secretKey: secretCoder.bytesLen,
|
|
442
|
+
});
|
|
443
|
+
return Object.freeze({
|
|
444
|
+
info: Object.freeze({ type: 'ml-kem' }),
|
|
445
|
+
lengths: kemLengths,
|
|
446
|
+
keygen: (seed: TArg<Uint8Array> = randomBytes(seedLen)) => {
|
|
344
447
|
abytes(seed, seedLen, 'seed');
|
|
345
448
|
const { publicKey, secretKey: sk } = KPKE.keygen(seed.subarray(0, 32));
|
|
346
449
|
const publicKeyHash = HASH256(publicKey);
|
|
347
450
|
// (dkPKE||ek||H(ek)||z)
|
|
348
451
|
const secretKey = secretCoder.encode([sk, publicKey, publicKeyHash, seed.subarray(32)]);
|
|
349
452
|
cleanBytes(sk, publicKeyHash);
|
|
350
|
-
return {
|
|
453
|
+
return {
|
|
454
|
+
publicKey: publicKey as TRet<Uint8Array>,
|
|
455
|
+
secretKey: secretKey as TRet<Uint8Array>,
|
|
456
|
+
};
|
|
351
457
|
},
|
|
352
|
-
getPublicKey: (secretKey: Uint8Array) => {
|
|
458
|
+
getPublicKey: (secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
|
|
353
459
|
const [_sk, publicKey, _publicKeyHash, _z] = secretCoder.decode(secretKey);
|
|
354
|
-
return Uint8Array.from(publicKey)
|
|
460
|
+
return Uint8Array.from(publicKey) as TRet<Uint8Array>;
|
|
355
461
|
},
|
|
356
|
-
encapsulate: (publicKey: Uint8Array
|
|
462
|
+
encapsulate: (publicKey: TArg<Uint8Array>, msg: TArg<Uint8Array> = randomBytes(msgLen)) => {
|
|
357
463
|
abytes(publicKey, lengths.publicKey, 'publicKey');
|
|
358
464
|
abytes(msg, msgLen, 'message');
|
|
359
|
-
|
|
360
|
-
// FIPS-203 includes additional verification check for modulus
|
|
361
|
-
const eke = publicKey.subarray(0, 384 * opts.K);
|
|
362
|
-
// Copy because of inplace encoding
|
|
363
|
-
const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
|
|
364
|
-
// (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
|
|
365
|
-
// If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
|
|
366
|
-
if (!equalBytes(ek, eke)) {
|
|
367
|
-
cleanBytes(ek);
|
|
368
|
-
throw new Error('ML-KEM.encapsulate: wrong publicKey modulus');
|
|
369
|
-
}
|
|
370
|
-
cleanBytes(ek);
|
|
465
|
+
validateModulus(publicKey, 'encapsulate');
|
|
371
466
|
// derive randomness
|
|
372
467
|
const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest();
|
|
373
468
|
const cipherText = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64));
|
|
374
469
|
cleanBytes(kr.subarray(32));
|
|
375
|
-
return {
|
|
470
|
+
return {
|
|
471
|
+
cipherText: cipherText as TRet<Uint8Array>,
|
|
472
|
+
sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
|
|
473
|
+
};
|
|
376
474
|
},
|
|
377
|
-
decapsulate: (cipherText: Uint8Array
|
|
475
|
+
decapsulate: (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
|
|
378
476
|
abytes(secretKey, secretCoder.bytesLen, 'secretKey'); // 768*k + 96
|
|
379
477
|
abytes(cipherText, lengths.cipherText, 'cipherText'); // 32(du*k + dv)
|
|
380
478
|
// test ← H(dk[384𝑘 ∶ 768𝑘 + 32])) .
|
|
@@ -394,20 +492,71 @@ function createKyber(opts: KyberOpts) {
|
|
|
394
492
|
// if ciphertexts do not match, “implicitly reject”
|
|
395
493
|
const isValid = equalBytes(cipherText, cipherText2);
|
|
396
494
|
const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
|
|
397
|
-
|
|
398
|
-
|
|
495
|
+
// kr[32:64] is the derived K-PKE encryption randomness: wipe it like encapsulate() does.
|
|
496
|
+
cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
|
|
497
|
+
return (isValid ? Khat : Kbar) as TRet<Uint8Array>;
|
|
399
498
|
},
|
|
400
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Experimental prototype: pre-expand a public key so repeated encapsulate/decapsulate
|
|
501
|
+
* against the same key skip re-validation, H(ek), t̂ decoding and the K² SampleNTT
|
|
502
|
+
* XOF expansions of Â. Only public data is cached; see {@link KEMPrepared}.
|
|
503
|
+
*/
|
|
504
|
+
prepare: (publicKey: TArg<Uint8Array>): TRet<KEMPrepared> => {
|
|
505
|
+
abytes(publicKey, lengths.publicKey, 'publicKey');
|
|
506
|
+
validateModulus(publicKey, 'prepare');
|
|
507
|
+
const ek = copyBytes(publicKey); // detach from the caller before caching
|
|
508
|
+
const publicKeyHash = HASH256(ek);
|
|
509
|
+
const cached = KPKE.prepare(ek);
|
|
510
|
+
return Object.freeze({
|
|
511
|
+
publicKey: ek as TRet<Uint8Array>,
|
|
512
|
+
encapsulate: (msg: TArg<Uint8Array> = randomBytes(msgLen)) => {
|
|
513
|
+
abytes(msg, msgLen, 'message');
|
|
514
|
+
const kr = HASH512.create().update(msg).update(publicKeyHash).digest();
|
|
515
|
+
const cipherText = cached.encrypt(msg, kr.subarray(32, 64));
|
|
516
|
+
cleanBytes(kr.subarray(32));
|
|
517
|
+
return {
|
|
518
|
+
cipherText: cipherText as TRet<Uint8Array>,
|
|
519
|
+
sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
|
|
520
|
+
};
|
|
521
|
+
},
|
|
522
|
+
decapsulate: (
|
|
523
|
+
cipherText: TArg<Uint8Array>,
|
|
524
|
+
secretKey: TArg<Uint8Array>
|
|
525
|
+
): TRet<Uint8Array> => {
|
|
526
|
+
abytes(secretKey, secretCoder.bytesLen, 'secretKey');
|
|
527
|
+
abytes(cipherText, lengths.cipherText, 'cipherText');
|
|
528
|
+
const [sk, ekEmbedded, storedHash, z] = secretCoder.decode(secretKey);
|
|
529
|
+
// Under KEMPrepared's read-only publicKey contract, bind dk to the prepared key.
|
|
530
|
+
// Together with publicKeyHash = H(ek) computed in prepare(), this is equivalent to (and
|
|
531
|
+
// stronger than) FIPS 203 §7.3's `H(dk[384k : 768k+32]) == dk[768k+32 : 768k+64]`.
|
|
532
|
+
if (!equalBytes(ekEmbedded, ek) || !equalBytes(storedHash, publicKeyHash))
|
|
533
|
+
throw new Error('ML-KEM.decapsulate: secretKey does not match prepared publicKey');
|
|
534
|
+
const msg = KPKE.decrypt(cipherText, sk);
|
|
535
|
+
// derive randomness, Khat, rHat = G(mHat || h)
|
|
536
|
+
const kr = HASH512.create().update(msg).update(publicKeyHash).digest();
|
|
537
|
+
const Khat = kr.subarray(0, 32);
|
|
538
|
+
// re-encrypt using the derived randomness and cached Â/t̂
|
|
539
|
+
const cipherText2 = cached.encrypt(msg, kr.subarray(32, 64));
|
|
540
|
+
// if ciphertexts do not match, “implicitly reject”
|
|
541
|
+
const isValid = equalBytes(cipherText, cipherText2);
|
|
542
|
+
const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
|
|
543
|
+
cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
|
|
544
|
+
return (isValid ? Khat : Kbar) as TRet<Uint8Array>;
|
|
545
|
+
},
|
|
546
|
+
clean: cached.clean,
|
|
547
|
+
}) as TRet<KEMPrepared>;
|
|
548
|
+
},
|
|
549
|
+
});
|
|
401
550
|
}
|
|
402
551
|
|
|
403
552
|
// FIPS 203's PRF_eta binding: current callers use only 32-byte keys, one-byte nonces,
|
|
404
553
|
// and dkLen values {128, 192}; out-of-range nonce numbers still wrap modulo 256 here.
|
|
405
|
-
function shakePRF(dkLen: number, key: Uint8Array
|
|
554
|
+
function shakePRF(dkLen: number, key: TArg<Uint8Array>, nonce: number): TRet<Uint8Array> {
|
|
406
555
|
return shake256
|
|
407
556
|
.create({ dkLen })
|
|
408
557
|
.update(key)
|
|
409
558
|
.update(new Uint8Array([nonce]))
|
|
410
|
-
.digest()
|
|
559
|
+
.digest() as TRet<Uint8Array>;
|
|
411
560
|
}
|
|
412
561
|
|
|
413
562
|
// Fixed ML-KEM hash/XOF bindings. `KDF` here is the spec's fixed 32-byte `J` call,
|
|
@@ -430,53 +579,65 @@ const mk = (params: KEMParam) =>
|
|
|
430
579
|
/**
|
|
431
580
|
* ML-KEM-512: Table 2 row `k=2, η1=3, η2=2, du=10, dv=4`; Table 3 sizes `800/1632/768/32`.
|
|
432
581
|
* The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
|
|
582
|
+
* @example
|
|
583
|
+
* Generate deterministic ML-KEM-512 keys, encapsulate a shared secret, and decapsulate it.
|
|
584
|
+
* ```ts
|
|
585
|
+
* import { ml_kem512 } from '@noble/post-quantum/ml-kem.js';
|
|
586
|
+
* const seed = new Uint8Array(ml_kem512.lengths.seed!);
|
|
587
|
+
* const { secretKey, publicKey } = ml_kem512.keygen(seed);
|
|
588
|
+
* const msg = new Uint8Array(ml_kem512.lengths.msgRand!);
|
|
589
|
+
* const { cipherText, sharedSecret } = ml_kem512.encapsulate(publicKey, msg);
|
|
590
|
+
* const recovered = ml_kem512.decapsulate(cipherText, secretKey);
|
|
591
|
+
* const publicKey2 = ml_kem512.getPublicKey(secretKey);
|
|
592
|
+
* ```
|
|
433
593
|
*/
|
|
434
|
-
export const ml_kem512:
|
|
594
|
+
export const ml_kem512: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[512]))();
|
|
435
595
|
/**
|
|
436
596
|
* ML-KEM-768: Table 2 row `k=3, η1=2, η2=2, du=10, dv=4`; Table 3 sizes `1184/2400/1088/32`.
|
|
437
597
|
* The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
|
|
438
598
|
*/
|
|
439
|
-
export const ml_kem768:
|
|
599
|
+
export const ml_kem768: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[768]))();
|
|
440
600
|
/**
|
|
441
601
|
* ML-KEM-1024: Table 2 row `k=4, η1=2, η2=2, du=11, dv=5`; Table 3 sizes `1568/3168/1568/32`.
|
|
442
602
|
* The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
|
|
443
603
|
*/
|
|
444
|
-
export const ml_kem1024:
|
|
604
|
+
export const ml_kem1024: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[1024]))();
|
|
445
605
|
|
|
446
606
|
// NOTE: for tests only, don't use. This keeps the exact internal ML-KEM math surfaces available
|
|
447
607
|
// without re-implementing them in separate test code.
|
|
448
|
-
export const __tests: any = /* @__PURE__ */ (() =>
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
MultiplyNTTs(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
608
|
+
export const __tests: any = /* @__PURE__ */ (() =>
|
|
609
|
+
Object.freeze({
|
|
610
|
+
Compress_d: (x: number, d: number) => {
|
|
611
|
+
if (d < 1 || d > 11) throw new Error(`Compress_d: expected d in [1..11], got ${d}`);
|
|
612
|
+
return compress(d).encode(x) & getMask(d);
|
|
613
|
+
},
|
|
614
|
+
Decompress_d: (y: number, d: number) => {
|
|
615
|
+
if (d < 1 || d > 11) throw new Error(`Decompress_d: expected d in [1..11], got ${d}`);
|
|
616
|
+
return compress(d).decode(y);
|
|
617
|
+
},
|
|
618
|
+
ByteEncode_d: (F: TArg<Uint16Array>, d: number) => {
|
|
619
|
+
if (d < 1 || d > 12) throw new Error(`ByteEncode_d: expected d in [1..12], got ${d}`);
|
|
620
|
+
return byteCoder(d).encode(F as TRet<Uint16Array>);
|
|
621
|
+
},
|
|
622
|
+
ByteDecode_d: (B: TArg<Uint8Array>, d: number) => {
|
|
623
|
+
if (d < 1 || d > 12) throw new Error(`ByteDecode_d: expected d in [1..12], got ${d}`);
|
|
624
|
+
return byteCoder(d).decode(B);
|
|
625
|
+
},
|
|
626
|
+
NTT: (f: TArg<Uint16Array>) => crystals.NTT.encode(Uint16Array.from(f)),
|
|
627
|
+
NTT_inv: (fHat: TArg<Uint16Array>) => crystals.NTT.decode(Uint16Array.from(fHat)),
|
|
628
|
+
MultiplyNTTs: (fHat: TArg<Uint16Array>, gHat: TArg<Uint16Array>) =>
|
|
629
|
+
MultiplyNTTs(Uint16Array.from(fHat), Uint16Array.from(gHat)),
|
|
630
|
+
SamplePolyCBD: (B: TArg<Uint8Array>, eta: number) => {
|
|
631
|
+
abytes(B, 64 * eta, 'B');
|
|
632
|
+
return sampleCBDBytes(B, eta);
|
|
633
|
+
},
|
|
634
|
+
SampleNTT: (B: TArg<Uint8Array>) => {
|
|
635
|
+
abytes(B, 34, 'B');
|
|
636
|
+
const xof = XOF128(B.subarray(0, 32));
|
|
637
|
+
try {
|
|
638
|
+
return SampleNTT(xof.get(B[32], B[33]));
|
|
639
|
+
} finally {
|
|
640
|
+
xof.clean();
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
}))();
|