leviathan-crypto 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CLAUDE.md +171 -7
  2. package/LICENSE +4 -0
  3. package/README.md +109 -54
  4. package/SECURITY.md +125 -238
  5. package/dist/chacha20/cipher-suite.d.ts +10 -0
  6. package/dist/chacha20/cipher-suite.js +65 -2
  7. package/dist/chacha20/generator.d.ts +12 -0
  8. package/dist/chacha20/generator.js +91 -0
  9. package/dist/chacha20/index.d.ts +97 -1
  10. package/dist/chacha20/index.js +139 -11
  11. package/dist/chacha20/ops.d.ts +57 -6
  12. package/dist/chacha20/ops.js +93 -13
  13. package/dist/chacha20/pool-worker.js +12 -0
  14. package/dist/chacha20/types.d.ts +1 -32
  15. package/dist/ct-wasm.js +1 -1
  16. package/dist/ct.wasm +0 -0
  17. package/dist/docs/aead.md +66 -26
  18. package/dist/docs/architecture.md +600 -521
  19. package/dist/docs/argon2id.md +17 -14
  20. package/dist/docs/chacha20.md +146 -39
  21. package/dist/docs/exports.md +46 -10
  22. package/dist/docs/fortuna.md +339 -122
  23. package/dist/docs/init.md +24 -25
  24. package/dist/docs/loader.md +142 -47
  25. package/dist/docs/serpent.md +139 -41
  26. package/dist/docs/sha2.md +77 -19
  27. package/dist/docs/sha3.md +81 -15
  28. package/dist/docs/types.md +155 -15
  29. package/dist/docs/utils.md +171 -81
  30. package/dist/embedded/chacha20-pool-worker.d.ts +1 -0
  31. package/dist/embedded/chacha20-pool-worker.js +5 -0
  32. package/dist/embedded/kyber.d.ts +1 -1
  33. package/dist/embedded/kyber.js +1 -1
  34. package/dist/embedded/serpent-pool-worker.d.ts +1 -0
  35. package/dist/embedded/serpent-pool-worker.js +5 -0
  36. package/dist/fortuna.d.ts +14 -8
  37. package/dist/fortuna.js +144 -50
  38. package/dist/index.d.ts +8 -6
  39. package/dist/index.js +6 -5
  40. package/dist/init.d.ts +0 -2
  41. package/dist/init.js +83 -3
  42. package/dist/kyber/indcpa.js +4 -4
  43. package/dist/kyber/index.js +25 -5
  44. package/dist/kyber/kem.js +56 -1
  45. package/dist/kyber/suite.d.ts +1 -2
  46. package/dist/kyber/types.d.ts +1 -0
  47. package/dist/kyber/validate.d.ts +8 -4
  48. package/dist/kyber/validate.js +18 -14
  49. package/dist/kyber.wasm +0 -0
  50. package/dist/loader.d.ts +7 -2
  51. package/dist/loader.js +25 -28
  52. package/dist/ratchet/index.d.ts +6 -0
  53. package/dist/ratchet/index.js +37 -0
  54. package/dist/ratchet/kdf-chain.d.ts +13 -0
  55. package/dist/ratchet/kdf-chain.js +85 -0
  56. package/dist/ratchet/ratchet-keypair.d.ts +9 -0
  57. package/dist/ratchet/ratchet-keypair.js +61 -0
  58. package/dist/ratchet/root-kdf.d.ts +4 -0
  59. package/dist/ratchet/root-kdf.js +124 -0
  60. package/dist/ratchet/skipped-key-store.d.ts +14 -0
  61. package/dist/ratchet/skipped-key-store.js +154 -0
  62. package/dist/ratchet/types.d.ts +36 -0
  63. package/dist/ratchet/types.js +26 -0
  64. package/dist/serpent/cipher-suite.d.ts +10 -0
  65. package/dist/serpent/cipher-suite.js +135 -50
  66. package/dist/serpent/generator.d.ts +12 -0
  67. package/dist/serpent/generator.js +97 -0
  68. package/dist/serpent/index.d.ts +61 -1
  69. package/dist/serpent/index.js +92 -7
  70. package/dist/serpent/pool-worker.js +25 -101
  71. package/dist/serpent/serpent-cbc.d.ts +14 -4
  72. package/dist/serpent/serpent-cbc.js +50 -32
  73. package/dist/serpent/shared-ops.d.ts +83 -0
  74. package/dist/serpent/shared-ops.js +213 -0
  75. package/dist/serpent/types.d.ts +1 -5
  76. package/dist/sha2/hash.d.ts +2 -0
  77. package/dist/sha2/hash.js +53 -0
  78. package/dist/sha2/index.d.ts +1 -0
  79. package/dist/sha2/index.js +15 -1
  80. package/dist/sha3/hash.d.ts +2 -0
  81. package/dist/sha3/hash.js +53 -0
  82. package/dist/sha3/index.d.ts +17 -2
  83. package/dist/sha3/index.js +79 -7
  84. package/dist/stream/header.js +5 -5
  85. package/dist/stream/open-stream.js +36 -14
  86. package/dist/stream/seal-stream-pool.d.ts +1 -0
  87. package/dist/stream/seal-stream-pool.js +38 -8
  88. package/dist/stream/seal-stream.js +29 -11
  89. package/dist/types.d.ts +21 -0
  90. package/dist/utils.d.ts +7 -8
  91. package/dist/utils.js +73 -40
  92. package/dist/wasm-source.d.ts +9 -8
  93. package/package.json +79 -64
