@noble/post-quantum 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -16
- package/_crystals.js +1 -1
- package/falcon.d.ts +1 -1
- package/falcon.js +121 -62
- package/hybrid.d.ts +33 -12
- package/hybrid.js +100 -41
- package/index.js +1 -1
- package/ml-dsa.d.ts +3 -3
- package/ml-dsa.js +57 -20
- package/ml-kem.js +85 -34
- package/package.json +15 -11
- package/slh-dsa.js +34 -11
- package/src/_crystals.ts +1 -1
- package/src/falcon.ts +127 -70
- package/src/hybrid.ts +108 -39
- package/src/index.ts +1 -1
- package/src/ml-dsa.ts +70 -25
- package/src/ml-kem.ts +76 -35
- package/src/slh-dsa.ts +44 -13
- package/src/utils.ts +115 -10
- package/src/webcrypto.ts +322 -0
- package/utils.d.ts +36 -2
- package/utils.js +105 -12
- package/webcrypto.d.ts +91 -0
- package/webcrypto.js +213 -0
package/ml-kem.js
CHANGED
|
@@ -296,8 +296,11 @@ const genKPKE = (opts_) => {
|
|
|
296
296
|
for (let i = 0; i < K; i++)
|
|
297
297
|
polyAdd(tmp, MultiplyNTTs(sk[i], crystals.NTT.encode(u[i])));
|
|
298
298
|
polySub(v, crystals.NTT.decode(tmp)); // w = v' - tmp
|
|
299
|
-
|
|
300
|
-
|
|
299
|
+
// `v` now holds w, from which the plaintext is just a 1-bit threshold away, so wipe it too.
|
|
300
|
+
// encode() allocates its own buffer, so the returned bytes do not alias `v`.
|
|
301
|
+
const res = poly1.encode(v);
|
|
302
|
+
cleanBytes(tmp, sk, u, v);
|
|
303
|
+
return res;
|
|
301
304
|
},
|
|
302
305
|
};
|
|
303
306
|
};
|
|
@@ -305,6 +308,11 @@ const genKPKE = (opts_) => {
|
|
|
305
308
|
* Public ML-KEM wrapper over the internal K-PKE subroutine.
|
|
306
309
|
* `keygen(seed)` and `encapsulate(publicKey, msg)` are deterministic/test-oriented hooks that map
|
|
307
310
|
* more directly to Algorithms 16-17 than to the pure no-input / random-internal Algorithms 19-20.
|
|
311
|
+
* `encapsulate`'s optional `msg` is the 32-byte message randomness `m` of Algorithm 17, the
|
|
312
|
+
* pre-image the shared secret is derived from, NOT a plaintext to encrypt: ML-KEM is a key
|
|
313
|
+
* encapsulation mechanism, not a cipher. Omit it to draw fresh randomness; pass it only to
|
|
314
|
+
* reproduce a known-answer vector, and only as 32 uniformly random bytes, since a low-entropy or
|
|
315
|
+
* reused value makes the shared secret predictable. The same holds for `keygen`'s optional `seed`.
|
|
308
316
|
* decapsulate() tries to follow the Algorithms 18/21 implicit-reject structure as closely as
|
|
309
317
|
* practical here by re-encrypting, comparing ciphertexts, returning `Khat` on match or `Kbar` on
|
|
310
318
|
* mismatch, and zeroizing the non-returned shared-secret candidate; JS/JIT still provides no
|
|
@@ -340,34 +348,65 @@ function createKyber(opts) {
|
|
|
340
348
|
return Object.freeze({
|
|
341
349
|
info: Object.freeze({ type: 'ml-kem' }),
|
|
342
350
|
lengths: kemLengths,
|
|
343
|
-
keygen: (seed
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
keygen: (seed) => {
|
|
352
|
+
// A generated seed carries z (the implicit-rejection secret) and must be wiped once the
|
|
353
|
+
// secret key holds a copy, matching ml-dsa / slh-dsa / falcon keygen. A caller-supplied
|
|
354
|
+
// seed is the caller's to manage (and the immutability test requires it stay untouched).
|
|
355
|
+
const ownSeed = seed === undefined;
|
|
356
|
+
const s = ownSeed ? randomBytes(seedLen) : seed;
|
|
357
|
+
let sk;
|
|
358
|
+
let publicKeyHash;
|
|
359
|
+
try {
|
|
360
|
+
abytes(s, seedLen, 'seed');
|
|
361
|
+
const keys = KPKE.keygen(s.subarray(0, 32));
|
|
362
|
+
const publicKey = keys.publicKey;
|
|
363
|
+
sk = keys.secretKey;
|
|
364
|
+
publicKeyHash = HASH256(publicKey);
|
|
365
|
+
// (dkPKE||ek||H(ek)||z)
|
|
366
|
+
const secretKey = secretCoder.encode([sk, publicKey, publicKeyHash, s.subarray(32)]);
|
|
367
|
+
return {
|
|
368
|
+
publicKey: publicKey,
|
|
369
|
+
secretKey: secretKey,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
if (sk !== undefined)
|
|
374
|
+
cleanBytes(sk);
|
|
375
|
+
if (publicKeyHash !== undefined)
|
|
376
|
+
cleanBytes(publicKeyHash);
|
|
377
|
+
if (ownSeed)
|
|
378
|
+
cleanBytes(s);
|
|
379
|
+
}
|
|
354
380
|
},
|
|
355
381
|
getPublicKey: (secretKey) => {
|
|
356
382
|
const [_sk, publicKey, _publicKeyHash, _z] = secretCoder.decode(secretKey);
|
|
357
383
|
return Uint8Array.from(publicKey);
|
|
358
384
|
},
|
|
359
|
-
encapsulate: (publicKey, msg
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
385
|
+
encapsulate: (publicKey, msg) => {
|
|
386
|
+
// A generated message is the preimage of the shared secret (K = G(m || H(ek))[0:32]) and
|
|
387
|
+
// must be wiped. A caller-supplied message is the deterministic-randomness hook and the
|
|
388
|
+
// caller's to manage (the immutability test requires it stay untouched).
|
|
389
|
+
const ownMsg = msg === undefined;
|
|
390
|
+
const m = ownMsg ? randomBytes(msgLen) : msg;
|
|
391
|
+
let kr;
|
|
392
|
+
try {
|
|
393
|
+
abytes(publicKey, lengths.publicKey, 'publicKey');
|
|
394
|
+
abytes(m, msgLen, 'message');
|
|
395
|
+
validateModulus(publicKey, 'encapsulate');
|
|
396
|
+
// derive randomness
|
|
397
|
+
kr = HASH512.create().update(m).update(HASH256(publicKey)).digest();
|
|
398
|
+
const cipherText = KPKE.encrypt(publicKey, m, kr.subarray(32, 64));
|
|
399
|
+
return {
|
|
400
|
+
cipherText: cipherText,
|
|
401
|
+
sharedSecret: kr.subarray(0, 32),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
finally {
|
|
405
|
+
if (kr !== undefined)
|
|
406
|
+
cleanBytes(kr.subarray(32));
|
|
407
|
+
if (ownMsg)
|
|
408
|
+
cleanBytes(m);
|
|
409
|
+
}
|
|
371
410
|
},
|
|
372
411
|
decapsulate: (cipherText, secretKey) => {
|
|
373
412
|
abytes(secretKey, secretCoder.bytesLen, 'secretKey'); // 768*k + 96
|
|
@@ -406,15 +445,27 @@ function createKyber(opts) {
|
|
|
406
445
|
const cached = KPKE.prepare(ek);
|
|
407
446
|
return Object.freeze({
|
|
408
447
|
publicKey: ek,
|
|
409
|
-
encapsulate: (msg
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
448
|
+
encapsulate: (msg) => {
|
|
449
|
+
// As in the non-prepared encapsulate: a generated message is the shared-secret
|
|
450
|
+
// preimage and is wiped; a caller-supplied one is left untouched.
|
|
451
|
+
const ownMsg = msg === undefined;
|
|
452
|
+
const m = ownMsg ? randomBytes(msgLen) : msg;
|
|
453
|
+
let kr;
|
|
454
|
+
try {
|
|
455
|
+
abytes(m, msgLen, 'message');
|
|
456
|
+
kr = HASH512.create().update(m).update(publicKeyHash).digest();
|
|
457
|
+
const cipherText = cached.encrypt(m, kr.subarray(32, 64));
|
|
458
|
+
return {
|
|
459
|
+
cipherText: cipherText,
|
|
460
|
+
sharedSecret: kr.subarray(0, 32),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
finally {
|
|
464
|
+
if (kr !== undefined)
|
|
465
|
+
cleanBytes(kr.subarray(32));
|
|
466
|
+
if (ownMsg)
|
|
467
|
+
cleanBytes(m);
|
|
468
|
+
}
|
|
418
469
|
},
|
|
419
470
|
decapsulate: (cipherText, secretKey) => {
|
|
420
471
|
abytes(secretKey, secretCoder.bytesLen, 'secretKey');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noble/post-quantum",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Auditable & minimal JS implementation of post-quantum cryptography: FIPS 203, 204, 205, Falcon",
|
|
5
5
|
"files": [
|
|
6
6
|
"*.js",
|
|
@@ -8,25 +8,28 @@
|
|
|
8
8
|
"src"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@noble/ciphers": "
|
|
12
|
-
"@noble/curves": "
|
|
13
|
-
"@noble/hashes": "
|
|
11
|
+
"@noble/ciphers": "2.4.0",
|
|
12
|
+
"@noble/curves": "2.4.0",
|
|
13
|
+
"@noble/hashes": "2.4.0"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@paulmillr/jsbt": "0.
|
|
17
|
-
"
|
|
16
|
+
"@paulmillr/jsbt": "0.7.1",
|
|
17
|
+
"bismar": "0.1.8",
|
|
18
|
+
"@types/node": "26.2.0",
|
|
18
19
|
"fast-check": "4.2.0",
|
|
19
|
-
"prettier": "3.6
|
|
20
|
-
"typescript": "6.0.
|
|
20
|
+
"prettier": "3.9.6",
|
|
21
|
+
"typescript": "6.0.3"
|
|
21
22
|
},
|
|
22
23
|
"scripts": {
|
|
23
24
|
"benchmark": "node benchmark/pq.ts",
|
|
24
|
-
"benchmark:size": "
|
|
25
|
+
"benchmark:size": "bismar -bsm",
|
|
25
26
|
"build": "tsc",
|
|
26
27
|
"check": "jsbt-check",
|
|
27
28
|
"build:clean": "rm *.{js,d.ts} 2> /dev/null",
|
|
28
29
|
"format": "prettier --write 'src/**/*.{js,ts}' 'test/**/*.{js,ts,mjs}'",
|
|
29
|
-
"test": "node test/index.ts",
|
|
30
|
+
"test": "node --no-warnings test/index.ts",
|
|
31
|
+
"test:bun": "bun test/index.ts",
|
|
32
|
+
"test:deno": "deno --allow-env --allow-read test/index.ts",
|
|
30
33
|
"test:slow": "SLOW_TESTS=1 node test/index.ts"
|
|
31
34
|
},
|
|
32
35
|
"exports": {
|
|
@@ -37,7 +40,8 @@
|
|
|
37
40
|
"./ml-dsa.js": "./ml-dsa.js",
|
|
38
41
|
"./ml-kem.js": "./ml-kem.js",
|
|
39
42
|
"./slh-dsa.js": "./slh-dsa.js",
|
|
40
|
-
"./utils.js": "./utils.js"
|
|
43
|
+
"./utils.js": "./utils.js",
|
|
44
|
+
"./webcrypto.js": "./webcrypto.js"
|
|
41
45
|
},
|
|
42
46
|
"engines": {
|
|
43
47
|
"node": ">= 20.19.0"
|
package/slh-dsa.js
CHANGED
|
@@ -33,6 +33,12 @@ import { sha256, sha512 } from '@noble/hashes/sha2.js';
|
|
|
33
33
|
import { shake256 } from '@noble/hashes/sha3.js';
|
|
34
34
|
import { concatBytes, createView } from '@noble/hashes/utils.js';
|
|
35
35
|
import { abytes, checkHash, cleanBytes, copyBytes, equalBytes, getMask, getMessage, getMessagePrehash, randomBytes, splitCoder, validateSigOpts, validateVerOpts, vecCoder, } from "./utils.js";
|
|
36
|
+
// Keys the internal SLH-DSA surface accepts. `context` is deliberately absent: the public
|
|
37
|
+
// wrappers consume it when they format M' and must not forward it, because a key that is
|
|
38
|
+
// accepted and then never read is the same silent downgrade this validation exists to prevent.
|
|
39
|
+
// `extraEntropy` is signing-only, so verification (which takes no options of its own) has none.
|
|
40
|
+
const INTERNAL_SIG_OPT_KEYS = /* @__PURE__ */ Object.freeze(['extraEntropy']);
|
|
41
|
+
const INTERNAL_VER_OPT_KEYS = /* @__PURE__ */ Object.freeze([]);
|
|
36
42
|
/** Winternitz signature params. */
|
|
37
43
|
/**
|
|
38
44
|
* Built-in SLH-DSA Table 2 subset keyed by strength/profile.
|
|
@@ -128,8 +134,18 @@ function gen(opts, hashOpts_) {
|
|
|
128
134
|
// `height` / `chain` and `index` / `hash` share the same spec words, so callers must use the
|
|
129
135
|
// address-type-specific combinations instead of mixing both meanings in one call.
|
|
130
136
|
const setAddr = (opts, addr = new Uint8Array(ADDR_BYTES)) => {
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
// These objects are created in hot internal loops, so avoid cloning them. Read only own fields:
|
|
138
|
+
// absent address words must stay absent even if Object.prototype was polluted.
|
|
139
|
+
const type = Object.hasOwn(opts, 'type') ? opts.type : undefined;
|
|
140
|
+
const height = Object.hasOwn(opts, 'height') ? opts.height : undefined;
|
|
141
|
+
const tree = Object.hasOwn(opts, 'tree') ? opts.tree : undefined;
|
|
142
|
+
const layer = Object.hasOwn(opts, 'layer') ? opts.layer : undefined;
|
|
143
|
+
const index = Object.hasOwn(opts, 'index') ? opts.index : undefined;
|
|
144
|
+
const chain = Object.hasOwn(opts, 'chain') ? opts.chain : undefined;
|
|
145
|
+
const hash = Object.hasOwn(opts, 'hash') ? opts.hash : undefined;
|
|
146
|
+
const keypair = Object.hasOwn(opts, 'keypair') ? opts.keypair : undefined;
|
|
147
|
+
const subtreeAddr = Object.hasOwn(opts, 'subtreeAddr') ? opts.subtreeAddr : undefined;
|
|
148
|
+
const keypairAddr = Object.hasOwn(opts, 'keypairAddr') ? opts.keypairAddr : undefined;
|
|
133
149
|
if (height !== undefined)
|
|
134
150
|
addr[OFFSET_CHAIN_ADDR] = height;
|
|
135
151
|
if (layer !== undefined)
|
|
@@ -361,7 +377,7 @@ function gen(opts, hashOpts_) {
|
|
|
361
377
|
return Uint8Array.from(pk);
|
|
362
378
|
},
|
|
363
379
|
sign: (msg, sk, opts = {}) => {
|
|
364
|
-
validateSigOpts(opts);
|
|
380
|
+
opts = validateSigOpts(opts, INTERNAL_SIG_OPT_KEYS);
|
|
365
381
|
let { extraEntropy: random } = opts;
|
|
366
382
|
const [skSeed, skPRF, pk] = secretCoder.decode(sk); // todo: fix
|
|
367
383
|
const [pkSeed, _] = publicCoder.decode(pk);
|
|
@@ -427,7 +443,11 @@ function gen(opts, hashOpts_) {
|
|
|
427
443
|
cleanBytes(R, random, treeAddr, wotsAddr, forsLeaf, forsTreeAddr, indices, roots);
|
|
428
444
|
return SIG;
|
|
429
445
|
},
|
|
430
|
-
verify: (sig, msg, publicKey) => {
|
|
446
|
+
verify: (sig, msg, publicKey, opts = {}) => {
|
|
447
|
+
// The internal verify reads no options; reject any so a stray key (e.g. a caller
|
|
448
|
+
// mistaking this for the public verify and passing `context`) is reported rather than
|
|
449
|
+
// silently swallowed by this function's arity.
|
|
450
|
+
validateVerOpts(opts, INTERNAL_VER_OPT_KEYS);
|
|
431
451
|
const [pkSeed, pubRoot] = publicCoder.decode(publicKey);
|
|
432
452
|
const pk = publicKey;
|
|
433
453
|
// FIPS 205 Algorithm 20 step 1: wrong-length signatures return false instead of throwing
|
|
@@ -503,14 +523,16 @@ function gen(opts, hashOpts_) {
|
|
|
503
523
|
keygen: internal.keygen,
|
|
504
524
|
getPublicKey: internal.getPublicKey,
|
|
505
525
|
sign: (msg, secretKey, opts = {}) => {
|
|
506
|
-
validateSigOpts(opts);
|
|
526
|
+
opts = validateSigOpts(opts);
|
|
507
527
|
const M = getMessage(msg, opts.context);
|
|
508
|
-
|
|
528
|
+
// `context` is consumed by getMessage() above; forwarding it would make the internal
|
|
529
|
+
// surface accept a key it never reads.
|
|
530
|
+
const res = internal.sign(M, secretKey, { extraEntropy: opts.extraEntropy });
|
|
509
531
|
cleanBytes(M);
|
|
510
532
|
return res;
|
|
511
533
|
},
|
|
512
534
|
verify: (sig, msg, publicKey, opts = {}) => {
|
|
513
|
-
validateVerOpts(opts);
|
|
535
|
+
opts = validateVerOpts(opts);
|
|
514
536
|
return internal.verify(sig, getMessage(msg, opts.context), publicKey);
|
|
515
537
|
},
|
|
516
538
|
prehash: (hash) => {
|
|
@@ -522,14 +544,15 @@ function gen(opts, hashOpts_) {
|
|
|
522
544
|
keygen: internal.keygen,
|
|
523
545
|
getPublicKey: internal.getPublicKey,
|
|
524
546
|
sign: (msg, secretKey, opts = {}) => {
|
|
525
|
-
validateSigOpts(opts);
|
|
547
|
+
opts = validateSigOpts(opts);
|
|
526
548
|
const M = getMessagePrehash(rawHash, msg, opts.context);
|
|
527
|
-
|
|
549
|
+
// As above: getMessagePrehash() consumes `context`, so it must not travel further.
|
|
550
|
+
const res = internal.sign(M, secretKey, { extraEntropy: opts.extraEntropy });
|
|
528
551
|
cleanBytes(M);
|
|
529
552
|
return res;
|
|
530
553
|
},
|
|
531
554
|
verify: (sig, msg, publicKey, opts = {}) => {
|
|
532
|
-
validateVerOpts(opts);
|
|
555
|
+
opts = validateVerOpts(opts);
|
|
533
556
|
return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey);
|
|
534
557
|
},
|
|
535
558
|
});
|
|
@@ -590,7 +613,7 @@ const genShake = () => (opts) => (pubSeed, skSeed) => {
|
|
|
590
613
|
},
|
|
591
614
|
};
|
|
592
615
|
};
|
|
593
|
-
const SHAKE_SIMPLE = /* @__PURE__ */ (() => ({ getContext: genShake() }))();
|
|
616
|
+
const SHAKE_SIMPLE = /* @__PURE__ */ (() => ({ isCompressed: false, getContext: genShake() }))();
|
|
594
617
|
/**
|
|
595
618
|
* SLH-DSA-SHAKE-128f: Table 2 row `n=16, h=66, d=22, h'=3, a=6, k=33, lg w=4, m=34`;
|
|
596
619
|
* lengths `publicKey=32`, `secretKey=64`, `signature=17088`, `seed=48`, `signRand=16`.
|
package/src/_crystals.ts
CHANGED
|
@@ -112,7 +112,7 @@ export const genCrystals = <T extends TypedArray>(opts: CrystalOpts<T>): TRet<Cr
|
|
|
112
112
|
// Normalize JS `%` into the canonical Z_m representative `[0, modulo-1]` expected by
|
|
113
113
|
// FIPS 203 §2.3 / FIPS 204 §2.3 before downstream mod-q arithmetic.
|
|
114
114
|
const mod = (a: number, modulo = Q): number => {
|
|
115
|
-
const result = a % modulo | 0;
|
|
115
|
+
const result = (a % modulo) | 0;
|
|
116
116
|
return (result >= 0 ? result | 0 : (modulo + result) | 0) | 0;
|
|
117
117
|
};
|
|
118
118
|
// FIPS 204 §7.4 uses the centered `mod ±` representative for low bits, keeping the
|