@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/hybrid.ts
CHANGED
|
@@ -80,6 +80,8 @@ import { type ECDSA } from '@noble/curves/abstract/weierstrass.js';
|
|
|
80
80
|
import { x25519 } from '@noble/curves/ed25519.js';
|
|
81
81
|
import { p256, p384 } from '@noble/curves/nist.js';
|
|
82
82
|
import {
|
|
83
|
+
abool,
|
|
84
|
+
afunction,
|
|
83
85
|
asciiToBytes,
|
|
84
86
|
bytesToNumberBE,
|
|
85
87
|
bytesToNumberLE,
|
|
@@ -92,6 +94,8 @@ import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
|
|
|
92
94
|
import { abytes, ahash, anumber, type CHash, type CHashXOF } from '@noble/hashes/utils.js';
|
|
93
95
|
import { ml_kem1024, ml_kem768 } from './ml-kem.ts';
|
|
94
96
|
import {
|
|
97
|
+
aobject,
|
|
98
|
+
astring,
|
|
95
99
|
cleanBytes,
|
|
96
100
|
copyBytes,
|
|
97
101
|
randomBytes,
|
|
@@ -101,14 +105,39 @@ import {
|
|
|
101
105
|
type CryptoKeys,
|
|
102
106
|
type KEM,
|
|
103
107
|
type Signer,
|
|
108
|
+
type TArg,
|
|
109
|
+
type TRet,
|
|
104
110
|
} from './utils.ts';
|
|
105
111
|
|
|
106
112
|
type CurveAll = ECDSA | EdDSA | MontgomeryECDH;
|
|
107
113
|
type CurveECDH = ECDSA | MontgomeryECDH;
|
|
108
114
|
type CurveSign = ECDSA | EdDSA;
|
|
115
|
+
const validateKEM = (kem: TArg<KEM>, title: string): TRet<KEM> => {
|
|
116
|
+
const k = aobject<KEM>(kem, title);
|
|
117
|
+
aobject(k.lengths, `${title}.lengths`);
|
|
118
|
+
afunction(k.keygen, `${title}.keygen`);
|
|
119
|
+
afunction(k.getPublicKey, `${title}.getPublicKey`);
|
|
120
|
+
afunction(k.encapsulate, `${title}.encapsulate`);
|
|
121
|
+
afunction(k.decapsulate, `${title}.decapsulate`);
|
|
122
|
+
return k as TRet<KEM>;
|
|
123
|
+
};
|
|
124
|
+
const validateSigner = (signer: TArg<Signer>, title: string): TRet<Signer> => {
|
|
125
|
+
const s = aobject<Signer>(signer, title);
|
|
126
|
+
aobject(s.lengths, `${title}.lengths`);
|
|
127
|
+
afunction(s.keygen, `${title}.keygen`);
|
|
128
|
+
afunction(s.getPublicKey, `${title}.getPublicKey`);
|
|
129
|
+
afunction(s.sign, `${title}.sign`);
|
|
130
|
+
afunction(s.verify, `${title}.verify`);
|
|
131
|
+
return s as TRet<Signer>;
|
|
132
|
+
};
|
|
109
133
|
|
|
110
134
|
// Can re-use if decide to signatures support, on other hand getSecretKey is specific and ugly
|
|
111
135
|
function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
|
|
136
|
+
const c = aobject<CurveAll>(curve, 'curve');
|
|
137
|
+
aobject(c.lengths, 'curve.lengths');
|
|
138
|
+
afunction(c.keygen, 'curve.keygen');
|
|
139
|
+
afunction(c.getPublicKey, 'curve.getPublicKey');
|
|
140
|
+
abool(allowZeroKey, 'allowZeroKey');
|
|
112
141
|
const lengths = curve.lengths;
|
|
113
142
|
let keygen = curve.keygen;
|
|
114
143
|
if (allowZeroKey) {
|
|
@@ -126,18 +155,26 @@ function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
|
|
|
126
155
|
// Unlike noble-curves' seeded Weierstrass keygen, this path removes the post-reduction +1.
|
|
127
156
|
// That is enough to match exact reduced-vector bytes, but an all-zero seed still reduces to
|
|
128
157
|
// scalar 0 here and getPublicKey(secretKey) throws instead of "allowing zero".
|
|
129
|
-
keygen = (seed: Uint8Array = randomBytes(lengths.seed)) => {
|
|
158
|
+
keygen = (seed: TArg<Uint8Array> = randomBytes(lengths.seed)) => {
|
|
130
159
|
abytes(seed, lengths.seed!, 'seed');
|
|
131
160
|
const seedScalar = Fn.isLE ? bytesToNumberLE(seed) : bytesToNumberBE(seed);
|
|
132
161
|
// Reduce directly into [0, ORDER); scalar 0 still stays invalid.
|
|
133
162
|
const secretKey = Fn.toBytes(Fn.create(seedScalar));
|
|
134
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
secretKey: secretKey as TRet<Uint8Array>,
|
|
165
|
+
publicKey: curve.getPublicKey(secretKey) as TRet<Uint8Array>,
|
|
166
|
+
};
|
|
135
167
|
};
|
|
136
168
|
}
|
|
137
169
|
return {
|
|
138
170
|
lengths: { secretKey: lengths.secretKey, publicKey: lengths.publicKey, seed: lengths.seed },
|
|
139
|
-
keygen
|
|
140
|
-
|
|
171
|
+
keygen: (seed?: TArg<Uint8Array>) =>
|
|
172
|
+
keygen(seed) as TRet<{
|
|
173
|
+
secretKey: Uint8Array;
|
|
174
|
+
publicKey: Uint8Array;
|
|
175
|
+
}>,
|
|
176
|
+
getPublicKey: (secretKey: TArg<Uint8Array>) =>
|
|
177
|
+
curve.getPublicKey(secretKey) as TRet<Uint8Array>,
|
|
141
178
|
};
|
|
142
179
|
}
|
|
143
180
|
|
|
@@ -164,22 +201,30 @@ function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
|
|
|
164
201
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
165
202
|
* ```
|
|
166
203
|
*/
|
|
167
|
-
export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): KEM {
|
|
204
|
+
export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): TRet<KEM> {
|
|
168
205
|
const kg = ecKeygen(curve, allowZeroKey);
|
|
169
206
|
if (!curve.getSharedSecret) throw new Error('wrong curve'); // ed25519 doesn't have one!
|
|
207
|
+
// Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
|
|
208
|
+
const decapsulate = (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>) => {
|
|
209
|
+
const res = curve.getSharedSecret(secretKey, cipherText);
|
|
210
|
+
return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res) as TRet<Uint8Array>;
|
|
211
|
+
};
|
|
170
212
|
return {
|
|
171
213
|
lengths: { ...kg.lengths, msg: kg.lengths.seed, cipherText: kg.lengths.publicKey },
|
|
172
214
|
keygen: kg.keygen,
|
|
173
215
|
getPublicKey: kg.getPublicKey,
|
|
174
|
-
encapsulate(
|
|
216
|
+
encapsulate(
|
|
217
|
+
publicKey: TArg<Uint8Array>,
|
|
218
|
+
rand: TArg<Uint8Array> = randomBytes(curve.lengths.seed)
|
|
219
|
+
) {
|
|
175
220
|
// Some curve.keygen(seed) paths reuse the provided seed buffer as secretKey; detach caller
|
|
176
221
|
// randomness first so cleanBytes() only wipes wrapper-owned material.
|
|
177
222
|
const seed = copyBytes(rand);
|
|
178
223
|
let ek: Uint8Array | undefined = undefined;
|
|
179
224
|
try {
|
|
180
|
-
ek =
|
|
181
|
-
const sharedSecret =
|
|
182
|
-
const cipherText = curve.getPublicKey(ek)
|
|
225
|
+
ek = kg.keygen(seed).secretKey;
|
|
226
|
+
const sharedSecret = decapsulate(publicKey, ek);
|
|
227
|
+
const cipherText = curve.getPublicKey(ek) as TRet<Uint8Array>;
|
|
183
228
|
return { sharedSecret, cipherText };
|
|
184
229
|
} finally {
|
|
185
230
|
// Invalid peer public keys can make decapsulation throw; wipe both the detached seed and
|
|
@@ -188,10 +233,7 @@ export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): KEM {
|
|
|
188
233
|
if (ek) cleanBytes(ek);
|
|
189
234
|
}
|
|
190
235
|
},
|
|
191
|
-
decapsulate
|
|
192
|
-
const res = curve.getSharedSecret(secretKey, cipherText);
|
|
193
|
-
return curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res;
|
|
194
|
-
},
|
|
236
|
+
decapsulate,
|
|
195
237
|
};
|
|
196
238
|
}
|
|
197
239
|
|
|
@@ -216,7 +258,7 @@ export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): KEM {
|
|
|
216
258
|
* const sigLen = signer.lengths.signature;
|
|
217
259
|
* ```
|
|
218
260
|
*/
|
|
219
|
-
export function ecSigner(curve: CurveSign, allowZeroKey: boolean = false): Signer {
|
|
261
|
+
export function ecSigner(curve: CurveSign, allowZeroKey: boolean = false): TRet<Signer> {
|
|
220
262
|
const kg = ecKeygen(curve, allowZeroKey);
|
|
221
263
|
if (!curve.sign || !curve.verify) throw new Error('wrong curve'); // ed25519 doesn't have one!
|
|
222
264
|
return {
|
|
@@ -234,7 +276,7 @@ export function ecSigner(curve: CurveSign, allowZeroKey: boolean = false): Signe
|
|
|
234
276
|
);
|
|
235
277
|
if (opts.context !== undefined)
|
|
236
278
|
throw new Error('ecSigner does not support context; use the underlying curve directly');
|
|
237
|
-
return curve.sign(message, secretKey)
|
|
279
|
+
return curve.sign(message, secretKey) as TRet<Uint8Array>;
|
|
238
280
|
},
|
|
239
281
|
/** Verify one wrapped curve signature.
|
|
240
282
|
* Returns the wrapped curve's `verify()` result for well-formed inputs. Throws on unsupported
|
|
@@ -264,7 +306,7 @@ function splitLengths<K extends string, T extends { lengths: Partial<Record<K, n
|
|
|
264
306
|
}
|
|
265
307
|
|
|
266
308
|
/** Seed-expansion callback used by the hybrid combiners. */
|
|
267
|
-
export type ExpandSeed = (seed: Uint8Array
|
|
309
|
+
export type ExpandSeed = (seed: TArg<Uint8Array>, len: number) => TRet<Uint8Array>;
|
|
268
310
|
type XOF = CHashXOF<any, { dkLen: number }>;
|
|
269
311
|
|
|
270
312
|
// It is XOF for most cases, but can be more complex!
|
|
@@ -282,30 +324,36 @@ type XOF = CHashXOF<any, { dkLen: number }>;
|
|
|
282
324
|
* const seed = expandSeed(new Uint8Array([1]), 4);
|
|
283
325
|
* ```
|
|
284
326
|
*/
|
|
285
|
-
export function expandSeedXof(xof: XOF): ExpandSeed {
|
|
327
|
+
export function expandSeedXof(xof: TArg<XOF>): TRet<ExpandSeed> {
|
|
286
328
|
// Forward the caller seed directly: XOFs are expected to treat inputs as read-only, and this
|
|
287
329
|
// adapter only translates the requested byte length into the hash API's `dkLen` option.
|
|
288
|
-
return (seed: Uint8Array
|
|
330
|
+
return ((seed: TArg<Uint8Array>, seedLen: number): TRet<Uint8Array> =>
|
|
331
|
+
(xof as XOF)(seed, { dkLen: seedLen }) as TRet<Uint8Array>) as TRet<ExpandSeed>;
|
|
289
332
|
}
|
|
290
333
|
|
|
291
334
|
/** Combines public keys, ciphertexts, and shared secrets into one shared secret. */
|
|
292
335
|
export type Combiner = (
|
|
293
|
-
publicKeys: Uint8Array[]
|
|
294
|
-
cipherTexts: Uint8Array[]
|
|
295
|
-
sharedSecrets: Uint8Array[]
|
|
296
|
-
) => Uint8Array
|
|
336
|
+
publicKeys: TArg<Uint8Array[]>,
|
|
337
|
+
cipherTexts: TArg<Uint8Array[]>,
|
|
338
|
+
sharedSecrets: TArg<Uint8Array[]>
|
|
339
|
+
) => TRet<Uint8Array>;
|
|
297
340
|
|
|
298
341
|
function combineKeys(
|
|
299
342
|
realSeedLen: number | undefined, // how much bytes expandSeed expects
|
|
300
|
-
|
|
301
|
-
...
|
|
343
|
+
expandSeed_: TArg<ExpandSeed>,
|
|
344
|
+
...ck_: TArg<CryptoKeys[]>
|
|
302
345
|
) {
|
|
346
|
+
const expandSeed = expandSeed_ as ExpandSeed;
|
|
347
|
+
const ck = ck_ as CryptoKeys[];
|
|
303
348
|
const seedCoder = splitLengths(ck, 'seed');
|
|
304
349
|
const pkCoder = splitLengths(ck, 'publicKey');
|
|
305
350
|
// Allows to use identity functions for combiner/expandSeed
|
|
306
351
|
if (realSeedLen === undefined) realSeedLen = seedCoder.bytesLen;
|
|
307
352
|
anumber(realSeedLen);
|
|
308
|
-
function expandDecapsulationKey(seed: Uint8Array) {
|
|
353
|
+
function expandDecapsulationKey(seed: TArg<Uint8Array>): TRet<{
|
|
354
|
+
secretKey: Uint8Array[];
|
|
355
|
+
publicKey: Uint8Array[];
|
|
356
|
+
}> {
|
|
309
357
|
abytes(seed, realSeedLen!);
|
|
310
358
|
const expandedRaw = expandSeed(seed, seedCoder.bytesLen);
|
|
311
359
|
// Identity/subarray expanders can hand back caller-owned seed storage. Detach those outputs so
|
|
@@ -328,7 +376,10 @@ function combineKeys(
|
|
|
328
376
|
publicKey.push(keys.publicKey);
|
|
329
377
|
}
|
|
330
378
|
ok = true;
|
|
331
|
-
return { secretKey, publicKey }
|
|
379
|
+
return { secretKey, publicKey } as TRet<{
|
|
380
|
+
secretKey: Uint8Array[];
|
|
381
|
+
publicKey: Uint8Array[];
|
|
382
|
+
}>;
|
|
332
383
|
} finally {
|
|
333
384
|
// Child keygen() can throw after deriving only a prefix of the composite key schedule. Keep
|
|
334
385
|
// the exported copies on success, but wipe all temporary and partially built secret material
|
|
@@ -337,25 +388,40 @@ function combineKeys(
|
|
|
337
388
|
if (!ok) cleanBytes(secretKey);
|
|
338
389
|
}
|
|
339
390
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const { publicKey: pk, secretKey } = expandDecapsulationKey(seed);
|
|
391
|
+
// Standalone (not a method) so getPublicKey / destructured usage never depends on `this`.
|
|
392
|
+
const keygen = (seed?: TArg<Uint8Array>) => {
|
|
393
|
+
// Detach the root: the exported secretKey must not alias caller-owned seed bytes, so later
|
|
394
|
+
// caller mutation of the seed cannot silently change the secret key (and vice versa).
|
|
395
|
+
const root = seed === undefined ? randomBytes(realSeedLen!) : copyBytes(seed);
|
|
396
|
+
let res;
|
|
397
|
+
try {
|
|
398
|
+
const { publicKey: pk, secretKey } = expandDecapsulationKey(root);
|
|
349
399
|
try {
|
|
350
|
-
|
|
351
|
-
|
|
400
|
+
res = {
|
|
401
|
+
secretKey: root as TRet<Uint8Array>,
|
|
402
|
+
publicKey: pkCoder.encode(pk) as TRet<Uint8Array>,
|
|
403
|
+
};
|
|
352
404
|
} finally {
|
|
353
|
-
|
|
354
|
-
// The exported secretKey is the caller/root seed itself; child secret keys are internal
|
|
405
|
+
// The exported secretKey is the (detached) root seed; child secret keys are internal
|
|
355
406
|
// expansion outputs that are cleaned whether encoding succeeds or throws.
|
|
356
|
-
cleanBytes(secretKey);
|
|
407
|
+
cleanBytes(pk, secretKey);
|
|
357
408
|
}
|
|
409
|
+
return res;
|
|
410
|
+
} finally {
|
|
411
|
+
if (!res) cleanBytes(root);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
return {
|
|
415
|
+
info: { lengths: { seed: realSeedLen, publicKey: pkCoder.bytesLen, secretKey: realSeedLen } },
|
|
416
|
+
// Composite secret keys are root seeds, so public-key derivation reruns key expansion from
|
|
417
|
+
// that seed instead of decoding a packed child-secret-key structure.
|
|
418
|
+
getPublicKey: (secretKey: TArg<Uint8Array>) => {
|
|
419
|
+
const keys = keygen(secretKey);
|
|
420
|
+
// keygen detaches its exported root; getPublicKey discards that half of the result.
|
|
421
|
+
cleanBytes(keys.secretKey);
|
|
422
|
+
return keys.publicKey as TRet<Uint8Array>;
|
|
358
423
|
},
|
|
424
|
+
keygen,
|
|
359
425
|
expandDecapsulationKey,
|
|
360
426
|
realSeedLen,
|
|
361
427
|
};
|
|
@@ -390,41 +456,54 @@ function combineKeys(
|
|
|
390
456
|
export function combineKEMS(
|
|
391
457
|
realSeedLen: number | undefined, // how much bytes expandSeed expects
|
|
392
458
|
realMsgLen: number | undefined, // how much bytes combiner returns
|
|
393
|
-
expandSeed: ExpandSeed
|
|
394
|
-
combiner: Combiner
|
|
395
|
-
...kems: KEM[]
|
|
396
|
-
): KEM {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
459
|
+
expandSeed: TArg<ExpandSeed>,
|
|
460
|
+
combiner: TArg<Combiner>,
|
|
461
|
+
...kems: TArg<KEM[]>
|
|
462
|
+
): TRet<KEM> {
|
|
463
|
+
if (realSeedLen !== undefined) anumber(realSeedLen, 'realSeedLen');
|
|
464
|
+
if (realMsgLen !== undefined) anumber(realMsgLen, 'realMsgLen');
|
|
465
|
+
if (typeof expandSeed !== 'function')
|
|
466
|
+
throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
|
|
467
|
+
if (typeof combiner !== 'function')
|
|
468
|
+
throw new TypeError('"combiner" expected function, got type=' + typeof combiner);
|
|
469
|
+
const rawCombiner = combiner as Combiner;
|
|
470
|
+
const rawKems = kems as KEM[];
|
|
471
|
+
for (let i = 0; i < rawKems.length; i++) validateKEM(rawKems[i], `kems[${i}]`);
|
|
472
|
+
const keys = combineKeys(realSeedLen, expandSeed, ...rawKems);
|
|
473
|
+
const ctCoder = splitLengths(rawKems, 'cipherText');
|
|
474
|
+
const pkCoder = splitLengths(rawKems, 'publicKey');
|
|
475
|
+
const msgCoder = splitLengths(rawKems, 'msg');
|
|
401
476
|
if (realMsgLen === undefined) realMsgLen = msgCoder.bytesLen;
|
|
402
|
-
anumber(realMsgLen);
|
|
403
|
-
|
|
404
|
-
lengths
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
477
|
+
anumber(realMsgLen, 'realMsgLen');
|
|
478
|
+
const lengths = Object.freeze({
|
|
479
|
+
...keys.info.lengths,
|
|
480
|
+
msg: realMsgLen,
|
|
481
|
+
msgRand: msgCoder.bytesLen,
|
|
482
|
+
cipherText: ctCoder.bytesLen,
|
|
483
|
+
});
|
|
484
|
+
return Object.freeze({
|
|
485
|
+
lengths,
|
|
410
486
|
getPublicKey: keys.getPublicKey,
|
|
411
487
|
keygen: keys.keygen,
|
|
412
|
-
encapsulate(
|
|
488
|
+
encapsulate(
|
|
489
|
+
pk: TArg<Uint8Array>,
|
|
490
|
+
randomness: TArg<Uint8Array> = randomBytes(msgCoder.bytesLen)
|
|
491
|
+
) {
|
|
413
492
|
const pks = pkCoder.decode(pk);
|
|
414
493
|
const rand = msgCoder.decode(randomness);
|
|
415
494
|
const sharedSecret: Uint8Array[] = [];
|
|
416
495
|
const cipherText: Uint8Array[] = [];
|
|
417
496
|
try {
|
|
418
|
-
for (let i = 0; i <
|
|
419
|
-
const enc =
|
|
497
|
+
for (let i = 0; i < rawKems.length; i++) {
|
|
498
|
+
const enc = rawKems[i].encapsulate(pks[i], rand[i]);
|
|
420
499
|
sharedSecret.push(enc.sharedSecret);
|
|
421
500
|
cipherText.push(enc.cipherText);
|
|
422
501
|
}
|
|
423
502
|
return {
|
|
424
503
|
// Detach the combiner result before cleanup: a caller-provided combiner may alias one of
|
|
425
504
|
// the child sharedSecret buffers, and those child buffers are zeroized immediately below.
|
|
426
|
-
sharedSecret: copyBytes(
|
|
427
|
-
cipherText: ctCoder.encode(cipherText)
|
|
505
|
+
sharedSecret: copyBytes(rawCombiner(pks, cipherText, sharedSecret)),
|
|
506
|
+
cipherText: ctCoder.encode(cipherText) as TRet<Uint8Array>,
|
|
428
507
|
};
|
|
429
508
|
} finally {
|
|
430
509
|
// Child encapsulation or combiner failures can happen after some components already
|
|
@@ -432,21 +511,21 @@ export function combineKEMS(
|
|
|
432
511
|
cleanBytes(sharedSecret, cipherText);
|
|
433
512
|
}
|
|
434
513
|
},
|
|
435
|
-
decapsulate(ct: Uint8Array
|
|
514
|
+
decapsulate(ct: TArg<Uint8Array>, seed: TArg<Uint8Array>) {
|
|
436
515
|
const cts = ctCoder.decode(ct);
|
|
437
516
|
const { publicKey, secretKey } = keys.expandDecapsulationKey(seed);
|
|
438
|
-
const sharedSecret =
|
|
517
|
+
const sharedSecret = rawKems.map((i, j) => i.decapsulate(cts[j], secretKey[j]));
|
|
439
518
|
try {
|
|
440
519
|
// Detach the decapsulation result before cleanup: the combiner may hand back one of the
|
|
441
520
|
// child shared-secret buffers, and those temporary buffers are zeroized below.
|
|
442
|
-
return copyBytes(
|
|
521
|
+
return copyBytes(rawCombiner(publicKey, cts, sharedSecret));
|
|
443
522
|
} finally {
|
|
444
523
|
// Decapsulation only needs the expanded child secret keys and child shared secrets for this
|
|
445
524
|
// call; keep the caller/root seed intact, but wipe all derived material even on errors.
|
|
446
525
|
cleanBytes(secretKey, sharedSecret);
|
|
447
526
|
}
|
|
448
527
|
},
|
|
449
|
-
};
|
|
528
|
+
});
|
|
450
529
|
}
|
|
451
530
|
// There is no specs for this, but can be useful
|
|
452
531
|
// realSeedLen: how much bytes expandSeed expects.
|
|
@@ -463,17 +542,26 @@ export function combineKEMS(
|
|
|
463
542
|
* import { combineSigners, expandSeedXof } from '@noble/post-quantum/hybrid.js';
|
|
464
543
|
* import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
|
|
465
544
|
* const hybrid = combineSigners(32, expandSeedXof(shake256), ml_dsa44, ml_dsa44);
|
|
466
|
-
* const
|
|
545
|
+
* const seed = new Uint8Array(hybrid.lengths.seed!).fill(1);
|
|
546
|
+
* const { secretKey, publicKey } = hybrid.keygen(seed);
|
|
547
|
+
* const msg = new TextEncoder().encode('hello noble');
|
|
548
|
+
* const sig = hybrid.sign(msg, secretKey);
|
|
549
|
+
* const isValid = hybrid.verify(sig, msg, publicKey);
|
|
467
550
|
* ```
|
|
468
551
|
*/
|
|
469
552
|
export function combineSigners(
|
|
470
553
|
realSeedLen: number | undefined,
|
|
471
|
-
expandSeed: ExpandSeed
|
|
472
|
-
...signers: Signer[]
|
|
473
|
-
): Signer {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
554
|
+
expandSeed: TArg<ExpandSeed>,
|
|
555
|
+
...signers: TArg<Signer[]>
|
|
556
|
+
): TRet<Signer> {
|
|
557
|
+
if (realSeedLen !== undefined) anumber(realSeedLen, 'realSeedLen');
|
|
558
|
+
if (typeof expandSeed !== 'function')
|
|
559
|
+
throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
|
|
560
|
+
const rawSigners = signers as Signer[];
|
|
561
|
+
for (let i = 0; i < rawSigners.length; i++) validateSigner(rawSigners[i], `signers[${i}]`);
|
|
562
|
+
const keys = combineKeys(realSeedLen, expandSeed, ...rawSigners);
|
|
563
|
+
const sigCoder = splitLengths(rawSigners, 'signature');
|
|
564
|
+
const pkCoder = splitLengths(rawSigners, 'publicKey');
|
|
477
565
|
return {
|
|
478
566
|
lengths: { ...keys.info.lengths, signature: sigCoder.bytesLen, signRand: 0 },
|
|
479
567
|
getPublicKey: keys.getPublicKey,
|
|
@@ -493,8 +581,8 @@ export function combineSigners(
|
|
|
493
581
|
);
|
|
494
582
|
const { secretKey } = keys.expandDecapsulationKey(seed);
|
|
495
583
|
try {
|
|
496
|
-
const sigs =
|
|
497
|
-
return sigCoder.encode(sigs)
|
|
584
|
+
const sigs = rawSigners.map((i, j) => i.sign(message, secretKey[j]));
|
|
585
|
+
return sigCoder.encode(sigs) as TRet<Uint8Array>;
|
|
498
586
|
} finally {
|
|
499
587
|
// Composite secret keys are root seeds; the per-signer child secret keys are temporary
|
|
500
588
|
// expansion outputs and must not stay live after the combined signature is produced.
|
|
@@ -502,8 +590,8 @@ export function combineSigners(
|
|
|
502
590
|
}
|
|
503
591
|
},
|
|
504
592
|
/** Verify one combined signature.
|
|
505
|
-
*
|
|
506
|
-
*
|
|
593
|
+
* Wrong-length aggregate signatures return `false` (matching ml-dsa / slh-dsa behavior), as
|
|
594
|
+
* does any failing child verify. Throws on unsupported generic opts or malformed publicKey.
|
|
507
595
|
*/
|
|
508
596
|
verify: (signature, message, publicKey, opts = {}) => {
|
|
509
597
|
validateVerOpts(opts);
|
|
@@ -511,10 +599,16 @@ export function combineSigners(
|
|
|
511
599
|
throw new Error(
|
|
512
600
|
'combineSigners does not support context; use the underlying signer directly'
|
|
513
601
|
);
|
|
602
|
+
// Malformed signature *length* is a verification failure, not a thrown type error —
|
|
603
|
+
// consistent with ml-dsa / slh-dsa. Must run before sigCoder.decode, which throws.
|
|
604
|
+
// Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
|
|
605
|
+
abytes(signature, undefined, 'signature');
|
|
606
|
+
// A signature failure must not hide malformed aggregate public-key bytes.
|
|
514
607
|
const pks = pkCoder.decode(publicKey);
|
|
608
|
+
if (signature.length !== sigCoder.bytesLen) return false;
|
|
515
609
|
const sigs = sigCoder.decode(signature);
|
|
516
|
-
for (let i = 0; i <
|
|
517
|
-
if (!
|
|
610
|
+
for (let i = 0; i < rawSigners.length; i++) {
|
|
611
|
+
if (!rawSigners[i].verify(sigs[i], message, pks[i])) return false;
|
|
518
612
|
}
|
|
519
613
|
return true;
|
|
520
614
|
},
|
|
@@ -545,21 +639,35 @@ export function combineSigners(
|
|
|
545
639
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
546
640
|
* ```
|
|
547
641
|
*/
|
|
548
|
-
export function QSF(
|
|
642
|
+
export function QSF(
|
|
643
|
+
label: string,
|
|
644
|
+
pqc: TArg<KEM>,
|
|
645
|
+
curveKEM: TArg<KEM>,
|
|
646
|
+
xof: TArg<XOF>,
|
|
647
|
+
kdf: CHash
|
|
648
|
+
): TRet<KEM> {
|
|
649
|
+
astring(label, 'label');
|
|
650
|
+
validateKEM(pqc, 'pqc');
|
|
651
|
+
validateKEM(curveKEM, 'curveKEM');
|
|
652
|
+
if (typeof xof !== 'function' || typeof (xof as any).create !== 'function')
|
|
653
|
+
throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
|
|
549
654
|
ahash(xof);
|
|
655
|
+
if (typeof kdf !== 'function' || typeof (kdf as any).create !== 'function')
|
|
656
|
+
throw new TypeError('"kdf" expected hash function, got type=' + typeof kdf);
|
|
550
657
|
ahash(kdf);
|
|
551
658
|
return combineKEMS(
|
|
552
659
|
32,
|
|
553
660
|
kdf.outputLen,
|
|
554
661
|
expandSeedXof(xof),
|
|
555
|
-
(pk
|
|
662
|
+
(pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) =>
|
|
663
|
+
kdf(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
|
|
556
664
|
pqc,
|
|
557
665
|
curveKEM
|
|
558
666
|
);
|
|
559
667
|
}
|
|
560
668
|
|
|
561
669
|
/** QSF preset combining ML-KEM-768 with P-256. */
|
|
562
|
-
export const QSF_ml_kem768_p256: KEM = /* @__PURE__ */ (() =>
|
|
670
|
+
export const QSF_ml_kem768_p256: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
563
671
|
QSF(
|
|
564
672
|
'QSF-KEM(ML-KEM-768,P-256)-XOF(SHAKE256)-KDF(SHA3-256)',
|
|
565
673
|
ml_kem768,
|
|
@@ -568,7 +676,7 @@ export const QSF_ml_kem768_p256: KEM = /* @__PURE__ */ (() =>
|
|
|
568
676
|
sha3_256
|
|
569
677
|
))();
|
|
570
678
|
/** QSF preset combining ML-KEM-1024 with P-384. */
|
|
571
|
-
export const QSF_ml_kem1024_p384: KEM = /* @__PURE__ */ (() =>
|
|
679
|
+
export const QSF_ml_kem1024_p384: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
572
680
|
QSF(
|
|
573
681
|
'QSF-KEM(ML-KEM-1024,P-384)-XOF(SHAKE256)-KDF(SHA3-256)',
|
|
574
682
|
ml_kem1024,
|
|
@@ -605,18 +713,25 @@ export const QSF_ml_kem1024_p384: KEM = /* @__PURE__ */ (() =>
|
|
|
605
713
|
*/
|
|
606
714
|
export function createKitchenSink(
|
|
607
715
|
label: string,
|
|
608
|
-
pqc: KEM
|
|
609
|
-
curveKEM: KEM
|
|
610
|
-
xof: XOF
|
|
716
|
+
pqc: TArg<KEM>,
|
|
717
|
+
curveKEM: TArg<KEM>,
|
|
718
|
+
xof: TArg<XOF>,
|
|
611
719
|
hash: CHash
|
|
612
|
-
): KEM {
|
|
720
|
+
): TRet<KEM> {
|
|
721
|
+
astring(label, 'label');
|
|
722
|
+
validateKEM(pqc, 'pqc');
|
|
723
|
+
validateKEM(curveKEM, 'curveKEM');
|
|
724
|
+
if (typeof xof !== 'function' || typeof (xof as any).create !== 'function')
|
|
725
|
+
throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
|
|
613
726
|
ahash(xof);
|
|
727
|
+
if (typeof hash !== 'function' || typeof (hash as any).create !== 'function')
|
|
728
|
+
throw new TypeError('"hash" expected hash function, got type=' + typeof hash);
|
|
614
729
|
ahash(hash);
|
|
615
730
|
return combineKEMS(
|
|
616
731
|
32,
|
|
617
732
|
32,
|
|
618
733
|
expandSeedXof(xof),
|
|
619
|
-
(pk
|
|
734
|
+
(pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) => {
|
|
620
735
|
const preimage = concatBytes(ss[0], ss[1], ct[0], pk[0], ct[1], pk[1], asciiToBytes(label));
|
|
621
736
|
const len = 32;
|
|
622
737
|
const ikm = concatBytes(asciiToBytes('hybrid_prk'), preimage);
|
|
@@ -641,7 +756,7 @@ const x25519kem = /* @__PURE__ */ ecdhKem(x25519);
|
|
|
641
756
|
/** KitchenSink preset combining ML-KEM-768 with X25519.
|
|
642
757
|
* Caller randomness splits into 32 ML-KEM coins plus a 32-byte X25519 ephemeral-secret seed.
|
|
643
758
|
*/
|
|
644
|
-
export const KitchenSink_ml_kem768_x25519: KEM = /* @__PURE__ */ (() =>
|
|
759
|
+
export const KitchenSink_ml_kem768_x25519: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
645
760
|
createKitchenSink(
|
|
646
761
|
'KitchenSink-KEM(ML-KEM-768,X25519)-XOF(SHAKE256)-KDF(HKDF-SHA-256)',
|
|
647
762
|
ml_kem768,
|
|
@@ -655,13 +770,14 @@ export const KitchenSink_ml_kem768_x25519: KEM = /* @__PURE__ */ (() =>
|
|
|
655
770
|
* Uses the hard-coded domain-separation label `\\.//^\\` and hashes only `ct1 || pk1`
|
|
656
771
|
* from the X25519 side in addition to the two component shared secrets.
|
|
657
772
|
*/
|
|
658
|
-
export const ml_kem768_x25519: KEM = /* @__PURE__ */ (() =>
|
|
773
|
+
export const ml_kem768_x25519: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
659
774
|
combineKEMS(
|
|
660
775
|
32,
|
|
661
776
|
32,
|
|
662
777
|
expandSeedXof(shake256),
|
|
663
778
|
// Awesome label, so much escaping hell in a single line.
|
|
664
|
-
(pk
|
|
779
|
+
(pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) =>
|
|
780
|
+
sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes('\\.//^\\'))),
|
|
665
781
|
ml_kem768,
|
|
666
782
|
x25519kem
|
|
667
783
|
))();
|
|
@@ -674,12 +790,15 @@ export const ml_kem768_x25519: KEM = /* @__PURE__ */ (() =>
|
|
|
674
790
|
* prefix, not the SEC 1 `x_P`-only primitive output, because current hybrid combiners hash
|
|
675
791
|
* both coordinates.
|
|
676
792
|
*/
|
|
677
|
-
function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: number): KEM {
|
|
793
|
+
function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: number): TRet<KEM> {
|
|
678
794
|
const Fn = curve.Point.Fn;
|
|
679
795
|
if (!Fn) throw new Error('no Point.Fn');
|
|
680
796
|
// Scan scalar-sized windows until one decodes to a nonzero scalar in `[1, n-1]`; if every
|
|
681
797
|
// window is zero or out of range, fail instead of silently reducing modulo `n`.
|
|
682
|
-
function rejectionSampling(seed: Uint8Array): {
|
|
798
|
+
function rejectionSampling(seed: TArg<Uint8Array>): TRet<{
|
|
799
|
+
secretKey: Uint8Array;
|
|
800
|
+
publicKey: Uint8Array;
|
|
801
|
+
}> {
|
|
683
802
|
let sk: bigint;
|
|
684
803
|
for (let start = 0, end = scalarLen; ; start = end, end += scalarLen) {
|
|
685
804
|
if (end > seed.length) throw new Error('rejection sampling failed');
|
|
@@ -688,9 +807,17 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
|
|
|
688
807
|
}
|
|
689
808
|
const secretKey = Fn.toBytes(Fn.create(sk));
|
|
690
809
|
const publicKey = curve.getPublicKey(secretKey, false);
|
|
691
|
-
return { secretKey, publicKey }
|
|
810
|
+
return { secretKey, publicKey } as TRet<{
|
|
811
|
+
secretKey: Uint8Array;
|
|
812
|
+
publicKey: Uint8Array;
|
|
813
|
+
}>;
|
|
692
814
|
}
|
|
693
815
|
|
|
816
|
+
// Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
|
|
817
|
+
const decapsulate = (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>) => {
|
|
818
|
+
const full = curve.getSharedSecret(secretKey, cipherText);
|
|
819
|
+
return full.subarray(1) as TRet<Uint8Array>;
|
|
820
|
+
};
|
|
694
821
|
return {
|
|
695
822
|
lengths: {
|
|
696
823
|
secretKey: scalarLen,
|
|
@@ -699,20 +826,20 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
|
|
|
699
826
|
msg: nseed,
|
|
700
827
|
cipherText: elemLen,
|
|
701
828
|
},
|
|
702
|
-
keygen(seed: Uint8Array = randomBytes(nseed)) {
|
|
829
|
+
keygen(seed: TArg<Uint8Array> = randomBytes(nseed)) {
|
|
703
830
|
abytes(seed, nseed, 'seed');
|
|
704
831
|
return rejectionSampling(seed);
|
|
705
832
|
},
|
|
706
|
-
getPublicKey(secretKey: Uint8Array) {
|
|
707
|
-
return curve.getPublicKey(secretKey, false)
|
|
833
|
+
getPublicKey(secretKey: TArg<Uint8Array>) {
|
|
834
|
+
return curve.getPublicKey(secretKey, false) as TRet<Uint8Array>;
|
|
708
835
|
},
|
|
709
|
-
encapsulate(publicKey: Uint8Array
|
|
836
|
+
encapsulate(publicKey: TArg<Uint8Array>, rand: TArg<Uint8Array> = randomBytes(nseed)) {
|
|
710
837
|
abytes(rand, nseed, 'rand');
|
|
711
838
|
let ek: Uint8Array | undefined = undefined;
|
|
712
839
|
try {
|
|
713
840
|
ek = rejectionSampling(rand).secretKey;
|
|
714
|
-
const sharedSecret =
|
|
715
|
-
const cipherText = curve.getPublicKey(ek, false)
|
|
841
|
+
const sharedSecret = decapsulate(publicKey, ek);
|
|
842
|
+
const cipherText = curve.getPublicKey(ek, false) as TRet<Uint8Array>;
|
|
716
843
|
return { sharedSecret, cipherText };
|
|
717
844
|
} finally {
|
|
718
845
|
// Rejection-sampled NIST-curve ephemeral secret keys are temporary encapsulation state and
|
|
@@ -720,10 +847,7 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
|
|
|
720
847
|
if (ek) cleanBytes(ek);
|
|
721
848
|
}
|
|
722
849
|
},
|
|
723
|
-
decapsulate
|
|
724
|
-
const full = curve.getSharedSecret(secretKey, cipherText);
|
|
725
|
-
return full.subarray(1);
|
|
726
|
-
},
|
|
850
|
+
decapsulate,
|
|
727
851
|
};
|
|
728
852
|
}
|
|
729
853
|
|
|
@@ -735,7 +859,12 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
|
|
|
735
859
|
* `shake256(seed, { dkLen: 64 + nseed })`,
|
|
736
860
|
* and the combiner hashes `ss0 || ss1 || ct1 || pk1 || label`.
|
|
737
861
|
*/
|
|
738
|
-
function concreteHybridKem(
|
|
862
|
+
function concreteHybridKem(
|
|
863
|
+
label: string,
|
|
864
|
+
mlkem: TArg<KEM>,
|
|
865
|
+
curve: ECDSA,
|
|
866
|
+
nseed: number
|
|
867
|
+
): TRet<KEM> {
|
|
739
868
|
const { secretKey: scalarLen, publicKeyUncompressed: elemLen } = curve.lengths;
|
|
740
869
|
if (!scalarLen || !elemLen) throw new Error('wrong curve');
|
|
741
870
|
const curveKem = nistCurveKem(curve, scalarLen, elemLen, nseed);
|
|
@@ -745,40 +874,25 @@ function concreteHybridKem(label: string, mlkem: KEM, curve: ECDSA, nseed: numbe
|
|
|
745
874
|
return combineKEMS(
|
|
746
875
|
32,
|
|
747
876
|
32,
|
|
748
|
-
(seed: Uint8Array) => {
|
|
877
|
+
(seed: TArg<Uint8Array>): TRet<Uint8Array> => {
|
|
749
878
|
abytes(seed, 32);
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
879
|
+
// One SHAKE256 stream split by the seed coder as mlkemSeed (64) || curveSeed (nseed).
|
|
880
|
+
// Returned directly: the previous concatBytes of two adjacent subarrays produced an
|
|
881
|
+
// identical copy while leaving this original buffer unwiped; expandDecapsulationKey
|
|
882
|
+
// wipes the returned buffer after the child seeds are copied out.
|
|
883
|
+
return shake256(seed, { dkLen: totalSeedLen }) as TRet<Uint8Array>;
|
|
754
884
|
},
|
|
755
|
-
(pk
|
|
885
|
+
(pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) =>
|
|
886
|
+
sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
|
|
756
887
|
mlkem,
|
|
757
888
|
curveKem
|
|
758
889
|
);
|
|
759
890
|
}
|
|
760
891
|
|
|
761
892
|
/** P-256 + ML-KEM-768 hybrid preset. */
|
|
762
|
-
export const ml_kem768_p256: KEM = /* @__PURE__ */ (() =>
|
|
893
|
+
export const ml_kem768_p256: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
763
894
|
concreteHybridKem('MLKEM768-P256', ml_kem768, p256, 128))();
|
|
764
895
|
|
|
765
896
|
/** P-384 + ML-KEM-1024 hybrid preset. */
|
|
766
|
-
export const ml_kem1024_p384: KEM = /* @__PURE__ */ (() =>
|
|
897
|
+
export const ml_kem1024_p384: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
767
898
|
concreteHybridKem('MLKEM1024-P384', ml_kem1024, p384, 48))();
|
|
768
|
-
|
|
769
|
-
// Legacy aliases
|
|
770
|
-
/** Legacy alias for `ml_kem768_x25519`. */
|
|
771
|
-
export const XWing: KEM = /* @__PURE__ */ (() => ml_kem768_x25519)();
|
|
772
|
-
/** Legacy alias for `ml_kem768_x25519`. */
|
|
773
|
-
export const MLKEM768X25519: KEM = /* @__PURE__ */ (() => ml_kem768_x25519)();
|
|
774
|
-
/** Legacy alias for `ml_kem768_p256`. */
|
|
775
|
-
export const MLKEM768P256: KEM = /* @__PURE__ */ (() => ml_kem768_p256)();
|
|
776
|
-
/** Legacy alias for `ml_kem1024_p384`. */
|
|
777
|
-
export const MLKEM1024P384: KEM = /* @__PURE__ */ (() => ml_kem1024_p384)();
|
|
778
|
-
/** Legacy alias for `QSF_ml_kem768_p256`. */
|
|
779
|
-
export const QSFMLKEM768P256: KEM = /* @__PURE__ */ (() => QSF_ml_kem768_p256)();
|
|
780
|
-
/** Legacy alias for `QSF_ml_kem1024_p384`. */
|
|
781
|
-
export const QSFMLKEM1024P384: KEM = /* @__PURE__ */ (() => QSF_ml_kem1024_p384)();
|
|
782
|
-
/** Legacy alias for `KitchenSink_ml_kem768_x25519`. */
|
|
783
|
-
export const KitchenSinkMLKEM768X25519: KEM = /* @__PURE__ */ (() =>
|
|
784
|
-
KitchenSink_ml_kem768_x25519)();
|