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.
- package/CLAUDE.md +171 -7
- package/LICENSE +4 -0
- package/README.md +109 -54
- package/SECURITY.md +125 -238
- package/dist/chacha20/cipher-suite.d.ts +10 -0
- package/dist/chacha20/cipher-suite.js +65 -2
- package/dist/chacha20/generator.d.ts +12 -0
- package/dist/chacha20/generator.js +91 -0
- package/dist/chacha20/index.d.ts +97 -1
- package/dist/chacha20/index.js +139 -11
- package/dist/chacha20/ops.d.ts +57 -6
- package/dist/chacha20/ops.js +93 -13
- package/dist/chacha20/pool-worker.js +12 -0
- package/dist/chacha20/types.d.ts +1 -32
- package/dist/ct-wasm.js +1 -1
- package/dist/ct.wasm +0 -0
- package/dist/docs/aead.md +66 -26
- package/dist/docs/architecture.md +600 -521
- package/dist/docs/argon2id.md +17 -14
- package/dist/docs/chacha20.md +146 -39
- package/dist/docs/exports.md +46 -10
- package/dist/docs/fortuna.md +339 -122
- package/dist/docs/init.md +24 -25
- package/dist/docs/loader.md +142 -47
- package/dist/docs/serpent.md +139 -41
- package/dist/docs/sha2.md +77 -19
- package/dist/docs/sha3.md +81 -15
- package/dist/docs/types.md +155 -15
- package/dist/docs/utils.md +171 -81
- package/dist/embedded/chacha20-pool-worker.d.ts +1 -0
- package/dist/embedded/chacha20-pool-worker.js +5 -0
- package/dist/embedded/kyber.d.ts +1 -1
- package/dist/embedded/kyber.js +1 -1
- package/dist/embedded/serpent-pool-worker.d.ts +1 -0
- package/dist/embedded/serpent-pool-worker.js +5 -0
- package/dist/fortuna.d.ts +14 -8
- package/dist/fortuna.js +144 -50
- package/dist/index.d.ts +8 -6
- package/dist/index.js +6 -5
- package/dist/init.d.ts +0 -2
- package/dist/init.js +83 -3
- package/dist/kyber/indcpa.js +4 -4
- package/dist/kyber/index.js +25 -5
- package/dist/kyber/kem.js +56 -1
- package/dist/kyber/suite.d.ts +1 -2
- package/dist/kyber/types.d.ts +1 -0
- package/dist/kyber/validate.d.ts +8 -4
- package/dist/kyber/validate.js +18 -14
- package/dist/kyber.wasm +0 -0
- package/dist/loader.d.ts +7 -2
- package/dist/loader.js +25 -28
- package/dist/ratchet/index.d.ts +6 -0
- package/dist/ratchet/index.js +37 -0
- package/dist/ratchet/kdf-chain.d.ts +13 -0
- package/dist/ratchet/kdf-chain.js +85 -0
- package/dist/ratchet/ratchet-keypair.d.ts +9 -0
- package/dist/ratchet/ratchet-keypair.js +61 -0
- package/dist/ratchet/root-kdf.d.ts +4 -0
- package/dist/ratchet/root-kdf.js +124 -0
- package/dist/ratchet/skipped-key-store.d.ts +14 -0
- package/dist/ratchet/skipped-key-store.js +154 -0
- package/dist/ratchet/types.d.ts +36 -0
- package/dist/ratchet/types.js +26 -0
- package/dist/serpent/cipher-suite.d.ts +10 -0
- package/dist/serpent/cipher-suite.js +135 -50
- package/dist/serpent/generator.d.ts +12 -0
- package/dist/serpent/generator.js +97 -0
- package/dist/serpent/index.d.ts +61 -1
- package/dist/serpent/index.js +92 -7
- package/dist/serpent/pool-worker.js +25 -101
- package/dist/serpent/serpent-cbc.d.ts +14 -4
- package/dist/serpent/serpent-cbc.js +50 -32
- package/dist/serpent/shared-ops.d.ts +83 -0
- package/dist/serpent/shared-ops.js +213 -0
- package/dist/serpent/types.d.ts +1 -5
- package/dist/sha2/hash.d.ts +2 -0
- package/dist/sha2/hash.js +53 -0
- package/dist/sha2/index.d.ts +1 -0
- package/dist/sha2/index.js +15 -1
- package/dist/sha3/hash.d.ts +2 -0
- package/dist/sha3/hash.js +53 -0
- package/dist/sha3/index.d.ts +17 -2
- package/dist/sha3/index.js +79 -7
- package/dist/stream/header.js +5 -5
- package/dist/stream/open-stream.js +36 -14
- package/dist/stream/seal-stream-pool.d.ts +1 -0
- package/dist/stream/seal-stream-pool.js +38 -8
- package/dist/stream/seal-stream.js +29 -11
- package/dist/types.d.ts +21 -0
- package/dist/utils.d.ts +7 -8
- package/dist/utils.js +73 -40
- package/dist/wasm-source.d.ts +9 -8
- package/package.json +79 -64
package/dist/docs/fortuna.md
CHANGED
|
@@ -1,90 +1,202 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
20
|
-
bytes that are indistinguishable from true randomness to any observer,
|
|
21
|
-
with significant computational resources. This matters because many
|
|
22
|
-
operations require unpredictable randomness: generating encryption
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
97
|
+
This library replaces both with a pluggable contract. The deviations:
|
|
52
98
|
|
|
53
|
-
**
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
| `opts.
|
|
83
|
-
| `opts.
|
|
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 `
|
|
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
|
|
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
|
|
100
|
-
after `create()` resolves, so this method is guaranteed to return
|
|
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
|
|
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
|
|
117
|
-
is distributed across pools using round-robin assignment. Each call
|
|
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.
|
|
131
|
-
is an estimate, not a guarantee. It reflects the sum of entropy credits
|
|
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
|
|
145
|
-
> operation. If a new Fortuna instance is needed, call
|
|
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
|
-
|
|
149
|
-
-
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
on
|
|
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
|
-
|
|
177
|
-
const
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
const nonce = rng.get(12)
|
|
346
|
+
await init({ chacha20: chacha20Wasm, sha3: sha3Wasm })
|
|
184
347
|
|
|
185
|
-
|
|
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 {
|
|
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({
|
|
388
|
+
await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
|
|
221
389
|
|
|
222
390
|
// Fortuna automatically registers browser event listeners on creation:
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
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
|
|
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
|
|
237
|
-
//
|
|
400
|
+
// the more entropy accumulates. The initial OS seed is strong enough
|
|
401
|
+
// for immediate use.
|
|
238
402
|
document.querySelector('#generate')?.addEventListener('click', () => {
|
|
239
|
-
|
|
240
|
-
|
|
403
|
+
const bytes = rng.get(32)
|
|
404
|
+
console.log('Generated:', bytes)
|
|
241
405
|
})
|
|
242
406
|
|
|
243
|
-
//
|
|
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
|
-
//
|
|
257
|
-
//
|
|
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({
|
|
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({
|
|
274
|
-
|
|
|
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
|
|
283
|
-
|
|
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
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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 |
|