@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/README.md +86 -42
- package/_crystals.d.ts +90 -4
- package/_crystals.d.ts.map +1 -1
- package/_crystals.js +77 -6
- package/_crystals.js.map +1 -1
- package/falcon.d.ts +84 -0
- package/falcon.d.ts.map +1 -0
- package/falcon.js +2385 -0
- package/falcon.js.map +1 -0
- package/hybrid.d.ts +194 -24
- package/hybrid.d.ts.map +1 -1
- package/hybrid.js +401 -77
- package/hybrid.js.map +1 -1
- package/index.js +8 -0
- package/index.js.map +1 -1
- package/ml-dsa.d.ts +29 -8
- package/ml-dsa.d.ts.map +1 -1
- package/ml-dsa.js +154 -78
- package/ml-dsa.js.map +1 -1
- package/ml-kem.d.ts +31 -7
- package/ml-kem.d.ts.map +1 -1
- package/ml-kem.js +194 -75
- package/ml-kem.js.map +1 -1
- package/package.json +15 -8
- package/slh-dsa.d.ts +137 -34
- package/slh-dsa.d.ts.map +1 -1
- package/slh-dsa.js +189 -68
- package/slh-dsa.js.map +1 -1
- package/src/_crystals.ts +135 -24
- package/src/falcon.ts +2503 -0
- package/src/hybrid.ts +515 -144
- package/src/index.ts +8 -0
- package/src/ml-dsa.ts +263 -138
- package/src/ml-kem.ts +240 -97
- package/src/slh-dsa.ts +391 -153
- package/src/utils.ts +491 -46
- package/utils.d.ts +362 -24
- package/utils.d.ts.map +1 -1
- package/utils.js +273 -20
- package/utils.js.map +1 -1
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
//
|
|
76
|
-
|
|
77
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
132
|
+
type Poly = Uint16Array;
|
|
96
133
|
|
|
97
|
-
function polyAdd(
|
|
98
|
-
|
|
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(
|
|
101
|
-
|
|
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
|
-
|
|
107
|
-
const
|
|
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.
|
|
112
|
-
//
|
|
113
|
-
function MultiplyNTTs(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
//
|
|
182
|
-
|
|
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[
|
|
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: (
|
|
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[
|
|
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
|
|
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
|
-
|
|
258
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
},
|
|
281
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
400
|
+
return {
|
|
401
|
+
cipherText: cipherText as TRet<Uint8Array>,
|
|
402
|
+
sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
|
|
403
|
+
};
|
|
312
404
|
},
|
|
313
|
-
decapsulate: (cipherText: 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
|
-
|
|
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
|
-
|
|
328
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
/**
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
+
}))();
|