leviathan-crypto 1.0.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.
Files changed (78) hide show
  1. package/CLAUDE.md +265 -0
  2. package/LICENSE +21 -0
  3. package/README.md +322 -0
  4. package/SECURITY.md +174 -0
  5. package/dist/chacha.wasm +0 -0
  6. package/dist/chacha20/index.d.ts +49 -0
  7. package/dist/chacha20/index.js +177 -0
  8. package/dist/chacha20/ops.d.ts +16 -0
  9. package/dist/chacha20/ops.js +146 -0
  10. package/dist/chacha20/pool.d.ts +52 -0
  11. package/dist/chacha20/pool.js +188 -0
  12. package/dist/chacha20/pool.worker.d.ts +1 -0
  13. package/dist/chacha20/pool.worker.js +37 -0
  14. package/dist/chacha20/types.d.ts +30 -0
  15. package/dist/chacha20/types.js +1 -0
  16. package/dist/docs/architecture.md +795 -0
  17. package/dist/docs/argon2id.md +290 -0
  18. package/dist/docs/chacha20.md +602 -0
  19. package/dist/docs/chacha20_pool.md +306 -0
  20. package/dist/docs/fortuna.md +322 -0
  21. package/dist/docs/init.md +308 -0
  22. package/dist/docs/loader.md +206 -0
  23. package/dist/docs/serpent.md +914 -0
  24. package/dist/docs/sha2.md +620 -0
  25. package/dist/docs/sha3.md +509 -0
  26. package/dist/docs/types.md +198 -0
  27. package/dist/docs/utils.md +273 -0
  28. package/dist/docs/wasm.md +193 -0
  29. package/dist/embedded/chacha.d.ts +1 -0
  30. package/dist/embedded/chacha.js +2 -0
  31. package/dist/embedded/serpent.d.ts +1 -0
  32. package/dist/embedded/serpent.js +2 -0
  33. package/dist/embedded/sha2.d.ts +1 -0
  34. package/dist/embedded/sha2.js +2 -0
  35. package/dist/embedded/sha3.d.ts +1 -0
  36. package/dist/embedded/sha3.js +2 -0
  37. package/dist/fortuna.d.ts +72 -0
  38. package/dist/fortuna.js +445 -0
  39. package/dist/index.d.ts +13 -0
  40. package/dist/index.js +44 -0
  41. package/dist/init.d.ts +11 -0
  42. package/dist/init.js +49 -0
  43. package/dist/loader.d.ts +4 -0
  44. package/dist/loader.js +30 -0
  45. package/dist/serpent/index.d.ts +65 -0
  46. package/dist/serpent/index.js +242 -0
  47. package/dist/serpent/seal.d.ts +8 -0
  48. package/dist/serpent/seal.js +70 -0
  49. package/dist/serpent/stream-encoder.d.ts +20 -0
  50. package/dist/serpent/stream-encoder.js +167 -0
  51. package/dist/serpent/stream-pool.d.ts +48 -0
  52. package/dist/serpent/stream-pool.js +285 -0
  53. package/dist/serpent/stream-sealer.d.ts +34 -0
  54. package/dist/serpent/stream-sealer.js +223 -0
  55. package/dist/serpent/stream.d.ts +28 -0
  56. package/dist/serpent/stream.js +205 -0
  57. package/dist/serpent/stream.worker.d.ts +32 -0
  58. package/dist/serpent/stream.worker.js +117 -0
  59. package/dist/serpent/types.d.ts +5 -0
  60. package/dist/serpent/types.js +1 -0
  61. package/dist/serpent.wasm +0 -0
  62. package/dist/sha2/hkdf.d.ts +16 -0
  63. package/dist/sha2/hkdf.js +108 -0
  64. package/dist/sha2/index.d.ts +40 -0
  65. package/dist/sha2/index.js +190 -0
  66. package/dist/sha2/types.d.ts +5 -0
  67. package/dist/sha2/types.js +1 -0
  68. package/dist/sha2.wasm +0 -0
  69. package/dist/sha3/index.d.ts +55 -0
  70. package/dist/sha3/index.js +246 -0
  71. package/dist/sha3/types.d.ts +5 -0
  72. package/dist/sha3/types.js +1 -0
  73. package/dist/sha3.wasm +0 -0
  74. package/dist/types.d.ts +24 -0
  75. package/dist/types.js +26 -0
  76. package/dist/utils.d.ts +26 -0
  77. package/dist/utils.js +169 -0
  78. package/package.json +90 -0
