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
package/SECURITY.md DELETED
@@ -1,163 +0,0 @@
1
- # Leviathan Crypto Library Security Policy
2
-
3
- <img src="https://github.com/xero/leviathan-crypto/raw/main/docs/logo.svg" alt="Leviathan logo" width="100" align="left">
4
-
5
- - **[Security Posture](#security-posture)**
6
- - **[Cryptanalytic Audits](#cryptanalytic-audits)**
7
- - **[Supported Versions](#supported-versions)**
8
- - **[Vulnerability Reporting](#reporting-a-vulnerability)**
9
-
10
- ---
11
-
12
- ## Security posture
13
-
14
- [leviathan-crypto](https://leviathan.3xi.club/) is a cryptography library. Security shapes every layer of the stack.
15
-
16
- ### Algorithm correctness
17
-
18
- Every primitive in this library was implemented by hand in AssemblyScript against the authoritative specification: [FIPS 180-4](https://csrc.nist.gov/publications/detail/fips/180/4/final) for SHA-2, [FIPS 202](https://csrc.nist.gov/publications/detail/fips/202/final) for SHA-3, [FIPS 203](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.pdf) for ML-KEM, [RFC 8439](https://www.rfc-editor.org/rfc/rfc8439) for ChaCha20-Poly1305, [RFC 2104](https://www.rfc-editor.org/rfc/rfc2104) for HMAC, [RFC 5869](https://www.rfc-editor.org/rfc/rfc5869) for HKDF, and the original [Serpent-256 specification](https://www.cl.cam.ac.uk/~rja14/Papers/serpent.pdf) with S-box reference. No algorithm came from an existing implementation. The spec is the source of truth.
19
-
20
- All implementations verify against published known-answer test vectors from NIST, RFC appendices, NESSIE, and the Argon2 reference suite. Test vectors are immutable; if an implementation produces incorrect output, we fix the code and never adjust the vectors to match.
21
-
22
- ### Side-channel resistance
23
-
24
- Serpent's S-boxes use Boolean gate circuits with no table lookups, no data-dependent memory access, and no data-dependent branches. Every bit processes unconditionally on every block. This is the most timing-safe cipher approach available in WASM, where JIT optimization can otherwise introduce observable timing variation.
25
-
26
- Security-sensitive comparisons (MAC verification, padding validation) use [`constantTimeEqual`](https://github.com/xero/leviathan-crypto/wiki/utils#constanttimeequal), backed by a dedicated WASM SIMD module. The v128 XOR accumulate with branch-free scalar tail reduction eliminates JIT short-circuiting and speculative optimization as side-channel vectors. The function requires WebAssembly SIMD and throws a branded error on runtimes without it, matching the library-wide SIMD requirement. WASM comparison memory gets wiped after every call.
27
-
28
- ### WASM execution model
29
-
30
- All cryptographic computation runs in WebAssembly, isolated outside the JavaScript JIT. WASM execution is deterministic and not subject to JIT speculation or optimization. Each primitive family compiles to its own isolated binary with its own linear memory. Key material in the Serpent module cannot interact with memory in the SHA-3 module, even in principle. A dedicated WASM module handles constant-time comparison with its own single-page memory that is wiped after every call.
31
-
32
- Serpent and ChaCha20 modules require WebAssembly SIMD (v128 instructions). `init()` and `initModule()` perform a SIMD preflight check and throw a clear error on runtimes without support. SIMD has been a baseline feature of all major browsers and runtimes since 2021. SHA-2 and SHA-3 modules run on any WASM-capable runtime.
33
-
34
- The kyber module requires WebAssembly SIMD for NTT and polynomial arithmetic (v128 instructions). The SIMD preflight check applies on `init()` alongside serpent and chacha20. Its linear memory is independent from all other modules. Kyber's constant-time path (FO transform decapsulation) uses dedicated `ct_verify` and `ct_cmov` functions implemented in the kyber WASM binary; comparison never passes through JavaScript.
35
-
36
- Stateful classes (`SHAKE128/256`, `ChaCha20`, `SerpentCtr`, `SerpentCbc`, `MlKem*`) enforce module exclusivity at runtime. A live instance holds an exclusivity token on its backing WASM module; constructing a second instance against the same module throws until the first is disposed. Cross-module operations (kyber decapsulate invoking sha3 hashing) assert non-ownership of the modules they touch before writing to them, preventing silent re-initialization of a live sponge or cipher state.
37
-
38
- ### Memory hygiene
39
-
40
- Every public cryptographic operation zeros its secret and secret-derived scratch before returning. Across all three ML-KEM operations (`keygen`, `encapsulate`, `decapsulate`), no kyber secret or secret-derived data persists in kyber or sha3 linear memory between operations. The CPA secret key, per-message noise polynomials, raw message bytes, PRF output buffers, and FO re-encryption intermediates all get wiped at the operation boundary. AEAD authentication failures wipe the full keystream block and the Poly1305 one-time subkey. Fortuna's `stop()` is a complete teardown: generator key, generator counter, all 32 pool-hash chain values, and a `wipeBuffers()` call on every WASM module the chosen generator and hash touched. Stream constructions (`SealStream`/`OpenStream`) transition to a terminal `'failed'` state on any mid-operation throw, wiping derived keys before the exception propagates.
41
-
42
- This wipe discipline defends against a narrow but concrete threat: an adversary with read access to WASM linear memory between operations. JS-side memory disclosure, host CPU side channels (cache, branch predictor, speculative execution beyond what WASM neuters), and physical device access remain out of scope; those are the runtime's and the hardware's responsibility.
43
-
44
- Key-validation helpers (`checkEncapsulationKey`, `checkDecapsulationKey`) operate on public material only and require no wipe. They are side-effect-free with respect to module state.
45
-
46
- ### Authenticated encryption by default
47
-
48
- Raw unauthenticated cipher modes (`SerpentCbc`, `SerpentCtr`, `ChaCha20`) and stateless caller-managed-nonce primitives (`ChaCha20Poly1305`, `XChaCha20Poly1305`) are exposed for power users but are not the recommended entry point. The primary API surfaces (`Seal`, `SealStream`, `OpenStream`, `SealStreamPool`, and `KyberSuite`) are authenticated by construction with internally managed nonces.
49
-
50
- All streaming constructions satisfy the _Cryptographic Doom Principle_.
51
-
52
- **SealStream/OpenStream with SerpentCipher.** Encrypt-then-MAC (SerpentCbc + HMAC-SHA256). The HMAC tag is compared against the expected tag via `constantTimeEqual` — backed by the dedicated WASM SIMD CT module — and that compare is the unconditional gate into the CBC WASM decrypt path; decryption is unreachable until the gate clears. HKDF key derivation with the stream nonce and counter-nonce domain separation extends this guarantee to full stream integrity.
53
-
54
- **SealStream/OpenStream with XChaCha20Cipher.** XChaCha20-Poly1305 AEAD per chunk. The Poly1305 tag is compared against the expected tag via `constantTimeEqual` — backed by the dedicated WASM SIMD CT module — before any call to the chacha20 WASM decrypt path. On authentication failure, the full chunk output buffer is wiped and plaintext bytes never return. Counter nonces with TAG_DATA/TAG_FINAL final-flag domain separation ensure reorder, splice, truncation, and cross-stream substitution all fail AEAD verification before decryption.
55
-
56
- **SealStreamPool.** Delegates per-chunk AEAD to isolated Web Workers. Each worker holds its own derived subkey and WASM instance. Any authentication error marks the pool dead, rejects all pending operations, requests that each worker zero its in-memory key material, and terminates workers after a short ACK window. Main-thread copies of the derived keys and master key are zeroed synchronously. No retry, no partial results.
57
-
58
- The stateless AEADs (`ChaCha20Poly1305`, `XChaCha20Poly1305`) enforce strict single-use; any throw from `encrypt()` (including length validation errors on `key` or `nonce`) terminates the instance. A retry with valid arguments always raises the single-use guard rather than potentially reusing a nonce. Consumers allocate a fresh AEAD per message.
59
-
60
- ### Key-material lifecycle
61
-
62
- `SkippedKeyStore.resolve` returns a transactional `ResolveHandle` rather than a raw key. The caller settles the handle via `commit()` on successful decryption (the key is wiped) or `rollback()` on authentication failure (the key returns to the store under its counter, so a subsequent legitimate delivery at the same counter can still decrypt). This closes a delete-on-retrieval DoS where an adversary injecting a garbage ciphertext at a valid counter would otherwise consume that counter's cached key before the legitimate message arrived. A `FinalizationRegistry` wipes the key best-effort if a handle is GC'd unsettled.
63
-
64
- `SkippedKeyStore` splits its work budget into `maxCacheSize` (memory bound, default 100) and `maxSkipPerResolve` (per-message HKDF work bound, default 50). A malicious header with a very high counter cannot force unbounded HKDF derivations on the receiver; eviction is O(1) via insertion-order iteration.
65
-
66
- `OpenStream.seek(index)` only moves forward. Backward seeks would reuse an already-consumed per-chunk counter nonce against a new ciphertext, permitting plaintext replay against a stale opener. The call throws rather than silently reusing the nonce.
67
-
68
- ### Dependency management
69
-
70
- This library has zero runtime dependencies by design. `sideEffects: false` is enforced in `package.json`. Argon2id integration is documented as an _optional_ external dependency. See: [leviathan-crypto/wiki/argon2id](https://github.com/xero/leviathan-crypto/wiki/argon2id).
71
-
72
- Build toolchain dependencies use exact version locks in `bun.lock`. GitHub Actions workflows use [SHA-pinned action references](https://github.com/xero/leviathan-crypto/blob/main/scripts/pin-actions.ts) throughout with no floating tags. Supply chain integrity is a first-class concern for a cryptography library.
73
-
74
- ### Explicit initialization
75
-
76
- No class silently auto-initializes. The [`init()`](https://github.com/xero/leviathan-crypto/wiki/init) gate is mandatory and explicit, giving you full control over when WASM modules load and ensuring no hidden initialization costs or race conditions. Classes throw immediately if used before initialization rather than failing silently.
77
-
78
- ### Agentic development contracts
79
-
80
- All AI-assisted development on this repository operates under a strict agentic contract defined in [AGENTS.md](https://github.com/xero/leviathan-crypto/blob/main/AGENTS.md). The contract enforces spec authority over planning documents, immutable test vectors, gate discipline before extending any test suite, independent algorithm derivation from published standards, and constant-time and wipe requirements for all security-sensitive code paths. Agents are explicitly prohibited from guessing cryptographic values or resolving spec ambiguities silently.
81
-
82
- The contract has been verified against Claude, GitHub Copilot (VS Code), OpenHands, Kilo Code, Cursor, Windsurf, and Aider. Configuration files for each are in the repository and all route to [AGENTS.md](https://github.com/xero/leviathan-crypto/blob/main/AGENTS.md) as the single source of authority.
83
-
84
- ---
85
-
86
- ## Cryptanalytic audits
87
-
88
- All primitives undergo periodic cryptographic implementation reviews. See the [audit index](https://github.com/xero/leviathan-crypto/wiki/audits) for a full summary.
89
-
90
- | Primitive | Audit description |
91
- | --- | --- |
92
- | [serpent_audit](https://github.com/xero/leviathan-crypto/wiki/serpent_audit) | Correctness verification, side-channel analysis, cryptanalytic attack paper review |
93
- | [chacha_audit](https://github.com/xero/leviathan-crypto/wiki/chacha_audit) | XChaCha20-Poly1305 correctness, Poly1305 field arithmetic, HChaCha20 nonce extension, post-auth-fail wipe hygiene |
94
- | [sha2_audit](https://github.com/xero/leviathan-crypto/wiki/sha2_audit) | SHA-256/512/384 correctness, HMAC and HKDF composition, constant verification |
95
- | [sha3_audit](https://github.com/xero/leviathan-crypto/wiki/sha3_audit) | Keccak permutation correctness, θ/ρ/π/χ/ι step verification, round constant derivation |
96
- | [hmac_audit](https://github.com/xero/leviathan-crypto/wiki/hmac_audit) | HMAC-SHA256/512/384 construction, key processing, RFC 4231 vector coverage |
97
- | [hkdf_audit](https://github.com/xero/leviathan-crypto/wiki/hkdf_audit) | HKDF extract-then-expand, info field domain separation, stream key derivation |
98
- | [kyber_audit](https://github.com/xero/leviathan-crypto/wiki/kyber_audit) | ML-KEM FIPS 203 correctness (§7.2/§7.3 direct coefficient-range validation), NTT/Montgomery/Barrett verification, FO transform CT analysis, per-op memory hygiene across keygen/encap/decap, ACVP validation |
99
- | [stream_audit](https://github.com/xero/leviathan-crypto/wiki/stream_audit) | Streaming AEAD composition, counter nonce binding, final-chunk detection, key wipe paths, `'failed'` terminal state |
100
- | [ratchet_audit](https://github.com/xero/leviathan-crypto/wiki/ratchet_audit) | SPQR KDF primitives: HKDF parameter assignments with full transcript binding (peerEk, kemCt, context), wipe coverage, counter encoding, direction slot alignment, transactional `ResolveHandle` DoS mitigation |
101
-
102
- ### Serpent-256 security margin research
103
-
104
- The security margin of Serpent-256 has been independently researched and documented. The best known attack on the full 32-round cipher, _biclique cryptanalysis_, achieves a complexity of 2²⁵⁵·¹⁹ with 2⁴ chosen ciphertexts. This provides less than one bit of advantage over exhaustive key search and has zero practical impact. Independent research conducted against this implementation improved on the published result by −0.20 bits through systematic parameter search, confirming no structural weakness beyond what the published literature describes.
105
-
106
- See: [xero/BicliqueFinder/biclique_research.md](https://github.com/xero/BicliqueFinder/blob/main/biclique-research.md)
107
-
108
- ---
109
-
110
- ## Supported versions
111
-
112
- Every fix is documented in the full [CHANGELOG](https://github.com/xero/leviathan-crypto/blob/main/CHANGELOG). Each version below links to the release notes documenting its fixes.
113
-
114
- | Version | Status | Summary |
115
- | --- | --- | --- |
116
- | [v2.1.x](https://github.com/xero/leviathan-crypto/blob/main/CHANGELOG#v2-1-0-XXXX-XX-XX) | ✓ supported | Adds SPQR post-quantum KEM ratchet primitives |
117
- | [v2.0.x](https://github.com/xero/leviathan-crypto/blob/main/CHANGELOG#v2-0-1-2026-04-10) | ✗ superseded | FIPS 203 key validation, per-op wipe hygiene, padding-oracle closure, and ratchet DoS mitigation. Upgrade to v2.1.x |
118
- | [v1.x](https://github.com/xero/leviathan-crypto/blob/main/CHANGELOG#v2-0-0-2026-04-10) | ✗ deprecated | Multiple partial-wipe and auth-handling issues. Upgrade to v2.1.x |
119
-
120
- > [!CAUTION]
121
- > v2.0.0 has a known silent-corruption bug. `SealStreamPool` with `SerpentCipher` silently produces corrupt plaintext with no authentication error on decrypt for inputs ≥ 65536 bytes. See [v2.0.1 release notes](https://github.com/xero/leviathan-crypto/blob/main/CHANGELOG#v2-0-1-2026-04-10) and update to the latest version immediately.
122
-
123
- ---
124
-
125
- ## Reporting a vulnerability
126
-
127
- > [!IMPORTANT]
128
- > Do not open a public issue for security vulnerabilities.
129
-
130
- ### Private advisory (preferred)
131
-
132
- Use GitHub's private vulnerability reporting form: [https://github.com/xero/leviathan-crypto/security/advisories/new](https://github.com/xero/leviathan-crypto/security/advisories/new)
133
-
134
- This opens a private channel between you and the maintainer, and you will receive a response promptly. If the vulnerability is confirmed, we collaborate to fully understand the issue, including a review of proposed fixes, so you can track and validate firsthand. Before any public advisory publishes, we agree on a coordinated disclosure timeline. After disclosure, you are encouraged to publish your own write-up, blog post, or research notes for full hacker scene credit.
135
-
136
- ### Direct contact
137
-
138
- If you prefer direct contact:
139
-
140
- - **Email:** x﹫xero.style · PGP: [0xAC1D0000](https://0w.nz/pgp.pub)
141
- - **Matrix:** x0﹫rx.haunted.computer
142
-
143
- > [!NOTE]
144
- > Encrypted communication is welcome and preferred for sensitive reports.
145
-
146
- ### In scope
147
-
148
- - Authentication bypass in AEAD constructions
149
- - Key material exposure or improper zeroing
150
- - Incorrect entropy or CSPRNG weaknesses in Fortuna
151
- - Side-channel vulnerabilities (timing, memory access patterns)
152
- - Correctness bugs in cryptographic implementations (wrong output against test vectors)
153
- - Platform-specific behavioral differences (WASM execution, binary output, or timing characteristics that differ across operating systems or CPU architectures)
154
- - Supply chain issues (dependency tampering, workflow compromise)
155
- - Improper scope of exported symbols
156
-
157
- ### Out of scope
158
-
159
- - Vulnerabilities in third-party packages not maintained by this project. This includes optional peer dependencies such as argon2id. Report those directly to their maintainers.
160
- - Issues requiring physical access to the user's device
161
- - Theoretical attacks with no practical exploit path (complexity improvements that remain computationally infeasible)
162
- - Issues in the demo applications that do not affect the core library. Open an issue in [leviathan-demos](https://github.com/xero/leviathan-demos/) instead.
163
-
package/dist/ct-wasm.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare const CT_WASM: Uint8Array<ArrayBuffer>;
package/dist/ct-wasm.js DELETED
@@ -1,3 +0,0 @@
1
- // auto-generated — do not edit
2
- // raw WASM bytes for constant-time comparison module
3
- export const CT_WASM = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 8, 1, 96, 3, 127, 127, 127, 1, 127, 3, 2, 1, 0, 5, 4, 1, 1, 1, 1, 7, 20, 2, 7, 99, 111, 109, 112, 97, 114, 101, 0, 0, 6, 109, 101, 109, 111, 114, 121, 2, 0, 10, 133, 1, 1, 130, 1, 3, 2, 127, 1, 126, 1, 123, 3, 64, 32, 3, 65, 16, 106, 34, 4, 32, 2, 76, 4, 64, 32, 6, 32, 0, 32, 3, 106, 253, 0, 4, 0, 32, 1, 32, 3, 106, 253, 0, 4, 0, 253, 81, 253, 80, 33, 6, 32, 4, 33, 3, 12, 1, 11, 11, 3, 64, 32, 2, 32, 3, 74, 4, 64, 32, 5, 32, 0, 32, 3, 106, 49, 0, 0, 32, 1, 32, 3, 106, 49, 0, 0, 133, 132, 33, 5, 32, 3, 65, 1, 106, 33, 3, 12, 1, 11, 11, 66, 0, 32, 5, 32, 6, 253, 29, 0, 32, 6, 253, 29, 1, 132, 132, 34, 5, 125, 32, 5, 132, 66, 63, 135, 66, 127, 133, 167, 65, 1, 113, 11]);
package/dist/docs/aead.md DELETED
@@ -1,363 +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
- ### Authenticated Encryption
4
-
5
- Cipher-agnostic authenticated encryption for any scale. One-shot with `Seal`, chunked with `SealStream` and `OpenStream`, or parallel with `SealStreamPool`. All four share a wire format and accept any `CipherSuite`.
6
-
7
- > ### Table of Contents
8
- > - [Overview](#overview)
9
- > - [Security Model](#security-model)
10
- > - [Wire Format](#wire-format)
11
- > - [API Reference](#api-reference)
12
-
13
- ---
14
-
15
- ## Overview
16
-
17
- Authenticated encryption in leviathan-crypto centers on four classes: `Seal`, `SealStream`, `OpenStream`, and `SealStreamPool`. All are cipher-agnostic. Pass a `CipherSuite` object at construction and they handle key derivation, nonce management, and authentication automatically.
18
-
19
- These four form a natural progression by use case. Use `Seal` for data that fits in memory. Use `SealStream` and `OpenStream` for data arriving in chunks or too large to buffer. Use `SealStreamPool` for parallel chunked encryption across Web Workers. All four share the same wire format, so `OpenStream` can decrypt a `Seal` blob and vice versa.
20
-
21
- leviathan-crypto includes two cipher suites. A third suite wraps either with ML-KEM for post-quantum hybrid encryption.
22
-
23
- | Suite | Cipher | Tag | Modules |
24
- |---|---|---|---|
25
- | `SerpentCipher` | Serpent-256 CBC + HMAC-SHA-256 | 32 B | `serpent`, `sha2` |
26
- | `XChaCha20Cipher` | XChaCha20-Poly1305 | 16 B | `chacha20`, `sha2` |
27
- | `KyberSuite` | ML-KEM + inner cipher | depends | `kyber`, `sha3`, + inner |
28
-
29
- See [ciphersuite.md](./ciphersuite.md) for full cipher suite documentation.
30
-
31
- ---
32
-
33
- ## Security Model
34
-
35
- The STREAM construction is based on [Hoang, Reyhanitabar, Rogaway, and Vizár (CRYPTO 2015)](https://eprint.iacr.org/2015/189.pdf). It provides online authenticated encryption with four guarantees.
36
-
37
- **Per-chunk authentication.** Each chunk carries its own authentication tag. The stream rejects a tampered chunk immediately and stops decrypting.
38
-
39
- **Counter binding.** Each chunk's nonce includes a monotonic counter. Reordering or duplicating chunks produces a counter mismatch and authentication fails.
40
-
41
- **Final-chunk detection.** The last chunk uses a distinct nonce flag (`TAG_FINAL` vs `TAG_DATA`). The opener expects a chunk marked final and rejects any stream that ends without one.
42
-
43
- **Stream isolation.** Each stream generates a fresh 16-byte random nonce on construction. Two streams with the same key derive independent subkeys via HKDF and cannot interfere with each other.
44
-
45
- > [!IMPORTANT]
46
- > `SealStream` is single-use. After `finalize()` is called the derived keys are wiped and no further chunks can be sealed. Create a new `SealStream` for each message. `SealStreamPool.seal()` enforces this with a guard that throws on subsequent calls.
47
- >
48
- > **`SealStream` / `OpenStream` have a three-state machine: `ready` → `finalized` | `failed`.** An auth failure, WASM error, or cipher exception inside `push()`, `pull()`, or `finalize()` wipes the derived keys and transitions the stream to `failed`. Subsequent method calls (`push`, `pull`, `finalize`, and `OpenStream.seek`) throw with `'failed'` in the message, never `'finalized'`. `dispose()` on a `failed` stream is a no-op. Construct a new stream to continue.
49
- >
50
- > **Argument-validation errors are non-terminal on both `SealStream` and `OpenStream`.** A `RangeError` from `push()` or `finalize()` for a chunk larger than `chunkSize` throws without wiping keys or entering `'failed'`. Symmetrically, a `RangeError` from `pull()` or `finalize()` throws without wiping keys when a chunk is too short to contain a tag, exceeds the maximum wire size, or (in framed mode) has a length prefix that does not match the payload length. The stream stays in `'ready'` and the caller can retry with a corrected chunk.
51
- >
52
- > This is safe because every validation error depends only on attacker-observable input lengths and never on secret-derived state. Distinguishing a validation throw from an auth failure gives an attacker no information they did not already have. Auth failures from `cipher.openChunk` remain terminal, as they are the crypto-path case.
53
- >
54
- > **`OpenStream.seek(index)` validates `index` before mutating state.** Indices that are not non-negative safe integers — `NaN`, `Infinity`, fractional, negative, or `> Number.MAX_SAFE_INTEGER` — throw `RangeError` without changing `counter`, so the caller can retry with a corrected index. The check uses `Number.isSafeInteger(index) && index >= 0` so values above `2^53 - 1` (where IEEE 754 doubles have integer gaps) are rejected directly rather than relying on a separate magnitude comparison. Backward seeks (`index < counter`) throw `'forward-only'` for the same reason (plaintext replay prevention). See `seek()` in the OpenStream API table.
55
- >
56
- > **AEAD `encrypt()` is strict single-use.** `ChaCha20Poly1305.encrypt()` and `XChaCha20Poly1305.encrypt()` are terminal on any throw, including key and nonce length validation. A retry on the same instance always raises the single-use guard, never a fresh length error. This tightens the 2.0-beta semantics where length validation was recoverable. Always allocate a new AEAD per message.
57
- >
58
- > **`SealStreamPool.seal()` is terminal on any throw.** Auth failures, worker crashes, job timeouts, output-size overflows (`RangeError` from assembling ciphertext that exceeds the runtime's typed-array max), or any other rejection kill the pool. Pending jobs reject, workers terminate, `_masterKey` and `_keys` are wiped, and subsequent calls throw `"pool is dead"`. Construct a new pool to continue. Any throw is terminal, which keeps the failure contract uniform with the strict single-use posture of `ChaCha20Poly1305.encrypt()`.
59
-
60
- ### WASM Side-Channel Posture
61
-
62
- All cryptographic computation runs in WASM outside the JavaScript JIT. Serpent's bitsliced S-box implementation and ChaCha20's quarter-round construction are both branchless and table-free, which eliminates data-dependent timing variation at the algorithm level. WASM lacks hardware-level constant-time guarantees, so this provides stronger posture than pure JavaScript but weaker than native constant-time code. If timing side channels are your primary threat model, a native cryptographic library with verified constant-time guarantees is more appropriate.
63
-
64
- ---
65
-
66
- ## Wire Format
67
-
68
- ### Header (20 bytes)
69
-
70
- Every stream begins with a 20-byte header:
71
-
72
- ```
73
- bytes:
74
- 0: compound enum (bit 7 = framed flag, bit 6 = reserved, bits 0-5 = format ID)
75
- 1-16: random nonce (16 bytes)
76
- 17-19: chunk size as u24 big-endian
77
- ```
78
-
79
- **Format IDs:** `0x01` = XChaCha20-Poly1305, `0x02` = Serpent-256. KEM suites encode both the parameter set and inner cipher in a single byte. See [ciphersuite.md](./ciphersuite.md#kybersuite) for the full format enum table.
80
-
81
- The 16-byte nonce is a HKDF salt, not a direct cipher nonce. `XChaCha20Cipher` passes it to HChaCha20 for subkey derivation. `SerpentCipher` uses it as the HKDF-SHA-256 salt to derive 96 bytes of enc/mac/iv key material.
82
-
83
- The framed flag (bit 7) prefixes each chunk with a `u32be` length. Use framed mode for flat byte streams where chunks are concatenated without an external framing layer. Leave it off when the transport provides its own message boundaries such as WebSocket frames or IPC messages.
84
-
85
- ### Counter Nonce (12 bytes)
86
-
87
- Each chunk is encrypted with a 12-byte nonce:
88
-
89
- ```
90
- bytes:
91
- 0-10: 11-byte big-endian counter (monotonically increasing)
92
- 11: final flag (0x00 = TAG_DATA, 0x01 = TAG_FINAL)
93
- ```
94
-
95
- The counter starts at 0 and increments with each chunk. The final chunk uses `TAG_FINAL` instead of `TAG_DATA`. A data chunk at counter N and a final chunk at counter N produce distinct nonces, so the construction never reuses a nonce.
96
-
97
- ### Key Derivation
98
-
99
- HKDF-SHA-256 derives cipher-specific key material from the master key and the random nonce at stream construction:
100
-
101
- | Cipher | HKDF info | Output | Structure |
102
- |---|---|---|---|
103
- | XChaCha20 | `xchacha20-sealstream-v2` | 32 B | HKDF → streamKey → HChaCha20 → subkey |
104
- | Serpent | `serpent-sealstream-v2` | 96 B | `enc_key[0:32] \| mac_key[32:64] \| iv_key[64:96]` |
105
-
106
- XChaCha20 performs an additional HChaCha20 subkey derivation step using the first 16 bytes of the nonce. The intermediate streamKey is wiped immediately after use.
107
-
108
- Serpent derives three keys: an encryption key for CBC, a MAC key for HMAC-SHA-256, and an IV key for per-chunk IV derivation via `HMAC-SHA-256(iv_key, counterNonce)[0:16]`. The CBC IV is derived deterministically on both sides and never transmitted.
109
-
110
- ---
111
-
112
- ## API Reference
113
-
114
- ### Seal
115
-
116
- `Seal` is a static class, never instantiated. It handles one-shot authenticated encryption and decryption. A `Seal` blob is structurally identical to a single-chunk `SealStream` output: `preamble || finalChunk(counter=0, TAG_FINAL)`. `OpenStream.finalize()` can open a `Seal` blob directly, and `Seal.decrypt()` can open a single-chunk `SealStream`.
117
-
118
- ```typescript
119
- import { init, Seal, XChaCha20Cipher } from 'leviathan-crypto'
120
- import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
121
- import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
122
-
123
- await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
124
-
125
- const key = XChaCha20Cipher.keygen()
126
- const blob = Seal.encrypt(XChaCha20Cipher, key, plaintext)
127
- const pt = Seal.decrypt(XChaCha20Cipher, key, blob) // throws AuthenticationError on tamper
128
- ```
129
-
130
- | Method | Returns | Description |
131
- |---|---|---|
132
- | `Seal.encrypt(suite, key, plaintext, opts?)` | `Uint8Array` | One-shot encrypt. Returns `preamble \|\| chunk`. |
133
- | `Seal.decrypt(suite, key, blob, opts?)` | `Uint8Array` | One-shot decrypt. Throws `AuthenticationError` on tamper. |
134
-
135
- **`opts.aad`.** Optional `Uint8Array` carrying Additional Authenticated Data. Authenticated but not encrypted. Pass the same value to both `encrypt` and `decrypt`.
136
-
137
- > [!NOTE]
138
- > **`chunkSize` in the wire header is a maximum, not an actual size.** For `Seal.encrypt` (single-chunk), the header always declares `max(plaintext.length, CHUNK_MIN)`, so a zero-byte seal still declares `chunkSize = CHUNK_MIN = 1024`. This is self-consistent on decode (the single final chunk is processed regardless of its actual length up to the declared bound) and prevents leaking the exact plaintext length through header analysis when `plaintext.length < CHUNK_MIN`. `SealStream` writes the configured `opts.chunkSize` verbatim; the receiver treats it as an upper bound on any incoming chunk's plaintext size.
139
-
140
- ---
141
-
142
- ### SealStream
143
-
144
- > [!NOTE]
145
- > All stream classes require `sha2` for HKDF key derivation. Load it alongside your cipher module before constructing any stream.
146
-
147
- ```typescript
148
- import { init, SealStream } from 'leviathan-crypto'
149
- import { XChaCha20Cipher } from 'leviathan-crypto/chacha20'
150
- import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
151
- import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
152
-
153
- await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
154
-
155
- const key = XChaCha20Cipher.keygen()
156
- const sealer = new SealStream(XChaCha20Cipher, key, { chunkSize: 65536 })
157
- const preamble = sealer.preamble // send first
158
-
159
- const ct0 = sealer.push(chunk0)
160
- const ct1 = sealer.push(chunk1)
161
- const ctLast = sealer.finalize(lastChunk) // keys wiped
162
- ```
163
-
164
- **Constructor:** `new SealStream(cipher, key, opts?)`
165
-
166
- | Parameter | Type | Description |
167
- |---|---|---|
168
- | `cipher` | `CipherSuite` | `XChaCha20Cipher`, `SerpentCipher`, or a `KyberSuite` instance. |
169
- | `key` | `Uint8Array` | Master key. Must be `cipher.keySize` bytes (32 for both symmetric suites). |
170
- | `opts.chunkSize` | `number` | Max plaintext bytes per chunk. Range: [1024, 16777215]. Default: 65536. |
171
- | `opts.framed` | `boolean` | Prepend `u32be` length prefix to each chunk. Default: false. |
172
-
173
- | Method | Returns | Description |
174
- |---|---|---|
175
- | `push(chunk, { aad? })` | `Uint8Array` | Encrypt a data chunk. Must be ≤ chunkSize bytes. |
176
- | `finalize(chunk, { aad? })` | `Uint8Array` | Encrypt the final chunk and wipe keys. Must be ≤ chunkSize bytes. |
177
- | `toTransformStream()` | `TransformStream` | Web Streams API wrapper. Emits preamble first, then sealed chunks. Finalizes on stream close. |
178
- | `preamble` | `Uint8Array` | The stream preamble (read-only). 20 bytes for symmetric suites. 20B header + KEM ciphertext for KEM suites. |
179
-
180
- ---
181
-
182
- ### OpenStream
183
-
184
- ```typescript
185
- import { OpenStream } from 'leviathan-crypto/stream'
186
- import { XChaCha20Cipher } from 'leviathan-crypto/chacha20'
187
- import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
188
- import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
189
-
190
- // init already called — preamble, key, and ciphertext chunks received from sender
191
- const opener = new OpenStream(XChaCha20Cipher, key, preamble)
192
-
193
- const pt0 = opener.pull(ct0)
194
- const pt1 = opener.pull(ct1)
195
- const ptLast = opener.finalize(ctLast) // keys wiped
196
- ```
197
-
198
- **Constructor:** `new OpenStream(cipher, key, preamble)`
199
-
200
- Throws if the preamble format enum doesn't match the cipher or if the preamble is too short.
201
-
202
- | Parameter | Type | Description |
203
- |---|---|---|
204
- | `cipher` | `CipherSuite` | Must match the cipher that produced the preamble. |
205
- | `key` | `Uint8Array` | Same master key used for sealing. |
206
- | `preamble` | `Uint8Array` | The preamble from `SealStream.preamble`. Pass it directly. |
207
-
208
- | Method | Returns | Description |
209
- |---|---|---|
210
- | `pull(chunk, { aad? })` | `Uint8Array` | Decrypt a data chunk. Throws `AuthenticationError` on tamper. |
211
- | `finalize(chunk, { aad? })` | `Uint8Array` | Decrypt the final chunk and wipe keys. |
212
- | `seek(index)` | `void` | Set the counter to `index`. The stream is forward-only; `index < counter` throws `RangeError` with `'forward-only'` in the message. `index` must satisfy `Number.isSafeInteger(index) && index >= 0` (i.e. a non-negative safe integer ≤ `Number.MAX_SAFE_INTEGER`). Argument-validation throws do not mutate `counter`; the stream stays usable and can retry with a corrected index. Throws on failed/finalized state (state guard fires before range check). |
213
- | `toTransformStream()` | `TransformStream` | Web Streams API wrapper. Buffers one chunk to detect the final chunk. |
214
-
215
- > [!IMPORTANT]
216
- > **`OpenStream.seek` is forward-only.** Backward seeks (`index < this.counter`) throw a `RangeError` with `'forward-only'` in the message. A backward seek would reuse an already-consumed per-chunk counter nonce against a new ciphertext, permitting plaintext replay against a stale opener. Construct a fresh `OpenStream` from the same preamble to restart from the beginning.
217
-
218
- ---
219
-
220
- ### SealStreamPool
221
-
222
- Parallel batch encryption and decryption using Web Workers. Each worker holds its own WASM instance and a copy of the derived keys.
223
-
224
- ```typescript
225
- import { init, SealStreamPool } from 'leviathan-crypto'
226
- import { XChaCha20Cipher } from 'leviathan-crypto/chacha20'
227
- import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
228
- import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
229
-
230
- await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
231
-
232
- const pool = await SealStreamPool.create(XChaCha20Cipher, key, {
233
- wasm: chacha20Wasm,
234
- workers: 4,
235
- chunkSize: 65536,
236
- })
237
-
238
- const ciphertext = await pool.seal(plaintext)
239
- const decrypted = await pool.open(ciphertext)
240
- pool.destroy()
241
- ```
242
-
243
- **`SealStreamPool.create(cipher, key, opts)`.** Async factory.
244
-
245
- | Option | Type | Default | Description |
246
- |---|---|---|---|
247
- | `wasm` | `WasmSource` or `Record<string, WasmSource>` | required | WASM source(s). Single value for XChaCha20. Record for Serpent: `{ serpent, sha2 }`. |
248
- | `workers` | `number` | `navigator.hardwareConcurrency` (4 if unset) | Worker count. |
249
- | `chunkSize` | `number` | `65536` | Chunk size in bytes. |
250
- | `framed` | `boolean` | `false` | Framed mode. |
251
- | `jobTimeout` | `number` | `30000` | Per-job timeout in ms. |
252
-
253
- > [!NOTE]
254
- > For padded ciphers (`SerpentCipher`), `create()` validates at startup that a full plaintext chunk fits in the WASM buffer after PKCS7 padding. If `chunkSize` is too large it throws a `RangeError` with the actual values before any workers are launched. The default `chunkSize: 65536` is valid for both built-in cipher suites.
255
-
256
- **Failure model.** Any error is fatal. Authentication failure, worker crash, and timeout all terminate every worker, wipe all keys, and mark the pool permanently dead. Pending promises reject. There is no retry and no worker replacement. Create a new pool for the next operation. `destroy()` is synchronous from the caller's perspective. The pool flips to `dead`, pending jobs reject, and main-thread keys are zeroed before the call returns. Worker teardown is bounded-async. The pool requests that each worker zero its in-memory key material and terminates workers after a short ACK window.
257
-
258
- | Method / Property | Description |
259
- |---|---|
260
- | `seal(plaintext)` | Encrypt. Returns `Promise<Uint8Array>`. Single-use. Throws on subsequent calls. |
261
- | `open(ciphertext)` | Decrypt. Returns `Promise<Uint8Array>`. Rejects empty ciphertext. |
262
- | `destroy()` | Wipes keys and terminates workers. Safe to call multiple times. |
263
- | `header` | The 20-byte stream header. `SealStreamPool` exposes `.header` while `SealStream` exposes `.preamble`, which also supports KEM preambles. |
264
- | `dead` | `true` after any fatal error or `destroy()`. |
265
- | `size` | Number of workers. |
266
-
267
- **Lifecycle.**
268
-
269
- - After `seal()` completes successfully, the pool holds the derived keys and
270
- master key in memory until you call `destroy()`. Call `destroy()` explicitly
271
- when you are finished; forgetting leaves key material resident until garbage
272
- collection.
273
- - After `seal()`, the pool is marked sealed and further `seal()` calls throw.
274
- But `open()` is still valid and can decrypt other ciphertexts using the same
275
- master key. This is intentional because a pool is a stateful encrypt/decrypt
276
- context tied to a master key, not a single-use seal operation. The word
277
- "sealed" can still mislead. If your usage is encrypt-once-then-discard, the
278
- idiom is `try { await pool.seal(pt) } finally { pool.destroy() }`.
279
- - On any job throw (worker crash, auth failure, timeout), the pool's
280
- `_killAll` runs. All workers terminate, all keys are wiped, and the pool is
281
- marked dead. Subsequent calls throw `'pool is dead'`.
282
-
283
- **Interop with `SealStream.push()`.** In unframed mode, `pool.open()` splits the body into chunks at fixed `chunkSize` boundaries. This works when the ciphertext came from `SealStreamPool.seal()` or from a `SealStream` that emitted every non-final chunk at exactly `chunkSize` plaintext bytes. A `SealStream` that called `push()` with sub-`chunkSize` chunks produces a valid blob that `OpenStream` can decrypt, but `pool.open()` cannot. The pool splits at the wrong boundary, stamps the wrong domain separator on the final chunk, and fails authentication. Use `framed: true` on both sides if producer and consumer may have different chunk shapes. Framed chunks carry a `u32be` length prefix that makes the split unambiguous.
284
-
285
- ---
286
-
287
- ### KyberSuite
288
-
289
- `KyberSuite` wraps an ML-KEM instance and an inner `CipherSuite` into a hybrid post-quantum construction. The result plugs into `Seal`, `SealStream`, `OpenStream`, and `SealStreamPool` identically to a symmetric suite.
290
-
291
- ```typescript
292
- import { init, SealStream, OpenStream } from 'leviathan-crypto'
293
- import { KyberSuite, MlKem768 } from 'leviathan-crypto/kyber'
294
- import { XChaCha20Cipher } from 'leviathan-crypto/chacha20'
295
- import { kyberWasm } from 'leviathan-crypto/kyber/embedded'
296
- import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
297
- import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
298
- import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
299
-
300
- await init({ kyber: kyberWasm, sha3: sha3Wasm, chacha20: chacha20Wasm, sha2: sha2Wasm })
301
-
302
- const suite = KyberSuite(new MlKem768(), XChaCha20Cipher)
303
- const { encapsulationKey: ek, decapsulationKey: dk } = suite.keygen()
304
-
305
- // sender — encrypts with the public key
306
- const sealer = new SealStream(suite, ek)
307
- const preamble = sealer.preamble // 1108 bytes for MlKem768
308
- const ct0 = sealer.push(chunk0)
309
- const ctLast = sealer.finalize(lastChunk)
310
-
311
- // recipient — decrypts with the private key
312
- const opener = new OpenStream(suite, dk, preamble)
313
- const pt0 = opener.pull(ct0)
314
- const ptLast = opener.finalize(ctLast)
315
- ```
316
-
317
- See [kyber.md](./kyber.md) for key management, parameter set selection, and the full ML-KEM reference. See [ciphersuite.md](./ciphersuite.md#kybersuite) for format enum values and key derivation details.
318
-
319
- ---
320
-
321
- ### Per-chunk AAD
322
-
323
- `push()` and `finalize()` on `SealStream` and `pull()` and `finalize()` on `OpenStream` all accept an optional `{ aad }` parameter for Additional Authenticated Data. AAD is authenticated but not encrypted. It binds each chunk to external context such as sequence numbers, metadata, or routing information without including that data in the ciphertext.
324
-
325
- AAD applies per chunk, not per stream. Each chunk can carry different AAD. If you sealed a chunk with AAD you must provide the same value when opening it. A mismatch causes authentication to fail.
326
-
327
- ---
328
-
329
- ### AuthenticationError
330
-
331
- `Seal.decrypt()`, `OpenStream.pull()`, `OpenStream.finalize()`, and `SealStreamPool.open()` throw `AuthenticationError` when authentication fails. It extends `Error` and carries the cipher name in the message.
332
-
333
- ```typescript
334
- import { AuthenticationError } from 'leviathan-crypto'
335
-
336
- try {
337
- const pt = Seal.decrypt(XChaCha20Cipher, key, tampered)
338
- } catch (e) {
339
- if (e instanceof AuthenticationError) {
340
- // ciphertext was modified
341
- }
342
- }
343
- ```
344
-
345
- Never attempt to recover plaintext after an `AuthenticationError`. The stream layer wipes output buffers before throwing.
346
-
347
- ---
348
-
349
-
350
- ## Cross-References
351
-
352
- | Document | Description |
353
- | -------- | ----------- |
354
- | [index](./README.md) | Project Documentation index |
355
- | [lexicon](./lexicon.md) | Glossary of cryptographic terms |
356
- | [architecture](./architecture.md) | architecture overview, module relationships, buffer layouts, and build pipeline |
357
- | [ciphersuite](./ciphersuite.md) | `SerpentCipher`, `XChaCha20Cipher`, `KyberSuite`, and the `CipherSuite` interface |
358
- | [kyber](./kyber.md) | ML-KEM key encapsulation, parameter sets, and key management |
359
- | [serpent](./serpent.md) | Serpent-256 raw primitives |
360
- | [chacha20](./chacha20.md) | ChaCha20 raw primitives |
361
- | [stream_audit](./stream_audit.md) | streaming AEAD composition audit |
362
- | [exports](./exports.md) | complete export reference |
363
- | [init](./init.md) | WASM loading and `WasmSource` |