leviathan-crypto 1.4.0 → 2.0.1

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 (122) hide show
  1. package/CLAUDE.md +129 -94
  2. package/README.md +166 -223
  3. package/SECURITY.md +90 -45
  4. package/dist/chacha20/cipher-suite.d.ts +4 -0
  5. package/dist/chacha20/cipher-suite.js +79 -0
  6. package/dist/chacha20/embedded.d.ts +1 -0
  7. package/dist/chacha20/embedded.js +27 -0
  8. package/dist/chacha20/index.d.ts +20 -27
  9. package/dist/chacha20/index.js +40 -59
  10. package/dist/chacha20/ops.d.ts +1 -1
  11. package/dist/chacha20/ops.js +19 -18
  12. package/dist/chacha20/pool-worker.js +77 -0
  13. package/dist/ct-wasm.d.ts +1 -0
  14. package/dist/ct-wasm.js +3 -0
  15. package/dist/ct.wasm +0 -0
  16. package/dist/docs/aead.md +323 -0
  17. package/dist/docs/architecture.md +427 -292
  18. package/dist/docs/argon2id.md +42 -30
  19. package/dist/docs/chacha20.md +192 -266
  20. package/dist/docs/exports.md +241 -0
  21. package/dist/docs/fortuna.md +60 -69
  22. package/dist/docs/init.md +172 -178
  23. package/dist/docs/loader.md +87 -142
  24. package/dist/docs/serpent.md +134 -583
  25. package/dist/docs/sha2.md +91 -103
  26. package/dist/docs/sha3.md +70 -36
  27. package/dist/docs/types.md +94 -16
  28. package/dist/docs/utils.md +109 -32
  29. package/dist/embedded/kyber.d.ts +1 -0
  30. package/dist/embedded/kyber.js +3 -0
  31. package/dist/embedded/serpent.d.ts +1 -1
  32. package/dist/embedded/serpent.js +1 -1
  33. package/dist/errors.d.ts +10 -0
  34. package/dist/errors.js +38 -0
  35. package/dist/fortuna.d.ts +0 -6
  36. package/dist/fortuna.js +5 -5
  37. package/dist/index.d.ts +25 -9
  38. package/dist/index.js +36 -7
  39. package/dist/init.d.ts +3 -7
  40. package/dist/init.js +18 -35
  41. package/dist/keccak/embedded.d.ts +1 -0
  42. package/dist/keccak/embedded.js +27 -0
  43. package/dist/keccak/index.d.ts +4 -0
  44. package/dist/keccak/index.js +31 -0
  45. package/dist/kyber/embedded.d.ts +1 -0
  46. package/dist/kyber/embedded.js +27 -0
  47. package/dist/kyber/indcpa.d.ts +49 -0
  48. package/dist/kyber/indcpa.js +352 -0
  49. package/dist/kyber/index.d.ts +38 -0
  50. package/dist/kyber/index.js +150 -0
  51. package/dist/kyber/kem.d.ts +21 -0
  52. package/dist/kyber/kem.js +160 -0
  53. package/dist/kyber/params.d.ts +14 -0
  54. package/dist/kyber/params.js +37 -0
  55. package/dist/kyber/suite.d.ts +13 -0
  56. package/dist/kyber/suite.js +94 -0
  57. package/dist/kyber/types.d.ts +98 -0
  58. package/dist/kyber/types.js +25 -0
  59. package/dist/kyber/validate.d.ts +19 -0
  60. package/dist/kyber/validate.js +68 -0
  61. package/dist/kyber.wasm +0 -0
  62. package/dist/loader.d.ts +15 -6
  63. package/dist/loader.js +65 -21
  64. package/dist/serpent/cipher-suite.d.ts +4 -0
  65. package/dist/serpent/cipher-suite.js +122 -0
  66. package/dist/serpent/embedded.d.ts +1 -0
  67. package/dist/serpent/embedded.js +27 -0
  68. package/dist/serpent/index.d.ts +6 -37
  69. package/dist/serpent/index.js +9 -118
  70. package/dist/serpent/pool-worker.d.ts +1 -0
  71. package/dist/serpent/pool-worker.js +208 -0
  72. package/dist/serpent/serpent-cbc.d.ts +30 -0
  73. package/dist/serpent/serpent-cbc.js +142 -0
  74. package/dist/serpent.wasm +0 -0
  75. package/dist/sha2/embedded.d.ts +1 -0
  76. package/dist/sha2/embedded.js +27 -0
  77. package/dist/sha2/hkdf.js +6 -2
  78. package/dist/sha2/index.d.ts +3 -2
  79. package/dist/sha2/index.js +3 -4
  80. package/dist/sha3/embedded.d.ts +1 -0
  81. package/dist/sha3/embedded.js +27 -0
  82. package/dist/sha3/index.d.ts +3 -2
  83. package/dist/sha3/index.js +3 -4
  84. package/dist/stream/constants.d.ts +6 -0
  85. package/dist/stream/constants.js +30 -0
  86. package/dist/stream/header.d.ts +9 -0
  87. package/dist/stream/header.js +77 -0
  88. package/dist/stream/index.d.ts +7 -0
  89. package/dist/stream/index.js +27 -0
  90. package/dist/stream/open-stream.d.ts +21 -0
  91. package/dist/stream/open-stream.js +146 -0
  92. package/dist/stream/seal-stream-pool.d.ts +38 -0
  93. package/dist/stream/seal-stream-pool.js +400 -0
  94. package/dist/stream/seal-stream.d.ts +20 -0
  95. package/dist/stream/seal-stream.js +142 -0
  96. package/dist/stream/seal.d.ts +9 -0
  97. package/dist/stream/seal.js +75 -0
  98. package/dist/stream/types.d.ts +25 -0
  99. package/dist/stream/types.js +26 -0
  100. package/dist/utils.d.ts +7 -2
  101. package/dist/utils.js +49 -3
  102. package/dist/wasm-source.d.ts +12 -0
  103. package/dist/wasm-source.js +26 -0
  104. package/package.json +13 -5
  105. package/dist/chacha20/pool.d.ts +0 -52
  106. package/dist/chacha20/pool.js +0 -178
  107. package/dist/chacha20/pool.worker.js +0 -37
  108. package/dist/chacha20/stream-sealer.d.ts +0 -49
  109. package/dist/chacha20/stream-sealer.js +0 -327
  110. package/dist/docs/chacha20_pool.md +0 -309
  111. package/dist/docs/wasm.md +0 -194
  112. package/dist/serpent/seal.d.ts +0 -8
  113. package/dist/serpent/seal.js +0 -72
  114. package/dist/serpent/stream-pool.d.ts +0 -48
  115. package/dist/serpent/stream-pool.js +0 -275
  116. package/dist/serpent/stream-sealer.d.ts +0 -55
  117. package/dist/serpent/stream-sealer.js +0 -342
  118. package/dist/serpent/stream.d.ts +0 -28
  119. package/dist/serpent/stream.js +0 -205
  120. package/dist/serpent/stream.worker.d.ts +0 -32
  121. package/dist/serpent/stream.worker.js +0 -117
  122. /package/dist/chacha20/{pool.worker.d.ts → pool-worker.d.ts} +0 -0
