leviathan-crypto 1.3.0 → 1.4.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 (50) hide show
  1. package/CLAUDE.md +22 -4
  2. package/README.md +25 -23
  3. package/SECURITY.md +21 -9
  4. package/dist/chacha20/index.d.ts +20 -0
  5. package/dist/chacha20/index.js +48 -2
  6. package/dist/chacha20/pool.js +3 -13
  7. package/dist/chacha20/stream-sealer.d.ts +49 -0
  8. package/dist/chacha20/stream-sealer.js +327 -0
  9. package/dist/{chacha.wasm → chacha20.wasm} +0 -0
  10. package/dist/docs/architecture.md +25 -25
  11. package/dist/docs/argon2id.md +6 -6
  12. package/dist/docs/chacha20.md +206 -60
  13. package/dist/docs/chacha20_pool.md +9 -6
  14. package/dist/docs/fortuna.md +13 -13
  15. package/dist/docs/init.md +4 -4
  16. package/dist/docs/loader.md +28 -18
  17. package/dist/docs/serpent.md +38 -18
  18. package/dist/docs/sha2.md +13 -8
  19. package/dist/docs/sha3.md +8 -5
  20. package/dist/docs/types.md +8 -8
  21. package/dist/docs/utils.md +16 -20
  22. package/dist/docs/wasm.md +8 -7
  23. package/dist/embedded/chacha20.d.ts +1 -0
  24. package/dist/embedded/chacha20.js +3 -0
  25. package/dist/embedded/serpent.d.ts +1 -1
  26. package/dist/embedded/serpent.js +2 -1
  27. package/dist/embedded/sha2.d.ts +1 -1
  28. package/dist/embedded/sha2.js +2 -1
  29. package/dist/embedded/sha3.d.ts +1 -1
  30. package/dist/embedded/sha3.js +2 -1
  31. package/dist/fortuna.d.ts +2 -2
  32. package/dist/fortuna.js +6 -4
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/init.js +1 -1
  36. package/dist/loader.d.ts +6 -0
  37. package/dist/loader.js +31 -9
  38. package/dist/serpent/index.js +1 -1
  39. package/dist/serpent/seal.d.ts +2 -2
  40. package/dist/serpent/seal.js +7 -5
  41. package/dist/serpent/stream-pool.js +5 -15
  42. package/dist/serpent/stream-sealer.d.ts +5 -0
  43. package/dist/serpent/stream-sealer.js +25 -24
  44. package/dist/sha2/index.js +1 -1
  45. package/dist/sha3/index.js +1 -1
  46. package/dist/utils.d.ts +5 -5
  47. package/dist/utils.js +26 -16
  48. package/package.json +1 -1
  49. package/dist/embedded/chacha.d.ts +0 -1
  50. package/dist/embedded/chacha.js +0 -2
package/CLAUDE.md CHANGED
@@ -88,7 +88,7 @@ await serpentInit()
88
88
  | Classes | `init()` call |
89
89
  |---------|--------------|
90
90
  | `SerpentSeal`, `SerpentStream`, `SerpentStreamPool`, `SerpentStreamSealer`, `SerpentStreamOpener`, `Serpent`, `SerpentCtr`, `SerpentCbc` | `init(['serpent', 'sha2'])` |
91
- | `ChaCha20`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305`, `XChaCha20Poly1305Pool` | `init(['chacha20'])` |
91
+ | `ChaCha20`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305`, `XChaCha20Seal`, `XChaCha20Poly1305Pool` | `init(['chacha20'])` |
92
92
  | `SHA256`, `SHA384`, `SHA512`, `HMAC_SHA256`, `HMAC_SHA384`, `HMAC_SHA512`, `HKDF_SHA256`, `HKDF_SHA512` | `init(['sha2'])` |
93
93
  | `SHA3_224`, `SHA3_256`, `SHA3_384`, `SHA3_512`, `SHAKE128`, `SHAKE256` | `init(['sha3'])` |
