leviathan-crypto 1.3.1 → 2.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 (124) hide show
  1. package/CLAUDE.md +129 -76
  2. package/README.md +166 -221
  3. package/SECURITY.md +89 -37
  4. package/dist/chacha20/cipher-suite.d.ts +4 -0
  5. package/dist/chacha20/cipher-suite.js +78 -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 -7
  9. package/dist/chacha20/index.js +41 -14
  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 +320 -0
  17. package/dist/docs/architecture.md +419 -285
  18. package/dist/docs/argon2id.md +42 -30
  19. package/dist/docs/chacha20.md +218 -150
  20. package/dist/docs/exports.md +241 -0
  21. package/dist/docs/fortuna.md +65 -74
  22. package/dist/docs/init.md +172 -178
  23. package/dist/docs/loader.md +87 -132
  24. package/dist/docs/serpent.md +134 -565
  25. package/dist/docs/sha2.md +91 -103
  26. package/dist/docs/sha3.md +70 -36
  27. package/dist/docs/types.md +93 -16
  28. package/dist/docs/utils.md +114 -41
  29. package/dist/embedded/chacha20.d.ts +1 -1
  30. package/dist/embedded/chacha20.js +2 -1
  31. package/dist/embedded/kyber.d.ts +1 -0
  32. package/dist/embedded/kyber.js +3 -0
  33. package/dist/embedded/serpent.d.ts +1 -1
  34. package/dist/embedded/serpent.js +2 -1
  35. package/dist/embedded/sha2.d.ts +1 -1
  36. package/dist/embedded/sha2.js +2 -1
  37. package/dist/embedded/sha3.d.ts +1 -1
  38. package/dist/embedded/sha3.js +2 -1
  39. package/dist/errors.d.ts +10 -0
  40. package/dist/{serpent/seal.js → errors.js} +14 -46
  41. package/dist/fortuna.d.ts +2 -8
  42. package/dist/fortuna.js +11 -9
  43. package/dist/index.d.ts +25 -9
  44. package/dist/index.js +36 -7
  45. package/dist/init.d.ts +3 -7
  46. package/dist/init.js +18 -35
  47. package/dist/keccak/embedded.d.ts +1 -0
  48. package/dist/keccak/embedded.js +27 -0
  49. package/dist/keccak/index.d.ts +4 -0
  50. package/dist/keccak/index.js +31 -0
  51. package/dist/kyber/embedded.d.ts +1 -0
  52. package/dist/kyber/embedded.js +27 -0
  53. package/dist/kyber/indcpa.d.ts +49 -0
  54. package/dist/kyber/indcpa.js +352 -0
  55. package/dist/kyber/index.d.ts +38 -0
  56. package/dist/kyber/index.js +150 -0
  57. package/dist/kyber/kem.d.ts +21 -0
  58. package/dist/kyber/kem.js +160 -0
  59. package/dist/kyber/params.d.ts +14 -0
  60. package/dist/kyber/params.js +37 -0
  61. package/dist/kyber/suite.d.ts +13 -0
  62. package/dist/kyber/suite.js +93 -0
  63. package/dist/kyber/types.d.ts +98 -0
  64. package/dist/kyber/types.js +25 -0
  65. package/dist/kyber/validate.d.ts +19 -0
  66. package/dist/kyber/validate.js +68 -0
  67. package/dist/kyber.wasm +0 -0
  68. package/dist/loader.d.ts +19 -4
  69. package/dist/loader.js +91 -25
  70. package/dist/serpent/cipher-suite.d.ts +4 -0
  71. package/dist/serpent/cipher-suite.js +121 -0
  72. package/dist/serpent/embedded.d.ts +1 -0
  73. package/dist/serpent/embedded.js +27 -0
  74. package/dist/serpent/index.d.ts +6 -37
  75. package/dist/serpent/index.js +9 -118
  76. package/dist/serpent/pool-worker.d.ts +1 -0
  77. package/dist/serpent/pool-worker.js +202 -0
  78. package/dist/serpent/serpent-cbc.d.ts +30 -0
  79. package/dist/serpent/serpent-cbc.js +136 -0
  80. package/dist/sha2/embedded.d.ts +1 -0
  81. package/dist/sha2/embedded.js +27 -0
  82. package/dist/sha2/hkdf.js +6 -2
  83. package/dist/sha2/index.d.ts +3 -2
  84. package/dist/sha2/index.js +3 -4
  85. package/dist/sha3/embedded.d.ts +1 -0
  86. package/dist/sha3/embedded.js +27 -0
  87. package/dist/sha3/index.d.ts +3 -2
  88. package/dist/sha3/index.js +3 -4
  89. package/dist/stream/constants.d.ts +6 -0
  90. package/dist/stream/constants.js +30 -0
  91. package/dist/stream/header.d.ts +9 -0
  92. package/dist/stream/header.js +77 -0
  93. package/dist/stream/index.d.ts +7 -0
  94. package/dist/stream/index.js +27 -0
  95. package/dist/stream/open-stream.d.ts +21 -0
  96. package/dist/stream/open-stream.js +146 -0
  97. package/dist/stream/seal-stream-pool.d.ts +38 -0
  98. package/dist/stream/seal-stream-pool.js +391 -0
  99. package/dist/stream/seal-stream.d.ts +20 -0
  100. package/dist/stream/seal-stream.js +142 -0
  101. package/dist/stream/seal.d.ts +9 -0
  102. package/dist/stream/seal.js +75 -0
  103. package/dist/stream/types.d.ts +24 -0
  104. package/dist/stream/types.js +26 -0
  105. package/dist/utils.d.ts +12 -7
  106. package/dist/utils.js +75 -19
  107. package/dist/wasm-source.d.ts +12 -0
  108. package/dist/wasm-source.js +26 -0
  109. package/package.json +13 -5
  110. package/dist/chacha20/pool.d.ts +0 -52
  111. package/dist/chacha20/pool.js +0 -188
  112. package/dist/chacha20/pool.worker.js +0 -37
  113. package/dist/docs/chacha20_pool.md +0 -309
  114. package/dist/docs/wasm.md +0 -194
  115. package/dist/serpent/seal.d.ts +0 -8
  116. package/dist/serpent/stream-pool.d.ts +0 -48
  117. package/dist/serpent/stream-pool.js +0 -285
  118. package/dist/serpent/stream-sealer.d.ts +0 -50
  119. package/dist/serpent/stream-sealer.js +0 -341
  120. package/dist/serpent/stream.d.ts +0 -28
  121. package/dist/serpent/stream.js +0 -205
  122. package/dist/serpent/stream.worker.d.ts +0 -32
  123. package/dist/serpent/stream.worker.js +0 -117
  124. /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,26 +12,39 @@
