leviathan-crypto 1.4.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CLAUDE.md +129 -94
  2. package/README.md +166 -223
  3. package/SECURITY.md +90 -45
  4. package/dist/chacha20/cipher-suite.d.ts +4 -0
  5. package/dist/chacha20/cipher-suite.js +79 -0
  6. package/dist/chacha20/embedded.d.ts +1 -0
  7. package/dist/chacha20/embedded.js +27 -0
  8. package/dist/chacha20/index.d.ts +20 -27
  9. package/dist/chacha20/index.js +40 -59
  10. package/dist/chacha20/ops.d.ts +1 -1
  11. package/dist/chacha20/ops.js +19 -18
  12. package/dist/chacha20/pool-worker.js +77 -0
  13. package/dist/ct-wasm.d.ts +1 -0
  14. package/dist/ct-wasm.js +3 -0
  15. package/dist/ct.wasm +0 -0
  16. package/dist/docs/aead.md +323 -0
  17. package/dist/docs/architecture.md +427 -292
  18. package/dist/docs/argon2id.md +42 -30
  19. package/dist/docs/chacha20.md +192 -266
  20. package/dist/docs/exports.md +241 -0
  21. package/dist/docs/fortuna.md +60 -69
  22. package/dist/docs/init.md +172 -178
  23. package/dist/docs/loader.md +87 -142
  24. package/dist/docs/serpent.md +134 -583
  25. package/dist/docs/sha2.md +91 -103
  26. package/dist/docs/sha3.md +70 -36
  27. package/dist/docs/types.md +94 -16
  28. package/dist/docs/utils.md +109 -32
  29. package/dist/embedded/kyber.d.ts +1 -0
  30. package/dist/embedded/kyber.js +3 -0
  31. package/dist/embedded/serpent.d.ts +1 -1
  32. package/dist/embedded/serpent.js +1 -1
  33. package/dist/errors.d.ts +10 -0
  34. package/dist/errors.js +38 -0
  35. package/dist/fortuna.d.ts +0 -6
  36. package/dist/fortuna.js +5 -5
  37. package/dist/index.d.ts +25 -9
  38. package/dist/index.js +36 -7
  39. package/dist/init.d.ts +3 -7
  40. package/dist/init.js +18 -35
  41. package/dist/keccak/embedded.d.ts +1 -0
  42. package/dist/keccak/embedded.js +27 -0
  43. package/dist/keccak/index.d.ts +4 -0
  44. package/dist/keccak/index.js +31 -0
  45. package/dist/kyber/embedded.d.ts +1 -0
  46. package/dist/kyber/embedded.js +27 -0
  47. package/dist/kyber/indcpa.d.ts +49 -0
  48. package/dist/kyber/indcpa.js +352 -0
  49. package/dist/kyber/index.d.ts +38 -0
  50. package/dist/kyber/index.js +150 -0
  51. package/dist/kyber/kem.d.ts +21 -0
  52. package/dist/kyber/kem.js +160 -0
  53. package/dist/kyber/params.d.ts +14 -0
  54. package/dist/kyber/params.js +37 -0
  55. package/dist/kyber/suite.d.ts +13 -0
  56. package/dist/kyber/suite.js +94 -0
  57. package/dist/kyber/types.d.ts +98 -0
  58. package/dist/kyber/types.js +25 -0
  59. package/dist/kyber/validate.d.ts +19 -0
  60. package/dist/kyber/validate.js +68 -0
  61. package/dist/kyber.wasm +0 -0
  62. package/dist/loader.d.ts +15 -6
  63. package/dist/loader.js +65 -21
  64. package/dist/serpent/cipher-suite.d.ts +4 -0
  65. package/dist/serpent/cipher-suite.js +122 -0
  66. package/dist/serpent/embedded.d.ts +1 -0
  67. package/dist/serpent/embedded.js +27 -0
  68. package/dist/serpent/index.d.ts +6 -37
  69. package/dist/serpent/index.js +9 -118
  70. package/dist/serpent/pool-worker.d.ts +1 -0
  71. package/dist/serpent/pool-worker.js +208 -0
  72. package/dist/serpent/serpent-cbc.d.ts +30 -0
  73. package/dist/serpent/serpent-cbc.js +142 -0
  74. package/dist/serpent.wasm +0 -0
  75. package/dist/sha2/embedded.d.ts +1 -0
  76. package/dist/sha2/embedded.js +27 -0
  77. package/dist/sha2/hkdf.js +6 -2
  78. package/dist/sha2/index.d.ts +3 -2
  79. package/dist/sha2/index.js +3 -4
  80. package/dist/sha3/embedded.d.ts +1 -0
  81. package/dist/sha3/embedded.js +27 -0
  82. package/dist/sha3/index.d.ts +3 -2
  83. package/dist/sha3/index.js +3 -4
  84. package/dist/stream/constants.d.ts +6 -0
  85. package/dist/stream/constants.js +30 -0
  86. package/dist/stream/header.d.ts +9 -0
  87. package/dist/stream/header.js +77 -0
  88. package/dist/stream/index.d.ts +7 -0
  89. package/dist/stream/index.js +27 -0
  90. package/dist/stream/open-stream.d.ts +21 -0
  91. package/dist/stream/open-stream.js +146 -0
  92. package/dist/stream/seal-stream-pool.d.ts +38 -0
  93. package/dist/stream/seal-stream-pool.js +400 -0
  94. package/dist/stream/seal-stream.d.ts +20 -0
  95. package/dist/stream/seal-stream.js +142 -0
  96. package/dist/stream/seal.d.ts +9 -0
  97. package/dist/stream/seal.js +75 -0
  98. package/dist/stream/types.d.ts +25 -0
  99. package/dist/stream/types.js +26 -0
  100. package/dist/utils.d.ts +7 -2
  101. package/dist/utils.js +49 -3
  102. package/dist/wasm-source.d.ts +12 -0
  103. package/dist/wasm-source.js +26 -0
  104. package/package.json +13 -5
  105. package/dist/chacha20/pool.d.ts +0 -52
  106. package/dist/chacha20/pool.js +0 -178
  107. package/dist/chacha20/pool.worker.js +0 -37
  108. package/dist/chacha20/stream-sealer.d.ts +0 -49
  109. package/dist/chacha20/stream-sealer.js +0 -327
  110. package/dist/docs/chacha20_pool.md +0 -309
  111. package/dist/docs/wasm.md +0 -194
  112. package/dist/serpent/seal.d.ts +0 -8
  113. package/dist/serpent/seal.js +0 -72
  114. package/dist/serpent/stream-pool.d.ts +0 -48
  115. package/dist/serpent/stream-pool.js +0 -275
  116. package/dist/serpent/stream-sealer.d.ts +0 -55
  117. package/dist/serpent/stream-sealer.js +0 -342
  118. package/dist/serpent/stream.d.ts +0 -28
  119. package/dist/serpent/stream.js +0 -205
  120. package/dist/serpent/stream.worker.d.ts +0 -32
  121. package/dist/serpent/stream.worker.js +0 -117
  122. /package/dist/chacha20/{pool.worker.d.ts → pool-worker.d.ts} +0 -0
