leviathan-crypto 1.3.1 → 2.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 (124) hide show
  1. package/CLAUDE.md +129 -76
  2. package/README.md +166 -221
  3. package/SECURITY.md +89 -37
  4. package/dist/chacha20/cipher-suite.d.ts +4 -0
  5. package/dist/chacha20/cipher-suite.js +78 -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 -7
  9. package/dist/chacha20/index.js +41 -14
  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 +320 -0
  17. package/dist/docs/architecture.md +419 -285
  18. package/dist/docs/argon2id.md +42 -30
  19. package/dist/docs/chacha20.md +218 -150
  20. package/dist/docs/exports.md +241 -0
  21. package/dist/docs/fortuna.md +65 -74
  22. package/dist/docs/init.md +172 -178
  23. package/dist/docs/loader.md +87 -132
  24. package/dist/docs/serpent.md +134 -565
  25. package/dist/docs/sha2.md +91 -103
  26. package/dist/docs/sha3.md +70 -36
  27. package/dist/docs/types.md +93 -16
  28. package/dist/docs/utils.md +114 -41
  29. package/dist/embedded/chacha20.d.ts +1 -1
  30. package/dist/embedded/chacha20.js +2 -1
  31. package/dist/embedded/kyber.d.ts +1 -0
  32. package/dist/embedded/kyber.js +3 -0
  33. package/dist/embedded/serpent.d.ts +1 -1
  34. package/dist/embedded/serpent.js +2 -1
  35. package/dist/embedded/sha2.d.ts +1 -1
  36. package/dist/embedded/sha2.js +2 -1
  37. package/dist/embedded/sha3.d.ts +1 -1
  38. package/dist/embedded/sha3.js +2 -1
  39. package/dist/errors.d.ts +10 -0
  40. package/dist/{serpent/seal.js → errors.js} +14 -46
  41. package/dist/fortuna.d.ts +2 -8
  42. package/dist/fortuna.js +11 -9
  43. package/dist/index.d.ts +25 -9
  44. package/dist/index.js +36 -7
  45. package/dist/init.d.ts +3 -7
  46. package/dist/init.js +18 -35
  47. package/dist/keccak/embedded.d.ts +1 -0
  48. package/dist/keccak/embedded.js +27 -0
  49. package/dist/keccak/index.d.ts +4 -0
  50. package/dist/keccak/index.js +31 -0
  51. package/dist/kyber/embedded.d.ts +1 -0
  52. package/dist/kyber/embedded.js +27 -0
  53. package/dist/kyber/indcpa.d.ts +49 -0
  54. package/dist/kyber/indcpa.js +352 -0
  55. package/dist/kyber/index.d.ts +38 -0
  56. package/dist/kyber/index.js +150 -0
  57. package/dist/kyber/kem.d.ts +21 -0
  58. package/dist/kyber/kem.js +160 -0
  59. package/dist/kyber/params.d.ts +14 -0
  60. package/dist/kyber/params.js +37 -0
  61. package/dist/kyber/suite.d.ts +13 -0
  62. package/dist/kyber/suite.js +93 -0
  63. package/dist/kyber/types.d.ts +98 -0
  64. package/dist/kyber/types.js +25 -0
  65. package/dist/kyber/validate.d.ts +19 -0
  66. package/dist/kyber/validate.js +68 -0
  67. package/dist/kyber.wasm +0 -0
  68. package/dist/loader.d.ts +19 -4
  69. package/dist/loader.js +91 -25
  70. package/dist/serpent/cipher-suite.d.ts +4 -0
  71. package/dist/serpent/cipher-suite.js +121 -0
  72. package/dist/serpent/embedded.d.ts +1 -0
  73. package/dist/serpent/embedded.js +27 -0
  74. package/dist/serpent/index.d.ts +6 -37
  75. package/dist/serpent/index.js +9 -118
  76. package/dist/serpent/pool-worker.d.ts +1 -0
  77. package/dist/serpent/pool-worker.js +202 -0
  78. package/dist/serpent/serpent-cbc.d.ts +30 -0
  79. package/dist/serpent/serpent-cbc.js +136 -0
  80. package/dist/sha2/embedded.d.ts +1 -0
  81. package/dist/sha2/embedded.js +27 -0
  82. package/dist/sha2/hkdf.js +6 -2
  83. package/dist/sha2/index.d.ts +3 -2
  84. package/dist/sha2/index.js +3 -4
  85. package/dist/sha3/embedded.d.ts +1 -0
  86. package/dist/sha3/embedded.js +27 -0
  87. package/dist/sha3/index.d.ts +3 -2
  88. package/dist/sha3/index.js +3 -4
  89. package/dist/stream/constants.d.ts +6 -0
  90. package/dist/stream/constants.js +30 -0
  91. package/dist/stream/header.d.ts +9 -0
  92. package/dist/stream/header.js +77 -0
  93. package/dist/stream/index.d.ts +7 -0
  94. package/dist/stream/index.js +27 -0
  95. package/dist/stream/open-stream.d.ts +21 -0
  96. package/dist/stream/open-stream.js +146 -0
  97. package/dist/stream/seal-stream-pool.d.ts +38 -0
  98. package/dist/stream/seal-stream-pool.js +391 -0
  99. package/dist/stream/seal-stream.d.ts +20 -0
  100. package/dist/stream/seal-stream.js +142 -0
  101. package/dist/stream/seal.d.ts +9 -0
  102. package/dist/stream/seal.js +75 -0
  103. package/dist/stream/types.d.ts +24 -0
  104. package/dist/stream/types.js +26 -0
  105. package/dist/utils.d.ts +12 -7
  106. package/dist/utils.js +75 -19
  107. package/dist/wasm-source.d.ts +12 -0
  108. package/dist/wasm-source.js +26 -0
  109. package/package.json +13 -5
  110. package/dist/chacha20/pool.d.ts +0 -52
  111. package/dist/chacha20/pool.js +0 -188
  112. package/dist/chacha20/pool.worker.js +0 -37
  113. package/dist/docs/chacha20_pool.md +0 -309
  114. package/dist/docs/wasm.md +0 -194
  115. package/dist/serpent/seal.d.ts +0 -8
  116. package/dist/serpent/stream-pool.d.ts +0 -48
  117. package/dist/serpent/stream-pool.js +0 -285
  118. package/dist/serpent/stream-sealer.d.ts +0 -50
  119. package/dist/serpent/stream-sealer.js +0 -341
  120. package/dist/serpent/stream.d.ts +0 -28
  121. package/dist/serpent/stream.js +0 -205
  122. package/dist/serpent/stream.worker.d.ts +0 -32
  123. package/dist/serpent/stream.worker.js +0 -117
  124. /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,40 @@
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, XChaCha20Poly1305 is the recommended choice.** It gives
31
- you encryption and authentication in one call, with a nonce size that eliminates
32
- the most common footgun (accidental nonce reuse).
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
+ ---
33
50
 