@@ -1,7 +1,22 @@
1
- # ChaCha20, Poly1305, and AEAD TypeScript API
1
+ # ChaCha20 TypeScript API
2
2
 
3
3
  > [!NOTE]
4
- > See [ChaCha20-Poly1305 implementation audit](./chacha_audit.md) for algorithm correctness verifications.
4
+ > API reference for `ChaCha20`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305`, and `XChaCha20Cipher`. Covers initialization, all class methods, usage examples, and error conditions.
5
+
6
+ > ### Table of Contents
7
+ > - [Overview](#overview)
8
+ > - [Security Notes](#security-notes)
9
+ > - [Module Init](#module-init)
10
+ > - [API Reference](#api-reference)
11
+ > - [`ChaCha20`](#chacha20)
12
+ > - [`Poly1305`](#poly1305)
13
+ > - [`ChaCha20Poly1305`](#chacha20poly1305)
14
+ > - [`XChaCha20Poly1305`](#xchacha20poly1305)
15
+ > - [XChaCha20Cipher](#xchacha20cipher)
16
+ > - [Usage Examples](#usage-examples)
17
+ > - [Error Conditions](#error-conditions)
18
+
19
+ ---
5
20
 
6
21
  ## Overview
7
22
 
@@ -9,37 +24,38 @@
9
24
  on all platforms (including those without hardware AES), resistant to timing attacks
10
25
  by design, and widely deployed in TLS, SSH, and WireGuard. ChaCha20 encrypts data
11
26
  by generating a pseudorandom keystream from a 256-bit key and a nonce, then XORing
12
- it with the plaintext. It does **not** provide authentication on its own a
13
- modified message will decrypt to garbage with no warning.
27
+ it with the plaintext. It does **not** provide authentication on its own. A modified message will decrypt to garbage with no warning.
14
28
 
15
29
  **Poly1305** is a one-time message authentication code (MAC). Given a unique 256-bit
16
30
  key and a message, it produces a 16-byte tag that proves the message has not been
17
31
  tampered with. The critical requirement is that each Poly1305 key is used **exactly
18
- once** reusing a key completely breaks its security. You almost never need to use
32
+ once**. Reusing a key completely breaks its security. You almost never need to use
19
33
  Poly1305 directly; the AEAD constructions below handle key derivation for you.
20
34
 
21
35
  **ChaCha20-Poly1305** (RFC 8439) combines both primitives into an AEAD
22
36
  (Authenticated Encryption with Associated Data). It encrypts your data and
23
37
  produces an authentication tag in a single operation. On decryption, it verifies
24
- the tag before returning any plaintext if someone tampered with the ciphertext,
38
+ the tag before returning any plaintext. If someone tampered with the ciphertext,
25
39
  you get an error instead of corrupted data. The nonce is 96 bits (12 bytes).
26
40
 
27
41
  **XChaCha20-Poly1305** extends the nonce to 192 bits (24 bytes) using the HChaCha20
28
- subkey derivation step. This makes random nonce generation completely safe with a
42
+ subkey derivation step. This makes random nonce generation completely safe. With a
29
43
  24-byte nonce, the probability of a collision is negligible even after billions of
30
- messages. **For most users, `XChaCha20Seal` is the recommended choice.** It binds the key
31
- at construction, generates a fresh nonce per call, and provides the simplest
32
- correct API. For protocol interop requiring explicit nonce control, use
33
- `XChaCha20Poly1305` directly.
44
+ messages. **For most users, `Seal` with `XChaCha20Cipher` is the recommended choice.** It
45
+ produces a self-contained authenticated blob with no nonce management, no
46
+ instantiation, and no `dispose()`. For protocol interop requiring explicit nonce
47
+ control, use `XChaCha20Poly1305` directly.
48
+
49
+ ---
34
50
 
35
51
  ## Security Notes
36
52
 
37
53
  > [!IMPORTANT]
38
- > Read this section before writing any code. These are not theoretical concerns
39
- > they are the mistakes that cause real-world breaches.
54
+ > Read this section before writing any code. These are not theoretical concerns.
55
+ > They are the mistakes that cause real-world breaches.
40
56
 
41
- - **Use `XChaCha20Seal` unless you need explicit nonce control.** It is the
42
- safest default: authenticated encryption with automatic nonce generation.
57
+ - **Use `Seal` with `XChaCha20Cipher` unless you need explicit nonce control.**
58
+ It is the safest default: authenticated encryption in a single static call.
43
59
  If you are unsure which class to pick, pick this one. Use `XChaCha20Poly1305`
44
60
  when protocol interop requires you to manage nonces yourself.
45
61
 
@@ -49,7 +65,7 @@ correct API. For protocol interop requiring explicit nonce control, use
49
65
  With `ChaCha20Poly1305` (12-byte nonce), random generation has a meaningful
50
66
  collision risk after roughly 2^32 messages under one key. With
51
67
  `XChaCha20Poly1305` (24-byte nonce), random generation is safe for any practical
52
- message count just call `randomBytes(24)` for each message.
68
+ message count. Just call `randomBytes(24)` for each message.
53
69
 
54
70
  - **Poly1305 keys are single-use.** Each Poly1305 key must be used to authenticate
55
71
  exactly one message. The AEAD classes (`ChaCha20Poly1305` and
@@ -58,13 +74,12 @@ correct API. For protocol interop requiring explicit nonce control, use
58
74
  `Poly1305` class directly, it is your responsibility to never reuse a key.
59
75
 
60
76
  - **AEAD protects both confidentiality and authenticity.** If authentication fails
61
- during decryption, the plaintext is never returned you get an error. This is
62
- intentional. Do not try to work around it. If decryption fails, the ciphertext
63
- was corrupted or tampered with.
77
+ during decryption, the plaintext is never returned. You get an error. This is
78
+ intentional. Do not try to work around it. If decryption fails, the ciphertext was corrupted or tampered with.
64
79
 
65
80
  - **Associated data (AAD) is authenticated but not encrypted.** Use AAD for data
66
81
  that must travel in the clear (headers, routing metadata, user IDs) but must be
67
- verified as unmodified. If someone changes the AAD, decryption will fail even
82
+ verified as unmodified. If someone changes the AAD, decryption will fail even
68
83
  if the ciphertext itself is untouched.
69
84
 
70
85
  - **Always call `dispose()` when you are done.** This wipes key material and
@@ -77,29 +92,32 @@ correct API. For protocol interop requiring explicit nonce control, use
77
92
  building your own authenticated construction (and you probably should not be),
78
93
  use one of the AEAD classes instead.
79
94
 
95
+ ---
96
+
80
97
  ## Module Init
81
98
 
82
99
  Each module subpath exports its own init function for consumers who want
83
100
  tree-shakeable imports.
84
101
 
85
- ### `chacha20Init(mode?, opts?)`
102
+ ### `chacha20Init(source)`
86
103
 
87
104
  Initializes only the chacha20 WASM binary. Equivalent to calling the
88
- root `init(['chacha20'], mode, opts)` but without pulling the other three
105
+ root `init({ chacha20: chacha20Wasm })` but without pulling the other three
89
106
  modules into the bundle.
90
107
 
91
108
  **Signature:**
92
109
 
93
110
  ```typescript
