leviathan-crypto 1.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 (78) hide show
  1. package/CLAUDE.md +265 -0
  2. package/LICENSE +21 -0
  3. package/README.md +322 -0
  4. package/SECURITY.md +174 -0
  5. package/dist/chacha.wasm +0 -0
  6. package/dist/chacha20/index.d.ts +49 -0
  7. package/dist/chacha20/index.js +177 -0
  8. package/dist/chacha20/ops.d.ts +16 -0
  9. package/dist/chacha20/ops.js +146 -0
  10. package/dist/chacha20/pool.d.ts +52 -0
  11. package/dist/chacha20/pool.js +188 -0
  12. package/dist/chacha20/pool.worker.d.ts +1 -0
  13. package/dist/chacha20/pool.worker.js +37 -0
  14. package/dist/chacha20/types.d.ts +30 -0
  15. package/dist/chacha20/types.js +1 -0
  16. package/dist/docs/architecture.md +795 -0
  17. package/dist/docs/argon2id.md +290 -0
  18. package/dist/docs/chacha20.md +602 -0
  19. package/dist/docs/chacha20_pool.md +306 -0
  20. package/dist/docs/fortuna.md +322 -0
  21. package/dist/docs/init.md +308 -0
  22. package/dist/docs/loader.md +206 -0
  23. package/dist/docs/serpent.md +914 -0
  24. package/dist/docs/sha2.md +620 -0
  25. package/dist/docs/sha3.md +509 -0
  26. package/dist/docs/types.md +198 -0
  27. package/dist/docs/utils.md +273 -0
  28. package/dist/docs/wasm.md +193 -0
  29. package/dist/embedded/chacha.d.ts +1 -0
  30. package/dist/embedded/chacha.js +2 -0
  31. package/dist/embedded/serpent.d.ts +1 -0
  32. package/dist/embedded/serpent.js +2 -0
  33. package/dist/embedded/sha2.d.ts +1 -0
  34. package/dist/embedded/sha2.js +2 -0
  35. package/dist/embedded/sha3.d.ts +1 -0
  36. package/dist/embedded/sha3.js +2 -0
  37. package/dist/fortuna.d.ts +72 -0
  38. package/dist/fortuna.js +445 -0
  39. package/dist/index.d.ts +13 -0
  40. package/dist/index.js +44 -0
  41. package/dist/init.d.ts +11 -0
  42. package/dist/init.js +49 -0
  43. package/dist/loader.d.ts +4 -0
  44. package/dist/loader.js +30 -0
  45. package/dist/serpent/index.d.ts +65 -0
  46. package/dist/serpent/index.js +242 -0
  47. package/dist/serpent/seal.d.ts +8 -0
  48. package/dist/serpent/seal.js +70 -0
  49. package/dist/serpent/stream-encoder.d.ts +20 -0
  50. package/dist/serpent/stream-encoder.js +167 -0
  51. package/dist/serpent/stream-pool.d.ts +48 -0
  52. package/dist/serpent/stream-pool.js +285 -0
  53. package/dist/serpent/stream-sealer.d.ts +34 -0
  54. package/dist/serpent/stream-sealer.js +223 -0
  55. package/dist/serpent/stream.d.ts +28 -0
  56. package/dist/serpent/stream.js +205 -0
  57. package/dist/serpent/stream.worker.d.ts +32 -0
  58. package/dist/serpent/stream.worker.js +117 -0
  59. package/dist/serpent/types.d.ts +5 -0
  60. package/dist/serpent/types.js +1 -0
  61. package/dist/serpent.wasm +0 -0
  62. package/dist/sha2/hkdf.d.ts +16 -0
  63. package/dist/sha2/hkdf.js +108 -0
  64. package/dist/sha2/index.d.ts +40 -0
  65. package/dist/sha2/index.js +190 -0
  66. package/dist/sha2/types.d.ts +5 -0
  67. package/dist/sha2/types.js +1 -0
  68. package/dist/sha2.wasm +0 -0
  69. package/dist/sha3/index.d.ts +55 -0
  70. package/dist/sha3/index.js +246 -0
  71. package/dist/sha3/types.d.ts +5 -0
  72. package/dist/sha3/types.js +1 -0
  73. package/dist/sha3.wasm +0 -0
  74. package/dist/types.d.ts +24 -0
  75. package/dist/types.js +26 -0
  76. package/dist/utils.d.ts +26 -0
  77. package/dist/utils.js +169 -0
  78. package/package.json +90 -0
