leviathan-crypto 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CLAUDE.md +265 -0
  2. package/LICENSE +21 -0
  3. package/README.md +322 -0
  4. package/SECURITY.md +174 -0
  5. package/dist/chacha.wasm +0 -0
  6. package/dist/chacha20/index.d.ts +49 -0
  7. package/dist/chacha20/index.js +177 -0
  8. package/dist/chacha20/ops.d.ts +16 -0
  9. package/dist/chacha20/ops.js +146 -0
  10. package/dist/chacha20/pool.d.ts +52 -0
  11. package/dist/chacha20/pool.js +188 -0
  12. package/dist/chacha20/pool.worker.d.ts +1 -0
  13. package/dist/chacha20/pool.worker.js +37 -0
  14. package/dist/chacha20/types.d.ts +30 -0
  15. package/dist/chacha20/types.js +1 -0
  16. package/dist/docs/architecture.md +795 -0
  17. package/dist/docs/argon2id.md +290 -0
  18. package/dist/docs/chacha20.md +602 -0
  19. package/dist/docs/chacha20_pool.md +306 -0
  20. package/dist/docs/fortuna.md +322 -0
  21. package/dist/docs/init.md +308 -0
  22. package/dist/docs/loader.md +206 -0
  23. package/dist/docs/serpent.md +914 -0
  24. package/dist/docs/sha2.md +620 -0
  25. package/dist/docs/sha3.md +509 -0
  26. package/dist/docs/types.md +198 -0
  27. package/dist/docs/utils.md +273 -0
  28. package/dist/docs/wasm.md +193 -0
  29. package/dist/embedded/chacha.d.ts +1 -0
  30. package/dist/embedded/chacha.js +2 -0
  31. package/dist/embedded/serpent.d.ts +1 -0
  32. package/dist/embedded/serpent.js +2 -0
  33. package/dist/embedded/sha2.d.ts +1 -0
  34. package/dist/embedded/sha2.js +2 -0
  35. package/dist/embedded/sha3.d.ts +1 -0
  36. package/dist/embedded/sha3.js +2 -0
  37. package/dist/fortuna.d.ts +72 -0
  38. package/dist/fortuna.js +445 -0
  39. package/dist/index.d.ts +13 -0
  40. package/dist/index.js +44 -0
  41. package/dist/init.d.ts +11 -0
  42. package/dist/init.js +49 -0
  43. package/dist/loader.d.ts +4 -0
  44. package/dist/loader.js +30 -0
  45. package/dist/serpent/index.d.ts +65 -0
  46. package/dist/serpent/index.js +242 -0
  47. package/dist/serpent/seal.d.ts +8 -0
  48. package/dist/serpent/seal.js +70 -0
  49. package/dist/serpent/stream-encoder.d.ts +20 -0
  50. package/dist/serpent/stream-encoder.js +167 -0
  51. package/dist/serpent/stream-pool.d.ts +48 -0
  52. package/dist/serpent/stream-pool.js +285 -0
  53. package/dist/serpent/stream-sealer.d.ts +34 -0
  54. package/dist/serpent/stream-sealer.js +223 -0
  55. package/dist/serpent/stream.d.ts +28 -0
  56. package/dist/serpent/stream.js +205 -0
  57. package/dist/serpent/stream.worker.d.ts +32 -0
  58. package/dist/serpent/stream.worker.js +117 -0
  59. package/dist/serpent/types.d.ts +5 -0
  60. package/dist/serpent/types.js +1 -0
  61. package/dist/serpent.wasm +0 -0
  62. package/dist/sha2/hkdf.d.ts +16 -0
  63. package/dist/sha2/hkdf.js +108 -0
  64. package/dist/sha2/index.d.ts +40 -0
  65. package/dist/sha2/index.js +190 -0
  66. package/dist/sha2/types.d.ts +5 -0
  67. package/dist/sha2/types.js +1 -0
  68. package/dist/sha2.wasm +0 -0
  69. package/dist/sha3/index.d.ts +55 -0
  70. package/dist/sha3/index.js +246 -0
  71. package/dist/sha3/types.d.ts +5 -0
  72. package/dist/sha3/types.js +1 -0
  73. package/dist/sha3.wasm +0 -0
  74. package/dist/types.d.ts +24 -0
  75. package/dist/types.js +26 -0
  76. package/dist/utils.d.ts +26 -0
  77. package/dist/utils.js +169 -0
  78. package/package.json +90 -0
