leviathan-crypto 1.4.0 → 2.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.
- package/CLAUDE.md +129 -94
- package/README.md +166 -223
- package/SECURITY.md +85 -45
- package/dist/chacha20/cipher-suite.d.ts +4 -0
- package/dist/chacha20/cipher-suite.js +78 -0
- package/dist/chacha20/embedded.d.ts +1 -0
- package/dist/chacha20/embedded.js +27 -0
- package/dist/chacha20/index.d.ts +20 -27
- package/dist/chacha20/index.js +40 -59
- package/dist/chacha20/ops.d.ts +1 -1
- package/dist/chacha20/ops.js +19 -18
- package/dist/chacha20/pool-worker.js +77 -0
- package/dist/ct-wasm.d.ts +1 -0
- package/dist/ct-wasm.js +3 -0
- package/dist/ct.wasm +0 -0
- package/dist/docs/aead.md +320 -0
- package/dist/docs/architecture.md +419 -285
- package/dist/docs/argon2id.md +42 -30
- package/dist/docs/chacha20.md +192 -266
- package/dist/docs/exports.md +241 -0
- package/dist/docs/fortuna.md +60 -69
- package/dist/docs/init.md +172 -178
- package/dist/docs/loader.md +87 -142
- package/dist/docs/serpent.md +134 -583
- package/dist/docs/sha2.md +91 -103
- package/dist/docs/sha3.md +70 -36
- package/dist/docs/types.md +93 -16
- package/dist/docs/utils.md +109 -32
- package/dist/embedded/kyber.d.ts +1 -0
- package/dist/embedded/kyber.js +3 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +38 -0
- package/dist/fortuna.d.ts +0 -6
- package/dist/fortuna.js +5 -5
- package/dist/index.d.ts +25 -9
- package/dist/index.js +36 -7
- package/dist/init.d.ts +3 -7
- package/dist/init.js +18 -35
- package/dist/keccak/embedded.d.ts +1 -0
- package/dist/keccak/embedded.js +27 -0
- package/dist/keccak/index.d.ts +4 -0
- package/dist/keccak/index.js +31 -0
- package/dist/kyber/embedded.d.ts +1 -0
- package/dist/kyber/embedded.js +27 -0
- package/dist/kyber/indcpa.d.ts +49 -0
- package/dist/kyber/indcpa.js +352 -0
- package/dist/kyber/index.d.ts +38 -0
- package/dist/kyber/index.js +150 -0
- package/dist/kyber/kem.d.ts +21 -0
- package/dist/kyber/kem.js +160 -0
- package/dist/kyber/params.d.ts +14 -0
- package/dist/kyber/params.js +37 -0
- package/dist/kyber/suite.d.ts +13 -0
- package/dist/kyber/suite.js +93 -0
- package/dist/kyber/types.d.ts +98 -0
- package/dist/kyber/types.js +25 -0
- package/dist/kyber/validate.d.ts +19 -0
- package/dist/kyber/validate.js +68 -0
- package/dist/kyber.wasm +0 -0
- package/dist/loader.d.ts +15 -6
- package/dist/loader.js +65 -21
- package/dist/serpent/cipher-suite.d.ts +4 -0
- package/dist/serpent/cipher-suite.js +121 -0
- package/dist/serpent/embedded.d.ts +1 -0
- package/dist/serpent/embedded.js +27 -0
- package/dist/serpent/index.d.ts +6 -37
- package/dist/serpent/index.js +9 -118
- package/dist/serpent/pool-worker.d.ts +1 -0
- package/dist/serpent/pool-worker.js +202 -0
- package/dist/serpent/serpent-cbc.d.ts +30 -0
- package/dist/serpent/serpent-cbc.js +136 -0
- package/dist/sha2/embedded.d.ts +1 -0
- package/dist/sha2/embedded.js +27 -0
- package/dist/sha2/hkdf.js +6 -2
- package/dist/sha2/index.d.ts +3 -2
- package/dist/sha2/index.js +3 -4
- package/dist/sha3/embedded.d.ts +1 -0
- package/dist/sha3/embedded.js +27 -0
- package/dist/sha3/index.d.ts +3 -2
- package/dist/sha3/index.js +3 -4
- package/dist/stream/constants.d.ts +6 -0
- package/dist/stream/constants.js +30 -0
- package/dist/stream/header.d.ts +9 -0
- package/dist/stream/header.js +77 -0
- package/dist/stream/index.d.ts +7 -0
- package/dist/stream/index.js +27 -0
- package/dist/stream/open-stream.d.ts +21 -0
- package/dist/stream/open-stream.js +146 -0
- package/dist/stream/seal-stream-pool.d.ts +38 -0
- package/dist/stream/seal-stream-pool.js +391 -0
- package/dist/stream/seal-stream.d.ts +20 -0
- package/dist/stream/seal-stream.js +142 -0
- package/dist/stream/seal.d.ts +9 -0
- package/dist/stream/seal.js +75 -0
- package/dist/stream/types.d.ts +24 -0
- package/dist/stream/types.js +26 -0
- package/dist/utils.d.ts +7 -2
- package/dist/utils.js +49 -3
- package/dist/wasm-source.d.ts +12 -0
- package/dist/wasm-source.js +26 -0
- package/package.json +13 -5
- package/dist/chacha20/pool.d.ts +0 -52
- package/dist/chacha20/pool.js +0 -178
- package/dist/chacha20/pool.worker.js +0 -37
- package/dist/chacha20/stream-sealer.d.ts +0 -49
- package/dist/chacha20/stream-sealer.js +0 -327
- package/dist/docs/chacha20_pool.md +0 -309
- package/dist/docs/wasm.md +0 -194
- package/dist/serpent/seal.d.ts +0 -8
- package/dist/serpent/seal.js +0 -72
- package/dist/serpent/stream-pool.d.ts +0 -48
- package/dist/serpent/stream-pool.js +0 -275
- package/dist/serpent/stream-sealer.d.ts +0 -55
- package/dist/serpent/stream-sealer.js +0 -342
- package/dist/serpent/stream.d.ts +0 -28
- package/dist/serpent/stream.js +0 -205
- package/dist/serpent/stream.worker.d.ts +0 -32
- package/dist/serpent/stream.worker.js +0 -117
- /package/dist/chacha20/{pool.worker.d.ts → pool-worker.d.ts} +0 -0
|
@@ -1,275 +0,0 @@
|
|
|
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 { decodeWasm } from '../loader.js';
|
|
30
|
-
import { u32be, u64be, deriveChunkKeys } from './stream.js';
|
|
31
|
-
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
32
|
-
const CHUNK_MIN = 1024;
|
|
33
|
-
const CHUNK_MAX = 65536;
|
|
34
|
-
const CHUNK_DEF = 65536;
|
|
35
|
-
// ── WASM module singletons ──────────────────────────────────────────────────
|
|
36
|
-
let _serpentModule;
|
|
37
|
-
let _sha2Module;
|
|
38
|
-
async function getSerpentModule() {
|
|
39
|
-
if (_serpentModule)
|
|
40
|
-
return _serpentModule;
|
|
41
|
-
const { WASM_GZ_BASE64 } = await import('../embedded/serpent.js');
|
|
42
|
-
const bytes = await decodeWasm(WASM_GZ_BASE64);
|
|
43
|
-
_serpentModule = await WebAssembly.compile(bytes.buffer);
|
|
44
|
-
return _serpentModule;
|
|
45
|
-
}
|
|
46
|
-
async function getSha2Module() {
|
|
47
|
-
if (_sha2Module)
|
|
48
|
-
return _sha2Module;
|
|
49
|
-
const { WASM_GZ_BASE64 } = await import('../embedded/sha2.js');
|
|
50
|
-
const bytes = await decodeWasm(WASM_GZ_BASE64);
|
|
51
|
-
_sha2Module = await WebAssembly.compile(bytes.buffer);
|
|
52
|
-
return _sha2Module;
|
|
53
|
-
}
|
|
54
|
-
// ── Worker spawning ──────────────────────────────────────────────────────────
|
|
55
|
-
function spawnWorker(serpentMod, sha2Mod) {
|
|
56
|
-
return new Promise((resolve, reject) => {
|
|
57
|
-
const worker = new Worker(new URL('./stream.worker.js', import.meta.url), { type: 'module' });
|
|
58
|
-
const onMessage = (e) => {
|
|
59
|
-
cleanup();
|
|
60
|
-
if (e.data.type === 'ready') {
|
|
61
|
-
resolve(worker);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
worker.terminate();
|
|
65
|
-
reject(new Error(`leviathan-crypto: worker init failed: ${e.data.message}`));
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
const onError = (e) => {
|
|
69
|
-
cleanup();
|
|
70
|
-
worker.terminate();
|
|
71
|
-
reject(new Error(`leviathan-crypto: worker init failed: ${e.message}`));
|
|
72
|
-
};
|
|
73
|
-
const cleanup = () => {
|
|
74
|
-
worker.removeEventListener('message', onMessage);
|
|
75
|
-
worker.removeEventListener('error', onError);
|
|
76
|
-
};
|
|
77
|
-
worker.addEventListener('message', onMessage);
|
|
78
|
-
worker.addEventListener('error', onError);
|
|
79
|
-
worker.postMessage({ type: 'init', serpentModule: serpentMod, sha2Module: sha2Mod });
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
// ── Pool class ───────────────────────────────────────────────────────────────
|
|
83
|
-
/**
|
|
84
|
-
* Parallel worker pool for SerpentStream chunked authenticated encryption.
|
|
85
|
-
*
|
|
86
|
-
* Each worker owns its own `serpent.wasm` and `sha2.wasm` instances with
|
|
87
|
-
* isolated linear memory. Key derivation happens on the main thread; workers
|
|
88
|
-
* receive pre-derived encKey/macKey per chunk.
|
|
89
|
-
*
|
|
90
|
-
* Produces the same wire format as `SerpentStream` -- either can decrypt
|
|
91
|
-
* the other's output.
|
|
92
|
-
*/
|
|
93
|
-
export class SerpentStreamPool {
|
|
94
|
-
_workers;
|
|
95
|
-
_idle;
|
|
96
|
-
_queue;
|
|
97
|
-
_pending;
|
|
98
|
-
_hkdf;
|
|
99
|
-
_nextId;
|
|
100
|
-
_disposed;
|
|
101
|
-
constructor(workers, hkdf) {
|
|
102
|
-
this._workers = workers;
|
|
103
|
-
this._idle = [...workers];
|
|
104
|
-
this._queue = [];
|
|
105
|
-
this._pending = new Map();
|
|
106
|
-
this._hkdf = hkdf;
|
|
107
|
-
this._nextId = 0;
|
|
108
|
-
this._disposed = false;
|
|
109
|
-
for (const w of workers) {
|
|
110
|
-
w.onmessage = (e) => this._onMessage(w, e);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Create a new pool. Requires `init(['serpent', 'sha2'])` to have been called.
|
|
115
|
-
* Compiles both WASM modules once and distributes them to all workers.
|
|
116
|
-
*/
|
|
117
|
-
static async create(opts) {
|
|
118
|
-
if (!isInitialized('serpent') || !isInitialized('sha2'))
|
|
119
|
-
throw new Error('leviathan-crypto: call init([\'serpent\', \'sha2\']) before using SerpentStreamPool');
|
|
120
|
-
const n = opts?.workers ?? (typeof navigator !== 'undefined' ? navigator.hardwareConcurrency : undefined) ?? 4;
|
|
121
|
-
const [serpentMod, sha2Mod] = await Promise.all([getSerpentModule(), getSha2Module()]);
|
|
122
|
-
// Sequential spawn — compatible with inline-worker test environments
|
|
123
|
-
const workers = [];
|
|
124
|
-
for (let i = 0; i < n; i++)
|
|
125
|
-
workers.push(await spawnWorker(serpentMod, sha2Mod));
|
|
126
|
-
const hkdf = new HKDF_SHA256();
|
|
127
|
-
return new SerpentStreamPool(workers, hkdf);
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Encrypt plaintext with SerpentStream chunked authenticated encryption.
|
|
131
|
-
* Returns the complete wire format (header + encrypted chunks).
|
|
132
|
-
*/
|
|
133
|
-
async seal(key, plaintext, chunkSize) {
|
|
134
|
-
if (this._disposed)
|
|
135
|
-
throw new Error('leviathan-crypto: pool is disposed');
|
|
136
|
-
if (key.length !== 32)
|
|
137
|
-
throw new RangeError(`SerpentStream key must be 32 bytes (got ${key.length})`);
|
|
138
|
-
const cs = chunkSize ?? CHUNK_DEF;
|
|
139
|
-
if (cs < CHUNK_MIN || cs > CHUNK_MAX)
|
|
140
|
-
throw new RangeError(`SerpentStream chunkSize must be ${CHUNK_MIN}..${CHUNK_MAX} (got ${cs})`);
|
|
141
|
-
const streamNonce = new Uint8Array(16);
|
|
142
|
-
crypto.getRandomValues(streamNonce);
|
|
143
|
-
const chunkCount = plaintext.length === 0 ? 1 : Math.ceil(plaintext.length / cs);
|
|
144
|
-
// Dispatch all chunk jobs in parallel
|
|
145
|
-
const chunkPromises = [];
|
|
146
|
-
for (let i = 0; i < chunkCount; i++) {
|
|
147
|
-
const start = i * cs;
|
|
148
|
-
const end = Math.min(start + cs, plaintext.length);
|
|
149
|
-
const slice = plaintext.slice(start, end);
|
|
150
|
-
const isLast = i === chunkCount - 1;
|
|
151
|
-
const { encKey, macKey } = deriveChunkKeys(this._hkdf, key, streamNonce, cs, chunkCount, i, isLast);
|
|
152
|
-
chunkPromises.push(this._dispatch('seal', encKey, macKey, slice));
|
|
153
|
-
}
|
|
154
|
-
const chunkResults = await Promise.all(chunkPromises);
|
|
155
|
-
// Compute total output size
|
|
156
|
-
let totalWire = 28;
|
|
157
|
-
for (const c of chunkResults)
|
|
158
|
-
totalWire += c.length;
|
|
159
|
-
const out = new Uint8Array(totalWire);
|
|
160
|
-
// Write header
|
|
161
|
-
out.set(streamNonce, 0);
|
|
162
|
-
out.set(u32be(cs), 16);
|
|
163
|
-
out.set(u64be(chunkCount), 20);
|
|
164
|
-
let pos = 28;
|
|
165
|
-
for (const c of chunkResults) {
|
|
166
|
-
out.set(c, pos);
|
|
167
|
-
pos += c.length;
|
|
168
|
-
}
|
|
169
|
-
return out;
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Decrypt SerpentStream wire format.
|
|
173
|
-
* If any chunk fails authentication, rejects immediately -- no partial plaintext.
|
|
174
|
-
*/
|
|
175
|
-
async open(key, ciphertext) {
|
|
176
|
-
if (this._disposed)
|
|
177
|
-
throw new Error('leviathan-crypto: pool is disposed');
|
|
178
|
-
if (key.length !== 32)
|
|
179
|
-
throw new RangeError(`SerpentStream key must be 32 bytes (got ${key.length})`);
|
|
180
|
-
if (ciphertext.length < 28 + 32)
|
|
181
|
-
throw new RangeError('SerpentStream: ciphertext too short');
|
|
182
|
-
// Parse header
|
|
183
|
-
const streamNonce = ciphertext.subarray(0, 16);
|
|
184
|
-
const csView = ciphertext.subarray(16, 20);
|
|
185
|
-
const cs = (csView[0] << 24) | (csView[1] << 16) | (csView[2] << 8) | csView[3];
|
|
186
|
-
const ccView = ciphertext.subarray(20, 28);
|
|
187
|
-
let chunkCount = 0;
|
|
188
|
-
for (let i = 0; i < 8; i++)
|
|
189
|
-
chunkCount = chunkCount * 256 + ccView[i];
|
|
190
|
-
// Dispatch all chunk jobs
|
|
191
|
-
const chunkPromises = [];
|
|
192
|
-
let pos = 28;
|
|
193
|
-
for (let i = 0; i < chunkCount; i++) {
|
|
194
|
-
const isLast = i === chunkCount - 1;
|
|
195
|
-
const wireLen = isLast ? ciphertext.length - pos : cs + 32;
|
|
196
|
-
const wireSlice = ciphertext.slice(pos, pos + wireLen);
|
|
197
|
-
const { encKey, macKey } = deriveChunkKeys(this._hkdf, key, streamNonce, cs, chunkCount, i, isLast);
|
|
198
|
-
chunkPromises.push(this._dispatch('open', encKey, macKey, wireSlice));
|
|
199
|
-
pos += wireLen;
|
|
200
|
-
}
|
|
201
|
-
// Await all — Promise.all rejects on first failure
|
|
202
|
-
const results = await Promise.all(chunkPromises);
|
|
203
|
-
// Reassemble plaintext
|
|
204
|
-
let totalPt = 0;
|
|
205
|
-
for (const r of results)
|
|
206
|
-
totalPt += r.length;
|
|
207
|
-
const plaintext = new Uint8Array(totalPt);
|
|
208
|
-
let ptPos = 0;
|
|
209
|
-
for (const r of results) {
|
|
210
|
-
plaintext.set(r, ptPos);
|
|
211
|
-
ptPos += r.length;
|
|
212
|
-
}
|
|
213
|
-
return plaintext;
|
|
214
|
-
}
|
|
215
|
-
/** Terminates all workers. Rejects all pending and queued jobs. */
|
|
216
|
-
dispose() {
|
|
217
|
-
if (this._disposed)
|
|
218
|
-
return;
|
|
219
|
-
this._disposed = true;
|
|
220
|
-
for (const w of this._workers)
|
|
221
|
-
w.terminate();
|
|
222
|
-
const err = new Error('leviathan-crypto: pool disposed');
|
|
223
|
-
for (const { reject } of this._pending.values())
|
|
224
|
-
reject(err);
|
|
225
|
-
for (const job of this._queue)
|
|
226
|
-
this._pending.get(job.id)?.reject(err);
|
|
227
|
-
this._pending.clear();
|
|
228
|
-
this._queue.length = 0;
|
|
229
|
-
this._hkdf.dispose();
|
|
230
|
-
}
|
|
231
|
-
/** Number of workers in the pool. */
|
|
232
|
-
get size() {
|
|
233
|
-
return this._workers.length;
|
|
234
|
-
}
|
|
235
|
-
/** Number of jobs currently queued (waiting for a free worker). */
|
|
236
|
-
get queueDepth() {
|
|
237
|
-
return this._queue.length;
|
|
238
|
-
}
|
|
239
|
-
// ── Internals ────────────────────────────────────────────────────────────
|
|
240
|
-
_dispatch(op, encKey, macKey, data) {
|
|
241
|
-
return new Promise((resolve, reject) => {
|
|
242
|
-
const id = this._nextId++;
|
|
243
|
-
const job = { id, op, encKey, macKey, data };
|
|
244
|
-
this._pending.set(id, { resolve, reject });
|
|
245
|
-
const worker = this._idle.pop();
|
|
246
|
-
if (worker)
|
|
247
|
-
this._send(worker, job);
|
|
248
|
-
else
|
|
249
|
-
this._queue.push(job);
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
_send(worker, job) {
|
|
253
|
-
// No transfer list: @vitest/web-worker (same-thread fake worker used in
|
|
254
|
-
// Vitest test environment) silently drops postMessage calls that include a
|
|
255
|
-
// non-empty Transferable array. encKey/macKey/data are already .slice()
|
|
256
|
-
// copies — neutering was never part of SerpentStreamPool's public contract.
|
|
257
|
-
worker.postMessage({ type: 'job', ...job });
|
|
258
|
-
}
|
|
259
|
-
_onMessage(worker, e) {
|
|
260
|
-
const msg = e.data;
|
|
261
|
-
const job = this._pending.get(msg.id);
|
|
262
|
-
if (!job)
|
|
263
|
-
return;
|
|
264
|
-
this._pending.delete(msg.id);
|
|
265
|
-
if (msg.type === 'result')
|
|
266
|
-
job.resolve(msg.data);
|
|
267
|
-
else
|
|
268
|
-
job.reject(new Error(msg.message));
|
|
269
|
-
const next = this._queue.shift();
|
|
270
|
-
if (next)
|
|
271
|
-
this._send(worker, next);
|
|
272
|
-
else
|
|
273
|
-
this._idle.push(worker);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
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 _framed;
|
|
9
|
-
private readonly _aad;
|
|
10
|
-
private readonly _ivs;
|
|
11
|
-
private _ivIdx;
|
|
12
|
-
private _index;
|
|
13
|
-
private _state;
|
|
14
|
-
/** Public: consumers use this 3-param form. */
|
|
15
|
-
constructor(key: Uint8Array, chunkSize?: number, opts?: {
|
|
16
|
-
framed?: boolean;
|
|
17
|
-
aad?: Uint8Array;
|
|
18
|
-
});
|
|
19
|
-
/** @internal Test-only overload to inject fixed nonce/IVs for deterministic KAT vectors. */
|
|
20
|
-
constructor(key: Uint8Array, chunkSize: number | undefined, opts: {
|
|
21
|
-
framed?: boolean;
|
|
22
|
-
aad?: Uint8Array;
|
|
23
|
-
} | undefined, _nonce: Uint8Array, _ivs?: Uint8Array[]);
|
|
24
|
-
header(): Uint8Array;
|
|
25
|
-
seal(plaintext: Uint8Array): Uint8Array;
|
|
26
|
-
final(plaintext: Uint8Array): Uint8Array;
|
|
27
|
-
private _sealChunk;
|
|
28
|
-
private _wipe;
|
|
29
|
-
dispose(): void;
|
|
30
|
-
}
|
|
31
|
-
export declare class SerpentStreamOpener {
|
|
32
|
-
private readonly _key;
|
|
33
|
-
private readonly _cs;
|
|
34
|
-
private readonly _nonce;
|
|
35
|
-
private readonly _cbc;
|
|
36
|
-
private readonly _hmac;
|
|
37
|
-
private readonly _hkdf;
|
|
38
|
-
private readonly _framed;
|
|
39
|
-
private readonly _aad;
|
|
40
|
-
private readonly _buf;
|
|
41
|
-
private readonly _maxFrame;
|
|
42
|
-
private _bufLen;
|
|
43
|
-
private _index;
|
|
44
|
-
private _dead;
|
|
45
|
-
constructor(key: Uint8Array, header: Uint8Array, opts?: {
|
|
46
|
-
framed?: boolean;
|
|
47
|
-
aad?: Uint8Array;
|
|
48
|
-
});
|
|
49
|
-
get closed(): boolean;
|
|
50
|
-
open(chunk: Uint8Array): Uint8Array;
|
|
51
|
-
private _openRaw;
|
|
52
|
-
feed(bytes: Uint8Array): Uint8Array[];
|
|
53
|
-
private _wipe;
|
|
54
|
-
dispose(): void;
|
|
55
|
-
}
|
|
@@ -1,342 +0,0 @@
|
|
|
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
|
-
_framed;
|
|
67
|
-
_aad;
|
|
68
|
-
_ivs; // test seam: fixed IVs
|
|
69
|
-
_ivIdx;
|
|
70
|
-
_index;
|
|
71
|
-
_state;
|
|
72
|
-
constructor(key, chunkSize, opts, _nonce, _ivs) {
|
|
73
|
-
if (!_serpentReady())
|
|
74
|
-
throw new Error('leviathan-crypto: call init([\'serpent\']) before using SerpentStreamSealer');
|
|
75
|
-
if (!_sha2Ready())
|
|
76
|
-
throw new Error('leviathan-crypto: call init([\'sha2\']) before using SerpentStreamSealer');
|
|
77
|
-
if (key.length !== 64)
|
|
78
|
-
throw new RangeError(`SerpentStreamSealer key must be 64 bytes (got ${key.length})`);
|
|
79
|
-
const cs = chunkSize ?? CHUNK_DEF;
|
|
80
|
-
if (cs < CHUNK_MIN || cs > CHUNK_MAX)
|
|
81
|
-
throw new RangeError(`SerpentStreamSealer chunkSize must be ${CHUNK_MIN}..${CHUNK_MAX} (got ${cs})`);
|
|
82
|
-
this._key = key.slice();
|
|
83
|
-
this._cs = cs;
|
|
84
|
-
this._framed = opts?.framed ?? false;
|
|
85
|
-
this._aad = opts?.aad ? opts.aad.slice() : new Uint8Array(0);
|
|
86
|
-
this._nonce = new Uint8Array(16);
|
|
87
|
-
if (_nonce && _nonce.length === 16) {
|
|
88
|
-
this._nonce.set(_nonce);
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
crypto.getRandomValues(this._nonce);
|
|
92
|
-
}
|
|
93
|
-
this._cbc = new SerpentCbc({ dangerUnauthenticated: true });
|
|
94
|
-
this._hmac = new HMAC_SHA256();
|
|
95
|
-
this._hkdf = new HKDF_SHA256();
|
|
96
|
-
this._ivs = _ivs;
|
|
97
|
-
this._ivIdx = 0;
|
|
98
|
-
this._index = 0;
|
|
99
|
-
this._state = 'fresh';
|
|
100
|
-
}
|
|
101
|
-
header() {
|
|
102
|
-
if (this._state === 'sealing')
|
|
103
|
-
throw new Error('SerpentStreamSealer: header() already called');
|
|
104
|
-
if (this._state === 'dead')
|
|
105
|
-
throw new Error('SerpentStreamSealer: stream is closed');
|
|
106
|
-
this._state = 'sealing';
|
|
107
|
-
const hdr = new Uint8Array(20);
|
|
108
|
-
hdr.set(this._nonce, 0);
|
|
109
|
-
hdr.set(u32be(this._cs), 16);
|
|
110
|
-
return hdr;
|
|
111
|
-
}
|
|
112
|
-
seal(plaintext) {
|
|
113
|
-
if (this._state === 'fresh')
|
|
114
|
-
throw new Error('SerpentStreamSealer: call header() first');
|
|
115
|
-
if (this._state === 'dead')
|
|
116
|
-
throw new Error('SerpentStreamSealer: stream is closed');
|
|
117
|
-
if (plaintext.length !== this._cs)
|
|
118
|
-
throw new RangeError(`SerpentStreamSealer: seal() requires exactly ${this._cs} bytes (got ${plaintext.length})`);
|
|
119
|
-
return this._sealChunk(plaintext, false);
|
|
120
|
-
}
|
|
121
|
-
final(plaintext) {
|
|
122
|
-
if (this._state === 'fresh')
|
|
123
|
-
throw new Error('SerpentStreamSealer: call header() first');
|
|
124
|
-
if (this._state === 'dead')
|
|
125
|
-
throw new Error('SerpentStreamSealer: stream is closed');
|
|
126
|
-
if (plaintext.length > this._cs)
|
|
127
|
-
throw new RangeError(`SerpentStreamSealer: final() plaintext exceeds chunkSize (got ${plaintext.length})`);
|
|
128
|
-
const out = this._sealChunk(plaintext, true);
|
|
129
|
-
this._wipe();
|
|
130
|
-
return out;
|
|
131
|
-
}
|
|
132
|
-
_sealChunk(plaintext, isLast) {
|
|
133
|
-
const { encKey, macKey } = deriveChunkKeys(this._hkdf, this._key, this._nonce, this._cs, this._index, isLast);
|
|
134
|
-
const iv = new Uint8Array(16);
|
|
135
|
-
if (this._ivs && this._ivIdx < this._ivs.length) {
|
|
136
|
-
iv.set(this._ivs[this._ivIdx++]);
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
crypto.getRandomValues(iv);
|
|
140
|
-
}
|
|
141
|
-
const ciphertext = this._cbc.encrypt(encKey, iv, plaintext);
|
|
142
|
-
const tag = this._hmac.hash(macKey, concat(u32be(this._aad.length), this._aad, iv, ciphertext));
|
|
143
|
-
this._index++;
|
|
144
|
-
const sealed = concat(new Uint8Array([isLast ? 1 : 0]), iv, ciphertext, tag);
|
|
145
|
-
if (!this._framed)
|
|
146
|
-
return sealed;
|
|
147
|
-
const out = new Uint8Array(4 + sealed.length);
|
|
148
|
-
out.set(u32be(sealed.length), 0);
|
|
149
|
-
out.set(sealed, 4);
|
|
150
|
-
return out;
|
|
151
|
-
}
|
|
152
|
-
_wipe() {
|
|
153
|
-
wipe(this._key);
|
|
154
|
-
wipe(this._nonce);
|
|
155
|
-
wipe(this._aad);
|
|
156
|
-
this._cbc.dispose();
|
|
157
|
-
this._hmac.dispose();
|
|
158
|
-
this._hkdf.dispose();
|
|
159
|
-
this._state = 'dead';
|
|
160
|
-
}
|
|
161
|
-
dispose() {
|
|
162
|
-
if (this._state !== 'dead')
|
|
163
|
-
this._wipe();
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
export class SerpentStreamOpener {
|
|
167
|
-
_key;
|
|
168
|
-
_cs;
|
|
169
|
-
_nonce;
|
|
170
|
-
_cbc;
|
|
171
|
-
_hmac;
|
|
172
|
-
_hkdf;
|
|
173
|
-
_framed;
|
|
174
|
-
_aad;
|
|
175
|
-
_buf;
|
|
176
|
-
_maxFrame;
|
|
177
|
-
_bufLen;
|
|
178
|
-
_index;
|
|
179
|
-
_dead;
|
|
180
|
-
constructor(key, header, opts) {
|
|
181
|
-
if (!_serpentReady())
|
|
182
|
-
throw new Error('leviathan-crypto: call init([\'serpent\']) before using SerpentStreamOpener');
|
|
183
|
-
if (!_sha2Ready())
|
|
184
|
-
throw new Error('leviathan-crypto: call init([\'sha2\']) before using SerpentStreamOpener');
|
|
185
|
-
if (key.length !== 64)
|
|
186
|
-
throw new RangeError(`SerpentStreamOpener key must be 64 bytes (got ${key.length})`);
|
|
187
|
-
if (header.length !== 20)
|
|
188
|
-
throw new RangeError(`SerpentStreamOpener header must be 20 bytes (got ${header.length})`);
|
|
189
|
-
this._key = key.slice();
|
|
190
|
-
this._nonce = header.slice(0, 16);
|
|
191
|
-
this._cs = (header[16] << 24 | header[17] << 16 | header[18] << 8 | header[19]) >>> 0;
|
|
192
|
-
if (this._cs < CHUNK_MIN || this._cs > CHUNK_MAX)
|
|
193
|
-
throw new RangeError(`SerpentStreamOpener: header contains invalid chunkSize ${this._cs} (expected ${CHUNK_MIN}..${CHUNK_MAX})`);
|
|
194
|
-
this._framed = opts?.framed ?? false;
|
|
195
|
-
this._aad = opts?.aad ? opts.aad.slice() : new Uint8Array(0);
|
|
196
|
-
this._cbc = new SerpentCbc({ dangerUnauthenticated: true });
|
|
197
|
-
this._hmac = new HMAC_SHA256();
|
|
198
|
-
this._hkdf = new HKDF_SHA256();
|
|
199
|
-
this._index = 0;
|
|
200
|
-
this._dead = false;
|
|
201
|
-
this._bufLen = 0;
|
|
202
|
-
if (this._framed) {
|
|
203
|
-
const cs = this._cs;
|
|
204
|
-
const maxSealed = 1 + 16 + (cs + (16 - (cs % 16))) + 32;
|
|
205
|
-
this._maxFrame = 4 + maxSealed;
|
|
206
|
-
this._buf = new Uint8Array(this._maxFrame);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
get closed() {
|
|
210
|
-
return this._dead;
|
|
211
|
-
}
|
|
212
|
-
open(chunk) {
|
|
213
|
-
if (this._dead)
|
|
214
|
-
throw new Error('SerpentStreamOpener: stream is closed');
|
|
215
|
-
if (this._framed)
|
|
216
|
-
throw new Error('SerpentStreamOpener: call feed() on framed openers — open() expects raw sealed chunks without length prefix');
|
|
217
|
-
return this._openRaw(chunk);
|
|
218
|
-
}
|
|
219
|
-
_openRaw(chunk) {
|
|
220
|
-
// isLast(1) || IV(16) || ciphertext || HMAC(32)
|
|
221
|
-
if (chunk.length < 1 + 16 + 16 + 32)
|
|
222
|
-
throw new RangeError('SerpentStreamOpener: chunk wire data too short');
|
|
223
|
-
const isLast = chunk[0] !== 0;
|
|
224
|
-
const { encKey, macKey } = deriveChunkKeys(this._hkdf, this._key, this._nonce, this._cs, this._index, isLast);
|
|
225
|
-
const iv = chunk.subarray(1, 17);
|
|
226
|
-
const ciphertext = chunk.subarray(17, chunk.length - 32);
|
|
227
|
-
const tag = chunk.subarray(chunk.length - 32);
|
|
228
|
-
const expectedTag = this._hmac.hash(macKey, concat(u32be(this._aad.length), this._aad, iv, ciphertext));
|
|
229
|
-
if (!constantTimeEqual(tag, expectedTag))
|
|
230
|
-
throw new Error('SerpentStreamOpener: authentication failed');
|
|
231
|
-
const plaintext = this._cbc.decrypt(encKey, iv, ciphertext);
|
|
232
|
-
this._index++;
|
|
233
|
-
if (isLast)
|
|
234
|
-
this._wipe();
|
|
235
|
-
return plaintext;
|
|
236
|
-
}
|
|
237
|
-
feed(bytes) {
|
|
238
|
-
if (!this._framed)
|
|
239
|
-
throw new Error('SerpentStreamOpener: feed() requires { framed: true }');
|
|
240
|
-
if (this._dead)
|
|
241
|
-
throw new Error('SerpentStreamOpener: stream is closed');
|
|
242
|
-
const buf = this._buf;
|
|
243
|
-
const maxFrame = this._maxFrame;
|
|
244
|
-
const results = [];
|
|
245
|
-
let consumed = 0;
|
|
246
|
-
// ── Phase 1: drain carry-over ─────────────────────────────────────
|
|
247
|
-
if (this._bufLen > 0) {
|
|
248
|
-
// Sub-case A: partial prefix — we have < 4 bytes buffered
|
|
249
|
-
if (this._bufLen < 4) {
|
|
250
|
-
const need = 4 - this._bufLen;
|
|
251
|
-
const take = Math.min(need, bytes.length - consumed);
|
|
252
|
-
buf.set(bytes.subarray(consumed, consumed + take), this._bufLen);
|
|
253
|
-
this._bufLen += take;
|
|
254
|
-
consumed += take;
|
|
255
|
-
if (this._bufLen < 4)
|
|
256
|
-
return results;
|
|
257
|
-
}
|
|
258
|
-
const sealedLen = (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
|
|
259
|
-
if (sealedLen === 0 || sealedLen > (maxFrame - 4)) {
|
|
260
|
-
this._wipe();
|
|
261
|
-
throw new Error('SerpentStreamOpener: invalid sealed chunk length');
|
|
262
|
-
}
|
|
263
|
-
const frameLen = 4 + sealedLen;
|
|
264
|
-
// Sub-case B: partial frame body
|
|
265
|
-
const haveInBuf = this._bufLen;
|
|
266
|
-
const needMore = frameLen - haveInBuf;
|
|
267
|
-
if (needMore > 0) {
|
|
268
|
-
const take = Math.min(needMore, bytes.length - consumed);
|
|
269
|
-
buf.set(bytes.subarray(consumed, consumed + take), haveInBuf);
|
|
270
|
-
this._bufLen += take;
|
|
271
|
-
consumed += take;
|
|
272
|
-
if (this._bufLen < frameLen)
|
|
273
|
-
return results;
|
|
274
|
-
}
|
|
275
|
-
const plaintext = this._openRaw(buf.subarray(4, frameLen));
|
|
276
|
-
results.push(plaintext);
|
|
277
|
-
this._bufLen = 0;
|
|
278
|
-
if (this._dead) {
|
|
279
|
-
if (consumed < bytes.length) {
|
|
280
|
-
this._wipe();
|
|
281
|
-
throw new Error('SerpentStreamOpener: unexpected bytes after final chunk');
|
|
282
|
-
}
|
|
283
|
-
return results;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
// ── Phase 2: parse complete frames directly from bytes ────────────
|
|
287
|
-
let pos = consumed;
|
|
288
|
-
while (true) {
|
|
289
|
-
if (bytes.length - pos < 4)
|
|
290
|
-
break;
|
|
291
|
-
const sealedLen = (bytes[pos] << 24 | bytes[pos + 1] << 16 | bytes[pos + 2] << 8 | bytes[pos + 3]) >>> 0;
|
|
292
|
-
if (sealedLen === 0 || sealedLen > (maxFrame - 4)) {
|
|
293
|
-
this._wipe();
|
|
294
|
-
throw new Error('SerpentStreamOpener: invalid sealed chunk length');
|
|
295
|
-
}
|
|
296
|
-
const frameLen = 4 + sealedLen;
|
|
297
|
-
if (bytes.length - pos < frameLen)
|
|
298
|
-
break;
|
|
299
|
-
const plaintext = this._openRaw(bytes.subarray(pos + 4, pos + frameLen));
|
|
300
|
-
results.push(plaintext);
|
|
301
|
-
if (this._dead) {
|
|
302
|
-
const remaining = bytes.length - pos - frameLen;
|
|
303
|
-
if (remaining > 0) {
|
|
304
|
-
this._wipe();
|
|
305
|
-
throw new Error('SerpentStreamOpener: unexpected bytes after final chunk');
|
|
306
|
-
}
|
|
307
|
-
return results;
|
|
308
|
-
}
|
|
309
|
-
pos += frameLen;
|
|
310
|
-
}
|
|
311
|
-
// ── Carry over any incomplete trailing bytes into _buf ────────────
|
|
312
|
-
const leftover = bytes.length - pos;
|
|
313
|
-
if (leftover > 0) {
|
|
314
|
-
if (leftover > maxFrame) {
|
|
315
|
-
this._wipe();
|
|
316
|
-
throw new Error('SerpentStreamOpener: input exceeds maximum frame size');
|
|
317
|
-
}
|
|
318
|
-
buf.set(bytes.subarray(pos), 0);
|
|
319
|
-
this._bufLen = leftover;
|
|
320
|
-
}
|
|
321
|
-
return results;
|
|
322
|
-
}
|
|
323
|
-
_wipe() {
|
|
324
|
-
if (this._dead)
|
|
325
|
-
return;
|
|
326
|
-
wipe(this._key);
|
|
327
|
-
wipe(this._nonce);
|
|
328
|
-
wipe(this._aad);
|
|
329
|
-
this._cbc.dispose();
|
|
330
|
-
this._hmac.dispose();
|
|
331
|
-
this._hkdf.dispose();
|
|
332
|
-
if (this._framed) {
|
|
333
|
-
wipe(this._buf);
|
|
334
|
-
this._bufLen = 0;
|
|
335
|
-
}
|
|
336
|
-
this._dead = true;
|
|
337
|
-
}
|
|
338
|
-
dispose() {
|
|
339
|
-
if (!this._dead)
|
|
340
|
-
this._wipe();
|
|
341
|
-
}
|
|
342
|
-
}
|