package/SECURITY.md ADDED
@@ -0,0 +1,174 @@
1
+ # Leviathan Crypto Library Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |---------|-----------|
7
+ | v1.x | ✓ |
8
+
9
+ > [!NOTE]
10
+ > v1.0.0 is the current stable release.
11
+ > This table will be updated as new versions ship.
12
+
13
+ ## Security Posture
14
+
15
+ leviathan-crypto is a cryptography library. Security is not an afterthought,
16
+ it is the primary design constraint at every layer of the stack.
17
+
18
+ ### Algorithm Correctness
19
+
20
+ Every primitive in this library was implemented by hand in AssemblyScript
21
+ against the authoritative specification for that algorithm:
22
+ [FIPS 180-4][fips180] (SHA-2), [FIPS 202][fips202] (SHA-3),
23
+ [RFC 8439][rfc8439] (ChaCha20-Poly1305), [RFC 2104][rfc2104] (HMAC),
24
+ [RFC 5869][rfc5869] (HKDF), and the original
25
+ [Serpent-256 specification][serpent] and S-box reference. No algorithm was
26
+ ported from an existing implementation — the specs are the source of truth.
27
+
28
+ All implementations are verified against published known-answer test vectors
29
+ from NIST, RFC appendices, NESSIE, and the Argon2 reference suite. Vectors
30
+ are immutable: if an implementation produces incorrect output, the
31
+ implementation is fixed — vectors are never adjusted to match code.
32
+
33
+ ### Side-Channel Resistance
34
+
35
+ Serpent's S-boxes are implemented as Boolean gate circuits — no table
36
+ lookups, no data-dependent memory access, no data-dependent branches. Every
37
+ bit is processed unconditionally on every block. This is the most
38
+ timing-safe cipher implementation approach available in a WASM runtime,
39
+ where JIT optimisation can otherwise introduce observable timing variation.
40
+
41
+ All security-sensitive comparisons (MAC verification, padding validation)
42
+ use XOR-accumulate patterns with no early return on mismatch.
43
+ [`constantTimeEqual`][utils] is the mandated comparison function throughout
44
+ the library and its demos.
45
+
46
+ ### WASM Execution Model
47
+
48
+ All cryptographic computation runs in WebAssembly, isolated outside the
49
+ JavaScript JIT. WASM execution is deterministic and not subject to JIT
50
+ speculation or optimisation. Each primitive family compiles to its own
51
+ isolated binary with its own linear memory — key material in the Serpent
52
+ module cannot interact with memory in the SHA-3 module even in principle.
53
+
54
+ ### Cryptanalytic Review
55
+
56
+ The security margin of Serpent-256 has been independently researched and
57
+ documented. The best known attack on the full 32-round cipher — biclique
58
+ cryptanalysis — achieves a complexity of 2²⁵⁵·¹⁹ with 2⁴ chosen
59
+ ciphertexts. This provides less than one bit of advantage over exhaustive
60
+ key search and has zero practical impact. Independent research conducted
61
+ against this implementation improved on the published result by −0.20 bits
62
+ through systematic parameter search, confirming no structural weakness
63
+ beyond what the published literature describes.
64
+
65
+ See: [`xero/BicliqueFinder/biclique_research.md`][biclique] and
66
+ [`leviathan-crypto/wiki/serpent_audit`][serpent-audit] for the full
67
+ analysis.
68
+
69
+ ### Authenticated Encryption by Default
70
+
71
+ Raw unauthenticated cipher modes (`SerpentCbc`, `SerpentCtr`) are exposed
72
+ for power users but are not the recommended entry point. The primary API
73
+ surfaces — `SerpentSeal`, `SerpentStream`, `SerpentStreamSealer` — are
74
+ authenticated by construction. `SerpentStreamSealer` satisfies the
75
+ Cryptographic Doom Principle: MAC verification is the unconditional gate on
76
+ the open path, decryption is unreachable until that gate clears, and
77
+ per-chunk HKDF key derivation with position-bound info extends this
78
+ guarantee to full stream integrity.
79
+
80
+ ### Dependency Management
81
+
82
+ The library has _zero_ runtime JavaScript dependencies by design.
83
+ `sideEffects: false` is enforced in `package.json`. Argon2id integration
84
+ is documented as an optional external dependency.
85
+ See:" [`leviathan-crypto/wiki/argon2id`][argon2id-wiki].
86
+
87
+ Build toolchain dependencies are pinned with exact version locks in
88
+ `bun.lock`. GitHub Actions workflows use SHA-pinned action references
89
+ throughout with no floating tags. Supply chain integrity is treated as a
90
+ first-class concern for a cryptography library.
91
+
92
+ ### Explicit Initialisation
93
+
94
+ No class silently auto-initialises. The [`init()`][init] gate is mandatory and
95
+ explicit, giving consumers full control over when WASM modules are loaded
96
+ and ensuring no hidden initialisation costs or race conditions. Classes
97
+ throw immediately if used before initialisation rather than failing
98
+ silently.
99
+
100
+ ### Agentic Development Contracts
101
+
102
+ All AI-assisted development on this repository operates under a strict
103
+ agentic contract defined in [`AGENTS.md`][agents]. The contract enforces
104
+ spec authority over planning documents, immutable test vectors, gate
105
+ discipline before extending any test suite, independent algorithm
106
+ derivation from published standards, and constant-time/wipe requirements
107
+ for all security-sensitive code paths. Agents are explicitly prohibited
108
+ from guessing cryptographic values or resolving spec ambiguities silently.
109
+
110
+ The contract has been verified against Claude, GitHub Copilot (VS Code),
111
+ OpenHands, Kilo Code, Cursor, Windsurf, and Aider. Configuration files for
112
+ each are present in the repository and all route to [`AGENTS.md`][agents]
113
+ as the single source of authority.
114
+
115
+ ---
116
+
117
+ ## Reporting a Vulnerability
118
+
119
+ > [!IMPORTANT]
120
+ > **_Please do not open a public issue for security vulnerabilities._**
121
+
122
+ ### Private Advisory (preferred)
123
+
124
+ Use GitHub's private vulnerability reporting form:
125
+ [https://github.com/xero/leviathan-crypto/security/advisories/new][advisory]
126
+
127
+ This opens a private channel between you and the maintainer. You will
128
+ receive a response ASAP. If the vulnerability is confirmed, a fix will be
129
+ prioritised and a coordinated disclosure timeline agreed upon before any
130
+ public advisory is published.
131
+
132
+ ### Direct Contact
133
+
134
+ If you prefer to contact the maintainer directly:
135
+
136
+ - **Email:** x﹫xero.style — PGP: [`0xAC1D0000`][pgp]
137
+ - **Matrix:** x0﹫rx.haunted.computer
138
+
139
+ > [!NOTE]
140
+ > Encrypted communication is welcome and _preferred_ for sensitive reports.
141
+
142
+ ### Scope
143
+
144
+ Reports are in scope for:
145
+
146
+ - Correctness bugs in cryptographic implementations (wrong output against
147
+ test vectors)
148
+ - Side-channel vulnerabilities (timing, memory access patterns)
149
+ - Authentication bypass in AEAD constructions
150
+ - Key material exposure or improper zeroing
151
+ - Supply chain issues (dependency tampering, workflow compromise)
152
+
153
+ Out of scope:
154
+
155
+ - Unpatched vulnerabilities in third-party packages not maintained by this
156
+ project
157
+ - Issues requiring physical access to the user's device
158
+
159
+ ---
160
+
161
+ [fips180]: https://csrc.nist.gov/publications/detail/fips/180/4/final
162
+ [fips202]: https://csrc.nist.gov/publications/detail/fips/202/final
163
+ [rfc8439]: https://www.rfc-editor.org/rfc/rfc8439
164
+ [rfc2104]: https://www.rfc-editor.org/rfc/rfc2104
165
+ [rfc5869]: https://www.rfc-editor.org/rfc/rfc5869
166
+ [serpent]: https://www.cl.cam.ac.uk/~rja14/Papers/serpent.pdf
167
+ [utils]: https://github.com/xero/leviathan-crypto/wiki/utils#constanttimeequal
168
+ [biclique]: https://github.com/xero/BicliqueFinder/blob/main/biclique-research.md
169
+ [serpent-audit]: https://github.com/xero/leviathan-crypto/wiki/serpent_audit
170
+ [argon2id-wiki]: https://github.com/xero/leviathan-crypto/wiki/argon2id
171
+ [init]: https://github.com/xero/leviathan-crypto/wiki/init
172
+ [agents]: https://github.com/xero/leviathan-crypto/blob/main/AGENTS.md
173
+ [advisory]: https://github.com/xero/leviathan-crypto/security/advisories/new
174
+ [pgp]: https://0w.nz/pgp.pub
Binary file
@@ -0,0 +1,49 @@
1
+ import type { Mode, InitOpts } from '../init.js';
2
+ export declare function chacha20Init(mode?: Mode, opts?: InitOpts): Promise<void>;
3
+ export declare class ChaCha20 {
4
+ private readonly x;
5
+ constructor();
6
+ beginEncrypt(key: Uint8Array, nonce: Uint8Array): void;
7
+ encryptChunk(chunk: Uint8Array): Uint8Array;
8
+ beginDecrypt(key: Uint8Array, nonce: Uint8Array): void;
9
+ decryptChunk(chunk: Uint8Array): Uint8Array;
10
+ dispose(): void;
11
+ }
12
+ export declare class Poly1305 {
13
+ private readonly x;
14
+ constructor();
15
+ mac(key: Uint8Array, msg: Uint8Array): Uint8Array;
16
+ dispose(): void;
17
+ }
18
+ /**
19
+ * ChaCha20-Poly1305 AEAD (RFC 8439 §2.8).
20
+ *
21
+ * `decrypt()` uses constant-time tag comparison — XOR-accumulate pattern,
22
+ * no early return on mismatch. Plaintext is never returned on failure.
23
+ */
24
+ export declare class ChaCha20Poly1305 {
25
+ private readonly x;
26
+ 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;
32
+ dispose(): void;
33
+ }
34
+ /**
35
+ * XChaCha20-Poly1305 AEAD (IETF draft-irtf-cfrg-xchacha).
36
+ *
37
+ * Recommended authenticated encryption primitive for most use cases.
38
+ * Uses a 24-byte nonce — safe for random generation via crypto.getRandomValues.
39
+ *
40
+ * `decrypt()` constant-time guarantee is inherited from the inner AEAD path.
41
+ */
42
+ export declare class XChaCha20Poly1305 {
43
+ private readonly x;
44
+ constructor();
45
+ encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array;
46
+ decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array;
47
+ dispose(): void;
48
+ }
49
+ export declare function _chachaReady(): boolean;
@@ -0,0 +1,177 @@
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/index.ts
23
+ //
24
+ // Public API classes for the ChaCha20 WASM module.
25
+ // Uses the init() module cache — call init('chacha20') before constructing.
26
+ import { getInstance, initModule } from '../init.js';
27
+ import { aeadEncrypt, aeadDecrypt, xcEncrypt, xcDecrypt } from './ops.js';
28
+ const _embedded = () => import('../embedded/chacha.js').then(m => m.WASM_BASE64);
29
+ export async function chacha20Init(mode = 'embedded', opts) {
30
+ return initModule('chacha20', _embedded, mode, opts);
31
+ }
32
+ function getExports() {
33
+ return getInstance('chacha20').exports;
34
+ }
35
+ // ── ChaCha20 ──────────────────────────────────────────────────────────────────
36
+ export class ChaCha20 {
37
+ x;
38
+ constructor() {
39
+ this.x = getExports();
40
+ }
41
+ beginEncrypt(key, nonce) {
42
+ if (key.length !== 32)
43
+ throw new RangeError(`ChaCha20 key must be 32 bytes (got ${key.length})`);
44
+ if (nonce.length !== 12)
45
+ throw new RangeError(`ChaCha20 nonce must be 12 bytes (got ${nonce.length})`);
46
+ const mem = new Uint8Array(this.x.memory.buffer);
47
+ mem.set(key, this.x.getKeyOffset());
48
+ mem.set(nonce, this.x.getChachaNonceOffset());
49
+ this.x.chachaSetCounter(1);
50
+ this.x.chachaLoadKey();
51
+ }
52
+ encryptChunk(chunk) {
53
+ const maxChunk = this.x.getChunkSize();
54
+ if (chunk.length > maxChunk)
55
+ throw new RangeError(`chunk exceeds maximum size of ${maxChunk} bytes — split into smaller chunks`);
56
+ const mem = new Uint8Array(this.x.memory.buffer);
57
+ const ptOff = this.x.getChunkPtOffset();
58
+ const ctOff = this.x.getChunkCtOffset();
59
+ mem.set(chunk, ptOff);
60
+ this.x.chachaEncryptChunk(chunk.length);
61
+ return mem.slice(ctOff, ctOff + chunk.length);
62
+ }
63
+ beginDecrypt(key, nonce) {
64
+ this.beginEncrypt(key, nonce);
65
+ }
66
+ decryptChunk(chunk) {
67
+ return this.encryptChunk(chunk);
68
+ }
69
+ dispose() {
70
+ this.x.wipeBuffers();
71
+ }
72
+ }
73
+ // ── Poly1305 ──────────────────────────────────────────────────────────────────
74
+ export class Poly1305 {
75
+ x;
76
+ constructor() {
77
+ this.x = getExports();
78
+ }
79
+ mac(key, msg) {
80
+ if (key.length !== 32)
81
+ throw new RangeError(`Poly1305 key must be 32 bytes (got ${key.length})`);
82
+ const mem = new Uint8Array(this.x.memory.buffer);
83
+ const keyOff = this.x.getPolyKeyOffset();
84
+ const msgOff = this.x.getPolyMsgOffset();
85
+ const tagOff = this.x.getPolyTagOffset();
86
+ mem.set(key, keyOff);
87
+ this.x.polyInit();
88
+ let pos = 0;
89
+ while (pos < msg.length) {
90
+ const chunk = Math.min(64, msg.length - pos);
91
+ mem.set(msg.subarray(pos, pos + chunk), msgOff);
92
+ this.x.polyUpdate(chunk);
93
+ pos += chunk;
94
+ }
95
+ this.x.polyFinal();
96
+ return new Uint8Array(this.x.memory.buffer).slice(tagOff, tagOff + 16);
97
+ }
98
+ dispose() {
99
+ this.x.wipeBuffers();
100
+ }
101
+ }
102
+ // ── ChaCha20Poly1305 ─────────────────────────────────────────────────────────
103
+ /**
104
+ * ChaCha20-Poly1305 AEAD (RFC 8439 §2.8).
105
+ *
106
+ * `decrypt()` uses constant-time tag comparison — XOR-accumulate pattern,
107
+ * no early return on mismatch. Plaintext is never returned on failure.
108
+ */
109
+ export class ChaCha20Poly1305 {
110
+ x;
111
+ constructor() {
112
+ this.x = getExports();
113
+ }
114
+ encrypt(key, nonce, plaintext, aad = new Uint8Array(0)) {
115
+ if (key.length !== 32)
116
+ throw new RangeError(`key must be 32 bytes (got ${key.length})`);
117
+ if (nonce.length !== 12)
118
+ throw new RangeError(`nonce must be 12 bytes (got ${nonce.length})`);
119
+ return aeadEncrypt(this.x, key, nonce, plaintext, aad);
120
+ }
121
+ decrypt(key, nonce, ciphertext, tag, aad = new Uint8Array(0)) {
122
+ if (key.length !== 32)
123
+ throw new RangeError(`key must be 32 bytes (got ${key.length})`);
124
+ if (nonce.length !== 12)
125
+ throw new RangeError(`nonce must be 12 bytes (got ${nonce.length})`);
126
+ if (tag.length !== 16)
127
+ throw new RangeError(`tag must be 16 bytes (got ${tag.length})`);
128
+ return aeadDecrypt(this.x, key, nonce, ciphertext, tag, aad);
129
+ }
130
+ dispose() {
131
+ this.x.wipeBuffers();
132
+ }
133
+ }
134
+ // ── XChaCha20Poly1305 ────────────────────────────────────────────────────────
135
+ /**
136
+ * XChaCha20-Poly1305 AEAD (IETF draft-irtf-cfrg-xchacha).
137
+ *
138
+ * Recommended authenticated encryption primitive for most use cases.
139
+ * Uses a 24-byte nonce — safe for random generation via crypto.getRandomValues.
140
+ *
141
+ * `decrypt()` constant-time guarantee is inherited from the inner AEAD path.
142
+ */
143
+ export class XChaCha20Poly1305 {
144
+ x;
145
+ constructor() {
146
+ this.x = getExports();
147
+ }
148
+ encrypt(key, nonce, plaintext, aad = new Uint8Array(0)) {
149
+ if (key.length !== 32)
150
+ throw new RangeError(`key must be 32 bytes (got ${key.length})`);
151
+ if (nonce.length !== 24)
152
+ throw new RangeError(`XChaCha20 nonce must be 24 bytes (got ${nonce.length})`);
153
+ return xcEncrypt(this.x, key, nonce, plaintext, aad);
154
+ }
155
+ decrypt(key, nonce, ciphertext, aad = new Uint8Array(0)) {
156
+ if (key.length !== 32)
157
+ throw new RangeError(`key must be 32 bytes (got ${key.length})`);
158
+ if (nonce.length !== 24)
159
+ throw new RangeError(`XChaCha20 nonce must be 24 bytes (got ${nonce.length})`);
160
+ if (ciphertext.length < 16)
161
+ throw new RangeError(`ciphertext too short — must include 16-byte tag (got ${ciphertext.length})`);
162
+ return xcDecrypt(this.x, key, nonce, ciphertext, aad);
163
+ }
164
+ dispose() {
165
+ this.x.wipeBuffers();
166
+ }
167
+ }
168
+ // ── Ready check ──────────────────────────────────────────────────────────────
169
+ export function _chachaReady() {
170
+ try {
171
+ getInstance('chacha20');
172
+ return true;
173
+ }
174
+ catch {
175
+ return false;
176
+ }
177
+ }
@@ -0,0 +1,16 @@
1
+ import type { ChaChaExports } from './types.js';
2
+ /** ChaCha20-Poly1305 AEAD encrypt (RFC 8439 §2.8). */
3
+ export declare function aeadEncrypt(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): {
4
+ ciphertext: Uint8Array;
5
+ tag: Uint8Array;
6
+ };
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;
9
+ /** HChaCha20 subkey derivation — first 16 bytes of nonce. */
10
+ export declare function deriveSubkey(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array): Uint8Array;
11
+ /** Build inner 12-byte nonce from bytes 16–23 of XChaCha nonce. */
12
+ export declare function innerNonce(nonce: Uint8Array): Uint8Array;
13
+ /** XChaCha20-Poly1305 encrypt → ciphertext || tag. */
14
+ export declare function xcEncrypt(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Uint8Array;
15
+ /** XChaCha20-Poly1305 decrypt → plaintext (throws on auth failure). */
16
+ export declare function xcDecrypt(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad: Uint8Array): Uint8Array;
@@ -0,0 +1,146 @@
1
+ // src/ts/chacha20/ops.ts
2
+ //
3
+ // Raw XChaCha20-Poly1305 operations — standalone functions that take
4
+ // ChaChaExports explicitly. Used by both the class wrappers (index.ts)
5
+ // and the pool worker (pool.worker.ts), eliminating duplication.
6
+ import { constantTimeEqual } from '../utils.js';
7
+ // ── Module-private helpers ───────────────────────────────────────────────────
8
+ function polyFeed(x, data) {
9
+ if (data.length === 0)
10
+ return;
11
+ const mem = new Uint8Array(x.memory.buffer);
12
+ const msgOff = x.getPolyMsgOffset();
13
+ let pos = 0;
14
+ while (pos < data.length) {
15
+ const chunk = Math.min(64, data.length - pos);
16
+ mem.set(data.subarray(pos, pos + chunk), msgOff);
17
+ x.polyUpdate(chunk);
18
+ pos += chunk;
19
+ }
20
+ }
21
+ function lenBlock(aadLen, ctLen) {
22
+ 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
+ }
33
+ return b;
34
+ }
35
+ // ── Inner AEAD (12-byte nonce) ───────────────────────────────────────────────
36
+ /** ChaCha20-Poly1305 AEAD encrypt (RFC 8439 §2.8). */
37
+ export function aeadEncrypt(x, key, nonce, plaintext, aad) {
38
+ const maxChunk = x.getChunkSize();
39
+ if (plaintext.length > maxChunk)
40
+ throw new RangeError(`plaintext exceeds ${maxChunk} bytes — split into smaller chunks`);
41
+ const mem = new Uint8Array(x.memory.buffer);
42
+ // Step 1: Generate Poly1305 one-time key at counter=0 (RFC 8439 §2.6)
43
+ mem.set(key, x.getKeyOffset());
44
+ mem.set(nonce, x.getChachaNonceOffset());
45
+ x.chachaSetCounter(1);
46
+ x.chachaLoadKey();
47
+ x.chachaGenPolyKey();
48
+ // Step 2: Initialise Poly1305
49
+ x.polyInit();
50
+ // Step 3: MAC AAD + pad
51
+ polyFeed(x, aad);
52
+ const aadPad = (16 - aad.length % 16) % 16;
53
+ if (aadPad > 0)
54
+ polyFeed(x, new Uint8Array(aadPad));
55
+ // Step 4: Re-init ChaCha20 at counter=1
56
+ x.chachaSetCounter(1);
57
+ x.chachaLoadKey();
58
+ // Step 5: Encrypt
59
+ mem.set(plaintext, x.getChunkPtOffset());
60
+ x.chachaEncryptChunk(plaintext.length);
61
+ const ctOff = x.getChunkCtOffset();
62
+ const ciphertext = new Uint8Array(x.memory.buffer).slice(ctOff, ctOff + plaintext.length);
63
+ // Step 6: MAC ciphertext + pad
64
+ polyFeed(x, ciphertext);
65
+ const ctPad = (16 - plaintext.length % 16) % 16;
66
+ if (ctPad > 0)
67
+ polyFeed(x, new Uint8Array(ctPad));
68
+ // Step 7: MAC length footer
69
+ polyFeed(x, lenBlock(aad.length, plaintext.length));
70
+ // Step 8: Finalise
71
+ x.polyFinal();
72
+ const tagOff = x.getPolyTagOffset();
73
+ const tag = new Uint8Array(x.memory.buffer).slice(tagOff, tagOff + 16);
74
+ return { ciphertext, tag };
75
+ }
76
+ /** ChaCha20-Poly1305 AEAD decrypt (RFC 8439 §2.8). Constant-time tag comparison. */
77
+ export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad) {
78
+ const maxChunk = x.getChunkSize();
79
+ if (ciphertext.length > maxChunk)
80
+ throw new RangeError(`ciphertext exceeds ${maxChunk} bytes — split into smaller chunks`);
81
+ const mem = new Uint8Array(x.memory.buffer);
82
+ // Compute expected tag
83
+ mem.set(key, x.getKeyOffset());
84
+ mem.set(nonce, x.getChachaNonceOffset());
85
+ x.chachaSetCounter(1);
86
+ x.chachaLoadKey();
87
+ x.chachaGenPolyKey();
88
+ x.polyInit();
89
+ polyFeed(x, aad);
90
+ const aadPad = (16 - aad.length % 16) % 16;
91
+ if (aadPad > 0)
92
+ polyFeed(x, new Uint8Array(aadPad));
93
+ polyFeed(x, ciphertext);
94
+ const ctPad = (16 - ciphertext.length % 16) % 16;
95
+ if (ctPad > 0)
96
+ polyFeed(x, new Uint8Array(ctPad));
97
+ polyFeed(x, lenBlock(aad.length, ciphertext.length));
98
+ x.polyFinal();
99
+ // Constant-time tag comparison
100
+ const tagOff = x.getPolyTagOffset();
101
+ const expectedTag = new Uint8Array(x.memory.buffer).slice(tagOff, tagOff + 16);
102
+ if (!constantTimeEqual(expectedTag, tag))
103
+ throw new Error('ChaCha20Poly1305: authentication failed');
104
+ // Decrypt only after authentication succeeds
105
+ x.chachaSetCounter(1);
106
+ x.chachaLoadKey();
107
+ new Uint8Array(x.memory.buffer).set(ciphertext, x.getChunkPtOffset());
108
+ x.chachaEncryptChunk(ciphertext.length);
109
+ const ptOff = x.getChunkCtOffset();
110
+ return new Uint8Array(x.memory.buffer).slice(ptOff, ptOff + ciphertext.length);
111
+ }
112
+ // ── XChaCha20 helpers ────────────────────────────────────────────────────────
113
+ /** HChaCha20 subkey derivation — first 16 bytes of nonce. */
114
+ export function deriveSubkey(x, key, nonce) {
115
+ const mem = new Uint8Array(x.memory.buffer);
116
+ mem.set(key, x.getKeyOffset());
117
+ mem.set(nonce.subarray(0, 16), x.getXChaChaNonceOffset());
118
+ x.hchacha20();
119
+ const off = x.getXChaChaSubkeyOffset();
120
+ return new Uint8Array(x.memory.buffer).slice(off, off + 32);
121
+ }
122
+ /** Build inner 12-byte nonce from bytes 16–23 of XChaCha nonce. */
123
+ export function innerNonce(nonce) {
124
+ const n = new Uint8Array(12);
125
+ n.set(nonce.subarray(16, 24), 4);
126
+ return n;
127
+ }
128
+ // ── Full XChaCha20-Poly1305 ──────────────────────────────────────────────────
129
+ /** XChaCha20-Poly1305 encrypt → ciphertext || tag. */
130
+ export function xcEncrypt(x, key, nonce, plaintext, aad) {
131
+ const subkey = deriveSubkey(x, key, nonce);
132
+ const inner = innerNonce(nonce);
133
+ const { ciphertext, tag } = aeadEncrypt(x, subkey, inner, plaintext, aad);
134
+ const result = new Uint8Array(ciphertext.length + 16);
135
+ result.set(ciphertext);
136
+ result.set(tag, ciphertext.length);
137
+ return result;
138
+ }
139
+ /** XChaCha20-Poly1305 decrypt → plaintext (throws on auth failure). */
140
+ export function xcDecrypt(x, key, nonce, ciphertext, aad) {
141
+ const ct = ciphertext.subarray(0, ciphertext.length - 16);
142
+ const tag = ciphertext.subarray(ciphertext.length - 16);
143
+ const subkey = deriveSubkey(x, key, nonce);
144
+ const inner = innerNonce(nonce);
145
+ return aeadDecrypt(x, subkey, inner, ct, tag, aad);
146
+ }
@@ -0,0 +1,52 @@
1
+ export interface PoolOpts {
2
+ /** Number of workers. Default: navigator.hardwareConcurrency ?? 4 */
3
+ workers?: number;
4
+ }
5
+ /**
6
+ * Parallel worker pool for XChaCha20-Poly1305 AEAD.
7
+ *
8
+ * Each worker owns its own `WebAssembly.Instance` with isolated linear memory.
9
+ * Jobs are dispatched round-robin to idle workers; excess jobs queue until a
10
+ * worker frees up.
11
+ *
12
+ * **Warning:** Input buffers (`key`, `nonce`, `plaintext`/`ciphertext`, `aad`)
13
+ * are transferred to the worker and neutered on the calling side. The caller
14
+ * must copy any buffer they need to retain after calling `encrypt()`/`decrypt()`.
15
+ */
16
+ export declare class XChaCha20Poly1305Pool {
17
+ private readonly _workers;
18
+ private readonly _idle;
19
+ private readonly _queue;
20
+ private readonly _pending;
21
+ private _nextId;
22
+ private _disposed;
23
+ private constructor();
24
+ /**
25
+ * Create a new pool. Requires `init(['chacha20'])` to have been called.
26
+ * Compiles the WASM module once and distributes it to all workers.
27
+ */
28
+ static create(opts?: PoolOpts): Promise<XChaCha20Poly1305Pool>;
29
+ /**
30
+ * Encrypt plaintext with XChaCha20-Poly1305.
31
+ * Returns `ciphertext || tag` (plaintext.length + 16 bytes).
32
+ *
33
+ * **Warning:** All input buffers are transferred and neutered after dispatch.
34
+ */
35
+ encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Promise<Uint8Array>;
36
+ /**
37
+ * Decrypt ciphertext with XChaCha20-Poly1305.
38
+ * Input is `ciphertext || tag` (at least 16 bytes).
39
+ *
40
+ * **Warning:** All input buffers are transferred and neutered after dispatch.
41
+ */
42
+ decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Promise<Uint8Array>;
43
+ /** Terminates all workers. Rejects all pending and queued jobs. */
44
+ dispose(): void;
45
+ /** Number of workers in the pool. */
46
+ get size(): number;
47
+ /** Number of jobs currently queued (waiting for a free worker). */
48
+ get queueDepth(): number;
49
+ private _dispatch;
50
+ private _send;
51
+ private _onMessage;
52
+ }