@@ -1,309 +0,0 @@
1
- # XChaCha20Poly1305Pool: Parallel Worker Pool for Authenticated Encryption
2
-
3
- > [!NOTE]
4
- > A worker pool that dispatches independent XChaCha20-Poly1305 AEAD operations
5
- > across multiple Web Workers, each with its own isolated WebAssembly instance.
6
- >
7
- > See [ChaCha20-Poly1305 implementation audit](./chacha_audit.md) for algorithm correctness verifications.
8
-
9
- ## Overview
10
-
11
- `XChaCha20Poly1305Pool` parallelizes XChaCha20-Poly1305 encrypt and decrypt
12
- operations across Web Workers. Each worker owns its own `WebAssembly.Instance`
13
- with its own linear memory -- there is no shared state between workers.
14
-
15
- Use the pool when you need to process many independent AEAD operations
16
- concurrently. Typical use cases include encrypting multiple independent messages,
17
- batch processing encrypted records, or any scenario where multiple independent
18
- encrypt/decrypt operations could benefit from parallelism.
19
-
20
- Use the single-instance `XChaCha20Poly1305` when operations are sequential, when
21
- you only process one message at a time, or when the overhead of worker
22
- communication is not justified by the operation size.
23
-
24
- **Throughput ceiling:** CPU-bound WASM throughput plateaus at
25
- `navigator.hardwareConcurrency`. Adding more workers beyond this adds scheduling
26
- overhead with no parallelism gain.
27
-
28
- **Per-job size limit:** Each job is limited to 64 KB, the same limit as the
29
- single-instance path. This is not a workaround limitation -- it is the correct
30
- security boundary for independent AEAD operations. Each job is one complete,
31
- independently authenticated AEAD operation. Do not split one logical message
32
- across multiple pool calls and concatenate results -- this provides no
33
- stream-level authenticity (reordering and truncation attacks go undetected).
34
-
35
- ---
36
-
37
- ## Security Notes
38
-
39
- - **Input buffers are transferred (neutered) after dispatch.** Once you call
40
- `encrypt()` or `decrypt()`, the `key`, `nonce`, `plaintext`/`ciphertext`, and
41
- `aad` buffers are transferred to the worker via `Transferable`. The caller's
42
- `Uint8Array` views become detached -- reading them after the call returns
43
- zero-length buffers. If you need to retain any input after calling
44
- `encrypt()`/`decrypt()`, copy it first with `.slice()`.
45
-
46
- - **64 KB limit is per independent AEAD operation.** Do not split one logical
47
- message across multiple pool calls and concatenate the results. This creates a
48
- stream without authentication -- an attacker can reorder, duplicate, or
49
- truncate chunks without detection. A future chunked-AEAD streaming API is the
50
- correct tool for large files.
51
-
52
- - **All XChaCha20-Poly1305 security properties apply.** Nonce uniqueness per key
53
- is required. The 24-byte nonce is safe for random generation via
54
- `crypto.getRandomValues()` (collision probability is negligible for 2^64
55
- messages).
56
-
57
- - **Each worker owns isolated WASM memory.** Key material in one worker's linear
58
- memory cannot leak to another worker, even in theory.
59
-
60
- - **Workers are terminated on `dispose()`.** All WASM memory is released when
61
- the worker process ends. There is no lingering key material.
62
-
63
- ---
64
-
65
- ## API Reference
66
-
67
- ### `PoolOpts`
68
-
69
- ```typescript
70
- interface PoolOpts {
71
- /** Number of workers. Default: navigator.hardwareConcurrency ?? 4 */
72
- workers?: number;
73
- }
74
- ```
75
-
76
- ---
77
-
78
- ### `XChaCha20Poly1305Pool.create(opts?)`
79
-
80
- Static async factory. Returns a `Promise<XChaCha20Poly1305Pool>`.
81
-
82
- ```typescript
83
- static async create(opts?: PoolOpts): Promise<XChaCha20Poly1305Pool>
84
- ```
85
-
86
- | Parameter | Type | Default | Description |
87
- |-----------|------|---------|-------------|
88
- | `opts.workers` | `number` | `navigator.hardwareConcurrency ?? 4` | Number of workers to spawn. |
89
-
90
- Throws if `init(['chacha20'])` has not been called.
91
-
92
- Direct construction with `new XChaCha20Poly1305Pool()` is not possible -- the
93
- constructor is private.
94
-
95
- ---
96
-
97
- ### `encrypt(key, nonce, plaintext, aad?)`
98
-
99
- Encrypt plaintext with XChaCha20-Poly1305.
100
-
101
- ```typescript
102
- encrypt(
103
- key: Uint8Array, // 32 bytes
104
- nonce: Uint8Array, // 24 bytes
105
- plaintext: Uint8Array, // up to 64 KB
106
- aad?: Uint8Array, // optional additional authenticated data
107
- ): Promise<Uint8Array> // ciphertext || tag (plaintext.length + 16 bytes)
108
- ```
109
-
110
- | Parameter | Type | Constraints | Description |
111
- |-----------|------|-------------|-------------|
112
- | `key` | `Uint8Array` | 32 bytes | Encryption key |
113
- | `nonce` | `Uint8Array` | 24 bytes | Unique nonce |
114
- | `plaintext` | `Uint8Array` | 0--65536 bytes | Data to encrypt |
115
- | `aad` | `Uint8Array` | any length | Additional authenticated data (default: empty) |
116
-
117
- Returns `ciphertext || tag` (`plaintext.length + 16` bytes).
118
-
119
- > [!WARNING]
120
- > All input buffers are transferred and neutered after dispatch.
121
-
122
- ### `decrypt(key, nonce, ciphertext, aad?)`
123
-
124
- Decrypt ciphertext with XChaCha20-Poly1305.
125
-
126
- ```typescript
127
- decrypt(
128
- key: Uint8Array, // 32 bytes
129
- nonce: Uint8Array, // 24 bytes
130
- ciphertext: Uint8Array, // ciphertext || tag (at least 16 bytes)
131
- aad?: Uint8Array, // must match the AAD used during encryption
132
- ): Promise<Uint8Array> // plaintext
133
- ```
134
-
135
- | Parameter | Type | Constraints | Description |
136
- |-----------|------|-------------|-------------|
137
- | `key` | `Uint8Array` | 32 bytes | Decryption key |
138
- | `nonce` | `Uint8Array` | 24 bytes | Same nonce used for encryption |
139
- | `ciphertext` | `Uint8Array` | >= 16 bytes | `ciphertext || tag` from `encrypt()` |
140
- | `aad` | `Uint8Array` | any length | Same AAD used during encryption (default: empty) |
141
-
142
- Returns the decrypted plaintext.
143
-
144
- Rejects with `Error('ChaCha20Poly1305: authentication failed')` if the tag does
145
- not match (tampered ciphertext, wrong key, wrong nonce, or wrong AAD).
146
-
147
- > [!WARNING]
148
- > All input buffers are transferred and neutered after dispatch.
149
-
150
- ### `dispose()`
151
-
152
- Terminate all workers and reject all pending and queued jobs.
153
-
154
- ```typescript
155
- dispose(): void
156
- ```
157
-
158
- After `dispose()`, all calls to `encrypt()` and `decrypt()` reject immediately.
159
- Calling `dispose()` multiple times is safe (idempotent).
160
-
161
- ---
162
-
163
- ### `size`
164
-
165
- Number of workers in the pool.
166
-
167
- ```typescript
168
- get size(): number
169
- ```
170
-
171
- ---
172
-
173
- ### `queueDepth`
174
-
175
- Number of jobs currently queued (waiting for a free worker).
176
-
177
- ```typescript
178
- get queueDepth(): number
179
- ```
180
-
181
- Returns 0 when all workers are idle.
182
-
183
- ---
184
-
185
- ## Performance Notes
186
-
187
- Throughput plateaus at `navigator.hardwareConcurrency` workers for CPU-bound
188
- WASM operations. Adding more workers beyond this count introduces scheduling
189
- overhead without additional parallelism.
190
-
191
- The `workers` option lets you tune the count:
192
- - **Default** (`navigator.hardwareConcurrency ?? 4`) -- optimal for most systems
193
- - **Fewer workers** -- useful if you need to leave cores available for other work
194
- - **More workers** -- only beneficial on hyperthreaded CPUs where
195
- `hardwareConcurrency` includes virtual cores that provide some additional
196
- throughput
197
-
198
- Each worker carries a fixed overhead: one `WebAssembly.Instance` (192 KB linear
199
- memory) plus the worker thread itself. For most workloads, the default is correct.
200
-
201
- Job dispatch uses `Transferable` buffers to avoid copy overhead on 64 KB payloads.
202
- The downside is that input buffers are neutered on the calling side -- see
203
- Security Notes.
204
-
205
- ---
206
-
207
- ## Usage Examples
208
-
209
- ### Basic -- create pool, encrypt/decrypt one message
210
-
211
- ```typescript
212
- import { init, XChaCha20Poly1305Pool, randomBytes } from 'leviathan-crypto'
213
-
214
- await init(['chacha20'])
215
-
216
- const pool = await XChaCha20Poly1305Pool.create()
217
-
218
- const key = randomBytes(32)
219
- const nonce = randomBytes(24)
220
- const plaintext = new TextEncoder().encode('Hello, world!')
221
-
222
- // Copy inputs before passing to the pool (they will be neutered)
223
- const ct = await pool.encrypt(key.slice(), nonce.slice(), plaintext.slice())
224
- const pt = await pool.decrypt(key.slice(), nonce.slice(), ct)
225
- console.log(new TextDecoder().decode(pt)) // "Hello, world!"
226
-
227
- pool.dispose()
228
- ```
229
-
230
- ### Concurrent burst -- `Promise.all()` over many independent messages
231
-
232
- ```typescript
233
- import { init, XChaCha20Poly1305Pool, randomBytes } from 'leviathan-crypto'
234
-
235
- await init(['chacha20'])
236
- const pool = await XChaCha20Poly1305Pool.create()
237
-
238
- const messages = ['message-1', 'message-2', 'message-3', 'message-4']
239
- const key = randomBytes(32)
240
-
241
- // Each message gets its own nonce -- all encrypt concurrently
242
- const encrypted = await Promise.all(
243
- messages.map(msg => {
244
- const nonce = randomBytes(24)
245
- const pt = new TextEncoder().encode(msg)
246
- return pool.encrypt(key.slice(), nonce, pt)
247
- })
248
- )
249
-
250
- pool.dispose()
251
- ```
252
-
253
- ### Manual worker count
254
-
255
- ```typescript
256
- const pool = await XChaCha20Poly1305Pool.create({ workers: 4 })
257
- console.log(pool.size) // 4
258
- ```
259
-
260
- ### Correct dispose pattern -- `try/finally`
261
-
262
- ```typescript
263
- const pool = await XChaCha20Poly1305Pool.create()
264
- try {
265
- const ct = await pool.encrypt(key, nonce, plaintext)
266
- // ... use ct ...
267
- } finally {
268
- pool.dispose()
269
- }
270
- ```
271
-
272
- ### What NOT to do -- splitting one message across pool calls
273
-
274
- ```typescript
275
- // WRONG -- this is NOT secure
276
- const chunk1 = await pool.encrypt(key, nonce1, largeFile.subarray(0, 65536))
277
- const chunk2 = await pool.encrypt(key, nonce2, largeFile.subarray(65536))
278
- const result = concat(chunk1, chunk2)
279
- // ^ An attacker can reorder, duplicate, or truncate chunks undetected.
280
- // There is no stream-level authentication.
281
- // Use a future chunked-AEAD streaming API for large files.
282
- ```
283
-
284
- ---
285
-
286
- ## Error Conditions
287
-
288
- | Condition | What happens |
289
- |-----------|-------------|
290
- | `init()` not called | `create()` throws: `leviathan-crypto: call init(['chacha20']) before using XChaCha20Poly1305Pool` |
291
- | `new XChaCha20Poly1305Pool()` | Compile-time error -- the constructor is private |
292
- | Wrong key length | `encrypt()`/`decrypt()` reject with `RangeError` |
293
- | Wrong nonce length | `encrypt()`/`decrypt()` reject with `RangeError` |
294
- | Ciphertext shorter than 16 bytes | `decrypt()` rejects with `RangeError` |
295
- | Authentication failure | `decrypt()` rejects with `Error('ChaCha20Poly1305: authentication failed')` |
296
- | Pool disposed | `encrypt()`/`decrypt()` reject with `Error('leviathan-crypto: pool is disposed')` |
297
- | Worker init failure | `create()` rejects with error message from the worker |
298
-
299
- ---
300
-
301
- > ## Cross-References
302
- >
303
- > - [index](./README.md) — Project Documentation index
304
- > - [chacha20](./chacha20.md) — single-instance XChaCha20-Poly1305 API
305
- > - [asm_chacha](./asm_chacha.md) — WASM implementation details (quarter-round, Poly1305 accumulator, HChaCha20)
306
- > - [wasm](./wasm.md) — WebAssembly primer: how one compiled module spawns many worker instances
307
- > - [fortuna](./fortuna.md) — another class using the `static async create()` factory pattern
308
- > - [architecture](./architecture.md) — architecture overview, module relationships, buffer layouts, and build pipeline
309
- > - [chacha_audit.md](./chacha_audit.md) — XChaCha20-Poly1305 implementation audit
package/dist/docs/wasm.md DELETED
@@ -1,194 +0,0 @@
1
- # WebAssembly Primer
2
-
3
- > [!NOTE]
4
- > - A short introduction to WebAssembly concepts as they apply to the leviathan-crypto library.
5
- > - If you already understand WASM, skip to [Project-Specific Concepts](#project-specific-concepts).
6
- > - WebAssembly specification and documentation: [webassembly.org](https://webassembly.org/)
7
-
8
- ## What is WebAssembly?
9
-
10
- WebAssembly (WASM) is a binary instruction format that runs in browsers and
11
- server-side runtimes alongside JavaScript. Rather than a programming language
12
- one writes by hand, it serves as a compilation target. Code is written in a higher-level
13
- language, compiled to `.wasm`, and then executed by the browser.
14
-
15
- Consider it a small, fast virtual machine built into every modern browser.
16
- JavaScript can load a `.wasm` binary, call its exported functions, and read
17
- its results. The WASM code runs in its own sandboxed memory space, and thus cannot
18
- touch the DOM, access JavaScript variables, or reach the network. It computes
19
- and returns values, and that is its sole function.
20
-
21
- ---
22
-
23
- ## How It Runs
24
-
25
- When a browser encounters a `.wasm` binary, it performs two steps:
26
-
27
- 1. Compilation: The binary is validated and compiled into native machine code.
28
- This is fast because WASM is already a low-level format, requiring less
29
- work for the compiler compared to parsing and optimizing JavaScript.
30
-
31
- 2. Instantiation: The compiled module is paired with its imports, such as a
32
- memory object, to create a live instance. The instance's exported functions
33
- are then callable from JavaScript.
34
-
35
- Once instantiated, calling a WASM function is similar to calling any JavaScript
36
- function: you pass arguments, it runs, and it returns a result. The key difference
37
- lies in how it runs. WASM execution is deterministic and not subject to the
38
- JIT compiler's speculative optimizations. Unlike JavaScript, the browser does not rewrite, inline,
39
- or de-optimize WASM code paths based on runtime profiling.
40
-
41
- ---
42
-
43
- ## Why We Use It
44
-
45
- Leviathan performs all cryptographic computations in WASM because JavaScript
46
- engines offer no formal constant-time guarantees for arbitrary code. The JIT
47
- compiler can introduce timing variations that leak information about secret
48
- data; WASM execution avoids this class of problem.
49
-
50
- For architectural details and security rationale, see [architecture.md](./architecture.md).
51
-
52
- **TLDR:** _TypeScript handles the API, and WASM handles the math._
53
-
54
- ---
55
-
56
- ## Core Concepts
57
-
58
- ### Module
59
-
60
- A `WebAssembly.Module` is a compiled `.wasm` binary and a stateless
61
- template for creating instances. You can compile a module once and create
62
- multiple instances from it. For example, [XChaCha20Poly1305Pool](./chacha20_pool.md)
63
- uses one compiled module to create many worker instances.
64
-
65
- ---
66
-
67
- ### Instance
68
-
69
- A WebAssembly.Instance is a live, runnable copy of a module, complete with
70
- its own memory and state. When you call `init('serpent')`, the library
71
- compiles the Serpent WASM binary and creates a single instance. All
72
- Serpent classes (`Serpent`, `SerpentCtr`, `SerpentCbc`) share this instance.
73
-
74
- ---
75
-
76
- ### Memory
77
-
78
- A `WebAssembly.Memory` is a contiguous block of bytes, essentially a
79
- `Uint8Array` that WASM functions can read and write, also known as **linear
80
- memory**. Each of our WASM modules gets its own memory (3 pages = 192 KB).
81
-
82
- The TypeScript layer communicates with WASM by writing inputs to specific offsets
83
- in this memory, calling a WASM function, and then reading the outputs from other
84
- offsets. There is no other communication channel, no function arguments for
85
- large data, and no return values beyond a single number. Memory is the data bus.
86
-
87
- ---
88
-
89
- ### Exports
90
-
91
- A WASM instance exposes exports: functions and memory that JavaScript can
92
- access. In leviathan-crypto, every WASM module exports:
93
-
94
- - **Getter functions** like getKeyOffset() and getChunkPtOffset(): these
95
- return the memory offsets where the TypeScript layer should write inputs
96
- or read outputs.
97
- - **Operation functions** like chachaEncryptChunk() and sha256Final():
98
- these perform the actual cryptographic computation on data already in memory.
99
- - **wipeBuffers():** this zeros all sensitive regions of memory and is called
100
- by every class's dispose() method.
101
- - **memory:** the linear memory object itself, which allows the TypeScript
102
- layer to create Uint8Array views over it.
103
-
104
- ---
105
-
106
- ### Imports
107
-
108
- When instantiating a module, you can pass **imports:** objects the WASM code
109
- needs from the host. Our modules import only one thing: the Memory object.
110
- We create the memory on the JavaScript side and pass it in, giving us
111
- control over its size and lifecycle.
112
-
113
- ---
114
-
115
- ## Project-Specific Concepts
116
-
117
- ### AssemblyScript
118
-
119
- The WASM binaries in this project are written in [AssemblyScript](https://www.assemblyscript.org/):
120
- a TypeScript-like language that compiles to WebAssembly. It resembles
121
- TypeScript but targets WASM instead of JavaScript. The source code
122
- resides in `src/asm/` and compiles into `.wasm` binaries in `build/`.
123
-
124
- AssemblyScript was selected because its syntax is familiar to TypeScript
125
- developers. It produces small binaries and grants low-level control over
126
- memory layout without requiring C, C++, or Rust.
127
-
128
- ---
129
-
130
- ### Thunks
131
-
132
- In this project, a **thunk** is a base64-encoded WASM binary embedded directly
133
- within a TypeScript file. The files in `src/ts/embedded/`
134
- (such as chacha20.ts and serpent.ts) each export a single constant:
135
-
136
- ```typescript
137
- export const WASM_BASE64 = 'AGFzbQEAAAAB...'
138
- ```
139
-
140
- This represents the entire compiled .wasm binary, encoded as a base64 string. When
141
- you call init('chacha20') in embedded mode (the default), the library
142
- imports this string, decodes it back into bytes, and compiles it into a
143
- `WebAssembly.Module`.
144
-
145
- Why embed the binary as a string? This approach enables the library to function with zero
146
- configuration. There's no need to serve .wasm files from a CDN, configure MIME
147
- types, or establish a build plugin to manage binary imports. Simply npm install and
148
- import. The tradeoff is bundle size, as base64 encoding increases the size by
149
- approximately 33% compared to the raw binary. For production deployments where bundle size is
150
- critical, the library also supports [streaming and manual loading modes](./loader.md).
151
-
152
- **TLDR:** Thunks are build artifacts generated by `scripts/embed-wasm.ts`.
153
- They are gitignored and regenerated during each build. Avoid manual edits.
154
-
155
- ---
156
-
157
- ### Buffer Layout
158
-
159
- Each WASM module divides its linear memory into fixed regions at known offsets.
160
- For example, the ChaCha20 module has a region for the key, a region for the
161
- nonce, a region for plaintext input, a region for ciphertext output, and so on.
162
- These offsets are defined in `src/asm/*/buffers.ts` and never change at runtime.
163
- The TypeScript layer calls getter functions (like `getKeyOffset()`) to
164
- determine where each region starts, then reads and writes `Uint8Array` slices at those
165
- positions. This is the only way data moves between TypeScript and
166
- WASM. There is no serialization, no copying to intermediate buffers, and no function call
167
- overhead for large data. Data is transferred via direct byte writes to shared memory.
168
-
169
- The buffer layouts for each module are documented in [architecture.md](./architecture.md).
170
-
171
- ---
172
-
173
- ### The `init()` Gate
174
-
175
- WASM modules must be compiled and instantiated before use. Because compilation
176
- returns a Promise, this is an asynchronous operation. Rather than hiding this
177
- behind lazy auto-initialization, which would make every cryptographic call
178
- implicitly asynchronous and create race conditions, the library requires an explicit
179
- `init()` call up front. If you forget, every class immediately throws an error message
180
- indicating which `init(init()` call is missing. _This is deliberate._
181
-
182
- See [init.md](./init.md) for the full API.
183
-
184
- ---
185
-
186
- > ## Cross-References
187
- >
188
- > - [index](./README.md) — Project Documentation index
189
- > - [architecture](./architecture.md) — architecture overview, module relationships, buffer layouts, and build pipeline
190
- > - [serpent_simd_bench](./serpent_simd_bench.md) Serpent-256 SIMD benchmark results: CTR and CBC-decrypt inter-block 4-wide, scalar vs SIMD across V8, SpiderMonkey, and JSC
191
- > - [chacha_simd_bench](./chacha_simd_bench.md) ChaCha20 SIMD benchmark results: 4-wide inter-block parallelism, scalar vs SIMD across V8, SpiderMonkey, and JSC. Includes documented negative result for intra-block approach
192
- > - [init](./init.md) — `init()` API and the three loading modes
193
- > - [loader](./loader.md) — how WASM binaries are loaded and instantiated
194
- > - [chacha20_pool](./chacha20_pool.md) — example of one compiled module spawning many instances across workers
@@ -1,8 +0,0 @@
1
- export declare class SerpentSeal {
2
- private readonly _cbc;
3
- private readonly _hmac;
4
- constructor();
5
- encrypt(key: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array, _iv?: Uint8Array): Uint8Array;
6
- decrypt(key: Uint8Array, data: Uint8Array, aad?: Uint8Array): Uint8Array;
7
- dispose(): void;
8
- }
@@ -1,72 +0,0 @@
1
- // ▄▄▄▄▄▄▄▄▄▄
2
- // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
- // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
- // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
- // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
- // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
- // ███████▌ ▀██▀ ███
8
- // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
- // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
- // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
- // ▀████▄ ▄██▄
12
- // ▐████ ▐███ Author: xero (https://x-e.ro)
13
- // ▄▄██████████ ▐███ ▄▄ License: MIT
14
- // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
- // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
- // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
- // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
- // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
- // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
- // ▀█████▀▀
21
- //
22
- // src/ts/serpent/seal.ts
23
- //
24
- // SerpentSeal — authenticated Serpent-256 encryption.
25
- // Encrypt-then-MAC: SerpentCbc + HMAC-SHA256. Tier 2 pure-TS composition.
26
- import { SerpentCbc, _serpentReady } from './index.js';
27
- import { HMAC_SHA256, _sha2Ready } from '../sha2/index.js';
28
- import { concat, constantTimeEqual } from '../utils.js';
29
- import { u32be } from './stream.js';
30
- export class SerpentSeal {
31
- _cbc;
32
- _hmac;
33
- constructor() {
34
- if (!_serpentReady() || !_sha2Ready())
35
- throw new Error('leviathan-crypto: call init([\'serpent\', \'sha2\']) before using SerpentSeal');
36
- this._cbc = new SerpentCbc({ dangerUnauthenticated: true });
37
- this._hmac = new HMAC_SHA256();
38
- }
39
- // _iv: test seam only — inject a fixed IV for deterministic KAT vectors
40
- // aad: authenticated but not encrypted; bound into HMAC-SHA256 tag
41
- encrypt(key, plaintext, aad = new Uint8Array(0), _iv) {
42
- if (key.length !== 64)
43
- throw new RangeError(`SerpentSeal key must be 64 bytes (got ${key.length})`);
44
- const encKey = key.subarray(0, 32);
45
- const macKey = key.subarray(32, 64);
46
- const iv = (_iv && _iv.length === 16) ? _iv : new Uint8Array(16);
47
- if (!_iv || _iv.length !== 16)
48
- crypto.getRandomValues(iv);
49
- const ciphertext = this._cbc.encrypt(encKey, iv, plaintext);
50
- const tag = this._hmac.hash(macKey, concat(u32be(aad.length), aad, iv, ciphertext));
51
- return concat(iv, ciphertext, tag);
52
- }
53
- decrypt(key, data, aad = new Uint8Array(0)) {
54
- if (key.length !== 64)
55
- throw new RangeError(`SerpentSeal key must be 64 bytes (got ${key.length})`);
56
- if (data.length < 64)
57
- throw new RangeError('SerpentSeal ciphertext too short');
58
- const encKey = key.subarray(0, 32);
59
- const macKey = key.subarray(32, 64);
60
- const iv = data.subarray(0, 16);
61
- const tag = data.subarray(data.length - 32);
62
- const ciphertext = data.subarray(16, data.length - 32);
63
- const expectedTag = this._hmac.hash(macKey, concat(u32be(aad.length), aad, iv, ciphertext));
64
- if (!constantTimeEqual(tag, expectedTag))
65
- throw new Error('SerpentSeal: authentication failed');
66
- return this._cbc.decrypt(encKey, iv, ciphertext);
67
- }
68
- dispose() {
69
- this._cbc.dispose();
70
- this._hmac.dispose();
71
- }
72
- }
@@ -1,48 +0,0 @@
1
- export interface StreamPoolOpts {
2
- /** Number of workers. Default: navigator.hardwareConcurrency ?? 4 */
3
- workers?: number;
4
- }
5
- /**
6
- * Parallel worker pool for SerpentStream chunked authenticated encryption.
7
- *
8
- * Each worker owns its own `serpent.wasm` and `sha2.wasm` instances with
9
- * isolated linear memory. Key derivation happens on the main thread; workers
10
- * receive pre-derived encKey/macKey per chunk.
11
- *
12
- * Produces the same wire format as `SerpentStream` -- either can decrypt
13
- * the other's output.
14
- */
15
- export declare class SerpentStreamPool {
16
- private readonly _workers;
17
- private readonly _idle;
18
- private readonly _queue;
19
- private readonly _pending;
20
- private readonly _hkdf;
21
- private _nextId;
22
- private _disposed;
23
- private constructor();
24
- /**
25
- * Create a new pool. Requires `init(['serpent', 'sha2'])` to have been called.
26
- * Compiles both WASM modules once and distributes them to all workers.
27
- */
28
- static create(opts?: StreamPoolOpts): Promise<SerpentStreamPool>;
29
- /**
30
- * Encrypt plaintext with SerpentStream chunked authenticated encryption.
31
- * Returns the complete wire format (header + encrypted chunks).
32
- */
33
- seal(key: Uint8Array, plaintext: Uint8Array, chunkSize?: number): Promise<Uint8Array>;
34
- /**
35
- * Decrypt SerpentStream wire format.
36
- * If any chunk fails authentication, rejects immediately -- no partial plaintext.
37
- */
38
- open(key: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array>;
39
- /** Terminates all workers. Rejects all pending and queued jobs. */
40
- dispose(): void;
41
- /** Number of workers in the pool. */
42
- get size(): number;
43
- /** Number of jobs currently queued (waiting for a free worker). */
44
- get queueDepth(): number;
45
- private _dispatch;
46
- private _send;
47
- private _onMessage;
48
- }