leviathan-crypto 2.0.1 → 2.1.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 (93) hide show
  1. package/CLAUDE.md +171 -7
  2. package/LICENSE +4 -0
  3. package/README.md +109 -54
  4. package/SECURITY.md +125 -238
  5. package/dist/chacha20/cipher-suite.d.ts +10 -0
  6. package/dist/chacha20/cipher-suite.js +65 -2
  7. package/dist/chacha20/generator.d.ts +12 -0
  8. package/dist/chacha20/generator.js +91 -0
  9. package/dist/chacha20/index.d.ts +97 -1
  10. package/dist/chacha20/index.js +139 -11
  11. package/dist/chacha20/ops.d.ts +57 -6
  12. package/dist/chacha20/ops.js +93 -13
  13. package/dist/chacha20/pool-worker.js +12 -0
  14. package/dist/chacha20/types.d.ts +1 -32
  15. package/dist/ct-wasm.js +1 -1
  16. package/dist/ct.wasm +0 -0
  17. package/dist/docs/aead.md +66 -26
  18. package/dist/docs/architecture.md +600 -521
  19. package/dist/docs/argon2id.md +17 -14
  20. package/dist/docs/chacha20.md +146 -39
  21. package/dist/docs/exports.md +46 -10
  22. package/dist/docs/fortuna.md +339 -122
  23. package/dist/docs/init.md +24 -25
  24. package/dist/docs/loader.md +142 -47
  25. package/dist/docs/serpent.md +139 -41
  26. package/dist/docs/sha2.md +77 -19
  27. package/dist/docs/sha3.md +81 -15
  28. package/dist/docs/types.md +155 -15
  29. package/dist/docs/utils.md +171 -81
  30. package/dist/embedded/chacha20-pool-worker.d.ts +1 -0
  31. package/dist/embedded/chacha20-pool-worker.js +5 -0
  32. package/dist/embedded/kyber.d.ts +1 -1
  33. package/dist/embedded/kyber.js +1 -1
  34. package/dist/embedded/serpent-pool-worker.d.ts +1 -0
  35. package/dist/embedded/serpent-pool-worker.js +5 -0
  36. package/dist/fortuna.d.ts +14 -8
  37. package/dist/fortuna.js +144 -50
  38. package/dist/index.d.ts +8 -6
  39. package/dist/index.js +6 -5
  40. package/dist/init.d.ts +0 -2
  41. package/dist/init.js +83 -3
  42. package/dist/kyber/indcpa.js +4 -4
  43. package/dist/kyber/index.js +25 -5
  44. package/dist/kyber/kem.js +56 -1
  45. package/dist/kyber/suite.d.ts +1 -2
  46. package/dist/kyber/types.d.ts +1 -0
  47. package/dist/kyber/validate.d.ts +8 -4
  48. package/dist/kyber/validate.js +18 -14
  49. package/dist/kyber.wasm +0 -0
  50. package/dist/loader.d.ts +7 -2
  51. package/dist/loader.js +25 -28
  52. package/dist/ratchet/index.d.ts +6 -0
  53. package/dist/ratchet/index.js +37 -0
  54. package/dist/ratchet/kdf-chain.d.ts +13 -0
  55. package/dist/ratchet/kdf-chain.js +85 -0
  56. package/dist/ratchet/ratchet-keypair.d.ts +9 -0
  57. package/dist/ratchet/ratchet-keypair.js +61 -0
  58. package/dist/ratchet/root-kdf.d.ts +4 -0
  59. package/dist/ratchet/root-kdf.js +124 -0
  60. package/dist/ratchet/skipped-key-store.d.ts +14 -0
  61. package/dist/ratchet/skipped-key-store.js +154 -0
  62. package/dist/ratchet/types.d.ts +36 -0
  63. package/dist/ratchet/types.js +26 -0
  64. package/dist/serpent/cipher-suite.d.ts +10 -0
  65. package/dist/serpent/cipher-suite.js +135 -50
  66. package/dist/serpent/generator.d.ts +12 -0
  67. package/dist/serpent/generator.js +97 -0
  68. package/dist/serpent/index.d.ts +61 -1
  69. package/dist/serpent/index.js +92 -7
  70. package/dist/serpent/pool-worker.js +25 -101
  71. package/dist/serpent/serpent-cbc.d.ts +14 -4
  72. package/dist/serpent/serpent-cbc.js +50 -32
  73. package/dist/serpent/shared-ops.d.ts +83 -0
  74. package/dist/serpent/shared-ops.js +213 -0
  75. package/dist/serpent/types.d.ts +1 -5
  76. package/dist/sha2/hash.d.ts +2 -0
  77. package/dist/sha2/hash.js +53 -0
  78. package/dist/sha2/index.d.ts +1 -0
  79. package/dist/sha2/index.js +15 -1
  80. package/dist/sha3/hash.d.ts +2 -0
  81. package/dist/sha3/hash.js +53 -0
  82. package/dist/sha3/index.d.ts +17 -2
  83. package/dist/sha3/index.js +79 -7
  84. package/dist/stream/header.js +5 -5
  85. package/dist/stream/open-stream.js +36 -14
  86. package/dist/stream/seal-stream-pool.d.ts +1 -0
  87. package/dist/stream/seal-stream-pool.js +38 -8
  88. package/dist/stream/seal-stream.js +29 -11
  89. package/dist/types.d.ts +21 -0
  90. package/dist/utils.d.ts +7 -8
  91. package/dist/utils.js +73 -40
  92. package/dist/wasm-source.d.ts +9 -8
  93. package/package.json +79 -64
