leviathan-crypto 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CLAUDE.md +171 -7
  2. package/LICENSE +4 -0
  3. package/README.md +109 -54
  4. package/SECURITY.md +125 -233
  5. package/dist/chacha20/cipher-suite.d.ts +10 -0
  6. package/dist/chacha20/cipher-suite.js +66 -2
  7. package/dist/chacha20/generator.d.ts +12 -0
  8. package/dist/chacha20/generator.js +91 -0
  9. package/dist/chacha20/index.d.ts +97 -1
  10. package/dist/chacha20/index.js +139 -11
  11. package/dist/chacha20/ops.d.ts +57 -6
  12. package/dist/chacha20/ops.js +93 -13
  13. package/dist/chacha20/pool-worker.js +12 -0
  14. package/dist/chacha20/types.d.ts +1 -32
  15. package/dist/ct-wasm.js +1 -1
  16. package/dist/ct.wasm +0 -0
  17. package/dist/docs/aead.md +69 -26
  18. package/dist/docs/architecture.md +600 -520
  19. package/dist/docs/argon2id.md +17 -14
  20. package/dist/docs/chacha20.md +146 -39
  21. package/dist/docs/exports.md +46 -10
  22. package/dist/docs/fortuna.md +339 -122
  23. package/dist/docs/init.md +24 -25
  24. package/dist/docs/loader.md +142 -47
  25. package/dist/docs/serpent.md +139 -41
  26. package/dist/docs/sha2.md +77 -19
  27. package/dist/docs/sha3.md +81 -15
  28. package/dist/docs/types.md +156 -15
  29. package/dist/docs/utils.md +171 -81
  30. package/dist/embedded/chacha20-pool-worker.d.ts +1 -0
  31. package/dist/embedded/chacha20-pool-worker.js +5 -0
  32. package/dist/embedded/kyber.d.ts +1 -1
  33. package/dist/embedded/kyber.js +1 -1
  34. package/dist/embedded/serpent-pool-worker.d.ts +1 -0
  35. package/dist/embedded/serpent-pool-worker.js +5 -0
  36. package/dist/embedded/serpent.d.ts +1 -1
  37. package/dist/embedded/serpent.js +1 -1
  38. package/dist/fortuna.d.ts +14 -8
  39. package/dist/fortuna.js +144 -50
  40. package/dist/index.d.ts +8 -6
  41. package/dist/index.js +6 -5
  42. package/dist/init.d.ts +0 -2
  43. package/dist/init.js +83 -3
  44. package/dist/kyber/indcpa.js +4 -4
  45. package/dist/kyber/index.js +25 -5
  46. package/dist/kyber/kem.js +56 -1
  47. package/dist/kyber/suite.d.ts +1 -2
  48. package/dist/kyber/suite.js +1 -0
  49. package/dist/kyber/types.d.ts +1 -0
  50. package/dist/kyber/validate.d.ts +8 -4
  51. package/dist/kyber/validate.js +18 -14
  52. package/dist/kyber.wasm +0 -0
  53. package/dist/loader.d.ts +7 -2
  54. package/dist/loader.js +25 -28
  55. package/dist/ratchet/index.d.ts +6 -0
  56. package/dist/ratchet/index.js +37 -0
  57. package/dist/ratchet/kdf-chain.d.ts +13 -0
  58. package/dist/ratchet/kdf-chain.js +85 -0
  59. package/dist/ratchet/ratchet-keypair.d.ts +9 -0
  60. package/dist/ratchet/ratchet-keypair.js +61 -0
  61. package/dist/ratchet/root-kdf.d.ts +4 -0
  62. package/dist/ratchet/root-kdf.js +124 -0
  63. package/dist/ratchet/skipped-key-store.d.ts +14 -0
  64. package/dist/ratchet/skipped-key-store.js +154 -0
  65. package/dist/ratchet/types.d.ts +36 -0
  66. package/dist/ratchet/types.js +26 -0
  67. package/dist/serpent/cipher-suite.d.ts +10 -0
  68. package/dist/serpent/cipher-suite.js +136 -50
  69. package/dist/serpent/generator.d.ts +12 -0
  70. package/dist/serpent/generator.js +97 -0
  71. package/dist/serpent/index.d.ts +61 -1
  72. package/dist/serpent/index.js +92 -7
  73. package/dist/serpent/pool-worker.js +25 -95
  74. package/dist/serpent/serpent-cbc.d.ts +14 -4
  75. package/dist/serpent/serpent-cbc.js +58 -34
  76. package/dist/serpent/shared-ops.d.ts +83 -0
  77. package/dist/serpent/shared-ops.js +213 -0
  78. package/dist/serpent/types.d.ts +1 -5
  79. package/dist/serpent.wasm +0 -0
  80. package/dist/sha2/hash.d.ts +2 -0
  81. package/dist/sha2/hash.js +53 -0
  82. package/dist/sha2/index.d.ts +1 -0
  83. package/dist/sha2/index.js +15 -1
  84. package/dist/sha3/hash.d.ts +2 -0
  85. package/dist/sha3/hash.js +53 -0
  86. package/dist/sha3/index.d.ts +17 -2
  87. package/dist/sha3/index.js +79 -7
  88. package/dist/stream/header.js +5 -5
  89. package/dist/stream/open-stream.js +36 -14
  90. package/dist/stream/seal-stream-pool.d.ts +1 -0
  91. package/dist/stream/seal-stream-pool.js +47 -8
  92. package/dist/stream/seal-stream.js +29 -11
  93. package/dist/stream/types.d.ts +1 -0
  94. package/dist/types.d.ts +21 -0
  95. package/dist/utils.d.ts +7 -8
  96. package/dist/utils.js +73 -40
  97. package/dist/wasm-source.d.ts +9 -8
  98. package/package.json +79 -64