@@ -0,0 +1,602 @@
1
+ # ChaCha20, Poly1305, and AEAD TypeScript API
2
+
3
+ ## Overview
4
+
5
+ **ChaCha20** is a modern stream cipher designed by Daniel J. Bernstein. It is fast
6
+ on all platforms (including those without hardware AES), resistant to timing attacks
7
+ by design, and widely deployed in TLS, SSH, and WireGuard. ChaCha20 encrypts data
8
+ by generating a pseudorandom keystream from a 256-bit key and a nonce, then XORing
9
+ it with the plaintext. It does **not** provide authentication on its own — a
10
+ modified message will decrypt to garbage with no warning.
11
+
12
+ **Poly1305** is a one-time message authentication code (MAC). Given a unique 256-bit
13
+ key and a message, it produces a 16-byte tag that proves the message has not been
14
+ tampered with. The critical requirement is that each Poly1305 key is used **exactly
15
+ once** — reusing a key completely breaks its security. You almost never need to use
16
+ Poly1305 directly; the AEAD constructions below handle key derivation for you.
17
+
18
+ **ChaCha20-Poly1305** (RFC 8439) combines both primitives into an AEAD
19
+ (Authenticated Encryption with Associated Data). It encrypts your data and
20
+ produces an authentication tag in a single operation. On decryption, it verifies
21
+ the tag before returning any plaintext — if someone tampered with the ciphertext,
22
+ you get an error instead of corrupted data. The nonce is 96 bits (12 bytes).
23
+
24
+ **XChaCha20-Poly1305** extends the nonce to 192 bits (24 bytes) using the HChaCha20
25
+ subkey derivation step. This makes random nonce generation completely safe — with a
26
+ 24-byte nonce, the probability of a collision is negligible even after billions of
27
+ messages. **For most users, XChaCha20Poly1305 is the recommended choice.** It gives
28
+ you encryption and authentication in one call, with a nonce size that eliminates
29
+ the most common footgun (accidental nonce reuse).
30
+
31
+ ## Security Notes
32
+
33
+ > [!IMPORTANT]
34
+ > Read this section before writing any code. These are not theoretical concerns —
35
+ > they are the mistakes that cause real-world breaches.
36
+
37
+ - **Use `XChaCha20Poly1305` unless you have a specific reason not to.** It is the
38
+ safest default: authenticated encryption with a nonce large enough for random
39
+ generation. If you are unsure which class to pick, pick this one.
40
+
41
+ - **Never reuse a nonce with the same key.** This is the single most important
42
+ rule. If you encrypt two different messages with the same key and the same nonce,
43
+ an attacker can XOR the two ciphertexts together and recover both plaintexts.
44
+ With `ChaCha20Poly1305` (12-byte nonce), random generation has a meaningful
45
+ collision risk after roughly 2^32 messages under one key. With
46
+ `XChaCha20Poly1305` (24-byte nonce), random generation is safe for any practical
47
+ message count — just call `randomBytes(24)` for each message.
48
+
49
+ - **Poly1305 keys are single-use.** Each Poly1305 key must be used to authenticate
50
+ exactly one message. The AEAD classes (`ChaCha20Poly1305` and
51
+ `XChaCha20Poly1305`) handle this automatically by deriving a fresh Poly1305 key
52
+ from the ChaCha20 keystream for each encryption. If you use the standalone
53
+ `Poly1305` class directly, it is your responsibility to never reuse a key.
54
+
55
+ - **AEAD protects both confidentiality and authenticity.** If authentication fails
56
+ during decryption, the plaintext is never returned — you get an error. This is
57
+ intentional. Do not try to work around it. If decryption fails, the ciphertext
58
+ was corrupted or tampered with.
59
+
60
+ - **Associated data (AAD) is authenticated but not encrypted.** Use AAD for data
61
+ that must travel in the clear (headers, routing metadata, user IDs) but must be
62
+ verified as unmodified. If someone changes the AAD, decryption will fail — even
63
+ if the ciphertext itself is untouched.
64
+
65
+ - **Always call `dispose()` when you are done.** This wipes key material and
66
+ intermediate state from WASM memory. Failing to call `dispose()` leaves
67
+ sensitive data in memory longer than necessary.
68
+
69
+ - **`ChaCha20` alone has no authentication.** If you use the raw `ChaCha20` class
70
+ without pairing it with a MAC, an attacker can flip bits in the ciphertext and
71
+ the corresponding bits in the plaintext will flip silently. Unless you are
72
+ building your own authenticated construction (and you probably should not be),
73
+ use one of the AEAD classes instead.
74
+
75
+ ## Module Init
76
+
77
+ Each module subpath exports its own init function for consumers who want
78
+ tree-shakeable imports.
79
+
80
+ ### `chacha20Init(mode?, opts?)`
81
+
82
+ Initializes only the chacha20 WASM binary. Equivalent to calling the
83
+ root `init(['chacha20'], mode, opts)` but without pulling the other three
84
+ modules into the bundle.
85
+
86
+ **Signature:**
87
+
88
+ ```typescript
89
+ async function chacha20Init(mode?: Mode, opts?: InitOpts): Promise<void>
90
+ ```
91
+
92
+ **Usage:**
93
+
94
+ ```typescript
95
+ import { chacha20Init, XChaCha20Poly1305 } from 'leviathan-crypto/chacha20'
96
+
97
+ await chacha20Init()
98
+ const aead = new XChaCha20Poly1305()
99
+ ```
100
+
101
+ ---
102
+
103
+ ## API Reference
104
+
105
+ All classes require calling `await init(['chacha20'])` or the subpath `chacha20Init()`
106
+ before construction. If you construct a class before initialization, it throws:
107
+ ```
108
+ Error: leviathan-crypto: call init(['chacha20']) before using this class
109
+ ```
110
+
111
+ ---
112
+
113
+ ### `ChaCha20`
114
+
115
+ Raw ChaCha20 stream cipher. **No authentication** — use `XChaCha20Poly1305` instead
116
+ unless you are building a custom protocol and understand the risks.
117
+
118
+ #### Constructor
119
+
120
+ ```typescript
121
+ new ChaCha20()
122
+ ```
123
+
124
+ Throws if `init(['chacha20'])` has not been called.
125
+
126
+ ---
127
+
128
+ #### `beginEncrypt(key: Uint8Array, nonce: Uint8Array): void`
129
+
130
+ Prepares the cipher for encryption with the given key and nonce.
131
+
132
+ | Parameter | Type | Description |
133
+ |-----------|------|-------------|
134
+ | `key` | `Uint8Array` | 32 bytes (256 bits) |
135
+ | `nonce` | `Uint8Array` | 12 bytes (96 bits) |
136
+
137
+ **Throws** `RangeError` if `key` is not 32 bytes or `nonce` is not 12 bytes.
138
+
139
+ ---
140
+
141
+ #### `encryptChunk(chunk: Uint8Array): Uint8Array`
142
+
143
+ Encrypts a chunk of plaintext. Call repeatedly for streaming encryption. Returns
144
+ a new `Uint8Array` containing the ciphertext (same length as input).
145
+
146
+ | Parameter | Type | Description |
147
+ |-----------|------|-------------|
148
+ | `chunk` | `Uint8Array` | Plaintext bytes (up to the module's chunk size limit) |
149
+
150
+ **Throws** `RangeError` if the chunk exceeds the maximum chunk size.
151
+
152
+ ---
153
+
154
+ #### `beginDecrypt(key: Uint8Array, nonce: Uint8Array): void`
155
+
156
+ Prepares the cipher for decryption. Identical to `beginEncrypt` — ChaCha20 is
157
+ symmetric (encryption and decryption are the same XOR operation).
158
+
159
+ | Parameter | Type | Description |
160
+ |-----------|------|-------------|
161
+ | `key` | `Uint8Array` | 32 bytes (256 bits) |
162
+ | `nonce` | `Uint8Array` | 12 bytes (96 bits) |
163
+
164
+ **Throws** `RangeError` if `key` is not 32 bytes or `nonce` is not 12 bytes.
165
+
166
+ ---
167
+
168
+ #### `decryptChunk(chunk: Uint8Array): Uint8Array`
169
+
170
+ Decrypts a chunk of ciphertext. Returns a new `Uint8Array` containing the
171
+ plaintext (same length as input).
172
+
173
+ | Parameter | Type | Description |
174
+ |-----------|------|-------------|
175
+ | `chunk` | `Uint8Array` | Ciphertext bytes |
176
+
177
+ **Throws** `RangeError` if the chunk exceeds the maximum chunk size.
178
+
179
+ ---
180
+
181
+ #### `dispose(): void`
182
+
183
+ Wipes all key material and intermediate state from WASM memory. Always call this
184
+ when you are done with the instance.
185
+
186
+ ---
187
+
188
+ ### `Poly1305`
189
+
190
+ Standalone Poly1305 one-time MAC. **Each key must be used exactly once.** You
191
+ almost certainly want `ChaCha20Poly1305` or `XChaCha20Poly1305` instead — they
192
+ handle Poly1305 key derivation automatically.
193
+
194
+ #### Constructor
195
+
196
+ ```typescript
197
+ new Poly1305()
198
+ ```
199
+
200
+ Throws if `init(['chacha20'])` has not been called.
201
+
202
+ ---
203
+
204
+ #### `mac(key: Uint8Array, msg: Uint8Array): Uint8Array`
205
+
206
+ Computes a 16-byte Poly1305 authentication tag over the given message.
207
+
208
+ | Parameter | Type | Description |
209
+ |-----------|------|-------------|
210
+ | `key` | `Uint8Array` | 32 bytes — must be unique per message |
211
+ | `msg` | `Uint8Array` | The message to authenticate (any length) |
212
+
213
+ **Returns** `Uint8Array` — a 16-byte authentication tag.
214
+
215
+ **Throws** `RangeError` if `key` is not 32 bytes.
216
+
217
+ ---
218
+
219
+ #### `dispose(): void`
220
+
221
+ Wipes all key material and intermediate state from WASM memory.
222
+
223
+ ---
224
+
225
+ ### `ChaCha20Poly1305`
226
+
227
+ ChaCha20-Poly1305 AEAD as specified in RFC 8439. Provides authenticated encryption
228
+ with a 12-byte (96-bit) nonce. The Poly1305 one-time key is derived automatically
229
+ from the ChaCha20 keystream (counter 0).
230
+
231
+ If you are generating nonces randomly, prefer `XChaCha20Poly1305` (24-byte nonce)
232
+ to avoid collision risk.
233
+
234
+ #### Constructor
235
+
236
+ ```typescript
237
+ new ChaCha20Poly1305()
238
+ ```
239
+
240
+ Throws if `init(['chacha20'])` has not been called.
241
+
242
+ ---
243
+
244
+ #### `encrypt(key, nonce, plaintext, aad?): { ciphertext: Uint8Array; tag: Uint8Array }`
245
+
246
+ Encrypts plaintext and produces a 16-byte authentication tag.
247
+
248
+ | Parameter | Type | Default | Description |
249
+ |-----------|------|---------|-------------|
250
+ | `key` | `Uint8Array` | | 32 bytes (256-bit key) |
251
+ | `nonce` | `Uint8Array` | | 12 bytes (96-bit nonce) |
252
+ | `plaintext` | `Uint8Array` | | Data to encrypt (up to the module's chunk size limit) |
253
+ | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data — authenticated but not encrypted |
254
+
255
+ **Returns** `{ ciphertext: Uint8Array; tag: Uint8Array }` — the ciphertext (same
256
+ length as plaintext) and a 16-byte authentication tag. You need both to decrypt.
257
+
258
+ **Throws:**
259
+ - `RangeError` if `key` is not 32 bytes
260
+ - `RangeError` if `nonce` is not 12 bytes
261
+ - `RangeError` if `plaintext` exceeds the maximum chunk size
262
+
263
+ ---
264
+
265
+ #### `decrypt(key, nonce, ciphertext, tag, aad?): Uint8Array`
266
+
267
+ Verifies the authentication tag and decrypts the ciphertext. If authentication
268
+ fails, an error is thrown and no plaintext is returned.
269
+
270
+ Tag comparison uses a constant-time XOR-accumulate pattern — no timing side
271
+ channel leaks whether the tag was "close" to correct.
272
+
273
+ | Parameter | Type | Default | Description |
274
+ |-----------|------|---------|-------------|
275
+ | `key` | `Uint8Array` | | 32 bytes (same key used for encryption) |
276
+ | `nonce` | `Uint8Array` | | 12 bytes (same nonce used for encryption) |
277
+ | `ciphertext` | `Uint8Array` | | Encrypted data |
278
+ | `tag` | `Uint8Array` | | 16-byte authentication tag from `encrypt()` |
279
+ | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data (must match what was passed to `encrypt()`) |
280
+
281
+ **Returns** `Uint8Array` — the decrypted plaintext.
282
+
283
+ **Throws:**
284
+ - `RangeError` if `key` is not 32 bytes
285
+ - `RangeError` if `nonce` is not 12 bytes
286
+ - `RangeError` if `tag` is not 16 bytes
287
+ - `RangeError` if `ciphertext` exceeds the maximum chunk size
288
+ - `Error('ChaCha20Poly1305: authentication failed')` if the tag does not match
289
+
290
+ ---
291
+
292
+ #### `dispose(): void`
293
+
294
+ Wipes all key material and intermediate state from WASM memory.
295
+
296
+ ---
297
+
298
+ ### `XChaCha20Poly1305`
299
+
300
+ XChaCha20-Poly1305 AEAD (draft-irtf-cfrg-xchacha). **This is the recommended
301
+ authenticated encryption class for most use cases.**
302
+
303
+ It uses a 24-byte (192-bit) nonce, which is large enough that randomly generated
304
+ nonces will never collide in practice. Internally, it derives a subkey via
305
+ HChaCha20 and delegates to `ChaCha20Poly1305`.
306
+
307
+ Unlike `ChaCha20Poly1305`, the `encrypt()` method returns a single `Uint8Array`
308
+ with the tag appended to the ciphertext. The `decrypt()` method expects this
309
+ combined format and splits it internally.
310
+
311
+ #### Constructor
312
+
313
+ ```typescript
314
+ new XChaCha20Poly1305()
315
+ ```
316
+
317
+ Throws if `init(['chacha20'])` has not been called.
318
+
319
+ ---
320
+
321
+ #### `encrypt(key, nonce, plaintext, aad?): Uint8Array`
322
+
323
+ Encrypts plaintext and returns the ciphertext with the 16-byte tag appended.
324
+
325
+ | Parameter | Type | Default | Description |
326
+ |-----------|------|---------|-------------|
327
+ | `key` | `Uint8Array` | | 32 bytes (256-bit key) |
328
+ | `nonce` | `Uint8Array` | | 24 bytes (192-bit nonce) |
329
+ | `plaintext` | `Uint8Array` | | Data to encrypt |
330
+ | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data — authenticated but not encrypted |
331
+
332
+ **Returns** `Uint8Array` — ciphertext + 16-byte tag (length = plaintext.length + 16).
333
+
334
+ **Throws:**
335
+ - `RangeError` if `key` is not 32 bytes
336
+ - `RangeError` if `nonce` is not 24 bytes
337
+
338
+ ---
339
+
340
+ #### `decrypt(key, nonce, ciphertext, aad?): Uint8Array`
341
+
342
+ Verifies the authentication tag and decrypts the ciphertext. The `ciphertext`
343
+ parameter must include the appended 16-byte tag (i.e., the exact output of
344
+ `encrypt()`).
345
+
346
+ | Parameter | Type | Default | Description |
347
+ |-----------|------|---------|-------------|
348
+ | `key` | `Uint8Array` | | 32 bytes (same key used for encryption) |
349
+ | `nonce` | `Uint8Array` | | 24 bytes (same nonce used for encryption) |
350
+ | `ciphertext` | `Uint8Array` | | Encrypted data with appended tag (output of `encrypt()`) |
351
+ | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data (must match what was passed to `encrypt()`) |
352
+
353
+ **Returns** `Uint8Array` — the decrypted plaintext.
354
+
355
+ **Throws:**
356
+ - `RangeError` if `key` is not 32 bytes
357
+ - `RangeError` if `nonce` is not 24 bytes
358
+ - `RangeError` if `ciphertext` is shorter than 16 bytes (no room for a tag)
359
+ - `Error('ChaCha20Poly1305: authentication failed')` if the tag does not match
360
+
361
+ ---
362
+
363
+ #### `dispose(): void`
364
+
365
+ Wipes all key material and intermediate state from WASM memory.
366
+
367
+ ---
368
+
369
+ ## Parallel pool — `XChaCha20Poly1305Pool`
370
+
371
+ For high-throughput workloads where multiple XChaCha20-Poly1305 operations should
372
+ run concurrently, `XChaCha20Poly1305Pool` dispatches work across a configurable
373
+ number of Web Workers, each holding an isolated `chacha.wasm` instance in its own
374
+ linear memory. This removes the single-threaded bottleneck of a shared WASM
375
+ instance and allows encryption and decryption operations to proceed in parallel.
376
+ The pool requires `init(['chacha20'])` to be called before construction and is
377
+ created via the static factory `XChaCha20Poly1305Pool.create(opts?)` — see
378
+ [chacha20_pool.md](./chacha20_pool.md) for the full API reference, pool sizing
379
+ guidance, and lifecycle docs.
380
+
381
+ ---
382
+
383
+ ## Usage Examples
384
+
385
+ ### Example 1: XChaCha20Poly1305 — Encrypt and Decrypt (Recommended)
386
+
387
+ This is the pattern most users should follow. Generate a random 24-byte nonce for
388
+ each message — no counter management, no collision risk.
389
+
390
+ ```typescript
391
+ import { init, XChaCha20Poly1305, randomBytes, utf8ToBytes, bytesToUtf8 } from 'leviathan-crypto'
392
+
393
+ // Step 1: Initialize the chacha20 WASM module (once, at application startup)
394
+ await init(['chacha20'])
395
+
396
+ // Step 2: Generate a 256-bit encryption key
397
+ // In a real application, this comes from a key derivation function or key exchange.
398
+ const key = randomBytes(32)
399
+
400
+ // Step 3: Create the AEAD instance
401
+ const aead = new XChaCha20Poly1305()
402
+
403
+ // Step 4: Encrypt
404
+ const nonce = randomBytes(24) // Safe to generate randomly — 24 bytes means no collision risk
405
+ const plaintext = utf8ToBytes('Hello, world!')
406
+ const sealed = aead.encrypt(key, nonce, plaintext)
407
+ // `sealed` contains the ciphertext + 16-byte authentication tag
408
+
409
+ // Step 5: Decrypt
410
+ // You need the same key and nonce to decrypt.
411
+ // Store or transmit the nonce alongside the ciphertext — the nonce is not secret.
412
+ const decrypted = aead.decrypt(key, nonce, sealed)
413
+ console.log(bytesToUtf8(decrypted)) // "Hello, world!"
414
+
415
+ // Step 6: Clean up — wipe key material from WASM memory
416
+ aead.dispose()
417
+ ```
418
+
419
+ ### Example 2: ChaCha20Poly1305 — Encrypt and Decrypt
420
+
421
+ Same idea as above, but with a 12-byte nonce. Use this if you are implementing
422
+ a protocol that specifies RFC 8439 ChaCha20-Poly1305 explicitly.
423
+
424
+ Note the differences from `XChaCha20Poly1305`:
425
+ - The nonce is 12 bytes, not 24
426
+ - `encrypt()` returns `{ ciphertext, tag }` as separate fields
427
+ - `decrypt()` takes the tag as a separate parameter
428
+
429
+ ```typescript
430
+ import { init, ChaCha20Poly1305, randomBytes, utf8ToBytes, bytesToUtf8 } from 'leviathan-crypto'
431
+
432
+ await init(['chacha20'])
433
+
434
+ const key = randomBytes(32)
435
+ const aead = new ChaCha20Poly1305()
436
+
437
+ // Encrypt
438
+ const nonce = randomBytes(12) // 12 bytes — be cautious with random generation under high volume
439
+ const plaintext = utf8ToBytes('Sensitive data')
440
+ const { ciphertext, tag } = aead.encrypt(key, nonce, plaintext)
441
+ // You must store/transmit nonce, ciphertext, AND tag — all three are needed to decrypt
442
+
443
+ // Decrypt
444
+ const decrypted = aead.decrypt(key, nonce, ciphertext, tag)
445
+ console.log(bytesToUtf8(decrypted)) // "Sensitive data"
446
+
447
+ aead.dispose()
448
+ ```
449
+
450
+ ### Example 3: Detecting Tampered Ciphertext
451
+
452
+ AEAD decryption fails loudly if anyone has modified the ciphertext, the tag, or
453
+ the associated data. This is a feature — it prevents you from processing corrupted
454
+ or maliciously altered data.
455
+
456
+ ```typescript
457
+ import { init, XChaCha20Poly1305, randomBytes, utf8ToBytes } from 'leviathan-crypto'
458
+
459
+ await init(['chacha20'])
460
+
461
+ const key = randomBytes(32)
462
+ const nonce = randomBytes(24)
463
+ const aead = new XChaCha20Poly1305()
464
+
465
+ const sealed = aead.encrypt(key, nonce, utf8ToBytes('Original message'))
466
+
467
+ // Simulate tampering: flip one bit in the ciphertext
468
+ const tampered = new Uint8Array(sealed)
469
+ tampered[0] ^= 0x01
470
+
471
+ try {
472
+ const plaintext = aead.decrypt(key, nonce, tampered)
473
+ // This line is never reached
474
+ console.log(plaintext)
475
+ } catch (err) {
476
+ console.error(err.message)
477
+ // "ChaCha20Poly1305: authentication failed"
478
+ // The plaintext is never returned — decryption stops immediately on failure.
479
+ }
480
+
481
+ aead.dispose()
482
+ ```
483
+
484
+ ### Example 4: Using Associated Data (AAD)
485
+
486
+ Associated data is metadata that you want to authenticate (prove unmodified) but
487
+ not encrypt. Common uses: user IDs, message sequence numbers, protocol version
488
+ headers, routing information.
489
+
490
+ ```typescript
491
+ import { init, XChaCha20Poly1305, randomBytes, utf8ToBytes, bytesToUtf8 } from 'leviathan-crypto'
492
+
493
+ await init(['chacha20'])
494
+
495
+ const key = randomBytes(32)
496
+ const nonce = randomBytes(24)
497
+ const aead = new XChaCha20Poly1305()
498
+
499
+ // The user ID travels in the clear, but decryption will fail if anyone changes it
500
+ const userId = utf8ToBytes('user-12345')
501
+ const message = utf8ToBytes('Your account balance is $1,000,000')
502
+
503
+ const sealed = aead.encrypt(key, nonce, message, userId)
504
+
505
+ // Decrypt — pass the same AAD
506
+ const decrypted = aead.decrypt(key, nonce, sealed, userId)
507
+ console.log(bytesToUtf8(decrypted))
508
+ // "Your account balance is $1,000,000"
509
+
510
+ // If someone changes the AAD, decryption fails
511
+ const wrongUserId = utf8ToBytes('user-99999')
512
+ try {
513
+ aead.decrypt(key, nonce, sealed, wrongUserId)
514
+ } catch (err) {
515
+ console.error(err.message)
516
+ // "ChaCha20Poly1305: authentication failed"
517
+ // Even though the ciphertext was not modified, the AAD mismatch is detected.
518
+ }
519
+
520
+ aead.dispose()
521
+ ```
522
+
523
+ ### Example 5: Encrypting and Decrypting Binary Data
524
+
525
+ The API works with raw bytes — not just text. Here is an example encrypting
526
+ arbitrary binary content.
527
+
528
+ ```typescript
529
+ import { init, XChaCha20Poly1305, randomBytes } from 'leviathan-crypto'
530
+
531
+ await init(['chacha20'])
532
+
533
+ const key = randomBytes(32)
534
+ const nonce = randomBytes(24)
535
+ const aead = new XChaCha20Poly1305()
536
+
537
+ // Encrypt binary data (e.g., an image thumbnail, a protobuf, a file chunk)
538
+ const binaryData = new Uint8Array([0x89, 0x50, 0x4e, 0x47, /* ...more bytes... */])
539
+ const sealed = aead.encrypt(key, nonce, binaryData)
540
+
541
+ // Decrypt
542
+ const recovered = aead.decrypt(key, nonce, sealed)
543
+ // `recovered` is byte-identical to `binaryData`
544
+
545
+ aead.dispose()
546
+ ```
547
+
548
+ ### Example 6: Raw ChaCha20 Stream Cipher (Advanced)
549
+
550
+ Use this only if you are building a custom protocol and will add your own
551
+ authentication layer. For almost all use cases, use `XChaCha20Poly1305` instead.
552
+
553
+ ```typescript
554
+ import { init, ChaCha20, randomBytes } from 'leviathan-crypto'
555
+
556
+ await init(['chacha20'])
557
+
558
+ const key = randomBytes(32)
559
+ const nonce = randomBytes(12)
560
+ const cipher = new ChaCha20()
561
+
562
+ // Encrypt
563
+ cipher.beginEncrypt(key, nonce)
564
+ const ct1 = cipher.encryptChunk(new Uint8Array([1, 2, 3, 4]))
565
+ const ct2 = cipher.encryptChunk(new Uint8Array([5, 6, 7, 8]))
566
+
567
+ // Decrypt — uses the same key and nonce
568
+ cipher.beginDecrypt(key, nonce)
569
+ const pt1 = cipher.decryptChunk(ct1)
570
+ const pt2 = cipher.decryptChunk(ct2)
571
+ // pt1 = [1, 2, 3, 4], pt2 = [5, 6, 7, 8]
572
+
573
+ // WARNING: Without authentication, an attacker can flip bits in ciphertext
574
+ // and the corresponding plaintext bits will flip with no error.
575
+ // Pair with HMAC (Encrypt-then-MAC) or use XChaCha20Poly1305 instead.
576
+
577
+ cipher.dispose()
578
+ ```
579
+
580
+ ## Error Conditions
581
+
582
+ | Condition | Error Type | Message |
583
+ |-----------|-----------|---------|
584
+ | `init(['chacha20'])` not called before constructing a class | `Error` | `leviathan-crypto: call init(['chacha20']) before using this class` |
585
+ | 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)` |
586
+ | `ChaCha20` nonce is not 12 bytes | `RangeError` | `ChaCha20 nonce must be 12 bytes (got N)` |
587
+ | `ChaCha20Poly1305` nonce is not 12 bytes | `RangeError` | `nonce must be 12 bytes (got N)` |
588
+ | `XChaCha20Poly1305` nonce is not 24 bytes | `RangeError` | `XChaCha20 nonce must be 24 bytes (got N)` |
589
+ | `ChaCha20Poly1305` tag is not 16 bytes | `RangeError` | `tag must be 16 bytes (got N)` |
590
+ | `XChaCha20Poly1305` ciphertext shorter than 16 bytes | `RangeError` | `ciphertext too short — must include 16-byte tag (got N)` |
591
+ | 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` |
592
+ | Authentication tag does not match on decrypt | `Error` | `ChaCha20Poly1305: authentication failed` |
593
+ | Empty plaintext | — | Allowed. Encrypting zero bytes produces just a 16-byte tag (AEAD) or zero bytes (raw ChaCha20). |
594
+
595
+ ## Cross-References
596
+
597
+ - [README.md](./README.md) — library documentation index and exports table
598
+ - [asm_chacha.md](./asm_chacha.md) — WASM (AssemblyScript) implementation details for the chacha20 module
599
+ - [chacha20_pool.md](./chacha20_pool.md) — `XChaCha20Poly1305Pool` worker-pool wrapper for parallel encryption
600
+ - [serpent.md](./serpent.md) — alternative: Serpent block cipher modes (CBC, CTR — unauthenticated, needs HMAC pairing)
601
+ - [sha2.md](./sha2.md) — SHA-2 hashes and HMAC — needed for Encrypt-then-MAC if using Serpent or raw ChaCha20
602
+ - [types.md](./types.md) — `AEAD` and `Streamcipher` interfaces implemented by ChaCha20 classes