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,306 @@
|
|
|
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
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
`XChaCha20Poly1305Pool` parallelizes XChaCha20-Poly1305 encrypt and decrypt
|
|
10
|
+
operations across Web Workers. Each worker owns its own `WebAssembly.Instance`
|
|
11
|
+
with its own linear memory -- there is no shared state between workers.
|
|
12
|
+
|
|
13
|
+
Use the pool when you need to process many independent AEAD operations
|
|
14
|
+
concurrently. Typical use cases include encrypting multiple independent messages,
|
|
15
|
+
batch processing encrypted records, or any scenario where multiple independent
|
|
16
|
+
encrypt/decrypt operations could benefit from parallelism.
|
|
17
|
+
|
|
18
|
+
Use the single-instance `XChaCha20Poly1305` when operations are sequential, when
|
|
19
|
+
you only process one message at a time, or when the overhead of worker
|
|
20
|
+
communication is not justified by the operation size.
|
|
21
|
+
|
|
22
|
+
**Throughput ceiling:** CPU-bound WASM throughput plateaus at
|
|
23
|
+
`navigator.hardwareConcurrency`. Adding more workers beyond this adds scheduling
|
|
24
|
+
overhead with no parallelism gain.
|
|
25
|
+
|
|
26
|
+
**Per-job size limit:** Each job is limited to 64 KB, the same limit as the
|
|
27
|
+
single-instance path. This is not a workaround limitation -- it is the correct
|
|
28
|
+
security boundary for independent AEAD operations. Each job is one complete,
|
|
29
|
+
independently authenticated AEAD operation. Do not split one logical message
|
|
30
|
+
across multiple pool calls and concatenate results -- this provides no
|
|
31
|
+
stream-level authenticity (reordering and truncation attacks go undetected).
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Security Notes
|
|
36
|
+
|
|
37
|
+
- **Input buffers are transferred (neutered) after dispatch.** Once you call
|
|
38
|
+
`encrypt()` or `decrypt()`, the `key`, `nonce`, `plaintext`/`ciphertext`, and
|
|
39
|
+
`aad` buffers are transferred to the worker via `Transferable`. The caller's
|
|
40
|
+
`Uint8Array` views become detached -- reading them after the call returns
|
|
41
|
+
zero-length buffers. If you need to retain any input after calling
|
|
42
|
+
`encrypt()`/`decrypt()`, copy it first with `.slice()`.
|
|
43
|
+
|
|
44
|
+
- **64 KB limit is per independent AEAD operation.** Do not split one logical
|
|
45
|
+
message across multiple pool calls and concatenate the results. This creates a
|
|
46
|
+
stream without authentication -- an attacker can reorder, duplicate, or
|
|
47
|
+
truncate chunks without detection. A future chunked-AEAD streaming API is the
|
|
48
|
+
correct tool for large files.
|
|
49
|
+
|
|
50
|
+
- **All XChaCha20-Poly1305 security properties apply.** Nonce uniqueness per key
|
|
51
|
+
is required. The 24-byte nonce is safe for random generation via
|
|
52
|
+
`crypto.getRandomValues()` (collision probability is negligible for 2^64
|
|
53
|
+
messages).
|
|
54
|
+
|
|
55
|
+
- **Each worker owns isolated WASM memory.** Key material in one worker's linear
|
|
56
|
+
memory cannot leak to another worker, even in theory.
|
|
57
|
+
|
|
58
|
+
- **Workers are terminated on `dispose()`.** All WASM memory is released when
|
|
59
|
+
the worker process ends. There is no lingering key material.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## API Reference
|
|
64
|
+
|
|
65
|
+
### `PoolOpts`
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
interface PoolOpts {
|
|
69
|
+
/** Number of workers. Default: navigator.hardwareConcurrency ?? 4 */
|
|
70
|
+
workers?: number;
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### `XChaCha20Poly1305Pool.create(opts?)`
|
|
77
|
+
|
|
78
|
+
Static async factory. Returns a `Promise<XChaCha20Poly1305Pool>`.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
static async create(opts?: PoolOpts): Promise<XChaCha20Poly1305Pool>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Parameter | Type | Default | Description |
|
|
85
|
+
|-----------|------|---------|-------------|
|
|
86
|
+
| `opts.workers` | `number` | `navigator.hardwareConcurrency ?? 4` | Number of workers to spawn. |
|
|
87
|
+
|
|
88
|
+
Throws if `init(['chacha20'])` has not been called.
|
|
89
|
+
|
|
90
|
+
Direct construction with `new XChaCha20Poly1305Pool()` is not possible -- the
|
|
91
|
+
constructor is private.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### `encrypt(key, nonce, plaintext, aad?)`
|
|
96
|
+
|
|
97
|
+
Encrypt plaintext with XChaCha20-Poly1305.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
encrypt(
|
|
101
|
+
key: Uint8Array, // 32 bytes
|
|
102
|
+
nonce: Uint8Array, // 24 bytes
|
|
103
|
+
plaintext: Uint8Array, // up to 64 KB
|
|
104
|
+
aad?: Uint8Array, // optional additional authenticated data
|
|
105
|
+
): Promise<Uint8Array> // ciphertext || tag (plaintext.length + 16 bytes)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| Parameter | Type | Constraints | Description |
|
|
109
|
+
|-----------|------|-------------|-------------|
|
|
110
|
+
| `key` | `Uint8Array` | 32 bytes | Encryption key |
|
|
111
|
+
| `nonce` | `Uint8Array` | 24 bytes | Unique nonce |
|
|
112
|
+
| `plaintext` | `Uint8Array` | 0--65536 bytes | Data to encrypt |
|
|
113
|
+
| `aad` | `Uint8Array` | any length | Additional authenticated data (default: empty) |
|
|
114
|
+
|
|
115
|
+
Returns `ciphertext || tag` (`plaintext.length + 16` bytes).
|
|
116
|
+
|
|
117
|
+
> [!WARNING]
|
|
118
|
+
> All input buffers are transferred and neutered after dispatch.
|
|
119
|
+
|
|
120
|
+
### `decrypt(key, nonce, ciphertext, aad?)`
|
|
121
|
+
|
|
122
|
+
Decrypt ciphertext with XChaCha20-Poly1305.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
decrypt(
|
|
126
|
+
key: Uint8Array, // 32 bytes
|
|
127
|
+
nonce: Uint8Array, // 24 bytes
|
|
128
|
+
ciphertext: Uint8Array, // ciphertext || tag (at least 16 bytes)
|
|
129
|
+
aad?: Uint8Array, // must match the AAD used during encryption
|
|
130
|
+
): Promise<Uint8Array> // plaintext
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
| Parameter | Type | Constraints | Description |
|
|
134
|
+
|-----------|------|-------------|-------------|
|
|
135
|
+
| `key` | `Uint8Array` | 32 bytes | Decryption key |
|
|
136
|
+
| `nonce` | `Uint8Array` | 24 bytes | Same nonce used for encryption |
|
|
137
|
+
| `ciphertext` | `Uint8Array` | >= 16 bytes | `ciphertext || tag` from `encrypt()` |
|
|
138
|
+
| `aad` | `Uint8Array` | any length | Same AAD used during encryption (default: empty) |
|
|
139
|
+
|
|
140
|
+
Returns the decrypted plaintext.
|
|
141
|
+
|
|
142
|
+
Rejects with `Error('ChaCha20Poly1305: authentication failed')` if the tag does
|
|
143
|
+
not match (tampered ciphertext, wrong key, wrong nonce, or wrong AAD).
|
|
144
|
+
|
|
145
|
+
> [!WARNING]
|
|
146
|
+
> All input buffers are transferred and neutered after dispatch.
|
|
147
|
+
|
|
148
|
+
### `dispose()`
|
|
149
|
+
|
|
150
|
+
Terminate all workers and reject all pending and queued jobs.
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
dispose(): void
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
After `dispose()`, all calls to `encrypt()` and `decrypt()` reject immediately.
|
|
157
|
+
Calling `dispose()` multiple times is safe (idempotent).
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### `size`
|
|
162
|
+
|
|
163
|
+
Number of workers in the pool.
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
get size(): number
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### `queueDepth`
|
|
172
|
+
|
|
173
|
+
Number of jobs currently queued (waiting for a free worker).
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
get queueDepth(): number
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Returns 0 when all workers are idle.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Performance Notes
|
|
184
|
+
|
|
185
|
+
Throughput plateaus at `navigator.hardwareConcurrency` workers for CPU-bound
|
|
186
|
+
WASM operations. Adding more workers beyond this count introduces scheduling
|
|
187
|
+
overhead without additional parallelism.
|
|
188
|
+
|
|
189
|
+
The `workers` option lets you tune the count:
|
|
190
|
+
- **Default** (`navigator.hardwareConcurrency ?? 4`) -- optimal for most systems
|
|
191
|
+
- **Fewer workers** -- useful if you need to leave cores available for other work
|
|
192
|
+
- **More workers** -- only beneficial on hyperthreaded CPUs where
|
|
193
|
+
`hardwareConcurrency` includes virtual cores that provide some additional
|
|
194
|
+
throughput
|
|
195
|
+
|
|
196
|
+
Each worker carries a fixed overhead: one `WebAssembly.Instance` (192 KB linear
|
|
197
|
+
memory) plus the worker thread itself. For most workloads, the default is correct.
|
|
198
|
+
|
|
199
|
+
Job dispatch uses `Transferable` buffers to avoid copy overhead on 64 KB payloads.
|
|
200
|
+
The downside is that input buffers are neutered on the calling side -- see
|
|
201
|
+
Security Notes.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Usage Examples
|
|
206
|
+
|
|
207
|
+
### Basic -- create pool, encrypt/decrypt one message
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { init, XChaCha20Poly1305Pool, randomBytes } from 'leviathan-crypto'
|
|
211
|
+
|
|
212
|
+
await init(['chacha20'])
|
|
213
|
+
|
|
214
|
+
const pool = await XChaCha20Poly1305Pool.create()
|
|
215
|
+
|
|
216
|
+
const key = randomBytes(32)
|
|
217
|
+
const nonce = randomBytes(24)
|
|
218
|
+
const plaintext = new TextEncoder().encode('Hello, world!')
|
|
219
|
+
|
|
220
|
+
// Copy inputs before passing to the pool (they will be neutered)
|
|
221
|
+
const ct = await pool.encrypt(key.slice(), nonce.slice(), plaintext.slice())
|
|
222
|
+
const pt = await pool.decrypt(key.slice(), nonce.slice(), ct)
|
|
223
|
+
console.log(new TextDecoder().decode(pt)) // "Hello, world!"
|
|
224
|
+
|
|
225
|
+
pool.dispose()
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Concurrent burst -- `Promise.all()` over many independent messages
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import { init, XChaCha20Poly1305Pool, randomBytes } from 'leviathan-crypto'
|
|
232
|
+
|
|
233
|
+
await init(['chacha20'])
|
|
234
|
+
const pool = await XChaCha20Poly1305Pool.create()
|
|
235
|
+
|
|
236
|
+
const messages = ['message-1', 'message-2', 'message-3', 'message-4']
|
|
237
|
+
const key = randomBytes(32)
|
|
238
|
+
|
|
239
|
+
// Each message gets its own nonce -- all encrypt concurrently
|
|
240
|
+
const encrypted = await Promise.all(
|
|
241
|
+
messages.map(msg => {
|
|
242
|
+
const nonce = randomBytes(24)
|
|
243
|
+
const pt = new TextEncoder().encode(msg)
|
|
244
|
+
return pool.encrypt(key.slice(), nonce, pt)
|
|
245
|
+
})
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
pool.dispose()
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Manual worker count
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
const pool = await XChaCha20Poly1305Pool.create({ workers: 4 })
|
|
255
|
+
console.log(pool.size) // 4
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Correct dispose pattern -- `try/finally`
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const pool = await XChaCha20Poly1305Pool.create()
|
|
262
|
+
try {
|
|
263
|
+
const ct = await pool.encrypt(key, nonce, plaintext)
|
|
264
|
+
// ... use ct ...
|
|
265
|
+
} finally {
|
|
266
|
+
pool.dispose()
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### What NOT to do -- splitting one message across pool calls
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// WRONG -- this is NOT secure
|
|
274
|
+
const chunk1 = await pool.encrypt(key, nonce1, largeFile.subarray(0, 65536))
|
|
275
|
+
const chunk2 = await pool.encrypt(key, nonce2, largeFile.subarray(65536))
|
|
276
|
+
const result = concat(chunk1, chunk2)
|
|
277
|
+
// ^ An attacker can reorder, duplicate, or truncate chunks undetected.
|
|
278
|
+
// There is no stream-level authentication.
|
|
279
|
+
// Use a future chunked-AEAD streaming API for large files.
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Error Conditions
|
|
285
|
+
|
|
286
|
+
| Condition | What happens |
|
|
287
|
+
|-----------|-------------|
|
|
288
|
+
| `init()` not called | `create()` throws: `leviathan-crypto: call init(['chacha20']) before using XChaCha20Poly1305Pool` |
|
|
289
|
+
| `new XChaCha20Poly1305Pool()` | Compile-time error -- the constructor is private |
|
|
290
|
+
| Wrong key length | `encrypt()`/`decrypt()` reject with `RangeError` |
|
|
291
|
+
| Wrong nonce length | `encrypt()`/`decrypt()` reject with `RangeError` |
|
|
292
|
+
| Ciphertext shorter than 16 bytes | `decrypt()` rejects with `RangeError` |
|
|
293
|
+
| Authentication failure | `decrypt()` rejects with `Error('ChaCha20Poly1305: authentication failed')` |
|
|
294
|
+
| Pool disposed | `encrypt()`/`decrypt()` reject with `Error('leviathan-crypto: pool is disposed')` |
|
|
295
|
+
| Worker init failure | `create()` rejects with error message from the worker |
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Cross-References
|
|
300
|
+
|
|
301
|
+
- [chacha20.md](./chacha20.md) — single-instance XChaCha20-Poly1305 API
|
|
302
|
+
- [asm_chacha.md](./asm_chacha.md) — WASM implementation details (quarter-round, Poly1305 accumulator, HChaCha20)
|
|
303
|
+
- [wasm.md](./wasm.md) — WebAssembly primer: how one compiled module spawns many worker instances
|
|
304
|
+
- [fortuna.md](./fortuna.md) — another class using the `static async create()` factory pattern
|
|
305
|
+
- [architecture.md](./architecture.md) — library architecture and module relationships
|
|
306
|
+
- [README.md](./README.md) — project overview and getting started
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Fortuna: Cryptographically Secure Pseudorandom Number Generator (CSPRNG)
|
|
2
|
+
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> A CSPRNG that continuously collects entropy from the environment and generates
|
|
5
|
+
> cryptographically secure random bytes, backed by WASM Serpent-256 and SHA-256.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
A cryptographically secure pseudorandom number generator (CSPRNG) produces random
|
|
10
|
+
bytes that are indistinguishable from true randomness to any observer, even one
|
|
11
|
+
with significant computational resources. This matters because many security
|
|
12
|
+
operations -- generating encryption keys, initialization vectors, nonces, tokens
|
|
13
|
+
-- require randomness that an attacker cannot predict. If an attacker can predict
|
|
14
|
+
the output of your random number generator, they can predict your keys, and your
|
|
15
|
+
encryption provides no protection.
|
|
16
|
+
|
|
17
|
+
Fortuna is a CSPRNG designed by Bruce Schneier and Niels Ferguson, published in
|
|
18
|
+
*Practical Cryptography* (2003). It continuously collects entropy from multiple
|
|
19
|
+
sources -- mouse movements, keyboard events, system timers, OS randomness -- and
|
|
20
|
+
feeds that entropy into 32 independent pools. When you request random bytes,
|
|
21
|
+
Fortuna combines pool contents and uses them to reseed an internal generator
|
|
22
|
+
built on Serpent-256 (block cipher) and SHA-256 (hash function). Both primitives
|
|
23
|
+
run entirely in WebAssembly.
|
|
24
|
+
|
|
25
|
+
Why use Fortuna instead of `crypto.getRandomValues()`? The OS random source is
|
|
26
|
+
good, and Fortuna seeds itself from it on creation. But Fortuna adds two
|
|
27
|
+
properties on top. First, **forward secrecy**: after every call to `get()`, the
|
|
28
|
+
internal generation key is replaced, so compromising the current state does not
|
|
29
|
+
reveal any past outputs. Second, **defense-in-depth entropy pooling**: Fortuna
|
|
30
|
+
collects entropy from many independent sources and distributes it across 32 pools
|
|
31
|
+
with exponentially increasing reseed intervals, making it resilient to entropy
|
|
32
|
+
estimation attacks and individual source failures.
|
|
33
|
+
|
|
34
|
+
Fortuna is the only class in leviathan-crypto that requires two WASM modules.
|
|
35
|
+
You must initialize both `serpent` and `sha2` before creating an instance, and
|
|
36
|
+
you must use the `Fortuna.create()` static factory rather than `new Fortuna()`.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Security Notes
|
|
41
|
+
|
|
42
|
+
- **Forward secrecy** -- The generation key is replaced after every call to
|
|
43
|
+
`get()`. If an attacker compromises the internal state at time T, they cannot
|
|
44
|
+
reconstruct any output produced before time T.
|
|
45
|
+
|
|
46
|
+
- **32 entropy pools** -- Entropy is distributed across 32 independent pools
|
|
47
|
+
using round-robin assignment. Pool 0 is used on every reseed, pool 1 on every
|
|
48
|
+
second reseed, pool 2 on every fourth, and so on. This exponential schedule
|
|
49
|
+
means that even if an attacker can observe or influence some entropy sources,
|
|
50
|
+
higher-numbered pools accumulate enough entropy over time to produce a strong
|
|
51
|
+
reseed eventually.
|
|
52
|
+
|
|
53
|
+
- **Immediate usability** -- Fortuna seeds itself from `crypto.getRandomValues()`
|
|
54
|
+
(browser) or `crypto.randomBytes()` (Node.js) during creation, so it is
|
|
55
|
+
immediately usable. You do not need to wait for entropy to accumulate before
|
|
56
|
+
calling `get()`.
|
|
57
|
+
|
|
58
|
+
- **Browser entropy sources** -- mouse movements, keyboard events, click events,
|
|
59
|
+
scroll position, touch events, device motion and orientation,
|
|
60
|
+
`performance.now()` timing, DOM content hash, and periodic
|
|
61
|
+
`crypto.getRandomValues()`.
|
|
62
|
+
|
|
63
|
+
- **Node.js entropy sources** -- `crypto.randomBytes()`, `process.hrtime` (nanosecond
|
|
64
|
+
timing jitter), `process.cpuUsage()`, `process.memoryUsage()`, `os.loadavg()`,
|
|
65
|
+
`os.freemem()`.
|
|
66
|
+
|
|
67
|
+
- **Wipe state when done** -- Call `stop()` when you are finished with the
|
|
68
|
+
instance. This wipes the generation key and counter from memory and stops all
|
|
69
|
+
background entropy collectors. Key material should not persist longer than
|
|
70
|
+
necessary.
|
|
71
|
+
|
|
72
|
+
- **Output quality depends on entropy** -- The initial seed from the OS random
|
|
73
|
+
source is strong. Over time, the additional entropy collectors improve the
|
|
74
|
+
state further. In environments with limited user interaction (headless servers,
|
|
75
|
+
automated tests), fewer entropy sources contribute, but the OS random seed
|
|
76
|
+
still provides a solid baseline.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## API Reference
|
|
81
|
+
|
|
82
|
+
### `Fortuna.create(opts?)`
|
|
83
|
+
|
|
84
|
+
Static async factory. Returns a `Promise<Fortuna>`.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
static async create(opts?: {
|
|
88
|
+
msPerReseed?: number;
|
|
89
|
+
entropy?: Uint8Array;
|
|
90
|
+
}): Promise<Fortuna>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| Parameter | Type | Default | Description |
|
|
94
|
+
|-----------|------|---------|-------------|
|
|
95
|
+
| `opts.msPerReseed` | `number` | `100` | Minimum milliseconds between reseeds. |
|
|
96
|
+
| `opts.entropy` | `Uint8Array` | -- | Optional extra entropy to mix in during creation. |
|
|
97
|
+
|
|
98
|
+
Throws if `init(['serpent', 'sha2'])` has not been called.
|
|
99
|
+
|
|
100
|
+
Direct construction with `new Fortuna()` is not possible -- the constructor is
|
|
101
|
+
private. Always use `Fortuna.create()`.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### `get(length)`
|
|
106
|
+
|
|
107
|
+
Generate `length` random bytes.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
get(length: number): Uint8Array | undefined
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Returns a `Uint8Array` of the requested length, or `undefined` if the generator
|
|
114
|
+
has not been seeded yet (this should not happen under normal usage, since
|
|
115
|
+
`create()` seeds the generator immediately).
|
|
116
|
+
|
|
117
|
+
After producing the output, the generation key is replaced with fresh
|
|
118
|
+
pseudorandom material. This is the forward secrecy mechanism -- the key used to
|
|
119
|
+
produce this output no longer exists.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### `addEntropy(entropy)`
|
|
124
|
+
|
|
125
|
+
Manually add entropy to the pools.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
addEntropy(entropy: Uint8Array): void
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Use this to feed application-specific randomness into the generator. The entropy
|
|
132
|
+
is distributed across pools using round-robin assignment. Each call advances to
|
|
133
|
+
the next pool.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### `getEntropy()`
|
|
138
|
+
|
|
139
|
+
Get the estimated available entropy in bytes.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
getEntropy(): number
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Returns the estimated total entropy accumulated across all pools, in bytes. This
|
|
146
|
+
is an estimate, not a guarantee -- it reflects the sum of entropy credits assigned
|
|
147
|
+
by each collector.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### `stop()`
|
|
152
|
+
|
|
153
|
+
Permanently dispose this Fortuna instance.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
stop(): void
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
> [!WARNING]
|
|
160
|
+
> Do not attempt to reuse a stopped instance. `stop()` is a permanent dispose
|
|
161
|
+
> operation. If a new Fortuna instance is needed, call `Fortuna.create()`.
|
|
162
|
+
|
|
163
|
+
Call this when you are done with the Fortuna instance. `stop()`:
|
|
164
|
+
- Removes all browser event listeners
|
|
165
|
+
- Clears all background timers (Node.js stats collection, periodic crypto random)
|
|
166
|
+
- Zeroes the generation key and counter
|
|
167
|
+
- Resets the reseed counter to 0
|
|
168
|
+
- Marks the instance as disposed
|
|
169
|
+
|
|
170
|
+
All subsequent method calls (`get()`, `addEntropy()`, `getEntropy()`, `stop()`)
|
|
171
|
+
on a disposed instance throw immediately:
|
|
172
|
+
```
|
|
173
|
+
Error: Fortuna instance has been disposed
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
There is no `start()` or restart capability.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Usage Examples
|
|
181
|
+
|
|
182
|
+
### Basic usage -- generate random bytes
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { init, Fortuna } from 'leviathan-crypto'
|
|
186
|
+
|
|
187
|
+
// Initialize both WASM modules that Fortuna depends on
|
|
188
|
+
await init(['serpent', 'sha2'])
|
|
189
|
+
|
|
190
|
+
// Create the CSPRNG
|
|
191
|
+
const rng = await Fortuna.create()
|
|
192
|
+
|
|
193
|
+
// Generate 32 random bytes (e.g., for an encryption key)
|
|
194
|
+
const key = rng.get(32)
|
|
195
|
+
|
|
196
|
+
// Generate 12 random bytes (e.g., for a nonce)
|
|
197
|
+
const nonce = rng.get(12)
|
|
198
|
+
|
|
199
|
+
// Clean up when done -- wipes key material from memory
|
|
200
|
+
rng.stop()
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Adding custom entropy
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { init, Fortuna, utf8ToBytes } from 'leviathan-crypto'
|
|
207
|
+
|
|
208
|
+
await init(['serpent', 'sha2'])
|
|
209
|
+
const rng = await Fortuna.create()
|
|
210
|
+
|
|
211
|
+
// Feed application-specific data as additional entropy.
|
|
212
|
+
// This supplements (never replaces) the automatic entropy collection.
|
|
213
|
+
const userData = utf8ToBytes(crypto.randomUUID())
|
|
214
|
+
rng.addEntropy(userData)
|
|
215
|
+
|
|
216
|
+
// Server-side: feed in request-specific data
|
|
217
|
+
const requestEntropy = new Uint8Array(16)
|
|
218
|
+
crypto.getRandomValues(requestEntropy)
|
|
219
|
+
rng.addEntropy(requestEntropy)
|
|
220
|
+
|
|
221
|
+
const token = rng.get(32)
|
|
222
|
+
rng.stop()
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Browser with automatic entropy collection
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { init, Fortuna } from 'leviathan-crypto'
|
|
229
|
+
|
|
230
|
+
await init(['serpent', 'sha2'])
|
|
231
|
+
|
|
232
|
+
// Fortuna automatically registers browser event listeners on creation:
|
|
233
|
+
// - mousemove (throttled to 50ms)
|
|
234
|
+
// - keydown
|
|
235
|
+
// - click
|
|
236
|
+
// - scroll
|
|
237
|
+
// - touchstart, touchmove, touchend
|
|
238
|
+
// - devicemotion, deviceorientation, orientationchange
|
|
239
|
+
//
|
|
240
|
+
// Every user interaction feeds entropy into the pools.
|
|
241
|
+
// No manual setup is needed -- it starts collecting immediately.
|
|
242
|
+
|
|
243
|
+
const rng = await Fortuna.create()
|
|
244
|
+
|
|
245
|
+
// The longer the user interacts with the page before you generate,
|
|
246
|
+
// the more entropy has been accumulated. But the initial OS seed
|
|
247
|
+
// is strong enough for immediate use.
|
|
248
|
+
document.querySelector('#generate')?.addEventListener('click', () => {
|
|
249
|
+
const bytes = rng.get(32)
|
|
250
|
+
console.log('Generated:', bytes)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// When the page unloads or the component unmounts, stop the collectors
|
|
254
|
+
window.addEventListener('beforeunload', () => rng.stop())
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Providing initial entropy at creation
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { init, Fortuna } from 'leviathan-crypto'
|
|
261
|
+
|
|
262
|
+
await init(['serpent', 'sha2'])
|
|
263
|
+
|
|
264
|
+
// You can pass extra entropy at creation time.
|
|
265
|
+
// This is mixed into the pools during initialization, before the
|
|
266
|
+
// generator is first seeded.
|
|
267
|
+
const extraSeed = new Uint8Array(64)
|
|
268
|
+
crypto.getRandomValues(extraSeed)
|
|
269
|
+
|
|
270
|
+
const rng = await Fortuna.create({ entropy: extraSeed })
|
|
271
|
+
const bytes = rng.get(32)
|
|
272
|
+
rng.stop()
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Error Conditions
|
|
278
|
+
|
|
279
|
+
| Condition | What happens |
|
|
280
|
+
|-----------|-------------|
|
|
281
|
+
| `init()` not called | `Fortuna.create()` throws: `leviathan-crypto: call init(['serpent', 'sha2']) before using Fortuna` |
|
|
282
|
+
| Only one module initialized | Same error -- both `serpent` and `sha2` must be initialized. |
|
|
283
|
+
| `new Fortuna()` | Compile-time error -- the constructor is private. TypeScript will not allow it. |
|
|
284
|
+
| `get()` before first reseed | Returns `undefined`. Under normal usage this does not happen because `create()` seeds the generator during initialization. |
|
|
285
|
+
| Any method after `stop()` | Throws: `Fortuna instance has been disposed`. The instance is permanently disposed. |
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## How It Works (Simplified)
|
|
290
|
+
|
|
291
|
+
For readers who want to understand what Fortuna does internally, without needing
|
|
292
|
+
to read the spec:
|
|
293
|
+
|
|
294
|
+
1. **Entropy collection** -- Background listeners and timers capture small,
|
|
295
|
+
unpredictable measurements (mouse coordinates, nanosecond timings, memory
|
|
296
|
+
usage) and feed them into 32 separate pools via SHA-256 hash chaining.
|
|
297
|
+
|
|
298
|
+
2. **Reseed** -- When pool 0 has accumulated enough entropy and enough time has
|
|
299
|
+
passed since the last reseed, Fortuna combines the contents of eligible pools
|
|
300
|
+
(determined by the reseed counter) into a seed, and derives a new generation
|
|
301
|
+
key: `genKey = SHA-256(genKey || seed)`.
|
|
302
|
+
|
|
303
|
+
3. **Generation** -- To produce output, the generator encrypts an incrementing
|
|
304
|
+
counter with Serpent-256 in ECB mode using the current generation key. The
|
|
305
|
+
output is the concatenation of encrypted counter blocks, truncated to the
|
|
306
|
+
requested length.
|
|
307
|
+
|
|
308
|
+
4. **Key replacement** -- Immediately after producing output, the generation key
|
|
309
|
+
is replaced with fresh pseudorandom blocks. The old key is gone. This is what
|
|
310
|
+
provides forward secrecy.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Cross-References
|
|
315
|
+
|
|
316
|
+
- [README.md](./README.md): Project overview and quick-start guide
|
|
317
|
+
- [architecture.md](./architecture.md): Library architecture and module relationships
|
|
318
|
+
- [serpent.md](./serpent.md): Serpent-256 TypeScript API (Fortuna uses Serpent ECB internally)
|
|
319
|
+
- [sha2.md](./sha2.md): SHA-256 TypeScript API (Fortuna uses SHA-256 for entropy accumulation)
|
|
320
|
+
- [asm_serpent.md](./asm_serpent.md): Serpent-256 WASM implementation details
|
|
321
|
+
- [asm_sha2.md](./asm_sha2.md): SHA-256 WASM implementation details
|
|
322
|
+
- [utils.md](./utils.md): `randomBytes()` for simpler random generation needs
|