@@ -8,6 +8,17 @@ import { aeadEncrypt, aeadDecrypt } from './ops.js';
8
8
  import { AuthenticationError } from '../errors.js';
9
9
  let x;
10
10
  let subkey;
11
+ /**
12
+ * Message handler for the XChaCha20 pool worker.
13
+ *
14
+ * Accepts three message types:
15
+ * - `'init'` — instantiate the chacha20 WASM module and store the derived subkey
16
+ * - `'wipe'` — zero subkey and WASM buffers, then post `{ type: 'wiped' }`
17
+ * - `{ op: 'seal' | 'open', ... }` — encrypt or decrypt one chunk
18
+ *
19
+ * Replies with `{ type: 'result', id, data }` on success or
20
+ * `{ type: 'error', id, message, isAuthError }` on failure.
21
+ */
11
22
  self.onmessage = async (e) => {
12
23
  const msg = e.data;
13
24
  if (msg.type === 'init') {
@@ -34,6 +45,7 @@ self.onmessage = async (e) => {
34
45
  if (x)
35
46
  x.wipeBuffers();
36
47
  x = undefined;
48
+ self.postMessage({ type: 'wiped' });
37
49
  return;
38
50
  }
39
51
  if (!x || !subkey) {
@@ -1,32 +1 @@
1
- /** WASM exports for the chacha module */
2
- export interface ChaChaExports {
3
- memory: WebAssembly.Memory;
4
- getModuleId(): number;
5
- getKeyOffset(): number;
6
- getChachaNonceOffset(): number;
7
- getChachaCtrOffset(): number;
8
- getChachaBlockOffset(): number;
9
- getChachaStateOffset(): number;
10
- getChunkPtOffset(): number;
11
- getChunkCtOffset(): number;
12
- getChunkSize(): number;
13
- getPolyKeyOffset(): number;
14
- getPolyMsgOffset(): number;
15
- getPolyTagOffset(): number;
16
- getPolyBufLenOffset(): number;
17
- getXChaChaNonceOffset(): number;
18
- getXChaChaSubkeyOffset(): number;
19
- chachaLoadKey(): void;
20
- chachaSetCounter(n: number): void;
21
- chachaResetCounter(): void;
22
- chachaEncryptChunk(n: number): number;
23
- chachaDecryptChunk(n: number): number;
24
- chachaEncryptChunk_simd(n: number): number;
25
- chachaDecryptChunk_simd(n: number): number;
26
- chachaGenPolyKey(): void;
27
- hchacha20(): void;
28
- polyInit(): void;
29
- polyUpdate(n: number): void;
30
- polyFinal(): void;
31
- wipeBuffers(): void;
32
- }
1
+ export {};
package/dist/ct-wasm.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // auto-generated — do not edit
2
2
  // raw WASM bytes for constant-time comparison module
3
- export const CT_WASM = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 8, 1, 96, 3, 127, 127, 127, 1, 127, 2, 16, 1, 3, 101, 110, 118, 6, 109, 101, 109, 111, 114, 121, 2, 1, 1, 1, 3, 2, 1, 0, 7, 20, 2, 7, 99, 111, 109, 112, 97, 114, 101, 0, 0, 6, 109, 101, 109, 111, 114, 121, 2, 0, 10, 111, 1, 109, 2, 3, 127, 1, 123, 3, 64, 32, 3, 65, 16, 106, 34, 4, 32, 2, 76, 4, 64, 32, 6, 32, 0, 32, 3, 106, 253, 0, 4, 0, 32, 1, 32, 3, 106, 253, 0, 4, 0, 253, 81, 253, 80, 33, 6, 32, 4, 33, 3, 12, 1, 11, 11, 3, 64, 32, 2, 32, 3, 74, 4, 64, 32, 5, 32, 0, 32, 3, 106, 45, 0, 0, 32, 1, 32, 3, 106, 45, 0, 0, 115, 114, 33, 5, 32, 3, 65, 1, 106, 33, 3, 12, 1, 11, 11, 32, 6, 253, 83, 4, 64, 65, 0, 15, 11, 32, 5, 69, 11]);
3
+ export const CT_WASM = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 8, 1, 96, 3, 127, 127, 127, 1, 127, 3, 2, 1, 0, 5, 4, 1, 1, 1, 1, 7, 20, 2, 7, 99, 111, 109, 112, 97, 114, 101, 0, 0, 6, 109, 101, 109, 111, 114, 121, 2, 0, 10, 133, 1, 1, 130, 1, 3, 2, 127, 1, 126, 1, 123, 3, 64, 32, 3, 65, 16, 106, 34, 4, 32, 2, 76, 4, 64, 32, 6, 32, 0, 32, 3, 106, 253, 0, 4, 0, 32, 1, 32, 3, 106, 253, 0, 4, 0, 253, 81, 253, 80, 33, 6, 32, 4, 33, 3, 12, 1, 11, 11, 3, 64, 32, 2, 32, 3, 74, 4, 64, 32, 5, 32, 0, 32, 3, 106, 49, 0, 0, 32, 1, 32, 3, 106, 49, 0, 0, 133, 132, 33, 5, 32, 3, 65, 1, 106, 33, 3, 12, 1, 11, 11, 66, 0, 32, 5, 32, 6, 253, 29, 0, 32, 6, 253, 29, 1, 132, 132, 34, 5, 125, 32, 5, 132, 66, 63, 135, 66, 127, 133, 167, 65, 1, 113, 11]);
package/dist/ct.wasm CHANGED
Binary file
package/dist/docs/aead.md CHANGED
@@ -1,7 +1,8 @@
1
- # Authenticated Encryption
1
+ <img src="https://github.com/xero/leviathan-crypto/raw/main/docs/logo.svg" alt="logo" width="120" align="left" margin="10">
2
2
 
