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,914 @@
1
+ # Serpent-256 block cipher TypeScript API
2
+
3
+ > [!NOTE]
4
+ > Authenticated encryption via `SerpentSeal`, plus low-level block, CTR, and CBC
5
+ > classes for advanced use.
6
+
7
+ ## Overview
8
+
9
+ `SerpentSeal` is the primary encryption API for the Serpent module. It provides
10
+ authenticated Serpent-256 encryption in a single call -- no manual IV generation,
11
+ no separate MAC step, no room for misuse. Internally it uses Encrypt-then-MAC
12
+ (SerpentCbc + HMAC-SHA256) and verifies authentication before decryption.
13
+
14
+ For advanced use cases, three lower-level classes are available: `Serpent` (raw
15
+ 16-byte block operations), `SerpentCtr` (counter mode streaming), and `SerpentCbc`
16
+ (cipher block chaining with PKCS7 padding). These are unauthenticated and require
17
+ explicit opt-in.
18
+
19
+ Serpent was an AES finalist. It uses 32 rounds versus AES's 10--14, yielding a
20
+ larger security margin at comparable speed in WASM.
21
+
22
+ ---
23
+
24
+ ## Security Notes
25
+
26
+ > [!IMPORTANT]
27
+ > Read this section carefully before using any Serpent class. These are not
28
+ > theoretical concerns. Ignoring them will render encryption useless.
29
+
30
+ ### SerpentCbc and SerpentCtr are unauthenticated
31
+
32
+ This is the most dangerous mistake you can make with this module. An attacker who
33
+ can modify ciphertext encrypted with `SerpentCbc` or `SerpentCtr` will produce
34
+ corrupted plaintext on decryption -- and decryption will succeed without any
35
+ indication of tampering. There is no integrity check. The caller receives garbage
36
+ and has no way to distinguish it from the original message.
37
+
38
+ `SerpentSeal` eliminates this problem. It computes an HMAC tag over the ciphertext
39
+ and verifies it before decryption. If anything has been modified, `decrypt()` throws
40
+ instead of returning corrupted data.
41
+
42
+ Leviathan also offers a `XChaCha20Poly1305` implementation an alternative that
43
+ provides authenticated encryption with a different cipher. See
44
+ [chacha20.md](./chacha20.md).
45
+
46
+ ### Never reuse a nonce or IV with the same key
47
+
48
+ - **CTR mode**: Reusing a nonce with the same key is catastrophic. It produces the
49
+ same keystream, which means an attacker can XOR two ciphertexts together and
50
+ recover both plaintexts. Always generate a fresh random nonce for each message.
51
+ - **CBC mode**: The IV (initialization vector) must be random and unpredictable for
52
+ each encryption. A predictable IV enables chosen-plaintext attacks.
53
+
54
+ Use `randomBytes(16)` to generate nonces and IVs. `SerpentSeal` handles IV
55
+ generation internally.
56
+
57
+ ### Always use 256-bit keys
58
+
59
+ Unless you have a specific reason to use a shorter key, pass a 32-byte key to
60
+ every Serpent operation. Shorter keys provide less security margin and there is no
61
+ meaningful performance benefit to using them. `SerpentSeal` requires a 64-byte key
62
+ (32 bytes encryption + 32 bytes MAC).
63
+
64
+ ### Call dispose() when done
65
+
66
+ Every Serpent class holds key material in WebAssembly memory. When you are finished
67
+ with an instance, call `dispose()` to zero out all key material, intermediate
68
+ state, and buffers. Failing to call `dispose()` means sensitive data may persist
69
+ in memory longer than necessary.
70
+
71
+ ---
72
+
73
+ ## Module Init
74
+
75
+ Each module subpath exports its own init function for consumers who want
76
+ tree-shakeable imports.
77
+
78
+ ### `serpentInit(mode?, opts?)`
79
+
80
+ Initializes only the serpent WASM binary. Equivalent to calling the
81
+ root `init(['serpent'], mode, opts)` but without pulling the other three
82
+ modules into the bundle.
83
+
84
+ **Signature:**
85
+
86
+ ```typescript
87
+ async function serpentInit(mode?: Mode, opts?: InitOpts): Promise<void>
88
+ ```
89
+
90
+ **Usage:**
91
+
92
+ ```typescript
93
+ import { serpentInit, Serpent } from 'leviathan-crypto/serpent'
94
+
95
+ await serpentInit()
96
+ const cipher = new Serpent()
97
+ ```
98
+
99
+ ---
100
+
101
+ ## API Reference
102
+
103
+ All classes require their WASM modules to be initialized before construction.
104
+ `SerpentSeal` requires both `serpent` and `sha2`. All other classes require
105
+ `serpent` only.
106
+
107
+ ### SerpentSeal
108
+
109
+ Authenticated Serpent-256 encryption. Handles IV generation, HMAC computation,
110
+ and verification internally -- no manual IV or MAC management required.
111
+
112
+ ```typescript
113
+ class SerpentSeal {
114
+ constructor()
115
+ encrypt(key: Uint8Array, plaintext: Uint8Array): Uint8Array
116
+ decrypt(key: Uint8Array, data: Uint8Array): Uint8Array
117
+ dispose(): void
118
+ }
119
+ ```
120
+
121
+ #### `constructor()`
122
+
123
+ Creates a new SerpentSeal instance. Throws if `init(['serpent', 'sha2'])` has not
124
+ been called.
125
+
126
+ ---
127
+
128
+ #### `encrypt(key: Uint8Array, plaintext: Uint8Array): Uint8Array`
129
+
130
+ Encrypts plaintext and returns a sealed blob containing the ciphertext and
131
+ authentication data. The output is opaque -- pass it directly to `decrypt()`.
132
+
133
+ - **key** -- exactly 64 bytes (32 bytes encryption key + 32 bytes MAC key).
134
+ Throws `RangeError` if the length is not 64.
135
+ - **plaintext** -- any length.
136
+
137
+ A fresh random IV is generated internally for each call. Two encryptions of the
138
+ same plaintext with the same key produce different output.
139
+
140
+ ---
141
+
142
+ #### `decrypt(key: Uint8Array, data: Uint8Array): Uint8Array`
143
+
144
+ Verifies the authentication tag and decrypts the sealed blob. MAC verification
145
+ happens before decryption -- if the data has been tampered with, `decrypt()` throws
146
+ and never returns corrupted plaintext.
147
+
148
+ - **key** -- exactly 64 bytes. Must be the same key used for encryption. Throws
149
+ `RangeError` if the length is not 64.
150
+ - **data** -- the sealed blob from `encrypt()`. Must be at least 64 bytes. Throws
151
+ `RangeError` if shorter. Throws `Error` if authentication fails.
152
+
153
+ ---
154
+
155
+ #### `dispose(): void`
156
+
157
+ Wipes all key material and intermediate state from WASM memory. Delegates to both
158
+ internal SerpentCbc and HMAC_SHA256 instances.
159
+
160
+ ---
161
+
162
+ ### Serpent
163
+
164
+ Raw Serpent block encryption and decryption. Operates on exactly 16-byte blocks.
165
+ This class is a low-level building block, most users should use `SerpentSeal`
166
+ instead.
167
+
168
+ ```typescript
169
+ class Serpent {
170
+ constructor()
171
+ loadKey(key: Uint8Array): void
172
+ encryptBlock(plaintext: Uint8Array): Uint8Array
173
+ decryptBlock(ciphertext: Uint8Array): Uint8Array
174
+ dispose(): void
175
+ }
176
+ ```
177
+
178
+ #### `constructor()`
179
+
180
+ Creates a new Serpent instance. Throws if `init(['serpent'])` has not been called.
181
+
182
+ ---
183
+
184
+ #### `loadKey(key: Uint8Array): void`
185
+
186
+ Loads and expands a key for subsequent block operations. Must be called before
187
+ `encryptBlock()` or `decryptBlock()`.
188
+
189
+ - **key** -- 16, 24, or 32 bytes. Throws `RangeError` if the length is invalid.
190
+
191
+ ---
192
+
193
+ #### `encryptBlock(plaintext: Uint8Array): Uint8Array`
194
+
195
+ Encrypts a single 16-byte block and returns the 16-byte ciphertext.
196
+
197
+ - **plaintext** -- exactly 16 bytes. Throws `RangeError` if the length is not 16.
198
+
199
+ ---
200
+
201
+ #### `decryptBlock(ciphertext: Uint8Array): Uint8Array`
202
+
203
+ Decrypts a single 16-byte block and returns the 16-byte plaintext.
204
+
205
+ - **ciphertext** -- exactly 16 bytes. Throws `RangeError` if the length is not 16.
206
+
207
+ ---
208
+
209
+ #### `dispose(): void`
210
+
211
+ Wipes all key material and intermediate state from WASM memory. Always call this
212
+ when you are done with the instance.
213
+
214
+ ---
215
+
216
+ ### SerpentCtr
217
+
218
+ Serpent in Counter (CTR) mode. Encrypts and decrypts data of any length as a
219
+ stream of chunks.
220
+
221
+ > [!WARNING]
222
+ > CTR mode is unauthenticated. An attacker can modify ciphertext
223
+ > without detection. Use `SerpentSeal` for authenticated encryption, or pair
224
+ > with HMAC-SHA256 (Encrypt-then-MAC).
225
+
226
+ ```typescript
227
+ class SerpentCtr {
228
+ constructor(opts: { dangerUnauthenticated: true })
229
+ beginEncrypt(key: Uint8Array, nonce: Uint8Array): void
230
+ encryptChunk(chunk: Uint8Array): Uint8Array
231
+ beginDecrypt(key: Uint8Array, nonce: Uint8Array): void
232
+ decryptChunk(chunk: Uint8Array): Uint8Array
233
+ dispose(): void
234
+ }
235
+ ```
236
+
237
+ #### `constructor(opts: { dangerUnauthenticated: true })`
238
+
239
+ Creates a new SerpentCtr instance. Throws if `init(['serpent'])` has not been
240
+ called. Throws if `{ dangerUnauthenticated: true }` is not passed:
241
+
242
+ ```
243
+ leviathan-crypto: SerpentCtr is unauthenticated — use SerpentSeal instead.
244
+ To use SerpentCtr directly, pass { dangerUnauthenticated: true }.
245
+ ```
246
+
247
+ ---
248
+
249
+ #### `beginEncrypt(key: Uint8Array, nonce: Uint8Array): void`
250
+
251
+ Initializes the CTR state for encryption. Loads the key, sets the nonce, and
252
+ resets the internal counter to zero.
253
+
254
+ - **key** -- 16, 24, or 32 bytes. Throws `RangeError` if the length is invalid.
255
+ - **nonce** -- exactly 16 bytes. Throws `RangeError` if the length is not 16.
256
+
257
+ ---
258
+
259
+ #### `encryptChunk(chunk: Uint8Array): Uint8Array`
260
+
261
+ Encrypts a chunk of plaintext and returns the same-length ciphertext. Call this
262
+ one or more times after `beginEncrypt()`. The internal counter advances
263
+ automatically.
264
+
265
+ - **chunk** -- any length up to the module's internal chunk buffer size. Throws
266
+ `RangeError` if the chunk exceeds the maximum size.
267
+
268
+ ---
269
+
270
+ #### `beginDecrypt(key: Uint8Array, nonce: Uint8Array): void`
271
+
272
+ Initializes the CTR state for decryption. Functionally identical to
273
+ `beginEncrypt()` -- CTR mode uses the same operation in both directions.
274
+
275
+ - **key** -- 16, 24, or 32 bytes. Throws `RangeError` if the length is invalid.
276
+ - **nonce** -- exactly 16 bytes. Throws `RangeError` if the length is not 16.
277
+
278
+ ---
279
+
280
+ #### `decryptChunk(chunk: Uint8Array): Uint8Array`
281
+
282
+ Decrypts a chunk of ciphertext and returns the same-length plaintext.
283
+ Functionally identical to `encryptChunk()`.
284
+
285
+ - **chunk** -- any length up to the module's internal chunk buffer size. Throws
286
+ `RangeError` if the chunk exceeds the maximum size.
287
+
288
+ ---
289
+
290
+ #### `dispose(): void`
291
+
292
+ Wipes all key material and intermediate state from WASM memory.
293
+
294
+ ---
295
+
296
+ ### SerpentCbc
297
+
298
+ Serpent in Cipher Block Chaining (CBC) mode with automatic PKCS7 padding.
299
+ Encrypts and decrypts entire messages in a single call.
300
+
301
+ > [!WARNING]
302
+ > CBC mode is unauthenticated. Always authenticate the output with
303
+ > HMAC-SHA256 (Encrypt-then-MAC) or use `SerpentSeal` instead.
304
+
305
+ ```typescript
306
+ class SerpentCbc {
307
+ constructor(opts: { dangerUnauthenticated: true })
308
+ encrypt(key: Uint8Array, iv: Uint8Array, plaintext: Uint8Array): Uint8Array
309
+ decrypt(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Uint8Array
310
+ dispose(): void
311
+ }
312
+ ```
313
+
314
+ #### `constructor(opts: { dangerUnauthenticated: true })`
315
+
316
+ Creates a new SerpentCbc instance. Throws if `init(['serpent'])` has not been
317
+ called. Throws if `{ dangerUnauthenticated: true }` is not passed:
318
+
319
+ ```
320
+ leviathan-crypto: SerpentCbc is unauthenticated — use SerpentSeal instead.
321
+ To use SerpentCbc directly, pass { dangerUnauthenticated: true }.
322
+ ```
323
+
324
+ ---
325
+
326
+ #### `encrypt(key: Uint8Array, iv: Uint8Array, plaintext: Uint8Array): Uint8Array`
327
+
328
+ Encrypts plaintext with Serpent CBC and PKCS7 padding. The returned ciphertext is
329
+ always a multiple of 16 bytes and is at least 16 bytes longer than the input (due
330
+ to padding).
331
+
332
+ - **key** -- 16, 24, or 32 bytes. Throws `RangeError` if the length is invalid.
333
+ - **iv** -- exactly 16 bytes. Must be random and unique for each (key, message)
334
+ pair. Throws `RangeError` if the length is not 16.
335
+ - **plaintext** -- any length (including zero). PKCS7 padding is applied
336
+ automatically.
337
+
338
+ Returns the ciphertext as a new `Uint8Array`.
339
+
340
+ ---
341
+
342
+ #### `decrypt(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Uint8Array`
343
+
344
+ Decrypts Serpent CBC ciphertext and strips PKCS7 padding.
345
+
346
+ - **key** -- 16, 24, or 32 bytes. Throws `RangeError` if the length is invalid.
347
+ - **iv** -- exactly 16 bytes. Must be the same IV that was used for encryption.
348
+ Throws `RangeError` if the length is not 16.
349
+ - **ciphertext** -- must be a non-zero multiple of 16 bytes. Throws `RangeError`
350
+ if the length is zero or not a multiple of 16. Also throws `RangeError` if PKCS7
351
+ padding is invalid (which typically indicates the wrong key, wrong IV, or
352
+ corrupted ciphertext).
353
+
354
+ Returns the decrypted plaintext as a new `Uint8Array`.
355
+
356
+ ---
357
+
358
+ #### `dispose(): void`
359
+
360
+ Wipes all key material and intermediate state from WASM memory.
361
+
362
+ ---
363
+
364
+ ### SerpentStream
365
+
366
+ Chunked authenticated encryption for large payloads. Each chunk is independently
367
+ encrypted with Serpent-CTR and authenticated with HMAC-SHA256 using per-chunk
368
+ keys derived via HKDF-SHA256. Position binding and truncation detection are
369
+ enforced at the key-derivation layer.
370
+
371
+ Use `SerpentStream` when the payload is large or when holding the entire
372
+ plaintext in memory is undesirable. For small/medium payloads where a single
373
+ `encrypt()`/`decrypt()` call is sufficient, use `SerpentSeal` instead.
374
+
375
+ > [!NOTE]
376
+ > `SerpentStream` takes a 32-byte key (HKDF handles expansion internally).
377
+ > This differs from `SerpentSeal`, which takes 64 bytes.
378
+
379
+ ```typescript
380
+ class SerpentStream {
381
+ constructor()
382
+ seal(key: Uint8Array, plaintext: Uint8Array, chunkSize?: number): Uint8Array
383
+ open(key: Uint8Array, ciphertext: Uint8Array): Uint8Array
384
+ dispose(): void
385
+ }
386
+ ```
387
+
388
+ #### `constructor()`
389
+
390
+ Creates a new SerpentStream instance. Throws if `init(['serpent', 'sha2'])` has
391
+ not been called.
392
+
393
+ ---
394
+
395
+ #### `seal(key: Uint8Array, plaintext: Uint8Array, chunkSize?: number): Uint8Array`
396
+
397
+ Encrypts plaintext into a chunked authenticated wire format.
398
+
399
+ - **key** -- exactly 32 bytes. Throws `RangeError` if not.
400
+ - **plaintext** -- any length (including zero).
401
+ - **chunkSize** -- optional, default 64KB. Valid range: 1KB to 64KB. Throws
402
+ `RangeError` if outside range.
403
+
404
+ A fresh random stream nonce is generated internally for each call. Two seals of
405
+ the same plaintext with the same key produce different output.
406
+
407
+ Wire format: `stream_nonce (16) || chunk_size (4, u32_be) || chunk_count (8, u64_be) || chunk_0 || ... || chunk_N-1`
408
+
409
+ Each chunk on the wire: `ciphertext || hmac_tag (32 bytes)`.
410
+
411
+ ---
412
+
413
+ #### `open(key: Uint8Array, ciphertext: Uint8Array): Uint8Array`
414
+
415
+ Verifies authentication and decrypts the chunked wire format. Each chunk's MAC
416
+ is verified before decryption (Encrypt-then-MAC). If any chunk fails
417
+ authentication, `open()` throws immediately and never returns partial plaintext.
418
+
419
+ - **key** -- exactly 32 bytes. Must be the same key used for `seal()`.
420
+ - **ciphertext** -- the wire format from `seal()`. Throws `RangeError` if too
421
+ short.
422
+
423
+ ---
424
+
425
+ #### `dispose(): void`
426
+
427
+ Wipes all key material and intermediate state from WASM memory. Delegates to
428
+ internal SerpentCtr, HMAC_SHA256, and HKDF_SHA256 instances.
429
+
430
+ **Security properties:**
431
+
432
+ - **Per-chunk EtM** -- HMAC-SHA256 over ciphertext, verified before decrypt.
433
+ - **Position binding** -- chunk index encoded in HKDF `info`. Reordering chunks
434
+ produces wrong keys; MAC fails.
435
+ - **Truncation detection** -- final chunk derives different keys than any
436
+ intermediate chunk at the same index.
437
+ - **Implicit header integrity** -- HKDF `info` embeds the full header. Tampering
438
+ with any header field invalidates every chunk's MAC.
439
+ - **Domain separation** -- `"serpent-stream-v1"` prefix prevents key confusion
440
+ with SerpentSeal or other constructions.
441
+
442
+ > [!IMPORTANT]
443
+ > This is a bespoke construction (no external RFC). The compositional security
444
+ > argument rests on HKDF (RFC 5869), HMAC-EtM, and Serpent-CTR. See
445
+ > [sha2.md](./sha2.md) for HKDF details.
446
+
447
+ > [!NOTE]
448
+ > `sealChunk` and `openChunk` are exported from the serpent submodule for
449
+ > internal use by the pool worker. They are not public API -- callers should use
450
+ > `SerpentStream` or `SerpentStreamPool`.
451
+
452
+ ---
453
+
454
+ ### SerpentStreamPool
455
+
456
+ Parallel worker pool for `SerpentStream`. Same wire format, same security
457
+ properties, faster on multi-core hardware for large payloads. Each worker owns
458
+ its own `serpent.wasm` and `sha2.wasm` instances with isolated linear memory.
459
+
460
+ `SerpentStream.seal()` and `SerpentStreamPool.seal()` produce compatible wire
461
+ formats -- either can decrypt the other's output.
462
+
463
+ ```typescript
464
+ class SerpentStreamPool {
465
+ static async create(opts?: StreamPoolOpts): Promise<SerpentStreamPool>
466
+ seal(key: Uint8Array, plaintext: Uint8Array, chunkSize?: number): Promise<Uint8Array>
467
+ open(key: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array>
468
+ dispose(): void
469
+ get size(): number
470
+ get queueDepth(): number
471
+ }
472
+ ```
473
+
474
+ #### `static async create(opts?: StreamPoolOpts): Promise<SerpentStreamPool>`
475
+
476
+ Creates a new pool. Requires `init(['serpent', 'sha2'])` to have been called.
477
+ Compiles both WASM modules once and distributes them to all workers.
478
+
479
+ - **opts.workers** -- number of workers to spawn. Default:
480
+ `navigator.hardwareConcurrency ?? 4`.
481
+
482
+ Uses a static factory pattern because worker initialization is async (WASM
483
+ compilation and instantiation happen per worker).
484
+
485
+ ---
486
+
487
+ #### `seal(key, plaintext, chunkSize?)`
488
+
489
+ Same parameters as `SerpentStream.seal()`, but returns a `Promise`. Key
490
+ derivation happens on the main thread; chunk encryption is parallelised across
491
+ workers.
492
+
493
+ ---
494
+
495
+ #### `open(key, ciphertext)`
496
+
497
+ Same parameters as `SerpentStream.open()`, but returns a `Promise`. If any chunk
498
+ fails authentication, the promise rejects immediately -- no partial plaintext is
499
+ returned.
500
+
501
+ ---
502
+
503
+ #### `dispose()`
504
+
505
+ Terminates all workers. Rejects all pending and queued jobs. Must be called to
506
+ release worker resources when the pool is no longer needed.
507
+
508
+ ---
509
+
510
+ ### SerpentStreamSealer / SerpentStreamOpener
511
+
512
+ Incremental streaming AEAD — seal and open one chunk at a time without holding
513
+ the full message in memory. Unlike `SerpentStream` (which is one-shot),
514
+ `SerpentStreamSealer` produces chunks as data arrives and `SerpentStreamOpener`
515
+ authenticates and decrypts them individually.
516
+
517
+ **Wire format:**
518
+ ```
519
+ header: nonce (16) || chunkSize_u32be (4) = 20 bytes
520
+ chunk: IV (16) || CBC_ciphertext (PKCS7-padded) || HMAC-SHA256 (32)
521
+ ```
522
+
523
+ Per-chunk keys are derived via HKDF-SHA256 from the stream key and a `chunkInfo`
524
+ blob binding the stream nonce, chunk size, chunk index, and `isLast` flag. Each
525
+ chunk is independently authenticated and position-bound — reordering, truncation,
526
+ and cross-stream splicing are all detected.
527
+
528
+ > [!NOTE]
529
+ > `SerpentStreamSealer` requires a 64-byte key (same as `SerpentSeal`). HKDF
530
+ > derives a fresh `encKey` + `macKey` pair for every chunk.
531
+
532
+ > [!IMPORTANT]
533
+ > The sealer produces a 20-byte header that **must** be transmitted to the opener
534
+ > before any chunks. The opener is initialized with this header.
535
+
536
+ ```typescript
537
+ class SerpentStreamSealer {
538
+ constructor(key: Uint8Array, chunkSize?: number)
539
+ header(): Uint8Array // call once before seal() — returns 20 bytes
540
+ seal(plaintext: Uint8Array): Uint8Array // exactly chunkSize bytes
541
+ final(plaintext: Uint8Array): Uint8Array // <= chunkSize bytes; wipes on return
542
+ dispose(): void // abort mid-stream; wipes without final chunk
543
+ }
544
+
545
+ class SerpentStreamOpener {
546
+ constructor(key: Uint8Array, header: Uint8Array)
547
+ open(chunk: Uint8Array): Uint8Array // throws on auth failure or post-final
548
+ dispose(): void
549
+ }
550
+ ```
551
+
552
+ #### Sealer state machine
553
+
554
+ | State | Valid calls |
555
+ |---|---|
556
+ | `fresh` | `header()`, `dispose()` |
557
+ | `sealing` | `seal()`, `final()`, `dispose()` |
558
+ | `dead` | `dispose()` (no-op) |
559
+
560
+ `header()` transitions `fresh → sealing`. `final()` seals the last chunk, wipes
561
+ all key material, and transitions to `dead`. `dispose()` wipes and transitions to
562
+ `dead` from any state — use it to abort a stream before `final()` is called.
563
+
564
+ Calling `header()` twice, `seal()` before `header()`, or any method after `final()`
565
+ all throw immediately.
566
+
567
+ ---
568
+
569
+ #### Opener state machine
570
+
571
+ The opener is ready as soon as it is constructed. It calls `open()` for each
572
+ chunk in order. Once a chunk with `isLast` set passes authentication, the opener
573
+ wipes its key material and transitions to `dead`. Subsequent `open()` calls throw.
574
+
575
+ `dispose()` wipes and marks the instance dead from any state.
576
+
577
+ ---
578
+
579
+ #### `constructor(key, chunkSize?)`
580
+
581
+ - **key** — 64-byte key. Throws `RangeError` if wrong length.
582
+ - **chunkSize** — bytes per chunk. Must be 1024–65536. Default: 65536. Throws
583
+ `RangeError` if out of range.
584
+
585
+ ---
586
+
587
+ #### `header()`
588
+
589
+ Returns the 20-byte stream header (`nonce || u32be(chunkSize)`). Must be called
590
+ once before the first `seal()`. Throws if called a second time or after `final()`.
591
+
592
+ ---
593
+
594
+ #### `seal(plaintext)`
595
+
596
+ Seals one chunk. **Plaintext must be exactly `chunkSize` bytes.** Returns
597
+ `IV (16) || ciphertext || HMAC (32)`. Throws `RangeError` if wrong size. Throws
598
+ if called before `header()` or after `final()`.
599
+
600
+ ---
601
+
602
+ #### `final(plaintext)`
603
+
604
+ Seals the last chunk. Plaintext may be 0–`chunkSize` bytes (partial chunk is
605
+ valid). After producing output, wipes all key material and marks the sealer dead.
606
+ Throws `RangeError` if plaintext exceeds `chunkSize`.
607
+
608
+ ---
609
+
610
+ #### `dispose()` (sealer)
611
+
612
+ Aborts the stream. Wipes key material without producing a final chunk. The opener
613
+ will see an incomplete stream and throw when it detects a missing final chunk.
614
+ Safe to call after `final()` — no-op if already dead.
615
+
616
+ ---
617
+
618
+ #### `constructor(key, header)` (opener)
619
+
620
+ - **key** — 64-byte key. Throws `RangeError` if wrong length.
621
+ - **header** — 20-byte stream header from `sealer.header()`. Throws `RangeError`
622
+ if wrong length.
623
+
624
+ ---
625
+
626
+ #### `open(chunk)`
627
+
628
+ Authenticates and decrypts one chunk. Throws `Error` on authentication failure.
629
+ Throws `Error` if called after the final chunk has already been opened. Returns
630
+ plaintext bytes (PKCS7 padding stripped).
631
+
632
+ ---
633
+
634
+ #### `dispose()` (opener)
635
+
636
+ Wipes key material. Safe to call at any point — use to abort opening a stream
637
+ early.
638
+
639
+ ---
640
+
641
+ ## Usage Examples
642
+
643
+ ### Example 1: SerpentSeal (authenticated encryption)
644
+
645
+ ```typescript
646
+ import { init, SerpentSeal, randomBytes } from 'leviathan-crypto';
647
+
648
+ await init(['serpent', 'sha2']);
649
+
650
+ // 64-byte key: 32 bytes encryption + 32 bytes MAC
651
+ const key = randomBytes(64);
652
+
653
+ const seal = new SerpentSeal();
654
+
655
+ const plaintext = new TextEncoder().encode('Authenticated secret message.');
656
+ const ciphertext = seal.encrypt(key, plaintext);
657
+ const decrypted = seal.decrypt(key, ciphertext);
658
+
659
+ console.log(new TextDecoder().decode(decrypted));
660
+ // "Authenticated secret message."
661
+
662
+ seal.dispose();
663
+ ```
664
+
665
+ ### Example 2: CTR mode (advanced)
666
+
667
+ Advanced use. For authenticated encryption, use `SerpentSeal`.
668
+
669
+ Use `SerpentCtr` to encrypt data of any length. CTR mode produces ciphertext
670
+ that is the same length as the plaintext -- no padding overhead.
671
+
672
+ ```typescript
673
+ import { init, SerpentCtr, randomBytes } from 'leviathan-crypto';
674
+
675
+ await init(['serpent']);
676
+
677
+ const key = randomBytes(32); // 256-bit key
678
+ const nonce = randomBytes(16); // 16-byte nonce -- NEVER reuse with the same key
679
+
680
+ const ctr = new SerpentCtr({ dangerUnauthenticated: true });
681
+
682
+ // Encrypt
683
+ ctr.beginEncrypt(key, nonce);
684
+ const ciphertext1 = ctr.encryptChunk(new TextEncoder().encode('Hello, '));
685
+ const ciphertext2 = ctr.encryptChunk(new TextEncoder().encode('world!'));
686
+
687
+ // Decrypt (same key and nonce)
688
+ ctr.beginDecrypt(key, nonce);
689
+ const plain1 = ctr.decryptChunk(ciphertext1);
690
+ const plain2 = ctr.decryptChunk(ciphertext2);
691
+
692
+ console.log(new TextDecoder().decode(plain1)); // "Hello, "
693
+ console.log(new TextDecoder().decode(plain2)); // "world!"
694
+
695
+ // Wipe key material
696
+ ctr.dispose();
697
+ ```
698
+
699
+ > [!IMPORTANT]
700
+ > CTR mode is unauthenticated. An attacker can tamper with the
701
+ > ciphertext without detection. Use `SerpentSeal` for authenticated encryption.
702
+
703
+ ### Example 3: CBC mode (advanced)
704
+
705
+ Advanced use. For authenticated encryption, use `SerpentSeal`.
706
+
707
+ Use `SerpentCbc` for message-level encryption with automatic PKCS7 padding.
708
+
709
+ ```typescript
710
+ import { init, SerpentCbc, randomBytes } from 'leviathan-crypto';
711
+
712
+ await init(['serpent']);
713
+
714
+ const key = randomBytes(32); // 256-bit key
715
+ const iv = randomBytes(16); // Random IV -- must be unique per message
716
+
717
+ const cbc = new SerpentCbc({ dangerUnauthenticated: true });
718
+
719
+ // Encrypt
720
+ const plaintext = new TextEncoder().encode('This is a secret message.');
721
+ const ciphertext = cbc.encrypt(key, iv, plaintext);
722
+
723
+ // Decrypt
724
+ const decrypted = cbc.decrypt(key, iv, ciphertext);
725
+ console.log(new TextDecoder().decode(decrypted)); // "This is a secret message."
726
+
727
+ // Wipe key material
728
+ cbc.dispose();
729
+ ```
730
+
731
+ > [!IMPORTANT]
732
+ > CBC mode is unauthenticated. Use `SerpentSeal` for authenticated encryption.
733
+
734
+ ### Example 4: SerpentStream (chunked authenticated encryption)
735
+
736
+ Use `SerpentStream` for large payloads where holding the entire plaintext in
737
+ memory is undesirable.
738
+
739
+ ```typescript
740
+ import { init, SerpentStream, randomBytes } from 'leviathan-crypto';
741
+
742
+ await init(['serpent', 'sha2']);
743
+
744
+ const key = randomBytes(32); // 32-byte key (HKDF handles expansion)
745
+
746
+ const stream = new SerpentStream();
747
+
748
+ const plaintext = new Uint8Array(1024 * 1024); // 1 MB
749
+ crypto.getRandomValues(plaintext);
750
+
751
+ const ciphertext = stream.seal(key, plaintext); // default 64KB chunks
752
+ const decrypted = stream.open(key, ciphertext);
753
+
754
+ // decrypted is byte-identical to plaintext
755
+
756
+ stream.dispose();
757
+ ```
758
+
759
+ ### Example 5: SerpentStreamPool (parallel chunked encryption)
760
+
761
+ Use `SerpentStreamPool` for maximum throughput on multi-core hardware.
762
+
763
+ ```typescript
764
+ import { init, SerpentStreamPool, randomBytes } from 'leviathan-crypto';
765
+
766
+ await init(['serpent', 'sha2']);
767
+
768
+ const pool = await SerpentStreamPool.create({ workers: 4 });
769
+
770
+ const key = randomBytes(32);
771
+ const plaintext = new Uint8Array(10 * 1024 * 1024); // 10 MB
772
+
773
+ const ciphertext = await pool.seal(key, plaintext);
774
+ const decrypted = await pool.open(key, ciphertext);
775
+
776
+ // decrypted is byte-identical to plaintext
777
+
778
+ pool.dispose(); // terminates workers
779
+ ```
780
+
781
+ ### Example 6: SerpentStreamSealer / SerpentStreamOpener (incremental streaming)
782
+
783
+ Use `SerpentStreamSealer` when data arrives in chunks and you cannot buffer the
784
+ entire plaintext before encrypting — network streams, file processors, live feeds.
785
+
786
+ ```typescript
787
+ import { init, SerpentStreamSealer, SerpentStreamOpener, randomBytes } from 'leviathan-crypto';
788
+
789
+ await init(['serpent', 'sha2']);
790
+
791
+ const key = randomBytes(64); // 64-byte key
792
+ const chunkSize = 65536; // 64 KB chunks
793
+
794
+ // ── Seal side ────────────────────────────────────────────────────────────────
795
+
796
+ const sealer = new SerpentStreamSealer(key, chunkSize);
797
+ const header = sealer.header(); // transmit this to the opener first
798
+
799
+ // seal() as data arrives — each chunk must be exactly chunkSize bytes
800
+ const chunk0 = sealer.seal(plaintext0);
801
+ const chunk1 = sealer.seal(plaintext1);
802
+
803
+ // final() for the last chunk — may be shorter than chunkSize
804
+ const lastChunk = sealer.final(lastPlaintext);
805
+ // sealer is now dead — key material wiped
806
+
807
+ // ── Open side ────────────────────────────────────────────────────────────────
808
+
809
+ const opener = new SerpentStreamOpener(key, header);
810
+
811
+ const pt0 = opener.open(chunk0);
812
+ const pt1 = opener.open(chunk1);
813
+ const ptN = opener.open(lastChunk); // opener detects isLast, wipes on return
814
+ // opener is now dead
815
+
816
+ // Truncation and reordering are detected — open() throws on auth failure
817
+ ```
818
+
819
+ To abort a stream mid-way (e.g. on connection drop):
820
+
821
+ ```typescript
822
+ sealer.dispose(); // wipes key material without producing a final chunk
823
+ // opener will throw when it receives no more chunks
824
+ ```
825
+
826
+ ### Example 7: Raw block operations (low-level)
827
+
828
+ Use the `Serpent` class for single 16-byte block operations. This is the lowest
829
+ level API, most users should use `SerpentSeal` instead.
830
+
831
+ ```typescript
832
+ import { init, Serpent } from 'leviathan-crypto';
833
+
834
+ await init(['serpent']);
835
+
836
+ const cipher = new Serpent();
837
+
838
+ // Load a 256-bit key (32 bytes)
839
+ const key = new Uint8Array(32);
840
+ crypto.getRandomValues(key);
841
+ cipher.loadKey(key);
842
+
843
+ // Encrypt a 16-byte block
844
+ const plaintext = new Uint8Array([
845
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
846
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
847
+ ]);
848
+ const ciphertext = cipher.encryptBlock(plaintext);
849
+
850
+ // Decrypt it back
851
+ const decrypted = cipher.decryptBlock(ciphertext);
852
+ // decrypted is identical to plaintext
853
+
854
+ // Wipe key material from memory when done
855
+ cipher.dispose();
856
+ ```
857
+
858
+ ---
859
+
860
+ ## Error Conditions
861
+
862
+ | Condition | Error type | Message |
863
+ |-----------|-----------|---------|
864
+ | `SerpentSeal` constructed before `init(['serpent', 'sha2'])` | `Error` | `leviathan-crypto: call init(['serpent', 'sha2']) before using SerpentSeal` |
865
+ | `SerpentSeal` key is not 64 bytes | `RangeError` | `SerpentSeal key must be 64 bytes (got N)` |
866
+ | `SerpentSeal` data too short for decrypt | `RangeError` | `SerpentSeal ciphertext too short` |
867
+ | `SerpentSeal` authentication failed | `Error` | `SerpentSeal: authentication failed` |
868
+ | `init(['serpent'])` not called before constructing `Serpent` | `Error` | `leviathan-crypto: call init(['serpent']) before using this class` |
869
+ | `SerpentCbc` constructed without `{ dangerUnauthenticated: true }` | `Error` | `leviathan-crypto: SerpentCbc is unauthenticated — use SerpentSeal instead. To use SerpentCbc directly, pass { dangerUnauthenticated: true }.` |
870
+ | `SerpentCtr` constructed without `{ dangerUnauthenticated: true }` | `Error` | `leviathan-crypto: SerpentCtr is unauthenticated — use SerpentSeal instead. To use SerpentCtr directly, pass { dangerUnauthenticated: true }.` |
871
+ | Key is not 16, 24, or 32 bytes (`Serpent.loadKey`) | `RangeError` | `key must be 16, 24, or 32 bytes (got N)` |
872
+ | Key is not 16, 24, or 32 bytes (`SerpentCbc`) | `RangeError` | `Serpent key must be 16, 24, or 32 bytes (got N)` |
873
+ | Key is not 16, 24, or 32 bytes (`SerpentCtr`) | `RangeError` | `key must be 16, 24, or 32 bytes` |
874
+ | Block is not 16 bytes (`Serpent`) | `RangeError` | `block must be 16 bytes (got N)` |
875
+ | Nonce is not 16 bytes (`SerpentCtr`) | `RangeError` | `nonce must be 16 bytes (got N)` |
876
+ | Chunk exceeds buffer size (`SerpentCtr`) | `RangeError` | `chunk exceeds maximum size of N bytes — split into smaller chunks` |
877
+ | IV is not 16 bytes (`SerpentCbc`) | `RangeError` | `CBC IV must be 16 bytes (got N)` |
878
+ | Ciphertext length is zero or not a multiple of 16 (`SerpentCbc.decrypt`) | `RangeError` | `ciphertext length must be a non-zero multiple of 16` |
879
+ | Invalid PKCS7 padding on decrypt (`SerpentCbc.decrypt`) | `RangeError` | `invalid PKCS7 padding` |
880
+ | `SerpentStream` constructed before `init(['serpent', 'sha2'])` | `Error` | `leviathan-crypto: call init(['serpent', 'sha2']) before using SerpentStream` |
881
+ | `SerpentStream` key is not 32 bytes | `RangeError` | `SerpentStream key must be 32 bytes (got N)` |
882
+ | `SerpentStream` chunkSize out of range | `RangeError` | `SerpentStream chunkSize must be 1024..65536 (got N)` |
883
+ | `SerpentStream` ciphertext too short | `RangeError` | `SerpentStream: ciphertext too short` |
884
+ | `SerpentStream` authentication failed | `Error` | `SerpentStream: authentication failed` |
885
+ | `SerpentStreamPool.create()` before `init(['serpent', 'sha2'])` | `Error` | `leviathan-crypto: call init(['serpent', 'sha2']) before using SerpentStreamPool` |
886
+ | `SerpentStreamPool` methods after `dispose()` | `Error` | `leviathan-crypto: pool is disposed` |
887
+ | `SerpentStreamSealer` constructed before `init(['serpent', 'sha2'])` | `Error` | `leviathan-crypto: call init(['serpent']) before using SerpentStreamSealer` |
888
+ | `SerpentStreamSealer` key is not 64 bytes | `RangeError` | `SerpentStreamSealer key must be 64 bytes (got N)` |
889
+ | `SerpentStreamSealer` chunkSize out of range | `RangeError` | `SerpentStreamSealer chunkSize must be 1024..65536 (got N)` |
890
+ | `SerpentStreamSealer.header()` called twice | `Error` | `SerpentStreamSealer: header() already called` |
891
+ | `SerpentStreamSealer.seal()` before `header()` | `Error` | `SerpentStreamSealer: call header() first` |
892
+ | `SerpentStreamSealer.seal()` or `final()` after `final()` or `dispose()` | `Error` | `SerpentStreamSealer: stream is closed` |
893
+ | `SerpentStreamSealer.seal()` wrong plaintext size | `RangeError` | `SerpentStreamSealer: seal() requires exactly N bytes (got M)` |
894
+ | `SerpentStreamSealer.final()` plaintext exceeds chunkSize | `RangeError` | `SerpentStreamSealer: final() plaintext exceeds chunkSize (got N)` |
895
+ | `SerpentStreamOpener` constructed before `init(['serpent', 'sha2'])` | `Error` | `leviathan-crypto: call init(['serpent']) before using SerpentStreamOpener` |
896
+ | `SerpentStreamOpener` key is not 64 bytes | `RangeError` | `SerpentStreamOpener key must be 64 bytes (got N)` |
897
+ | `SerpentStreamOpener` header is not 20 bytes | `RangeError` | `SerpentStreamOpener header must be 20 bytes (got N)` |
898
+ | `SerpentStreamOpener.open()` authentication failed | `Error` | `SerpentStreamOpener: authentication failed` |
899
+ | `SerpentStreamOpener.open()` after stream closed | `Error` | `SerpentStreamOpener: stream is closed` |
900
+
901
+ ---
902
+
903
+ ## Cross-References
904
+
905
+ - [README.md](./README.md) — library documentation index and exports table
906
+ - [architecture.md](./architecture.md) — module structure, buffer layouts, and build pipeline
907
+ - [asm_serpent.md](./asm_serpent.md) — WASM implementation details and buffer layout
908
+ - [serpent_reference.md](./serpent_reference.md) — algorithm specification, S-boxes, linear transform, and known attacks
909
+ - [serpent_audit.md](./serpent_audit.md) — security audit findings (correctness, side-channel analysis)
910
+ - [chacha20.md](./chacha20.md) — XChaCha20Poly1305 authenticated encryption (alternative AEAD)
911
+ - [sha2.md](./sha2.md) — HMAC-SHA256 and HKDF used internally by SerpentSeal and SerpentStream
912
+ - [types.md](./types.md) — `Blockcipher`, `Streamcipher`, and `AEAD` interfaces implemented by Serpent classes
913
+ - [utils.md](./utils.md) — `constantTimeEqual`, `wipe`, `randomBytes` used by Serpent wrappers
914
+