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
package/dist/chacha20/pool.js
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
// src/ts/chacha20/pool.ts
|
|
2
|
-
//
|
|
3
|
-
// XChaCha20Poly1305Pool — parallel worker pool for XChaCha20-Poly1305 AEAD.
|
|
4
|
-
// Dispatches independent encrypt/decrypt jobs across Web Workers, each with
|
|
5
|
-
// its own WebAssembly.Instance and isolated linear memory.
|
|
6
|
-
import { isInitialized } from '../init.js';
|
|
7
|
-
import { decodeWasm } from '../loader.js';
|
|
8
|
-
// ── WASM module singleton ────────────────────────────────────────────────────
|
|
9
|
-
let _wasmModule;
|
|
10
|
-
async function getWasmModule() {
|
|
11
|
-
if (_wasmModule)
|
|
12
|
-
return _wasmModule;
|
|
13
|
-
const { WASM_GZ_BASE64 } = await import('../embedded/chacha20.js');
|
|
14
|
-
const bytes = await decodeWasm(WASM_GZ_BASE64);
|
|
15
|
-
_wasmModule = await WebAssembly.compile(bytes.buffer);
|
|
16
|
-
return _wasmModule;
|
|
17
|
-
}
|
|
18
|
-
// ── Worker spawning ──────────────────────────────────────────────────────────
|
|
19
|
-
function spawnWorker(mod) {
|
|
20
|
-
return new Promise((resolve, reject) => {
|
|
21
|
-
const worker = new Worker(new URL('./pool.worker.js', import.meta.url), { type: 'module' });
|
|
22
|
-
const onMessage = (e) => {
|
|
23
|
-
cleanup();
|
|
24
|
-
if (e.data.type === 'ready') {
|
|
25
|
-
resolve(worker);
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
worker.terminate();
|
|
29
|
-
reject(new Error(`leviathan-crypto: worker init failed: ${e.data.message}`));
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
const onError = (e) => {
|
|
33
|
-
cleanup();
|
|
34
|
-
worker.terminate();
|
|
35
|
-
reject(new Error(`leviathan-crypto: worker init failed: ${e.message}`));
|
|
36
|
-
};
|
|
37
|
-
const cleanup = () => {
|
|
38
|
-
worker.removeEventListener('message', onMessage);
|
|
39
|
-
worker.removeEventListener('error', onError);
|
|
40
|
-
};
|
|
41
|
-
worker.addEventListener('message', onMessage);
|
|
42
|
-
worker.addEventListener('error', onError);
|
|
43
|
-
worker.postMessage({ type: 'init', module: mod });
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
// ── Pool class ───────────────────────────────────────────────────────────────
|
|
47
|
-
/**
|
|
48
|
-
* Parallel worker pool for XChaCha20-Poly1305 AEAD.
|
|
49
|
-
*
|
|
50
|
-
* Each worker owns its own `WebAssembly.Instance` with isolated linear memory.
|
|
51
|
-
* Jobs are dispatched round-robin to idle workers; excess jobs queue until a
|
|
52
|
-
* worker frees up.
|
|
53
|
-
*
|
|
54
|
-
* **Warning:** Input buffers (`key`, `nonce`, `plaintext`/`ciphertext`, `aad`)
|
|
55
|
-
* are transferred to the worker and neutered on the calling side. The caller
|
|
56
|
-
* must copy any buffer they need to retain after calling `encrypt()`/`decrypt()`.
|
|
57
|
-
*/
|
|
58
|
-
export class XChaCha20Poly1305Pool {
|
|
59
|
-
_workers;
|
|
60
|
-
_idle;
|
|
61
|
-
_queue;
|
|
62
|
-
_pending;
|
|
63
|
-
_nextId;
|
|
64
|
-
_disposed;
|
|
65
|
-
constructor(workers) {
|
|
66
|
-
this._workers = workers;
|
|
67
|
-
this._idle = [...workers];
|
|
68
|
-
this._queue = [];
|
|
69
|
-
this._pending = new Map();
|
|
70
|
-
this._nextId = 0;
|
|
71
|
-
this._disposed = false;
|
|
72
|
-
for (const w of workers) {
|
|
73
|
-
w.onmessage = (e) => this._onMessage(w, e);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Create a new pool. Requires `init(['chacha20'])` to have been called.
|
|
78
|
-
* Compiles the WASM module once and distributes it to all workers.
|
|
79
|
-
*/
|
|
80
|
-
static async create(opts) {
|
|
81
|
-
if (!isInitialized('chacha20'))
|
|
82
|
-
throw new Error('leviathan-crypto: call init([\'chacha20\']) before using XChaCha20Poly1305Pool');
|
|
83
|
-
const n = opts?.workers ?? (typeof navigator !== 'undefined' ? navigator.hardwareConcurrency : undefined) ?? 4;
|
|
84
|
-
const mod = await getWasmModule();
|
|
85
|
-
// Sequential spawn — compatible with inline-worker test environments
|
|
86
|
-
const workers = [];
|
|
87
|
-
for (let i = 0; i < n; i++)
|
|
88
|
-
workers.push(await spawnWorker(mod));
|
|
89
|
-
return new XChaCha20Poly1305Pool(workers);
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Encrypt plaintext with XChaCha20-Poly1305.
|
|
93
|
-
* Returns `ciphertext || tag` (plaintext.length + 16 bytes).
|
|
94
|
-
*
|
|
95
|
-
* **Warning:** All input buffers are transferred and neutered after dispatch.
|
|
96
|
-
*/
|
|
97
|
-
encrypt(key, nonce, plaintext, aad = new Uint8Array(0)) {
|
|
98
|
-
if (this._disposed)
|
|
99
|
-
return Promise.reject(new Error('leviathan-crypto: pool is disposed'));
|
|
100
|
-
if (key.length !== 32)
|
|
101
|
-
return Promise.reject(new RangeError(`key must be 32 bytes (got ${key.length})`));
|
|
102
|
-
if (nonce.length !== 24)
|
|
103
|
-
return Promise.reject(new RangeError(`XChaCha20 nonce must be 24 bytes (got ${nonce.length})`));
|
|
104
|
-
return this._dispatch('encrypt', key, nonce, plaintext, aad);
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Decrypt ciphertext with XChaCha20-Poly1305.
|
|
108
|
-
* Input is `ciphertext || tag` (at least 16 bytes).
|
|
109
|
-
*
|
|
110
|
-
* **Warning:** All input buffers are transferred and neutered after dispatch.
|
|
111
|
-
*/
|
|
112
|
-
decrypt(key, nonce, ciphertext, aad = new Uint8Array(0)) {
|
|
113
|
-
if (this._disposed)
|
|
114
|
-
return Promise.reject(new Error('leviathan-crypto: pool is disposed'));
|
|
115
|
-
if (key.length !== 32)
|
|
116
|
-
return Promise.reject(new RangeError(`key must be 32 bytes (got ${key.length})`));
|
|
117
|
-
if (nonce.length !== 24)
|
|
118
|
-
return Promise.reject(new RangeError(`XChaCha20 nonce must be 24 bytes (got ${nonce.length})`));
|
|
119
|
-
if (ciphertext.length < 16)
|
|
120
|
-
return Promise.reject(new RangeError(`ciphertext too short — must include 16-byte tag (got ${ciphertext.length})`));
|
|
121
|
-
return this._dispatch('decrypt', key, nonce, ciphertext, aad);
|
|
122
|
-
}
|
|
123
|
-
/** Terminates all workers. Rejects all pending and queued jobs. */
|
|
124
|
-
dispose() {
|
|
125
|
-
if (this._disposed)
|
|
126
|
-
return;
|
|
127
|
-
this._disposed = true;
|
|
128
|
-
for (const w of this._workers)
|
|
129
|
-
w.terminate();
|
|
130
|
-
const err = new Error('leviathan-crypto: pool disposed');
|
|
131
|
-
for (const { reject } of this._pending.values())
|
|
132
|
-
reject(err);
|
|
133
|
-
for (const job of this._queue)
|
|
134
|
-
this._pending.get(job.id)?.reject(err);
|
|
135
|
-
this._pending.clear();
|
|
136
|
-
this._queue.length = 0;
|
|
137
|
-
}
|
|
138
|
-
/** Number of workers in the pool. */
|
|
139
|
-
get size() {
|
|
140
|
-
return this._workers.length;
|
|
141
|
-
}
|
|
142
|
-
/** Number of jobs currently queued (waiting for a free worker). */
|
|
143
|
-
get queueDepth() {
|
|
144
|
-
return this._queue.length;
|
|
145
|
-
}
|
|
146
|
-
// ── Internals ────────────────────────────────────────────────────────────
|
|
147
|
-
_dispatch(op, key, nonce, data, aad) {
|
|
148
|
-
return new Promise((resolve, reject) => {
|
|
149
|
-
const id = this._nextId++;
|
|
150
|
-
const job = { id, op, key, nonce, data, aad };
|
|
151
|
-
this._pending.set(id, { resolve, reject });
|
|
152
|
-
const worker = this._idle.pop();
|
|
153
|
-
if (worker)
|
|
154
|
-
this._send(worker, job);
|
|
155
|
-
else
|
|
156
|
-
this._queue.push(job);
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
_send(worker, job) {
|
|
160
|
-
worker.postMessage({ type: 'job', ...job }, [job.key.buffer, job.nonce.buffer, job.data.buffer, job.aad.buffer]);
|
|
161
|
-
}
|
|
162
|
-
_onMessage(worker, e) {
|
|
163
|
-
const msg = e.data;
|
|
164
|
-
const job = this._pending.get(msg.id);
|
|
165
|
-
if (!job)
|
|
166
|
-
return;
|
|
167
|
-
this._pending.delete(msg.id);
|
|
168
|
-
if (msg.type === 'result')
|
|
169
|
-
job.resolve(msg.data);
|
|
170
|
-
else
|
|
171
|
-
job.reject(new Error(msg.message));
|
|
172
|
-
const next = this._queue.shift();
|
|
173
|
-
if (next)
|
|
174
|
-
this._send(worker, next);
|
|
175
|
-
else
|
|
176
|
-
this._idle.push(worker);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// / <reference lib="webworker" />
|
|
2
|
-
// src/ts/chacha20/pool.worker.ts
|
|
3
|
-
//
|
|
4
|
-
// Worker entry point for XChaCha20Poly1305Pool. Runs in a Web Worker or
|
|
5
|
-
// worker_threads context — no access to the main thread's module cache.
|
|
6
|
-
// Owns its own WebAssembly.Instance with its own linear memory.
|
|
7
|
-
import { xcEncrypt, xcDecrypt } from './ops.js';
|
|
8
|
-
let x;
|
|
9
|
-
self.onmessage = async (e) => {
|
|
10
|
-
const msg = e.data;
|
|
11
|
-
if (msg.type === 'init') {
|
|
12
|
-
try {
|
|
13
|
-
const mem = new WebAssembly.Memory({ initial: 3, maximum: 3 });
|
|
14
|
-
const inst = await WebAssembly.instantiate(msg.module, { env: { memory: mem } });
|
|
15
|
-
x = inst.exports;
|
|
16
|
-
self.postMessage({ type: 'ready' });
|
|
17
|
-
}
|
|
18
|
-
catch (err) {
|
|
19
|
-
self.postMessage({ type: 'error', id: -1, message: err.message });
|
|
20
|
-
}
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (!x) {
|
|
24
|
-
self.postMessage({ type: 'error', id: msg.id, message: 'worker not initialized' });
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
try {
|
|
28
|
-
const { id, op, key, nonce, data, aad } = msg;
|
|
29
|
-
const result = op === 'encrypt'
|
|
30
|
-
? xcEncrypt(x, key, nonce, data, aad)
|
|
31
|
-
: xcDecrypt(x, key, nonce, data, aad);
|
|
32
|
-
self.postMessage({ type: 'result', id, data: result }, [result.buffer]);
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
self.postMessage({ type: 'error', id: msg.id, message: err.message });
|
|
36
|
-
}
|
|
37
|
-
};
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export declare class XChaCha20StreamSealer {
|
|
2
|
-
private readonly _x;
|
|
3
|
-
private readonly _key;
|
|
4
|
-
private readonly _cs;
|
|
5
|
-
private readonly _id;
|
|
6
|
-
private readonly _framed;
|
|
7
|
-
private readonly _aad;
|
|
8
|
-
private _index;
|
|
9
|
-
private _state;
|
|
10
|
-
/** Public: consumers use this 3-param form. */
|
|
11
|
-
constructor(key: Uint8Array, chunkSize?: number, opts?: {
|
|
12
|
-
framed?: boolean;
|
|
13
|
-
aad?: Uint8Array;
|
|
14
|
-
});
|
|
15
|
-
/** @internal Test-only overload to inject fixed stream_id for deterministic output. */
|
|
16
|
-
constructor(key: Uint8Array, chunkSize: number | undefined, opts: {
|
|
17
|
-
framed?: boolean;
|
|
18
|
-
aad?: Uint8Array;
|
|
19
|
-
} | undefined, _id: Uint8Array);
|
|
20
|
-
header(): Uint8Array;
|
|
21
|
-
seal(plaintext: Uint8Array): Uint8Array;
|
|
22
|
-
final(plaintext: Uint8Array): Uint8Array;
|
|
23
|
-
private _sealChunk;
|
|
24
|
-
private _wipe;
|
|
25
|
-
dispose(): void;
|
|
26
|
-
}
|
|
27
|
-
export declare class XChaCha20StreamOpener {
|
|
28
|
-
private readonly _x;
|
|
29
|
-
private readonly _key;
|
|
30
|
-
private readonly _cs;
|
|
31
|
-
private readonly _id;
|
|
32
|
-
private readonly _framed;
|
|
33
|
-
private readonly _aad;
|
|
34
|
-
private readonly _buf;
|
|
35
|
-
private readonly _maxFrame;
|
|
36
|
-
private _bufLen;
|
|
37
|
-
private _index;
|
|
38
|
-
private _dead;
|
|
39
|
-
constructor(key: Uint8Array, header: Uint8Array, opts?: {
|
|
40
|
-
framed?: boolean;
|
|
41
|
-
aad?: Uint8Array;
|
|
42
|
-
});
|
|
43
|
-
get closed(): boolean;
|
|
44
|
-
open(chunk: Uint8Array): Uint8Array;
|
|
45
|
-
private _openRaw;
|
|
46
|
-
feed(bytes: Uint8Array): Uint8Array[];
|
|
47
|
-
private _wipe;
|
|
48
|
-
dispose(): void;
|
|
49
|
-
}
|
|
@@ -1,327 +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/chacha20/stream-sealer.ts
|
|
23
|
-
//
|
|
24
|
-
// XChaCha20Stream — chunked authenticated encryption for large payloads.
|
|
25
|
-
// Tier 2 pure-TS: XChaCha20-Poly1305 per chunk. Simpler than SerpentStream —
|
|
26
|
-
// no HKDF, no HMAC; Poly1305 handles auth. Stream binding via per-chunk AAD.
|
|
27
|
-
// Wire format: header(20) | chunks: isLast(1) || nonce(24) || ct || tag(16)
|
|
28
|
-
import { getInstance } from '../init.js';
|
|
29
|
-
import { xcEncrypt, xcDecrypt } from './ops.js';
|
|
30
|
-
import { randomBytes, wipe } from '../utils.js';
|
|
31
|
-
import { _chachaReady } from './index.js';
|
|
32
|
-
const CHUNK_MIN = 1024;
|
|
33
|
-
const CHUNK_MAX = 65536;
|
|
34
|
-
const CHUNK_DEF = 65536;
|
|
35
|
-
function u32be(n) {
|
|
36
|
-
const b = new Uint8Array(4);
|
|
37
|
-
b[0] = (n >>> 24) & 0xff;
|
|
38
|
-
b[1] = (n >>> 16) & 0xff;
|
|
39
|
-
b[2] = (n >>> 8) & 0xff;
|
|
40
|
-
b[3] = n & 0xff;
|
|
41
|
-
return b;
|
|
42
|
-
}
|
|
43
|
-
function u64be(n) {
|
|
44
|
-
const b = new Uint8Array(8);
|
|
45
|
-
const hi = Math.floor(n / 0x100000000);
|
|
46
|
-
const lo = n >>> 0;
|
|
47
|
-
b[0] = (hi >>> 24) & 0xff;
|
|
48
|
-
b[1] = (hi >>> 16) & 0xff;
|
|
49
|
-
b[2] = (hi >>> 8) & 0xff;
|
|
50
|
-
b[3] = hi & 0xff;
|
|
51
|
-
b[4] = (lo >>> 24) & 0xff;
|
|
52
|
-
b[5] = (lo >>> 16) & 0xff;
|
|
53
|
-
b[6] = (lo >>> 8) & 0xff;
|
|
54
|
-
b[7] = lo & 0xff;
|
|
55
|
-
return b;
|
|
56
|
-
}
|
|
57
|
-
function getExports() {
|
|
58
|
-
return getInstance('chacha20').exports;
|
|
59
|
-
}
|
|
60
|
-
/** Build internal AAD: stream_id(16) || u64be(index) || isLast(1) || u32be(userAad.length) || userAad */
|
|
61
|
-
function chunkAAD(streamId, index, isLast, userAad) {
|
|
62
|
-
const out = new Uint8Array(16 + 8 + 1 + 4 + userAad.length);
|
|
63
|
-
let off = 0;
|
|
64
|
-
out.set(streamId, off);
|
|
65
|
-
off += 16;
|
|
66
|
-
out.set(u64be(index), off);
|
|
67
|
-
off += 8;
|
|
68
|
-
out[off++] = isLast ? 1 : 0;
|
|
69
|
-
out.set(u32be(userAad.length), off);
|
|
70
|
-
off += 4;
|
|
71
|
-
out.set(userAad, off);
|
|
72
|
-
return out;
|
|
73
|
-
}
|
|
74
|
-
export class XChaCha20StreamSealer {
|
|
75
|
-
_x;
|
|
76
|
-
_key;
|
|
77
|
-
_cs;
|
|
78
|
-
_id; // stream_id (16 bytes)
|
|
79
|
-
_framed;
|
|
80
|
-
_aad;
|
|
81
|
-
_index;
|
|
82
|
-
_state;
|
|
83
|
-
constructor(key, chunkSize, opts, _id) {
|
|
84
|
-
if (!_chachaReady())
|
|
85
|
-
throw new Error('leviathan-crypto: call init([\'chacha20\']) before using XChaCha20StreamSealer');
|
|
86
|
-
if (key.length !== 32)
|
|
87
|
-
throw new RangeError(`XChaCha20StreamSealer key must be 32 bytes (got ${key.length})`);
|
|
88
|
-
const cs = chunkSize ?? CHUNK_DEF;
|
|
89
|
-
if (cs < CHUNK_MIN || cs > CHUNK_MAX)
|
|
90
|
-
throw new RangeError(`XChaCha20StreamSealer chunkSize must be ${CHUNK_MIN}..${CHUNK_MAX} (got ${cs})`);
|
|
91
|
-
this._x = getExports();
|
|
92
|
-
this._key = key.slice();
|
|
93
|
-
this._cs = cs;
|
|
94
|
-
this._framed = opts?.framed ?? false;
|
|
95
|
-
this._aad = opts?.aad ? opts.aad.slice() : new Uint8Array(0);
|
|
96
|
-
this._id = new Uint8Array(16);
|
|
97
|
-
if (_id && _id.length === 16)
|
|
98
|
-
this._id.set(_id);
|
|
99
|
-
else
|
|
100
|
-
crypto.getRandomValues(this._id);
|
|
101
|
-
this._index = 0;
|
|
102
|
-
this._state = 'fresh';
|
|
103
|
-
}
|
|
104
|
-
header() {
|
|
105
|
-
if (this._state === 'sealing')
|
|
106
|
-
throw new Error('XChaCha20StreamSealer: header() already called');
|
|
107
|
-
if (this._state === 'dead')
|
|
108
|
-
throw new Error('XChaCha20StreamSealer: stream is closed');
|
|
109
|
-
this._state = 'sealing';
|
|
110
|
-
const hdr = new Uint8Array(20);
|
|
111
|
-
hdr.set(this._id, 0);
|
|
112
|
-
hdr.set(u32be(this._cs), 16);
|
|
113
|
-
return hdr;
|
|
114
|
-
}
|
|
115
|
-
seal(plaintext) {
|
|
116
|
-
if (this._state === 'fresh')
|
|
117
|
-
throw new Error('XChaCha20StreamSealer: call header() first');
|
|
118
|
-
if (this._state === 'dead')
|
|
119
|
-
throw new Error('XChaCha20StreamSealer: stream is closed');
|
|
120
|
-
if (plaintext.length !== this._cs)
|
|
121
|
-
throw new RangeError(`XChaCha20StreamSealer: seal() requires exactly ${this._cs} bytes (got ${plaintext.length})`);
|
|
122
|
-
return this._sealChunk(plaintext, false);
|
|
123
|
-
}
|
|
124
|
-
final(plaintext) {
|
|
125
|
-
if (this._state === 'fresh')
|
|
126
|
-
throw new Error('XChaCha20StreamSealer: call header() first');
|
|
127
|
-
if (this._state === 'dead')
|
|
128
|
-
throw new Error('XChaCha20StreamSealer: stream is closed');
|
|
129
|
-
if (plaintext.length > this._cs)
|
|
130
|
-
throw new RangeError(`XChaCha20StreamSealer: final() plaintext exceeds chunkSize (got ${plaintext.length})`);
|
|
131
|
-
const out = this._sealChunk(plaintext, true);
|
|
132
|
-
this._wipe();
|
|
133
|
-
return out;
|
|
134
|
-
}
|
|
135
|
-
_sealChunk(plaintext, isLast) {
|
|
136
|
-
const nonce = randomBytes(24);
|
|
137
|
-
const aad = chunkAAD(this._id, this._index, isLast, this._aad);
|
|
138
|
-
const sealed = xcEncrypt(this._x, this._key, nonce, plaintext, aad);
|
|
139
|
-
this._index++;
|
|
140
|
-
// sealed = ciphertext || tag(16)
|
|
141
|
-
// wire chunk = isLast(1) || nonce(24) || sealed
|
|
142
|
-
const chunk = new Uint8Array(1 + 24 + sealed.length);
|
|
143
|
-
chunk[0] = isLast ? 1 : 0;
|
|
144
|
-
chunk.set(nonce, 1);
|
|
145
|
-
chunk.set(sealed, 25);
|
|
146
|
-
if (!this._framed)
|
|
147
|
-
return chunk;
|
|
148
|
-
const out = new Uint8Array(4 + chunk.length);
|
|
149
|
-
out.set(u32be(chunk.length), 0);
|
|
150
|
-
out.set(chunk, 4);
|
|
151
|
-
return out;
|
|
152
|
-
}
|
|
153
|
-
_wipe() {
|
|
154
|
-
wipe(this._key);
|
|
155
|
-
wipe(this._id);
|
|
156
|
-
wipe(this._aad);
|
|
157
|
-
this._x.wipeBuffers();
|
|
158
|
-
this._state = 'dead';
|
|
159
|
-
}
|
|
160
|
-
dispose() {
|
|
161
|
-
if (this._state !== 'dead')
|
|
162
|
-
this._wipe();
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
export class XChaCha20StreamOpener {
|
|
166
|
-
_x;
|
|
167
|
-
_key;
|
|
168
|
-
_cs;
|
|
169
|
-
_id; // stream_id from header
|
|
170
|
-
_framed;
|
|
171
|
-
_aad;
|
|
172
|
-
_buf;
|
|
173
|
-
_maxFrame;
|
|
174
|
-
_bufLen;
|
|
175
|
-
_index;
|
|
176
|
-
_dead;
|
|
177
|
-
constructor(key, header, opts) {
|
|
178
|
-
if (!_chachaReady())
|
|
179
|
-
throw new Error('leviathan-crypto: call init([\'chacha20\']) before using XChaCha20StreamOpener');
|
|
180
|
-
if (key.length !== 32)
|
|
181
|
-
throw new RangeError(`XChaCha20StreamOpener key must be 32 bytes (got ${key.length})`);
|
|
182
|
-
if (header.length !== 20)
|
|
183
|
-
throw new RangeError(`XChaCha20StreamOpener header must be 20 bytes (got ${header.length})`);
|
|
184
|
-
this._x = getExports();
|
|
185
|
-
this._key = key.slice();
|
|
186
|
-
this._id = header.slice(0, 16);
|
|
187
|
-
this._cs = (header[16] << 24 | header[17] << 16 | header[18] << 8 | header[19]) >>> 0;
|
|
188
|
-
if (this._cs < CHUNK_MIN || this._cs > CHUNK_MAX)
|
|
189
|
-
throw new RangeError(`XChaCha20StreamOpener: header contains invalid chunkSize ${this._cs} (expected ${CHUNK_MIN}..${CHUNK_MAX})`);
|
|
190
|
-
this._framed = opts?.framed ?? false;
|
|
191
|
-
this._aad = opts?.aad ? opts.aad.slice() : new Uint8Array(0);
|
|
192
|
-
this._index = 0;
|
|
193
|
-
this._dead = false;
|
|
194
|
-
this._bufLen = 0;
|
|
195
|
-
if (this._framed) {
|
|
196
|
-
// max sealed chunk: 1 + 24 + CHUNK_MAX + 16
|
|
197
|
-
this._maxFrame = 4 + 1 + 24 + CHUNK_MAX + 16;
|
|
198
|
-
this._buf = new Uint8Array(this._maxFrame);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
get closed() {
|
|
202
|
-
return this._dead;
|
|
203
|
-
}
|
|
204
|
-
open(chunk) {
|
|
205
|
-
if (this._dead)
|
|
206
|
-
throw new Error('XChaCha20StreamOpener: stream is closed');
|
|
207
|
-
if (this._framed)
|
|
208
|
-
throw new Error('XChaCha20StreamOpener: call feed() on framed openers — open() expects raw sealed chunks without length prefix');
|
|
209
|
-
return this._openRaw(chunk);
|
|
210
|
-
}
|
|
211
|
-
_openRaw(chunk) {
|
|
212
|
-
// isLast(1) || nonce(24) || ciphertext || tag(16)
|
|
213
|
-
if (chunk.length < 1 + 24 + 16)
|
|
214
|
-
throw new RangeError('XChaCha20StreamOpener: chunk too short');
|
|
215
|
-
const isLast = chunk[0] !== 0;
|
|
216
|
-
const nonce = chunk.subarray(1, 25);
|
|
217
|
-
const payload = chunk.subarray(25); // ciphertext || tag(16)
|
|
218
|
-
if (payload.length < 16)
|
|
219
|
-
throw new RangeError('XChaCha20StreamOpener: chunk too short for tag');
|
|
220
|
-
const aad = chunkAAD(this._id, this._index, isLast, this._aad);
|
|
221
|
-
// xcDecrypt expects ciphertext || tag combined
|
|
222
|
-
const plaintext = xcDecrypt(this._x, this._key, nonce, payload, aad);
|
|
223
|
-
this._index++;
|
|
224
|
-
if (isLast)
|
|
225
|
-
this._wipe();
|
|
226
|
-
return plaintext;
|
|
227
|
-
}
|
|
228
|
-
feed(bytes) {
|
|
229
|
-
if (!this._framed)
|
|
230
|
-
throw new Error('XChaCha20StreamOpener: feed() requires { framed: true }');
|
|
231
|
-
if (this._dead)
|
|
232
|
-
throw new Error('XChaCha20StreamOpener: stream is closed');
|
|
233
|
-
const buf = this._buf;
|
|
234
|
-
const maxFrame = this._maxFrame;
|
|
235
|
-
const results = [];
|
|
236
|
-
let consumed = 0;
|
|
237
|
-
// ── Phase 1: drain carry-over ─────────────────────────────────────
|
|
238
|
-
if (this._bufLen > 0) {
|
|
239
|
-
if (this._bufLen < 4) {
|
|
240
|
-
const need = 4 - this._bufLen;
|
|
241
|
-
const take = Math.min(need, bytes.length - consumed);
|
|
242
|
-
buf.set(bytes.subarray(consumed, consumed + take), this._bufLen);
|
|
243
|
-
this._bufLen += take;
|
|
244
|
-
consumed += take;
|
|
245
|
-
if (this._bufLen < 4)
|
|
246
|
-
return results;
|
|
247
|
-
}
|
|
248
|
-
const sealedLen = (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
|
|
249
|
-
if (sealedLen === 0 || sealedLen > (maxFrame - 4)) {
|
|
250
|
-
this._wipe();
|
|
251
|
-
throw new Error('XChaCha20StreamOpener: invalid sealed chunk length');
|
|
252
|
-
}
|
|
253
|
-
const frameLen = 4 + sealedLen;
|
|
254
|
-
const haveInBuf = this._bufLen;
|
|
255
|
-
const needMore = frameLen - haveInBuf;
|
|
256
|
-
if (needMore > 0) {
|
|
257
|
-
const take = Math.min(needMore, bytes.length - consumed);
|
|
258
|
-
buf.set(bytes.subarray(consumed, consumed + take), haveInBuf);
|
|
259
|
-
this._bufLen += take;
|
|
260
|
-
consumed += take;
|
|
261
|
-
if (this._bufLen < frameLen)
|
|
262
|
-
return results;
|
|
263
|
-
}
|
|
264
|
-
results.push(this._openRaw(buf.subarray(4, frameLen)));
|
|
265
|
-
this._bufLen = 0;
|
|
266
|
-
if (this._dead) {
|
|
267
|
-
if (consumed < bytes.length) {
|
|
268
|
-
this._wipe();
|
|
269
|
-
throw new Error('XChaCha20StreamOpener: unexpected bytes after final chunk');
|
|
270
|
-
}
|
|
271
|
-
return results;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
// ── Phase 2: parse complete frames directly from bytes ────────────
|
|
275
|
-
let pos = consumed;
|
|
276
|
-
while (true) {
|
|
277
|
-
if (bytes.length - pos < 4)
|
|
278
|
-
break;
|
|
279
|
-
const sealedLen = (bytes[pos] << 24 | bytes[pos + 1] << 16 | bytes[pos + 2] << 8 | bytes[pos + 3]) >>> 0;
|
|
280
|
-
if (sealedLen === 0 || sealedLen > (maxFrame - 4)) {
|
|
281
|
-
this._wipe();
|
|
282
|
-
throw new Error('XChaCha20StreamOpener: invalid sealed chunk length');
|
|
283
|
-
}
|
|
284
|
-
const frameLen = 4 + sealedLen;
|
|
285
|
-
if (bytes.length - pos < frameLen)
|
|
286
|
-
break;
|
|
287
|
-
results.push(this._openRaw(bytes.subarray(pos + 4, pos + frameLen)));
|
|
288
|
-
if (this._dead) {
|
|
289
|
-
const remaining = bytes.length - pos - frameLen;
|
|
290
|
-
if (remaining > 0) {
|
|
291
|
-
this._wipe();
|
|
292
|
-
throw new Error('XChaCha20StreamOpener: unexpected bytes after final chunk');
|
|
293
|
-
}
|
|
294
|
-
return results;
|
|
295
|
-
}
|
|
296
|
-
pos += frameLen;
|
|
297
|
-
}
|
|
298
|
-
// ── Carry over incomplete trailing bytes ──────────────────────────
|
|
299
|
-
const leftover = bytes.length - pos;
|
|
300
|
-
if (leftover > 0) {
|
|
301
|
-
if (leftover > maxFrame) {
|
|
302
|
-
this._wipe();
|
|
303
|
-
throw new Error('XChaCha20StreamOpener: input exceeds maximum frame size');
|
|
304
|
-
}
|
|
305
|
-
buf.set(bytes.subarray(pos), 0);
|
|
306
|
-
this._bufLen = leftover;
|
|
307
|
-
}
|
|
308
|
-
return results;
|
|
309
|
-
}
|
|
310
|
-
_wipe() {
|
|
311
|
-
if (this._dead)
|
|
312
|
-
return;
|
|
313
|
-
wipe(this._key);
|
|
314
|
-
wipe(this._id);
|
|
315
|
-
wipe(this._aad);
|
|
316
|
-
this._x.wipeBuffers();
|
|
317
|
-
if (this._framed) {
|
|
318
|
-
wipe(this._buf);
|
|
319
|
-
this._bufLen = 0;
|
|
320
|
-
}
|
|
321
|
-
this._dead = true;
|
|
322
|
-
}
|
|
323
|
-
dispose() {
|
|
324
|
-
if (!this._dead)
|
|
325
|
-
this._wipe();
|
|
326
|
-
}
|
|
327
|
-
}
|