leviathan-crypto 1.4.0 → 2.0.1

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 (122) hide show
  1. package/CLAUDE.md +129 -94
  2. package/README.md +166 -223
  3. package/SECURITY.md +90 -45
  4. package/dist/chacha20/cipher-suite.d.ts +4 -0
  5. package/dist/chacha20/cipher-suite.js +79 -0
  6. package/dist/chacha20/embedded.d.ts +1 -0
  7. package/dist/chacha20/embedded.js +27 -0
  8. package/dist/chacha20/index.d.ts +20 -27
  9. package/dist/chacha20/index.js +40 -59
  10. package/dist/chacha20/ops.d.ts +1 -1
  11. package/dist/chacha20/ops.js +19 -18
  12. package/dist/chacha20/pool-worker.js +77 -0
  13. package/dist/ct-wasm.d.ts +1 -0
  14. package/dist/ct-wasm.js +3 -0
  15. package/dist/ct.wasm +0 -0
  16. package/dist/docs/aead.md +323 -0
  17. package/dist/docs/architecture.md +427 -292
  18. package/dist/docs/argon2id.md +42 -30
  19. package/dist/docs/chacha20.md +192 -266
  20. package/dist/docs/exports.md +241 -0
  21. package/dist/docs/fortuna.md +60 -69
  22. package/dist/docs/init.md +172 -178
  23. package/dist/docs/loader.md +87 -142
  24. package/dist/docs/serpent.md +134 -583
  25. package/dist/docs/sha2.md +91 -103
  26. package/dist/docs/sha3.md +70 -36
  27. package/dist/docs/types.md +94 -16
  28. package/dist/docs/utils.md +109 -32
  29. package/dist/embedded/kyber.d.ts +1 -0
  30. package/dist/embedded/kyber.js +3 -0
  31. package/dist/embedded/serpent.d.ts +1 -1
  32. package/dist/embedded/serpent.js +1 -1
  33. package/dist/errors.d.ts +10 -0
  34. package/dist/errors.js +38 -0
  35. package/dist/fortuna.d.ts +0 -6
  36. package/dist/fortuna.js +5 -5
  37. package/dist/index.d.ts +25 -9
  38. package/dist/index.js +36 -7
  39. package/dist/init.d.ts +3 -7
  40. package/dist/init.js +18 -35
  41. package/dist/keccak/embedded.d.ts +1 -0
  42. package/dist/keccak/embedded.js +27 -0
  43. package/dist/keccak/index.d.ts +4 -0
  44. package/dist/keccak/index.js +31 -0
  45. package/dist/kyber/embedded.d.ts +1 -0
  46. package/dist/kyber/embedded.js +27 -0
  47. package/dist/kyber/indcpa.d.ts +49 -0
  48. package/dist/kyber/indcpa.js +352 -0
  49. package/dist/kyber/index.d.ts +38 -0
  50. package/dist/kyber/index.js +150 -0
  51. package/dist/kyber/kem.d.ts +21 -0
  52. package/dist/kyber/kem.js +160 -0
  53. package/dist/kyber/params.d.ts +14 -0
  54. package/dist/kyber/params.js +37 -0
  55. package/dist/kyber/suite.d.ts +13 -0
  56. package/dist/kyber/suite.js +94 -0
  57. package/dist/kyber/types.d.ts +98 -0
  58. package/dist/kyber/types.js +25 -0
  59. package/dist/kyber/validate.d.ts +19 -0
  60. package/dist/kyber/validate.js +68 -0
  61. package/dist/kyber.wasm +0 -0
  62. package/dist/loader.d.ts +15 -6
  63. package/dist/loader.js +65 -21
  64. package/dist/serpent/cipher-suite.d.ts +4 -0
  65. package/dist/serpent/cipher-suite.js +122 -0
  66. package/dist/serpent/embedded.d.ts +1 -0
  67. package/dist/serpent/embedded.js +27 -0
  68. package/dist/serpent/index.d.ts +6 -37
  69. package/dist/serpent/index.js +9 -118
  70. package/dist/serpent/pool-worker.d.ts +1 -0
  71. package/dist/serpent/pool-worker.js +208 -0
  72. package/dist/serpent/serpent-cbc.d.ts +30 -0
  73. package/dist/serpent/serpent-cbc.js +142 -0
  74. package/dist/serpent.wasm +0 -0
  75. package/dist/sha2/embedded.d.ts +1 -0
  76. package/dist/sha2/embedded.js +27 -0
  77. package/dist/sha2/hkdf.js +6 -2
  78. package/dist/sha2/index.d.ts +3 -2
  79. package/dist/sha2/index.js +3 -4
  80. package/dist/sha3/embedded.d.ts +1 -0
  81. package/dist/sha3/embedded.js +27 -0
  82. package/dist/sha3/index.d.ts +3 -2
  83. package/dist/sha3/index.js +3 -4
  84. package/dist/stream/constants.d.ts +6 -0
  85. package/dist/stream/constants.js +30 -0
  86. package/dist/stream/header.d.ts +9 -0
  87. package/dist/stream/header.js +77 -0
  88. package/dist/stream/index.d.ts +7 -0
  89. package/dist/stream/index.js +27 -0
  90. package/dist/stream/open-stream.d.ts +21 -0
  91. package/dist/stream/open-stream.js +146 -0
  92. package/dist/stream/seal-stream-pool.d.ts +38 -0
  93. package/dist/stream/seal-stream-pool.js +400 -0
  94. package/dist/stream/seal-stream.d.ts +20 -0
  95. package/dist/stream/seal-stream.js +142 -0
  96. package/dist/stream/seal.d.ts +9 -0
  97. package/dist/stream/seal.js +75 -0
  98. package/dist/stream/types.d.ts +25 -0
  99. package/dist/stream/types.js +26 -0
  100. package/dist/utils.d.ts +7 -2
  101. package/dist/utils.js +49 -3
  102. package/dist/wasm-source.d.ts +12 -0
  103. package/dist/wasm-source.js +26 -0
  104. package/package.json +13 -5
  105. package/dist/chacha20/pool.d.ts +0 -52
  106. package/dist/chacha20/pool.js +0 -178
  107. package/dist/chacha20/pool.worker.js +0 -37
  108. package/dist/chacha20/stream-sealer.d.ts +0 -49
  109. package/dist/chacha20/stream-sealer.js +0 -327
  110. package/dist/docs/chacha20_pool.md +0 -309
  111. package/dist/docs/wasm.md +0 -194
  112. package/dist/serpent/seal.d.ts +0 -8
  113. package/dist/serpent/seal.js +0 -72
  114. package/dist/serpent/stream-pool.d.ts +0 -48
  115. package/dist/serpent/stream-pool.js +0 -275
  116. package/dist/serpent/stream-sealer.d.ts +0 -55
  117. package/dist/serpent/stream-sealer.js +0 -342
  118. package/dist/serpent/stream.d.ts +0 -28
  119. package/dist/serpent/stream.js +0 -205
  120. package/dist/serpent/stream.worker.d.ts +0 -32
  121. package/dist/serpent/stream.worker.js +0 -117
  122. /package/dist/chacha20/{pool.worker.d.ts → pool-worker.d.ts} +0 -0