@@ -0,0 +1,445 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // src/ts/fortuna.ts
23
+ //
24
+ // Fortuna CSPRNG — Ferguson & Schneier, Practical Cryptography (2003), Chapter 9.
25
+ // Backed by WASM Serpent-256 ECB (generator) and WASM SHA-256 (accumulator pools).
26
+ // Requires init(['serpent', 'sha2']) before Fortuna.create().
27
+ import { isInitialized } from './init.js';
28
+ import { Serpent } from './serpent/index.js';
29
+ import { SHA256 } from './sha2/index.js';
30
+ import { wipe, utf8ToBytes, concat } from './utils.js';
31
+ const isBrowser = typeof window !== 'undefined';
32
+ const isNode = typeof process !== 'undefined' && typeof process.pid === 'number';
33
+ /**
34
+ * Fortuna CSPRNG — spec §9.3–§9.5
35
+ *
36
+ * Use `Fortuna.create()` to instantiate. Direct construction is not allowed.
37
+ */
38
+ export class Fortuna {
39
+ // ── Constants ──────────────────────────────────────────────────────────
40
+ static NUM_POOLS = 32;
41
+ static RESEED_LIMIT = 64; // bits — pool 0 threshold (spec §9.5)
42
+ static MS_PER_RESEED = 100; // ms — minimum reseed interval (spec §9.5)
43
+ static NODE_STATS_INTERVAL = 1000; // ms — OS stats collector interval
44
+ static CRYPTO_INTERVAL = 3000; // ms — crypto.randomBytes interval
45
+ // ── State ─────────────────────────────────────────────────────────────
46
+ serpent;
47
+ sha;
48
+ poolHash; // 32 running SHA-256 chain hashes (32 bytes each)
49
+ poolEntropy;
50
+ genKey;
51
+ genCnt;
52
+ reseedCnt;
53
+ lastReseed;
54
+ entropyLevel;
55
+ eventId;
56
+ active;
57
+ disposed;
58
+ msPerReseed;
59
+ robin;
60
+ // Collector references for cleanup
61
+ boundCollectors = {};
62
+ timers = [];
63
+ // ── Static factory ────────────────────────────────────────────────────
64
+ static async create(opts) {
65
+ if (!isInitialized('serpent'))
66
+ throw new Error('leviathan-crypto: call init([\'serpent\', \'sha2\']) before using Fortuna');
67
+ if (!isInitialized('sha2'))
68
+ throw new Error('leviathan-crypto: call init([\'serpent\', \'sha2\']) before using Fortuna');
69
+ const f = new Fortuna(opts?.msPerReseed ?? Fortuna.MS_PER_RESEED);
70
+ f.initialize(opts?.entropy);
71
+ return f;
72
+ }
73
+ constructor(msPerReseed) {
74
+ this.serpent = new Serpent();
75
+ this.sha = new SHA256();
76
+ this.poolHash = [];
77
+ this.poolEntropy = [];
78
+ this.genKey = new Uint8Array(32);
79
+ this.genCnt = new Uint8Array(16);
80
+ this.reseedCnt = 0;
81
+ this.lastReseed = 0;
82
+ this.entropyLevel = 0;
83
+ this.eventId = 0;
84
+ this.active = false;
85
+ this.disposed = false;
86
+ this.msPerReseed = msPerReseed;
87
+ this.robin = { kbd: 0, mouse: 0, scroll: 0, touch: 0, motion: 0, time: 0, rnd: 0, dom: 0 };
88
+ for (let i = 0; i < Fortuna.NUM_POOLS; i++) {
89
+ this.poolHash.push(new Uint8Array(32)); // zero-initialized chain value
90
+ this.poolEntropy.push(0);
91
+ }
92
+ }
93
+ // ── Public API ────────────────────────────────────────────────────────
94
+ /** Get n random bytes. Returns undefined if not yet seeded (reseedCnt === 0). */
95
+ get(length) {
96
+ if (this.disposed)
97
+ throw new Error('Fortuna instance has been disposed');
98
+ // Capture hrtime jitter at call time (Node.js) — spec §9.5
99
+ if (isNode)
100
+ this.captureHrtime();
101
+ // Check reseed trigger — spec §9.5
102
+ if (this.poolEntropy[0] >= Fortuna.RESEED_LIMIT &&
103
+ Date.now() >= this.lastReseed + this.msPerReseed) {
104
+ this.reseedCnt = (this.reseedCnt + 1) >>> 0; // u32 wrap
105
+ let seed = new Uint8Array(0);
106
+ let strength = 0;
107
+ for (let i = 0; i < Fortuna.NUM_POOLS; i++) {
108
+ if ((this.reseedCnt & (1 << i)) !== 0) {
109
+ // Pool digest = current chain hash
110
+ seed = concat(seed, this.poolHash[i]);
111
+ strength += this.poolEntropy[i];
112
+ // Reset pool
113
+ this.poolHash[i] = new Uint8Array(32);
114
+ this.poolEntropy[i] = 0;
115
+ }
116
+ }
117
+ this.entropyLevel -= strength;
118
+ this.reseed(seed);
119
+ }
120
+ if (this.reseedCnt === 0)
121
+ return undefined;
122
+ return this.pseudoRandomData(length);
123
+ }
124
+ /** Add external entropy to the pools. */
125
+ addEntropy(entropy) {
126
+ if (this.disposed)
127
+ throw new Error('Fortuna instance has been disposed');
128
+ this.addRandomEvent(entropy, this.robin.rnd, entropy.length * 8);
129
+ this.robin.rnd = (this.robin.rnd + 1) % Fortuna.NUM_POOLS;
130
+ }
131
+ /** Get estimated available entropy in bytes. */
132
+ getEntropy() {
133
+ if (this.disposed)
134
+ throw new Error('Fortuna instance has been disposed');
135
+ return Math.floor(this.entropyLevel / 8);
136
+ }
137
+ /** Permanently dispose this instance. Wipes key material, stops all collectors. */
138
+ stop() {
139
+ if (this.disposed)
140
+ throw new Error('Fortuna instance has been disposed');
141
+ this.stopCollectors();
142
+ wipe(this.genKey);
143
+ wipe(this.genCnt);
144
+ this.reseedCnt = 0;
145
+ this.disposed = true;
146
+ }
147
+ // ── Test-only accessors ───────────────────────────────────────────────
148
+ /** @internal — exposed for testing key replacement */
149
+ _getGenKey() {
150
+ return this.genKey;
151
+ }
152
+ /** @internal — exposed for testing pool state */
153
+ _getPoolEntropy() {
154
+ return this.poolEntropy;
155
+ }
156
+ /** @internal — exposed for testing reseed count */
157
+ _getReseedCnt() {
158
+ return this.reseedCnt;
159
+ }
160
+ // ── Generator (spec §9.4) ─────────────────────────────────────────────
161
+ /** Generate n blocks of 16 bytes each. — spec §9.4 */
162
+ generateBlocks(n) {
163
+ const out = new Uint8Array(n * 16);
164
+ for (let i = 0; i < n; i++) {
165
+ // Encrypt genCnt with Serpent-256 ECB
166
+ this.serpent.loadKey(this.genKey);
167
+ out.set(this.serpent.encryptBlock(this.genCnt), i * 16);
168
+ this.incrementCounter();
169
+ }
170
+ return out;
171
+ }
172
+ /** Get length pseudo-random bytes. — spec §9.4 */
173
+ pseudoRandomData(length) {
174
+ // Generate ceil(length/16) + 1 blocks — +1 ensures extra block before key replacement
175
+ const blocks = Math.ceil(length / 16) + 1;
176
+ const raw = this.generateBlocks(blocks);
177
+ const output = raw.slice(0, length);
178
+ // Key replacement — mandatory forward secrecy (spec §9.4)
179
+ this.genKey = this.generateBlocks(2);
180
+ return output;
181
+ }
182
+ /** Reseed the generator — spec §9.4 */
183
+ reseed(seed) {
184
+ // genKey = SHA256(genKey ‖ seed)
185
+ this.genKey = this.sha.hash(concat(this.genKey, seed));
186
+ // Increment counter — makes it nonzero on first reseed, marking generator as seeded
187
+ this.incrementCounter();
188
+ this.lastReseed = Date.now();
189
+ }
190
+ /** Increment 16-byte little-endian counter. — spec §9.4 */
191
+ incrementCounter() {
192
+ for (let i = 0; i < 16; i++) {
193
+ if (++this.genCnt[i] !== 0)
194
+ break;
195
+ }
196
+ }
197
+ // ── Accumulator (spec §9.5) ───────────────────────────────────────────
198
+ /** Add an event to a pool via hash chaining: poolHash[i] = SHA256(poolHash[i] ‖ eventId ‖ data). */
199
+ addRandomEvent(data, poolIdx, entropyBits) {
200
+ // Encode eventId as 4 bytes little-endian
201
+ const id = new Uint8Array(4);
202
+ id[0] = this.eventId & 0xff;
203
+ id[1] = (this.eventId >>> 8) & 0xff;
204
+ id[2] = (this.eventId >>> 16) & 0xff;
205
+ id[3] = (this.eventId >>> 24) & 0xff;
206
+ this.eventId = (this.eventId + 1) >>> 0; // u32 wrap
207
+ // Chain: poolHash[i] = SHA256(poolHash[i] ‖ id ‖ data)
208
+ this.poolHash[poolIdx] = this.sha.hash(concat(concat(this.poolHash[poolIdx], id), data));
209
+ this.poolEntropy[poolIdx] += entropyBits;
210
+ this.entropyLevel += entropyBits;
211
+ }
212
+ // ── Initialization ────────────────────────────────────────────────────
213
+ initialize(entropy) {
214
+ // Initial seeding — crypto random per pool (spec §9.5)
215
+ for (let i = 0; i < Fortuna.NUM_POOLS * 4; i++) {
216
+ this.collectorCryptoRandom();
217
+ }
218
+ // Timing entropy
219
+ this.collectorTime();
220
+ // DOM entropy (browser only)
221
+ this.collectorDom();
222
+ // Extra entropy from caller
223
+ if (entropy) {
224
+ this.addRandomEvent(entropy, this.robin.rnd, entropy.length * 8);
225
+ this.robin.rnd = (this.robin.rnd + 1) % Fortuna.NUM_POOLS;
226
+ }
227
+ this.startCollectors();
228
+ }
229
+ // ── Collectors ────────────────────────────────────────────────────────
230
+ startCollectors() {
231
+ if (this.active)
232
+ return;
233
+ if (isBrowser) {
234
+ const target = typeof window !== 'undefined' ? window : document;
235
+ if (target) {
236
+ this.boundCollectors.click = this.collectorClick.bind(this);
237
+ this.boundCollectors.keydown = this.collectorKeyboard.bind(this);
238
+ this.boundCollectors.scroll = this.collectorScroll.bind(this);
239
+ this.boundCollectors.mousemove = this.throttle(this.collectorMouse, 50, this);
240
+ this.boundCollectors.devicemotion = this.throttle(this.collectorMotion, 100, this);
241
+ this.boundCollectors.deviceorientation = this.collectorMotion.bind(this);
242
+ this.boundCollectors.orientationchange = this.collectorMotion.bind(this);
243
+ this.boundCollectors.touchmove = this.throttle(this.collectorTouch, 50, this);
244
+ this.boundCollectors.touchstart = this.collectorTouch.bind(this);
245
+ this.boundCollectors.touchend = this.collectorTouch.bind(this);
246
+ this.boundCollectors.load = this.collectorTime.bind(this);
247
+ for (const [event, handler] of Object.entries(this.boundCollectors)) {
248
+ target.addEventListener(event, handler, true);
249
+ }
250
+ }
251
+ }
252
+ if (isNode) {
253
+ // OS stats timer
254
+ this.timers.push(setInterval(() => this.collectNodeStats(), Fortuna.NODE_STATS_INTERVAL));
255
+ }
256
+ // Crypto timer — both environments
257
+ this.timers.push(setInterval(() => this.collectorCryptoRandom(), Fortuna.CRYPTO_INTERVAL));
258
+ this.active = true;
259
+ }
260
+ stopCollectors() {
261
+ if (!this.active)
262
+ return;
263
+ if (isBrowser && typeof window !== 'undefined') {
264
+ for (const [event, handler] of Object.entries(this.boundCollectors)) {
265
+ window.removeEventListener(event, handler, true);
266
+ }
267
+ }
268
+ for (const timer of this.timers)
269
+ clearInterval(timer);
270
+ this.timers = [];
271
+ this.boundCollectors = {};
272
+ this.active = false;
273
+ }
274
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- legacy throttle utility
275
+ throttle(fn, threshold, scope) {
276
+ let last;
277
+ let deferTimer;
278
+ return function (...args) {
279
+ const context = scope || this;
280
+ const now = Date.now();
281
+ if (last && now < last + threshold) {
282
+ clearTimeout(deferTimer);
283
+ deferTimer = setTimeout(() => {
284
+ last = now;
285
+ fn.apply(context, args);
286
+ }, threshold);
287
+ }
288
+ else {
289
+ last = now;
290
+ fn.apply(context, args);
291
+ }
292
+ };
293
+ }
294
+ collectorKeyboard(ev) {
295
+ const key = ev.key || '';
296
+ const b = new Uint8Array([key.charCodeAt(0) || 0, (ev.timeStamp || 0) & 0xff]);
297
+ this.addRandomEvent(b, this.robin.kbd, 1);
298
+ this.robin.kbd = (this.robin.kbd + 1) % Fortuna.NUM_POOLS;
299
+ this.collectorTime();
300
+ }
301
+ collectorMouse(ev) {
302
+ const x = ev.clientX || 0, y = ev.clientY || 0;
303
+ this.addRandomEvent(new Uint8Array([x >>> 8, x & 0xff, y >>> 8, y & 0xff]), this.robin.mouse, 2);
304
+ this.robin.mouse = (this.robin.mouse + 1) % Fortuna.NUM_POOLS;
305
+ }
306
+ collectorClick(ev) {
307
+ const x = ev.clientX || 0, y = ev.clientY || 0;
308
+ this.addRandomEvent(new Uint8Array([x >>> 8, x & 0xff, y >>> 8, y & 0xff]), this.robin.mouse, 2);
309
+ this.robin.mouse = (this.robin.mouse + 1) % Fortuna.NUM_POOLS;
310
+ this.collectorTime();
311
+ }
312
+ collectorTouch(ev) {
313
+ const touch = ev.touches[0] || ev.changedTouches[0];
314
+ if (!touch)
315
+ return;
316
+ const x = touch.pageX || touch.clientX || 0;
317
+ const y = touch.pageY || touch.clientY || 0;
318
+ this.addRandomEvent(new Uint8Array([x >>> 8, x & 0xff, y >>> 8, y & 0xff]), this.robin.touch, 2);
319
+ this.robin.touch = (this.robin.touch + 1) % Fortuna.NUM_POOLS;
320
+ this.collectorTime();
321
+ }
322
+ collectorScroll() {
323
+ if (typeof window === 'undefined')
324
+ return;
325
+ const x = window.scrollX || 0, y = window.scrollY || 0;
326
+ this.addRandomEvent(new Uint8Array([x >>> 8, x & 0xff, y >>> 8, y & 0xff]), this.robin.scroll, 1);
327
+ this.robin.scroll = (this.robin.scroll + 1) % Fortuna.NUM_POOLS;
328
+ }
329
+ collectorMotion(ev) {
330
+ const motion = ev;
331
+ const orient = ev;
332
+ if (motion.accelerationIncludingGravity) {
333
+ const a = motion.accelerationIncludingGravity;
334
+ const x = a.x || 0, y = a.y || 0, z = a.z || 0;
335
+ this.addRandomEvent(new Uint8Array([(x * 100) & 0xff, (y * 100) & 0xff, (z * 100) & 0xff]), this.robin.motion, 3);
336
+ }
337
+ if (typeof orient.alpha === 'number' && typeof orient.beta === 'number' && typeof orient.gamma === 'number') {
338
+ this.addRandomEvent(utf8ToBytes(orient.alpha.toString() + orient.beta.toString() + orient.gamma.toString()), this.robin.motion, 3);
339
+ }
340
+ this.robin.motion = (this.robin.motion + 1) % Fortuna.NUM_POOLS;
341
+ }
342
+ collectorTime() {
343
+ if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
344
+ this.addRandomEvent(utf8ToBytes(performance.now().toString()), this.robin.time, 2);
345
+ }
346
+ else {
347
+ const t = Date.now();
348
+ const b = new Uint8Array(4);
349
+ b[0] = t & 0xff;
350
+ b[1] = (t >>> 8) & 0xff;
351
+ b[2] = (t >>> 16) & 0xff;
352
+ b[3] = (t >>> 24) & 0xff;
353
+ this.addRandomEvent(b, this.robin.time, 2);
354
+ }
355
+ this.robin.time = (this.robin.time + 1) % Fortuna.NUM_POOLS;
356
+ }
357
+ collectorDom() {
358
+ if (typeof document !== 'undefined' && document.documentElement) {
359
+ this.addRandomEvent(this.sha.hash(utf8ToBytes(document.documentElement.innerHTML)), this.robin.dom, 2);
360
+ this.robin.dom = (this.robin.dom + 1) % Fortuna.NUM_POOLS;
361
+ }
362
+ }
363
+ collectorCryptoRandom() {
364
+ try {
365
+ const rnd = new Uint8Array(128);
366
+ if (typeof globalThis.crypto !== 'undefined' && typeof globalThis.crypto.getRandomValues === 'function') {
367
+ globalThis.crypto.getRandomValues(rnd);
368
+ }
369
+ else if (isNode) {
370
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
371
+ const nodeCrypto = require('node:crypto');
372
+ const buf = nodeCrypto.randomBytes(128);
373
+ rnd.set(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
374
+ }
375
+ else {
376
+ return; // no crypto source available
377
+ }
378
+ this.addRandomEvent(rnd, this.robin.rnd, 1024);
379
+ this.robin.rnd = (this.robin.rnd + 1) % Fortuna.NUM_POOLS;
380
+ }
381
+ catch { /* crypto may not be available */ }
382
+ }
383
+ captureHrtime() {
384
+ try {
385
+ const hr = process.hrtime.bigint();
386
+ const hrBytes = new Uint8Array(8);
387
+ for (let i = 0; i < 8; i++)
388
+ hrBytes[i] = Number((hr >> BigInt(i * 8)) & 0xffn);
389
+ this.addRandomEvent(hrBytes, this.robin.time, 8);
390
+ this.robin.time = (this.robin.time + 1) % Fortuna.NUM_POOLS;
391
+ }
392
+ catch { /* hrtime may not be available */ }
393
+ }
394
+ collectNodeStats() {
395
+ try {
396
+ // hrtime — nanosecond scheduling jitter
397
+ const hr = process.hrtime.bigint();
398
+ const hrBytes = new Uint8Array(8);
399
+ for (let i = 0; i < 8; i++)
400
+ hrBytes[i] = Number((hr >> BigInt(i * 8)) & 0xffn);
401
+ this.addRandomEvent(hrBytes, this.robin.time, 8);
402
+ this.robin.time = (this.robin.time + 1) % Fortuna.NUM_POOLS;
403
+ // cpuUsage — user + system CPU microseconds
404
+ const cpu = process.cpuUsage();
405
+ const cpuBytes = new Uint8Array(8);
406
+ cpuBytes[0] = cpu.user & 0xff;
407
+ cpuBytes[1] = (cpu.user >>> 8) & 0xff;
408
+ cpuBytes[2] = (cpu.user >>> 16) & 0xff;
409
+ cpuBytes[3] = (cpu.user >>> 24) & 0xff;
410
+ cpuBytes[4] = cpu.system & 0xff;
411
+ cpuBytes[5] = (cpu.system >>> 8) & 0xff;
412
+ cpuBytes[6] = (cpu.system >>> 16) & 0xff;
413
+ cpuBytes[7] = (cpu.system >>> 24) & 0xff;
414
+ this.addRandomEvent(cpuBytes, this.robin.rnd, 2);
415
+ this.robin.rnd = (this.robin.rnd + 1) % Fortuna.NUM_POOLS;
416
+ // memoryUsage — heapUsed changes constantly
417
+ const mem = process.memoryUsage();
418
+ const memVal = mem.heapUsed;
419
+ const memBytes = new Uint8Array(4);
420
+ memBytes[0] = memVal & 0xff;
421
+ memBytes[1] = (memVal >>> 8) & 0xff;
422
+ memBytes[2] = (memVal >>> 16) & 0xff;
423
+ memBytes[3] = (memVal >>> 24) & 0xff;
424
+ this.addRandomEvent(memBytes, this.robin.rnd, 1);
425
+ this.robin.rnd = (this.robin.rnd + 1) % Fortuna.NUM_POOLS;
426
+ // loadavg — slow-changing but real system state
427
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
428
+ const os = require('node:os');
429
+ const la = os.loadavg();
430
+ const laStr = la.map((n) => Math.round(n * 1000).toString()).join('');
431
+ this.addRandomEvent(utf8ToBytes(laStr), this.robin.time, 1);
432
+ this.robin.time = (this.robin.time + 1) % Fortuna.NUM_POOLS;
433
+ // freemem — changes with allocation activity
434
+ const fm = os.freemem();
435
+ const fmBytes = new Uint8Array(4);
436
+ fmBytes[0] = fm & 0xff;
437
+ fmBytes[1] = (fm >>> 8) & 0xff;
438
+ fmBytes[2] = (fm >>> 16) & 0xff;
439
+ fmBytes[3] = (fm >>> 24) & 0xff;
440
+ this.addRandomEvent(fmBytes, this.robin.rnd, 1);
441
+ this.robin.rnd = (this.robin.rnd + 1) % Fortuna.NUM_POOLS;
442
+ }
443
+ catch { /* Node APIs may not be available */ }
444
+ }
445
+ }
@@ -0,0 +1,13 @@
1
+ import type { Module, Mode, InitOpts } from './init.js';
2
+ export declare function init(modules: Module | Module[], mode?: Mode, opts?: InitOpts): Promise<void>;
3
+ export { type Module, type Mode, type InitOpts, isInitialized, _resetForTesting } from './init.js';
4
+ export { serpentInit, SerpentSeal, Serpent, SerpentCtr, SerpentCbc, SerpentStream, SerpentStreamPool, SerpentStreamSealer, SerpentStreamOpener, SerpentStreamEncoder, SerpentStreamDecoder, _serpentReady } from './serpent/index.js';
5
+ export type { StreamPoolOpts } from './serpent/index.js';
6
+ export { chacha20Init, ChaCha20, Poly1305, ChaCha20Poly1305, XChaCha20Poly1305, _chachaReady } from './chacha20/index.js';
7
+ export { XChaCha20Poly1305Pool } from './chacha20/pool.js';
8
+ export type { PoolOpts } from './chacha20/pool.js';
9
+ export { sha2Init, SHA256, SHA512, SHA384, HMAC_SHA256, HMAC_SHA512, HMAC_SHA384, HKDF_SHA256, HKDF_SHA512, _sha2Ready } from './sha2/index.js';
10
+ export { sha3Init, SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, _sha3Ready } from './sha3/index.js';
11
+ export { Fortuna } from './fortuna.js';
12
+ export type { Hash, KeyedHash, Blockcipher, Streamcipher, AEAD } from './types.js';
13
+ export { hexToBytes, bytesToHex, utf8ToBytes, bytesToUtf8, base64ToBytes, bytesToBase64, constantTimeEqual, wipe, xor, concat, randomBytes, } from './utils.js';
package/dist/index.js ADDED
@@ -0,0 +1,44 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // Root barrel — re-exports everything
23
+ import { serpentInit } from './serpent/index.js';
24
+ import { chacha20Init } from './chacha20/index.js';
25
+ import { sha2Init } from './sha2/index.js';
26
+ import { sha3Init } from './sha3/index.js';
27
+ const _dispatchers = {
28
+ serpent: serpentInit,
29
+ chacha20: chacha20Init,
30
+ sha2: sha2Init,
31
+ sha3: sha3Init,
32
+ };
33
+ export async function init(modules, mode = 'embedded', opts) {
34
+ const list = Array.isArray(modules) ? modules : [modules];
35
+ await Promise.all(list.map(mod => _dispatchers[mod](mode, opts)));
36
+ }
37
+ export { isInitialized, _resetForTesting } from './init.js';
38
+ export { serpentInit, SerpentSeal, Serpent, SerpentCtr, SerpentCbc, SerpentStream, SerpentStreamPool, SerpentStreamSealer, SerpentStreamOpener, SerpentStreamEncoder, SerpentStreamDecoder, _serpentReady } from './serpent/index.js';
39
+ export { chacha20Init, ChaCha20, Poly1305, ChaCha20Poly1305, XChaCha20Poly1305, _chachaReady } from './chacha20/index.js';
40
+ export { XChaCha20Poly1305Pool } from './chacha20/pool.js';
41
+ export { sha2Init, SHA256, SHA512, SHA384, HMAC_SHA256, HMAC_SHA512, HMAC_SHA384, HKDF_SHA256, HKDF_SHA512, _sha2Ready } from './sha2/index.js';
42
+ export { sha3Init, SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, _sha3Ready } from './sha3/index.js';
43
+ export { Fortuna } from './fortuna.js';
44
+ export { hexToBytes, bytesToHex, utf8ToBytes, bytesToUtf8, base64ToBytes, bytesToBase64, constantTimeEqual, wipe, xor, concat, randomBytes, } from './utils.js';
package/dist/init.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type Module = 'serpent' | 'chacha20' | 'sha2' | 'sha3';
2
+ export type Mode = 'embedded' | 'streaming' | 'manual';
3
+ export interface InitOpts {
4
+ wasmUrl?: URL | string;
5
+ wasmBinary?: Partial<Record<Module, Uint8Array | ArrayBuffer>>;
6
+ }
7
+ export declare function initModule(mod: Module, embeddedThunk: () => Promise<string>, mode?: Mode, opts?: InitOpts): Promise<void>;
8
+ export declare function getInstance(mod: Module): WebAssembly.Instance;
9
+ export declare function isInitialized(mod: Module): boolean;
10
+ /** Reset all cached instances — for testing only */
11
+ export declare function _resetForTesting(): void;
package/dist/init.js ADDED
@@ -0,0 +1,49 @@
1
+ // Module-scope cache: one WebAssembly.Instance per module
2
+ const instances = new Map();
3
+ // Map from public module name to WASM filename
4
+ const WASM_FILES = {
5
+ serpent: 'serpent.wasm',
6
+ chacha20: 'chacha.wasm',
7
+ sha2: 'sha2.wasm',
8
+ sha3: 'sha3.wasm',
9
+ };
10
+ export async function initModule(mod, embeddedThunk, mode = 'embedded', opts) {
11
+ if (instances.has(mod))
12
+ return;
13
+ let instance;
14
+ if (mode === 'embedded') {
15
+ const { loadEmbedded } = await import('./loader.js');
16
+ instance = await loadEmbedded(embeddedThunk);
17
+ }
18
+ else if (mode === 'streaming') {
19
+ if (!opts?.wasmUrl)
20
+ throw new Error('leviathan-crypto: streaming mode requires wasmUrl');
21
+ const { loadStreaming } = await import('./loader.js');
22
+ instance = await loadStreaming(mod, opts.wasmUrl, WASM_FILES[mod]);
23
+ }
24
+ else if (mode === 'manual') {
25
+ const binary = opts?.wasmBinary?.[mod];
26
+ if (!binary)
27
+ throw new Error(`leviathan-crypto: manual mode requires wasmBinary['${mod}']`);
28
+ const { loadManual } = await import('./loader.js');
29
+ instance = await loadManual(binary);
30
+ }
31
+ else {
32
+ throw new Error(`leviathan-crypto: unknown mode '${mode}'`);
33
+ }
34
+ instances.set(mod, instance);
35
+ }
36
+ export function getInstance(mod) {
37
+ const inst = instances.get(mod);
38
+ if (!inst) {
39
+ throw new Error(`leviathan-crypto: call init(['${mod}']) before using this class`);
40
+ }
41
+ return inst;
42
+ }
43
+ export function isInitialized(mod) {
44
+ return instances.has(mod);
45
+ }
46
+ /** Reset all cached instances — for testing only */
47
+ export function _resetForTesting() {
48
+ instances.clear();
49
+ }
@@ -0,0 +1,4 @@
1
+ import type { Module } from './init.js';
2
+ export declare function loadEmbedded(thunk: () => Promise<string>): Promise<WebAssembly.Instance>;
3
+ export declare function loadStreaming(_mod: Module, baseUrl: URL | string, filename: string): Promise<WebAssembly.Instance>;
4
+ export declare function loadManual(binary: Uint8Array | ArrayBuffer): Promise<WebAssembly.Instance>;
package/dist/loader.js ADDED
@@ -0,0 +1,30 @@
1
+ function base64ToBytes(b64) {
2
+ if (typeof atob === 'function') {
3
+ const raw = atob(b64);
4
+ const out = new Uint8Array(raw.length);
5
+ for (let i = 0; i < raw.length; i++)
6
+ out[i] = raw.charCodeAt(i);
7
+ return out;
8
+ }
9
+ return new Uint8Array(Buffer.from(b64, 'base64'));
10
+ }
11
+ async function instantiateFromBytes(bytes) {
12
+ const result = await WebAssembly.instantiate(bytes.buffer, { env: { memory: new WebAssembly.Memory({ initial: 3, maximum: 3 }) } });
13
+ return result.instance;
14
+ }
15
+ export async function loadEmbedded(thunk) {
16
+ const b64 = await thunk();
17
+ const bytes = base64ToBytes(b64);
18
+ return instantiateFromBytes(bytes);
19
+ }
20
+ export async function loadStreaming(_mod, baseUrl, filename) {
21
+ const url = new URL(filename, baseUrl).href;
22
+ const result = await WebAssembly.instantiateStreaming(fetch(url), {
23
+ env: { memory: new WebAssembly.Memory({ initial: 3, maximum: 3 }) },
24
+ });
25
+ return result.instance;
26
+ }
27
+ export async function loadManual(binary) {
28
+ const bytes = binary instanceof ArrayBuffer ? new Uint8Array(binary) : binary;
29
+ return instantiateFromBytes(bytes);
30
+ }
@@ -0,0 +1,65 @@
1
+ import type { Mode, InitOpts } from '../init.js';
2
+ export declare function serpentInit(mode?: Mode, opts?: InitOpts): Promise<void>;
3
+ export declare class Serpent {
4
+ private readonly x;
5
+ constructor();
6
+ loadKey(key: Uint8Array): void;
7
+ encryptBlock(plaintext: Uint8Array): Uint8Array;
8
+ decryptBlock(ciphertext: Uint8Array): Uint8Array;
9
+ dispose(): void;
10
+ }
11
+ /**
12
+ * Serpent-256 in CTR mode.
13
+ *
14
+ * **WARNING: CTR mode is unauthenticated.** An attacker can flip ciphertext
15
+ * bits without detection. Always pair with HMAC-SHA256 (Encrypt-then-MAC)
16
+ * or use `XChaCha20Poly1305` instead.
17
+ */
18
+ export declare class SerpentCtr {
19
+ private readonly x;
20
+ constructor(opts?: {
21
+ dangerUnauthenticated: true;
22
+ });
23
+ beginEncrypt(key: Uint8Array, nonce: Uint8Array): void;
24
+ encryptChunk(chunk: Uint8Array): Uint8Array;
25
+ beginDecrypt(key: Uint8Array, nonce: Uint8Array): void;
26
+ decryptChunk(chunk: Uint8Array): Uint8Array;
27
+ dispose(): void;
28
+ }
29
+ /**
30
+ * Serpent-256 in CBC mode with PKCS7 padding.
31
+ *
32
+ * **WARNING: CBC mode is unauthenticated.** Always authenticate the output
33
+ * with HMAC-SHA256 (Encrypt-then-MAC) or use `XChaCha20Poly1305` instead.
34
+ */
35
+ export declare class SerpentCbc {
36
+ private readonly x;
37
+ constructor(opts?: {
38
+ dangerUnauthenticated: true;
39
+ });
40
+ private get mem();
41
+ /**
42
+ * Encrypt plaintext with Serpent-256 CBC + PKCS7 padding.
43
+ *
44
+ * @param key 16, 24, or 32 bytes
45
+ * @param iv 16 bytes — must be random and unique per (key, message)
46
+ * @param plaintext any length — PKCS7 padding applied automatically
47
+ * @returns ciphertext (length = ceil((plaintext.length + 1) / 16) * 16)
48
+ */
49
+ encrypt(key: Uint8Array, iv: Uint8Array, plaintext: Uint8Array): Uint8Array;
50
+ /**
51
+ * Decrypt Serpent-256 CBC + PKCS7.
52
+ * Throws if ciphertext length is not a non-zero multiple of 16 or PKCS7 is invalid.
53
+ */
54
+ decrypt(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Uint8Array;
55
+ dispose(): void;
56
+ private _loadKey;
57
+ private _setIv;
58
+ }
59
+ export { SerpentSeal } from './seal.js';
60
+ export { SerpentStream, sealChunk, openChunk } from './stream.js';
61
+ export { SerpentStreamPool } from './stream-pool.js';
62
+ export type { StreamPoolOpts } from './stream-pool.js';
63
+ export { SerpentStreamSealer, SerpentStreamOpener } from './stream-sealer.js';
64
+ export { SerpentStreamEncoder, SerpentStreamDecoder } from './stream-encoder.js';
65
+ export declare function _serpentReady(): boolean;