94
- async function chacha20Init(mode?: Mode, opts?: InitOpts): Promise<void>
111
+ async function chacha20Init(source: WasmSource): Promise<void>
95
112
  ```
96
113
 
97
114
  **Usage:**
98
115
 
99
116
  ```typescript
100
117
  import { chacha20Init, XChaCha20Poly1305 } from 'leviathan-crypto/chacha20'
118
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
101
119
 
102
- await chacha20Init()
120
+ await chacha20Init(chacha20Wasm)
103
121
  const aead = new XChaCha20Poly1305()
104
122
  ```
105
123
 
@@ -107,17 +125,17 @@ const aead = new XChaCha20Poly1305()
107
125
 
108
126
  ## API Reference
109
127
 
110
- All classes require calling `await init(['chacha20'])` or the subpath `chacha20Init()`
128
+ All classes require calling `await init({ chacha20: chacha20Wasm })` or the subpath `chacha20Init()`
111
129
  before construction. If you construct a class before initialization, it throws:
112
130
  ```
113
- Error: leviathan-crypto: call init(['chacha20']) before using this class
131
+ Error: leviathan-crypto: call init({ chacha20: ... }) before using this class
114
132
  ```
115
133
 
116
134
  ---
117
135
 
118
136
  ### `ChaCha20`
119
137
 
120
- Raw ChaCha20 stream cipher. **No authentication** use `XChaCha20Poly1305` instead
138
+ Raw ChaCha20 stream cipher. **No authentication.** Use `XChaCha20Poly1305` instead
121
139
  unless you are building a custom protocol and understand the risks.
122
140
 
123
141
  #### Constructor
@@ -126,7 +144,7 @@ unless you are building a custom protocol and understand the risks.
126
144
  new ChaCha20()
127
145
  ```
128
146
 
129
- Throws if `init(['chacha20'])` has not been called.
147
+ Throws if `init({ chacha20: chacha20Wasm })` has not been called.
130
148
 
131
149
  ---
132
150
 
@@ -158,8 +176,7 @@ a new `Uint8Array` containing the ciphertext (same length as input).
158
176
 
159
177
  #### `beginDecrypt(key: Uint8Array, nonce: Uint8Array): void`
160
178
 
161
- Prepares the cipher for decryption. Identical to `beginEncrypt` ChaCha20 is
162
- symmetric (encryption and decryption are the same XOR operation).
179
+ Prepares the cipher for decryption. Identical to `beginEncrypt`. ChaCha20 is symmetric; encryption and decryption are the same XOR operation.
163
180
 
164
181
  | Parameter | Type | Description |
165
182
  |-----------|------|-------------|
@@ -193,7 +210,7 @@ when you are done with the instance.
193
210
  ### `Poly1305`
194
211
 
195
212
  Standalone Poly1305 one-time MAC. **Each key must be used exactly once.** You
196
- almost certainly want `ChaCha20Poly1305` or `XChaCha20Poly1305` instead — they
213
+ almost certainly want `ChaCha20Poly1305` or `XChaCha20Poly1305` instead. They
197
214
  handle Poly1305 key derivation automatically.
198
215
 
199
216
  #### Constructor
@@ -202,7 +219,7 @@ handle Poly1305 key derivation automatically.
202
219
  new Poly1305()
203
220
  ```
204
221
 
205
- Throws if `init(['chacha20'])` has not been called.
222
+ Throws if `init({ chacha20: chacha20Wasm })` has not been called.
206
223
 
207
224
  ---
208
225
 
@@ -212,10 +229,10 @@ Computes a 16-byte Poly1305 authentication tag over the given message.
212
229
 
213
230
  | Parameter | Type | Description |
214
231
  |-----------|------|-------------|
215
- | `key` | `Uint8Array` | 32 bytes must be unique per message |
232
+ | `key` | `Uint8Array` | 32 bytes. Must be unique per message. |
216
233
  | `msg` | `Uint8Array` | The message to authenticate (any length) |
217
234
 
218
- **Returns** `Uint8Array` a 16-byte authentication tag.
235
+ **Returns** `Uint8Array`: a 16-byte authentication tag.
219
236
 
220
237
  **Throws** `RangeError` if `key` is not 32 bytes.
221
238
 
@@ -242,54 +259,59 @@ to avoid collision risk.
242
259
  new ChaCha20Poly1305()
243
260
  ```
244
261
 
245
- Throws if `init(['chacha20'])` has not been called.
262
+ Throws if `init({ chacha20: chacha20Wasm })` has not been called.
246
263
 
247
264
  ---
248
265
 
249
- #### `encrypt(key, nonce, plaintext, aad?): { ciphertext: Uint8Array; tag: Uint8Array }`
266
+ #### `encrypt(key, nonce, plaintext, aad?): Uint8Array`
250
267
 
251
- Encrypts plaintext and produces a 16-byte authentication tag.
268
+ Encrypts plaintext and returns the ciphertext with the 16-byte Poly1305 tag
269
+ appended.
270
+
271
+ > [!WARNING]
272
+ > Each `ChaCha20Poly1305` instance allows only **one** `encrypt()` call. A second
273
+ > call throws to prevent accidental nonce reuse. Create a new instance for each
274
+ > encryption, or use `Seal` with `XChaCha20Cipher` for automatic nonce management.
252
275
 