34
51
  ## Security Notes
35
52
 
36
53
  > [!IMPORTANT]
37
- > Read this section before writing any code. These are not theoretical concerns
38
- > 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.
39
56
 
40
- - **Use `XChaCha20Poly1305` unless you have a specific reason not to.** It is the
41
- safest default: authenticated encryption with a nonce large enough for random
42
- generation. If you are unsure which class to pick, pick this one.
57
+ - **Use `Seal` with `XChaCha20Cipher` unless you need explicit nonce control.**
58
+ It is the safest default: authenticated encryption in a single static call.
59
+ If you are unsure which class to pick, pick this one. Use `XChaCha20Poly1305`
60
+ when protocol interop requires you to manage nonces yourself.
43
61
 
44
62
  - **Never reuse a nonce with the same key.** This is the single most important
45
63
  rule. If you encrypt two different messages with the same key and the same nonce,
@@ -47,7 +65,7 @@ the most common footgun (accidental nonce reuse).
47
65
  With `ChaCha20Poly1305` (12-byte nonce), random generation has a meaningful
48
66
  collision risk after roughly 2^32 messages under one key. With
49
67
  `XChaCha20Poly1305` (24-byte nonce), random generation is safe for any practical
50
- message count just call `randomBytes(24)` for each message.
68
+ message count. Just call `randomBytes(24)` for each message.
51
69
 
52
70
  - **Poly1305 keys are single-use.** Each Poly1305 key must be used to authenticate
