leviathan-crypto 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/CLAUDE.md +86 -443
  2. package/README.md +198 -65
  3. package/dist/aes/aes-cbc.d.ts +40 -0
  4. package/dist/aes/aes-cbc.js +158 -0
  5. package/dist/aes/aes-ctr.d.ts +50 -0
  6. package/dist/aes/aes-ctr.js +141 -0
  7. package/dist/aes/aes-gcm-siv.d.ts +67 -0
  8. package/dist/aes/aes-gcm-siv.js +217 -0
  9. package/dist/aes/aes-gcm.d.ts +61 -0
  10. package/dist/aes/aes-gcm.js +226 -0
  11. package/dist/aes/cipher-suite.d.ts +21 -0
  12. package/dist/aes/cipher-suite.js +179 -0
  13. package/dist/aes/embedded.d.ts +1 -0
  14. package/dist/aes/embedded.js +26 -0
  15. package/dist/aes/generator.d.ts +14 -0
  16. package/dist/aes/generator.js +103 -0
  17. package/dist/aes/index.d.ts +58 -0
  18. package/dist/aes/index.js +125 -0
  19. package/dist/aes/ops.d.ts +60 -0
  20. package/dist/aes/ops.js +164 -0
  21. package/dist/aes/pool-worker.d.ts +1 -0
  22. package/dist/aes/pool-worker.js +92 -0
  23. package/dist/aes/types.d.ts +1 -0
  24. package/dist/aes/types.js +23 -0
  25. package/dist/aes.wasm +0 -0
  26. package/dist/blake3/embedded.d.ts +1 -0
  27. package/dist/blake3/embedded.js +26 -0
  28. package/dist/blake3/index.d.ts +143 -0
  29. package/dist/blake3/index.js +620 -0
  30. package/dist/blake3/types.d.ts +102 -0
  31. package/dist/blake3/types.js +31 -0
  32. package/dist/blake3/validate.d.ts +29 -0
  33. package/dist/blake3/validate.js +80 -0
  34. package/dist/blake3.wasm +0 -0
  35. package/dist/chacha20/cipher-suite.js +47 -25
  36. package/dist/chacha20/generator.d.ts +2 -2
  37. package/dist/chacha20/generator.js +4 -4
  38. package/dist/chacha20/index.d.ts +16 -15
  39. package/dist/chacha20/index.js +52 -46
  40. package/dist/chacha20/ops.d.ts +7 -7
  41. package/dist/chacha20/ops.js +34 -34
  42. package/dist/chacha20/pool-worker.js +5 -3
  43. package/dist/cte-wasm.d.ts +1 -0
  44. package/dist/cte-wasm.js +3 -0
  45. package/dist/curve25519.wasm +0 -0
  46. package/dist/ecdsa/der.d.ts +23 -0
  47. package/dist/ecdsa/der.js +192 -0
  48. package/dist/ecdsa/ecprivatekey-der.d.ts +32 -0
  49. package/dist/ecdsa/ecprivatekey-der.js +230 -0
  50. package/dist/ecdsa/embedded.d.ts +1 -0
  51. package/dist/ecdsa/embedded.js +25 -0
  52. package/dist/ecdsa/index.d.ts +124 -0
  53. package/dist/ecdsa/index.js +366 -0
  54. package/dist/ecdsa/types.d.ts +31 -0
  55. package/dist/ecdsa/types.js +28 -0
  56. package/dist/ecdsa/validate.d.ts +18 -0
  57. package/dist/ecdsa/validate.js +92 -0
  58. package/dist/ed25519/embedded.d.ts +1 -0
  59. package/dist/ed25519/embedded.js +31 -0
  60. package/dist/ed25519/index.d.ts +70 -0
  61. package/dist/ed25519/index.js +308 -0
  62. package/dist/ed25519/types.d.ts +27 -0
  63. package/dist/ed25519/types.js +27 -0
  64. package/dist/ed25519/validate.d.ts +7 -0
  65. package/dist/ed25519/validate.js +77 -0
  66. package/dist/embedded/aes-pool-worker.d.ts +1 -0
  67. package/dist/embedded/aes-pool-worker.js +5 -0
  68. package/dist/embedded/aes.d.ts +1 -0
  69. package/dist/embedded/aes.js +3 -0
  70. package/dist/embedded/blake3.d.ts +1 -0
  71. package/dist/embedded/blake3.js +3 -0
  72. package/dist/embedded/chacha20-pool-worker.d.ts +1 -1
  73. package/dist/embedded/chacha20-pool-worker.js +2 -2
  74. package/dist/embedded/chacha20.d.ts +1 -1
  75. package/dist/embedded/chacha20.js +2 -2
  76. package/dist/embedded/curve25519.d.ts +1 -0
  77. package/dist/embedded/curve25519.js +3 -0
  78. package/dist/embedded/mldsa.d.ts +1 -0
  79. package/dist/embedded/mldsa.js +3 -0
  80. package/dist/embedded/mlkem.d.ts +1 -0
  81. package/dist/embedded/mlkem.js +3 -0
  82. package/dist/embedded/p256.d.ts +1 -0
  83. package/dist/embedded/p256.js +3 -0
  84. package/dist/embedded/serpent-pool-worker.d.ts +1 -1
  85. package/dist/embedded/serpent-pool-worker.js +2 -2
  86. package/dist/embedded/serpent.d.ts +1 -1
  87. package/dist/embedded/serpent.js +2 -2
  88. package/dist/embedded/sha2.d.ts +1 -1
  89. package/dist/embedded/sha2.js +2 -2
  90. package/dist/embedded/sha3.d.ts +1 -1
  91. package/dist/embedded/sha3.js +2 -2
  92. package/dist/embedded/slhdsa.d.ts +1 -0
  93. package/dist/embedded/slhdsa.js +3 -0
  94. package/dist/errors.d.ts +92 -1
  95. package/dist/errors.js +111 -1
  96. package/dist/fortuna.d.ts +5 -5
  97. package/dist/fortuna.js +37 -64
  98. package/dist/index.d.ts +38 -9
  99. package/dist/index.js +63 -19
  100. package/dist/init.d.ts +1 -1
  101. package/dist/init.js +11 -25
  102. package/dist/keccak/embedded.js +1 -1
  103. package/dist/keccak/index.d.ts +2 -0
  104. package/dist/keccak/index.js +4 -2
  105. package/dist/loader.d.ts +1 -24
  106. package/dist/loader.js +13 -16
  107. package/dist/merkle/blake3-tree.d.ts +35 -0
  108. package/dist/merkle/blake3-tree.js +187 -0
  109. package/dist/merkle/checkpoint.d.ts +58 -0
  110. package/dist/merkle/checkpoint.js +217 -0
  111. package/dist/merkle/index.d.ts +19 -0
  112. package/dist/merkle/index.js +37 -0
  113. package/dist/merkle/merkle-log.d.ts +130 -0
  114. package/dist/merkle/merkle-log.js +207 -0
  115. package/dist/merkle/merkle-verifier.d.ts +126 -0
  116. package/dist/merkle/merkle-verifier.js +296 -0
  117. package/dist/merkle/proof.d.ts +70 -0
  118. package/dist/merkle/proof.js +300 -0
  119. package/dist/merkle/sha256-tree.d.ts +33 -0
  120. package/dist/merkle/sha256-tree.js +145 -0
  121. package/dist/merkle/signed-log.d.ts +156 -0
  122. package/dist/merkle/signed-log.js +356 -0
  123. package/dist/merkle/signed-note.d.ts +309 -0
  124. package/dist/merkle/signed-note.js +648 -0
  125. package/dist/merkle/sth.d.ts +31 -0
  126. package/dist/merkle/sth.js +31 -0
  127. package/dist/merkle/storage.d.ts +40 -0
  128. package/dist/merkle/storage.js +71 -0
  129. package/dist/merkle/tree.d.ts +68 -0
  130. package/dist/merkle/tree.js +94 -0
  131. package/dist/mldsa/embedded.d.ts +1 -0
  132. package/dist/{kyber → mldsa}/embedded.js +5 -5
  133. package/dist/mldsa/expand.d.ts +53 -0
  134. package/dist/mldsa/expand.js +188 -0
  135. package/dist/mldsa/format.d.ts +16 -0
  136. package/dist/mldsa/format.js +68 -0
  137. package/dist/mldsa/hashvariant.d.ts +32 -0
  138. package/dist/mldsa/hashvariant.js +248 -0
  139. package/dist/mldsa/index.d.ts +142 -0
  140. package/dist/mldsa/index.js +463 -0
  141. package/dist/mldsa/keygen.d.ts +16 -0
  142. package/dist/mldsa/keygen.js +232 -0
  143. package/dist/mldsa/params.d.ts +21 -0
  144. package/dist/mldsa/params.js +55 -0
  145. package/dist/mldsa/sha3-helpers.d.ts +30 -0
  146. package/dist/mldsa/sha3-helpers.js +124 -0
  147. package/dist/mldsa/sign.d.ts +36 -0
  148. package/dist/mldsa/sign.js +380 -0
  149. package/dist/mldsa/types.d.ts +91 -0
  150. package/dist/mldsa/types.js +25 -0
  151. package/dist/mldsa/validate.d.ts +55 -0
  152. package/dist/mldsa/validate.js +125 -0
  153. package/dist/mldsa/verify.d.ts +29 -0
  154. package/dist/mldsa/verify.js +269 -0
  155. package/dist/mldsa.wasm +0 -0
  156. package/dist/mlkem/embedded.d.ts +1 -0
  157. package/dist/mlkem/embedded.js +27 -0
  158. package/dist/mlkem/indcpa.d.ts +49 -0
  159. package/dist/{kyber → mlkem}/indcpa.js +44 -44
  160. package/dist/mlkem/index.d.ts +37 -0
  161. package/dist/{kyber → mlkem}/index.js +24 -34
  162. package/dist/mlkem/kem.d.ts +21 -0
  163. package/dist/{kyber → mlkem}/kem.js +44 -64
  164. package/dist/{kyber → mlkem}/params.d.ts +4 -4
  165. package/dist/{kyber → mlkem}/params.js +2 -2
  166. package/dist/mlkem/suite.d.ts +12 -0
  167. package/dist/{kyber → mlkem}/suite.js +17 -12
  168. package/dist/{kyber → mlkem}/types.d.ts +3 -3
  169. package/dist/{kyber → mlkem}/types.js +1 -1
  170. package/dist/{kyber → mlkem}/validate.d.ts +7 -7
  171. package/dist/{kyber → mlkem}/validate.js +7 -7
  172. package/dist/{kyber.wasm → mlkem.wasm} +0 -0
  173. package/dist/p256.wasm +0 -0
  174. package/dist/ratchet/index.d.ts +2 -0
  175. package/dist/ratchet/index.js +1 -0
  176. package/dist/ratchet/kdf-chain.js +3 -3
  177. package/dist/ratchet/ratchet-keypair.js +2 -2
  178. package/dist/ratchet/root-kdf.js +7 -7
  179. package/dist/ratchet/skipped-key-store.js +4 -4
  180. package/dist/ratchet/types.d.ts +1 -1
  181. package/dist/serpent/cipher-suite.js +20 -17
  182. package/dist/serpent/generator.d.ts +1 -1
  183. package/dist/serpent/generator.js +2 -2
  184. package/dist/serpent/index.d.ts +8 -7
  185. package/dist/serpent/index.js +18 -27
  186. package/dist/serpent/pool-worker.js +7 -5
  187. package/dist/serpent/serpent-cbc.d.ts +4 -4
  188. package/dist/serpent/serpent-cbc.js +11 -8
  189. package/dist/serpent/shared-ops.d.ts +3 -23
  190. package/dist/serpent/shared-ops.js +50 -85
  191. package/dist/serpent.wasm +0 -0
  192. package/dist/sha2/hkdf.js +5 -5
  193. package/dist/sha2/index.d.ts +21 -1
  194. package/dist/sha2/index.js +65 -10
  195. package/dist/sha2/types.d.ts +41 -2
  196. package/dist/sha2.wasm +0 -0
  197. package/dist/sha3/index.d.ts +72 -3
  198. package/dist/sha3/index.js +240 -14
  199. package/dist/sha3/kmac.d.ts +121 -0
  200. package/dist/sha3/kmac.js +800 -0
  201. package/dist/sha3.wasm +0 -0
  202. package/dist/shared/pkcs7.d.ts +22 -0
  203. package/dist/shared/pkcs7.js +84 -0
  204. package/dist/sign/ctx.d.ts +41 -0
  205. package/dist/sign/ctx.js +102 -0
  206. package/dist/sign/envelope.d.ts +45 -0
  207. package/dist/sign/envelope.js +152 -0
  208. package/dist/sign/hasher.d.ts +9 -0
  209. package/dist/sign/hasher.js +132 -0
  210. package/dist/sign/index.d.ts +11 -0
  211. package/dist/sign/index.js +34 -0
  212. package/dist/sign/sign-stream.d.ts +25 -0
  213. package/dist/sign/sign-stream.js +112 -0
  214. package/dist/sign/suites/ecdsa-p256.d.ts +2 -0
  215. package/dist/sign/suites/ecdsa-p256.js +120 -0
  216. package/dist/sign/suites/ed25519.d.ts +3 -0
  217. package/dist/sign/suites/ed25519.js +165 -0
  218. package/dist/sign/suites/hybrid-classical.d.ts +23 -0
  219. package/dist/sign/suites/hybrid-classical.js +526 -0
  220. package/dist/sign/suites/hybrid-pq.d.ts +4 -0
  221. package/dist/sign/suites/hybrid-pq.js +234 -0
  222. package/dist/sign/suites/mldsa.d.ts +7 -0
  223. package/dist/sign/suites/mldsa.js +161 -0
  224. package/dist/sign/suites/slhdsa.d.ts +7 -0
  225. package/dist/sign/suites/slhdsa.js +176 -0
  226. package/dist/sign/types.d.ts +106 -0
  227. package/dist/sign/types.js +28 -0
  228. package/dist/sign/verify-stream.d.ts +30 -0
  229. package/dist/sign/verify-stream.js +227 -0
  230. package/dist/slhdsa/embedded.d.ts +1 -0
  231. package/dist/slhdsa/embedded.js +26 -0
  232. package/dist/slhdsa/index.d.ts +149 -0
  233. package/dist/slhdsa/index.js +493 -0
  234. package/dist/slhdsa/params.d.ts +26 -0
  235. package/dist/slhdsa/params.js +70 -0
  236. package/dist/slhdsa/prehash.d.ts +68 -0
  237. package/dist/slhdsa/prehash.js +307 -0
  238. package/dist/slhdsa/sign.d.ts +39 -0
  239. package/dist/slhdsa/sign.js +116 -0
  240. package/dist/slhdsa/types.d.ts +129 -0
  241. package/dist/slhdsa/types.js +27 -0
  242. package/dist/slhdsa/validate.d.ts +60 -0
  243. package/dist/slhdsa/validate.js +127 -0
  244. package/dist/slhdsa/verify.d.ts +32 -0
  245. package/dist/slhdsa/verify.js +107 -0
  246. package/dist/slhdsa.wasm +0 -0
  247. package/dist/stream/header.js +3 -3
  248. package/dist/stream/index.d.ts +1 -0
  249. package/dist/stream/index.js +1 -0
  250. package/dist/stream/open-stream.js +31 -10
  251. package/dist/stream/seal-stream-pool.d.ts +1 -0
  252. package/dist/stream/seal-stream-pool.js +63 -26
  253. package/dist/stream/seal-stream.d.ts +1 -1
  254. package/dist/stream/seal-stream.js +20 -9
  255. package/dist/stream/seal.js +6 -6
  256. package/dist/stream/types.d.ts +3 -1
  257. package/dist/stream/types.js +1 -1
  258. package/dist/types.d.ts +1 -1
  259. package/dist/types.js +1 -1
  260. package/dist/utils.d.ts +3 -3
  261. package/dist/utils.js +46 -54
  262. package/dist/wasm-source.d.ts +7 -7
  263. package/dist/wasm-source.js +1 -1
  264. package/dist/x25519/embedded.d.ts +1 -0
  265. package/dist/x25519/embedded.js +31 -0
  266. package/dist/x25519/index.d.ts +43 -0
  267. package/dist/x25519/index.js +159 -0
  268. package/dist/x25519/types.d.ts +25 -0
  269. package/dist/x25519/types.js +27 -0
  270. package/dist/x25519/validate.d.ts +2 -0
  271. package/dist/x25519/validate.js +39 -0
  272. package/package.json +70 -26
  273. package/SECURITY.md +0 -163
  274. package/dist/ct-wasm.d.ts +0 -1
  275. package/dist/ct-wasm.js +0 -3
  276. package/dist/docs/aead.md +0 -363
  277. package/dist/docs/architecture.md +0 -1011
  278. package/dist/docs/argon2id.md +0 -305
  279. package/dist/docs/chacha20.md +0 -781
  280. package/dist/docs/exports.md +0 -277
  281. package/dist/docs/fortuna.md +0 -530
  282. package/dist/docs/init.md +0 -301
  283. package/dist/docs/loader.md +0 -256
  284. package/dist/docs/serpent.md +0 -617
  285. package/dist/docs/sha2.md +0 -671
  286. package/dist/docs/sha3.md +0 -612
  287. package/dist/docs/types.md +0 -416
  288. package/dist/docs/utils.md +0 -457
  289. package/dist/embedded/kyber.d.ts +0 -1
  290. package/dist/embedded/kyber.js +0 -3
  291. package/dist/kyber/embedded.d.ts +0 -1
  292. package/dist/kyber/indcpa.d.ts +0 -49
  293. package/dist/kyber/index.d.ts +0 -38
  294. package/dist/kyber/kem.d.ts +0 -21
  295. package/dist/kyber/suite.d.ts +0 -12
  296. /package/dist/{ct.wasm → cte.wasm} +0 -0
