leviathan-crypto 2.0.0 → 2.1.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/CLAUDE.md +171 -7
- package/LICENSE +4 -0
- package/README.md +109 -54
- package/SECURITY.md +125 -233
- package/dist/chacha20/cipher-suite.d.ts +10 -0
- package/dist/chacha20/cipher-suite.js +66 -2
- package/dist/chacha20/generator.d.ts +12 -0
- package/dist/chacha20/generator.js +91 -0
- package/dist/chacha20/index.d.ts +97 -1
- package/dist/chacha20/index.js +139 -11
- package/dist/chacha20/ops.d.ts +57 -6
- package/dist/chacha20/ops.js +93 -13
- package/dist/chacha20/pool-worker.js +12 -0
- package/dist/chacha20/types.d.ts +1 -32
- package/dist/ct-wasm.js +1 -1
- package/dist/ct.wasm +0 -0
- package/dist/docs/aead.md +69 -26
- package/dist/docs/architecture.md +600 -520
- package/dist/docs/argon2id.md +17 -14
- package/dist/docs/chacha20.md +146 -39
- package/dist/docs/exports.md +46 -10
- package/dist/docs/fortuna.md +339 -122
- package/dist/docs/init.md +24 -25
- package/dist/docs/loader.md +142 -47
- package/dist/docs/serpent.md +139 -41
- package/dist/docs/sha2.md +77 -19
- package/dist/docs/sha3.md +81 -15
- package/dist/docs/types.md +156 -15
- package/dist/docs/utils.md +171 -81
- package/dist/embedded/chacha20-pool-worker.d.ts +1 -0
- package/dist/embedded/chacha20-pool-worker.js +5 -0
- package/dist/embedded/kyber.d.ts +1 -1
- package/dist/embedded/kyber.js +1 -1
- package/dist/embedded/serpent-pool-worker.d.ts +1 -0
- package/dist/embedded/serpent-pool-worker.js +5 -0
- package/dist/embedded/serpent.d.ts +1 -1
- package/dist/embedded/serpent.js +1 -1
- package/dist/fortuna.d.ts +14 -8
- package/dist/fortuna.js +144 -50
- package/dist/index.d.ts +8 -6
- package/dist/index.js +6 -5
- package/dist/init.d.ts +0 -2
- package/dist/init.js +83 -3
- package/dist/kyber/indcpa.js +4 -4
- package/dist/kyber/index.js +25 -5
- package/dist/kyber/kem.js +56 -1
- package/dist/kyber/suite.d.ts +1 -2
- package/dist/kyber/suite.js +1 -0
- package/dist/kyber/types.d.ts +1 -0
- package/dist/kyber/validate.d.ts +8 -4
- package/dist/kyber/validate.js +18 -14
- package/dist/kyber.wasm +0 -0
- package/dist/loader.d.ts +7 -2
- package/dist/loader.js +25 -28
- package/dist/ratchet/index.d.ts +6 -0
- package/dist/ratchet/index.js +37 -0
- package/dist/ratchet/kdf-chain.d.ts +13 -0
- package/dist/ratchet/kdf-chain.js +85 -0
- package/dist/ratchet/ratchet-keypair.d.ts +9 -0
- package/dist/ratchet/ratchet-keypair.js +61 -0
- package/dist/ratchet/root-kdf.d.ts +4 -0
- package/dist/ratchet/root-kdf.js +124 -0
- package/dist/ratchet/skipped-key-store.d.ts +14 -0
- package/dist/ratchet/skipped-key-store.js +154 -0
- package/dist/ratchet/types.d.ts +36 -0
- package/dist/ratchet/types.js +26 -0
- package/dist/serpent/cipher-suite.d.ts +10 -0
- package/dist/serpent/cipher-suite.js +136 -50
- package/dist/serpent/generator.d.ts +12 -0
- package/dist/serpent/generator.js +97 -0
- package/dist/serpent/index.d.ts +61 -1
- package/dist/serpent/index.js +92 -7
- package/dist/serpent/pool-worker.js +25 -95
- package/dist/serpent/serpent-cbc.d.ts +14 -4
- package/dist/serpent/serpent-cbc.js +58 -34
- package/dist/serpent/shared-ops.d.ts +83 -0
- package/dist/serpent/shared-ops.js +213 -0
- package/dist/serpent/types.d.ts +1 -5
- package/dist/serpent.wasm +0 -0
- package/dist/sha2/hash.d.ts +2 -0
- package/dist/sha2/hash.js +53 -0
- package/dist/sha2/index.d.ts +1 -0
- package/dist/sha2/index.js +15 -1
- package/dist/sha3/hash.d.ts +2 -0
- package/dist/sha3/hash.js +53 -0
- package/dist/sha3/index.d.ts +17 -2
- package/dist/sha3/index.js +79 -7
- package/dist/stream/header.js +5 -5
- package/dist/stream/open-stream.js +36 -14
- package/dist/stream/seal-stream-pool.d.ts +1 -0
- package/dist/stream/seal-stream-pool.js +47 -8
- package/dist/stream/seal-stream.js +29 -11
- package/dist/stream/types.d.ts +1 -0
- package/dist/types.d.ts +21 -0
- package/dist/utils.d.ts +7 -8
- package/dist/utils.js +73 -40
- package/dist/wasm-source.d.ts +9 -8
- package/package.json +79 -64
package/dist/fortuna.js
CHANGED
|
@@ -22,18 +22,16 @@
|
|
|
22
22
|
// src/ts/fortuna.ts
|
|
23
23
|
//
|
|
24
24
|
// Fortuna CSPRNG — Ferguson & Schneier, Practical Cryptography (2003), Chapter 9.
|
|
25
|
-
// Backed by
|
|
26
|
-
// Requires init(
|
|
27
|
-
import { isInitialized } from './init.js';
|
|
28
|
-
import { Serpent } from './serpent/index.js';
|
|
29
|
-
import { SHA256 } from './sha2/index.js';
|
|
25
|
+
// Backed by a pluggable Generator (cipher PRF) and HashFn (accumulator hash).
|
|
26
|
+
// Requires init() for the modules used by the chosen generator and hash pair.
|
|
27
|
+
import { isInitialized, getInstance } from './init.js';
|
|
30
28
|
import { wipe, utf8ToBytes, concat } from './utils.js';
|
|
31
29
|
const isBrowser = typeof window !== 'undefined';
|
|
32
30
|
const isNode = typeof process !== 'undefined' && typeof process.pid === 'number';
|
|
33
31
|
/**
|
|
34
32
|
* Fortuna CSPRNG — spec §9.3–§9.5
|
|
35
33
|
*
|
|
36
|
-
* Use `Fortuna.create()` to instantiate. Direct construction is not allowed.
|
|
34
|
+
* Use `Fortuna.create({ generator, hash })` to instantiate. Direct construction is not allowed.
|
|
37
35
|
*/
|
|
38
36
|
export class Fortuna {
|
|
39
37
|
// ── Constants ──────────────────────────────────────────────────────────
|
|
@@ -43,9 +41,9 @@ export class Fortuna {
|
|
|
43
41
|
static NODE_STATS_INTERVAL = 1000; // ms — OS stats collector interval
|
|
44
42
|
static CRYPTO_INTERVAL = 3000; // ms — crypto.randomBytes interval
|
|
45
43
|
// ── State ─────────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
poolHash; // 32 running
|
|
44
|
+
gen;
|
|
45
|
+
hash;
|
|
46
|
+
poolHash; // 32 running hash chain values
|
|
49
47
|
poolEntropy;
|
|
50
48
|
genKey;
|
|
51
49
|
genCnt;
|
|
@@ -62,25 +60,33 @@ export class Fortuna {
|
|
|
62
60
|
timers = [];
|
|
63
61
|
// ── Static factory ────────────────────────────────────────────────────
|
|
64
62
|
static async create(opts) {
|
|
65
|
-
if (!
|
|
66
|
-
throw new
|
|
67
|
-
if (
|
|
68
|
-
throw new
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
if (!opts || !opts.generator || !opts.hash)
|
|
64
|
+
throw new TypeError('leviathan-crypto: Fortuna.create() requires { generator, hash }');
|
|
65
|
+
if (opts.hash.outputSize !== opts.generator.keySize)
|
|
66
|
+
throw new RangeError(`leviathan-crypto: Fortuna requires hash.outputSize (${opts.hash.outputSize}) `
|
|
67
|
+
+ `to match generator.keySize (${opts.generator.keySize})`);
|
|
68
|
+
const required = new Set([...opts.generator.wasmModules, ...opts.hash.wasmModules]);
|
|
69
|
+
for (const mod of required) {
|
|
70
|
+
if (!isInitialized(mod)) {
|
|
71
|
+
const args = [...required].map(m => `${m}: ...`).join(', ');
|
|
72
|
+
throw new Error(`leviathan-crypto: call init({ ${args} }) before using Fortuna`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const f = new Fortuna(opts.generator, opts.hash, opts.msPerReseed ?? Fortuna.MS_PER_RESEED);
|
|
76
|
+
f.initialize(opts.entropy);
|
|
71
77
|
// Force the first reseed — pool[0] is saturated by initialize(),
|
|
72
78
|
// so this call triggers an immediate reseed and guarantees get() never
|
|
73
79
|
// returns undefined. The byte is discarded.
|
|
74
80
|
f.get(1);
|
|
75
81
|
return f;
|
|
76
82
|
}
|
|
77
|
-
constructor(msPerReseed) {
|
|
78
|
-
this.
|
|
79
|
-
this.
|
|
83
|
+
constructor(gen, hash, msPerReseed) {
|
|
84
|
+
this.gen = gen;
|
|
85
|
+
this.hash = hash;
|
|
80
86
|
this.poolHash = [];
|
|
81
87
|
this.poolEntropy = [];
|
|
82
|
-
this.genKey = new Uint8Array(
|
|
83
|
-
this.genCnt = new Uint8Array(
|
|
88
|
+
this.genKey = new Uint8Array(gen.keySize);
|
|
89
|
+
this.genCnt = new Uint8Array(gen.counterSize);
|
|
84
90
|
this.reseedCnt = 0;
|
|
85
91
|
this.lastReseed = 0;
|
|
86
92
|
this.entropyLevel = 0;
|
|
@@ -90,7 +96,7 @@ export class Fortuna {
|
|
|
90
96
|
this.msPerReseed = msPerReseed;
|
|
91
97
|
this.robin = { kbd: 0, mouse: 0, scroll: 0, touch: 0, motion: 0, time: 0, rnd: 0, dom: 0 };
|
|
92
98
|
for (let i = 0; i < Fortuna.NUM_POOLS; i++) {
|
|
93
|
-
this.poolHash.push(new Uint8Array(
|
|
99
|
+
this.poolHash.push(new Uint8Array(hash.outputSize)); // zero-initialized chain value
|
|
94
100
|
this.poolEntropy.push(0);
|
|
95
101
|
}
|
|
96
102
|
}
|
|
@@ -109,17 +115,23 @@ export class Fortuna {
|
|
|
109
115
|
let seed = new Uint8Array(0);
|
|
110
116
|
let strength = 0;
|
|
111
117
|
for (let i = 0; i < Fortuna.NUM_POOLS; i++) {
|
|
112
|
-
|
|
118
|
+
// Practical Cryptography (Ferguson & Schneier, 2003) §9.5.5:
|
|
119
|
+
// pool P_i is used in reseed r iff 2^i divides r.
|
|
120
|
+
if ((this.reseedCnt & ((1 << i) - 1)) === 0) {
|
|
113
121
|
// Pool digest = current chain hash
|
|
114
122
|
seed = concat(seed, this.poolHash[i]);
|
|
115
123
|
strength += this.poolEntropy[i];
|
|
116
|
-
// Reset pool
|
|
117
|
-
this.poolHash[i]
|
|
124
|
+
// Reset pool — wipe old chain hash before dropping the reference.
|
|
125
|
+
const old = this.poolHash[i];
|
|
126
|
+
this.poolHash[i] = new Uint8Array(this.hash.outputSize);
|
|
127
|
+
wipe(old);
|
|
118
128
|
this.poolEntropy[i] = 0;
|
|
119
129
|
}
|
|
120
130
|
}
|
|
121
131
|
this.entropyLevel -= strength;
|
|
122
132
|
this.reseed(seed);
|
|
133
|
+
// seed is built from concatenated pool-hash copies; wipe the temp.
|
|
134
|
+
wipe(seed);
|
|
123
135
|
}
|
|
124
136
|
return this.pseudoRandomData(length);
|
|
125
137
|
}
|
|
@@ -140,11 +152,33 @@ export class Fortuna {
|
|
|
140
152
|
stop() {
|
|
141
153
|
if (this.disposed)
|
|
142
154
|
throw new Error('Fortuna instance has been disposed');
|
|
155
|
+
// Mark disposed FIRST. WASM wipeBuffers can throw if a stateful instance
|
|
156
|
+
// holds the module; we must not allow get()/addEntropy()/getEntropy() to
|
|
157
|
+
// run on a partially-disposed instance.
|
|
158
|
+
this.disposed = true;
|
|
143
159
|
this.stopCollectors();
|
|
144
160
|
wipe(this.genKey);
|
|
145
161
|
wipe(this.genCnt);
|
|
162
|
+
// Wipe all 32 pool-hash chain values so residual entropy-bearing
|
|
163
|
+
// bytes do not outlive the instance.
|
|
164
|
+
for (const p of this.poolHash)
|
|
165
|
+
wipe(p);
|
|
146
166
|
this.reseedCnt = 0;
|
|
147
|
-
|
|
167
|
+
// Best-effort wipe of WASM scratch buffers for every module the chosen
|
|
168
|
+
// generator and hash touched. Surface the first error so the caller
|
|
169
|
+
// knows the WASM scratch leak occurred.
|
|
170
|
+
const required = new Set([...this.gen.wasmModules, ...this.hash.wasmModules]);
|
|
171
|
+
let err;
|
|
172
|
+
for (const mod of required) {
|
|
173
|
+
try {
|
|
174
|
+
getInstance(mod).exports.wipeBuffers();
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
err ??= e;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (err)
|
|
181
|
+
throw err;
|
|
148
182
|
}
|
|
149
183
|
// ── Test-only accessors ───────────────────────────────────────────────
|
|
150
184
|
/** @internal — exposed for testing key replacement */
|
|
@@ -159,45 +193,93 @@ export class Fortuna {
|
|
|
159
193
|
_getReseedCnt() {
|
|
160
194
|
return this.reseedCnt;
|
|
161
195
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
196
|
+
/** @internal — exposed for testing pool-hash backing arrays */
|
|
197
|
+
_getPoolHash() {
|
|
198
|
+
return this.poolHash;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* @internal — test-only deterministic factory. Seeds pool[0] with the provided
|
|
202
|
+
* entropy and triggers one reseed directly, bypassing all OS entropy collection
|
|
203
|
+
* and the hrtime jitter capture in get(). This makes KAT vectors reproducible
|
|
204
|
+
* across runs. Not suitable for production use.
|
|
205
|
+
*/
|
|
206
|
+
static async _createDeterministicForTesting(opts) {
|
|
207
|
+
if (!opts || !opts.generator || !opts.hash)
|
|
208
|
+
throw new TypeError('Fortuna._createDeterministicForTesting() requires { generator, hash, entropy }');
|
|
209
|
+
if (opts.hash.outputSize !== opts.generator.keySize)
|
|
210
|
+
throw new RangeError(`leviathan-crypto: Fortuna requires hash.outputSize (${opts.hash.outputSize}) `
|
|
211
|
+
+ `to match generator.keySize (${opts.generator.keySize})`);
|
|
212
|
+
const required = new Set([...opts.generator.wasmModules, ...opts.hash.wasmModules]);
|
|
213
|
+
for (const mod of required) {
|
|
214
|
+
if (!isInitialized(mod)) {
|
|
215
|
+
const args = [...required].map(m => `${m}: ...`).join(', ');
|
|
216
|
+
throw new Error(`leviathan-crypto: call init({ ${args} }) before using Fortuna`);
|
|
217
|
+
}
|
|
171
218
|
}
|
|
172
|
-
|
|
219
|
+
const f = new Fortuna(opts.generator, opts.hash, 0);
|
|
220
|
+
// Seed pool[0] with the provided entropy, no OS collection.
|
|
221
|
+
f.addRandomEvent(opts.entropy, 0, opts.entropy.length * 8);
|
|
222
|
+
// Manually trigger reseed #1 without calling get() — get() calls captureHrtime()
|
|
223
|
+
// in Node.js which adds non-deterministic data before the reseed fires.
|
|
224
|
+
f.reseedFromPool0();
|
|
225
|
+
return f;
|
|
173
226
|
}
|
|
227
|
+
// ── Generator (spec §9.4) ─────────────────────────────────────────────
|
|
174
228
|
/** Get length pseudo-random bytes. — spec §9.4 */
|
|
175
229
|
pseudoRandomData(length) {
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
230
|
+
const blocks = Math.ceil(length / this.gen.blockSize);
|
|
231
|
+
const out = this.gen.generate(this.genKey, this.genCnt, length);
|
|
232
|
+
// External counter advance — generator is stateless and does not mutate caller's counter
|
|
233
|
+
for (let i = 0; i < blocks; i++)
|
|
234
|
+
this.incrementCounter();
|
|
235
|
+
// Key replacement — mandatory forward secrecy (spec §9.4).
|
|
236
|
+
// Wipe the prior key BEFORE dropping its reference so no key bytes are
|
|
237
|
+
// reachable after key replacement; anyone holding a Uint8Array view to
|
|
238
|
+
// the old key now observes zero.
|
|
239
|
+
const newKey = this.gen.generate(this.genKey, this.genCnt, this.gen.keySize);
|
|
240
|
+
for (let i = 0; i < Math.ceil(this.gen.keySize / this.gen.blockSize); i++)
|
|
241
|
+
this.incrementCounter();
|
|
242
|
+
wipe(this.genKey);
|
|
243
|
+
this.genKey = newKey;
|
|
244
|
+
return out;
|
|
183
245
|
}
|
|
184
246
|
/** Reseed the generator — spec §9.4 */
|
|
185
247
|
reseed(seed) {
|
|
186
|
-
// genKey =
|
|
187
|
-
|
|
248
|
+
// genKey = hash(genKey ‖ seed). Wipe both the hash input and the
|
|
249
|
+
// prior key before dropping references.
|
|
250
|
+
const combined = concat(this.genKey, seed);
|
|
251
|
+
const newKey = this.hash.digest(combined);
|
|
252
|
+
wipe(combined);
|
|
253
|
+
wipe(this.genKey);
|
|
254
|
+
this.genKey = newKey;
|
|
188
255
|
// Increment counter — makes it nonzero on first reseed, marking generator as seeded
|
|
189
256
|
this.incrementCounter();
|
|
190
257
|
this.lastReseed = Date.now();
|
|
191
258
|
}
|
|
192
|
-
/**
|
|
259
|
+
/** Drain pool 0 into a fresh seed and reseed. Used by the deterministic
|
|
260
|
+
* test factory; production reseeds in get() walk the §9.5.5 schedule
|
|
261
|
+
* across all pools, not just pool 0. Caller is responsible for any
|
|
262
|
+
* entropy-threshold check. */
|
|
263
|
+
reseedFromPool0() {
|
|
264
|
+
this.reseedCnt = (this.reseedCnt + 1) >>> 0;
|
|
265
|
+
const seed = this.poolHash[0].slice();
|
|
266
|
+
const old = this.poolHash[0];
|
|
267
|
+
this.poolHash[0] = new Uint8Array(this.hash.outputSize);
|
|
268
|
+
wipe(old);
|
|
269
|
+
this.entropyLevel -= this.poolEntropy[0];
|
|
270
|
+
this.poolEntropy[0] = 0;
|
|
271
|
+
this.reseed(seed);
|
|
272
|
+
wipe(seed);
|
|
273
|
+
}
|
|
274
|
+
/** Increment little-endian counter. — spec §9.4 */
|
|
193
275
|
incrementCounter() {
|
|
194
|
-
for (let i = 0; i <
|
|
276
|
+
for (let i = 0; i < this.genCnt.length; i++) {
|
|
195
277
|
if (++this.genCnt[i] !== 0)
|
|
196
278
|
break;
|
|
197
279
|
}
|
|
198
280
|
}
|
|
199
281
|
// ── Accumulator (spec §9.5) ───────────────────────────────────────────
|
|
200
|
-
/** Add an event to a pool via hash chaining: poolHash[i] =
|
|
282
|
+
/** Add an event to a pool via hash chaining: poolHash[i] = hash(poolHash[i] ‖ eventId ‖ data). */
|
|
201
283
|
addRandomEvent(data, poolIdx, entropyBits) {
|
|
202
284
|
// Encode eventId as 4 bytes little-endian
|
|
203
285
|
const id = new Uint8Array(4);
|
|
@@ -206,8 +288,13 @@ export class Fortuna {
|
|
|
206
288
|
id[2] = (this.eventId >>> 16) & 0xff;
|
|
207
289
|
id[3] = (this.eventId >>> 24) & 0xff;
|
|
208
290
|
this.eventId = (this.eventId + 1) >>> 0; // u32 wrap
|
|
209
|
-
// Chain: poolHash[i] =
|
|
210
|
-
|
|
291
|
+
// Chain: poolHash[i] = hash(poolHash[i] ‖ id ‖ data).
|
|
292
|
+
// Wipe the chain input and the prior chain value before dropping refs.
|
|
293
|
+
const combined = concat(this.poolHash[poolIdx], id, data);
|
|
294
|
+
const newChain = this.hash.digest(combined);
|
|
295
|
+
wipe(combined);
|
|
296
|
+
wipe(this.poolHash[poolIdx]);
|
|
297
|
+
this.poolHash[poolIdx] = newChain;
|
|
211
298
|
this.poolEntropy[poolIdx] += entropyBits;
|
|
212
299
|
this.entropyLevel += entropyBits;
|
|
213
300
|
}
|
|
@@ -226,6 +313,13 @@ export class Fortuna {
|
|
|
226
313
|
this.addRandomEvent(entropy, this.robin.rnd, entropy.length * 8);
|
|
227
314
|
this.robin.rnd = (this.robin.rnd + 1) % Fortuna.NUM_POOLS;
|
|
228
315
|
}
|
|
316
|
+
// F-2 invariant: fail loudly if no OS entropy source delivered anything.
|
|
317
|
+
// The try/catch in collectorCryptoRandom is preserved to protect against
|
|
318
|
+
// platforms where crypto.getRandomValues itself throws (non-standard
|
|
319
|
+
// runtimes). This post-init check covers all silent-failure paths uniformly.
|
|
320
|
+
if (this.poolEntropy[0] < Fortuna.RESEED_LIMIT)
|
|
321
|
+
throw new Error('leviathan-crypto: Fortuna initialization could not gather sufficient entropy. '
|
|
322
|
+
+ 'No working crypto.getRandomValues or node:crypto in this environment.');
|
|
229
323
|
this.startCollectors();
|
|
230
324
|
}
|
|
231
325
|
// ── Collectors ────────────────────────────────────────────────────────
|
|
@@ -358,7 +452,7 @@ export class Fortuna {
|
|
|
358
452
|
}
|
|
359
453
|
collectorDom() {
|
|
360
454
|
if (typeof document !== 'undefined' && document.documentElement) {
|
|
361
|
-
this.addRandomEvent(this.
|
|
455
|
+
this.addRandomEvent(this.hash.digest(utf8ToBytes(document.documentElement.innerHTML)), this.robin.dom, 2);
|
|
362
456
|
this.robin.dom = (this.robin.dom + 1) % Fortuna.NUM_POOLS;
|
|
363
457
|
}
|
|
364
458
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,17 +13,19 @@ import type { WasmSource } from './wasm-source.js';
|
|
|
13
13
|
*/
|
|
14
14
|
export declare function init(sources: Partial<Record<Module, WasmSource>>): Promise<void>;
|
|
15
15
|
export type { Module, WasmSource };
|
|
16
|
-
export { isInitialized
|
|
16
|
+
export { isInitialized } from './init.js';
|
|
17
17
|
export { AuthenticationError } from './errors.js';
|
|
18
|
-
export { serpentInit, Serpent, SerpentCtr, SerpentCbc, SerpentCipher, _serpentReady } from './serpent/index.js';
|
|
19
|
-
export { chacha20Init, ChaCha20, Poly1305, ChaCha20Poly1305, XChaCha20Poly1305, XChaCha20Cipher, _chachaReady } from './chacha20/index.js';
|
|
20
|
-
export { sha2Init, SHA256, SHA512, SHA384, HMAC_SHA256, HMAC_SHA512, HMAC_SHA384, HKDF_SHA256, HKDF_SHA512, _sha2Ready } from './sha2/index.js';
|
|
21
|
-
export { sha3Init, SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, _sha3Ready } from './sha3/index.js';
|
|
18
|
+
export { serpentInit, Serpent, SerpentCtr, SerpentCbc, SerpentCipher, SerpentGenerator, _serpentReady } from './serpent/index.js';
|
|
19
|
+
export { chacha20Init, ChaCha20, Poly1305, ChaCha20Poly1305, XChaCha20Poly1305, XChaCha20Cipher, ChaCha20Generator, _chachaReady } from './chacha20/index.js';
|
|
20
|
+
export { sha2Init, SHA256, SHA512, SHA384, HMAC_SHA256, HMAC_SHA512, HMAC_SHA384, HKDF_SHA256, HKDF_SHA512, SHA256Hash, _sha2Ready } from './sha2/index.js';
|
|
21
|
+
export { sha3Init, SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, SHA3_256Hash, _sha3Ready } from './sha3/index.js';
|
|
22
22
|
export { keccakInit } from './keccak/index.js';
|
|
23
23
|
export { kyberInit, MlKem512, MlKem768, MlKem1024, MlKemBase, KyberSuite, _kyberReady } from './kyber/index.js';
|
|
24
24
|
export type { KyberKeyPair, KyberEncapsulation, KyberParams } from './kyber/index.js';
|
|
25
25
|
export { SealStream, OpenStream, Seal, SealStreamPool, FLAG_FRAMED, TAG_DATA, TAG_FINAL, HEADER_SIZE, CHUNK_MIN, CHUNK_MAX } from './stream/index.js';
|
|
26
26
|
export type { CipherSuite, DerivedKeys, SealStreamOpts, PoolOpts } from './stream/index.js';
|
|
27
27
|
export { Fortuna } from './fortuna.js';
|
|
28
|
-
export type { Hash, KeyedHash, Blockcipher, Streamcipher, AEAD } from './types.js';
|
|
28
|
+
export type { Hash, KeyedHash, Blockcipher, Streamcipher, AEAD, Generator, HashFn } from './types.js';
|
|
29
|
+
export { KDFChain, ratchetInit, kemRatchetEncap, kemRatchetDecap, ratchetReady, SkippedKeyStore, RatchetKeypair, } from './ratchet/index.js';
|
|
30
|
+
export type { RatchetInitResult, KemEncapResult, KemDecapResult, MlKemLike, RatchetMessageHeader, ResolveHandle, SkippedKeyStoreOpts, } from './ratchet/index.js';
|
|
29
31
|
export { hexToBytes, bytesToHex, utf8ToBytes, bytesToUtf8, base64ToBytes, bytesToBase64, constantTimeEqual, CT_MAX_BYTES, wipe, xor, concat, randomBytes, hasSIMD, } from './utils.js';
|
package/dist/index.js
CHANGED
|
@@ -60,14 +60,15 @@ export async function init(sources) {
|
|
|
60
60
|
}
|
|
61
61
|
await Promise.all(entries.map(([mod, src]) => _dispatchers[mod](src)));
|
|
62
62
|
}
|
|
63
|
-
export { isInitialized
|
|
63
|
+
export { isInitialized } from './init.js';
|
|
64
64
|
export { AuthenticationError } from './errors.js';
|
|
65
|
-
export { serpentInit, Serpent, SerpentCtr, SerpentCbc, SerpentCipher, _serpentReady } from './serpent/index.js';
|
|
66
|
-
export { chacha20Init, ChaCha20, Poly1305, ChaCha20Poly1305, XChaCha20Poly1305, XChaCha20Cipher, _chachaReady } from './chacha20/index.js';
|
|
67
|
-
export { sha2Init, SHA256, SHA512, SHA384, HMAC_SHA256, HMAC_SHA512, HMAC_SHA384, HKDF_SHA256, HKDF_SHA512, _sha2Ready } from './sha2/index.js';
|
|
68
|
-
export { sha3Init, SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, _sha3Ready } from './sha3/index.js';
|
|
65
|
+
export { serpentInit, Serpent, SerpentCtr, SerpentCbc, SerpentCipher, SerpentGenerator, _serpentReady } from './serpent/index.js';
|
|
66
|
+
export { chacha20Init, ChaCha20, Poly1305, ChaCha20Poly1305, XChaCha20Poly1305, XChaCha20Cipher, ChaCha20Generator, _chachaReady } from './chacha20/index.js';
|
|
67
|
+
export { sha2Init, SHA256, SHA512, SHA384, HMAC_SHA256, HMAC_SHA512, HMAC_SHA384, HKDF_SHA256, HKDF_SHA512, SHA256Hash, _sha2Ready } from './sha2/index.js';
|
|
68
|
+
export { sha3Init, SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, SHA3_256Hash, _sha3Ready } from './sha3/index.js';
|
|
69
69
|
export { keccakInit } from './keccak/index.js';
|
|
70
70
|
export { kyberInit, MlKem512, MlKem768, MlKem1024, MlKemBase, KyberSuite, _kyberReady } from './kyber/index.js';
|
|
71
71
|
export { SealStream, OpenStream, Seal, SealStreamPool, FLAG_FRAMED, TAG_DATA, TAG_FINAL, HEADER_SIZE, CHUNK_MIN, CHUNK_MAX } from './stream/index.js';
|
|
72
72
|
export { Fortuna } from './fortuna.js';
|
|
73
|
+
export { KDFChain, ratchetInit, kemRatchetEncap, kemRatchetDecap, ratchetReady, SkippedKeyStore, RatchetKeypair, } from './ratchet/index.js';
|
|
73
74
|
export { hexToBytes, bytesToHex, utf8ToBytes, bytesToUtf8, base64ToBytes, bytesToBase64, constantTimeEqual, CT_MAX_BYTES, wipe, xor, concat, randomBytes, hasSIMD, } from './utils.js';
|
package/dist/init.d.ts
CHANGED
|
@@ -3,5 +3,3 @@ export type Module = 'serpent' | 'chacha20' | 'sha2' | 'sha3' | 'keccak' | 'kybe
|
|
|
3
3
|
export declare function initModule(mod: Module, source: WasmSource): Promise<void>;
|
|
4
4
|
export declare function getInstance(mod: Module): WebAssembly.Instance;
|
|
5
5
|
export declare function isInitialized(mod: Module): boolean;
|
|
6
|
-
/** Reset all cached instances — for testing only */
|
|
7
|
-
export declare function _resetForTesting(): void;
|
package/dist/init.js
CHANGED
|
@@ -7,26 +7,106 @@ function resolve(mod) {
|
|
|
7
7
|
}
|
|
8
8
|
// Module-scope cache: one WebAssembly.Instance per canonical module
|
|
9
9
|
const instances = new Map();
|
|
10
|
+
// Pending inits — coalesces concurrent initModule calls for the same module.
|
|
11
|
+
const pending = new Map();
|
|
12
|
+
// Exclusivity registry: per-module ownership token held by a stateful wrapper
|
|
13
|
+
// for its entire lifetime. Prevents shared-WASM-state clobber when two
|
|
14
|
+
// instances from the same module would otherwise trample each other's memory.
|
|
15
|
+
const owners = new Map();
|
|
10
16
|
export async function initModule(mod, source) {
|
|
11
17
|
const resolved = resolve(mod);
|
|
12
18
|
if (instances.has(resolved))
|
|
13
19
|
return;
|
|
20
|
+
const inflight = pending.get(resolved);
|
|
21
|
+
if (inflight) {
|
|
22
|
+
await inflight;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
14
25
|
if ((resolved === 'serpent' || resolved === 'chacha20' || resolved === 'kyber') && !hasSIMD())
|
|
15
26
|
throw new Error('leviathan-crypto: serpent, chacha20, and kyber require WebAssembly SIMD — '
|
|
16
27
|
+ 'this runtime does not support it');
|
|
17
|
-
|
|
28
|
+
const p = loadWasm(source);
|
|
29
|
+
pending.set(resolved, p);
|
|
30
|
+
try {
|
|
31
|
+
instances.set(resolved, await p);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
pending.delete(resolved);
|
|
35
|
+
}
|
|
18
36
|
}
|
|
19
37
|
export function getInstance(mod) {
|
|
20
|
-
const
|
|
38
|
+
const r = resolve(mod);
|
|
39
|
+
const inst = instances.get(r);
|
|
21
40
|
if (!inst) {
|
|
22
41
|
throw new Error(`leviathan-crypto: call init({ ${mod}: ... }) before using this class`);
|
|
23
42
|
}
|
|
43
|
+
if (owners.has(r)) {
|
|
44
|
+
throw new Error(`leviathan-crypto: another stateful instance is using the '${r}' WASM module — `
|
|
45
|
+
+ 'call dispose() on it before constructing a new one');
|
|
46
|
+
}
|
|
24
47
|
return inst;
|
|
25
48
|
}
|
|
26
49
|
export function isInitialized(mod) {
|
|
27
50
|
return instances.has(resolve(mod));
|
|
28
51
|
}
|
|
29
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Acquire exclusive access to `mod`. Throws if another stateful instance
|
|
54
|
+
* currently holds it. Returned token must be passed to `_releaseModule`.
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
export function _acquireModule(mod) {
|
|
58
|
+
const r = resolve(mod);
|
|
59
|
+
if (owners.has(r))
|
|
60
|
+
throw new Error(`leviathan-crypto: another stateful instance is using the '${r}' WASM module — `
|
|
61
|
+
+ 'call dispose() on it before constructing a new one');
|
|
62
|
+
const tok = Symbol(r);
|
|
63
|
+
owners.set(r, tok);
|
|
64
|
+
return tok;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Release exclusive access. No-op if the token doesn't match the current
|
|
68
|
+
* owner (makes dispose idempotent).
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
export function _releaseModule(mod, tok) {
|
|
72
|
+
const r = resolve(mod);
|
|
73
|
+
if (owners.get(r) === tok)
|
|
74
|
+
owners.delete(r);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* True if a stateful instance currently holds the module.
|
|
78
|
+
* @internal
|
|
79
|
+
*/
|
|
80
|
+
export function _isModuleBusy(mod) {
|
|
81
|
+
return owners.has(resolve(mod));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Throw if `mod` is currently held by a stateful instance. Called at the top
|
|
85
|
+
* of every atomic WASM-touching method so that cached-exports access paths
|
|
86
|
+
* cannot silently clobber a live stateful instance's WASM state.
|
|
87
|
+
*
|
|
88
|
+
* The error message is intentionally identical to `_acquireModule`'s so that
|
|
89
|
+
* error handlers matching on text work uniformly across construction-time and
|
|
90
|
+
* method-time ownership failures.
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
export function _assertNotOwned(mod) {
|
|
94
|
+
// Deliberately unoptimized. Do not add caching or epoch tracking: the
|
|
95
|
+
// check must read current ownership on every call so an atomic op cannot
|
|
96
|
+
// race ahead of a stateful acquirer.
|
|
97
|
+
const r = resolve(mod);
|
|
98
|
+
if (owners.has(r))
|
|
99
|
+
throw new Error(`leviathan-crypto: another stateful instance is using the '${r}' WASM module — `
|
|
100
|
+
+ 'call dispose() on it before constructing a new one');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Reset all cached instances — for testing only. Clears `instances`, `pending`,
|
|
104
|
+
* and `owners` so tests can re-exercise module lifecycle (init, exclusivity,
|
|
105
|
+
* race) from a known-empty state.
|
|
106
|
+
* @internal
|
|
107
|
+
*/
|
|
30
108
|
export function _resetForTesting() {
|
|
31
109
|
instances.clear();
|
|
110
|
+
pending.clear();
|
|
111
|
+
owners.clear();
|
|
32
112
|
}
|
package/dist/kyber/indcpa.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// ML-KEM IND-CPA PKE scheme — FIPS 203 Algorithms 12, 13, 14 (K-PKE).
|
|
25
25
|
// Orchestrates kyber WASM (polynomial math) and sha3 WASM (Keccak sponge).
|
|
26
26
|
import { wipe } from '../utils.js';
|
|
27
|
-
// ── SHA3 helpers
|
|
27
|
+
// ── SHA3 helpers ────────────────────────────────────────────────────────────
|
|
28
28
|
// All operate directly on raw Sha3Exports, no init() system involved.
|
|
29
29
|
/** Absorb msg into the sha3 sponge in 168-byte chunks (max rate). */
|
|
30
30
|
function sha3Absorb(sx, msg) {
|
|
@@ -77,7 +77,7 @@ export function shake256Hash(sx, msg, n) {
|
|
|
77
77
|
}
|
|
78
78
|
return out;
|
|
79
79
|
}
|
|
80
|
-
// ── Matrix generation
|
|
80
|
+
// ── Matrix generation ───────────────────────────────────────────────────────
|
|
81
81
|
/**
|
|
82
82
|
* Generate row `rowI` of matrix  (or Â^T) into polyvec slot `pvecSlot`.
|
|
83
83
|
*
|
|
@@ -120,7 +120,7 @@ function genMatrixRow(kx, sx, k, rho, transposed, pvecSlot, rowI) {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
-
// ── Noise generation
|
|
123
|
+
// ── Noise generation ────────────────────────────────────────────────────────
|
|
124
124
|
/**
|
|
125
125
|
* CBD noise polyvec: SHAKE256(σ || nonce) for each entry.
|
|
126
126
|
* FIPS 203 Algorithm 7: PRF_η(σ, N) = SHAKE256(σ || N)[0..64η-1].
|
|
@@ -185,7 +185,7 @@ function noisePoly(kx, sx, polyOff, sigma, nonce, eta) {
|
|
|
185
185
|
wipe(prfInput);
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
|
-
// ── IND-CPA functions
|
|
188
|
+
// ── IND-CPA functions ───────────────────────────────────────────────────────
|
|
189
189
|
/**
|
|
190
190
|
* K-PKE.KeyGen (FIPS 203 Algorithm 12) — deterministic.
|
|
191
191
|
*
|
package/dist/kyber/index.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
//
|
|
24
24
|
// ML-KEM public API — MlKem512, MlKem768, MlKem1024 classes.
|
|
25
25
|
// Uses the init() module cache — call init({ kyber: ..., sha3: ... }) before constructing.
|
|
26
|
-
import { getInstance, initModule, isInitialized } from '../init.js';
|
|
26
|
+
import { getInstance, initModule, isInitialized, _assertNotOwned } from '../init.js';
|
|
27
27
|
import { randomBytes, wipe } from '../utils.js';
|
|
28
28
|
import { MLKEM512, MLKEM768, MLKEM1024 } from './params.js';
|
|
29
29
|
import { kemKeypairDerand, kemEncapsulateDerand, kemDecapsulate } from './kem.js';
|
|
@@ -44,7 +44,7 @@ export function _kyberReady() {
|
|
|
44
44
|
export { MLKEM512, MLKEM768, MLKEM1024 };
|
|
45
45
|
export { isInitialized };
|
|
46
46
|
export { KyberSuite } from './suite.js';
|
|
47
|
-
// ── Layout assertion
|
|
47
|
+
// ── Layout assertion ────────────────────────────────────────────────────────
|
|
48
48
|
function assertLayout(kx, p) {
|
|
49
49
|
const pk = kx.getPkOffset();
|
|
50
50
|
const sk = kx.getSkOffset();
|
|
@@ -60,7 +60,7 @@ function assertLayout(kx, p) {
|
|
|
60
60
|
if (ctPrime + p.ctBytes > xof)
|
|
61
61
|
throw new Error('leviathan-crypto: kyber buffer overflow — ct_prime overflows into XOF region');
|
|
62
62
|
}
|
|
63
|
-
// ── Base class
|
|
63
|
+
// ── Base class ──────────────────────────────────────────────────────────────
|
|
64
64
|
export class MlKemBase {
|
|
65
65
|
params;
|
|
66
66
|
constructor(params) {
|
|
@@ -78,6 +78,8 @@ export class MlKemBase {
|
|
|
78
78
|
return getInstance('sha3').exports;
|
|
79
79
|
}
|
|
80
80
|
keygenDerand(d, z) {
|
|
81
|
+
_assertNotOwned('sha3');
|
|
82
|
+
_assertNotOwned('kyber');
|
|
81
83
|
if (d.length !== 32)
|
|
82
84
|
throw new RangeError(`d seed must be 32 bytes (got ${d.length})`);
|
|
83
85
|
if (z.length !== 32)
|
|
@@ -96,10 +98,14 @@ export class MlKemBase {
|
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
encapsulateDerand(ek, m) {
|
|
101
|
+
_assertNotOwned('sha3');
|
|
102
|
+
_assertNotOwned('kyber');
|
|
99
103
|
if (ek.length !== this.params.ekBytes)
|
|
100
104
|
throw new RangeError(`encapsulation key must be ${this.params.ekBytes} bytes (got ${ek.length})`);
|
|
101
105
|
if (m.length !== 32)
|
|
102
106
|
throw new RangeError(`randomness m must be 32 bytes (got ${m.length})`);
|
|
107
|
+
if (!checkEncapsulationKey(this.kx, this.params, ek))
|
|
108
|
+
throw new RangeError('leviathan-crypto: encapsulation key failed FIPS 203 §7.2 validity check');
|
|
103
109
|
return kemEncapsulateDerand(this.kx, this.sx, this.params, ek, m);
|
|
104
110
|
}
|
|
105
111
|
encapsulate(ek) {
|
|
@@ -112,24 +118,38 @@ export class MlKemBase {
|
|
|
112
118
|
}
|
|
113
119
|
}
|
|
114
120
|
decapsulate(dk, c) {
|
|
121
|
+
_assertNotOwned('sha3');
|
|
122
|
+
_assertNotOwned('kyber');
|
|
115
123
|
if (dk.length !== this.params.dkBytes)
|
|
116
124
|
throw new RangeError(`decapsulation key must be ${this.params.dkBytes} bytes (got ${dk.length})`);
|
|
117
125
|
if (c.length !== this.params.ctBytes)
|
|
118
126
|
throw new RangeError(`ciphertext must be ${this.params.ctBytes} bytes (got ${c.length})`);
|
|
127
|
+
if (!checkDecapsulationKey(this.kx, this.sx, this.params, dk))
|
|
128
|
+
throw new RangeError('leviathan-crypto: decapsulation key failed FIPS 203 §7.3 validity check');
|
|
119
129
|
return kemDecapsulate(this.kx, this.sx, this.params, dk, c);
|
|
120
130
|
}
|
|
121
131
|
checkEncapsulationKey(ek) {
|
|
132
|
+
_assertNotOwned('sha3');
|
|
133
|
+
_assertNotOwned('kyber');
|
|
122
134
|
return checkEncapsulationKey(this.kx, this.params, ek);
|
|
123
135
|
}
|
|
124
136
|
checkDecapsulationKey(dk) {
|
|
137
|
+
_assertNotOwned('sha3');
|
|
138
|
+
_assertNotOwned('kyber');
|
|
125
139
|
return checkDecapsulationKey(this.kx, this.sx, this.params, dk);
|
|
126
140
|
}
|
|
127
141
|
dispose() {
|
|
128
142
|
this.kx.wipeBuffers();
|
|
129
|
-
this.sx.wipeBuffers()
|
|
143
|
+
// MlKemBase does not own the sha3 module — wiping this.sx.wipeBuffers()
|
|
144
|
+
// here would clobber any SHAKE128/SHAKE256 instance live at the time
|
|
145
|
+
// of dispose(). The wipe is not needed: every public kyber op
|
|
146
|
+
// (keygen, encapsulate, decapsulate, checkDecapsulationKey) calls
|
|
147
|
+
// sx.wipeBuffers() before returning, under the _assertNotOwned('sha3')
|
|
148
|
+
// guard it holds for the duration. sha3 scratch therefore carries no
|
|
149
|
+
// residue across a kyber op boundary — secret-derived or otherwise.
|
|
130
150
|
}
|
|
131
151
|
}
|
|
132
|
-
// ── Public classes
|
|
152
|
+
// ── Public classes ──────────────────────────────────────────────────────────
|
|
133
153
|
/** ML-KEM-512 — k=2, η₁=3, η₂=2, dᵤ=10, dᵥ=4. */
|
|
134
154
|
export class MlKem512 extends MlKemBase {
|
|
135
155
|
constructor() {
|