53
71
  exactly one message. The AEAD classes (`ChaCha20Poly1305` and
@@ -56,13 +74,12 @@ the most common footgun (accidental nonce reuse).
56
74
  `Poly1305` class directly, it is your responsibility to never reuse a key.
57
75
 
58
76
  - **AEAD protects both confidentiality and authenticity.** If authentication fails
59
- during decryption, the plaintext is never returned you get an error. This is
60
- intentional. Do not try to work around it. If decryption fails, the ciphertext
61
- 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.
62
79
 
63
80
  - **Associated data (AAD) is authenticated but not encrypted.** Use AAD for data
64
81
  that must travel in the clear (headers, routing metadata, user IDs) but must be
65
- 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
66
83
  if the ciphertext itself is untouched.
67
84
 
68
85
  - **Always call `dispose()` when you are done.** This wipes key material and
@@ -75,29 +92,32 @@ the most common footgun (accidental nonce reuse).
75
92
  building your own authenticated construction (and you probably should not be),
76
93
  use one of the AEAD classes instead.
77
94
 
95
+ ---
96
+
78
97
  ## Module Init
79
98
 
80
99
  Each module subpath exports its own init function for consumers who want
81
100
  tree-shakeable imports.
82
101
 
83
- ### `chacha20Init(mode?, opts?)`
102
+ ### `chacha20Init(source)`
84
103
 
85
104
  Initializes only the chacha20 WASM binary. Equivalent to calling the
86
- root `init(['chacha20'], mode, opts)` but without pulling the other three
105
+ root `init({ chacha20: chacha20Wasm })` but without pulling the other three
87
106
  modules into the bundle.
88
107
 
89
108
  **Signature:**
90
109
 
91
110
  ```typescript
92
- async function chacha20Init(mode?: Mode, opts?: InitOpts): Promise<void>
111
+ async function chacha20Init(source: WasmSource): Promise<void>
93
112
  ```
94
113
 
95
114
  **Usage:**
96
115
 
97
116
  ```typescript
98
117
  import { chacha20Init, XChaCha20Poly1305 } from 'leviathan-crypto/chacha20'
118
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
99
119
 
100
- await chacha20Init()
120
+ await chacha20Init(chacha20Wasm)
101
121
  const aead = new XChaCha20Poly1305()
102
122
  ```
103
123
 
@@ -105,17 +125,17 @@ const aead = new XChaCha20Poly1305()
105
125
 
106
126
  ## API Reference
107
127
 
108
- All classes require calling `await init(['chacha20'])` or the subpath `chacha20Init()`
128
+ All classes require calling `await init({ chacha20: chacha20Wasm })` or the subpath `chacha20Init()`
109
129
  before construction. If you construct a class before initialization, it throws:
110
130
  ```
111
- Error: leviathan-crypto: call init(['chacha20']) before using this class
131
+ Error: leviathan-crypto: call init({ chacha20: ... }) before using this class
112
132
  ```
113
133
 
114
134
  ---
115
135
 
116
136
  ### `ChaCha20`
117
137
 
118
- Raw ChaCha20 stream cipher. **No authentication** use `XChaCha20Poly1305` instead
138
+ Raw ChaCha20 stream cipher. **No authentication.** Use `XChaCha20Poly1305` instead
119
139
  unless you are building a custom protocol and understand the risks.
120
140
 
121
141
  #### Constructor
@@ -124,7 +144,7 @@ unless you are building a custom protocol and understand the risks.
124
144
  new ChaCha20()
125
145
  ```
126
146
 
127
- Throws if `init(['chacha20'])` has not been called.
147
+ Throws if `init({ chacha20: chacha20Wasm })` has not been called.
128
148
 
129
149
  ---
130
150
 
@@ -156,8 +176,7 @@ a new `Uint8Array` containing the ciphertext (same length as input).
156
176
 
157
177
  #### `beginDecrypt(key: Uint8Array, nonce: Uint8Array): void`
158
178
 
159
- Prepares the cipher for decryption. Identical to `beginEncrypt` ChaCha20 is
160
- 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.
161
180
 
162
181
  | Parameter | Type | Description |
163
182
  |-----------|------|-------------|
@@ -191,7 +210,7 @@ when you are done with the instance.
191
210
  ### `Poly1305`
192
211
 
193
212
  Standalone Poly1305 one-time MAC. **Each key must be used exactly once.** You
194
- almost certainly want `ChaCha20Poly1305` or `XChaCha20Poly1305` instead — they
213
+ almost certainly want `ChaCha20Poly1305` or `XChaCha20Poly1305` instead. They
195
214
  handle Poly1305 key derivation automatically.
196
215
 
197
216
  #### Constructor
@@ -200,7 +219,7 @@ handle Poly1305 key derivation automatically.
200
219
  new Poly1305()
201
220
  ```
202
221
 
203
- Throws if `init(['chacha20'])` has not been called.
222
+ Throws if `init({ chacha20: chacha20Wasm })` has not been called.
204
223
 
205
224
  ---
206
225
 
@@ -210,10 +229,10 @@ Computes a 16-byte Poly1305 authentication tag over the given message.
210
229
 
211
230
  | Parameter | Type | Description |
212
231
  |-----------|------|-------------|
213
- | `key` | `Uint8Array` | 32 bytes must be unique per message |
232
+ | `key` | `Uint8Array` | 32 bytes. Must be unique per message. |
214
233
  | `msg` | `Uint8Array` | The message to authenticate (any length) |
215
234
 
216
- **Returns** `Uint8Array` a 16-byte authentication tag.
235
+ **Returns** `Uint8Array`: a 16-byte authentication tag.
217
236
 
218
237
  **Throws** `RangeError` if `key` is not 32 bytes.
219
238
 
@@ -240,54 +259,59 @@ to avoid collision risk.
240
259
  new ChaCha20Poly1305()
241
260
  ```
242
261
 
243
- Throws if `init(['chacha20'])` has not been called.
262
+ Throws if `init({ chacha20: chacha20Wasm })` has not been called.
244
263
 
245
264
  ---
246
265
 
247
- #### `encrypt(key, nonce, plaintext, aad?): { ciphertext: Uint8Array; tag: Uint8Array }`
266
+ #### `encrypt(key, nonce, plaintext, aad?): Uint8Array`
267
+
268
+ Encrypts plaintext and returns the ciphertext with the 16-byte Poly1305 tag
269
+ appended.
248
270
 
249
- Encrypts plaintext and produces a 16-byte authentication tag.
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.
250
275
 
251
276
  | Parameter | Type | Default | Description |
252
277
  |-----------|------|---------|-------------|
253
278
  | `key` | `Uint8Array` | | 32 bytes (256-bit key) |
254
279
  | `nonce` | `Uint8Array` | | 12 bytes (96-bit nonce) |
255
280
  | `plaintext` | `Uint8Array` | | Data to encrypt (up to the module's chunk size limit) |
256
- | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data authenticated but not encrypted |
281
+ | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data. Authenticated but not encrypted. |
257
282
 
258
- **Returns** `{ ciphertext: Uint8Array; tag: Uint8Array }` the ciphertext (same
259
- 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).
260
284
 
261
285
  **Throws:**
262
286
  - `RangeError` if `key` is not 32 bytes
263
287
  - `RangeError` if `nonce` is not 12 bytes
264
288
  - `RangeError` if `plaintext` exceeds the maximum chunk size
289
+ - `Error` if `encrypt()` has already been called on this instance
265
290
 
266
291
  ---
267
292
 
268
- #### `decrypt(key, nonce, ciphertext, tag, aad?): Uint8Array`
293
+ #### `decrypt(key, nonce, ciphertext, aad?): Uint8Array`
269
294
 
270
- Verifies the authentication tag and decrypts the ciphertext. If authentication
271
- 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.
272
299
 
273
- Tag comparison uses a constant-time XOR-accumulate pattern no timing side
274
- 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.
275
301
 
276
302
  | Parameter | Type | Default | Description |
277
303
  |-----------|------|---------|-------------|
278
304
  | `key` | `Uint8Array` | | 32 bytes (same key used for encryption) |
279
305
  | `nonce` | `Uint8Array` | | 12 bytes (same nonce used for encryption) |
280
- | `ciphertext` | `Uint8Array` | | Encrypted data |
281
- | `tag` | `Uint8Array` | | 16-byte authentication tag from `encrypt()` |
306
+ | `ciphertext` | `Uint8Array` | | Encrypted data with appended tag (output of `encrypt()`) |
282
307
  | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data (must match what was passed to `encrypt()`) |
283
308
 
284
- **Returns** `Uint8Array` the decrypted plaintext.
309
+ **Returns** `Uint8Array`: the decrypted plaintext.
285
310
 
286
311
  **Throws:**
287
312
  - `RangeError` if `key` is not 32 bytes
288
313
  - `RangeError` if `nonce` is not 12 bytes
289
- - `RangeError` if `tag` is not 16 bytes
290
- - `RangeError` if `ciphertext` exceeds the maximum chunk size
314
+ - `RangeError` if `ciphertext` is shorter than 16 bytes (no room for a tag)
291
315
  - `Error('ChaCha20Poly1305: authentication failed')` if the tag does not match
292
316
 
293
317
  ---
@@ -300,14 +324,16 @@ Wipes all key material and intermediate state from WASM memory.
300
324
 
301
325
  ### `XChaCha20Poly1305`
302
326
 
303
- XChaCha20-Poly1305 AEAD (draft-irtf-cfrg-xchacha). **This is the recommended
304
- authenticated encryption class for most use cases.**
327
+ XChaCha20-Poly1305 AEAD (draft-irtf-cfrg-xchacha). RFC-faithful stateless
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).
305
331
 
306
332
  It uses a 24-byte (192-bit) nonce, which is large enough that randomly generated
307
333
  nonces will never collide in practice. Internally, it derives a subkey via
308
334
  HChaCha20 and delegates to `ChaCha20Poly1305`.
309
335
 
310
- Unlike `ChaCha20Poly1305`, the `encrypt()` method returns a single `Uint8Array`
336
+ Like `ChaCha20Poly1305`, the `encrypt()` method returns a single `Uint8Array`
311
337
  with the tag appended to the ciphertext. The `decrypt()` method expects this
312
338
  combined format and splits it internally.
313
339
 
@@ -317,7 +343,7 @@ combined format and splits it internally.
317
343
  new XChaCha20Poly1305()
318
344
  ```
319
345
 
320
- Throws if `init(['chacha20'])` has not been called.
346
+ Throws if `init({ chacha20: chacha20Wasm })` has not been called.
321
347
 
322
348
  ---
323
349
 
@@ -330,9 +356,9 @@ Encrypts plaintext and returns the ciphertext with the 16-byte tag appended.
330
356
  | `key` | `Uint8Array` | | 32 bytes (256-bit key) |
331
357
  | `nonce` | `Uint8Array` | | 24 bytes (192-bit nonce) |
332
358
  | `plaintext` | `Uint8Array` | | Data to encrypt |
333
- | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data authenticated but not encrypted |
359
+ | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data. Authenticated but not encrypted. |
334
360
 
335
- **Returns** `Uint8Array` ciphertext + 16-byte tag (length = plaintext.length + 16).
361
+ **Returns** `Uint8Array`: ciphertext + 16-byte tag (length = plaintext.length + 16).
336
362
 
337
363
  **Throws:**
338
364
  - `RangeError` if `key` is not 32 bytes
@@ -353,7 +379,7 @@ parameter must include the appended 16-byte tag (i.e., the exact output of
353
379
  | `ciphertext` | `Uint8Array` | | Encrypted data with appended tag (output of `encrypt()`) |
354
380
  | `aad` | `Uint8Array` | `new Uint8Array(0)` | Associated data (must match what was passed to `encrypt()`) |
355
381
 
356
- **Returns** `Uint8Array` the decrypted plaintext.
382
+ **Returns** `Uint8Array`: the decrypted plaintext.
357
383
 
358
384
  **Throws:**
359
385
  - `RangeError` if `key` is not 32 bytes
@@ -369,119 +395,159 @@ Wipes all key material and intermediate state from WASM memory.
369
395
 
370
396
  ---
371
397
 
372
- ## Parallel pool — `XChaCha20Poly1305Pool`
398
+ ## XChaCha20Cipher
373
399
 
374
- For high-throughput workloads where multiple XChaCha20-Poly1305 operations should
375
- run concurrently, `XChaCha20Poly1305Pool` dispatches work across a configurable
376
- number of Web Workers, each holding an isolated `chacha20.wasm` instance in its own
377
- linear memory. This removes the single-threaded bottleneck of a shared WASM
378
- instance and allows encryption and decryption operations to proceed in parallel.
379
- The pool requires `init(['chacha20'])` to be called before construction and is
380
- created via the static factory `XChaCha20Poly1305Pool.create(opts?)` — see
381
- [chacha20_pool.md](./chacha20_pool.md) for the full API reference, pool sizing
382
- guidance, and lifecycle docs.
400
+ `CipherSuite` implementation for XChaCha20-Poly1305. Pass to `Seal`,
401
+ `SealStream`, or `OpenStream`. Never instantiated directly.
383
402
 
384
- ---
403
+ Requires `init({ chacha20: chacha20Wasm, sha2: sha2Wasm })`.
385
404
 
386
- ## Usage Examples
405
+ > [!NOTE]
406
+ > `sha2` is required by the stream layer for HKDF key derivation, not by
407
+ > `XChaCha20Poly1305` itself.
408
+
409
+ | Property | Value |
410
+ |----------|-------|
411
+ | `formatEnum` | `0x01` |
412
+ | `keySize` | `32` |
413
+ | `tagSize` | `16` (Poly1305) |
414
+ | `padded` | `false` |
415
+ | `wasmModules` | `['chacha20', 'sha2']` |
387
416
 
388
- ### Example 1: XChaCha20Poly1305 — Encrypt and Decrypt (Recommended)
417
+ #### `XChaCha20Cipher.keygen(): Uint8Array`
389
418
 
390
- This is the pattern most users should follow. Generate a random 24-byte nonce for
391
- each message — no counter management, no collision risk.
419
+ Returns `randomBytes(32)`. Convenience method. Not on the `CipherSuite` interface.
420
+
421
+ #### Usage with `Seal`
392
422
 
393
423
  ```typescript
394
- import { init, XChaCha20Poly1305, randomBytes, utf8ToBytes, bytesToUtf8 } from 'leviathan-crypto'
424
+ import { init, Seal, XChaCha20Cipher } from 'leviathan-crypto'
425
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
426
+ import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
395
427
 
396
- // Step 1: Initialize the chacha20 WASM module (once, at application startup)
397
- await init(['chacha20'])
428
+ await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
398
429
 
399
- // Step 2: Generate a 256-bit encryption key
400
- // In a real application, this comes from a key derivation function or key exchange.
401
- const key = randomBytes(32)
430
+ const key = XChaCha20Cipher.keygen()
431
+ const blob = Seal.encrypt(XChaCha20Cipher, key, plaintext)
432
+ const pt = Seal.decrypt(XChaCha20Cipher, key, blob) // throws on tamper
433
+ ```
402
434
 
403
- // Step 3: Create the AEAD instance
404
- const aead = new XChaCha20Poly1305()
435
+ #### Usage with `SealStream` / `OpenStream`
405
436
 
406
- // Step 4: Encrypt
407
- const nonce = randomBytes(24) // Safe to generate randomly — 24 bytes means no collision risk
408
- const plaintext = utf8ToBytes('Hello, world!')
409
- const sealed = aead.encrypt(key, nonce, plaintext)
410
- // `sealed` contains the ciphertext + 16-byte authentication tag
437
+ ```typescript
438
+ import { SealStream, OpenStream } from 'leviathan-crypto/stream'
439
+ import { XChaCha20Cipher } from 'leviathan-crypto/chacha20'
411
440
 
412
- // Step 5: Decrypt
413
- // You need the same key and nonce to decrypt.
414
- // Store or transmit the nonce alongside the ciphertext — the nonce is not secret.
415
- const decrypted = aead.decrypt(key, nonce, sealed)
416
- console.log(bytesToUtf8(decrypted)) // "Hello, world!"
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)
417
445
 
418
- // Step 6: Clean up — wipe key material from WASM memory
419
- aead.dispose()
446
+ const opener = new OpenStream(XChaCha20Cipher, key, preamble)
447
+ const pt0 = opener.pull(ct0)
448
+ const ptLast = opener.finalize(ctLast)
449
+ ```
450
+
451
+ See [aead.md](./aead.md) for the full `Seal`, `SealStream`, and `OpenStream` API.
452
+
453
+ ---
454
+
455
+ ## Usage Examples
456
+
457
+ ### Example 1: Seal with XChaCha20Cipher (recommended)
458
+
459
+ One-shot AEAD. No instantiation, no `dispose()`. The blob format is
460
+ `preamble(20) || ciphertext || tag(16)`.
461
+
462
+ ```typescript
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'
466
+
467
+ await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
468
+
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
472
+
473
+ console.log(bytesToUtf8(pt)) // "Hello, world!"
474
+
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 })
420
479
  ```
421
480
 
422
- ### Example 2: ChaCha20Poly1305 Encrypt and Decrypt
481
+ > For explicit nonce control, use `XChaCha20Poly1305` directly. Same
482
+ > API shape, 24-byte nonce passed per call. See Example 2.
483
+
484
+ ### Example 2: ChaCha20Poly1305
423
485
 
424
486
  Same idea as above, but with a 12-byte nonce. Use this if you are implementing
425
487
  a protocol that specifies RFC 8439 ChaCha20-Poly1305 explicitly.
426
488
 
427
- Note the differences from `XChaCha20Poly1305`:
428
- - The nonce is 12 bytes, not 24
429
- - `encrypt()` returns `{ ciphertext, tag }` as separate fields
430
- - `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.
431
497
 
432
498
  ```typescript
433
499
  import { init, ChaCha20Poly1305, randomBytes, utf8ToBytes, bytesToUtf8 } from 'leviathan-crypto'
500
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
434
501
 
435
- await init(['chacha20'])
502
+ await init({ chacha20: chacha20Wasm })
436
503
 
437
504
  const key = randomBytes(32)
438
505
  const aead = new ChaCha20Poly1305()
439
506
 
440
507
  // Encrypt
441
- 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
442
509
  const plaintext = utf8ToBytes('Sensitive data')
443
- const { ciphertext, tag } = aead.encrypt(key, nonce, plaintext)
444
- // 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
445
512
 
446
- // Decrypt
447
- 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)
448
516
  console.log(bytesToUtf8(decrypted)) // "Sensitive data"
449
517
 
450
518
  aead.dispose()
519
+ aead2.dispose()
451
520
  ```
452
521
 
453
522
  ### Example 3: Detecting Tampered Ciphertext
454
523
 
455
- AEAD decryption fails loudly if anyone has modified the ciphertext, the tag, or
456
- the associated data. This is a feature it prevents you from processing corrupted
457
- or maliciously altered data.
524
+ AEAD decryption fails loudly if anyone has modified the ciphertext or
525
+ the associated data. This is a feature. It prevents you from processing
526
+ corrupted or maliciously altered data.
458
527
 
459
528
  ```typescript
460
- import { init, XChaCha20Poly1305, 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'
461
532
 
462
- await init(['chacha20'])
533
+ await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
463
534
 
464
- const key = randomBytes(32)
465
- const nonce = randomBytes(24)
466
- const aead = new XChaCha20Poly1305()
467
-
468
- const sealed = aead.encrypt(key, nonce, utf8ToBytes('Original message'))
535
+ const key = XChaCha20Cipher.keygen()
536
+ const sealed = Seal.encrypt(XChaCha20Cipher, key, utf8ToBytes('Original message'))
469
537
 
470
538
  // Simulate tampering: flip one bit in the ciphertext
471
539
  const tampered = new Uint8Array(sealed)
472
- tampered[0] ^= 0x01
540
+ tampered[20] ^= 0x01 // byte 20 is the first ciphertext byte (after the 20-byte preamble)
473
541
 
474
542
  try {
475
- const plaintext = aead.decrypt(key, nonce, tampered)
543
+ const plaintext = Seal.decrypt(XChaCha20Cipher, key, tampered)
476
544
  // This line is never reached
477
545
  console.log(plaintext)
478
546
  } catch (err) {
479
547
  console.error(err.message)
480
548
  // "ChaCha20Poly1305: authentication failed"
481
- // The plaintext is never returned decryption stops immediately on failure.
549
+ // The plaintext is never returned. Decryption stops immediately on failure.
482
550
  }
483
-
484
- aead.dispose()
485
551
  ```
486
552
 
487
553
  ### Example 4: Using Associated Data (AAD)
@@ -491,72 +557,69 @@ not encrypt. Common uses: user IDs, message sequence numbers, protocol version
491
557
  headers, routing information.
492
558
 
493
559
  ```typescript
494
- import { init, XChaCha20Poly1305, 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'
495
563
 
496
- await init(['chacha20'])
564
+ await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
497
565
 
498
- const key = randomBytes(32)
499
- const nonce = randomBytes(24)
500
- const aead = new XChaCha20Poly1305()
566
+ const key = XChaCha20Cipher.keygen()
501
567
 
502
568
  // The user ID travels in the clear, but decryption will fail if anyone changes it
503
- const userId = utf8ToBytes('user-12345')
569
+ const userId = utf8ToBytes('user-12345')
504
570
  const message = utf8ToBytes('Your account balance is $1,000,000')
505
571
 
506
- const sealed = aead.encrypt(key, nonce, message, userId)
572
+ const sealed = Seal.encrypt(XChaCha20Cipher, key, message, { aad: userId })
507
573
 
508
- // Decrypt pass the same AAD
509
- const decrypted = aead.decrypt(key, nonce, sealed, userId)
574
+ // Decrypt, pass the same AAD
575
+ const decrypted = Seal.decrypt(XChaCha20Cipher, key, sealed, { aad: userId })
510
576
  console.log(bytesToUtf8(decrypted))
511
577
  // "Your account balance is $1,000,000"
512
578
 
513
579
  // If someone changes the AAD, decryption fails
514
580
  const wrongUserId = utf8ToBytes('user-99999')
515
581
  try {
516
- aead.decrypt(key, nonce, sealed, wrongUserId)
582
+ Seal.decrypt(XChaCha20Cipher, key, sealed, { aad: wrongUserId })
517
583
  } catch (err) {
518
584
  console.error(err.message)
519
585
  // "ChaCha20Poly1305: authentication failed"
520
586
  // Even though the ciphertext was not modified, the AAD mismatch is detected.
521
587
  }
522
-
523
- aead.dispose()
524
588
  ```
525
589
 
526
590
  ### Example 5: Encrypting and Decrypting Binary Data
527
591
 
528
- 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
529
593
  arbitrary binary content.
530
594
 
531
595
  ```typescript
532
- import { init, XChaCha20Poly1305, 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'
533
599
 
534
- await init(['chacha20'])
600
+ await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
535
601
 
536
- const key = randomBytes(32)
537
- const nonce = randomBytes(24)
538
- const aead = new XChaCha20Poly1305()
602
+ const key = XChaCha20Cipher.keygen()
539
603
 
540
604
  // Encrypt binary data (e.g., an image thumbnail, a protobuf, a file chunk)
541
605
  const binaryData = new Uint8Array([0x89, 0x50, 0x4e, 0x47, /* ...more bytes... */])
542
- const sealed = aead.encrypt(key, nonce, binaryData)
606
+ const sealed = Seal.encrypt(XChaCha20Cipher, key, binaryData)
543
607
 
544
608
  // Decrypt
545
- const recovered = aead.decrypt(key, nonce, sealed)
609
+ const recovered = Seal.decrypt(XChaCha20Cipher, key, sealed)
546
610
  // `recovered` is byte-identical to `binaryData`
547
-
548
- aead.dispose()
549
611
  ```
550
612
 
551
613
  ### Example 6: Raw ChaCha20 Stream Cipher (Advanced)
552
614
 
553
615
  Use this only if you are building a custom protocol and will add your own
554
- authentication layer. For almost all use cases, use `XChaCha20Poly1305` instead.
616
+ authentication layer. For almost all use cases, use `Seal` with `XChaCha20Cipher` instead.
555
617
 
556
618
  ```typescript
557
619
  import { init, ChaCha20, randomBytes } from 'leviathan-crypto'
620
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
558
621
 
559
- await init(['chacha20'])
622
+ await init({ chacha20: chacha20Wasm })
560
623
 
561
624
  const key = randomBytes(32)
562
625
  const nonce = randomBytes(12)
@@ -567,7 +630,7 @@ cipher.beginEncrypt(key, nonce)
567
630
  const ct1 = cipher.encryptChunk(new Uint8Array([1, 2, 3, 4]))
568
631
  const ct2 = cipher.encryptChunk(new Uint8Array([5, 6, 7, 8]))
569
632
 
570
- // Decrypt uses the same key and nonce
633
+ // Decrypt, uses the same key and nonce
571
634
  cipher.beginDecrypt(key, nonce)
572
635
  const pt1 = cipher.decryptChunk(ct1)
573
636
  const pt2 = cipher.decryptChunk(ct2)
@@ -580,27 +643,32 @@ const pt2 = cipher.decryptChunk(ct2)
580
643
  cipher.dispose()
581
644
  ```
582
645
 
646
+ ---
647
+
583
648
  ## Error Conditions
584
649
 
585
650
  | Condition | Error Type | Message |
586
651
  |-----------|-----------|---------|
587
- | `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` |
588
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)` |
589
654
  | `ChaCha20` nonce is not 12 bytes | `RangeError` | `ChaCha20 nonce must be 12 bytes (got N)` |
590
655
  | `ChaCha20Poly1305` nonce is not 12 bytes | `RangeError` | `nonce must be 12 bytes (got N)` |
591
656
  | `XChaCha20Poly1305` nonce is not 24 bytes | `RangeError` | `XChaCha20 nonce must be 24 bytes (got N)` |
592
- | `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)` |
593
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. |
594
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` |
595
661
  | Authentication tag does not match on decrypt | `Error` | `ChaCha20Poly1305: authentication failed` |
596
- | 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). |
597
663
 
598
664
  > ## Cross-References
599
665
  >
600
666
  > - [index](./README.md) — Project Documentation index
667
+ > - [lexicon](./lexicon.md) — Glossary of cryptographic terms
601
668
  > - [asm_chacha](./asm_chacha.md) — WASM (AssemblyScript) implementation details for the chacha20 module
602
- > - [chacha20_pool](./chacha20_pool.md) — `XChaCha20Poly1305Pool` worker-pool wrapper for parallel encryption
603
- > - [serpent](./serpent.md) — alternative: Serpent block cipher modes (CBC, CTR — unauthenticated, needs HMAC pairing)
604
- > - [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
605
672
  > - [types](./types.md) — `AEAD` and `Streamcipher` interfaces implemented by ChaCha20 classes
606
- > - [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