@@ -1,530 +0,0 @@
1
- <img src="https://github.com/xero/leviathan-crypto/raw/main/docs/logo.svg" alt="logo" width="120" align="left" margin="10">
2
-
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
-
7
- > ### Table of Contents
8
- > - [Overview](#overview)
9
- > - [Pluggable primitives](#pluggable-primitives)
10
- > - [Spec deviations](#spec-deviations)
11
- > - [Security Notes](#security-notes)
12
- > - [API Reference](#api-reference)
13
- > - [Usage Examples](#usage-examples)
14
- > - [Error Conditions](#error-conditions)
15
- > - [How It Works (Simplified)](#how-it-works-simplified)
16
- > - [Coexistence with raw ciphers](#coexistence-with-raw-ciphers)
17
- > - [Cross-References](#cross-references)
18
-
19
- ---
20
-
21
- ## Overview
22
-
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.
53
-
54
- ---
55
-
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:
93
-
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.
96
-
97
- This library replaces both with a pluggable contract. The deviations:
98
-
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.
108
-
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.
113
-
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.
120
-
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.
124
-
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.
166
-
167
- ---
168
-
169
- ## API Reference
170
-
171
- ### `Fortuna.create(opts)`
172
-
173
- Static async factory. Returns a `Promise<Fortuna>`. The returned instance is
174
- guaranteed to be seeded. `create()` forces an initial reseed before
175
- resolving, so `get()` is immediately usable.
176
-
177
- ```typescript
178
- static async create(opts: {
179
- generator: Generator;
180
- hash: HashFn;
181
- msPerReseed?: number;
182
- entropy?: Uint8Array;
183
- }): Promise<Fortuna>
184
- ```
185
-
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. |
192
-
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.
197
-
198
- Direct construction with `new Fortuna()` is not possible. The constructor
199
- is private. Always use `Fortuna.create()`.
200
-
201
- ---
202
-
203
- ### `get(length)`
204
-
205
- Generate `length` random bytes.
206
-
207
- ```typescript
208
- get(length: number): Uint8Array
209
- ```
210
-
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.
214
-
215
- After producing the output, the generation key is replaced with fresh
216
- pseudorandom material. This is the forward secrecy mechanism. The key used
217
- to produce this output no longer exists.
218
-
219
- ---
220
-
221
- ### `addEntropy(entropy)`
222
-
223
- Manually add entropy to the pools.
224
-
225
- ```typescript
226
- addEntropy(entropy: Uint8Array): void
227
- ```
228
-
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.
232
-
233
- ---
234
-
235
- ### `getEntropy()`
236
-
237
- Get the estimated available entropy in bytes.
238
-
239
- ```typescript
240
- getEntropy(): number
241
- ```
242
-
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.
246
-
247
- ---
248
-
249
- ### `stop()`
250
-
251
- Permanently dispose this Fortuna instance.
252
-
253
- ```typescript
254
- stop(): void
255
- ```
256
-
257
- > [!WARNING]
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()`.
261
-
262
- Call this when you are done with the Fortuna instance. `stop()`:
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
-
275
- ```
276
- Error: Fortuna instance has been disposed
277
- ```
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
-
288
- There is no `start()` or restart capability.
289
-
290
- ---
291
-
292
- ## Usage Examples
293
-
294
- ### Basic usage
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
-
320
- ```typescript
321
- import { init, Fortuna } from 'leviathan-crypto'
322
- import { SerpentGenerator } from 'leviathan-crypto/serpent'
323
- import { SHA256Hash } from 'leviathan-crypto/sha2'
324
- import { serpentWasm } from 'leviathan-crypto/serpent/embedded'
325
- import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
326
-
327
- await init({ serpent: serpentWasm, sha2: sha2Wasm })
328
-
329
- const rng = await Fortuna.create({ generator: SerpentGenerator, hash: SHA256Hash })
330
- const bytes = rng.get(32)
331
- rng.stop()
332
- ```
333
-
334
- ### Modern combination
335
-
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'
345
-
346
- await init({ chacha20: chacha20Wasm, sha3: sha3Wasm })
347
-
348
- const rng = await Fortuna.create({ generator: ChaCha20Generator, hash: SHA3_256Hash })
349
- const bytes = rng.get(32)
350
- rng.stop()
351
- ```
352
-
353
- ### Adding custom entropy
354
-
355
- ```typescript
356
- import { init, Fortuna, utf8ToBytes } from 'leviathan-crypto'
357
- import { SerpentGenerator } from 'leviathan-crypto/serpent'
358
- import { SHA256Hash } from 'leviathan-crypto/sha2'
359
- import { serpentWasm } from 'leviathan-crypto/serpent/embedded'
360
- import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
361
-
362
- await init({ serpent: serpentWasm, sha2: sha2Wasm })
363
- const rng = await Fortuna.create({ generator: SerpentGenerator, hash: SHA256Hash })
364
-
365
- // Feed application-specific data as additional entropy.
366
- // This supplements (never replaces) the automatic entropy collection.
367
- const userData = utf8ToBytes(crypto.randomUUID())
368
- rng.addEntropy(userData)
369
-
370
- // Server-side: feed in request-specific data
371
- const requestEntropy = new Uint8Array(16)
372
- crypto.getRandomValues(requestEntropy)
373
- rng.addEntropy(requestEntropy)
374
-
375
- const token = rng.get(32)
376
- rng.stop()
377
- ```
378
-
379
- ### Browser with automatic entropy collection
380
-
381
- ```typescript
382
- import { init, Fortuna } from 'leviathan-crypto'
383
- import { ChaCha20Generator } from 'leviathan-crypto/chacha20'
384
- import { SHA256Hash } from 'leviathan-crypto/sha2'
385
- import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
386
- import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
387
-
388
- await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
389
-
390
- // Fortuna automatically registers browser event listeners on creation:
391
- // mousemove (throttled to 50ms), keydown, click, scroll,
392
- // touchstart, touchmove, touchend,
393
- // devicemotion, deviceorientation, orientationchange.
394
- // Every user interaction feeds entropy into the pools.
395
- // No manual setup is needed; collection starts immediately.
396
-
397
- const rng = await Fortuna.create({ generator: ChaCha20Generator, hash: SHA256Hash })
398
-
399
- // The longer the user interacts with the page before you generate,
400
- // the more entropy accumulates. The initial OS seed is strong enough
401
- // for immediate use.
402
- document.querySelector('#generate')?.addEventListener('click', () => {
403
- const bytes = rng.get(32)
404
- console.log('Generated:', bytes)
405
- })
406
-
407
- // Stop the collectors when the page unloads or the component unmounts.
408
- window.addEventListener('beforeunload', () => rng.stop())
409
- ```
410
-
411
- ### Providing initial entropy at creation
412
-
413
- ```typescript
414
- import { init, Fortuna } from 'leviathan-crypto'
415
- import { SerpentGenerator } from 'leviathan-crypto/serpent'
416
- import { SHA256Hash } from 'leviathan-crypto/sha2'
417
- import { serpentWasm } from 'leviathan-crypto/serpent/embedded'
418
- import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
419
-
420
- await init({ serpent: serpentWasm, sha2: sha2Wasm })
421
-
422
- // Pass extra entropy at creation time. It is mixed into the pools during
423
- // initialization, before the generator is first seeded.
424
- const extraSeed = new Uint8Array(64)
425
- crypto.getRandomValues(extraSeed)
426
-
427
- const rng = await Fortuna.create({
428
- generator: SerpentGenerator,
429
- hash: SHA256Hash,
430
- entropy: extraSeed,
431
- })
432
- const bytes = rng.get(32)
433
- rng.stop()
434
- ```
435
-
436
- ---
437
-
438
- ## Error Conditions
439
-
440
- | Condition | What happens |
441
- |-----------|-------------|
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.` |
446
- | `new Fortuna()` | Compile-time error. The constructor is private. TypeScript will not allow it. |
447
- | Any method after `stop()` | Throws: `Fortuna instance has been disposed`. The instance is permanently disposed. |
448
-
449
- ---
450
-
451
- ## How It Works (Simplified)
452
-
453
- For readers who want to understand what Fortuna does internally, without
454
- reading the spec:
455
-
456
- 1. **Entropy collection.** Background listeners and timers capture small,
457
- unpredictable measurements (mouse coordinates, nanosecond timings, memory
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:
496
-
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.
501
-
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.
508
-
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.
512
-
513
- ---
514
-
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 |