94
94
  | `Fortuna` | `init(['serpent', 'sha2'])` |
@@ -106,8 +106,9 @@ await init(['serpent', 'sha2'])
106
106
 
107
107
  const key = randomBytes(64) // 64-byte key (encKey + macKey)
108
108
  const seal = new SerpentSeal()
109
- const ciphertext = seal.encrypt(key, plaintext) // Serpent-CBC + HMAC-SHA256
110
- const decrypted = seal.decrypt(key, ciphertext) // throws on tamper
109
+ const ciphertext = seal.encrypt(key, plaintext) // Serpent-CBC + HMAC-SHA256
110
+ const decrypted = seal.decrypt(key, ciphertext) // throws on tamper
111
+ // Optional AAD: seal.encrypt(key, plaintext, aad) / seal.decrypt(key, ciphertext, aad)
111
112
  seal.dispose()
112
113
  ```
113
114
 
@@ -154,6 +155,23 @@ const opener = new SerpentStreamOpener(key, header, { framed: true })
154
155
  const chunks = opener.feed(frame0) // Uint8Array[] — throws on auth failure
155
156
  ```
156
157
 
158
+ ### XChaCha20Seal (recommended)
159
+
160
+ ```typescript
161
+ import { init, XChaCha20Seal, randomBytes } from 'leviathan-crypto'
162
+
163
+ await init(['chacha20'])
164
+
165
+ const seal = new XChaCha20Seal(randomBytes(32)) // 32-byte key
166
+ const ct = seal.encrypt(plaintext) // nonce(24) || ct || tag(16)
167
+ const pt = seal.decrypt(ct) // throws on tamper
168
+ seal.dispose()
169
+ ```
170
+
171
+ Binds key at construction, generates a fresh nonce per `encrypt()` call. No nonce
172
+ management needed. For protocol interop requiring explicit nonces, use
173
+ `XChaCha20Poly1305` directly.
174
+
157
175
  ### XChaCha20-Poly1305
158
176
 