12
12
  ## Supported Versions
13
13
 
14
14
  | Version | Supported |
15
- |---------|-----------|
16
- | v1.3.x | ︎✓ |
17
- | v1.2.x | |
15
+ | ------- | --------- |
16
+ | v2.0.x | |
17
+ | v1.4.x | |
18
+ | v1.3.x | ✗ |
19
+ | v1.2.x | ✗ |
18
20
  | v1.1.x | ✗ |
19
21
  | v1.0.x | ✗ |
20
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
+
21
27
  > [!WARNING]
22
- > v1.0.x does not zero intermediate key material in HMAC and HKDF operations.
23
- > Upgrading to v1.1.0 or later is strongly recommended.
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
+
24
35
 
25
36
  ## Security Posture
26
37
 
27
- [`leviathan-crypto`](https://leviathan.3xi.club) is a cryptography library. Security is not an afterthought,
28
- it is the primary design constraint at every layer of the stack.
38
+ [`leviathan-crypto`](https://leviathan.3xi.club) is a cryptography library.
39
+ Security is not an afterthought, it is the primary design constraint at every
40
+ layer of the stack.
29
41
 
30
42
  ### Algorithm Correctness
31
43
 
32
44
  Every primitive in this library was implemented by hand in AssemblyScript
33
45
  against the authoritative specification for that algorithm:
34
46
  [FIPS 180-4][fips180] (SHA-2), [FIPS 202][fips202] (SHA-3),
47
+ [FIPS 203][fips203] (ML-KEM),
35
48
  [RFC 8439][rfc8439] (ChaCha20-Poly1305), [RFC 2104][rfc2104] (HMAC),
36
49
  [RFC 5869][rfc5869] (HKDF), and the original
37
50
  [Serpent-256 specification][serpent] and S-box reference. No algorithm was
@@ -51,9 +64,13 @@ timing-safe cipher implementation approach available in a WASM runtime,
51
64
  where JIT optimisation can otherwise introduce observable timing variation.
52
65
 
53
66
  All security-sensitive comparisons (e.g. MAC verification, padding validation)
54
- use XOR-accumulate patterns with no early return on mismatch.
55
- [`constantTimeEqual`][utils] is the mandated comparison function throughout
56
- the library and its [demos][demos].
67
+ use [`constantTimeEqual`][utils], which is backed by a dedicated WASM SIMD module
68
+ (v128 XOR accumulate + `any_true`) when WebAssembly SIMD is available. The WASM
69
+ execution path eliminates JIT short-circuiting and speculative optimization as
70
+ theoretical side-channel vectors. On runtimes without SIMD (sha2/sha3-only
71
+ consumers), the function falls back to a JS XOR-accumulate loop. This is best-effort
72
+ constant-time, not a hardware-level guarantee. WASM comparison memory is
73
+ wiped after every call.
57
74
 
58
75
  ### WASM Execution Model
59
76
 
@@ -62,20 +79,36 @@ JavaScript JIT. WASM execution is deterministic and not subject to JIT
62
79
  speculation or optimisation. Each primitive family compiles to its own
63
80
  isolated binary with its own linear memory. For example, key material in
64
81
  the Serpent module cannot interact with memory in the SHA-3 module,
65
- even in principle.
66
-
67
- ### Cryptanalytic Reviews
68
-
69
- All of our primitives undergo periodic cryptographic implementation reviews.
70
-
71
- | Primitive | Audit Description |
72
- |-----------|-------------------|
73
- | [serpent_audit][serpent_audit] | Correctness verification, side-channel analysis, cryptanalytic attack paper review |
74
- | [chacha_audit][chacha_audit] | XChaCha20-Poly1305 correctness, Poly1305 field arithmetic, HChaCha20 nonce extension |
75
- | [sha2_audit][sha2_audit] | SHA-256/512/384 correctness, HMAC and HKDF composition, constant verification |
76
- | [sha3_audit][sha3_audit] | Keccak permutation correctness, θ/ρ/π/χ/ι step verification, round constant derivation |
77
- | [hmac_audit][hmac_audit] | HMAC-SHA256/512/384 construction, key processing, RFC 4231 vector coverage |
78
- | [hkdf_audit][hkdf_audit] | HKDF extract-then-expand, info field domain separation, SerpentStream key derivation |
82
+ even in principle. A dedicated WASM module handles constant-time comparison
83
+ with its own single-page memory that is wiped after every call.
84
+
85
+ Serpent and ChaCha20 modules require WebAssembly SIMD (`v128` instructions).
86
+ `init()` and `initModule()` perform a SIMD preflight check and throw a
87
+ clear error on runtimes without support. SIMD has been a baseline feature
88
+ of all major browsers and runtimes since 2021. SHA-2 and SHA-3 modules
89
+ run on any WASM-capable runtime.
90
+
91
+ The `kyber` module requires WebAssembly SIMD for NTT and polynomial
92
+ arithmetic (`v128` instructions). The SIMD preflight check is applied on
93
+ `init()` alongside serpent and chacha20. Its linear memory is independent
94
+ from all other modules. The kyber module's constant-time path (FO transform
95
+ decapsulation) uses dedicated `ct_verify` and `ct_cmov` functions implemented
96
+ in the kyber WASM binary. The comparison never passes through JavaScript.
97
+
98
+ ### Cryptanalytic Audits
99
+
100
+ All primitives undergo periodic cryptographic implementation reviews. See the [audit index][audits] for a full summary.
101
+
102
+ | Primitive | Audit Description |
103
+ | ------------------------------ | -------------------------------------------------------------------------------------- |
104
+ | [serpent_audit][serpent_audit] | Correctness verification, side-channel analysis, cryptanalytic attack paper review |
105
+ | [chacha_audit][chacha_audit] | XChaCha20-Poly1305 correctness, Poly1305 field arithmetic, HChaCha20 nonce extension |
106
+ | [sha2_audit][sha2_audit] | SHA-256/512/384 correctness, HMAC and HKDF composition, constant verification |
107
+ | [sha3_audit][sha3_audit] | Keccak permutation correctness, θ/ρ/π/χ/ι step verification, round constant derivation |
108
+ | [hmac_audit][hmac_audit] | HMAC-SHA256/512/384 construction, key processing, RFC 4231 vector coverage |
109
+ | [hkdf_audit][hkdf_audit] | HKDF extract-then-expand, info field domain separation, stream key derivation |
110
+ | [kyber_audit][kyber_audit] | ML-KEM FIPS 203 correctness, NTT/Montgomery/Barrett verification, FO transform CT analysis, ACVP validation |
111
+ | [stream_audit][stream_audit] | Streaming AEAD composition, counter nonce binding, final-chunk detection, key wipe paths |
79
112
 
80
113
  #### Additional Serpent-256 research
81
114
 
@@ -92,17 +125,32 @@ See: [`xero/BicliqueFinder/biclique_research.md`][biclique]
92
125
 
93
126
  ### Authenticated Encryption by Default
94
127
 
95
- Raw unauthenticated cipher modes (`SerpentCbc`, `SerpentCtr`) are exposed
96
- for power users but are not the recommended entry point. The primary API
97
- surfaces `SerpentSeal`, `SerpentStream`, `SerpentStreamSealer` are
98
- authenticated by construction.
99
-
100
- **`SerpentStreamSealer` satisfies the _Cryptographic Doom Principle_:**
101
-
102
- MAC verification is the unconditional gate on the open path,
103
- decryption is unreachable until that gate clears, and per-chunk
104
- HKDF key derivation with position-bound info extends this
105
- guarantee to full stream integrity.
128
+ Raw unauthenticated cipher modes (`SerpentCbc`, `SerpentCtr`, `ChaCha20`) and
129
+ stateless caller-managed-nonce primitives (`ChaCha20Poly1305`,
130
+ `XChaCha20Poly1305`) are exposed for power users but are not the recommended
131
+ entry point. The primary API surfaces (`Seal`, `SealStream`, `OpenStream`,
132
+ `SealStreamPool`, and `KyberSuite`) are authenticated by construction with
133
+ internally managed nonces.
134
+
135
+ **All streaming constructions satisfy the _Cryptographic Doom Principle_:**
136
+
137
+ `SealStream` / `OpenStream` with `SerpentCipher` uses encrypt-then-MAC
138
+ (SerpentCbc + HMAC-SHA256). MAC verification is the unconditional gate on
139
+ the open path. Decryption is unreachable until that gate clears. HKDF key
140
+ derivation with the stream nonce and counter-nonce domain separation
141
+ extends this guarantee to full stream integrity.
142
+
143
+ `SealStream` / `OpenStream` with `XChaCha20Cipher` uses XChaCha20-Poly1305
144
+ AEAD per chunk. The Poly1305 tag is verified inside the WASM `aeadDecrypt`
145
+ call before any plaintext is produced. On authentication failure, the full
146
+ chunk output buffer is wiped and plaintext bytes are never returned.
147
+ Counter nonces with TAG_DATA/TAG_FINAL final-flag domain separation ensure
148
+ reorder, splice, truncation, and cross-stream substitution all fail AEAD
149
+ verification before decryption.
150
+
151
+ `SealStreamPool` delegates per-chunk AEAD to isolated Web Workers. Each
152
+ worker holds its own derived subkey and WASM instance. Any authentication
153
+ error kills all workers, wipes all key material, and marks the pool dead. No retry, no partial results.
106
154
 
107
155
  ### Dependency Management
108
156
 
@@ -163,7 +211,7 @@ or research notes, for full hacker scene credit.
163
211
 
164
212
  If you prefer to contact the maintainer directly:
165
213
 
166
- - **Email:** x﹫xero.style PGP: [`0xAC1D0000`][pgp]
214
+ - **Email:** x﹫xero.style · PGP: [`0xAC1D0000`][pgp]
167
215
  - **Matrix:** x0﹫rx.haunted.computer
168
216
 
169
217
  > [!NOTE]
@@ -198,6 +246,7 @@ If you prefer to contact the maintainer directly:
198
246
 
199
247
  [fips180]: https://csrc.nist.gov/publications/detail/fips/180/4/final
200
248
  [fips202]: https://csrc.nist.gov/publications/detail/fips/202/final
249
+ [fips203]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.pdf
201
250
  [rfc8439]: https://www.rfc-editor.org/rfc/rfc8439
202
251
  [rfc2104]: https://www.rfc-editor.org/rfc/rfc2104
203
252
  [rfc5869]: https://www.rfc-editor.org/rfc/rfc5869
@@ -210,6 +259,9 @@ If you prefer to contact the maintainer directly:
210
259
  [sha3_audit]: https://github.com/xero/leviathan-crypto/wiki/sha3_audit
211
260
  [hmac_audit]: https://github.com/xero/leviathan-crypto/wiki/hmac_audit
212
261
  [hkdf_audit]: https://github.com/xero/leviathan-crypto/wiki/hkdf_audit
262
+ [kyber_audit]: https://github.com/xero/leviathan-crypto/wiki/kyber_audit
263
+ [stream_audit]: https://github.com/xero/leviathan-crypto/wiki/stream_audit
264
+ [audits]: https://github.com/xero/leviathan-crypto/wiki/audits
213
265
  [biclique]: https://github.com/xero/BicliqueFinder/blob/main/biclique-research.md
214
266
  [argon2id-wiki]: https://github.com/xero/leviathan-crypto/wiki/argon2id
215
267
  [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,78 @@
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
+ wasmModules: ['chacha20'],
43
+ keygen() {
44
+ return randomBytes(32);
45
+ },
46
+ deriveKeys(masterKey, nonce, _kemCt) {
47
+ const hkdf = new HKDF_SHA256();
48
+ const streamKey = hkdf.derive(masterKey, nonce, INFO, 32);
49
+ hkdf.dispose();
50
+ // HChaCha20 subkey derivation — nonce[0:16] as XChaCha input
51
+ const x = getExports();
52
+ const subkey = deriveSubkey(x, streamKey, nonce);
53
+ wipe(streamKey);
54
+ return { bytes: subkey };
55
+ },
56
+ sealChunk(keys, counterNonce, chunk, aad) {
57
+ const x = getExports();
58
+ const { ciphertext, tag } = aeadEncrypt(x, keys.bytes, counterNonce, chunk, aad ?? new Uint8Array(0));
59
+ const out = new Uint8Array(ciphertext.length + 16);
60
+ out.set(ciphertext);
61
+ out.set(tag, ciphertext.length);
62
+ return out;
63
+ },
64
+ openChunk(keys, counterNonce, chunk, aad) {
65
+ if (chunk.length < 16)
66
+ throw new RangeError(`chunk too short for 16-byte tag (got ${chunk.length})`);
67
+ const x = getExports();
68
+ const ct = chunk.subarray(0, chunk.length - 16);
69
+ const tag = chunk.subarray(chunk.length - 16);
70
+ return aeadDecrypt(x, keys.bytes, counterNonce, ct, tag, aad ?? new Uint8Array(0), 'xchacha20-poly1305');
71
+ },
72
+ wipeKeys(keys) {
73
+ wipe(keys.bytes);
74
+ },
75
+ createPoolWorker() {
76
+ return new Worker(new URL('./pool-worker.js', import.meta.url), { type: 'module' });
77
+ },
78
+ };
@@ -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,13 +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
  }
