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.
- package/CLAUDE.md +171 -7
- package/LICENSE +4 -0
- package/README.md +109 -54
- package/SECURITY.md +125 -233
- package/dist/chacha20/cipher-suite.d.ts +10 -0
- package/dist/chacha20/cipher-suite.js +66 -2
- package/dist/chacha20/generator.d.ts +12 -0
- package/dist/chacha20/generator.js +91 -0
- package/dist/chacha20/index.d.ts +97 -1
- package/dist/chacha20/index.js +139 -11
- package/dist/chacha20/ops.d.ts +57 -6
- package/dist/chacha20/ops.js +93 -13
- package/dist/chacha20/pool-worker.js +12 -0
- package/dist/chacha20/types.d.ts +1 -32
- package/dist/ct-wasm.js +1 -1
- package/dist/ct.wasm +0 -0
- package/dist/docs/aead.md +69 -26
- package/dist/docs/architecture.md +600 -520
- package/dist/docs/argon2id.md +17 -14
- package/dist/docs/chacha20.md +146 -39
- package/dist/docs/exports.md +46 -10
- package/dist/docs/fortuna.md +339 -122
- package/dist/docs/init.md +24 -25
- package/dist/docs/loader.md +142 -47
- package/dist/docs/serpent.md +139 -41
- package/dist/docs/sha2.md +77 -19
- package/dist/docs/sha3.md +81 -15
- package/dist/docs/types.md +156 -15
- package/dist/docs/utils.md +171 -81
- package/dist/embedded/chacha20-pool-worker.d.ts +1 -0
- package/dist/embedded/chacha20-pool-worker.js +5 -0
- package/dist/embedded/kyber.d.ts +1 -1
- package/dist/embedded/kyber.js +1 -1
- package/dist/embedded/serpent-pool-worker.d.ts +1 -0
- package/dist/embedded/serpent-pool-worker.js +5 -0
- package/dist/embedded/serpent.d.ts +1 -1
- package/dist/embedded/serpent.js +1 -1
- package/dist/fortuna.d.ts +14 -8
- package/dist/fortuna.js +144 -50
- package/dist/index.d.ts +8 -6
- package/dist/index.js +6 -5
- package/dist/init.d.ts +0 -2
- package/dist/init.js +83 -3
- package/dist/kyber/indcpa.js +4 -4
- package/dist/kyber/index.js +25 -5
- package/dist/kyber/kem.js +56 -1
- package/dist/kyber/suite.d.ts +1 -2
- package/dist/kyber/suite.js +1 -0
- package/dist/kyber/types.d.ts +1 -0
- package/dist/kyber/validate.d.ts +8 -4
- package/dist/kyber/validate.js +18 -14
- package/dist/kyber.wasm +0 -0
- package/dist/loader.d.ts +7 -2
- package/dist/loader.js +25 -28
- package/dist/ratchet/index.d.ts +6 -0
- package/dist/ratchet/index.js +37 -0
- package/dist/ratchet/kdf-chain.d.ts +13 -0
- package/dist/ratchet/kdf-chain.js +85 -0
- package/dist/ratchet/ratchet-keypair.d.ts +9 -0
- package/dist/ratchet/ratchet-keypair.js +61 -0
- package/dist/ratchet/root-kdf.d.ts +4 -0
- package/dist/ratchet/root-kdf.js +124 -0
- package/dist/ratchet/skipped-key-store.d.ts +14 -0
- package/dist/ratchet/skipped-key-store.js +154 -0
- package/dist/ratchet/types.d.ts +36 -0
- package/dist/ratchet/types.js +26 -0
- package/dist/serpent/cipher-suite.d.ts +10 -0
- package/dist/serpent/cipher-suite.js +136 -50
- package/dist/serpent/generator.d.ts +12 -0
- package/dist/serpent/generator.js +97 -0
- package/dist/serpent/index.d.ts +61 -1
- package/dist/serpent/index.js +92 -7
- package/dist/serpent/pool-worker.js +25 -95
- package/dist/serpent/serpent-cbc.d.ts +14 -4
- package/dist/serpent/serpent-cbc.js +58 -34
- package/dist/serpent/shared-ops.d.ts +83 -0
- package/dist/serpent/shared-ops.js +213 -0
- package/dist/serpent/types.d.ts +1 -5
- package/dist/serpent.wasm +0 -0
- package/dist/sha2/hash.d.ts +2 -0
- package/dist/sha2/hash.js +53 -0
- package/dist/sha2/index.d.ts +1 -0
- package/dist/sha2/index.js +15 -1
- package/dist/sha3/hash.d.ts +2 -0
- package/dist/sha3/hash.js +53 -0
- package/dist/sha3/index.d.ts +17 -2
- package/dist/sha3/index.js +79 -7
- package/dist/stream/header.js +5 -5
- package/dist/stream/open-stream.js +36 -14
- package/dist/stream/seal-stream-pool.d.ts +1 -0
- package/dist/stream/seal-stream-pool.js +47 -8
- package/dist/stream/seal-stream.js +29 -11
- package/dist/stream/types.d.ts +1 -0
- package/dist/types.d.ts +21 -0
- package/dist/utils.d.ts +7 -8
- package/dist/utils.js +73 -40
- package/dist/wasm-source.d.ts +9 -8
- 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) {
|
package/dist/chacha20/types.d.ts
CHANGED
|
@@ -1,32 +1 @@
|
|
|
1
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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`).
|
|
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
|
|
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
|
|
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`.
|
|
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)
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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` |
|