159
177
  ```typescript
@@ -257,7 +275,7 @@ The complete API reference ships in `docs/` alongside this file:
257
275
  | File | Contents |
258
276
  |------|----------|
259
277
  | `docs/serpent.md` | `SerpentSeal`, `SerpentStream`, `SerpentStreamPool`, `SerpentStreamSealer`, `SerpentStreamOpener`, `Serpent`, `SerpentCtr`, `SerpentCbc` |
260
- | `docs/chacha20.md` | `ChaCha20`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305`, `XChaCha20Poly1305Pool` |
278
+ | `docs/chacha20.md` | `ChaCha20`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305`, `XChaCha20Seal`, `XChaCha20Poly1305Pool` |
261
279
  | `docs/sha2.md` | `SHA256`, `SHA384`, `SHA512`, `HMAC_SHA256`, `HMAC_SHA384`, `HMAC_SHA512`, `HKDF_SHA256`, `HKDF_SHA512` |
262
280
  | `docs/sha3.md` | `SHA3_224`, `SHA3_256`, `SHA3_384`, `SHA3_512`, `SHAKE128`, `SHAKE256` |
263
281
  | `docs/fortuna.md` | `Fortuna` CSPRNG |
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- [![Version](https://img.shields.io/github/package-json/version/xero/leviathan-crypto?labelColor=33383e&logo=npm&&logoColor=979da4&color=6e2aa5)](https://github.com/xero/leviathan-crypto/releases/latest) [![GitHub repo size](https://img.shields.io/github/repo-size/xero/leviathan-crypto?labelColor=262a2e&logo=googlecontaineroptimizedos&logoColor=979da4&color=6e2aa5)](https://github.com/xero/leviathan-crypto/) [![test suite](https://github.com/xero/leviathan-crypto/actions/workflows/test-suite.yml/badge.svg)](https://github.com/xero/leviathan-crypto/actions/workflows/test-suite.yml) [![wiki](https://github.com/xero/leviathan-crypto/actions/workflows/wiki.yml/badge.svg)](https://github.com/xero/leviathan-crypto/wiki)
1
+ [![GitHub Release](https://img.shields.io/github/v/release/xero/leviathan-crypto?sort=semver&display_name=tag&style=flat&logo=github&logoColor=989da4&label=latest%20release&labelColor=161925&color=1c7293)](https://github.com/xero/leviathan-crypto/releases/latest) [![npm package minimized gzipped size](https://img.shields.io/bundlejs/size/leviathan-crypto?format=both&style=flat&logo=googlecontaineroptimizedos&logoColor=989da4&label=package%20size&labelColor=161925&color=1c7293)](https://www.npmjs.com/package/leviathan-crypto) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/xero/leviathan-crypto/test-suite.yml?branch=main&style=flat&logo=github&logoColor=989da4&label=test%20suite&labelColor=161925&color=1a936f)](https://github.com/xero/leviathan-crypto/actions/workflows/test-suite.yml) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/xero/leviathan-crypto/wiki.yml?branch=main&style=flat&logo=gitbook&logoColor=989da4&label=wiki%20publish&labelColor=161925&color=1a936f)](https://github.com/xero/leviathan-crypto/wiki)
2
2
 
3
- ![side-effect free](https://github.com/xero/leviathan-crypto/raw/main/docs/badge-side-effect-free.svg) ![tree-shakeable](https://github.com/xero/leviathan-crypto/raw/main/docs/badge-tree-shakable.svg) ![zero dependencies](https://github.com/xero/leviathan-crypto/raw/main/docs/badge-zero-dependancies.svg) [![MIT Licensed](https://github.com/xero/leviathan-crypto/raw/main/docs/badge-mit-license.svg)](https://github.com/xero/text0wnz/blob/main/LICENSE)
3
+ ![simd webassembly](https://img.shields.io/badge/SIMD%20-%20WASM?style=flat&logo=wasmer&logoColor=1a936f&label=WASM&labelColor=33383e&color=161925) ![side-effect free](https://github.com/xero/leviathan-crypto/raw/main/docs/badge-side-effect-free.svg) ![tree-shakeable](https://github.com/xero/leviathan-crypto/raw/main/docs/badge-tree-shakable.svg) ![zero dependencies](https://github.com/xero/leviathan-crypto/raw/main/docs/badge-zero-dependancies.svg) [![MIT Licensed](https://github.com/xero/leviathan-crypto/raw/main/docs/badge-mit-license.svg)](https://github.com/xero/leviathan-crypto/blob/main/LICENSE)
4
4
 
5
5
  <img src="https://github.com/xero/leviathan-crypto/raw/main/docs/logo.svg" alt="Leviathan logo" width="400" >
6
6
 
@@ -77,15 +77,15 @@ npm install leviathan-crypto
77
77
 
78
78
  > [!NOTE]
79
79
  > The Serpent and ChaCha20 modules require a runtime with WebAssembly SIMD
80
- > support. This has been a feature of all major browsers and runtimes since
81
- > 2021. All other primitives (SHA-2, SHA-3, Poly1305) run on any WASM-capable
82
- > runtime.
80
+ > support. [This has been a feature of all major browsers and runtimes since
81
+ > 2021](https://caniuse.com/wasm-simd). All other primitives (SHA-2, SHA-3,
82
+ > Poly1305) run on any WASM-capable runtime.
83
83
 
84
84
  ---
85
85
 
86
86
  ## Demos
87
87
 
88
- **`lvthn-web`** [ [demo](https://leviathan.3xi.club/web) · [source](https://github.com/xero/leviathan-demos/tree/main/lvthn-web) · [readme](https://github.com/xero/leviathan-demos/blob/main/lvthn-web/README.md) ]
88
+ **`lvthn-web`** [ [demo](https://leviathan.3xi.club/web) · [source](https://github.com/xero/leviathan-demos/tree/main/web) · [readme](https://github.com/xero/leviathan-demos/blob/main/web/README.md) ]
89
89
 
90
90
  A browser encryption tool in a single, self-contained HTML file. Encrypt text
91
91
  or files using Serpent-256-CBC and Argon2id key derivation, then share the
@@ -94,7 +94,7 @@ initial load. The code is written to be read. The Encrypt-then-MAC
94
94
  construction, HMAC input (header with HMAC field zeroed + ciphertext), and
95
95
  Argon2id parameters are all intentional examples worth reading.
96
96
 
97
- **`lvthn-chat`** [ [demo](https://leviathan.3xi.club/chat) · [source](https://github.com/xero/leviathan-demos/tree/main/lvthn-chat) · [readme](https://github.com/xero/leviathan-demos/blob/main/lvthn-chat/README.md) ]
97
+ **`lvthn-chat`** [ [demo](https://leviathan.3xi.club/chat) · [source](https://github.com/xero/leviathan-demos/tree/main/chat) · [readme](https://github.com/xero/leviathan-demos/blob/main/chat/README.md) ]
98
98
 
99
99
  End-to-end encrypted chat featuring two-party messaging over X25519 key
100
100
  exchange and XChaCha20-Poly1305 message encryption. The relay server functions
@@ -126,17 +126,20 @@ cat secret.txt | lvthn encrypt -k my.key --armor > secret.enc
126
126
  | Class | Module | Auth | Notes |
127
127
  | ----------------------------------------------------------- | ----------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
128
128
  | **Authenticated encryption** | | | |
129
- | `SerpentSeal` | `serpent`, `sha2` | **Yes** | Serpent-CBC + HMAC-SHA256. Recommended default for most use cases. 64-byte key. |
129
+ | `SerpentSeal` | `serpent`, `sha2` | **Yes** | Serpent-CBC + HMAC-SHA256. Recommended Serpent default. 64-byte key. |
130
+ | `XChaCha20Seal` | `chacha20` | **Yes** | XChaCha20-Poly1305 with internal nonce management. Recommended ChaCha20 default. 32-byte key. |
130
131
  | `SerpentStream`, `SerpentStreamPool` | `serpent`, `sha2` | **Yes** | Chunked one-shot AEAD for large payloads. Pool variant parallelises across workers. 32-byte key. |
131
132
  | `SerpentStreamSealer`, `SerpentStreamOpener` | `serpent`, `sha2` | **Yes** | Incremental streaming AEAD: seal/open one chunk at a time. Pass `{ framed: true }` for self-delimiting `u32be` length-prefix framing. 64-byte key. |
132
- | `XChaCha20Poly1305` | `chacha20` | **Yes** | XChaCha20-Poly1305 AEAD. Recommended when you want a simpler API or a 192-bit nonce safe for random generation. 32-byte key. |
133
+ | `XChaCha20StreamSealer`, `XChaCha20StreamOpener` | `chacha20` | **Yes** | Incremental streaming AEAD using XChaCha20-Poly1305. Per-chunk random nonces, position-bound AAD. `{ framed: true }` for length-prefixed framing. 32-byte key. |
133
134
  | `XChaCha20Poly1305Pool` | `chacha20` | **Yes** | Worker-pool wrapper for `XChaCha20Poly1305`. Parallelises encryption across isolated WASM instances. |
134
- | `ChaCha20Poly1305` | `chacha20` | **Yes** | ChaCha20-Poly1305 AEAD — RFC 8439. 12-byte nonce; prefer `XChaCha20Poly1305` unless you need RFC 8439 exact compliance. |
135
+ | **Stateless primitives** _caller manages nonces_ | | | |
136
+ | `XChaCha20Poly1305` | `chacha20` | **Yes** | RFC-faithful stateless AEAD. 24-byte nonce, caller-managed. Use `XChaCha20Seal` unless you need explicit nonce control. |
137
+ | `ChaCha20Poly1305` | `chacha20` | **Yes** | RFC 8439 stateless AEAD. 12-byte nonce, caller-managed. Prefer `XChaCha20Seal` unless you need RFC 8439 exact compliance. |
135
138
  | **Unauthenticated primitives** _pair with HMAC or use AEAD_ | | | |
136
139
  | `Serpent` | `serpent` | **No** | Serpent-256 ECB block cipher. Single-block encrypt/decrypt. |
137
140
  | `SerpentCtr` | `serpent` | **No** | Serpent-256 CTR mode stream cipher. Requires `{ dangerUnauthenticated: true }`. |
138
141
  | `SerpentCbc` | `serpent` | **No** | Serpent-256 CBC mode with PKCS7 padding. Requires `{ dangerUnauthenticated: true }`. |
139
- | `ChaCha20` | `chacha20` | **No** | ChaCha20 stream cipher — RFC 8439. Unauthenticated; use `XChaCha20Poly1305` unless you need raw keystream. |
142
+ | `ChaCha20` | `chacha20` | **No** | ChaCha20 stream cipher — RFC 8439. Unauthenticated; use `XChaCha20Seal` unless you need raw keystream. |
140
143
  | `Poly1305` | `chacha20` | **No** | Poly1305 one-time MAC — RFC 8439. Use via the AEAD classes unless you have a specific reason not to. |
141
144
  | **Hashing and key derivation** | | | |
142
145
  | `SHA256`, `SHA384`, `SHA512` | `sha2` | — | SHA-2 family — FIPS 180-4. |
@@ -174,25 +177,24 @@ const decrypted = seal.decrypt(key, ciphertext)
174
177
  seal.dispose()
175
178
  ```