3
- > [!NOTE]
4
- > Cipher-agnostic authenticated encryption for any scale. One-shot with `Seal`, chunked with `SealStream` and `OpenStream`, or parallel with `SealStreamPool`. All four share a wire format and accept any `CipherSuite`.
3
+ ### Authenticated Encryption
4
+
5
+ Cipher-agnostic authenticated encryption for any scale. One-shot with `Seal`, chunked with `SealStream` and `OpenStream`, or parallel with `SealStreamPool`. All four share a wire format and accept any `CipherSuite`.
5
6
 
6
7
  > ### Table of Contents
7
8
  > - [Overview](#overview)
@@ -13,11 +14,11 @@
13
14
 
14
15
  ## Overview
15
16
 
16
- `Seal`, `SealStream`, `OpenStream`, and `SealStreamPool` are the primary API for authenticated encryption in leviathan-crypto. They are cipher-agnostic: you pass a `CipherSuite` object at construction and the implementation handles key derivation, nonce management, and authentication for you.
17
+ Authenticated encryption in leviathan-crypto centers on four classes: `Seal`, `SealStream`, `OpenStream`, and `SealStreamPool`. All are cipher-agnostic. Pass a `CipherSuite` object at construction and they handle key derivation, nonce management, and authentication automatically.
17
18
 
18
- The four classes form a natural progression. `Seal` handles data that fits in memory. `SealStream` and `OpenStream` handle data that arrives in chunks or is too large to buffer. `SealStreamPool` parallelizes the chunked approach across Web Workers. All four produce and consume the same wire format, so a `Seal` blob can be opened by `OpenStream` and vice versa.
19
+ These four form a natural progression by use case. Use `Seal` for data that fits in memory. Use `SealStream` and `OpenStream` for data arriving in chunks or too large to buffer. Use `SealStreamPool` for parallel chunked encryption across Web Workers. All four share the same wire format, so `OpenStream` can decrypt a `Seal` blob and vice versa.
19
20
 
20
- Two cipher suites are included. A third wraps either with ML-KEM for post-quantum hybrid encryption.
21
+ leviathan-crypto includes two cipher suites. A third suite wraps either with ML-KEM for post-quantum hybrid encryption.
21
22
 
22
23
  | Suite | Cipher | Tag | Modules |
23
24
  |---|---|---|---|
@@ -33,16 +34,28 @@ See [ciphersuite.md](./ciphersuite.md) for full cipher suite documentation.
33
34
 
34
35
  The STREAM construction is based on [Hoang, Reyhanitabar, Rogaway, and Vizár (CRYPTO 2015)](https://eprint.iacr.org/2015/189.pdf). It provides online authenticated encryption with four guarantees.
35
36
 
36
- **Per-chunk authentication.** Each chunk is individually authenticated. A tampered chunk is rejected immediately without decrypting anything that follows.
37
+ **Per-chunk authentication.** Each chunk carries its own authentication tag. The stream rejects a tampered chunk immediately and stops decrypting.
37
38
 
38
39
  **Counter binding.** Each chunk's nonce includes a monotonic counter. Reordering or duplicating chunks produces a counter mismatch and authentication fails.
39
40
 
40
- **Final-chunk detection.** The last chunk uses a distinct nonce flag (`TAG_FINAL` vs `TAG_DATA`). Truncating the stream by dropping the final chunk is detected because the opener expects a chunk marked final.
41
+ **Final-chunk detection.** The last chunk uses a distinct nonce flag (`TAG_FINAL` vs `TAG_DATA`). The opener expects a chunk marked final and rejects any stream that ends without one.
41
42
 
42
43
  **Stream isolation.** Each stream generates a fresh 16-byte random nonce on construction. Two streams with the same key derive independent subkeys via HKDF and cannot interfere with each other.
43
44
 
44
45
  > [!IMPORTANT]
45
46
  > `SealStream` is single-use. After `finalize()` is called the derived keys are wiped and no further chunks can be sealed. Create a new `SealStream` for each message. `SealStreamPool.seal()` enforces this with a guard that throws on subsequent calls.
47
+ >
48
+ > **`SealStream` / `OpenStream` have a three-state machine: `ready` → `finalized` | `failed`.** An auth failure, WASM error, or cipher exception inside `push()`, `pull()`, or `finalize()` wipes the derived keys and transitions the stream to `failed`. Subsequent method calls (`push`, `pull`, `finalize`, and `OpenStream.seek`) throw with `'failed'` in the message, never `'finalized'`. `dispose()` on a `failed` stream is a no-op. Construct a new stream to continue.
49
+ >
50
+ > **Argument-validation errors are non-terminal on both `SealStream` and `OpenStream`.** A `RangeError` from `push()` or `finalize()` for a chunk larger than `chunkSize` throws without wiping keys or entering `'failed'`. Symmetrically, a `RangeError` from `pull()` or `finalize()` throws without wiping keys when a chunk is too short to contain a tag, exceeds the maximum wire size, or (in framed mode) has a length prefix that does not match the payload length. The stream stays in `'ready'` and the caller can retry with a corrected chunk.
51
+ >
52
+ > This is safe because every validation error depends only on attacker-observable input lengths and never on secret-derived state. Distinguishing a validation throw from an auth failure gives an attacker no information they did not already have. Auth failures from `cipher.openChunk` remain terminal, as they are the crypto-path case.
53
+ >
54
+ > **`OpenStream.seek(index)` validates `index` before mutating state.** Indices that are not non-negative safe integers — `NaN`, `Infinity`, fractional, negative, or `> Number.MAX_SAFE_INTEGER` — throw `RangeError` without changing `counter`, so the caller can retry with a corrected index. The check uses `Number.isSafeInteger(index) && index >= 0` so values above `2^53 - 1` (where IEEE 754 doubles have integer gaps) are rejected directly rather than relying on a separate magnitude comparison. Backward seeks (`index < counter`) throw `'forward-only'` for the same reason (plaintext replay prevention). See `seek()` in the OpenStream API table.
55
+ >
56
+ > **AEAD `encrypt()` is strict single-use.** `ChaCha20Poly1305.encrypt()` and `XChaCha20Poly1305.encrypt()` are terminal on any throw, including key and nonce length validation. A retry on the same instance always raises the single-use guard, never a fresh length error. This tightens the 2.0-beta semantics where length validation was recoverable. Always allocate a new AEAD per message.
57
+ >
58
+ > **`SealStreamPool.seal()` is terminal on any throw.** Auth failures, worker crashes, job timeouts, output-size overflows (`RangeError` from assembling ciphertext that exceeds the runtime's typed-array max), or any other rejection kill the pool. Pending jobs reject, workers terminate, `_masterKey` and `_keys` are wiped, and subsequent calls throw `"pool is dead"`. Construct a new pool to continue. Any throw is terminal, which keeps the failure contract uniform with the strict single-use posture of `ChaCha20Poly1305.encrypt()`.
46
59
 
47
60
  ### WASM Side-Channel Posture
48
61
 
@@ -119,7 +132,10 @@ const pt = Seal.decrypt(XChaCha20Cipher, key, blob) // throws AuthenticationE
119
132
  | `Seal.encrypt(suite, key, plaintext, opts?)` | `Uint8Array` | One-shot encrypt. Returns `preamble \|\| chunk`. |
120
133
  | `Seal.decrypt(suite, key, blob, opts?)` | `Uint8Array` | One-shot decrypt. Throws `AuthenticationError` on tamper. |
121
134
 
122
- **`opts.aad`** optional `Uint8Array`. Additional Authenticated Data: authenticated but not encrypted. Pass the same value to both `encrypt` and `decrypt`.
135
+ **`opts.aad`.** Optional `Uint8Array` carrying Additional Authenticated Data. Authenticated but not encrypted. Pass the same value to both `encrypt` and `decrypt`.
136
+
137
+ > [!NOTE]
138
+ > **`chunkSize` in the wire header is a maximum, not an actual size.** For `Seal.encrypt` (single-chunk), the header always declares `max(plaintext.length, CHUNK_MIN)`, so a zero-byte seal still declares `chunkSize = CHUNK_MIN = 1024`. This is self-consistent on decode (the single final chunk is processed regardless of its actual length up to the declared bound) and prevents leaking the exact plaintext length through header analysis when `plaintext.length < CHUNK_MIN`. `SealStream` writes the configured `opts.chunkSize` verbatim; the receiver treats it as an upper bound on any incoming chunk's plaintext size.
123
139
 
124
140
  ---
125
141
 
@@ -181,7 +197,7 @@ const ptLast = opener.finalize(ctLast) // keys wiped
181
197
 
182
198
  **Constructor:** `new OpenStream(cipher, key, preamble)`
183
199
 
184
- Throws if the preamble format enum doesn't match the cipher, or if the preamble is too short.
200
+ Throws if the preamble format enum doesn't match the cipher or if the preamble is too short.
185
201
 
186
202
  | Parameter | Type | Description |
187
203
  |---|---|---|
@@ -193,9 +209,12 @@ Throws if the preamble format enum doesn't match the cipher, or if the preamble
193
209
  |---|---|---|
194
210
  | `pull(chunk, { aad? })` | `Uint8Array` | Decrypt a data chunk. Throws `AuthenticationError` on tamper. |
195
211
  | `finalize(chunk, { aad? })` | `Uint8Array` | Decrypt the final chunk and wipe keys. |
196
- | `seek(index)` | `void` | Set the counter to `index`. Enables random access decryption. Must be a non-negative integer. |
212
+ | `seek(index)` | `void` | Set the counter to `index`. The stream is forward-only; `index < counter` throws `RangeError` with `'forward-only'` in the message. `index` must satisfy `Number.isSafeInteger(index) && index >= 0` (i.e. a non-negative safe integer ≤ `Number.MAX_SAFE_INTEGER`). Argument-validation throws do not mutate `counter`; the stream stays usable and can retry with a corrected index. Throws on failed/finalized state (state guard fires before range check). |
197
213
  | `toTransformStream()` | `TransformStream` | Web Streams API wrapper. Buffers one chunk to detect the final chunk. |
198
214
 
215
+ > [!IMPORTANT]
216
+ > **`OpenStream.seek` is forward-only.** Backward seeks (`index < this.counter`) throw a `RangeError` with `'forward-only'` in the message. A backward seek would reuse an already-consumed per-chunk counter nonce against a new ciphertext, permitting plaintext replay against a stale opener. Construct a fresh `OpenStream` from the same preamble to restart from the beginning.
217
+
199
218
  ---
200
219
 
201
220
  ### SealStreamPool
@@ -221,7 +240,7 @@ const decrypted = await pool.open(ciphertext)
221
240
  pool.destroy()
222
241
  ```
223
242
 
224
- **`SealStreamPool.create(cipher, key, opts)`** async factory.
243
+ **`SealStreamPool.create(cipher, key, opts)`.** Async factory.
225
244
 
226
245
  | Option | Type | Default | Description |
227
246
  |---|---|---|---|
@@ -231,7 +250,10 @@ pool.destroy()
231
250
  | `framed` | `boolean` | `false` | Framed mode. |
232
251
  | `jobTimeout` | `number` | `30000` | Per-job timeout in ms. |
233
252
 
234
- **Failure model.** Any error is fatal. Authentication failure, worker crash, and timeout all terminate every worker, wipe all keys, and mark the pool permanently dead. Pending promises reject. There is no retry and no worker replacement. Create a new pool for the next operation.
253
+ > [!NOTE]
254
+ > For padded ciphers (`SerpentCipher`), `create()` validates at startup that a full plaintext chunk fits in the WASM buffer after PKCS7 padding. If `chunkSize` is too large it throws a `RangeError` with the actual values before any workers are launched. The default `chunkSize: 65536` is valid for both built-in cipher suites.
255
+
256
+ **Failure model.** Any error is fatal. Authentication failure, worker crash, and timeout all terminate every worker, wipe all keys, and mark the pool permanently dead. Pending promises reject. There is no retry and no worker replacement. Create a new pool for the next operation. `destroy()` is synchronous from the caller's perspective. The pool flips to `dead`, pending jobs reject, and main-thread keys are zeroed before the call returns. Worker teardown is bounded-async. The pool requests that each worker zero its in-memory key material and terminates workers after a short ACK window.
235
257
 
236
258
  | Method / Property | Description |
237
259
  |---|---|
@@ -242,6 +264,24 @@ pool.destroy()
242
264
  | `dead` | `true` after any fatal error or `destroy()`. |
243
265
  | `size` | Number of workers. |
244
266
 
267
+ **Lifecycle.**
268
+
269
+ - After `seal()` completes successfully, the pool holds the derived keys and
270
+ master key in memory until you call `destroy()`. Call `destroy()` explicitly
271
+ when you are finished; forgetting leaves key material resident until garbage
272
+ collection.
273
+ - After `seal()`, the pool is marked sealed and further `seal()` calls throw.
274
+ But `open()` is still valid and can decrypt other ciphertexts using the same
275
+ master key. This is intentional because a pool is a stateful encrypt/decrypt
276
+ context tied to a master key, not a single-use seal operation. The word
277
+ "sealed" can still mislead. If your usage is encrypt-once-then-discard, the
278
+ idiom is `try { await pool.seal(pt) } finally { pool.destroy() }`.
279
+ - On any job throw (worker crash, auth failure, timeout), the pool's
280
+ `_killAll` runs. All workers terminate, all keys are wiped, and the pool is
281
+ marked dead. Subsequent calls throw `'pool is dead'`.
282
+
283
+ **Interop with `SealStream.push()`.** In unframed mode, `pool.open()` splits the body into chunks at fixed `chunkSize` boundaries. This works when the ciphertext came from `SealStreamPool.seal()` or from a `SealStream` that emitted every non-final chunk at exactly `chunkSize` plaintext bytes. A `SealStream` that called `push()` with sub-`chunkSize` chunks produces a valid blob that `OpenStream` can decrypt, but `pool.open()` cannot. The pool splits at the wrong boundary, stamps the wrong domain separator on the final chunk, and fails authentication. Use `framed: true` on both sides if producer and consumer may have different chunk shapes. Framed chunks carry a `u32be` length prefix that makes the split unambiguous.
284
+
245
285
  ---
246
286
 
247
287
  ### KyberSuite
@@ -288,7 +328,7 @@ AAD applies per chunk, not per stream. Each chunk can carry different AAD. If yo
288
328
 
289
329
  ### AuthenticationError
290
330
 
291
- `AuthenticationError` is thrown by `Seal.decrypt()`, `OpenStream.pull()`, `OpenStream.finalize()`, and `SealStreamPool.open()` when authentication fails. It extends `Error` and carries the cipher name in the message.
331
+ `Seal.decrypt()`, `OpenStream.pull()`, `OpenStream.finalize()`, and `SealStreamPool.open()` throw `AuthenticationError` when authentication fails. It extends `Error` and carries the cipher name in the message.
292
332
 
293
333
  ```typescript
294
334
  import { AuthenticationError } from 'leviathan-crypto'
@@ -306,15 +346,18 @@ Never attempt to recover plaintext after an `AuthenticationError`. The stream la
306
346
 
307
347
  ---
308
348
 
309
- > ## Cross-References
310
- >
311
- > - [index](./README.md) — Project Documentation index
312
- > - [lexicon](./lexicon.md) Glossary of cryptographic terms
313
- > - [architecture](./architecture.md) architecture overview, module relationships, buffer layouts, and build pipeline
314
- > - [ciphersuite](./ciphersuite.md) `SerpentCipher`, `XChaCha20Cipher`, `KyberSuite`, and the `CipherSuite` interface
315
- > - [kyber](./kyber.md) ML-KEM key encapsulation, parameter sets, and key management
316
- > - [serpent](./serpent.md) Serpent-256 raw primitives
317
- > - [chacha20](./chacha20.md) ChaCha20 raw primitives
318
- > - [stream_audit](./stream_audit.md) streaming AEAD composition audit
319
- > - [exports](./exports.md) complete export reference
320
- > - [init](./init.md) WASM loading and `WasmSource`
349
+
350
+ ## Cross-References
351
+
352
+ | Document | Description |
353
+ | -------- | ----------- |
354
+ | [index](./README.md) | Project Documentation index |
355
+ | [lexicon](./lexicon.md) | Glossary of cryptographic terms |
356
+ | [architecture](./architecture.md) | architecture overview, module relationships, buffer layouts, and build pipeline |
357
+ | [ciphersuite](./ciphersuite.md) | `SerpentCipher`, `XChaCha20Cipher`, `KyberSuite`, and the `CipherSuite` interface |
358
+ | [kyber](./kyber.md) | ML-KEM key encapsulation, parameter sets, and key management |
359
+ | [serpent](./serpent.md) | Serpent-256 raw primitives |
360
+ | [chacha20](./chacha20.md) | ChaCha20 raw primitives |
361
+ | [stream_audit](./stream_audit.md) | streaming AEAD composition audit |
362
+ | [exports](./exports.md) | complete export reference |
363
+ | [init](./init.md) | WASM loading and `WasmSource` |