61
+ export { XChaCha20Cipher } from './cipher-suite.js';
49
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 } from '../utils.js';
29
- const _embedded = () => import('../embedded/chacha20.js').then(m => m.WASM_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);
122
- }
123
- decrypt(key, nonce, ciphertext, tag, aad = new Uint8Array(0)) {
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;
136
+ }
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,6 +193,7 @@ export class XChaCha20Poly1305 {
167
193
  this.x.wipeBuffers();
168
194
  }
169
195
  }
196
+ export { XChaCha20Cipher } from './cipher-suite.js';
170
197
  // ── Ready check ──────────────────────────────────────────────────────────────
171
198
  export function _chachaReady() {
172
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. */
@@ -4,6 +4,7 @@
4
4
  // ChaChaExports explicitly. Used by both the class wrappers (index.ts)
5
5
  // and the pool worker (pool.worker.ts), eliminating duplication.
6
6
  import { constantTimeEqual } from '../utils.js';
7
+ import { AuthenticationError } from '../errors.js';
7
8
  // ── Module-private helpers ───────────────────────────────────────────────────
8
9
  function polyFeed(x, data) {
9
10
  if (data.length === 0)
@@ -20,16 +21,14 @@ function polyFeed(x, data) {
20
21
  }
21
22
  function lenBlock(aadLen, ctLen) {
22
23
  const b = new Uint8Array(16);
23
- let n = aadLen;
24
- for (let i = 0; i < 4; i++) {
25
- b[i] = n & 0xff;
26
- n >>>= 8;
27
- }
28
- n = ctLen;
29
- for (let i = 0; i < 4; i++) {
30
- b[8 + i] = n & 0xff;
31
- n >>>= 8;
32
- }
24
+ const dv = new DataView(b.buffer);
25
+ // RFC 8439 §2.8 64-bit LE lengths.
26
+ // JS numbers are f64 — write low 32 bits directly, high bits via
27
+ // Math.floor(n / 2^32). Safe for n ≤ Number.MAX_SAFE_INTEGER.
28
+ dv.setUint32(0, aadLen >>> 0, true);
29
+ dv.setUint32(4, Math.floor(aadLen / 0x100000000) >>> 0, true);
30
+ dv.setUint32(8, ctLen >>> 0, true);
31
+ dv.setUint32(12, Math.floor(ctLen / 0x100000000) >>> 0, true);
33
32
  return b;
34
33
  }
35
34
  // ── Inner AEAD (12-byte nonce) ───────────────────────────────────────────────
@@ -42,7 +41,6 @@ export function aeadEncrypt(x, key, nonce, plaintext, aad) {
42
41
  // Step 1: Generate Poly1305 one-time key at counter=0 (RFC 8439 §2.6)
43
42
  mem.set(key, x.getKeyOffset());
44
43
  mem.set(nonce, x.getChachaNonceOffset());
45
- x.chachaSetCounter(1);
46
44
  x.chachaLoadKey();
47
45
  x.chachaGenPolyKey();
48
46
  // Step 2: Initialise Poly1305
@@ -57,7 +55,7 @@ export function aeadEncrypt(x, key, nonce, plaintext, aad) {
57
55
  x.chachaLoadKey();
58
56
  // Step 5: Encrypt
59
57
  mem.set(plaintext, x.getChunkPtOffset());
60
- x.chachaEncryptChunk(plaintext.length);
58
+ x.chachaEncryptChunk_simd(plaintext.length);
61
59
  const ctOff = x.getChunkCtOffset();
62
60
  const ciphertext = new Uint8Array(x.memory.buffer).slice(ctOff, ctOff + plaintext.length);
63
61
  // Step 6: MAC ciphertext + pad
@@ -74,7 +72,7 @@ export function aeadEncrypt(x, key, nonce, plaintext, aad) {
74
72
  return { ciphertext, tag };
75
73
  }
76
74
  /** ChaCha20-Poly1305 AEAD decrypt (RFC 8439 §2.8). Constant-time tag comparison. */
77
- export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad) {
75
+ export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad, cipherName = 'chacha20-poly1305') {
78
76
  const maxChunk = x.getChunkSize();
79
77
  if (ciphertext.length > maxChunk)
80
78
  throw new RangeError(`ciphertext exceeds ${maxChunk} bytes — split into smaller chunks`);
@@ -82,7 +80,6 @@ export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad) {
82
80
  // Compute expected tag
83
81
  mem.set(key, x.getKeyOffset());
84
82
  mem.set(nonce, x.getChachaNonceOffset());
85
- x.chachaSetCounter(1);
86
83
  x.chachaLoadKey();
87
84
  x.chachaGenPolyKey();
88
85
  x.polyInit();
@@ -99,13 +96,17 @@ export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad) {
99
96
  // Constant-time tag comparison
100
97
  const tagOff = x.getPolyTagOffset();
101
98
  const expectedTag = new Uint8Array(x.memory.buffer).slice(tagOff, tagOff + 16);
102
- if (!constantTimeEqual(expectedTag, tag))
103
- throw new Error('ChaCha20Poly1305: authentication failed');
99
+ if (!constantTimeEqual(expectedTag, tag)) {
100
+ // Wipe the full chunk output buffer — defense-in-depth before throwing
101
+ const ctOff = x.getChunkCtOffset();
102
+ mem.fill(0, ctOff, ctOff + maxChunk);
103
+ throw new AuthenticationError(cipherName);
104
+ }
104
105
  // Decrypt only after authentication succeeds
105
106
  x.chachaSetCounter(1);
106
107
  x.chachaLoadKey();
107
108
  new Uint8Array(x.memory.buffer).set(ciphertext, x.getChunkPtOffset());
108
- x.chachaEncryptChunk(ciphertext.length);
109
+ x.chachaEncryptChunk_simd(ciphertext.length);
109
110
  const ptOff = x.getChunkCtOffset();
110
111
  return new Uint8Array(x.memory.buffer).slice(ptOff, ptOff + ciphertext.length);
111
112
  }
@@ -142,5 +143,5 @@ export function xcDecrypt(x, key, nonce, ciphertext, aad) {
142
143
  const tag = ciphertext.subarray(ciphertext.length - 16);
143
144
  const subkey = deriveSubkey(x, key, nonce);
144
145
  const inner = innerNonce(nonce);
145
- return aeadDecrypt(x, subkey, inner, ct, tag, aad);
146
+ return aeadDecrypt(x, subkey, inner, ct, tag, aad, 'xchacha20-poly1305');
146
147
  }