176
179
 
177
- ### Authenticated encryption with XChaCha20-Poly1305
180
+ ### Authenticated encryption with XChaCha20
178
181
 
179
182
  ```typescript
180
- import { init, XChaCha20Poly1305, randomBytes } from 'leviathan-crypto'
183
+ import { init, XChaCha20Seal, randomBytes } from 'leviathan-crypto'
181
184
 
182
185
  await init(['chacha20'])
183
186
 
184
- const key = randomBytes(32) // 32-byte key
185
- const nonce = randomBytes(24) // 24-byte nonce (XChaCha20 extended nonce)
187
+ const key = randomBytes(32) // 32-byte key
186
188
 
187
- const chacha = new XChaCha20Poly1305()
189
+ const seal = new XChaCha20Seal(key)
188
190
 
189
- // Encrypt and authenticate
190
- const ciphertext = chacha.encrypt(key, nonce, plaintext)
191
+ // Encrypt and authenticate (nonce generated internally)
192
+ const ciphertext = seal.encrypt(plaintext)
191
193
 
192
194
  // Decrypt and verify (throws on tamper)
193
- const decrypted = chacha.decrypt(key, nonce, ciphertext)
195
+ const decrypted = seal.decrypt(ciphertext)
194
196
 
195
- chacha.dispose()
197
+ seal.dispose()
196
198
  ```
