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.
- package/CLAUDE.md +265 -0
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/SECURITY.md +174 -0
- package/dist/chacha.wasm +0 -0
- package/dist/chacha20/index.d.ts +49 -0
- package/dist/chacha20/index.js +177 -0
- package/dist/chacha20/ops.d.ts +16 -0
- package/dist/chacha20/ops.js +146 -0
- package/dist/chacha20/pool.d.ts +52 -0
- package/dist/chacha20/pool.js +188 -0
- package/dist/chacha20/pool.worker.d.ts +1 -0
- package/dist/chacha20/pool.worker.js +37 -0
- package/dist/chacha20/types.d.ts +30 -0
- package/dist/chacha20/types.js +1 -0
- package/dist/docs/architecture.md +795 -0
- package/dist/docs/argon2id.md +290 -0
- package/dist/docs/chacha20.md +602 -0
- package/dist/docs/chacha20_pool.md +306 -0
- package/dist/docs/fortuna.md +322 -0
- package/dist/docs/init.md +308 -0
- package/dist/docs/loader.md +206 -0
- package/dist/docs/serpent.md +914 -0
- package/dist/docs/sha2.md +620 -0
- package/dist/docs/sha3.md +509 -0
- package/dist/docs/types.md +198 -0
- package/dist/docs/utils.md +273 -0
- package/dist/docs/wasm.md +193 -0
- package/dist/embedded/chacha.d.ts +1 -0
- package/dist/embedded/chacha.js +2 -0
- package/dist/embedded/serpent.d.ts +1 -0
- package/dist/embedded/serpent.js +2 -0
- package/dist/embedded/sha2.d.ts +1 -0
- package/dist/embedded/sha2.js +2 -0
- package/dist/embedded/sha3.d.ts +1 -0
- package/dist/embedded/sha3.js +2 -0
- package/dist/fortuna.d.ts +72 -0
- package/dist/fortuna.js +445 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +44 -0
- package/dist/init.d.ts +11 -0
- package/dist/init.js +49 -0
- package/dist/loader.d.ts +4 -0
- package/dist/loader.js +30 -0
- package/dist/serpent/index.d.ts +65 -0
- package/dist/serpent/index.js +242 -0
- package/dist/serpent/seal.d.ts +8 -0
- package/dist/serpent/seal.js +70 -0
- package/dist/serpent/stream-encoder.d.ts +20 -0
- package/dist/serpent/stream-encoder.js +167 -0
- package/dist/serpent/stream-pool.d.ts +48 -0
- package/dist/serpent/stream-pool.js +285 -0
- package/dist/serpent/stream-sealer.d.ts +34 -0
- package/dist/serpent/stream-sealer.js +223 -0
- package/dist/serpent/stream.d.ts +28 -0
- package/dist/serpent/stream.js +205 -0
- package/dist/serpent/stream.worker.d.ts +32 -0
- package/dist/serpent/stream.worker.js +117 -0
- package/dist/serpent/types.d.ts +5 -0
- package/dist/serpent/types.js +1 -0
- package/dist/serpent.wasm +0 -0
- package/dist/sha2/hkdf.d.ts +16 -0
- package/dist/sha2/hkdf.js +108 -0
- package/dist/sha2/index.d.ts +40 -0
- package/dist/sha2/index.js +190 -0
- package/dist/sha2/types.d.ts +5 -0
- package/dist/sha2/types.js +1 -0
- package/dist/sha2.wasm +0 -0
- package/dist/sha3/index.d.ts +55 -0
- package/dist/sha3/index.js +246 -0
- package/dist/sha3/types.d.ts +5 -0
- package/dist/sha3/types.js +1 -0
- package/dist/sha3.wasm +0 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.js +26 -0
- package/dist/utils.d.ts +26 -0
- package/dist/utils.js +169 -0
- 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
|
+
}
|