leviathan-crypto 1.3.1 → 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 -76
- package/README.md +166 -221
- package/SECURITY.md +89 -37
- 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 -7
- package/dist/chacha20/index.js +41 -14
- 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 +218 -150
- package/dist/docs/exports.md +241 -0
- package/dist/docs/fortuna.md +65 -74
- package/dist/docs/init.md +172 -178
- package/dist/docs/loader.md +87 -132
- package/dist/docs/serpent.md +134 -565
- 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 +114 -41
- package/dist/embedded/chacha20.d.ts +1 -1
- package/dist/embedded/chacha20.js +2 -1
- package/dist/embedded/kyber.d.ts +1 -0
- package/dist/embedded/kyber.js +3 -0
- package/dist/embedded/serpent.d.ts +1 -1
- package/dist/embedded/serpent.js +2 -1
- package/dist/embedded/sha2.d.ts +1 -1
- package/dist/embedded/sha2.js +2 -1
- package/dist/embedded/sha3.d.ts +1 -1
- package/dist/embedded/sha3.js +2 -1
- package/dist/errors.d.ts +10 -0
- package/dist/{serpent/seal.js → errors.js} +14 -46
- package/dist/fortuna.d.ts +2 -8
- package/dist/fortuna.js +11 -9
- 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 +19 -4
- package/dist/loader.js +91 -25
- 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 +12 -7
- package/dist/utils.js +75 -19
- 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 -188
- package/dist/chacha20/pool.worker.js +0 -37
- 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/stream-pool.d.ts +0 -48
- package/dist/serpent/stream-pool.js +0 -285
- package/dist/serpent/stream-sealer.d.ts +0 -50
- package/dist/serpent/stream-sealer.js +0 -341
- 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,188 +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
|
-
// ── Module-private base64 decoder (copied from loader.ts) ────────────────────
|
|
8
|
-
function base64ToBytes(b64) {
|
|
9
|
-
if (typeof atob === 'function') {
|
|
10
|
-
const raw = atob(b64);
|
|
11
|
-
const out = new Uint8Array(raw.length);
|
|
12
|
-
for (let i = 0; i < raw.length; i++)
|
|
13
|
-
out[i] = raw.charCodeAt(i);
|
|
14
|
-
return out;
|
|
15
|
-
}
|
|
16
|
-
return new Uint8Array(Buffer.from(b64, 'base64'));
|
|
17
|
-
}
|
|
18
|
-
// ── WASM module singleton ────────────────────────────────────────────────────
|
|
19
|
-
let _wasmModule;
|
|
20
|
-
async function getWasmModule() {
|
|
21
|
-
if (_wasmModule)
|
|
22
|
-
return _wasmModule;
|
|
23
|
-
const { WASM_BASE64 } = await import('../embedded/chacha20.js');
|
|
24
|
-
const bytes = base64ToBytes(WASM_BASE64);
|
|
25
|
-
_wasmModule = await WebAssembly.compile(bytes.buffer);
|
|
26
|
-
return _wasmModule;
|
|
27
|
-
}
|
|
28
|
-
// ── Worker spawning ──────────────────────────────────────────────────────────
|
|
29
|
-
function spawnWorker(mod) {
|
|
30
|
-
return new Promise((resolve, reject) => {
|
|
31
|
-
const worker = new Worker(new URL('./pool.worker.js', import.meta.url), { type: 'module' });
|
|
32
|
-
const onMessage = (e) => {
|
|
33
|
-
cleanup();
|
|
34
|
-
if (e.data.type === 'ready') {
|
|
35
|
-
resolve(worker);
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
worker.terminate();
|
|
39
|
-
reject(new Error(`leviathan-crypto: worker init failed: ${e.data.message}`));
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
const onError = (e) => {
|
|
43
|
-
cleanup();
|
|
44
|
-
worker.terminate();
|
|
45
|
-
reject(new Error(`leviathan-crypto: worker init failed: ${e.message}`));
|
|
46
|
-
};
|
|
47
|
-
const cleanup = () => {
|
|
48
|
-
worker.removeEventListener('message', onMessage);
|
|
49
|
-
worker.removeEventListener('error', onError);
|
|
50
|
-
};
|
|
51
|
-
worker.addEventListener('message', onMessage);
|
|
52
|
-
worker.addEventListener('error', onError);
|
|
53
|
-
worker.postMessage({ type: 'init', module: mod });
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
// ── Pool class ───────────────────────────────────────────────────────────────
|
|
57
|
-
/**
|
|
58
|
-
* Parallel worker pool for XChaCha20-Poly1305 AEAD.
|
|
59
|
-
*
|
|
60
|
-
* Each worker owns its own `WebAssembly.Instance` with isolated linear memory.
|
|
61
|
-
* Jobs are dispatched round-robin to idle workers; excess jobs queue until a
|
|
62
|
-
* worker frees up.
|
|
63
|
-
*
|
|
64
|
-
* **Warning:** Input buffers (`key`, `nonce`, `plaintext`/`ciphertext`, `aad`)
|
|
65
|
-
* are transferred to the worker and neutered on the calling side. The caller
|
|
66
|
-
* must copy any buffer they need to retain after calling `encrypt()`/`decrypt()`.
|
|
67
|
-
*/
|
|
68
|
-
export class XChaCha20Poly1305Pool {
|
|
69
|
-
_workers;
|
|
70
|
-
_idle;
|
|
71
|
-
_queue;
|
|
72
|
-
_pending;
|
|
73
|
-
_nextId;
|
|
74
|
-
_disposed;
|
|
75
|
-
constructor(workers) {
|
|
76
|
-
this._workers = workers;
|
|
77
|
-
this._idle = [...workers];
|
|
78
|
-
this._queue = [];
|
|
79
|
-
this._pending = new Map();
|
|
80
|
-
this._nextId = 0;
|
|
81
|
-
this._disposed = false;
|
|
82
|
-
for (const w of workers) {
|
|
83
|
-
w.onmessage = (e) => this._onMessage(w, e);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Create a new pool. Requires `init(['chacha20'])` to have been called.
|
|
88
|
-
* Compiles the WASM module once and distributes it to all workers.
|
|
89
|
-
*/
|
|
90
|
-
static async create(opts) {
|
|
91
|
-
if (!isInitialized('chacha20'))
|
|
92
|
-
throw new Error('leviathan-crypto: call init([\'chacha20\']) before using XChaCha20Poly1305Pool');
|
|
93
|
-
const n = opts?.workers ?? (typeof navigator !== 'undefined' ? navigator.hardwareConcurrency : undefined) ?? 4;
|
|
94
|
-
const mod = await getWasmModule();
|
|
95
|
-
// Sequential spawn — compatible with inline-worker test environments
|
|
96
|
-
const workers = [];
|
|
97
|
-
for (let i = 0; i < n; i++)
|
|
98
|
-
workers.push(await spawnWorker(mod));
|
|
99
|
-
return new XChaCha20Poly1305Pool(workers);
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Encrypt plaintext with XChaCha20-Poly1305.
|
|
103
|
-
* Returns `ciphertext || tag` (plaintext.length + 16 bytes).
|
|
104
|
-
*
|
|
105
|
-
* **Warning:** All input buffers are transferred and neutered after dispatch.
|
|
106
|
-
*/
|
|
107
|
-
encrypt(key, nonce, plaintext, aad = new Uint8Array(0)) {
|
|
108
|
-
if (this._disposed)
|
|
109
|
-
return Promise.reject(new Error('leviathan-crypto: pool is disposed'));
|
|
110
|
-
if (key.length !== 32)
|
|
111
|
-
return Promise.reject(new RangeError(`key must be 32 bytes (got ${key.length})`));
|
|
112
|
-
if (nonce.length !== 24)
|
|
113
|
-
return Promise.reject(new RangeError(`XChaCha20 nonce must be 24 bytes (got ${nonce.length})`));
|
|
114
|
-
return this._dispatch('encrypt', key, nonce, plaintext, aad);
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Decrypt ciphertext with XChaCha20-Poly1305.
|
|
118
|
-
* Input is `ciphertext || tag` (at least 16 bytes).
|
|
119
|
-
*
|
|
120
|
-
* **Warning:** All input buffers are transferred and neutered after dispatch.
|
|
121
|
-
*/
|
|
122
|
-
decrypt(key, nonce, ciphertext, aad = new Uint8Array(0)) {
|
|
123
|
-
if (this._disposed)
|
|
124
|
-
return Promise.reject(new Error('leviathan-crypto: pool is disposed'));
|
|
125
|
-
if (key.length !== 32)
|
|
126
|
-
return Promise.reject(new RangeError(`key must be 32 bytes (got ${key.length})`));
|
|
127
|
-
if (nonce.length !== 24)
|
|
128
|
-
return Promise.reject(new RangeError(`XChaCha20 nonce must be 24 bytes (got ${nonce.length})`));
|
|
129
|
-
if (ciphertext.length < 16)
|
|
130
|
-
return Promise.reject(new RangeError(`ciphertext too short — must include 16-byte tag (got ${ciphertext.length})`));
|
|
131
|
-
return this._dispatch('decrypt', key, nonce, ciphertext, aad);
|
|
132
|
-
}
|
|
133
|
-
/** Terminates all workers. Rejects all pending and queued jobs. */
|
|
134
|
-
dispose() {
|
|
135
|
-
if (this._disposed)
|
|
136
|
-
return;
|
|
137
|
-
this._disposed = true;
|
|
138
|
-
for (const w of this._workers)
|
|
139
|
-
w.terminate();
|
|
140
|
-
const err = new Error('leviathan-crypto: pool disposed');
|
|
141
|
-
for (const { reject } of this._pending.values())
|
|
142
|
-
reject(err);
|
|
143
|
-
for (const job of this._queue)
|
|
144
|
-
this._pending.get(job.id)?.reject(err);
|
|
145
|
-
this._pending.clear();
|
|
146
|
-
this._queue.length = 0;
|
|
147
|
-
}
|
|
148
|
-
/** Number of workers in the pool. */
|
|
149
|
-
get size() {
|
|
150
|
-
return this._workers.length;
|
|
151
|
-
}
|
|
152
|
-
/** Number of jobs currently queued (waiting for a free worker). */
|
|
153
|
-
get queueDepth() {
|
|
154
|
-
return this._queue.length;
|
|
155
|
-
}
|
|
156
|
-
// ── Internals ────────────────────────────────────────────────────────────
|
|
157
|
-
_dispatch(op, key, nonce, data, aad) {
|
|
158
|
-
return new Promise((resolve, reject) => {
|
|
159
|
-
const id = this._nextId++;
|
|
160
|
-
const job = { id, op, key, nonce, data, aad };
|
|
161
|
-
this._pending.set(id, { resolve, reject });
|
|
162
|
-
const worker = this._idle.pop();
|
|
163
|
-
if (worker)
|
|
164
|
-
this._send(worker, job);
|
|
165
|
-
else
|
|
166
|
-
this._queue.push(job);
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
_send(worker, job) {
|
|
170
|
-
worker.postMessage({ type: 'job', ...job }, [job.key.buffer, job.nonce.buffer, job.data.buffer, job.aad.buffer]);
|
|
171
|
-
}
|
|
172
|
-
_onMessage(worker, e) {
|
|
173
|
-
const msg = e.data;
|
|
174
|
-
const job = this._pending.get(msg.id);
|
|
175
|
-
if (!job)
|
|
176
|
-
return;
|
|
177
|
-
this._pending.delete(msg.id);
|
|
178
|
-
if (msg.type === 'result')
|
|
179
|
-
job.resolve(msg.data);
|
|
180
|
-
else
|
|
181
|
-
job.reject(new Error(msg.message));
|
|
182
|
-
const next = this._queue.shift();
|
|
183
|
-
if (next)
|
|
184
|
-
this._send(worker, next);
|
|
185
|
-
else
|
|
186
|
-
this._idle.push(worker);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
@@ -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,309 +0,0 @@
|
|
|
1
|
-
# XChaCha20Poly1305Pool: Parallel Worker Pool for Authenticated Encryption
|
|
2
|
-
|
|
3
|
-
> [!NOTE]
|
|
4
|
-
> A worker pool that dispatches independent XChaCha20-Poly1305 AEAD operations
|
|
5
|
-
> across multiple Web Workers, each with its own isolated WebAssembly instance.
|
|
6
|
-
>
|
|
7
|
-
> See [ChaCha20-Poly1305 implementation audit](./chacha_audit.md) for algorithm correctness verifications.
|
|
8
|
-
|
|
9
|
-
## Overview
|
|
10
|
-
|
|
11
|
-
`XChaCha20Poly1305Pool` parallelizes XChaCha20-Poly1305 encrypt and decrypt
|
|
12
|
-
operations across Web Workers. Each worker owns its own `WebAssembly.Instance`
|
|
13
|
-
with its own linear memory -- there is no shared state between workers.
|
|
14
|
-
|
|
15
|
-
Use the pool when you need to process many independent AEAD operations
|
|
16
|
-
concurrently. Typical use cases include encrypting multiple independent messages,
|
|
17
|
-
batch processing encrypted records, or any scenario where multiple independent
|
|
18
|
-
encrypt/decrypt operations could benefit from parallelism.
|
|
19
|
-
|
|
20
|
-
Use the single-instance `XChaCha20Poly1305` when operations are sequential, when
|
|
21
|
-
you only process one message at a time, or when the overhead of worker
|
|
22
|
-
communication is not justified by the operation size.
|
|
23
|
-
|
|
24
|
-
**Throughput ceiling:** CPU-bound WASM throughput plateaus at
|
|
25
|
-
`navigator.hardwareConcurrency`. Adding more workers beyond this adds scheduling
|
|
26
|
-
overhead with no parallelism gain.
|
|
27
|
-
|
|
28
|
-
**Per-job size limit:** Each job is limited to 64 KB, the same limit as the
|
|
29
|
-
single-instance path. This is not a workaround limitation -- it is the correct
|
|
30
|
-
security boundary for independent AEAD operations. Each job is one complete,
|
|
31
|
-
independently authenticated AEAD operation. Do not split one logical message
|
|
32
|
-
across multiple pool calls and concatenate results -- this provides no
|
|
33
|
-
stream-level authenticity (reordering and truncation attacks go undetected).
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## Security Notes
|
|
38
|
-
|
|
39
|
-
- **Input buffers are transferred (neutered) after dispatch.** Once you call
|
|
40
|
-
`encrypt()` or `decrypt()`, the `key`, `nonce`, `plaintext`/`ciphertext`, and
|
|
41
|
-
`aad` buffers are transferred to the worker via `Transferable`. The caller's
|
|
42
|
-
`Uint8Array` views become detached -- reading them after the call returns
|
|
43
|
-
zero-length buffers. If you need to retain any input after calling
|
|
44
|
-
`encrypt()`/`decrypt()`, copy it first with `.slice()`.
|
|
45
|
-
|
|
46
|
-
- **64 KB limit is per independent AEAD operation.** Do not split one logical
|
|
47
|
-
message across multiple pool calls and concatenate the results. This creates a
|
|
48
|
-
stream without authentication -- an attacker can reorder, duplicate, or
|
|
49
|
-
truncate chunks without detection. A future chunked-AEAD streaming API is the
|
|
50
|
-
correct tool for large files.
|
|
51
|
-
|
|
52
|
-
- **All XChaCha20-Poly1305 security properties apply.** Nonce uniqueness per key
|
|
53
|
-
is required. The 24-byte nonce is safe for random generation via
|
|
54
|
-
`crypto.getRandomValues()` (collision probability is negligible for 2^64
|
|
55
|
-
messages).
|
|
56
|
-
|
|
57
|
-
- **Each worker owns isolated WASM memory.** Key material in one worker's linear
|
|
58
|
-
memory cannot leak to another worker, even in theory.
|
|
59
|
-
|
|
60
|
-
- **Workers are terminated on `dispose()`.** All WASM memory is released when
|
|
61
|
-
the worker process ends. There is no lingering key material.
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## API Reference
|
|
66
|
-
|
|
67
|
-
### `PoolOpts`
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
interface PoolOpts {
|
|
71
|
-
/** Number of workers. Default: navigator.hardwareConcurrency ?? 4 */
|
|
72
|
-
workers?: number;
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
### `XChaCha20Poly1305Pool.create(opts?)`
|
|
79
|
-
|
|
80
|
-
Static async factory. Returns a `Promise<XChaCha20Poly1305Pool>`.
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
static async create(opts?: PoolOpts): Promise<XChaCha20Poly1305Pool>
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
| Parameter | Type | Default | Description |
|
|
87
|
-
|-----------|------|---------|-------------|
|
|
88
|
-
| `opts.workers` | `number` | `navigator.hardwareConcurrency ?? 4` | Number of workers to spawn. |
|
|
89
|
-
|
|
90
|
-
Throws if `init(['chacha20'])` has not been called.
|
|
91
|
-
|
|
92
|
-
Direct construction with `new XChaCha20Poly1305Pool()` is not possible -- the
|
|
93
|
-
constructor is private.
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
### `encrypt(key, nonce, plaintext, aad?)`
|
|
98
|
-
|
|
99
|
-
Encrypt plaintext with XChaCha20-Poly1305.
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
encrypt(
|
|
103
|
-
key: Uint8Array, // 32 bytes
|
|
104
|
-
nonce: Uint8Array, // 24 bytes
|
|
105
|
-
plaintext: Uint8Array, // up to 64 KB
|
|
106
|
-
aad?: Uint8Array, // optional additional authenticated data
|
|
107
|
-
): Promise<Uint8Array> // ciphertext || tag (plaintext.length + 16 bytes)
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
| Parameter | Type | Constraints | Description |
|
|
111
|
-
|-----------|------|-------------|-------------|
|
|
112
|
-
| `key` | `Uint8Array` | 32 bytes | Encryption key |
|
|
113
|
-
| `nonce` | `Uint8Array` | 24 bytes | Unique nonce |
|
|
114
|
-
| `plaintext` | `Uint8Array` | 0--65536 bytes | Data to encrypt |
|
|
115
|
-
| `aad` | `Uint8Array` | any length | Additional authenticated data (default: empty) |
|
|
116
|
-
|
|
117
|
-
Returns `ciphertext || tag` (`plaintext.length + 16` bytes).
|
|
118
|
-
|
|
119
|
-
> [!WARNING]
|
|
120
|
-
> All input buffers are transferred and neutered after dispatch.
|
|
121
|
-
|
|
122
|
-
### `decrypt(key, nonce, ciphertext, aad?)`
|
|
123
|
-
|
|
124
|
-
Decrypt ciphertext with XChaCha20-Poly1305.
|
|
125
|
-
|
|
126
|
-
```typescript
|
|
127
|
-
decrypt(
|
|
128
|
-
key: Uint8Array, // 32 bytes
|
|
129
|
-
nonce: Uint8Array, // 24 bytes
|
|
130
|
-
ciphertext: Uint8Array, // ciphertext || tag (at least 16 bytes)
|
|
131
|
-
aad?: Uint8Array, // must match the AAD used during encryption
|
|
132
|
-
): Promise<Uint8Array> // plaintext
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
| Parameter | Type | Constraints | Description |
|
|
136
|
-
|-----------|------|-------------|-------------|
|
|
137
|
-
| `key` | `Uint8Array` | 32 bytes | Decryption key |
|
|
138
|
-
| `nonce` | `Uint8Array` | 24 bytes | Same nonce used for encryption |
|
|
139
|
-
| `ciphertext` | `Uint8Array` | >= 16 bytes | `ciphertext || tag` from `encrypt()` |
|
|
140
|
-
| `aad` | `Uint8Array` | any length | Same AAD used during encryption (default: empty) |
|
|
141
|
-
|
|
142
|
-
Returns the decrypted plaintext.
|
|
143
|
-
|
|
144
|
-
Rejects with `Error('ChaCha20Poly1305: authentication failed')` if the tag does
|
|
145
|
-
not match (tampered ciphertext, wrong key, wrong nonce, or wrong AAD).
|
|
146
|
-
|
|
147
|
-
> [!WARNING]
|
|
148
|
-
> All input buffers are transferred and neutered after dispatch.
|
|
149
|
-
|
|
150
|
-
### `dispose()`
|
|
151
|
-
|
|
152
|
-
Terminate all workers and reject all pending and queued jobs.
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
dispose(): void
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
After `dispose()`, all calls to `encrypt()` and `decrypt()` reject immediately.
|
|
159
|
-
Calling `dispose()` multiple times is safe (idempotent).
|
|
160
|
-
|
|
161
|
-
---
|
|
162
|
-
|
|
163
|
-
### `size`
|
|
164
|
-
|
|
165
|
-
Number of workers in the pool.
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
get size(): number
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
---
|
|
172
|
-
|
|
173
|
-
### `queueDepth`
|
|
174
|
-
|
|
175
|
-
Number of jobs currently queued (waiting for a free worker).
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
get queueDepth(): number
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Returns 0 when all workers are idle.
|
|
182
|
-
|
|
183
|
-
---
|
|
184
|
-
|
|
185
|
-
## Performance Notes
|
|
186
|
-
|
|
187
|
-
Throughput plateaus at `navigator.hardwareConcurrency` workers for CPU-bound
|
|
188
|
-
WASM operations. Adding more workers beyond this count introduces scheduling
|
|
189
|
-
overhead without additional parallelism.
|
|
190
|
-
|
|
191
|
-
The `workers` option lets you tune the count:
|
|
192
|
-
- **Default** (`navigator.hardwareConcurrency ?? 4`) -- optimal for most systems
|
|
193
|
-
- **Fewer workers** -- useful if you need to leave cores available for other work
|
|
194
|
-
- **More workers** -- only beneficial on hyperthreaded CPUs where
|
|
195
|
-
`hardwareConcurrency` includes virtual cores that provide some additional
|
|
196
|
-
throughput
|
|
197
|
-
|
|
198
|
-
Each worker carries a fixed overhead: one `WebAssembly.Instance` (192 KB linear
|
|
199
|
-
memory) plus the worker thread itself. For most workloads, the default is correct.
|
|
200
|
-
|
|
201
|
-
Job dispatch uses `Transferable` buffers to avoid copy overhead on 64 KB payloads.
|
|
202
|
-
The downside is that input buffers are neutered on the calling side -- see
|
|
203
|
-
Security Notes.
|
|
204
|
-
|
|
205
|
-
---
|
|
206
|
-
|
|
207
|
-
## Usage Examples
|
|
208
|
-
|
|
209
|
-
### Basic -- create pool, encrypt/decrypt one message
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
import { init, XChaCha20Poly1305Pool, randomBytes } from 'leviathan-crypto'
|
|
213
|
-
|
|
214
|
-
await init(['chacha20'])
|
|
215
|
-
|
|
216
|
-
const pool = await XChaCha20Poly1305Pool.create()
|
|
217
|
-
|
|
218
|
-
const key = randomBytes(32)
|
|
219
|
-
const nonce = randomBytes(24)
|
|
220
|
-
const plaintext = new TextEncoder().encode('Hello, world!')
|
|
221
|
-
|
|
222
|
-
// Copy inputs before passing to the pool (they will be neutered)
|
|
223
|
-
const ct = await pool.encrypt(key.slice(), nonce.slice(), plaintext.slice())
|
|
224
|
-
const pt = await pool.decrypt(key.slice(), nonce.slice(), ct)
|
|
225
|
-
console.log(new TextDecoder().decode(pt)) // "Hello, world!"
|
|
226
|
-
|
|
227
|
-
pool.dispose()
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### Concurrent burst -- `Promise.all()` over many independent messages
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
import { init, XChaCha20Poly1305Pool, randomBytes } from 'leviathan-crypto'
|
|
234
|
-
|
|
235
|
-
await init(['chacha20'])
|
|
236
|
-
const pool = await XChaCha20Poly1305Pool.create()
|
|
237
|
-
|
|
238
|
-
const messages = ['message-1', 'message-2', 'message-3', 'message-4']
|
|
239
|
-
const key = randomBytes(32)
|
|
240
|
-
|
|
241
|
-
// Each message gets its own nonce -- all encrypt concurrently
|
|
242
|
-
const encrypted = await Promise.all(
|
|
243
|
-
messages.map(msg => {
|
|
244
|
-
const nonce = randomBytes(24)
|
|
245
|
-
const pt = new TextEncoder().encode(msg)
|
|
246
|
-
return pool.encrypt(key.slice(), nonce, pt)
|
|
247
|
-
})
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
pool.dispose()
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
### Manual worker count
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
const pool = await XChaCha20Poly1305Pool.create({ workers: 4 })
|
|
257
|
-
console.log(pool.size) // 4
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
### Correct dispose pattern -- `try/finally`
|
|
261
|
-
|
|
262
|
-
```typescript
|
|
263
|
-
const pool = await XChaCha20Poly1305Pool.create()
|
|
264
|
-
try {
|
|
265
|
-
const ct = await pool.encrypt(key, nonce, plaintext)
|
|
266
|
-
// ... use ct ...
|
|
267
|
-
} finally {
|
|
268
|
-
pool.dispose()
|
|
269
|
-
}
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### What NOT to do -- splitting one message across pool calls
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
// WRONG -- this is NOT secure
|
|
276
|
-
const chunk1 = await pool.encrypt(key, nonce1, largeFile.subarray(0, 65536))
|
|
277
|
-
const chunk2 = await pool.encrypt(key, nonce2, largeFile.subarray(65536))
|
|
278
|
-
const result = concat(chunk1, chunk2)
|
|
279
|
-
// ^ An attacker can reorder, duplicate, or truncate chunks undetected.
|
|
280
|
-
// There is no stream-level authentication.
|
|
281
|
-
// Use a future chunked-AEAD streaming API for large files.
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
---
|
|
285
|
-
|
|
286
|
-
## Error Conditions
|
|
287
|
-
|
|
288
|
-
| Condition | What happens |
|
|
289
|
-
|-----------|-------------|
|
|
290
|
-
| `init()` not called | `create()` throws: `leviathan-crypto: call init(['chacha20']) before using XChaCha20Poly1305Pool` |
|
|
291
|
-
| `new XChaCha20Poly1305Pool()` | Compile-time error -- the constructor is private |
|
|
292
|
-
| Wrong key length | `encrypt()`/`decrypt()` reject with `RangeError` |
|
|
293
|
-
| Wrong nonce length | `encrypt()`/`decrypt()` reject with `RangeError` |
|
|
294
|
-
| Ciphertext shorter than 16 bytes | `decrypt()` rejects with `RangeError` |
|
|
295
|
-
| Authentication failure | `decrypt()` rejects with `Error('ChaCha20Poly1305: authentication failed')` |
|
|
296
|
-
| Pool disposed | `encrypt()`/`decrypt()` reject with `Error('leviathan-crypto: pool is disposed')` |
|
|
297
|
-
| Worker init failure | `create()` rejects with error message from the worker |
|
|
298
|
-
|
|
299
|
-
---
|
|
300
|
-
|
|
301
|
-
> ## Cross-References
|
|
302
|
-
>
|
|
303
|
-
> - [index](./README.md) — Project Documentation index
|
|
304
|
-
> - [chacha20](./chacha20.md) — single-instance XChaCha20-Poly1305 API
|
|
305
|
-
> - [asm_chacha](./asm_chacha.md) — WASM implementation details (quarter-round, Poly1305 accumulator, HChaCha20)
|
|
306
|
-
> - [wasm](./wasm.md) — WebAssembly primer: how one compiled module spawns many worker instances
|
|
307
|
-
> - [fortuna](./fortuna.md) — another class using the `static async create()` factory pattern
|
|
308
|
-
> - [architecture](./architecture.md) — architecture overview, module relationships, buffer layouts, and build pipeline
|
|
309
|
-
> - [chacha_audit.md](./chacha_audit.md) — XChaCha20-Poly1305 implementation audit
|