@@ -1,90 +1,202 @@
1
- # Fortuna CSPRNG
1
+ <img src="https://github.com/xero/leviathan-crypto/raw/main/docs/logo.svg" alt="logo" width="120" align="left" margin="10">
2
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.
3
+ ### Fortuna CSPRNG
4
+
5
+ A CSPRNG that continuously collects entropy from the environment and generates cryptographically secure random bytes. The cipher and hash primitives are pluggable; you pick which pair to use at create time.
6
6
 
7
7
  > ### Table of Contents
8
8
  > - [Overview](#overview)
9
+ > - [Pluggable primitives](#pluggable-primitives)
10
+ > - [Spec deviations](#spec-deviations)
9
11
  > - [Security Notes](#security-notes)
10
12
  > - [API Reference](#api-reference)
11
13
  > - [Usage Examples](#usage-examples)
12
14
  > - [Error Conditions](#error-conditions)
13
15
  > - [How It Works (Simplified)](#how-it-works-simplified)
16
+ > - [Coexistence with raw ciphers](#coexistence-with-raw-ciphers)
17
+ > - [Cross-References](#cross-references)
14
18
 
15
19
  ---
16
20
 
17
21
  ## Overview
18
22
 
19
- A cryptographically secure pseudorandom number generator (CSPRNG) produces random
20
- bytes that are indistinguishable from true randomness to any observer, even one
21
- with significant computational resources. This matters because many security
22
- operations require unpredictable randomness: generating encryption keys, initialization vectors, nonces, and tokens. If an attacker can predict
23
- the output of your random number generator, they can predict your keys, and your
24
- encryption provides no protection.
25
-
26
- Fortuna is a CSPRNG designed by Bruce Schneier and Niels Ferguson, published in
27
- *Practical Cryptography* (2003). It continuously collects entropy from multiple
28
- sources (mouse movements, keyboard events, system timers, OS randomness) and
29
- feeds that entropy into 32 independent pools. When you request random bytes,
30
- Fortuna combines pool contents and uses them to reseed an internal generator
31
- built on Serpent-256 (block cipher) and SHA-256 (hash function). Both primitives
32
- run entirely in WebAssembly.
33
-
34
- Fortuna adds two properties on top of `crypto.getRandomValues()`. First, **forward secrecy**: after every call to `get()`, the
35
- internal generation key is replaced, so compromising the current state does not
36
- reveal any past outputs. Second, **defense-in-depth entropy pooling**: Fortuna
37
- collects entropy from many independent sources and distributes it across 32 pools
38
- with exponentially increasing reseed intervals, making it resilient to entropy
39
- estimation attacks and individual source failures.
40
-
41
- Fortuna is the only class in leviathan-crypto that requires two WASM modules.
42
- You must initialize both `serpent` and `sha2` before creating an instance, and
43
- you must use the `Fortuna.create()` static factory rather than `new Fortuna()`.
23
+ A cryptographically secure pseudorandom number generator (CSPRNG) produces
24
+ random bytes that are indistinguishable from true randomness to any observer,
25
+ even one with significant computational resources. This matters because many
26
+ security operations require unpredictable randomness: generating encryption
27
+ keys, initialization vectors, nonces, and tokens. If an attacker can predict
28
+ the output of your random number generator, they can predict your keys, and
29
+ your encryption provides no protection.
30
+
31
+ Fortuna is a CSPRNG designed by Bruce Schneier and Niels Ferguson, published
32
+ in *Practical Cryptography* (2003). It continuously collects entropy from
33
+ multiple sources (mouse movements, keyboard events, system timers, OS
34
+ randomness) and feeds that entropy into 32 independent pools. When you
35
+ request random bytes, Fortuna combines pool contents and uses them to reseed
36
+ an internal generator built on a cipher-as-PRF construction and a hash
37
+ function. You pick which cipher and hash at create time.
38
+
39
+ Fortuna adds two properties on top of `crypto.getRandomValues()`. First,
40
+ **forward secrecy**: after every call to `get()`, the internal generation
41
+ key is replaced, so compromising the current state does not reveal any past
42
+ outputs. Second, **defense-in-depth entropy pooling**: Fortuna collects
43
+ entropy from many independent sources and distributes it across 32 pools
44
+ with exponentially increasing reseed intervals, making it resilient to
45
+ entropy estimation attacks and individual source failures.
46
+
47
+ The original spec uses AES-256 in counter mode with SHA-256. This
48
+ implementation lets you pick from Serpent-256 or ChaCha20 for the generator,
49
+ paired with SHA-256 or SHA3-256 for the hash. See
50
+ [Pluggable primitives](#pluggable-primitives) for the available combinations
51
+ and [Spec deviations](#spec-deviations) for what changes when you pick
52
+ something other than the original pair.
44
53
 
45
54
  ---
46
55
 
47
- ## Security Notes
56
+ ## Pluggable primitives
57
+
58
+ Fortuna takes two primitives at create time:
59
+
60
+ - A **`Generator`**, the cipher-as-PRF that produces output blocks from `(key, counter)`.
61
+ - A **`HashFn`**, the stateless hash used for accumulator chaining and reseed key derivation.
62
+
63
+ Both ship as plain const objects from each cipher and hash module. The const
64
+ pattern matches `SerpentCipher` and `XChaCha20Cipher` in the AEAD layer.
65
+
66
+ | Generator | Source path | `keySize` | Cipher backend |
67
+ | ------------------- | ---------------------------- | --------- | ------------------------ |
68
+ | `SerpentGenerator` | `leviathan-crypto/serpent` | 32 | Serpent-256 ECB |
69
+ | `ChaCha20Generator` | `leviathan-crypto/chacha20` | 32 | ChaCha20 with zero nonce |
70
+
71
+ | Hash | Source path | `outputSize` | Hash backend |
72
+ | -------------- | -------------------------- | ------------ | ------------ |
73
+ | `SHA256Hash` | `leviathan-crypto/sha2` | 32 | SHA-256 |
74
+ | `SHA3_256Hash` | `leviathan-crypto/sha3` | 32 | SHA3-256 |
75
+
76
+ All four combinations are valid because every shipped `Generator` has
77
+ `keySize: 32` and every shipped `HashFn` has `outputSize: 32`.
78
+ `Fortuna.create()` asserts `hash.outputSize === generator.keySize` and
79
+ throws `RangeError` if you pair primitives of different sizes.
80
+
81
+ The motivation for pluggability is bundle size. Earlier versions of Fortuna
82
+ pinned Serpent + SHA-256, which meant a chacha-only consumer paid for
83
+ Serpent's 123 KB WASM module just to use the CSPRNG. With pluggable
84
+ primitives, an XChaCha20-Poly1305 application can pair `Fortuna` with
85
+ `ChaCha20Generator` + `SHA256Hash` and the bundle never sees Serpent.
86
+
87
+ ---
88
+
89
+ ## Spec deviations
90
+
91
+ The original Fortuna spec (Ferguson and Schneier, *Practical Cryptography*
92
+ 2003) is concrete about its choice of primitives:
48
93
 
49
- **Forward secrecy.** The generation key is replaced after every call to `get()`. If an attacker compromises the internal state at time T, they cannot reconstruct any output produced before time T.
94
+ - §9.4 specifies AES-256 in counter mode as the generator.
95
+ - §9.5 specifies SHA-256 for the accumulator pools and reseed key derivation.
50
96
 
51
- **32 entropy pools.** Entropy is distributed across 32 independent pools using round-robin assignment. Pool 0 is used on every reseed, pool 1 on every second reseed, pool 2 on every fourth, and so on. This exponential schedule means that even if an attacker can observe or influence some entropy sources, higher-numbered pools accumulate enough entropy over time to produce a strong reseed eventually.
97
+ This library replaces both with a pluggable contract. The deviations:
52
98
 
53
- **Immediate usability.** Fortuna seeds itself from `crypto.getRandomValues()` (browser) or `crypto.randomBytes()` (Node.js) during creation. You do not need to wait for entropy to accumulate before calling `get()`.
99
+ 1. **Generator can be Serpent-256 or ChaCha20.** Serpent-256 is a 256-bit-key
100
+ block cipher with the same shape as AES; substituting it changes the
101
+ underlying permutation but preserves the counter-mode-PRF construction.
102
+ ChaCha20 is a stream cipher whose block function is itself a strong PRF
103
+ on `(key, nonce, counter)`; we fix the nonce to zero and treat the block
104
+ counter as Fortuna's generator counter. Both substitutions are valid in
105
+ the sense that the security argument for Fortuna's generator depends on
106
+ the underlying primitive being a strong PRF, which Serpent-256 and
107
+ ChaCha20 both are under standard assumptions.
54
108
 
55
- **Browser entropy sources.** Mouse movements, keyboard events, click events, scroll position, touch events, device motion and orientation, `performance.now()` timing, DOM content hash, and periodic `crypto.getRandomValues()`.
109
+ 2. **Hash can be SHA-256 or SHA3-256.** SHA3-256 is a sponge-based hash; the
110
+ security properties Fortuna requires from the hash (collision resistance,
111
+ second-preimage resistance, output indistinguishable from random) hold
112
+ for both.
56
113
 
57
- **Node.js entropy sources.** `crypto.randomBytes()`, `process.hrtime` (nanosecond timing jitter), `process.cpuUsage()`, `process.memoryUsage()`, `os.loadavg()`, `os.freemem()`.
114
+ 3. **Hash output size is required to match generator key size in v2.2.0.**
115
+ The reseed step `genKey = hash(genKey || seed)` writes the hash output
116
+ directly into the generator key slot, with no KDF layer. This forbids
117
+ exotic combinations such as SHA-512 paired with a 32-byte-key generator.
118
+ If a real use case for size mismatches appears later, an HKDF mode can
119
+ be added without breaking existing pairings.
58
120
 
59
- **Wipe state when done.** Call `stop()` when you are finished with the instance. This wipes the generation key and counter from memory and stops all background entropy collectors. Key material should not persist longer than necessary.
121
+ The pool-selection schedule, the 32-pool count, the 64-bit reseed threshold,
122
+ the 100ms reseed interval, and the entropy-credit constants are unchanged
123
+ from the spec.
60
124
 
61
- **Output quality depends on entropy.** The initial seed from the OS random source is strong. Over time the additional entropy collectors improve the state further. In environments with limited user interaction (headless servers, automated tests), fewer entropy sources contribute, but the OS random seed still provides a solid baseline.
125
+ ---
126
+
127
+ ## Security Notes
128
+
129
+ **Forward secrecy.** The generation key is replaced after every call to
130
+ `get()`. If an attacker compromises the internal state at time T, they
131
+ cannot reconstruct any output produced before time T.
132
+
133
+ **32 entropy pools.** Entropy is distributed across 32 independent pools
134
+ using round-robin assignment. Pool 0 is used on every reseed, pool 1 on
135
+ every second reseed, pool 2 on every fourth, and so on. This exponential
136
+ schedule means that even if an attacker can observe or influence some
137
+ entropy sources, higher-numbered pools accumulate enough entropy over time
138
+ to produce a strong reseed eventually.
139
+
140
+ **Immediate usability.** Fortuna seeds itself from `crypto.getRandomValues()`
141
+ (browser) or `crypto.randomBytes()` (Node.js) during creation. `create()`
142
+ asserts that pool 0 received at least 64 bits of entropy from the OS source
143
+ before resolving, and throws if no working entropy source is available. You
144
+ do not need to wait for entropy to accumulate before calling `get()`.
145
+
146
+ **Browser entropy sources.** Mouse movements, keyboard events, click events,
147
+ scroll position, touch events, device motion and orientation,
148
+ `performance.now()` timing, DOM content hash, and periodic
149
+ `crypto.getRandomValues()`.
150
+
151
+ **Node.js entropy sources.** `crypto.randomBytes()`, `process.hrtime`
152
+ (nanosecond timing jitter), `process.cpuUsage()`, `process.memoryUsage()`,
153
+ `os.loadavg()`, `os.freemem()`.
154
+
155
+ **Wipe state when done.** Call `stop()` when you are finished with the
156
+ instance. This wipes the generation key and counter from JavaScript memory,
157
+ calls `wipeBuffers()` on every WASM module the chosen `Generator` and
158
+ `HashFn` touched, and stops all background entropy collectors. Key material
159
+ should not persist longer than necessary.
160
+
161
+ **Output quality depends on entropy.** The initial seed from the OS random
162
+ source is strong. Over time the additional entropy collectors improve the
163
+ state further. In environments with limited user interaction (headless
164
+ servers, automated tests), fewer entropy sources contribute, but the OS
165
+ random seed still provides a solid baseline.
62
166
 
63
167
  ---
64
168
 
65
169
  ## API Reference
66
170
 
67
- ### `Fortuna.create(opts?)`
171
+ ### `Fortuna.create(opts)`
68
172
 
69
173
  Static async factory. Returns a `Promise<Fortuna>`. The returned instance is
70
- guaranteed to be seeded. `create()` forces an initial reseed before resolving,
71
- so `get()` is immediately usable.
174
+ guaranteed to be seeded. `create()` forces an initial reseed before
175
+ resolving, so `get()` is immediately usable.
72
176
 
73
177
  ```typescript
74
- static async create(opts?: {
75
- msPerReseed?: number;
76
- entropy?: Uint8Array;
178
+ static async create(opts: {
179
+ generator: Generator;
180
+ hash: HashFn;
181
+ msPerReseed?: number;
182
+ entropy?: Uint8Array;
77
183
  }): Promise<Fortuna>
78
184
  ```
79
185
 
80
- | Parameter | Type | Default | Description |
81
- |-----------|------|---------|-------------|
82
- | `opts.msPerReseed` | `number` | `100` | Minimum milliseconds between reseeds. |
83
- | `opts.entropy` | `Uint8Array` | | Optional extra entropy to mix in during creation. |
186
+ | Parameter | Type | Default | Description |
187
+ |--------------------|--------------|----------|-------------|
188
+ | `opts.generator` | `Generator` | required | Cipher-as-PRF backing the generator. `SerpentGenerator` or `ChaCha20Generator`. |
189
+ | `opts.hash` | `HashFn` | required | Stateless hash for accumulator and reseed. `SHA256Hash` or `SHA3_256Hash`. |
190
+ | `opts.msPerReseed` | `number` | `100` | Minimum milliseconds between reseeds. |
191
+ | `opts.entropy` | `Uint8Array` | | Optional extra entropy mixed in during creation. |
84
192
 
85
- Throws if `init({ serpent: serpentWasm, sha2: sha2Wasm })` has not been called.
193
+ Throws `TypeError` if `opts.generator` or `opts.hash` is missing. Throws
194
+ `RangeError` if `opts.hash.outputSize !== opts.generator.keySize`. Throws
195
+ if any required WASM module has not been initialized via `init()`. Throws
196
+ if no working entropy source is available at create time.
86
197
 
87
- Direct construction with `new Fortuna()` is not possible. The constructor is private. Always use `Fortuna.create()`.
198
+ Direct construction with `new Fortuna()` is not possible. The constructor
199
+ is private. Always use `Fortuna.create()`.
88
200
 
89
201
  ---
90
202
 
@@ -96,12 +208,13 @@ Generate `length` random bytes.
96
208
  get(length: number): Uint8Array
97
209
  ```
98
210
 
99
- Returns a `Uint8Array` of the requested length. The instance is always seeded
100
- after `create()` resolves, so this method is guaranteed to return data.
211
+ Returns a `Uint8Array` of the requested length. The instance is always
212
+ seeded after `create()` resolves, so this method is guaranteed to return
213
+ data.
101
214
 
102
215
  After producing the output, the generation key is replaced with fresh
103
- pseudorandom material. This is the forward secrecy mechanism. The key used to
104
- produce this output no longer exists.
216
+ pseudorandom material. This is the forward secrecy mechanism. The key used
217
+ to produce this output no longer exists.
105
218
 
106
219
  ---
107
220
 
@@ -113,9 +226,9 @@ Manually add entropy to the pools.
113
226
  addEntropy(entropy: Uint8Array): void
114
227
  ```
115
228
 
116
- Use this to feed application-specific randomness into the generator. The entropy
117
- is distributed across pools using round-robin assignment. Each call advances to
118
- the next pool.
229
+ Use this to feed application-specific randomness into the generator. The
230
+ entropy is distributed across pools using round-robin assignment. Each call
231
+ advances to the next pool.
119
232
 
120
233
  ---
121
234
 
@@ -127,8 +240,9 @@ Get the estimated available entropy in bytes.
127
240
  getEntropy(): number
128
241
  ```
129
242
 
130
- Returns the estimated total entropy accumulated across all pools, in bytes. This
131
- is an estimate, not a guarantee. It reflects the sum of entropy credits assigned by each collector.
243
+ Returns the estimated total entropy accumulated across all pools, in bytes.
244
+ This is an estimate, not a guarantee. It reflects the sum of entropy credits
245
+ assigned by each collector.
132
246
 
133
247
  ---
134
248
 
@@ -141,22 +255,36 @@ stop(): void
141
255
  ```
142
256
 
143
257
  > [!WARNING]
144
- > Do not attempt to reuse a stopped instance. `stop()` is a permanent dispose
145
- > operation. If a new Fortuna instance is needed, call `Fortuna.create()`.
258
+ > Do not attempt to reuse a stopped instance. `stop()` is a permanent
259
+ > dispose operation. If a new Fortuna instance is needed, call
260
+ > `Fortuna.create()`.
146
261
 
147
262
  Call this when you are done with the Fortuna instance. `stop()`:
148
- - Removes all browser event listeners
149
- - Clears all background timers (Node.js stats collection, periodic crypto random)
150
- - Zeroes the generation key and counter
151
- - Resets the reseed counter to 0
152
- - Marks the instance as disposed
153
-
154
- All subsequent method calls (`get()`, `addEntropy()`, `getEntropy()`, `stop()`)
155
- on a disposed instance throw immediately:
263
+
264
+ - Marks the instance as disposed (first, before any operation that can throw).
265
+ - Removes all browser event listeners.
266
+ - Clears all background timers (Node.js stats collection, periodic crypto random).
267
+ - Zeroes the generation key and the generation counter.
268
+ - Zeroes every pool-hash chain value (all 32 pools).
269
+ - Resets the reseed counter to 0.
270
+ - Calls `wipeBuffers()` on every WASM module the chosen `Generator` and `HashFn` touched.
271
+
272
+ All subsequent method calls (`get()`, `addEntropy()`, `getEntropy()`,
273
+ `stop()`) on a disposed instance throw immediately:
274
+
156
275
  ```
157
276
  Error: Fortuna instance has been disposed
158
277
  ```
159
278
 
279
+ The WASM `wipeBuffers()` step is best-effort. If a stateful cipher (a live
280
+ `SerpentCtr`, `SerpentCbc`, `ChaCha20`, `ChaCha20Poly1305`, or
281
+ `XChaCha20Poly1305`, for example) currently holds one of the modules, the
282
+ corresponding `wipeBuffers()` call throws an ownership error and `stop()`
283
+ re-throws it after every other step has run. The Fortuna instance is still
284
+ marked disposed and all JavaScript-side key material is still wiped; the
285
+ only casualty is the WASM scratch buffer of whichever module threw, which
286
+ the caller can clean up by disposing the conflicting cipher.
287
+
160
288
  There is no `start()` or restart capability.
161
289
 
162
290
  ---
@@ -165,24 +293,60 @@ There is no `start()` or restart capability.
165
293
 
166
294
  ### Basic usage
167
295
 
296
+ The smallest-bundle pair: ChaCha20 generator with SHA-256 hash. No Serpent
297
+ WASM is loaded.
298
+
299
+ ```typescript
300
+ import { init, Fortuna } from 'leviathan-crypto'
301
+ import { ChaCha20Generator } from 'leviathan-crypto/chacha20'
302
+ import { SHA256Hash } from 'leviathan-crypto/sha2'
303
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
304
+ import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
305
+
306
+ await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
307
+
308
+ const rng = await Fortuna.create({ generator: ChaCha20Generator, hash: SHA256Hash })
309
+ const key = rng.get(32) // for an encryption key
310
+ const nonce = rng.get(12) // for a nonce
311
+ rng.stop()
312
+ ```
313
+
314
+ ### Original Fortuna pair
315
+
316
+ Serpent-256 with SHA-256 matches the closest analogue to the spec
317
+ (swapping AES for Serpent). Use this if your application already pulls in
318
+ the Serpent module.
319
+
168
320
  ```typescript
169
321
  import { init, Fortuna } from 'leviathan-crypto'
322
+ import { SerpentGenerator } from 'leviathan-crypto/serpent'
323
+ import { SHA256Hash } from 'leviathan-crypto/sha2'
170
324
  import { serpentWasm } from 'leviathan-crypto/serpent/embedded'
171
325
  import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
172
326
 
173
- // Initialize both WASM modules that Fortuna depends on
174
327
  await init({ serpent: serpentWasm, sha2: sha2Wasm })
175
328
 
176
- // Create the CSPRNG
177
- const rng = await Fortuna.create()
329
+ const rng = await Fortuna.create({ generator: SerpentGenerator, hash: SHA256Hash })
330
+ const bytes = rng.get(32)
331
+ rng.stop()
332
+ ```
333
+
334
+ ### Modern combination
178
335
 
179
- // Generate 32 random bytes (e.g., for an encryption key)
180
- const key = rng.get(32)
336
+ ChaCha20 with SHA3-256. Both primitives are post-2010 designs; useful when
337
+ you want the SHA-3 sponge construction in your CSPRNG accumulator.
338
+
339
+ ```typescript
340
+ import { init, Fortuna } from 'leviathan-crypto'
341
+ import { ChaCha20Generator } from 'leviathan-crypto/chacha20'
342
+ import { SHA3_256Hash } from 'leviathan-crypto/sha3'
343
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
344
+ import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
181
345
 
182
- // Generate 12 random bytes (e.g., for a nonce)
183
- const nonce = rng.get(12)
346
+ await init({ chacha20: chacha20Wasm, sha3: sha3Wasm })
184
347
 
185
- // Clean up when done, wipes key material from memory
348
+ const rng = await Fortuna.create({ generator: ChaCha20Generator, hash: SHA3_256Hash })
349
+ const bytes = rng.get(32)
186
350
  rng.stop()
187
351
  ```
188
352
 
@@ -190,11 +354,13 @@ rng.stop()
190
354
 
191
355
  ```typescript
192
356
  import { init, Fortuna, utf8ToBytes } from 'leviathan-crypto'
357
+ import { SerpentGenerator } from 'leviathan-crypto/serpent'
358
+ import { SHA256Hash } from 'leviathan-crypto/sha2'
193
359
  import { serpentWasm } from 'leviathan-crypto/serpent/embedded'
194
360
  import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
195
361
 
196
362
  await init({ serpent: serpentWasm, sha2: sha2Wasm })
197
- const rng = await Fortuna.create()
363
+ const rng = await Fortuna.create({ generator: SerpentGenerator, hash: SHA256Hash })
198
364
 
199
365
  // Feed application-specific data as additional entropy.
200
366
  // This supplements (never replaces) the automatic entropy collection.
@@ -214,33 +380,31 @@ rng.stop()
214
380
 
215
381
  ```typescript
216
382
  import { init, Fortuna } from 'leviathan-crypto'
217
- import { serpentWasm } from 'leviathan-crypto/serpent/embedded'
383
+ import { ChaCha20Generator } from 'leviathan-crypto/chacha20'
384
+ import { SHA256Hash } from 'leviathan-crypto/sha2'
385
+ import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
218
386
  import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
219
387
 
220
- await init({ serpent: serpentWasm, sha2: sha2Wasm })
388
+ await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
221
389
 
222
390
  // Fortuna automatically registers browser event listeners on creation:
223
- // - mousemove (throttled to 50ms)
224
- // - keydown
225
- // - click
226
- // - scroll
227
- // - touchstart, touchmove, touchend
228
- // - devicemotion, deviceorientation, orientationchange
229
- //
391
+ // mousemove (throttled to 50ms), keydown, click, scroll,
392
+ // touchstart, touchmove, touchend,
393
+ // devicemotion, deviceorientation, orientationchange.
230
394
  // Every user interaction feeds entropy into the pools.
231
- // No manual setup is needed, it starts collecting immediately.
395
+ // No manual setup is needed; collection starts immediately.
232
396
 
233
- const rng = await Fortuna.create()
397
+ const rng = await Fortuna.create({ generator: ChaCha20Generator, hash: SHA256Hash })
234
398
 
235
399
  // The longer the user interacts with the page before you generate,
236
- // the more entropy has been accumulated. But the initial OS seed
237
- // is strong enough for immediate use.
400
+ // the more entropy accumulates. The initial OS seed is strong enough
401
+ // for immediate use.
238
402
  document.querySelector('#generate')?.addEventListener('click', () => {
239
- const bytes = rng.get(32)
240
- console.log('Generated:', bytes)
403
+ const bytes = rng.get(32)
404
+ console.log('Generated:', bytes)
241
405
  })
242
406
 
243
- // When the page unloads or the component unmounts, stop the collectors
407
+ // Stop the collectors when the page unloads or the component unmounts.
244
408
  window.addEventListener('beforeunload', () => rng.stop())
245
409
  ```
246
410
 
@@ -248,18 +412,23 @@ window.addEventListener('beforeunload', () => rng.stop())
248
412
 
249
413
  ```typescript
250
414
  import { init, Fortuna } from 'leviathan-crypto'
415
+ import { SerpentGenerator } from 'leviathan-crypto/serpent'
416
+ import { SHA256Hash } from 'leviathan-crypto/sha2'
251
417
  import { serpentWasm } from 'leviathan-crypto/serpent/embedded'
252
418
  import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
253
419
 
254
420
  await init({ serpent: serpentWasm, sha2: sha2Wasm })
255
421
 
256
- // You can pass extra entropy at creation time.
257
- // This is mixed into the pools during initialization, before the
258
- // generator is first seeded.
422
+ // Pass extra entropy at creation time. It is mixed into the pools during
423
+ // initialization, before the generator is first seeded.
259
424
  const extraSeed = new Uint8Array(64)
260
425
  crypto.getRandomValues(extraSeed)
261
426
 
262
- const rng = await Fortuna.create({ entropy: extraSeed })
427
+ const rng = await Fortuna.create({
428
+ generator: SerpentGenerator,
429
+ hash: SHA256Hash,
430
+ entropy: extraSeed,
431
+ })
263
432
  const bytes = rng.get(32)
264
433
  rng.stop()
265
434
  ```
@@ -270,8 +439,10 @@ rng.stop()
270
439
 
271
440
  | Condition | What happens |
272
441
  |-----------|-------------|
273
- | `init()` not called | `Fortuna.create()` throws: `leviathan-crypto: call init({ serpent: ..., sha2: ... }) before using Fortuna` |
274
- | Only one module initialized | Same error. Both `serpent` and `sha2` must be initialized. |
442
+ | `init()` not called for the required modules | `Fortuna.create()` throws: `leviathan-crypto: call init({ <m1>: ..., <m2>: ... }) before using Fortuna`, naming the modules required by the chosen generator and hash. |
443
+ | `opts.generator` or `opts.hash` missing | `Fortuna.create()` throws `TypeError: leviathan-crypto: Fortuna.create() requires { generator, hash }`. |
444
+ | `hash.outputSize !== generator.keySize` | `Fortuna.create()` throws `RangeError: leviathan-crypto: Fortuna requires hash.outputSize (X) to match generator.keySize (Y)`. |
445
+ | No working entropy source | `Fortuna.create()` throws: `leviathan-crypto: Fortuna initialization could not gather sufficient entropy. No working crypto.getRandomValues or node:crypto in this environment.` |
275
446
  | `new Fortuna()` | Compile-time error. The constructor is private. TypeScript will not allow it. |
276
447
  | Any method after `stop()` | Throws: `Fortuna instance has been disposed`. The instance is permanently disposed. |
277
448
 
@@ -279,35 +450,81 @@ rng.stop()
279
450
 
280
451
  ## How It Works (Simplified)
281
452
 
282
- For readers who want to understand what Fortuna does internally, without needing
283
- to read the spec:
453
+ For readers who want to understand what Fortuna does internally, without
454
+ reading the spec:
284
455
 
285
456
  1. **Entropy collection.** Background listeners and timers capture small,
286
457
  unpredictable measurements (mouse coordinates, nanosecond timings, memory
287
- usage) and feed them into 32 separate pools via SHA-256 hash chaining.
458
+ usage) and feed them into 32 separate pools via the chosen hash
459
+ function's chaining construction.
460
+
461
+ 2. **Reseed.** When pool 0 has accumulated enough entropy and enough time
462
+ has passed since the last reseed, Fortuna combines the contents of
463
+ eligible pools (per *Practical Cryptography* §9.5.5: pool P_i contributes
464
+ when 2^i divides the reseed counter) into a seed, and derives a new
465
+ generation key: `genKey = hash(genKey || seed)`.
466
+
467
+ 3. **Generation.** To produce output, the generator runs the chosen cipher
468
+ PRF on an incrementing counter under the current generation key. For
469
+ Serpent-256, this is ECB encryption of the counter block. For ChaCha20,
470
+ this is the block function with a fixed zero nonce and the counter as
471
+ block index. The output is the concatenation of cipher output blocks,
472
+ truncated to the requested length.
473
+
474
+ 4. **Key replacement.** Immediately after producing output, the generator
475
+ runs again to produce 32 fresh bytes for the new generation key. The old
476
+ key is wiped. This is what provides forward secrecy.
477
+
478
+ ---
479
+
480
+ ## Coexistence with raw ciphers
481
+
482
+ `Fortuna` calls into the chosen `Generator` and `HashFn` for every
483
+ operation. Both are stateless: they assert that no other instance owns the
484
+ WASM module before each call, but they do not acquire the module
485
+ themselves.
486
+
487
+ If you construct a stateful cipher that does acquire the module, subsequent
488
+ Fortuna operations on the same module throw the ownership error from
489
+ `init.ts`:
490
+
491
+ ```
492
+ leviathan-crypto: another stateful instance is using the '<module>' WASM module — call dispose() on it before constructing a new one
493
+ ```
494
+
495
+ The relevant pairings:
288
496
 
289
- 2. **Reseed.** When pool 0 has accumulated enough entropy and enough time has
290
- passed since the last reseed, Fortuna combines the contents of eligible pools
291
- (determined by the reseed counter) into a seed, and derives a new generation
292
- key: `genKey = SHA-256(genKey || seed)`.
497
+ - `SerpentGenerator` blocked by `Serpent`, `SerpentCtr`, `SerpentCbc`, or any other live serpent acquirer.
498
+ - `ChaCha20Generator` blocked by `ChaCha20` (the raw stream cipher acquires the chacha20 module on construction).
499
+ - `SHA256Hash` blocked by any future stateful sha2 user (none currently exist; `HMAC_SHA256` and `HKDF_SHA256` are atomic).
500
+ - `SHA3_256Hash` blocked by `SHAKE128` or `SHAKE256` while they hold the sha3 module, or by `MlKem*` keypair generation while it holds the sha3 module for its duration.
293
501
 
294
- 3. **Generation.** To produce output, the generator encrypts an incrementing
295
- counter with Serpent-256 in ECB mode using the current generation key. The
296
- output is the concatenation of encrypted counter blocks, truncated to the
297
- requested length.
502
+ Disposing the conflicting cipher restores normal operation. `fortuna.stop()`
503
+ called while a conflicting cipher still holds the module also throws the
504
+ same ownership error, but does so *after* marking the instance disposed and
505
+ wiping all JavaScript-side key material. The throw signals only that the
506
+ inner WASM module's scratch buffer was not zeroed. The Fortuna instance is
507
+ permanently disposed regardless.
298
508
 
299
- 4. **Key replacement.** Immediately after producing output, the generation key
300
- is replaced with fresh pseudorandom blocks. The old key is gone. This is what
301
- provides forward secrecy.
509
+ The library raises this as an error rather than allowing two instances to
510
+ clobber each other's WASM state, which would silently produce incorrect
511
+ output from both.
302
512
 
303
513
  ---
304
514
 
305
- > ## Cross-References
306
- >
307
- > - [index](./README.md) Project Documentation index
308
- > - [architecture](./architecture.md) architecture overview, module relationships, buffer layouts, and build pipeline
309
- > - [serpent](./serpent.md) Serpent-256 TypeScript API (Fortuna uses Serpent ECB internally)
310
- > - [sha2](./sha2.md) SHA-256 TypeScript API (Fortuna uses SHA-256 for entropy accumulation)
311
- > - [asm_serpent](./asm_serpent.md) Serpent-256 WASM implementation details
312
- > - [asm_sha2](./asm_sha2.md) SHA-256 WASM implementation details
313
- > - [utils](./utils.md) `randomBytes()` for simpler random generation needs
515
+ ## Cross-References
516
+
517
+ | Document | Description |
518
+ | -------- | ----------- |
519
+ | [index](./README.md) | Project Documentation index |
520
+ | [architecture](./architecture.md) | architecture overview, module relationships, buffer layouts, and build pipeline |
521
+ | [serpent](./serpent.md) | Serpent-256 TypeScript API (one option for the Fortuna generator) |
522
+ | [chacha20](./chacha20.md) | ChaCha20 TypeScript API (the other option for the Fortuna generator) |
523
+ | [sha2](./sha2.md) | SHA-256 TypeScript API (one option for the Fortuna hash) |
524
+ | [sha3](./sha3.md) | SHA3-256 TypeScript API (the other option for the Fortuna hash) |
525
+ | [types](./types.md) | `Generator` and `HashFn` interface definitions |
526
+ | [asm_serpent](./asm_serpent.md) | Serpent-256 WASM implementation details |
527
+ | [asm_chacha](./asm_chacha.md) | ChaCha20 WASM implementation details |
528
+ | [asm_sha2](./asm_sha2.md) | SHA-256 WASM implementation details |
529
+ | [asm_sha3](./asm_sha3.md) | SHA3 WASM implementation details |
530
+ | [utils](./utils.md) | `randomBytes()` for simpler random generation needs |