253
276
  | Parameter | Type | Default | Description |
254
277
  |-----------|------|---------|-------------|
255
278
  | `key` | `Uint8Array` | | 32 bytes (256-bit key) |
256
279
  | `nonce` | `Uint8Array` | | 12 bytes (96-bit nonce) |
257
280
  | `plaintext` | `Uint8Array` | | Data to encrypt (up to the module's chunk size limit) |
258
- | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data authenticated but not encrypted |
281
+ | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data. Authenticated but not encrypted. |
259
282
 
260
- **Returns** `{ ciphertext: Uint8Array; tag: Uint8Array }` the ciphertext (same
261
- length as plaintext) and a 16-byte authentication tag. You need both to decrypt.
283
+ **Returns** `Uint8Array`: ciphertext + 16-byte tag (length = plaintext.length + 16).
262
284
 
263
285
  **Throws:**
264
286
  - `RangeError` if `key` is not 32 bytes
265
287
  - `RangeError` if `nonce` is not 12 bytes
266
288
  - `RangeError` if `plaintext` exceeds the maximum chunk size
289
+ - `Error` if `encrypt()` has already been called on this instance
267
290
 
268
291
  ---
269
292
 
270
- #### `decrypt(key, nonce, ciphertext, tag, aad?): Uint8Array`
293
+ #### `decrypt(key, nonce, ciphertext, aad?): Uint8Array`
271
294
 
272
- Verifies the authentication tag and decrypts the ciphertext. If authentication
273
- fails, an error is thrown and no plaintext is returned.
295
+ Verifies the authentication tag and decrypts the ciphertext. The `ciphertext`
296
+ parameter must include the appended 16-byte tag (i.e., the exact output of
297
+ `encrypt()`). If authentication fails, an error is thrown and no plaintext is
298
+ returned.
274
299
 
275
- Tag comparison uses a constant-time XOR-accumulate pattern no timing side
276
- channel leaks whether the tag was "close" to correct.
300
+ Tag comparison uses a constant-time XOR-accumulate pattern; no timing side channel leaks whether the tag was "close" to correct.
277
301
 
278
302
  | Parameter | Type | Default | Description |
279
303
  |-----------|------|---------|-------------|
280
304
  | `key` | `Uint8Array` | | 32 bytes (same key used for encryption) |
281
305
  | `nonce` | `Uint8Array` | | 12 bytes (same nonce used for encryption) |
282
- | `ciphertext` | `Uint8Array` | | Encrypted data |
283
- | `tag` | `Uint8Array` | | 16-byte authentication tag from `encrypt()` |
306
+ | `ciphertext` | `Uint8Array` | | Encrypted data with appended tag (output of `encrypt()`) |
284
307
  | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data (must match what was passed to `encrypt()`) |
285
308
 
286
- **Returns** `Uint8Array` the decrypted plaintext.
309
+ **Returns** `Uint8Array`: the decrypted plaintext.
287
310
 
288
311
  **Throws:**
289
312
  - `RangeError` if `key` is not 32 bytes
290
313
  - `RangeError` if `nonce` is not 12 bytes
291
- - `RangeError` if `tag` is not 16 bytes
292
- - `RangeError` if `ciphertext` exceeds the maximum chunk size
314
+ - `RangeError` if `ciphertext` is shorter than 16 bytes (no room for a tag)
293
315
  - `Error('ChaCha20Poly1305: authentication failed')` if the tag does not match
294
316
 
295
317
  ---
@@ -303,15 +325,15 @@ Wipes all key material and intermediate state from WASM memory.
303
325
  ### `XChaCha20Poly1305`
304
326
 
305
327
  XChaCha20-Poly1305 AEAD (draft-irtf-cfrg-xchacha). RFC-faithful stateless
306
- primitive key and nonce are passed per-call. Use when protocol interop
307
- requires explicit nonce control. For most use cases, prefer `XChaCha20Seal`
308
- (bound key, automatic nonce management).
328
+ primitive. Key and nonce are passed per-call. Use when protocol interop
329
+ requires explicit nonce control. For most use cases, prefer `Seal` with
330
+ `XChaCha20Cipher` (automatic nonce management, no instantiation).
309
331
 
310
332
  It uses a 24-byte (192-bit) nonce, which is large enough that randomly generated
311
333
  nonces will never collide in practice. Internally, it derives a subkey via
312
334
  HChaCha20 and delegates to `ChaCha20Poly1305`.
313
335
 
314
- Unlike `ChaCha20Poly1305`, the `encrypt()` method returns a single `Uint8Array`
336
+ Like `ChaCha20Poly1305`, the `encrypt()` method returns a single `Uint8Array`
315
337
  with the tag appended to the ciphertext. The `decrypt()` method expects this
316
338
  combined format and splits it internally.
317
339
 
@@ -321,7 +343,7 @@ combined format and splits it internally.
321
343
  new XChaCha20Poly1305()
322
344
  ```
323
345
 
324
- Throws if `init(['chacha20'])` has not been called.
346
+ Throws if `init({ chacha20: chacha20Wasm })` has not been called.
325
347
 
326
348
  ---
327
349
 
@@ -334,9 +356,9 @@ Encrypts plaintext and returns the ciphertext with the 16-byte tag appended.
334
356
  | `key` | `Uint8Array` | | 32 bytes (256-bit key) |
335
357
  | `nonce` | `Uint8Array` | | 24 bytes (192-bit nonce) |
336
358
  | `plaintext` | `Uint8Array` | | Data to encrypt |
337
- | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data authenticated but not encrypted |
359
+ | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data. Authenticated but not encrypted. |
338
360
 
339
- **Returns** `Uint8Array` ciphertext + 16-byte tag (length = plaintext.length + 16).
361
+ **Returns** `Uint8Array`: ciphertext + 16-byte tag (length = plaintext.length + 16).
340
362
 
341
363
  **Throws:**
342
364
  - `RangeError` if `key` is not 32 bytes
@@ -357,7 +379,7 @@ parameter must include the appended 16-byte tag (i.e., the exact output of
357
379
  | `ciphertext` | `Uint8Array` | | Encrypted data with appended tag (output of `encrypt()`) |
358
380
  | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data (must match what was passed to `encrypt()`) |
359
381
 
360
- **Returns** `Uint8Array` the decrypted plaintext.
382
+ **Returns** `Uint8Array`: the decrypted plaintext.
361
383
 
362
384
  **Throws:**
363
385
  - `RangeError` if `key` is not 32 bytes
@@ -373,261 +395,159 @@ Wipes all key material and intermediate state from WASM memory.
373
395
 
374
396
  ---
375
397
 
376
- ## `XChaCha20Seal`
398
+ ## XChaCha20Cipher
377
399
 
378
- XChaCha20-Poly1305 AEAD with a bound key and automatic nonce management.
379
- **This is the recommended authenticated encryption class.** It implements the
380
- `AEAD` interface — `encrypt()` and `decrypt()` require only plaintext and
381
- optional AAD. Each `encrypt()` call generates a fresh 24-byte random nonce
382
- internally, eliminating any risk of nonce reuse.
400
+ `CipherSuite` implementation for XChaCha20-Poly1305. Pass to `Seal`,
401
+ `SealStream`, or `OpenStream`. Never instantiated directly.
383
402
 
384
- **Wire format:** `nonce(24) || ciphertext || tag(16)`
403
+ Requires `init({ chacha20: chacha20Wasm, sha2: sha2Wasm })`.
385
404
 
386
405
  > [!NOTE]
387
- > The nonce is generated internally via `crypto.getRandomValues` you never
388
- > need to manage nonces. For protocol interop requiring explicit nonce control,
389
- > use `XChaCha20Poly1305` directly.
390
-
391
- #### Constructor
392
-
393
- ```typescript
394
- new XChaCha20Seal(key: Uint8Array) // 32 bytes
395
- ```
406
+ > `sha2` is required by the stream layer for HKDF key derivation, not by
407
+ > `XChaCha20Poly1305` itself.
396
408
 
397
- Binds the key at construction. Throws if `init(['chacha20'])` has not been
398
- called. Throws `RangeError` if key is not 32 bytes. The key is copied
399
- internally the caller's buffer can be wiped after construction.
409
+ | Property | Value |
410
+ |----------|-------|
411
+ | `formatEnum` | `0x01` |
412
+ | `keySize` | `32` |
413
+ | `tagSize` | `16` (Poly1305) |
414
+ | `padded` | `false` |
415
+ | `wasmModules` | `['chacha20', 'sha2']` |
400
416
 
401
- ---
402
-
403
- #### `encrypt(plaintext, aad?): Uint8Array`
404
-
405
- Encrypts plaintext and returns a sealed blob with nonce prepended and tag
406
- appended.
407
-
408
- | Parameter | Type | Default | Description |
409
- |-----------|------|---------|-------------|
410
- | `plaintext` | `Uint8Array` | | Data to encrypt (any length, including empty) |
411
- | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data — authenticated but not encrypted |
412
-
413
- **Returns** `Uint8Array` — `nonce(24) || ciphertext || tag(16)` (length = plaintext.length + 40).
414
-
415
- ---
417
+ #### `XChaCha20Cipher.keygen(): Uint8Array`
416
418
 
417
- #### `decrypt(ciphertext, aad?): Uint8Array`
419
+ Returns `randomBytes(32)`. Convenience method. Not on the `CipherSuite` interface.
418
420
 
419
- Reads the nonce from the first 24 bytes, verifies the tag, and decrypts.
420
-
421
- | Parameter | Type | Default | Description |
422
- |-----------|------|---------|-------------|
423
- | `ciphertext` | `Uint8Array` | | Sealed blob from `encrypt()` |
424
- | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data (must match what was passed to `encrypt()`) |
425
-
426
- **Returns** `Uint8Array` — the decrypted plaintext.
427
-
428
- **Throws:**
429
- - `RangeError` if `ciphertext` is shorter than 40 bytes (nonce + tag minimum)
430
- - `Error('ChaCha20Poly1305: authentication failed')` if the tag does not match
431
-
432
- ---
433
-
434
- #### `dispose(): void`
435
-
436
- Wipes the key copy and all WASM buffers.
437
-
438
- ---
439
-
440
- #### Usage
421
+ #### Usage with `Seal`
441
422
 
442
423
  ```typescript
443
- await init(['chacha20'])
444
- const key = randomBytes(32)
445
- const seal = new XChaCha20Seal(key)
446
- const ct = seal.encrypt(plaintext) // nonce generated internally
447
- const pt = seal.decrypt(ct) // throws on tamper
448
- seal.dispose()
449
- ```
450
-
451
- ---
424
+ import { init, Seal, XChaCha20Cipher } from 'leviathan-crypto'
425
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
426
+ import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
452
427
 
453
- ## `XChaCha20StreamSealer` / `XChaCha20StreamOpener`
428
+ await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
454
429
 
455
- Incremental streaming AEAD using XChaCha20-Poly1305 — counterpart to
456
- `SerpentStreamSealer` / `SerpentStreamOpener` for the ChaCha20 family.
457
-
458
- Each chunk is independently encrypted with `XChaCha20-Poly1305` using a fresh
459
- random 24-byte nonce. Stream binding (preventing cross-stream splice and
460
- reordering) is achieved through per-chunk AAD that includes the stream ID,
461
- chunk index, and finality flag. Simpler than `SerpentStreamSealer` — no HKDF
462
- key derivation, no separate HMAC.
463
-
464
- **Wire format:**
465
- ```
466
- header: stream_id (16) || chunkSize_u32be (4) = 20 bytes
467
- chunk: isLast (1) || nonce (24) || ciphertext || tag (16)
468
- ```
469
-
470
- ```typescript
471
- class XChaCha20StreamSealer {
472
- constructor(key: Uint8Array, chunkSize?: number, opts?: {
473
- framed?: boolean;
474
- aad?: Uint8Array;
475
- })
476
- header(): Uint8Array
477
- seal(plaintext: Uint8Array): Uint8Array
478
- final(plaintext: Uint8Array): Uint8Array
479
- dispose(): void
480
- }
481
-
482
- class XChaCha20StreamOpener {
483
- constructor(key: Uint8Array, header: Uint8Array, opts?: {
484
- framed?: boolean;
485
- aad?: Uint8Array;
486
- })
487
- open(chunk: Uint8Array): Uint8Array
488
- feed(bytes: Uint8Array): Uint8Array[]
489
- dispose(): void
490
- }
430
+ const key = XChaCha20Cipher.keygen()
431
+ const blob = Seal.encrypt(XChaCha20Cipher, key, plaintext)
432
+ const pt = Seal.decrypt(XChaCha20Cipher, key, blob) // throws on tamper
491
433
  ```
492
434
 
493
- - **key** 32 bytes. Throws `RangeError` if wrong length.
494
- - **chunkSize** — 1024–65536. Default: 65536.
495
- - **opts.framed** — prepend `u32be(sealedLen)` to each output for flat streams.
496
- - **opts.aad** — associated data folded into every chunk's internal AAD.
497
-
498
- The API shape is identical to `SerpentStreamSealer` / `SerpentStreamOpener` —
499
- same state machine (`fresh → sealing → dead`), same `header()` / `seal()` /
500
- `final()` / `open()` / `feed()` contract.
501
-
502
- #### Usage
435
+ #### Usage with `SealStream` / `OpenStream`
503
436
 
504
437
  ```typescript
505
- await init(['chacha20'])
506
-
507
- const key = randomBytes(32)
508
- const sealer = new XChaCha20StreamSealer(key, 65536)
509
- const header = sealer.header()
438
+ import { SealStream, OpenStream } from 'leviathan-crypto/stream'
439
+ import { XChaCha20Cipher } from 'leviathan-crypto/chacha20'
510
440
 
511
- const chunk0 = sealer.seal(data0) // exactly chunkSize bytes
512
- const last = sealer.final(tail) // any size up to chunkSize; wipes key
441
+ const sealer = new SealStream(XChaCha20Cipher, key)
442
+ const preamble = sealer.preamble // 20 bytes, send before first chunk
443
+ const ct0 = sealer.push(chunk0)
444
+ const ctLast = sealer.finalize(lastChunk)
513
445
 
514
- const opener = new XChaCha20StreamOpener(key, header)
515
- const pt0 = opener.open(chunk0) // throws on auth failure
516
- const ptLast = opener.open(last)
446
+ const opener = new OpenStream(XChaCha20Cipher, key, preamble)
447
+ const pt0 = opener.pull(ct0)
448
+ const ptLast = opener.finalize(ctLast)
517
449
  ```
518
450
 
519
- ---
520
-
521
- ## Parallel pool — `XChaCha20Poly1305Pool`
522
-
523
- For high-throughput workloads where multiple XChaCha20-Poly1305 operations should
524
- run concurrently, `XChaCha20Poly1305Pool` dispatches work across a configurable
525
- number of Web Workers, each holding an isolated `chacha20.wasm` instance in its own
526
- linear memory. This removes the single-threaded bottleneck of a shared WASM
527
- instance and allows encryption and decryption operations to proceed in parallel.
528
- The pool requires `init(['chacha20'])` to be called before construction and is
529
- created via the static factory `XChaCha20Poly1305Pool.create(opts?)` — see
530
- [chacha20_pool.md](./chacha20_pool.md) for the full API reference, pool sizing
531
- guidance, and lifecycle docs.
451
+ See [aead.md](./aead.md) for the full `Seal`, `SealStream`, and `OpenStream` API.
532
452
 
533
453
  ---
534
454
 
535
455
  ## Usage Examples
536
456
 
537
- ### Example 1: XChaCha20Seal Encrypt and Decrypt (Recommended)
457
+ ### Example 1: Seal with XChaCha20Cipher (recommended)
538
458
 
539
- This is the pattern most users should follow. Bind the key at construction —
540
- nonces are generated internally, no nonce management needed.
459
+ One-shot AEAD. No instantiation, no `dispose()`. The blob format is
460
+ `preamble(20) || ciphertext || tag(16)`.
541
461
 
542
462
  ```typescript
543
- import { init, XChaCha20Seal, randomBytes, utf8ToBytes, bytesToUtf8 } from 'leviathan-crypto'
544
-
545
- // Step 1: Initialize the chacha20 WASM module (once, at application startup)
546
- await init(['chacha20'])
547
-
548
- // Step 2: Generate a 256-bit encryption key
549
- const key = randomBytes(32)
463
+ import { init, Seal, XChaCha20Cipher, utf8ToBytes, bytesToUtf8 } from 'leviathan-crypto'
464
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
465
+ import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
550
466
 
551
- // Step 3: Create the seal instance — key is bound at construction
552
- const seal = new XChaCha20Seal(key)
467
+ await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
553
468
 
554
- // Step 4: Encrypt — nonce is generated internally, no nonce management needed
555
- const plaintext = utf8ToBytes('Hello, world!')
556
- const ciphertext = seal.encrypt(plaintext)
557
- // `ciphertext` = nonce(24) || encrypted data || tag(16)
469
+ const key = XChaCha20Cipher.keygen()
470
+ const blob = Seal.encrypt(XChaCha20Cipher, key, utf8ToBytes('Hello, world!'))
471
+ const pt = Seal.decrypt(XChaCha20Cipher, key, blob) // throws on tamper
558
472
 
559
- // Step 5: Decrypt — nonce is read from the ciphertext automatically
560
- const decrypted = seal.decrypt(ciphertext)
561
- console.log(bytesToUtf8(decrypted)) // "Hello, world!"
473
+ console.log(bytesToUtf8(pt)) // "Hello, world!"
562
474
 
563
- // Step 6: Clean up
564
- seal.dispose()
475
+ // Optional: bind metadata without encrypting it (AAD)
476
+ const aad = utf8ToBytes('document-v2')
477
+ const blob2 = Seal.encrypt(XChaCha20Cipher, key, utf8ToBytes('Hello, world!'), { aad })
478
+ const pt2 = Seal.decrypt(XChaCha20Cipher, key, blob2, { aad })
565
479
  ```
566
480
 
567
- > **Need explicit nonce control?** See Example 2 (`ChaCha20Poly1305`) or use
568
- > `XChaCha20Poly1305` directly — same API shape, 24-byte nonce passed per call.
481
+ > For explicit nonce control, use `XChaCha20Poly1305` directly. Same
482
+ > API shape, 24-byte nonce passed per call. See Example 2.
569
483
 
570
- ### Example 2: ChaCha20Poly1305 — Encrypt and Decrypt
484
+ ### Example 2: ChaCha20Poly1305
571
485
 
572
486
  Same idea as above, but with a 12-byte nonce. Use this if you are implementing
573
487
  a protocol that specifies RFC 8439 ChaCha20-Poly1305 explicitly.
574
488
 
575
- Note the differences from `XChaCha20Poly1305`:
576
- - The nonce is 12 bytes, not 24
577
- - `encrypt()` returns `{ ciphertext, tag }` as separate fields
578
- - `decrypt()` takes the tag as a separate parameter
489
+ Both `ChaCha20Poly1305` and `XChaCha20Poly1305` return a single `Uint8Array`
490
+ with the tag appended, and `decrypt()` expects the same combined format.
491
+ The only difference is the nonce size (12 vs 24 bytes).
492
+
493
+ > [!NOTE]
494
+ > Each `ChaCha20Poly1305` instance allows only one `encrypt()` call. A second
495
+ > call throws to prevent accidental nonce reuse. Create a new instance for each
496
+ > encryption.
579
497
 
580
498
  ```typescript
581
499
  import { init, ChaCha20Poly1305, randomBytes, utf8ToBytes, bytesToUtf8 } from 'leviathan-crypto'
500
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
582
501
 
583
- await init(['chacha20'])
502
+ await init({ chacha20: chacha20Wasm })
584
503
 
585
504
  const key = randomBytes(32)
586
505
  const aead = new ChaCha20Poly1305()
587
506
 
588
507
  // Encrypt
589
- const nonce = randomBytes(12) // 12 bytes be cautious with random generation under high volume
508
+ const nonce = randomBytes(12) // 12 bytes, use XChaCha20Poly1305 for high-volume random generation
590
509
  const plaintext = utf8ToBytes('Sensitive data')
591
- const { ciphertext, tag } = aead.encrypt(key, nonce, plaintext)
592
- // You must store/transmit nonce, ciphertext, AND tag all three are needed to decrypt
510
+ const sealed = aead.encrypt(key, nonce, plaintext)
511
+ // sealed = ciphertext || tag(16), store/transmit nonce and sealed together
593
512
 
594
- // Decrypt
595
- const decrypted = aead.decrypt(key, nonce, ciphertext, tag)
513
+ // Decrypt (new instance, encrypt is single-use)
514
+ const aead2 = new ChaCha20Poly1305()
515
+ const decrypted = aead2.decrypt(key, nonce, sealed)
596
516
  console.log(bytesToUtf8(decrypted)) // "Sensitive data"
597
517
 
598
518
  aead.dispose()
519
+ aead2.dispose()
599
520
  ```
600
521
 
601
522
  ### Example 3: Detecting Tampered Ciphertext
602
523
 
603
524
  AEAD decryption fails loudly if anyone has modified the ciphertext or
604
- the associated data. This is a feature it prevents you from processing
525
+ the associated data. This is a feature. It prevents you from processing
605
526
  corrupted or maliciously altered data.
606
527
 
607
528
  ```typescript
608
- import { init, XChaCha20Seal, randomBytes, utf8ToBytes } from 'leviathan-crypto'
529
+ import { init, Seal, XChaCha20Cipher, randomBytes, utf8ToBytes } from 'leviathan-crypto'
530
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
531
+ import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
609
532
 
610
- await init(['chacha20'])
533
+ await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
611
534
 
612
- const seal = new XChaCha20Seal(randomBytes(32))
613
-
614
- const sealed = seal.encrypt(utf8ToBytes('Original message'))
535
+ const key = XChaCha20Cipher.keygen()
536
+ const sealed = Seal.encrypt(XChaCha20Cipher, key, utf8ToBytes('Original message'))
615
537
 
616
538
  // Simulate tampering: flip one bit in the ciphertext
617
539
  const tampered = new Uint8Array(sealed)
618
- tampered[24] ^= 0x01 // byte 24 is the first ciphertext byte (after the nonce)
540
+ tampered[20] ^= 0x01 // byte 20 is the first ciphertext byte (after the 20-byte preamble)
619
541
 
620
542
  try {
621
- const plaintext = seal.decrypt(tampered)
543
+ const plaintext = Seal.decrypt(XChaCha20Cipher, key, tampered)
622
544
  // This line is never reached
623
545
  console.log(plaintext)
624
546
  } catch (err) {
625
547
  console.error(err.message)
626
548
  // "ChaCha20Poly1305: authentication failed"
627
- // The plaintext is never returned decryption stops immediately on failure.
549
+ // The plaintext is never returned. Decryption stops immediately on failure.
628
550
  }
629
-
630
- seal.dispose()
631
551
  ```
632
552
 
633
553
  ### Example 4: Using Associated Data (AAD)
@@ -637,68 +557,69 @@ not encrypt. Common uses: user IDs, message sequence numbers, protocol version
637
557
  headers, routing information.
638
558
 
639
559
  ```typescript
640
- import { init, XChaCha20Seal, randomBytes, utf8ToBytes, bytesToUtf8 } from 'leviathan-crypto'
560
+ import { init, Seal, XChaCha20Cipher, randomBytes, utf8ToBytes, bytesToUtf8 } from 'leviathan-crypto'
561
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
562
+ import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
641
563
 
642
- await init(['chacha20'])
564
+ await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
643
565
 
644
- const seal = new XChaCha20Seal(randomBytes(32))
566
+ const key = XChaCha20Cipher.keygen()
645
567
 
646
568
  // The user ID travels in the clear, but decryption will fail if anyone changes it
647
569
  const userId = utf8ToBytes('user-12345')
648
570
  const message = utf8ToBytes('Your account balance is $1,000,000')
649
571
 
650
- const sealed = seal.encrypt(message, userId)
572
+ const sealed = Seal.encrypt(XChaCha20Cipher, key, message, { aad: userId })
651
573
 
652
- // Decrypt pass the same AAD
653
- const decrypted = seal.decrypt(sealed, userId)
574
+ // Decrypt, pass the same AAD
575
+ const decrypted = Seal.decrypt(XChaCha20Cipher, key, sealed, { aad: userId })
654
576
  console.log(bytesToUtf8(decrypted))
655
577
  // "Your account balance is $1,000,000"
656
578
 
657
579
  // If someone changes the AAD, decryption fails
658
580
  const wrongUserId = utf8ToBytes('user-99999')
659
581
  try {
660
- seal.decrypt(sealed, wrongUserId)
582
+ Seal.decrypt(XChaCha20Cipher, key, sealed, { aad: wrongUserId })
661
583
  } catch (err) {
662
584
  console.error(err.message)
663
585
  // "ChaCha20Poly1305: authentication failed"
664
586
  // Even though the ciphertext was not modified, the AAD mismatch is detected.
665
587
  }
666
-
667
- seal.dispose()
668
588
  ```
669
589
 
670
590
  ### Example 5: Encrypting and Decrypting Binary Data
671
591
 
672
- The API works with raw bytes not just text. Here is an example encrypting
592
+ The API works with raw bytes, not just text. Here is an example encrypting
673
593
  arbitrary binary content.
674
594
 
675
595
  ```typescript
676
- import { init, XChaCha20Seal, randomBytes } from 'leviathan-crypto'
596
+ import { init, Seal, XChaCha20Cipher, randomBytes } from 'leviathan-crypto'
597
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
598
+ import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
677
599
 
678
- await init(['chacha20'])
600
+ await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
679
601
 
680
- const seal = new XChaCha20Seal(randomBytes(32))
602
+ const key = XChaCha20Cipher.keygen()
681
603
 
682
604
  // Encrypt binary data (e.g., an image thumbnail, a protobuf, a file chunk)
683
605
  const binaryData = new Uint8Array([0x89, 0x50, 0x4e, 0x47, /* ...more bytes... */])
684
- const sealed = seal.encrypt(binaryData)
606
+ const sealed = Seal.encrypt(XChaCha20Cipher, key, binaryData)
685
607
 
686
608
  // Decrypt
687
- const recovered = seal.decrypt(sealed)
609
+ const recovered = Seal.decrypt(XChaCha20Cipher, key, sealed)
688
610
  // `recovered` is byte-identical to `binaryData`
689
-
690
- seal.dispose()
691
611
  ```
692
612
 
693
613
  ### Example 6: Raw ChaCha20 Stream Cipher (Advanced)
694
614
 
695
615
  Use this only if you are building a custom protocol and will add your own
696
- authentication layer. For almost all use cases, use `XChaCha20Seal` instead.
616
+ authentication layer. For almost all use cases, use `Seal` with `XChaCha20Cipher` instead.
697
617
 
698
618
  ```typescript
699
619
  import { init, ChaCha20, randomBytes } from 'leviathan-crypto'
620
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
700
621
 
701
- await init(['chacha20'])
622
+ await init({ chacha20: chacha20Wasm })
702
623
 
703
624
  const key = randomBytes(32)
704
625
  const nonce = randomBytes(12)
@@ -709,7 +630,7 @@ cipher.beginEncrypt(key, nonce)
709
630
  const ct1 = cipher.encryptChunk(new Uint8Array([1, 2, 3, 4]))
710
631
  const ct2 = cipher.encryptChunk(new Uint8Array([5, 6, 7, 8]))
711
632
 
712
- // Decrypt uses the same key and nonce
633
+ // Decrypt, uses the same key and nonce
713
634
  cipher.beginDecrypt(key, nonce)
714
635
  const pt1 = cipher.decryptChunk(ct1)
715
636
  const pt2 = cipher.decryptChunk(ct2)
@@ -722,27 +643,32 @@ const pt2 = cipher.decryptChunk(ct2)
722
643
  cipher.dispose()
723
644
  ```
724
645
 
646
+ ---
647
+
725
648
  ## Error Conditions
726
649
 
727
650
  | Condition | Error Type | Message |
728
651
  |-----------|-----------|---------|
729
- | `init(['chacha20'])` not called before constructing a class | `Error` | `leviathan-crypto: call init(['chacha20']) before using this class` |
652
+ | `init({ chacha20: ... })` not called before constructing a class | `Error` | `leviathan-crypto: call init({ chacha20: ... }) before using this class` |
730
653
  | Key is not 32 bytes | `RangeError` | `ChaCha20 key must be 32 bytes (got N)` / `key must be 32 bytes (got N)` / `Poly1305 key must be 32 bytes (got N)` |
731
654
  | `ChaCha20` nonce is not 12 bytes | `RangeError` | `ChaCha20 nonce must be 12 bytes (got N)` |
732
655
  | `ChaCha20Poly1305` nonce is not 12 bytes | `RangeError` | `nonce must be 12 bytes (got N)` |
733
656
  | `XChaCha20Poly1305` nonce is not 24 bytes | `RangeError` | `XChaCha20 nonce must be 24 bytes (got N)` |
734
- | `ChaCha20Poly1305` tag is not 16 bytes | `RangeError` | `tag must be 16 bytes (got N)` |
657
+ | `ChaCha20Poly1305` ciphertext shorter than 16 bytes | `RangeError` | `ciphertext too short — must include 16-byte tag (got N)` |
735
658
  | `XChaCha20Poly1305` ciphertext shorter than 16 bytes | `RangeError` | `ciphertext too short — must include 16-byte tag (got N)` |
659
+ | `ChaCha20Poly1305.encrypt()` called a second time | `Error` | Single-use encrypt guard. Create a new instance for each encryption. |
736
660
  | Chunk or plaintext exceeds WASM buffer size | `RangeError` | `plaintext exceeds N bytes — split into smaller chunks` / `chunk exceeds maximum size of N bytes — split into smaller chunks` |
737
661
  | Authentication tag does not match on decrypt | `Error` | `ChaCha20Poly1305: authentication failed` |
738
- | Empty plaintext | | Allowed. Encrypting zero bytes produces just a 16-byte tag (AEAD) or zero bytes (raw ChaCha20). |
662
+ | Empty plaintext | | Allowed. Encrypting zero bytes produces just a 16-byte tag (AEAD) or zero bytes (raw ChaCha20). |
739
663
 
740
664
  > ## Cross-References
741
665
  >
742
666
  > - [index](./README.md) — Project Documentation index
667
+ > - [lexicon](./lexicon.md) — Glossary of cryptographic terms
743
668
  > - [asm_chacha](./asm_chacha.md) — WASM (AssemblyScript) implementation details for the chacha20 module
744
- > - [chacha20_pool](./chacha20_pool.md) — `XChaCha20Poly1305Pool` worker-pool wrapper for parallel encryption
745
- > - [serpent](./serpent.md) — alternative: Serpent block cipher modes (CBC, CTR — unauthenticated, needs HMAC pairing)
746
- > - [sha2](./sha2.md) — SHA-2 hashes and HMAC needed for Encrypt-then-MAC if using Serpent or raw ChaCha20
669
+ > - [authenticated encryption](./aead.md) — `Seal`, `SealStream`, `OpenStream`: use `XChaCha20Cipher` as the suite argument
670
+ > - [serpent](./serpent.md) — `SerpentCipher`: alternative `CipherSuite` for `Seal` and streaming
671
+ > - [sha2](./sha2.md) — SHA-2 hashes and HMAC. Needed for Encrypt-then-MAC if using raw ChaCha20
747
672
  > - [types](./types.md) — `AEAD` and `Streamcipher` interfaces implemented by ChaCha20 classes
748
- > - [chacha_audit.md](./chacha_audit.md) — XChaCha20-Poly1305 implementation audit
673
+ > - [architecture](./architecture.md) — architecture overview, module relationships, buffer layouts, and build pipeline
674
+ > - [chacha_audit](./chacha_audit.md) — XChaCha20-Poly1305 implementation audit