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,285 @@
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/serpent/stream-pool.ts
23
+ //
24
+ // SerpentStreamPool — parallel worker pool for SerpentStream.
25
+ // Dispatches chunk-level seal/open jobs across Web Workers, each
26
+ // with its own serpent.wasm and sha2.wasm instances.
27
+ import { isInitialized } from '../init.js';
28
+ import { HKDF_SHA256 } from '../sha2/index.js';
29
+ import { u32be, u64be, deriveChunkKeys } from './stream.js';
30
+ // ── Constants ─────────────────────────────────────────────────────────────────
31
+ const CHUNK_MIN = 1024;
32
+ const CHUNK_MAX = 65536;
33
+ const CHUNK_DEF = 65536;
34
+ // ── Module-private base64 decoder ─────────────────────────────────────────────
35
+ function base64ToBytes(b64) {
36
+ if (typeof atob === 'function') {
37
+ const raw = atob(b64);
38
+ const out = new Uint8Array(raw.length);
39
+ for (let i = 0; i < raw.length; i++)
40
+ out[i] = raw.charCodeAt(i);
41
+ return out;
42
+ }
43
+ return new Uint8Array(Buffer.from(b64, 'base64'));
44
+ }
45
+ // ── WASM module singletons ──────────────────────────────────────────────────
46
+ let _serpentModule;
47
+ let _sha2Module;
48
+ async function getSerpentModule() {
49
+ if (_serpentModule)
50
+ return _serpentModule;
51
+ const { WASM_BASE64 } = await import('../embedded/serpent.js');
52
+ const bytes = base64ToBytes(WASM_BASE64);
53
+ _serpentModule = await WebAssembly.compile(bytes.buffer);
54
+ return _serpentModule;
55
+ }
56
+ async function getSha2Module() {
57
+ if (_sha2Module)
58
+ return _sha2Module;
59
+ const { WASM_BASE64 } = await import('../embedded/sha2.js');
60
+ const bytes = base64ToBytes(WASM_BASE64);
61
+ _sha2Module = await WebAssembly.compile(bytes.buffer);
62
+ return _sha2Module;
63
+ }
64
+ // ── Worker spawning ──────────────────────────────────────────────────────────
65
+ function spawnWorker(serpentMod, sha2Mod) {
66
+ return new Promise((resolve, reject) => {
67
+ const worker = new Worker(new URL('./stream.worker.js', import.meta.url), { type: 'module' });
68
+ const onMessage = (e) => {
69
+ cleanup();
70
+ if (e.data.type === 'ready') {
71
+ resolve(worker);
72
+ }
73
+ else {
74
+ worker.terminate();
75
+ reject(new Error(`leviathan-crypto: worker init failed: ${e.data.message}`));
76
+ }
77
+ };
78
+ const onError = (e) => {
79
+ cleanup();
80
+ worker.terminate();
81
+ reject(new Error(`leviathan-crypto: worker init failed: ${e.message}`));
82
+ };
83
+ const cleanup = () => {
84
+ worker.removeEventListener('message', onMessage);
85
+ worker.removeEventListener('error', onError);
86
+ };
87
+ worker.addEventListener('message', onMessage);
88
+ worker.addEventListener('error', onError);
89
+ worker.postMessage({ type: 'init', serpentModule: serpentMod, sha2Module: sha2Mod });
90
+ });
91
+ }
92
+ // ── Pool class ───────────────────────────────────────────────────────────────
93
+ /**
94
+ * Parallel worker pool for SerpentStream chunked authenticated encryption.
95
+ *
96
+ * Each worker owns its own `serpent.wasm` and `sha2.wasm` instances with
97
+ * isolated linear memory. Key derivation happens on the main thread; workers
98
+ * receive pre-derived encKey/macKey per chunk.
99
+ *
100
+ * Produces the same wire format as `SerpentStream` -- either can decrypt
101
+ * the other's output.
102
+ */
103
+ export class SerpentStreamPool {
104
+ _workers;
105
+ _idle;
106
+ _queue;
107
+ _pending;
108
+ _hkdf;
109
+ _nextId;
110
+ _disposed;
111
+ constructor(workers, hkdf) {
112
+ this._workers = workers;
113
+ this._idle = [...workers];
114
+ this._queue = [];
115
+ this._pending = new Map();
116
+ this._hkdf = hkdf;
117
+ this._nextId = 0;
118
+ this._disposed = false;
119
+ for (const w of workers) {
120
+ w.onmessage = (e) => this._onMessage(w, e);
121
+ }
122
+ }
123
+ /**
124
+ * Create a new pool. Requires `init(['serpent', 'sha2'])` to have been called.
125
+ * Compiles both WASM modules once and distributes them to all workers.
126
+ */
127
+ static async create(opts) {
128
+ if (!isInitialized('serpent') || !isInitialized('sha2'))
129
+ throw new Error('leviathan-crypto: call init([\'serpent\', \'sha2\']) before using SerpentStreamPool');
130
+ const n = opts?.workers ?? (typeof navigator !== 'undefined' ? navigator.hardwareConcurrency : undefined) ?? 4;
131
+ const [serpentMod, sha2Mod] = await Promise.all([getSerpentModule(), getSha2Module()]);
132
+ // Sequential spawn — compatible with inline-worker test environments
133
+ const workers = [];
134
+ for (let i = 0; i < n; i++)
135
+ workers.push(await spawnWorker(serpentMod, sha2Mod));
136
+ const hkdf = new HKDF_SHA256();
137
+ return new SerpentStreamPool(workers, hkdf);
138
+ }
139
+ /**
140
+ * Encrypt plaintext with SerpentStream chunked authenticated encryption.
141
+ * Returns the complete wire format (header + encrypted chunks).
142
+ */
143
+ async seal(key, plaintext, chunkSize) {
144
+ if (this._disposed)
145
+ throw new Error('leviathan-crypto: pool is disposed');
146
+ if (key.length !== 32)
147
+ throw new RangeError(`SerpentStream key must be 32 bytes (got ${key.length})`);
148
+ const cs = chunkSize ?? CHUNK_DEF;
149
+ if (cs < CHUNK_MIN || cs > CHUNK_MAX)
150
+ throw new RangeError(`SerpentStream chunkSize must be ${CHUNK_MIN}..${CHUNK_MAX} (got ${cs})`);
151
+ const streamNonce = new Uint8Array(16);
152
+ crypto.getRandomValues(streamNonce);
153
+ const chunkCount = plaintext.length === 0 ? 1 : Math.ceil(plaintext.length / cs);
154
+ // Dispatch all chunk jobs in parallel
155
+ const chunkPromises = [];
156
+ for (let i = 0; i < chunkCount; i++) {
157
+ const start = i * cs;
158
+ const end = Math.min(start + cs, plaintext.length);
159
+ const slice = plaintext.slice(start, end);
160
+ const isLast = i === chunkCount - 1;
161
+ const { encKey, macKey } = deriveChunkKeys(this._hkdf, key, streamNonce, cs, chunkCount, i, isLast);
162
+ chunkPromises.push(this._dispatch('seal', encKey, macKey, slice));
163
+ }
164
+ const chunkResults = await Promise.all(chunkPromises);
165
+ // Compute total output size
166
+ let totalWire = 28;
167
+ for (const c of chunkResults)
168
+ totalWire += c.length;
169
+ const out = new Uint8Array(totalWire);
170
+ // Write header
171
+ out.set(streamNonce, 0);
172
+ out.set(u32be(cs), 16);
173
+ out.set(u64be(chunkCount), 20);
174
+ let pos = 28;
175
+ for (const c of chunkResults) {
176
+ out.set(c, pos);
177
+ pos += c.length;
178
+ }
179
+ return out;
180
+ }
181
+ /**
182
+ * Decrypt SerpentStream wire format.
183
+ * If any chunk fails authentication, rejects immediately -- no partial plaintext.
184
+ */
185
+ async open(key, ciphertext) {
186
+ if (this._disposed)
187
+ throw new Error('leviathan-crypto: pool is disposed');
188
+ if (key.length !== 32)
189
+ throw new RangeError(`SerpentStream key must be 32 bytes (got ${key.length})`);
190
+ if (ciphertext.length < 28 + 32)
191
+ throw new RangeError('SerpentStream: ciphertext too short');
192
+ // Parse header
193
+ const streamNonce = ciphertext.subarray(0, 16);
194
+ const csView = ciphertext.subarray(16, 20);
195
+ const cs = (csView[0] << 24) | (csView[1] << 16) | (csView[2] << 8) | csView[3];
196
+ const ccView = ciphertext.subarray(20, 28);
197
+ let chunkCount = 0;
198
+ for (let i = 0; i < 8; i++)
199
+ chunkCount = chunkCount * 256 + ccView[i];
200
+ // Dispatch all chunk jobs
201
+ const chunkPromises = [];
202
+ let pos = 28;
203
+ for (let i = 0; i < chunkCount; i++) {
204
+ const isLast = i === chunkCount - 1;
205
+ const wireLen = isLast ? ciphertext.length - pos : cs + 32;
206
+ const wireSlice = ciphertext.slice(pos, pos + wireLen);
207
+ const { encKey, macKey } = deriveChunkKeys(this._hkdf, key, streamNonce, cs, chunkCount, i, isLast);
208
+ chunkPromises.push(this._dispatch('open', encKey, macKey, wireSlice));
209
+ pos += wireLen;
210
+ }
211
+ // Await all — Promise.all rejects on first failure
212
+ const results = await Promise.all(chunkPromises);
213
+ // Reassemble plaintext
214
+ let totalPt = 0;
215
+ for (const r of results)
216
+ totalPt += r.length;
217
+ const plaintext = new Uint8Array(totalPt);
218
+ let ptPos = 0;
219
+ for (const r of results) {
220
+ plaintext.set(r, ptPos);
221
+ ptPos += r.length;
222
+ }
223
+ return plaintext;
224
+ }
225
+ /** Terminates all workers. Rejects all pending and queued jobs. */
226
+ dispose() {
227
+ if (this._disposed)
228
+ return;
229
+ this._disposed = true;
230
+ for (const w of this._workers)
231
+ w.terminate();
232
+ const err = new Error('leviathan-crypto: pool disposed');
233
+ for (const { reject } of this._pending.values())
234
+ reject(err);
235
+ for (const job of this._queue)
236
+ this._pending.get(job.id)?.reject(err);
237
+ this._pending.clear();
238
+ this._queue.length = 0;
239
+ this._hkdf.dispose();
240
+ }
241
+ /** Number of workers in the pool. */
242
+ get size() {
243
+ return this._workers.length;
244
+ }
245
+ /** Number of jobs currently queued (waiting for a free worker). */
246
+ get queueDepth() {
247
+ return this._queue.length;
248
+ }
249
+ // ── Internals ────────────────────────────────────────────────────────────
250
+ _dispatch(op, encKey, macKey, data) {
251
+ return new Promise((resolve, reject) => {
252
+ const id = this._nextId++;
253
+ const job = { id, op, encKey, macKey, data };
254
+ this._pending.set(id, { resolve, reject });
255
+ const worker = this._idle.pop();
256
+ if (worker)
257
+ this._send(worker, job);
258
+ else
259
+ this._queue.push(job);
260
+ });
261
+ }
262
+ _send(worker, job) {
263
+ // No transfer list: @vitest/web-worker (same-thread fake worker used in
264
+ // Vitest test environment) silently drops postMessage calls that include a
265
+ // non-empty Transferable array. encKey/macKey/data are already .slice()
266
+ // copies — neutering was never part of SerpentStreamPool's public contract.
267
+ worker.postMessage({ type: 'job', ...job });
268
+ }
269
+ _onMessage(worker, e) {
270
+ const msg = e.data;
271
+ const job = this._pending.get(msg.id);
272
+ if (!job)
273
+ return;
274
+ this._pending.delete(msg.id);
275
+ if (msg.type === 'result')
276
+ job.resolve(msg.data);
277
+ else
278
+ job.reject(new Error(msg.message));
279
+ const next = this._queue.shift();
280
+ if (next)
281
+ this._send(worker, next);
282
+ else
283
+ this._idle.push(worker);
284
+ }
285
+ }
@@ -0,0 +1,34 @@
1
+ export declare class SerpentStreamSealer {
2
+ private readonly _key;
3
+ private readonly _cs;
4
+ private readonly _nonce;
5
+ private readonly _cbc;
6
+ private readonly _hmac;
7
+ private readonly _hkdf;
8
+ private readonly _ivs;
9
+ private _ivIdx;
10
+ private _index;
11
+ private _state;
12
+ constructor(key: Uint8Array, chunkSize?: number, _nonce?: Uint8Array, _ivs?: Uint8Array[]);
13
+ header(): Uint8Array;
14
+ seal(plaintext: Uint8Array): Uint8Array;
15
+ final(plaintext: Uint8Array): Uint8Array;
16
+ private _sealChunk;
17
+ private _wipe;
18
+ dispose(): void;
19
+ }
20
+ export declare class SerpentStreamOpener {
21
+ private readonly _key;
22
+ private readonly _cs;
23
+ private readonly _nonce;
24
+ private readonly _cbc;
25
+ private readonly _hmac;
26
+ private readonly _hkdf;
27
+ private _index;
28
+ private _dead;
29
+ constructor(key: Uint8Array, header: Uint8Array);
30
+ get closed(): boolean;
31
+ open(chunk: Uint8Array): Uint8Array;
32
+ private _wipe;
33
+ dispose(): void;
34
+ }
@@ -0,0 +1,223 @@
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/serpent/stream-sealer.ts
23
+ //
24
+ // Tier 2 pure-TS composition: SerpentCbc + HMAC_SHA256 + HKDF_SHA256.
25
+ // Incremental streaming AEAD — seal/open one chunk at a time.
26
+ // Wire format: header (20) | chunks: IV(16) || ct(padded) || HMAC(32)
27
+ import { SerpentCbc, _serpentReady } from './index.js';
28
+ import { HMAC_SHA256, HKDF_SHA256, _sha2Ready } from '../sha2/index.js';
29
+ import { concat, constantTimeEqual, wipe } from '../utils.js';
30
+ import { u32be, u64be } from './stream.js';
31
+ const DOMAIN = 'serpent-sealstream-v1';
32
+ const DOMAIN_BYTES = new TextEncoder().encode(DOMAIN); // 21 bytes
33
+ const CHUNK_MIN = 1024;
34
+ const CHUNK_MAX = 65536;
35
+ const CHUNK_DEF = 65536;
36
+ function chunkInfo(streamNonce, chunkSize, index, isLast) {
37
+ // 21 + 16 + 4 + 8 + 1 = 50 bytes
38
+ const info = new Uint8Array(50);
39
+ let off = 0;
40
+ info.set(DOMAIN_BYTES, off);
41
+ off += 21;
42
+ info.set(streamNonce, off);
43
+ off += 16;
44
+ info.set(u32be(chunkSize), off);
45
+ off += 4;
46
+ info.set(u64be(index), off);
47
+ off += 8;
48
+ info[off] = isLast ? 0x01 : 0x00;
49
+ return info;
50
+ }
51
+ function deriveChunkKeys(hkdf, key, streamNonce, chunkSize, index, isLast) {
52
+ const info = chunkInfo(streamNonce, chunkSize, index, isLast);
53
+ const derived = hkdf.derive(key, new Uint8Array(0), info, 64);
54
+ return {
55
+ encKey: derived.subarray(0, 32),
56
+ macKey: derived.subarray(32, 64),
57
+ };
58
+ }
59
+ export class SerpentStreamSealer {
60
+ _key;
61
+ _cs; // chunk size
62
+ _nonce; // stream nonce (16 bytes)
63
+ _cbc;
64
+ _hmac;
65
+ _hkdf;
66
+ _ivs; // test seam: fixed IVs
67
+ _ivIdx;
68
+ _index;
69
+ _state;
70
+ // _nonce, _ivs: test seams — inject fixed nonce/IVs for deterministic KAT vectors
71
+ constructor(key, chunkSize, _nonce, _ivs) {
72
+ if (!_serpentReady())
73
+ throw new Error('leviathan-crypto: call init([\'serpent\']) before using SerpentStreamSealer');
74
+ if (!_sha2Ready())
75
+ throw new Error('leviathan-crypto: call init([\'sha2\']) before using SerpentStreamSealer');
76
+ if (key.length !== 64)
77
+ throw new RangeError(`SerpentStreamSealer key must be 64 bytes (got ${key.length})`);
78
+ const cs = chunkSize ?? CHUNK_DEF;
79
+ if (cs < CHUNK_MIN || cs > CHUNK_MAX)
80
+ throw new RangeError(`SerpentStreamSealer chunkSize must be ${CHUNK_MIN}..${CHUNK_MAX} (got ${cs})`);
81
+ this._key = key.slice();
82
+ this._cs = cs;
83
+ this._nonce = new Uint8Array(16);
84
+ if (_nonce && _nonce.length === 16) {
85
+ this._nonce.set(_nonce);
86
+ }
87
+ else {
88
+ crypto.getRandomValues(this._nonce);
89
+ }
90
+ this._cbc = new SerpentCbc({ dangerUnauthenticated: true });
91
+ this._hmac = new HMAC_SHA256();
92
+ this._hkdf = new HKDF_SHA256();
93
+ this._ivs = _ivs;
94
+ this._ivIdx = 0;
95
+ this._index = 0;
96
+ this._state = 'fresh';
97
+ }
98
+ header() {
99
+ if (this._state === 'sealing')
100
+ throw new Error('SerpentStreamSealer: header() already called');
101
+ if (this._state === 'dead')
102
+ throw new Error('SerpentStreamSealer: stream is closed');
103
+ this._state = 'sealing';
104
+ const hdr = new Uint8Array(20);
105
+ hdr.set(this._nonce, 0);
106
+ hdr.set(u32be(this._cs), 16);
107
+ return hdr;
108
+ }
109
+ seal(plaintext) {
110
+ if (this._state === 'fresh')
111
+ throw new Error('SerpentStreamSealer: call header() first');
112
+ if (this._state === 'dead')
113
+ throw new Error('SerpentStreamSealer: stream is closed');
114
+ if (plaintext.length !== this._cs)
115
+ throw new RangeError(`SerpentStreamSealer: seal() requires exactly ${this._cs} bytes (got ${plaintext.length})`);
116
+ return this._sealChunk(plaintext, false);
117
+ }
118
+ final(plaintext) {
119
+ if (this._state === 'fresh')
120
+ throw new Error('SerpentStreamSealer: call header() first');
121
+ if (this._state === 'dead')
122
+ throw new Error('SerpentStreamSealer: stream is closed');
123
+ if (plaintext.length > this._cs)
124
+ throw new RangeError(`SerpentStreamSealer: final() plaintext exceeds chunkSize (got ${plaintext.length})`);
125
+ const out = this._sealChunk(plaintext, true);
126
+ this._wipe();
127
+ return out;
128
+ }
129
+ _sealChunk(plaintext, isLast) {
130
+ const { encKey, macKey } = deriveChunkKeys(this._hkdf, this._key, this._nonce, this._cs, this._index, isLast);
131
+ const iv = new Uint8Array(16);
132
+ if (this._ivs && this._ivIdx < this._ivs.length) {
133
+ iv.set(this._ivs[this._ivIdx++]);
134
+ }
135
+ else {
136
+ crypto.getRandomValues(iv);
137
+ }
138
+ const ciphertext = this._cbc.encrypt(encKey, iv, plaintext);
139
+ const tag = this._hmac.hash(macKey, concat(iv, ciphertext));
140
+ this._index++;
141
+ return concat(concat(iv, ciphertext), tag);
142
+ }
143
+ _wipe() {
144
+ wipe(this._key);
145
+ wipe(this._nonce);
146
+ this._cbc.dispose();
147
+ this._hmac.dispose();
148
+ this._hkdf.dispose();
149
+ this._state = 'dead';
150
+ }
151
+ dispose() {
152
+ if (this._state !== 'dead')
153
+ this._wipe();
154
+ }
155
+ }
156
+ export class SerpentStreamOpener {
157
+ _key;
158
+ _cs;
159
+ _nonce;
160
+ _cbc;
161
+ _hmac;
162
+ _hkdf;
163
+ _index;
164
+ _dead;
165
+ constructor(key, header) {
166
+ if (!_serpentReady())
167
+ throw new Error('leviathan-crypto: call init([\'serpent\']) before using SerpentStreamOpener');
168
+ if (!_sha2Ready())
169
+ throw new Error('leviathan-crypto: call init([\'sha2\']) before using SerpentStreamOpener');
170
+ if (key.length !== 64)
171
+ throw new RangeError(`SerpentStreamOpener key must be 64 bytes (got ${key.length})`);
172
+ if (header.length !== 20)
173
+ throw new RangeError(`SerpentStreamOpener header must be 20 bytes (got ${header.length})`);
174
+ this._key = key.slice();
175
+ this._nonce = header.slice(0, 16);
176
+ this._cs = (header[16] << 24 | header[17] << 16 | header[18] << 8 | header[19]) >>> 0;
177
+ this._cbc = new SerpentCbc({ dangerUnauthenticated: true });
178
+ this._hmac = new HMAC_SHA256();
179
+ this._hkdf = new HKDF_SHA256();
180
+ this._index = 0;
181
+ this._dead = false;
182
+ }
183
+ get closed() {
184
+ return this._dead;
185
+ }
186
+ open(chunk) {
187
+ if (this._dead)
188
+ throw new Error('SerpentStreamOpener: stream is closed');
189
+ // Try isLast = true first, then false.
190
+ // Whichever passes auth is the correct interpretation.
191
+ for (const isLast of [true, false]) {
192
+ const { encKey, macKey } = deriveChunkKeys(this._hkdf, this._key, this._nonce, this._cs, this._index, isLast);
193
+ // chunk = IV (16) || ciphertext || HMAC (32)
194
+ if (chunk.length < 16 + 16 + 32)
195
+ continue; // too short to be valid
196
+ const iv = chunk.subarray(0, 16);
197
+ const ciphertext = chunk.subarray(16, chunk.length - 32);
198
+ const tag = chunk.subarray(chunk.length - 32);
199
+ const expectedTag = this._hmac.hash(macKey, concat(iv, ciphertext));
200
+ if (!constantTimeEqual(tag, expectedTag))
201
+ continue;
202
+ const plaintext = this._cbc.decrypt(encKey, iv, ciphertext);
203
+ this._index++;
204
+ if (isLast) {
205
+ this._wipe();
206
+ }
207
+ return plaintext;
208
+ }
209
+ throw new Error('SerpentStreamOpener: authentication failed');
210
+ }
211
+ _wipe() {
212
+ wipe(this._key);
213
+ wipe(this._nonce);
214
+ this._cbc.dispose();
215
+ this._hmac.dispose();
216
+ this._hkdf.dispose();
217
+ this._dead = true;
218
+ }
219
+ dispose() {
220
+ if (!this._dead)
221
+ this._wipe();
222
+ }
223
+ }
@@ -0,0 +1,28 @@
1
+ import { SerpentCtr } from './index.js';
2
+ import { HMAC_SHA256, HKDF_SHA256 } from '../sha2/index.js';
3
+ export declare function u32be(n: number): Uint8Array;
4
+ export declare function u64be(n: number): Uint8Array;
5
+ export declare function chunkInfo(streamNonce: Uint8Array, chunkSize: number, chunkCount: number, index: number, isLast: boolean): Uint8Array;
6
+ export declare function deriveChunkKeys(hkdf: HKDF_SHA256, masterKey: Uint8Array, streamNonce: Uint8Array, chunkSize: number, chunkCount: number, index: number, isLast: boolean): {
7
+ encKey: Uint8Array;
8
+ macKey: Uint8Array;
9
+ };
10
+ /**
11
+ * Encrypt one chunk. Returns ciphertext || hmac_tag (32 bytes).
12
+ * Does not generate keys -- caller provides encKey and macKey.
13
+ */
14
+ export declare function sealChunk(ctr: SerpentCtr, hmac: HMAC_SHA256, encKey: Uint8Array, macKey: Uint8Array, chunk: Uint8Array): Uint8Array;
15
+ /**
16
+ * Decrypt one chunk. Throws 'SerpentStream: authentication failed' on bad tag.
17
+ * Returns plaintext.
18
+ */
19
+ export declare function openChunk(ctr: SerpentCtr, hmac: HMAC_SHA256, encKey: Uint8Array, macKey: Uint8Array, wire: Uint8Array): Uint8Array;
20
+ export declare class SerpentStream {
21
+ private readonly _ctr;
22
+ private readonly _hmac;
23
+ private readonly _hkdf;
24
+ constructor();
25
+ seal(key: Uint8Array, plaintext: Uint8Array, chunkSize?: number, _nonce?: Uint8Array): Uint8Array;
26
+ open(key: Uint8Array, ciphertext: Uint8Array): Uint8Array;
27
+ dispose(): void;
28
+ }