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.
- package/CLAUDE.md +265 -0
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/SECURITY.md +174 -0
- package/dist/chacha.wasm +0 -0
- package/dist/chacha20/index.d.ts +49 -0
- package/dist/chacha20/index.js +177 -0
- package/dist/chacha20/ops.d.ts +16 -0
- package/dist/chacha20/ops.js +146 -0
- package/dist/chacha20/pool.d.ts +52 -0
- package/dist/chacha20/pool.js +188 -0
- package/dist/chacha20/pool.worker.d.ts +1 -0
- package/dist/chacha20/pool.worker.js +37 -0
- package/dist/chacha20/types.d.ts +30 -0
- package/dist/chacha20/types.js +1 -0
- package/dist/docs/architecture.md +795 -0
- package/dist/docs/argon2id.md +290 -0
- package/dist/docs/chacha20.md +602 -0
- package/dist/docs/chacha20_pool.md +306 -0
- package/dist/docs/fortuna.md +322 -0
- package/dist/docs/init.md +308 -0
- package/dist/docs/loader.md +206 -0
- package/dist/docs/serpent.md +914 -0
- package/dist/docs/sha2.md +620 -0
- package/dist/docs/sha3.md +509 -0
- package/dist/docs/types.md +198 -0
- package/dist/docs/utils.md +273 -0
- package/dist/docs/wasm.md +193 -0
- package/dist/embedded/chacha.d.ts +1 -0
- package/dist/embedded/chacha.js +2 -0
- package/dist/embedded/serpent.d.ts +1 -0
- package/dist/embedded/serpent.js +2 -0
- package/dist/embedded/sha2.d.ts +1 -0
- package/dist/embedded/sha2.js +2 -0
- package/dist/embedded/sha3.d.ts +1 -0
- package/dist/embedded/sha3.js +2 -0
- package/dist/fortuna.d.ts +72 -0
- package/dist/fortuna.js +445 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +44 -0
- package/dist/init.d.ts +11 -0
- package/dist/init.js +49 -0
- package/dist/loader.d.ts +4 -0
- package/dist/loader.js +30 -0
- package/dist/serpent/index.d.ts +65 -0
- package/dist/serpent/index.js +242 -0
- package/dist/serpent/seal.d.ts +8 -0
- package/dist/serpent/seal.js +70 -0
- package/dist/serpent/stream-encoder.d.ts +20 -0
- package/dist/serpent/stream-encoder.js +167 -0
- package/dist/serpent/stream-pool.d.ts +48 -0
- package/dist/serpent/stream-pool.js +285 -0
- package/dist/serpent/stream-sealer.d.ts +34 -0
- package/dist/serpent/stream-sealer.js +223 -0
- package/dist/serpent/stream.d.ts +28 -0
- package/dist/serpent/stream.js +205 -0
- package/dist/serpent/stream.worker.d.ts +32 -0
- package/dist/serpent/stream.worker.js +117 -0
- package/dist/serpent/types.d.ts +5 -0
- package/dist/serpent/types.js +1 -0
- package/dist/serpent.wasm +0 -0
- package/dist/sha2/hkdf.d.ts +16 -0
- package/dist/sha2/hkdf.js +108 -0
- package/dist/sha2/index.d.ts +40 -0
- package/dist/sha2/index.js +190 -0
- package/dist/sha2/types.d.ts +5 -0
- package/dist/sha2/types.js +1 -0
- package/dist/sha2.wasm +0 -0
- package/dist/sha3/index.d.ts +55 -0
- package/dist/sha3/index.js +246 -0
- package/dist/sha3/types.d.ts +5 -0
- package/dist/sha3/types.js +1 -0
- package/dist/sha3.wasm +0 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.js +26 -0
- package/dist/utils.d.ts +26 -0
- package/dist/utils.js +169 -0
- 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
|