package/SECURITY.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  - **[Version Support](#supported-versions)**
6
6
  - **[Security Posture](#security-posture)**
7
- - **[Cryptanalytic Audits](#cryptanalytic-reviews)**
7
+ - **[Cryptanalytic Audits](#cryptanalytic-audits)**
8
8
  - **[Vulnerability Reporting](#reporting-a-vulnerability)**
9
9
 
10
10
  ---
@@ -12,27 +12,44 @@
12
12
  ## Supported Versions
13
13
 
14
14
  | Version | Supported |
15
- |---------|-----------|
16
- | v1.4.x | ✓ |
17
- | v1.3.x | |
15
+ | ------- | --------- |
16
+ | v2.0.x | ✓ |
17
+ | v1.4.x | |
18
+ | v1.3.x | ✗ |
18
19
  | v1.2.x | ✗ |
19
20
  | v1.1.x | ✗ |
20
21
  | v1.0.x | ✗ |
21
22
 
23
+ > [!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
+
22
35
  > [!WARNING]
23
- > v1.0.x does not zero intermediate key material in HMAC and HKDF operations.
24
- > Upgrading to v1.1.0 or later is strongly recommended.
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.
25
40
 
26
41
  ## Security Posture
27
42
 
28
- [`leviathan-crypto`](https://leviathan.3xi.club) is a cryptography library. Security is not an afterthought,
29
- it is the primary design constraint at every layer of the stack.
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.
30
46
 
31
47
  ### Algorithm Correctness
32
48
 
33
49
  Every primitive in this library was implemented by hand in AssemblyScript
34
50
  against the authoritative specification for that algorithm:
35
51
  [FIPS 180-4][fips180] (SHA-2), [FIPS 202][fips202] (SHA-3),
52
+ [FIPS 203][fips203] (ML-KEM),
36
53
  [RFC 8439][rfc8439] (ChaCha20-Poly1305), [RFC 2104][rfc2104] (HMAC),
37
54
  [RFC 5869][rfc5869] (HKDF), and the original
38
55
  [Serpent-256 specification][serpent] and S-box reference. No algorithm was
@@ -52,9 +69,13 @@ timing-safe cipher implementation approach available in a WASM runtime,
52
69
  where JIT optimisation can otherwise introduce observable timing variation.
53
70
 
54
71
  All security-sensitive comparisons (e.g. MAC verification, padding validation)
55
- use XOR-accumulate patterns with no early return on mismatch.
56
- [`constantTimeEqual`][utils] is the mandated comparison function throughout
57
- the library and its [demos][demos].
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.
58
79
 
59
80
  ### WASM Execution Model
60
81
 
@@ -63,20 +84,36 @@ JavaScript JIT. WASM execution is deterministic and not subject to JIT
63
84
  speculation or optimisation. Each primitive family compiles to its own
64
85
  isolated binary with its own linear memory. For example, key material in
65
86
  the Serpent module cannot interact with memory in the SHA-3 module,
66
- even in principle.
67
-
68
- ### Cryptanalytic Reviews
69
-
70
- All of our primitives undergo periodic cryptographic implementation reviews.
71
-
72
- | Primitive | Audit Description |
73
- |-----------|-------------------|
74
- | [serpent_audit][serpent_audit] | Correctness verification, side-channel analysis, cryptanalytic attack paper review |
75
- | [chacha_audit][chacha_audit] | XChaCha20-Poly1305 correctness, Poly1305 field arithmetic, HChaCha20 nonce extension |
76
- | [sha2_audit][sha2_audit] | SHA-256/512/384 correctness, HMAC and HKDF composition, constant verification |
77
- | [sha3_audit][sha3_audit] | Keccak permutation correctness, θ/ρ/π/χ/ι step verification, round constant derivation |
78
- | [hmac_audit][hmac_audit] | HMAC-SHA256/512/384 construction, key processing, RFC 4231 vector coverage |
79
- | [hkdf_audit][hkdf_audit] | HKDF extract-then-expand, info field domain separation, SerpentStream key derivation |
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 |
80
117
 
81
118
  #### Additional Serpent-256 research
82
119
 
@@ -96,25 +133,29 @@ See: [`xero/BicliqueFinder/biclique_research.md`][biclique]
96
133
  Raw unauthenticated cipher modes (`SerpentCbc`, `SerpentCtr`, `ChaCha20`) and
97
134
  stateless caller-managed-nonce primitives (`ChaCha20Poly1305`,
98
135
  `XChaCha20Poly1305`) are exposed for power users but are not the recommended
99
- entry point. The primary API surfaces `SerpentSeal`, `SerpentStream`,
100
- `SerpentStreamSealer`, `XChaCha20Seal`, and `XChaCha20StreamSealer` are
101
- authenticated by construction with internally managed nonces.
102
-
103
- **Both streaming constructions satisfy the _Cryptographic Doom Principle_:**
104
-
105
- `SerpentStreamSealer` uses encrypt-then-MAC (SerpentCbc + HMAC-SHA256).
106
- MAC verification is the unconditional gate on the open path.
107
- Decryption is unreachable until that gate clears. Per-chunk
108
- HKDF key derivation with position-bound info extends this
109
- guarantee to full stream integrity.
110
-
111
- `XChaCha20StreamSealer` uses XChaCha20-Poly1305 AEAD per chunk.
112
- The Poly1305 tag is verified inside the WASM `xcDecrypt` call
113
- before any plaintext is produced. On authentication failure,
114
- plaintext bytes are never generated and never returned. Stream-level
115
- binding via per-chunk AAD (`stream_id || index || isLast || user AAD`)
116
- ensures reorder, splice, truncation, and cross-stream substitution
117
- all fail AEAD verification before decryption.
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.
118
159
 
119
160
  ### Dependency Management
120
161
 
@@ -175,7 +216,7 @@ or research notes, for full hacker scene credit.
175
216
 
176
217
  If you prefer to contact the maintainer directly:
177
218
 
178
- - **Email:** x﹫xero.style PGP: [`0xAC1D0000`][pgp]
219
+ - **Email:** x﹫xero.style · PGP: [`0xAC1D0000`][pgp]
179
220
  - **Matrix:** x0﹫rx.haunted.computer
180
221
 
181
222
  > [!NOTE]
@@ -210,6 +251,7 @@ If you prefer to contact the maintainer directly:
210
251
 
211
252
  [fips180]: https://csrc.nist.gov/publications/detail/fips/180/4/final
212
253
  [fips202]: https://csrc.nist.gov/publications/detail/fips/202/final
254
+ [fips203]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.pdf
213
255
  [rfc8439]: https://www.rfc-editor.org/rfc/rfc8439
214
256
  [rfc2104]: https://www.rfc-editor.org/rfc/rfc2104
215
257
  [rfc5869]: https://www.rfc-editor.org/rfc/rfc5869
@@ -222,6 +264,9 @@ If you prefer to contact the maintainer directly:
222
264
  [sha3_audit]: https://github.com/xero/leviathan-crypto/wiki/sha3_audit
223
265
  [hmac_audit]: https://github.com/xero/leviathan-crypto/wiki/hmac_audit
224
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
225
270
  [biclique]: https://github.com/xero/BicliqueFinder/blob/main/biclique-research.md
226
271
  [argon2id-wiki]: https://github.com/xero/leviathan-crypto/wiki/argon2id
227
272
  [workflows]: https://github.com/xero/leviathan-crypto/blob/main/scripts/pin-actions.ts
@@ -0,0 +1,4 @@
1
+ import type { CipherSuite } from '../stream/types.js';
2
+ export declare const XChaCha20Cipher: CipherSuite & {
3
+ keygen(): Uint8Array;
4
+ };
@@ -0,0 +1,79 @@
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/cipher-suite.ts
23
+ //
24
+ // XChaCha20Cipher — CipherSuite implementation for the STREAM construction.
25
+ // HKDF-SHA-256 key derivation → HChaCha20 subkey → ChaCha20-Poly1305 per chunk.
26
+ import { getInstance } from '../init.js';
27
+ import { HKDF_SHA256 } from '../sha2/index.js';
28
+ import { aeadEncrypt, aeadDecrypt, deriveSubkey } from './ops.js';
29
+ import { wipe, randomBytes } from '../utils.js';
30
+ const INFO = new TextEncoder().encode('xchacha20-sealstream-v2');
31
+ function getExports() {
32
+ return getInstance('chacha20').exports;
33
+ }
34
+ export const XChaCha20Cipher = {
35
+ formatEnum: 0x01,
36
+ formatName: 'xchacha20',
37
+ hkdfInfo: 'xchacha20-sealstream-v2',
38
+ keySize: 32,
39
+ kemCtSize: 0,
40
+ tagSize: 16,
41
+ padded: false,
42
+ wasmChunkSize: 65536, // src/asm/chacha20/buffers.ts CHUNK_SIZE
43
+ wasmModules: ['chacha20'],
44
+ keygen() {
45
+ return randomBytes(32);
46
+ },
47
+ deriveKeys(masterKey, nonce, _kemCt) {
48
+ const hkdf = new HKDF_SHA256();
49
+ const streamKey = hkdf.derive(masterKey, nonce, INFO, 32);
50
+ hkdf.dispose();
51
+ // HChaCha20 subkey derivation — nonce[0:16] as XChaCha input
52
+ const x = getExports();
53
+ const subkey = deriveSubkey(x, streamKey, nonce);
54
+ wipe(streamKey);
55
+ return { bytes: subkey };
56
+ },
57
+ sealChunk(keys, counterNonce, chunk, aad) {
58
+ const x = getExports();
59
+ const { ciphertext, tag } = aeadEncrypt(x, keys.bytes, counterNonce, chunk, aad ?? new Uint8Array(0));
60
+ const out = new Uint8Array(ciphertext.length + 16);
61
+ out.set(ciphertext);
62
+ out.set(tag, ciphertext.length);
63
+ return out;
64
+ },
65
+ openChunk(keys, counterNonce, chunk, aad) {
66
+ if (chunk.length < 16)
67
+ throw new RangeError(`chunk too short for 16-byte tag (got ${chunk.length})`);
68
+ const x = getExports();
69
+ const ct = chunk.subarray(0, chunk.length - 16);
70
+ const tag = chunk.subarray(chunk.length - 16);
71
+ return aeadDecrypt(x, keys.bytes, counterNonce, ct, tag, aad ?? new Uint8Array(0), 'xchacha20-poly1305');
72
+ },
73
+ wipeKeys(keys) {
74
+ wipe(keys.bytes);
75
+ },
76
+ createPoolWorker() {
77
+ return new Worker(new URL('./pool-worker.js', import.meta.url), { type: 'module' });
78
+ },
79
+ };
@@ -0,0 +1 @@
1
+ export { WASM_GZ_BASE64 as chacha20Wasm } from '../embedded/chacha20.js';
@@ -0,0 +1,27 @@
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/embedded.ts
23
+ //
24
+ // Exports the gzip+base64 chacha20 WASM blob for use as a WasmSource.
25
+ // This is the only file in the chacha20 subpath that references the embedded blob.
26
+ // Import via `leviathan-crypto/chacha20/embedded`.
27
+ export { WASM_GZ_BASE64 as chacha20Wasm } from '../embedded/chacha20.js';
@@ -1,5 +1,8 @@
1
- import type { Mode, InitOpts } from '../init.js';
2
- export declare function chacha20Init(mode?: Mode, opts?: InitOpts): Promise<void>;
1
+ import type { WasmSource } from '../wasm-source.js';
2
+ import { AuthenticationError } from '../errors.js';
3
+ export { AuthenticationError };
4
+ export declare function chacha20Init(source: WasmSource): Promise<void>;
5
+ export type { WasmSource };
3
6
  export declare class ChaCha20 {
4
7
  private readonly x;
5
8
  constructor();
@@ -18,17 +21,22 @@ export declare class Poly1305 {
18
21
  /**
19
22
  * ChaCha20-Poly1305 AEAD (RFC 8439 §2.8).
20
23
  *
24
+ * `encrypt()` returns ciphertext || tag(16) as a single Uint8Array.
25
+ * `decrypt()` accepts the same combined format and splits internally.
26
+ *
27
+ * Single-use encrypt guard: `encrypt()` may only be called once per instance.
28
+ * Create a new instance for each encryption to prevent nonce reuse.
29
+ *
21
30
  * `decrypt()` uses constant-time tag comparison — XOR-accumulate pattern,
22
31
  * no early return on mismatch. Plaintext is never returned on failure.
23
32
  */
24
33
  export declare class ChaCha20Poly1305 {
25
34
  private readonly x;
35
+ private _used;
26
36
  constructor();
27
- encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): {
28
- ciphertext: Uint8Array;
29
- tag: Uint8Array;
30
- };
31
- decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, tag: Uint8Array, aad?: Uint8Array): Uint8Array;
37
+ encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array;
38
+ decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, // ciphertext || tag(16) combined
39
+ aad?: Uint8Array): Uint8Array;
32
40
  dispose(): void;
33
41
  }
34
42
  /**
@@ -37,33 +45,18 @@ export declare class ChaCha20Poly1305 {
37
45
  * Recommended authenticated encryption primitive for most use cases.
38
46
  * Uses a 24-byte nonce — safe for random generation via crypto.getRandomValues.
39
47
  *
48
+ * Single-use encrypt guard: `encrypt()` may only be called once per instance.
49
+ * Create a new instance for each encryption to prevent nonce reuse.
50
+ *
40
51
  * `decrypt()` constant-time guarantee is inherited from the inner AEAD path.
41
52
  */
42
53
  export declare class XChaCha20Poly1305 {
43
54
  private readonly x;
55
+ private _used;
44
56
  constructor();
45
57
  encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array;
46
58
  decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array;
47
59
  dispose(): void;
48
60
  }
49
- /**
50
- * XChaCha20-Poly1305 AEAD with bound key and automatic nonce management.
51
- * Implements the AEAD interface — encrypt()/decrypt() require only plaintext
52
- * and optional AAD. Each encrypt() call generates a fresh 24-byte random nonce.
53
- *
54
- * Wire format: nonce(24) || ciphertext || tag(16)
55
- *
56
- * Use this when you want the simplest correct API and do not need to manage
57
- * nonces yourself. For protocol interop requiring explicit nonce control,
58
- * use XChaCha20Poly1305 directly.
59
- */
60
- export declare class XChaCha20Seal {
61
- private readonly _x;
62
- private readonly _key;
63
- constructor(key: Uint8Array);
64
- encrypt(plaintext: Uint8Array, aad?: Uint8Array, _nonce?: Uint8Array): Uint8Array;
65
- decrypt(ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array;
66
- dispose(): void;
67
- }
68
- export { XChaCha20StreamSealer, XChaCha20StreamOpener } from './stream-sealer.js';
61
+ export { XChaCha20Cipher } from './cipher-suite.js';
69
62
  export declare function _chachaReady(): boolean;
@@ -22,13 +22,13 @@
22
22
  // src/ts/chacha20/index.ts
23
23
  //
24
24
  // Public API classes for the ChaCha20 WASM module.
25
- // Uses the init() module cache — call init('chacha20') before constructing.
25
+ // Uses the init() module cache — call chacha20Init(source) before constructing.
26
26
  import { getInstance, initModule } from '../init.js';
27
27
  import { aeadEncrypt, aeadDecrypt, xcEncrypt, xcDecrypt } from './ops.js';
28
- import { hasSIMD, randomBytes, wipe } from '../utils.js';
29
- const _embedded = () => import('../embedded/chacha20.js').then(m => m.WASM_GZ_BASE64);
30
- export async function chacha20Init(mode = 'embedded', opts) {
31
- return initModule('chacha20', _embedded, mode, opts);
28
+ import { AuthenticationError } from '../errors.js';
29
+ export { AuthenticationError };
30
+ export async function chacha20Init(source) {
31
+ return initModule('chacha20', source);
32
32
  }
33
33
  function getExports() {
34
34
  return getInstance('chacha20').exports;
@@ -58,8 +58,7 @@ export class ChaCha20 {
58
58
  const ptOff = this.x.getChunkPtOffset();
59
59
  const ctOff = this.x.getChunkCtOffset();
60
60
  mem.set(chunk, ptOff);
61
- const fn = hasSIMD() ? this.x.chachaEncryptChunk_simd : this.x.chachaEncryptChunk;
62
- fn(chunk.length);
61
+ this.x.chachaEncryptChunk_simd(chunk.length);
63
62
  return mem.slice(ctOff, ctOff + chunk.length);
64
63
  }
65
64
  beginDecrypt(key, nonce) {
@@ -105,29 +104,47 @@ export class Poly1305 {
105
104
  /**
106
105
  * ChaCha20-Poly1305 AEAD (RFC 8439 §2.8).
107
106
  *
107
+ * `encrypt()` returns ciphertext || tag(16) as a single Uint8Array.
108
+ * `decrypt()` accepts the same combined format and splits internally.
109
+ *
110
+ * Single-use encrypt guard: `encrypt()` may only be called once per instance.
111
+ * Create a new instance for each encryption to prevent nonce reuse.
112
+ *
108
113
  * `decrypt()` uses constant-time tag comparison — XOR-accumulate pattern,
109
114
  * no early return on mismatch. Plaintext is never returned on failure.
110
115
  */
111
116
  export class ChaCha20Poly1305 {
112
117
  x;
118
+ _used = false;
113
119
  constructor() {
114
120
  this.x = getExports();
115
121
  }
116
122
  encrypt(key, nonce, plaintext, aad = new Uint8Array(0)) {
123
+ if (this._used)
124
+ throw new Error('leviathan-crypto: encrypt() already called on this instance. '
125
+ + 'Create a new instance for each encryption to prevent nonce reuse.');
117
126
  if (key.length !== 32)
118
127
  throw new RangeError(`key must be 32 bytes (got ${key.length})`);
119
128
  if (nonce.length !== 12)
120
129
  throw new RangeError(`nonce must be 12 bytes (got ${nonce.length})`);
121
- return aeadEncrypt(this.x, key, nonce, plaintext, aad);
130
+ const { ciphertext, tag } = aeadEncrypt(this.x, key, nonce, plaintext, aad);
131
+ this._used = true;
132
+ const out = new Uint8Array(ciphertext.length + 16);
133
+ out.set(ciphertext);
134
+ out.set(tag, ciphertext.length);
135
+ return out;
122
136
  }
123
- decrypt(key, nonce, ciphertext, tag, aad = new Uint8Array(0)) {
137
+ decrypt(key, nonce, ciphertext, // ciphertext || tag(16) combined
138
+ aad = new Uint8Array(0)) {
124
139
  if (key.length !== 32)
125
140
  throw new RangeError(`key must be 32 bytes (got ${key.length})`);
126
141
  if (nonce.length !== 12)
127
142
  throw new RangeError(`nonce must be 12 bytes (got ${nonce.length})`);
128
- if (tag.length !== 16)
129
- throw new RangeError(`tag must be 16 bytes (got ${tag.length})`);
130
- return aeadDecrypt(this.x, key, nonce, ciphertext, tag, aad);
143
+ if (ciphertext.length < 16)
144
+ throw new RangeError(`ciphertext too short — must include 16-byte tag (got ${ciphertext.length})`);
145
+ const ct = ciphertext.subarray(0, ciphertext.length - 16);
146
+ const tag = ciphertext.subarray(ciphertext.length - 16);
147
+ return aeadDecrypt(this.x, key, nonce, ct, tag, aad);
131
148
  }
132
149
  dispose() {
133
150
  this.x.wipeBuffers();
@@ -140,19 +157,28 @@ export class ChaCha20Poly1305 {
140
157
  * Recommended authenticated encryption primitive for most use cases.
141
158
  * Uses a 24-byte nonce — safe for random generation via crypto.getRandomValues.
142
159
  *
160
+ * Single-use encrypt guard: `encrypt()` may only be called once per instance.
161
+ * Create a new instance for each encryption to prevent nonce reuse.
162
+ *
143
163
  * `decrypt()` constant-time guarantee is inherited from the inner AEAD path.
144
164
  */
145
165
  export class XChaCha20Poly1305 {
146
166
  x;
167
+ _used = false;
147
168
  constructor() {
148
169
  this.x = getExports();
149
170
  }
150
171
  encrypt(key, nonce, plaintext, aad = new Uint8Array(0)) {
172
+ if (this._used)
173
+ throw new Error('leviathan-crypto: encrypt() already called on this instance. '
174
+ + 'Create a new instance for each encryption to prevent nonce reuse.');
151
175
  if (key.length !== 32)
152
176
  throw new RangeError(`key must be 32 bytes (got ${key.length})`);
153
177
  if (nonce.length !== 24)
154
178
  throw new RangeError(`XChaCha20 nonce must be 24 bytes (got ${nonce.length})`);
155
- return xcEncrypt(this.x, key, nonce, plaintext, aad);
179
+ const result = xcEncrypt(this.x, key, nonce, plaintext, aad);
180
+ this._used = true;
181
+ return result;
156
182
  }
157
183
  decrypt(key, nonce, ciphertext, aad = new Uint8Array(0)) {
158
184
  if (key.length !== 32)
@@ -167,52 +193,7 @@ export class XChaCha20Poly1305 {
167
193
  this.x.wipeBuffers();
168
194
  }
169
195
  }
170
- // ── XChaCha20Seal ────────────────────────────────────────────────────────────
171
- /**
172
- * XChaCha20-Poly1305 AEAD with bound key and automatic nonce management.
173
- * Implements the AEAD interface — encrypt()/decrypt() require only plaintext
174
- * and optional AAD. Each encrypt() call generates a fresh 24-byte random nonce.
175
- *
176
- * Wire format: nonce(24) || ciphertext || tag(16)
177
- *
178
- * Use this when you want the simplest correct API and do not need to manage
179
- * nonces yourself. For protocol interop requiring explicit nonce control,
180
- * use XChaCha20Poly1305 directly.
181
- */
182
- export class XChaCha20Seal {
183
- _x;
184
- _key;
185
- constructor(key) {
186
- if (!_chachaReady())
187
- throw new Error('leviathan-crypto: call init([\'chacha20\']) before using XChaCha20Seal');
188
- if (key.length !== 32)
189
- throw new RangeError(`XChaCha20Seal key must be 32 bytes (got ${key.length})`);
190
- this._x = getExports();
191
- this._key = key.slice();
192
- }
193
- // _nonce: test seam only — inject a fixed nonce for deterministic KAT vectors
194
- encrypt(plaintext, aad = new Uint8Array(0), _nonce) {
195
- const nonce = (_nonce && _nonce.length === 24) ? _nonce : randomBytes(24);
196
- const sealed = xcEncrypt(this._x, this._key, nonce, plaintext, aad);
197
- // Prepend nonce to sealed output (ciphertext || tag)
198
- const out = new Uint8Array(24 + sealed.length);
199
- out.set(nonce, 0);
200
- out.set(sealed, 24);
201
- return out;
202
- }
203
- decrypt(ciphertext, aad = new Uint8Array(0)) {
204
- if (ciphertext.length < 40)
205
- throw new RangeError(`XChaCha20Seal ciphertext too short — need nonce(24)+tag(16)=40 bytes minimum (got ${ciphertext.length})`);
206
- const nonce = ciphertext.subarray(0, 24);
207
- const payload = ciphertext.subarray(24);
208
- return xcDecrypt(this._x, this._key, nonce, payload, aad);
209
- }
210
- dispose() {
211
- wipe(this._key);
212
- this._x.wipeBuffers();
213
- }
214
- }
215
- export { XChaCha20StreamSealer, XChaCha20StreamOpener } from './stream-sealer.js';
196
+ export { XChaCha20Cipher } from './cipher-suite.js';
216
197
  // ── Ready check ──────────────────────────────────────────────────────────────
217
198
  export function _chachaReady() {
218
199
  try {
@@ -5,7 +5,7 @@ export declare function aeadEncrypt(x: ChaChaExports, key: Uint8Array, nonce: Ui
5
5
  tag: Uint8Array;
6
6
  };
7
7
  /** ChaCha20-Poly1305 AEAD decrypt (RFC 8439 §2.8). Constant-time tag comparison. */
8
- export declare function aeadDecrypt(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, tag: Uint8Array, aad: Uint8Array): Uint8Array;
8
+ export declare function aeadDecrypt(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, tag: Uint8Array, aad: Uint8Array, cipherName?: string): Uint8Array;
9
9
  /** HChaCha20 subkey derivation — first 16 bytes of nonce. */
10
10
  export declare function deriveSubkey(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array): Uint8Array;
11
11
  /** Build inner 12-byte nonce from bytes 16–23 of XChaCha nonce. */