package/SECURITY.md CHANGED
@@ -2,275 +2,162 @@
2
2
 
3
3
  <img src="https://github.com/xero/leviathan-crypto/raw/main/docs/logo.svg" alt="Leviathan logo" width="100" align="left">
4
4
 
5
- - **[Version Support](#supported-versions)**
6
5
  - **[Security Posture](#security-posture)**
7
6
  - **[Cryptanalytic Audits](#cryptanalytic-audits)**
7
+ - **[Supported Versions](#supported-versions)**
8
8
  - **[Vulnerability Reporting](#reporting-a-vulnerability)**
9
9
 
10
10
  ---
11
11
 
12
- ## Supported Versions
12
+ ## Security posture
13
13
 
14
- | Version | Supported |
15
- | ------- | --------- |
16
- | v2.0.x | ✓ |
17
- | v1.4.x | ✗ |
18
- | v1.3.x | ✗ |
19
- | v1.2.x | ✗ |
20
- | v1.1.x | ✗ |
21
- | v1.0.x | ✗ |
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 |
22
119
 
23
120
  > [!CAUTION]
24
- > **All v1.x releases are deprecated.** Upgrading to v2 is strongly
25
- > recommended. v1.x releases will not receive security patches.
26
-
27
- > [!WARNING]
28
- > **v1.x known issues** (addressed in v2):
29
- > - Partial WASM buffer wipe on AEAD and serpent auth failure.
30
- > - HMAC tag and HKDF operations do not zero intermediate key material.
31
- > - TransformStream error paths leak derived keys.
32
- > - Pool workers copy result buffers.
33
- > - Scalar JS `constantTimeEqual` was "best-effort only".
34
-
35
- > [!WARNING]
36
- > **v2.0.0 known issue** (addressed in v2.0.1):
37
- > - `SealStreamPool` with `SerpentCipher` and `chunkSize: 65536` silently
38
- > produces corrupt plaintext on decrypt for inputs >= 65536 bytes. No
39
- > authentication error is raised. Upgrade to v2.0.1 immediately.
40
-
41
- ## Security Posture
42
-
43
- [`leviathan-crypto`](https://leviathan.3xi.club) is a cryptography library.
44
- Security is not an afterthought, it is the primary design constraint at every
45
- layer of the stack.
46
-
47
- ### Algorithm Correctness
48
-
49
- Every primitive in this library was implemented by hand in AssemblyScript
50
- against the authoritative specification for that algorithm:
51
- [FIPS 180-4][fips180] (SHA-2), [FIPS 202][fips202] (SHA-3),
52
- [FIPS 203][fips203] (ML-KEM),
53
- [RFC 8439][rfc8439] (ChaCha20-Poly1305), [RFC 2104][rfc2104] (HMAC),
54
- [RFC 5869][rfc5869] (HKDF), and the original
55
- [Serpent-256 specification][serpent] and S-box reference. No algorithm was
56
- ported from an existing implementation. The specs are always the source of truth.
57
-
58
- All implementations are verified against published known-answer test vectors
59
- from NIST, RFC appendices, NESSIE, and the Argon2 reference suite. Vectors
60
- are immutable: if an implementation produces incorrect output, the
61
- implementation is fixed and vectors are never adjusted to match code.
62
-
63
- ### Side-Channel Resistance
64
-
65
- Serpent's S-boxes are implemented as Boolean gate circuits designed with no table
66
- lookups, no data-dependent memory access, and no data-dependent branches. Every
67
- bit is processed unconditionally on every block. This is the most
68
- timing-safe cipher implementation approach available in a WASM runtime,
69
- where JIT optimisation can otherwise introduce observable timing variation.
70
-
71
- All security-sensitive comparisons (e.g. MAC verification, padding validation)
72
- use [`constantTimeEqual`][utils], which is backed by a dedicated WASM SIMD module
73
- (v128 XOR accumulate + `any_true`) when WebAssembly SIMD is available. The WASM
74
- execution path eliminates JIT short-circuiting and speculative optimization as
75
- theoretical side-channel vectors. On runtimes without SIMD (sha2/sha3-only
76
- consumers), the function falls back to a JS XOR-accumulate loop. This is best-effort
77
- constant-time, not a hardware-level guarantee. WASM comparison memory is
78
- wiped after every call.
79
-
80
- ### WASM Execution Model
81
-
82
- All cryptographic computation runs in WebAssembly, isolated outside the
83
- JavaScript JIT. WASM execution is deterministic and not subject to JIT
84
- speculation or optimisation. Each primitive family compiles to its own
85
- isolated binary with its own linear memory. For example, key material in
86
- the Serpent module cannot interact with memory in the SHA-3 module,
87
- even in principle. A dedicated WASM module handles constant-time comparison
88
- with its own single-page memory that is wiped after every call.
89
-
90
- Serpent and ChaCha20 modules require WebAssembly SIMD (`v128` instructions).
91
- `init()` and `initModule()` perform a SIMD preflight check and throw a
92
- clear error on runtimes without support. SIMD has been a baseline feature
93
- of all major browsers and runtimes since 2021. SHA-2 and SHA-3 modules
94
- run on any WASM-capable runtime.
95
-
96
- The `kyber` module requires WebAssembly SIMD for NTT and polynomial
97
- arithmetic (`v128` instructions). The SIMD preflight check is applied on
98
- `init()` alongside serpent and chacha20. Its linear memory is independent
99
- from all other modules. The kyber module's constant-time path (FO transform
100
- decapsulation) uses dedicated `ct_verify` and `ct_cmov` functions implemented
101
- in the kyber WASM binary. The comparison never passes through JavaScript.
102
-
103
- ### Cryptanalytic Audits
104
-
105
- All primitives undergo periodic cryptographic implementation reviews. See the [audit index][audits] for a full summary.
106
-
107
- | Primitive | Audit Description |
108
- | ------------------------------ | -------------------------------------------------------------------------------------- |
109
- | [serpent_audit][serpent_audit] | Correctness verification, side-channel analysis, cryptanalytic attack paper review |
110
- | [chacha_audit][chacha_audit] | XChaCha20-Poly1305 correctness, Poly1305 field arithmetic, HChaCha20 nonce extension |
111
- | [sha2_audit][sha2_audit] | SHA-256/512/384 correctness, HMAC and HKDF composition, constant verification |
112
- | [sha3_audit][sha3_audit] | Keccak permutation correctness, θ/ρ/π/χ/ι step verification, round constant derivation |
113
- | [hmac_audit][hmac_audit] | HMAC-SHA256/512/384 construction, key processing, RFC 4231 vector coverage |
114
- | [hkdf_audit][hkdf_audit] | HKDF extract-then-expand, info field domain separation, stream key derivation |
115
- | [kyber_audit][kyber_audit] | ML-KEM FIPS 203 correctness, NTT/Montgomery/Barrett verification, FO transform CT analysis, ACVP validation |
116
- | [stream_audit][stream_audit] | Streaming AEAD composition, counter nonce binding, final-chunk detection, key wipe paths |
117
-
118
- #### Additional Serpent-256 research
119
-
120
- The security margin of Serpent-256 has been independently researched and
121
- documented. The best known attack on the full 32-round cipher, _"biclique
122
- cryptanalysis"_, achieves a complexity of 2²⁵⁵·¹⁹ with 2⁴ chosen
123
- ciphertexts. This provides less than one bit of advantage over exhaustive
124
- key search and has zero practical impact. Independent research conducted
125
- against this implementation improved on the published result by −0.20 bits
126
- through systematic parameter search, confirming no structural weakness
127
- beyond what the published literature describes.
128
-
129
- See: [`xero/BicliqueFinder/biclique_research.md`][biclique]
130
-
131
- ### Authenticated Encryption by Default
132
-
133
- Raw unauthenticated cipher modes (`SerpentCbc`, `SerpentCtr`, `ChaCha20`) and
134
- stateless caller-managed-nonce primitives (`ChaCha20Poly1305`,
135
- `XChaCha20Poly1305`) are exposed for power users but are not the recommended
136
- entry point. The primary API surfaces (`Seal`, `SealStream`, `OpenStream`,
137
- `SealStreamPool`, and `KyberSuite`) are authenticated by construction with
138
- internally managed nonces.
139
-
140
- **All streaming constructions satisfy the _Cryptographic Doom Principle_:**
141
-
142
- `SealStream` / `OpenStream` with `SerpentCipher` uses encrypt-then-MAC
143
- (SerpentCbc + HMAC-SHA256). MAC verification is the unconditional gate on
144
- the open path. Decryption is unreachable until that gate clears. HKDF key
145
- derivation with the stream nonce and counter-nonce domain separation
146
- extends this guarantee to full stream integrity.
147
-
148
- `SealStream` / `OpenStream` with `XChaCha20Cipher` uses XChaCha20-Poly1305
149
- AEAD per chunk. The Poly1305 tag is verified inside the WASM `aeadDecrypt`
150
- call before any plaintext is produced. On authentication failure, the full
151
- chunk output buffer is wiped and plaintext bytes are never returned.
152
- Counter nonces with TAG_DATA/TAG_FINAL final-flag domain separation ensure
153
- reorder, splice, truncation, and cross-stream substitution all fail AEAD
154
- verification before decryption.
155
-
156
- `SealStreamPool` delegates per-chunk AEAD to isolated Web Workers. Each
157
- worker holds its own derived subkey and WASM instance. Any authentication
158
- error kills all workers, wipes all key material, and marks the pool dead. No retry, no partial results.
159
-
160
- ### Dependency Management
161
-
162
- The library has **zero** runtime dependencies by design.
163
- `sideEffects: false` is enforced in `package.json`. Argon2id integration
164
- is documented as an _optional_ external dependency.
165
- See: [`leviathan-crypto/wiki/argon2id`][argon2id-wiki].
166
-
167
- Build toolchain dependencies are pinned with exact version locks in
168
- `bun.lock`. GitHub Actions workflows use [SHA-pinned action references][workflows]
169
- throughout with no floating tags. Supply chain integrity is treated as a
170
- first-class concern for a cryptography library.
171
-
172
- ### Explicit Initialisation
173
-
174
- No class silently auto-initialises. The [`init()`][init] gate is mandatory and
175
- explicit, giving consumers full control over when WASM modules are loaded
176
- and ensuring no hidden initialisation costs or race conditions. Classes
177
- throw immediately if used before initialisation rather than failing
178
- silently.
179
-
180
- ### Agentic Development Contracts
181
-
182
- All AI-assisted development on this repository operates under a strict
183
- agentic contract defined in [`AGENTS.md`][agents]. The contract enforces
184
- spec authority over planning documents, immutable test vectors, gate
185
- discipline before extending any test suite, independent algorithm
186
- derivation from published standards, and constant-time/wipe requirements
187
- for all security-sensitive code paths. Agents are explicitly prohibited
188
- from guessing cryptographic values or resolving spec ambiguities silently.
189
-
190
- The contract has been verified against Claude, GitHub Copilot (VS Code),
191
- OpenHands, Kilo Code, Cursor, Windsurf, and Aider. Configuration files for
192
- each are present in the repository and all route to [`AGENTS.md`][agents]
193
- as the single source of authority.
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.
194
122
 
195
123
  ---
196
124
 
197
- ## Reporting a Vulnerability
125
+ ## Reporting a vulnerability
198
126
 
199
127
  > [!IMPORTANT]
200
- > **_Please do not open a public issue for security vulnerabilities._**
128
+ > Do not open a public issue for security vulnerabilities.
201
129
 
202
- ### Private Advisory (preferred)
130
+ ### Private advisory (preferred)
203
131
 
204
- Use GitHub's private vulnerability reporting form:
205
- [https://github.com/xero/leviathan-crypto/security/advisories/new][advisory]
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)
206
133
 
207
- This opens a private channel between you and the maintainer, and you will
208
- receive a response promptly. If the vulnerability is confirmed,
209
- we will collaborate to fully understand the issue, including a review of
210
- proposed fixes, so you can track and validate firsthand. Before any public
211
- advisory is published, we will agree on a coordinated disclosure timeline.
212
- After disclosure, you are encouraged to publish your own write-up, blog post,
213
- or research notes, for full hacker scene credit.
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.
214
135
 
215
- ### Direct Contact
136
+ ### Direct contact
216
137
 
217
- If you prefer to contact the maintainer directly:
138
+ If you prefer direct contact:
218
139
 
219
- - **Email:** x﹫xero.style · PGP: [`0xAC1D0000`][pgp]
140
+ - **Email:** x﹫xero.style · PGP: [0xAC1D0000](https://0w.nz/pgp.pub)
220
141
  - **Matrix:** x0﹫rx.haunted.computer
221
142
 
222
143
  > [!NOTE]
223
- > Encrypted communication is welcome and _preferred_ for sensitive reports.
224
-
225
- ### Scope
144
+ > Encrypted communication is welcome and preferred for sensitive reports.
226
145
 
227
- **Reports are in scope for:**
146
+ ### In scope
228
147
 
229
148
  - Authentication bypass in AEAD constructions
230
149
  - Key material exposure or improper zeroing
231
150
  - Incorrect entropy or CSPRNG weaknesses in Fortuna
232
151
  - Side-channel vulnerabilities (timing, memory access patterns)
233
- - Correctness bugs in cryptographic implementations (wrong output against
234
- test vectors)
235
- - Platform-specific behavioral differences (WASM execution, binary output,
236
- or timing characteristics that differ across operating systems or CPU
237
- architectures)
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)
238
154
  - Supply chain issues (dependency tampering, workflow compromise)
239
155
  - Improper scope of exported symbols
240
156
 
241
- **Out of scope:**
157
+ ### Out of scope
242
158
 
243
- - Vulnerabilities in third-party packages not maintained by this project.
244
- This includes optional peer dependencies such as argon2id.
245
- Please report those directly to their maintainers.
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.
246
160
  - Issues requiring physical access to the user's device
247
- - Theoretical attacks with no practical exploit path (e.g. complexity
248
- improvements that remain computationally infeasible)
249
- - Issues in the demo applications that do not affect the core library.
250
- Please open an issue in the [`leviathan-demos`][demos] repository instead.
251
-
252
- [fips180]: https://csrc.nist.gov/publications/detail/fips/180/4/final
253
- [fips202]: https://csrc.nist.gov/publications/detail/fips/202/final
254
- [fips203]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.pdf
255
- [rfc8439]: https://www.rfc-editor.org/rfc/rfc8439
256
- [rfc2104]: https://www.rfc-editor.org/rfc/rfc2104
257
- [rfc5869]: https://www.rfc-editor.org/rfc/rfc5869
258
- [serpent]: https://www.cl.cam.ac.uk/~rja14/Papers/serpent.pdf
259
- [utils]: https://github.com/xero/leviathan-crypto/wiki/utils#constanttimeequal
260
- [demos]: https://github.com/xero/leviathan-demos/
261
- [serpent_audit]: https://github.com/xero/leviathan-crypto/wiki/serpent_audit
262
- [chacha_audit]: https://github.com/xero/leviathan-crypto/wiki/chacha_audit
263
- [sha2_audit]: https://github.com/xero/leviathan-crypto/wiki/sha2_audit
264
- [sha3_audit]: https://github.com/xero/leviathan-crypto/wiki/sha3_audit
265
- [hmac_audit]: https://github.com/xero/leviathan-crypto/wiki/hmac_audit
266
- [hkdf_audit]: https://github.com/xero/leviathan-crypto/wiki/hkdf_audit
267
- [kyber_audit]: https://github.com/xero/leviathan-crypto/wiki/kyber_audit
268
- [stream_audit]: https://github.com/xero/leviathan-crypto/wiki/stream_audit
269
- [audits]: https://github.com/xero/leviathan-crypto/wiki/audits
270
- [biclique]: https://github.com/xero/BicliqueFinder/blob/main/biclique-research.md
271
- [argon2id-wiki]: https://github.com/xero/leviathan-crypto/wiki/argon2id
272
- [workflows]: https://github.com/xero/leviathan-crypto/blob/main/scripts/pin-actions.ts
273
- [init]: https://github.com/xero/leviathan-crypto/wiki/init
274
- [agents]: https://github.com/xero/leviathan-crypto/blob/main/AGENTS.md
275
- [advisory]: https://github.com/xero/leviathan-crypto/security/advisories/new
276
- [pgp]: https://0w.nz/pgp.pub
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
+
@@ -1,4 +1,14 @@
1
1
  import type { CipherSuite } from '../stream/types.js';
2
+ /**
3
+ * `CipherSuite` implementation for the stream construction using XChaCha20-Poly1305.
4
+ *
5
+ * Each chunk is encrypted with ChaCha20-Poly1305 using a HChaCha20 subkey
6
+ * derived via HKDF-SHA-256 + HChaCha20. This is the recommended cipher suite
7
+ * for `SealStream` / `OpenStream` / `SealStreamPool`.
8
+ *
9
+ * Pass to `SealStream` / `OpenStream` / `SealStreamPool` instead of constructing
10
+ * this object directly. Use `XChaCha20Cipher.keygen()` to generate a 32-byte key.
11
+ */
2
12
  export declare const XChaCha20Cipher: CipherSuite & {
3
13
  keygen(): Uint8Array;
4
14
  };
@@ -23,14 +23,26 @@
23
23
  //
24
24
  // XChaCha20Cipher — CipherSuite implementation for the STREAM construction.
25
25
  // HKDF-SHA-256 key derivation → HChaCha20 subkey → ChaCha20-Poly1305 per chunk.
26
- import { getInstance } from '../init.js';
26
+ import { getInstance, _assertNotOwned } from '../init.js';
27
27
  import { HKDF_SHA256 } from '../sha2/index.js';
28
28
  import { aeadEncrypt, aeadDecrypt, deriveSubkey } from './ops.js';
29
29
  import { wipe, randomBytes } from '../utils.js';
30
+ import { WORKER_SOURCE } from '../embedded/chacha20-pool-worker.js';
30
31
  const INFO = new TextEncoder().encode('xchacha20-sealstream-v2');
32
+ /** Returns the raw chacha20 WASM export object. @internal */
31
33
  function getExports() {
32
34
  return getInstance('chacha20').exports;
33
35
  }
36
+ /**
37
+ * `CipherSuite` implementation for the stream construction using XChaCha20-Poly1305.
38
+ *
39
+ * Each chunk is encrypted with ChaCha20-Poly1305 using a HChaCha20 subkey
40
+ * derived via HKDF-SHA-256 + HChaCha20. This is the recommended cipher suite
41
+ * for `SealStream` / `OpenStream` / `SealStreamPool`.
42
+ *
43
+ * Pass to `SealStream` / `OpenStream` / `SealStreamPool` instead of constructing
44
+ * this object directly. Use `XChaCha20Cipher.keygen()` to generate a 32-byte key.
45
+ */
34
46
  export const XChaCha20Cipher = {
35
47
  formatEnum: 0x01,
36
48
  formatName: 'xchacha20',
@@ -41,10 +53,19 @@ export const XChaCha20Cipher = {
41
53
  padded: false,
42
54
  wasmChunkSize: 65536, // src/asm/chacha20/buffers.ts CHUNK_SIZE
43
55
  wasmModules: ['chacha20'],
56
+ /** Generate a random 32-byte master key suitable for use with `XChaCha20Cipher`. @returns 32 cryptographically random bytes */
44
57
  keygen() {
45
58
  return randomBytes(32);
46
59
  },
60
+ /**
61
+ * Derive a 32-byte HChaCha20 subkey from `masterKey` and `nonce` via
62
+ * HKDF-SHA-256 followed by HChaCha20 subkey derivation.
63
+ * @param masterKey 32-byte master key
64
+ * @param nonce Stream nonce (24 bytes minimum for XChaCha20 subkey derivation)
65
+ * @returns `DerivedKeys` holding the 32-byte subkey
66
+ */
47
67
  deriveKeys(masterKey, nonce, _kemCt) {
68
+ _assertNotOwned('chacha20');
48
69
  const hkdf = new HKDF_SHA256();
49
70
  const streamKey = hkdf.derive(masterKey, nonce, INFO, 32);
50
71
  hkdf.dispose();
@@ -54,7 +75,17 @@ export const XChaCha20Cipher = {
54
75
  wipe(streamKey);
55
76
  return { bytes: subkey };
56
77
  },
78
+ /**
79
+ * Encrypt and authenticate one stream chunk with ChaCha20-Poly1305.
80
+ * Output: ciphertext || 16-byte Poly1305 tag.
81
+ * @param keys Derived keys from `deriveKeys`
82
+ * @param counterNonce 12-byte per-chunk nonce (unique per chunk in the stream)
83
+ * @param chunk Plaintext chunk
84
+ * @param aad Optional additional authenticated data
85
+ * @returns Authenticated ciphertext
86
+ */
57
87
  sealChunk(keys, counterNonce, chunk, aad) {
88
+ _assertNotOwned('chacha20');
58
89
  const x = getExports();
59
90
  const { ciphertext, tag } = aeadEncrypt(x, keys.bytes, counterNonce, chunk, aad ?? new Uint8Array(0));
60
91
  const out = new Uint8Array(ciphertext.length + 16);
@@ -62,7 +93,16 @@ export const XChaCha20Cipher = {
62
93
  out.set(tag, ciphertext.length);
63
94
  return out;
64
95
  },
96
+ /**
97
+ * Verify and decrypt one stream chunk. Throws `AuthenticationError` on tag mismatch.
98
+ * @param keys Derived keys from `deriveKeys`
99
+ * @param counterNonce 12-byte per-chunk nonce — must match the value used by `sealChunk`
100
+ * @param chunk Ciphertext || 16-byte Poly1305 tag
101
+ * @param aad Optional additional authenticated data
102
+ * @returns Plaintext
103
+ */
65
104
  openChunk(keys, counterNonce, chunk, aad) {
105
+ _assertNotOwned('chacha20');
66
106
  if (chunk.length < 16)
67
107
  throw new RangeError(`chunk too short for 16-byte tag (got ${chunk.length})`);
68
108
  const x = getExports();
@@ -70,10 +110,33 @@ export const XChaCha20Cipher = {
70
110
  const tag = chunk.subarray(chunk.length - 16);
71
111
  return aeadDecrypt(x, keys.bytes, counterNonce, ct, tag, aad ?? new Uint8Array(0), 'xchacha20-poly1305');
72
112
  },
113
+ /**
114
+ * Zero all derived key material in `keys`. Called by the stream layer on
115
+ * teardown and after auth failure.
116
+ * @param keys Derived keys to wipe
117
+ */
73
118
  wipeKeys(keys) {
74
119
  wipe(keys.bytes);
75
120
  },
121
+ /**
122
+ * Spawn an XChaCha20 pool worker from the embedded IIFE bundle.
123
+ * The worker holds its own chacha20 WASM instance and derived subkey.
124
+ * @returns Newly constructed `Worker` instance
125
+ */
76
126
  createPoolWorker() {
77
- return new Worker(new URL('./pool-worker.js', import.meta.url), { type: 'module' });
127
+ // IIFE source is bundled at lib build time (scripts/embed-workers.ts).
128
+ // Avoids the syntactic `new Worker(new URL(..., import.meta.url))`
129
+ // pattern that triggers eager worker-chunk emission in Vite's
130
+ // transform hook (issue.md). Classic worker via blob URL —
131
+ // module workers fail on file:// in Chromium (issue2.md).
132
+ const blob = new Blob([WORKER_SOURCE], { type: 'application/javascript' });
133
+ const url = URL.createObjectURL(blob);
134
+ const w = new Worker(url);
135
+ // Worker spec fetches the URL synchronously at construction. Revoke
136
+ // in a macrotask so the spawn completes first; releases the Blob
137
+ // (~5 KB per spawn × N workers) instead of leaking it for the
138
+ // document's lifetime.
139
+ setTimeout(() => URL.revokeObjectURL(url), 0);
140
+ return w;
78
141
  },
79
142
  };
@@ -0,0 +1,12 @@
1
+ import type { Generator } from '../types.js';
2
+ /**
3
+ * ChaCha20 counter-mode PRF for Fortuna's generator slot.
4
+ *
5
+ * The 32-bit counter is read from `counter` as a little-endian u32. Each call
6
+ * to `generate()` encrypts zero blocks to produce keystream output (RFC 8439 §2.3).
7
+ * The nonce is fixed to zero — Fortuna's counter is the only varying input.
8
+ *
9
+ * Pass to `Fortuna.create({ generator: ChaCha20Generator, ... })` — do not
10
+ * call `generate()` directly outside of Fortuna.
11
+ */
12
+ export declare const ChaCha20Generator: Generator;
@@ -0,0 +1,91 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // src/ts/chacha20/generator.ts
23
+ //
24
+ // RFC 8439 §2.3 — ChaCha20 block function as PRF
25
+ // ChaCha20 counter-mode PRF for Fortuna's generator slot.
26
+ //
27
+ // Counter is treated as a 32-bit LE integer. Fortuna's 4-byte genCnt provides
28
+ // 2^32 blocks × 64 bytes = 256 GiB of output between reseeds.
29
+ import { _assertNotOwned, getInstance } from '../init.js';
30
+ /**
31
+ * ChaCha20 counter-mode PRF for Fortuna's generator slot.
32
+ *
33
+ * The 32-bit counter is read from `counter` as a little-endian u32. Each call
34
+ * to `generate()` encrypts zero blocks to produce keystream output (RFC 8439 §2.3).
35
+ * The nonce is fixed to zero — Fortuna's counter is the only varying input.
36
+ *
37
+ * Pass to `Fortuna.create({ generator: ChaCha20Generator, ... })` — do not
38
+ * call `generate()` directly outside of Fortuna.
39
+ */
40
+ export const ChaCha20Generator = {
41
+ keySize: 32,
42
+ blockSize: 64,
43
+ counterSize: 4,
44
+ wasmModules: ['chacha20'],
45
+ /**
46
+ * Generate `n` pseudorandom bytes using ChaCha20 with a zero nonce.
47
+ * @param key 32-byte ChaCha20 key
48
+ * @param counter 4-byte counter (little-endian u32)
49
+ * @param n Number of bytes to generate (0 ≤ n ≤ 2^30)
50
+ * @returns `n` pseudorandom bytes
51
+ */
52
+ generate(key, counter, n) {
53
+ _assertNotOwned('chacha20');
54
+ if (key.length !== 32)
55
+ throw new RangeError(`ChaCha20Generator: key must be 32 bytes (got ${key.length})`);
56
+ if (counter.length !== 4)
57
+ throw new RangeError(`ChaCha20Generator: counter must be 4 bytes (got ${counter.length})`);
58
+ if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 30)
59
+ throw new RangeError(`ChaCha20Generator: n must be a non-negative safe integer <= 2^30 (got ${n})`);
60
+ const x = getInstance('chacha20').exports;
61
+ const c = new DataView(counter.buffer, counter.byteOffset, 4).getUint32(0, true);
62
+ const mem = new Uint8Array(x.memory.buffer);
63
+ try {
64
+ mem.set(key, x.getKeyOffset());
65
+ // Fixed zero nonce — Fortuna's counter is the only varying input
66
+ mem.fill(0, x.getChachaNonceOffset(), x.getChachaNonceOffset() + 12);
67
+ x.chachaSetCounter(c);
68
+ x.chachaLoadKey();
69
+ const output = new Uint8Array(n);
70
+ let remaining = n;
71
+ let outPos = 0;
72
+ const maxChunk = x.getChunkSize();
73
+ const ptOff = x.getChunkPtOffset();
74
+ const ctOff = x.getChunkCtOffset();
75
+ while (remaining > 0) {
76
+ const chunkLen = Math.min(remaining, maxChunk);
77
+ mem.fill(0, ptOff, ptOff + chunkLen);
78
+ x.chachaEncryptChunk_simd(chunkLen);
79
+ output.set(mem.subarray(ctOff, ctOff + chunkLen), outPos);
80
+ outPos += chunkLen;
81
+ remaining -= chunkLen;
82
+ }
83
+ return output;
84
+ }
85
+ finally {
86
+ // Wipe the WASM key/state/keystream scratch so secret-derived bytes
87
+ // do not outlive this call in the shared chacha20 linear memory.
88
+ x.wipeBuffers();
89
+ }
90
+ },
91
+ };