197
199
 
198
200
  For more examples, including streaming, chunking, hashing, and key derivation,
@@ -224,7 +226,7 @@ import { serpentInit, SerpentSeal } from 'leviathan-crypto/serpent'
224
226
  await serpentInit()
225
227
 
226
228
  // Only chacha20.wasm ends up in your bundle
227
- import { chacha20Init, XChaCha20Poly1305 } from 'leviathan-crypto/chacha20'
229
+ import { chacha20Init, XChaCha20Seal } from 'leviathan-crypto/chacha20'
228
230
  await chacha20Init()
229
231
  ```
230
232
 
@@ -252,7 +254,7 @@ await chacha20Init()
252
254
  | ----------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
253
255
  | serpent | [▼](./docs/serpent.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/serpent) | Serpent-256 TypeScript API (`SerpentSeal`, `SerpentStream`, `SerpentStreamPool`, `SerpentStreamSealer`, `SerpentStreamOpener`, `Serpent`, `SerpentCtr`, `SerpentCbc`) |
254
256
  | asm_serpent | [▼](./docs/asm_serpent.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/asm_serpent) | Serpent-256 WASM implementation (bitslice S-boxes, key schedule, CTR/CBC) |
255
- | chacha20 | [▼](./docs/chacha20.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/chacha20) | ChaCha20/Poly1305 TypeScript API (`ChaCha20`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305`, `XChaCha20Poly1305Pool`) |
257
+ | chacha20 | [▼](./docs/chacha20.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/chacha20) | ChaCha20/Poly1305 TypeScript API (`XChaCha20Seal`, `XChaCha20StreamSealer`, `XChaCha20StreamOpener`, `ChaCha20`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305`, `XChaCha20Poly1305Pool`) |
256
258
  | asm_chacha | [▼](./docs/asm_chacha.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/asm_chacha) | ChaCha20/Poly1305 WASM implementation (quarter-round, HChaCha20) |
257
259
  | sha2 | [▼](./docs/sha2.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/sha2) | SHA-2 TypeScript API (`SHA256`, `SHA512`, `SHA384`, `HMAC_SHA256`, `HMAC_SHA512`, `HMAC_SHA384`) |
258
260
  | asm_sha2 | [▼](./docs/asm_sha2.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/asm_sha2) | SHA-2 WASM implementation (compression functions, HMAC) |
@@ -278,7 +280,7 @@ These helpers are available immediately on import with no `init()` required.
278
280
  | `constantTimeEqual(a, b)` | [▼](./docs/utils.md#constanttimeequal) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#constanttimeequal) | Constant-time byte comparison (XOR-accumulate) |
279
281
  | `wipe(data)` | [▼](./docs/utils.md#wipe) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#wipe) | Zero a typed array in place |
280
282
  | `xor(a, b)` | [▼](./docs/utils.md#xor) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#xor) | XOR two equal-length `Uint8Array`s |
281
- | `concat(a, b)` | [▼](./docs/utils.md#concat) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#concat) | Concatenate two `Uint8Array`s |
283
+ | `concat(...arrays)` | [▼](./docs/utils.md#concat) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#concat) | Concatenate `Uint8Array`s (variadic) |
282
284
  | `hasSIMD()` | [▼](./docs/utils.md#hassimd) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#hassimd) | Detects WebAssembly SIMD support. Cached after first call. Used internally for CTR/CBC/ChaCha20 dispatch. |
283
285
 
284
286
  ### Algorithm correctness and verifications
package/SECURITY.md CHANGED
@@ -13,8 +13,9 @@
13
13
 
14
14
  | Version | Supported |
15
15
  |---------|-----------|
16
- | v1.3.x | ︎✓ |
17
- | v1.2.x | ✓ |
16
+ | v1.4.x | |
17
+ | v1.3.x | ✓ |
18
+ | v1.2.x | ✗ |
18
19
  | v1.1.x | ✗ |
19
20
  | v1.0.x | ✗ |
20
21
 
@@ -92,18 +93,29 @@ See: [`xero/BicliqueFinder/biclique_research.md`][biclique]
92
93
 
93
94
  ### Authenticated Encryption by Default
94
95
 
95
- Raw unauthenticated cipher modes (`SerpentCbc`, `SerpentCtr`) are exposed
96
- for power users but are not the recommended entry point. The primary API
97
- surfaces `SerpentSeal`, `SerpentStream`, `SerpentStreamSealer` are
98
- authenticated by construction.
96
+ Raw unauthenticated cipher modes (`SerpentCbc`, `SerpentCtr`, `ChaCha20`) and
97
+ stateless caller-managed-nonce primitives (`ChaCha20Poly1305`,
98
+ `XChaCha20Poly1305`) are exposed for power users but are not the recommended
99
+ entry point. The primary API surfaces — `SerpentSeal`, `SerpentStream`,
100
+ `SerpentStreamSealer`, `XChaCha20Seal`, and `XChaCha20StreamSealer` — are
101
+ authenticated by construction with internally managed nonces.
99
102
 
100
- **`SerpentStreamSealer` satisfies the _Cryptographic Doom Principle_:**
103
+ **Both streaming constructions satisfy the _Cryptographic Doom Principle_:**
101
104
 
102
- MAC verification is the unconditional gate on the open path,
103
- decryption is unreachable until that gate clears, and per-chunk
105
+ `SerpentStreamSealer` uses encrypt-then-MAC (SerpentCbc + HMAC-SHA256).
106
+ MAC verification is the unconditional gate on the open path.
107
+ Decryption is unreachable until that gate clears. Per-chunk
104
108
  HKDF key derivation with position-bound info extends this
105
109
  guarantee to full stream integrity.
106
110
 
111
+ `XChaCha20StreamSealer` uses XChaCha20-Poly1305 AEAD per chunk.
112
+ The Poly1305 tag is verified inside the WASM `xcDecrypt` call
113
+ before any plaintext is produced. On authentication failure,
114
+ plaintext bytes are never generated and never returned. Stream-level
115
+ binding via per-chunk AAD (`stream_id || index || isLast || user AAD`)
116
+ ensures reorder, splice, truncation, and cross-stream substitution
117
+ all fail AEAD verification before decryption.
118
+
107
119
  ### Dependency Management
108
120
 
109
121
  The library has **zero** runtime dependencies by design.
@@ -46,4 +46,24 @@ export declare class XChaCha20Poly1305 {
46
46
  decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array;
47
47
  dispose(): void;
48
48
  }
49
+ /**
50
+ * XChaCha20-Poly1305 AEAD with bound key and automatic nonce management.
51
+ * Implements the AEAD interface — encrypt()/decrypt() require only plaintext
52
+ * and optional AAD. Each encrypt() call generates a fresh 24-byte random nonce.
53
+ *
54
+ * Wire format: nonce(24) || ciphertext || tag(16)
55
+ *
56
+ * Use this when you want the simplest correct API and do not need to manage
57
+ * nonces yourself. For protocol interop requiring explicit nonce control,
58
+ * use XChaCha20Poly1305 directly.
59
+ */
60
+ export declare class XChaCha20Seal {
61
+ private readonly _x;
62
+ private readonly _key;
63
+ constructor(key: Uint8Array);
64
+ encrypt(plaintext: Uint8Array, aad?: Uint8Array, _nonce?: Uint8Array): Uint8Array;
65
+ decrypt(ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array;
66
+ dispose(): void;
67
+ }
68
+ export { XChaCha20StreamSealer, XChaCha20StreamOpener } from './stream-sealer.js';
49
69
  export declare function _chachaReady(): boolean;
@@ -25,8 +25,8 @@
25
25
  // Uses the init() module cache — call init('chacha20') before constructing.
26
26
  import { getInstance, initModule } from '../init.js';
27
27
  import { aeadEncrypt, aeadDecrypt, xcEncrypt, xcDecrypt } from './ops.js';
28
- import { hasSIMD } from '../utils.js';
29
- const _embedded = () => import('../embedded/chacha.js').then(m => m.WASM_BASE64);
28
+ import { hasSIMD, randomBytes, wipe } from '../utils.js';
29
+ const _embedded = () => import('../embedded/chacha20.js').then(m => m.WASM_GZ_BASE64);
30
30
  export async function chacha20Init(mode = 'embedded', opts) {
31
31
  return initModule('chacha20', _embedded, mode, opts);
32
32
  }
@@ -167,6 +167,52 @@ export class XChaCha20Poly1305 {
167
167
  this.x.wipeBuffers();
168
168
  }
169
169
  }
170
+ // ── XChaCha20Seal ────────────────────────────────────────────────────────────
171
+ /**
172
+ * XChaCha20-Poly1305 AEAD with bound key and automatic nonce management.
173
+ * Implements the AEAD interface — encrypt()/decrypt() require only plaintext
174
+ * and optional AAD. Each encrypt() call generates a fresh 24-byte random nonce.
175
+ *
176
+ * Wire format: nonce(24) || ciphertext || tag(16)
177
+ *
178
+ * Use this when you want the simplest correct API and do not need to manage
179
+ * nonces yourself. For protocol interop requiring explicit nonce control,
180
+ * use XChaCha20Poly1305 directly.
181
+ */
182
+ export class XChaCha20Seal {
183
+ _x;
184
+ _key;
185
+ constructor(key) {
186
+ if (!_chachaReady())
187
+ throw new Error('leviathan-crypto: call init([\'chacha20\']) before using XChaCha20Seal');
188
+ if (key.length !== 32)
189
+ throw new RangeError(`XChaCha20Seal key must be 32 bytes (got ${key.length})`);
190
+ this._x = getExports();
191
+ this._key = key.slice();
192
+ }
193
+ // _nonce: test seam only — inject a fixed nonce for deterministic KAT vectors
194
+ encrypt(plaintext, aad = new Uint8Array(0), _nonce) {
195
+ const nonce = (_nonce && _nonce.length === 24) ? _nonce : randomBytes(24);
196
+ const sealed = xcEncrypt(this._x, this._key, nonce, plaintext, aad);
197
+ // Prepend nonce to sealed output (ciphertext || tag)
198
+ const out = new Uint8Array(24 + sealed.length);
199
+ out.set(nonce, 0);
200
+ out.set(sealed, 24);
201
+ return out;
202
+ }
203
+ decrypt(ciphertext, aad = new Uint8Array(0)) {
204
+ if (ciphertext.length < 40)
205
+ throw new RangeError(`XChaCha20Seal ciphertext too short — need nonce(24)+tag(16)=40 bytes minimum (got ${ciphertext.length})`);
206
+ const nonce = ciphertext.subarray(0, 24);
207
+ const payload = ciphertext.subarray(24);
208
+ return xcDecrypt(this._x, this._key, nonce, payload, aad);
209
+ }
210
+ dispose() {
211
+ wipe(this._key);
212
+ this._x.wipeBuffers();
213
+ }
214
+ }
215
+ export { XChaCha20StreamSealer, XChaCha20StreamOpener } from './stream-sealer.js';
170
216
  // ── Ready check ──────────────────────────────────────────────────────────────
171
217
  export function _chachaReady() {
172
218
  try {
@@ -4,24 +4,14 @@
4
4
  // Dispatches independent encrypt/decrypt jobs across Web Workers, each with
5
5
  // its own WebAssembly.Instance and isolated linear memory.
6
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
- }
7
+ import { decodeWasm } from '../loader.js';
18
8
  // ── WASM module singleton ────────────────────────────────────────────────────
19
9
  let _wasmModule;
20
10
  async function getWasmModule() {
21
11
  if (_wasmModule)
22
12
  return _wasmModule;
23
- const { WASM_BASE64 } = await import('../embedded/chacha.js');
24
- const bytes = base64ToBytes(WASM_BASE64);
13
+ const { WASM_GZ_BASE64 } = await import('../embedded/chacha20.js');
14
+ const bytes = await decodeWasm(WASM_GZ_BASE64);
25
15
  _wasmModule = await WebAssembly.compile(bytes.buffer);
26
16
  return _wasmModule;
27
17
  }
@@ -0,0 +1,49 @@
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
+ }