leviathan-crypto 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CLAUDE.md +171 -7
  2. package/LICENSE +4 -0
  3. package/README.md +109 -54
  4. package/SECURITY.md +125 -233
  5. package/dist/chacha20/cipher-suite.d.ts +10 -0
  6. package/dist/chacha20/cipher-suite.js +66 -2
  7. package/dist/chacha20/generator.d.ts +12 -0
  8. package/dist/chacha20/generator.js +91 -0
  9. package/dist/chacha20/index.d.ts +97 -1
  10. package/dist/chacha20/index.js +139 -11
  11. package/dist/chacha20/ops.d.ts +57 -6
  12. package/dist/chacha20/ops.js +93 -13
  13. package/dist/chacha20/pool-worker.js +12 -0
  14. package/dist/chacha20/types.d.ts +1 -32
  15. package/dist/ct-wasm.js +1 -1
  16. package/dist/ct.wasm +0 -0
  17. package/dist/docs/aead.md +69 -26
  18. package/dist/docs/architecture.md +600 -520
  19. package/dist/docs/argon2id.md +17 -14
  20. package/dist/docs/chacha20.md +146 -39
  21. package/dist/docs/exports.md +46 -10
  22. package/dist/docs/fortuna.md +339 -122
  23. package/dist/docs/init.md +24 -25
  24. package/dist/docs/loader.md +142 -47
  25. package/dist/docs/serpent.md +139 -41
  26. package/dist/docs/sha2.md +77 -19
  27. package/dist/docs/sha3.md +81 -15
  28. package/dist/docs/types.md +156 -15
  29. package/dist/docs/utils.md +171 -81
  30. package/dist/embedded/chacha20-pool-worker.d.ts +1 -0
  31. package/dist/embedded/chacha20-pool-worker.js +5 -0
  32. package/dist/embedded/kyber.d.ts +1 -1
  33. package/dist/embedded/kyber.js +1 -1
  34. package/dist/embedded/serpent-pool-worker.d.ts +1 -0
  35. package/dist/embedded/serpent-pool-worker.js +5 -0
  36. package/dist/embedded/serpent.d.ts +1 -1
  37. package/dist/embedded/serpent.js +1 -1
  38. package/dist/fortuna.d.ts +14 -8
  39. package/dist/fortuna.js +144 -50
  40. package/dist/index.d.ts +8 -6
  41. package/dist/index.js +6 -5
  42. package/dist/init.d.ts +0 -2
  43. package/dist/init.js +83 -3
  44. package/dist/kyber/indcpa.js +4 -4
  45. package/dist/kyber/index.js +25 -5
  46. package/dist/kyber/kem.js +56 -1
  47. package/dist/kyber/suite.d.ts +1 -2
  48. package/dist/kyber/suite.js +1 -0
  49. package/dist/kyber/types.d.ts +1 -0
  50. package/dist/kyber/validate.d.ts +8 -4
  51. package/dist/kyber/validate.js +18 -14
  52. package/dist/kyber.wasm +0 -0
  53. package/dist/loader.d.ts +7 -2
  54. package/dist/loader.js +25 -28
  55. package/dist/ratchet/index.d.ts +6 -0
  56. package/dist/ratchet/index.js +37 -0
  57. package/dist/ratchet/kdf-chain.d.ts +13 -0
  58. package/dist/ratchet/kdf-chain.js +85 -0
  59. package/dist/ratchet/ratchet-keypair.d.ts +9 -0
  60. package/dist/ratchet/ratchet-keypair.js +61 -0
  61. package/dist/ratchet/root-kdf.d.ts +4 -0
  62. package/dist/ratchet/root-kdf.js +124 -0
  63. package/dist/ratchet/skipped-key-store.d.ts +14 -0
  64. package/dist/ratchet/skipped-key-store.js +154 -0
  65. package/dist/ratchet/types.d.ts +36 -0
  66. package/dist/ratchet/types.js +26 -0
  67. package/dist/serpent/cipher-suite.d.ts +10 -0
  68. package/dist/serpent/cipher-suite.js +136 -50
  69. package/dist/serpent/generator.d.ts +12 -0
  70. package/dist/serpent/generator.js +97 -0
  71. package/dist/serpent/index.d.ts +61 -1
  72. package/dist/serpent/index.js +92 -7
  73. package/dist/serpent/pool-worker.js +25 -95
  74. package/dist/serpent/serpent-cbc.d.ts +14 -4
  75. package/dist/serpent/serpent-cbc.js +58 -34
  76. package/dist/serpent/shared-ops.d.ts +83 -0
  77. package/dist/serpent/shared-ops.js +213 -0
  78. package/dist/serpent/types.d.ts +1 -5
  79. package/dist/serpent.wasm +0 -0
  80. package/dist/sha2/hash.d.ts +2 -0
  81. package/dist/sha2/hash.js +53 -0
  82. package/dist/sha2/index.d.ts +1 -0
  83. package/dist/sha2/index.js +15 -1
  84. package/dist/sha3/hash.d.ts +2 -0
  85. package/dist/sha3/hash.js +53 -0
  86. package/dist/sha3/index.d.ts +17 -2
  87. package/dist/sha3/index.js +79 -7
  88. package/dist/stream/header.js +5 -5
  89. package/dist/stream/open-stream.js +36 -14
  90. package/dist/stream/seal-stream-pool.d.ts +1 -0
  91. package/dist/stream/seal-stream-pool.js +47 -8
  92. package/dist/stream/seal-stream.js +29 -11
  93. package/dist/stream/types.d.ts +1 -0
  94. package/dist/types.d.ts +21 -0
  95. package/dist/utils.d.ts +7 -8
  96. package/dist/utils.js +73 -40
  97. package/dist/wasm-source.d.ts +9 -8
  98. package/package.json +79 -64
@@ -1,21 +1,72 @@
1
1
  import type { WasmSource } from '../wasm-source.js';
2
2
  import { AuthenticationError } from '../errors.js';
3
3
  export { AuthenticationError };
4
+ /**
5
+ * Load and initialise the ChaCha20 WASM module from `source`.
6
+ * Must be called before constructing any ChaCha20 class.
7
+ * @param source WASM binary — gzip+base64 string, URL, ArrayBuffer, Uint8Array,
8
+ * pre-compiled WebAssembly.Module, Response, or Promise<Response>
9
+ */
4
10
  export declare function chacha20Init(source: WasmSource): Promise<void>;
5
11
  export type { WasmSource };
12
+ /**
13
+ * Raw ChaCha20 stream cipher (RFC 8439 §2.4).
14
+ *
15
+ * Holds exclusive access to the `chacha20` WASM module from construction
16
+ * until `dispose()`. Constructing a second ChaCha20 or any other chacha20
17
+ * user while this instance is live throws. Call `dispose()` when done.
18
+ */
6
19
  export declare class ChaCha20 {
7
20
  private readonly x;
21
+ private _tok;
8
22
  constructor();
23
+ /**
24
+ * Load key and nonce into WASM state and set the block counter to 1.
25
+ * Must be called before each message (RFC 8439 §2.4).
26
+ * @param key 32 bytes
27
+ * @param nonce 12 bytes — must be unique per (key, message)
28
+ */
9
29
  beginEncrypt(key: Uint8Array, nonce: Uint8Array): void;
30
+ /**
31
+ * XOR `chunk` with the next keystream block(s). Counter advances automatically.
32
+ * @param chunk Plaintext chunk — must not exceed WASM CHUNK_SIZE
33
+ * @returns Ciphertext of the same length
34
+ */
10
35
  encryptChunk(chunk: Uint8Array): Uint8Array;
36
+ /**
37
+ * Alias for `beginEncrypt` — ChaCha20 is a stream cipher (symmetric).
38
+ * @param key 32 bytes
39
+ * @param nonce 12 bytes — must match the value used to encrypt
40
+ */
11
41
  beginDecrypt(key: Uint8Array, nonce: Uint8Array): void;
42
+ /**
43
+ * Alias for `encryptChunk` — ChaCha20 is a stream cipher (symmetric).
44
+ * @param chunk Ciphertext chunk
45
+ * @returns Plaintext of the same length
46
+ */
12
47
  decryptChunk(chunk: Uint8Array): Uint8Array;
48
+ /** Wipe WASM state and release exclusive module access. Idempotent. */
13
49
  dispose(): void;
14
50
  }
51
+ /**
52
+ * Poly1305 one-time MAC (RFC 8439 §2.5).
53
+ *
54
+ * Atomic (stateless) class: each `mac()` call is independent.
55
+ * The key must never be reused across messages. For most use cases, prefer
56
+ * `ChaCha20Poly1305` or `XChaCha20Poly1305` which manage the one-time key
57
+ * automatically.
58
+ */
15
59
  export declare class Poly1305 {
16
60
  private readonly x;
17
61
  constructor();
62
+ /**
63
+ * Compute a 16-byte Poly1305 MAC for `msg` using `key`.
64
+ * @param key 32-byte one-time key — must not be reused across messages
65
+ * @param msg Message to authenticate
66
+ * @returns 16-byte Poly1305 tag
67
+ */
18
68
  mac(key: Uint8Array, msg: Uint8Array): Uint8Array;
69
+ /** Wipe WASM MAC state. */
19
70
  dispose(): void;
20
71
  }
21
72
  /**
@@ -34,9 +85,31 @@ export declare class ChaCha20Poly1305 {
34
85
  private readonly x;
35
86
  private _used;
36
87
  constructor();
88
+ /**
89
+ * Encrypt and authenticate `plaintext` with ChaCha20-Poly1305 (RFC 8439 §2.8).
90
+ *
91
+ * **Single-use guard:** `encrypt()` may only be called once per instance.
92
+ * Any throw — including validation errors — permanently locks this instance.
93
+ * Always create a new `ChaCha20Poly1305` per message.
94
+ * @param key 32 bytes
95
+ * @param nonce 12 bytes — must be unique per (key, message)
96
+ * @param plaintext Data to encrypt
97
+ * @param aad Additional authenticated data (optional)
98
+ * @returns Ciphertext || 16-byte Poly1305 tag
99
+ */
37
100
  encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array;
101
+ /**
102
+ * Verify and decrypt a ChaCha20-Poly1305 ciphertext (RFC 8439 §2.8).
103
+ * Throws `AuthenticationError` if the tag does not match.
104
+ * @param key 32 bytes
105
+ * @param nonce 12 bytes — must match the value used to encrypt
106
+ * @param ciphertext Ciphertext || 16-byte tag (combined format from `encrypt`)
107
+ * @param aad Additional authenticated data (optional)
108
+ * @returns Plaintext
109
+ */
38
110
  decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, // ciphertext || tag(16) combined
39
111
  aad?: Uint8Array): Uint8Array;
112
+ /** Wipe WASM cipher and MAC state. */
40
113
  dispose(): void;
41
114
  }
42
115
  /**
@@ -54,9 +127,32 @@ export declare class XChaCha20Poly1305 {
54
127
  private readonly x;
55
128
  private _used;
56
129
  constructor();
130
+ /**
131
+ * Encrypt and authenticate `plaintext` with XChaCha20-Poly1305
132
+ * (draft-irtf-cfrg-xchacha). Recommended for general-purpose AEAD.
133
+ *
134
+ * **Single-use guard:** `encrypt()` may only be called once per instance.
135
+ * Any throw — including validation errors — permanently locks this instance.
136
+ * Always create a new `XChaCha20Poly1305` per message to prevent nonce reuse.
137
+ * @param key 32 bytes
138
+ * @param nonce 24 bytes — safe to generate randomly via `randomBytes(24)`
139
+ * @param plaintext Data to encrypt
140
+ * @param aad Additional authenticated data (optional)
141
+ * @returns Ciphertext || 16-byte Poly1305 tag
142
+ */
57
143
  encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array;
144
+ /**
145
+ * Verify and decrypt an XChaCha20-Poly1305 ciphertext.
146
+ * Throws `AuthenticationError` if the tag does not match.
147
+ * @param key 32 bytes
148
+ * @param nonce 24 bytes — must match the value used to encrypt
149
+ * @param ciphertext Ciphertext || 16-byte tag (combined format from `encrypt`)
150
+ * @param aad Additional authenticated data (optional)
151
+ * @returns Plaintext
152
+ */
58
153
  decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array;
154
+ /** Wipe WASM cipher and MAC state. */
59
155
  dispose(): void;
60
156
  }
61
157
  export { XChaCha20Cipher } from './cipher-suite.js';
62
- export declare function _chachaReady(): boolean;
158
+ export { ChaCha20Generator } from './generator.js';
@@ -23,23 +23,47 @@
23
23
  //
24
24
  // Public API classes for the ChaCha20 WASM module.
25
25
  // Uses the init() module cache — call chacha20Init(source) before constructing.
26
- import { getInstance, initModule } from '../init.js';
26
+ import { getInstance, initModule, _acquireModule, _releaseModule, _assertNotOwned } from '../init.js';
27
27
  import { aeadEncrypt, aeadDecrypt, xcEncrypt, xcDecrypt } from './ops.js';
28
28
  import { AuthenticationError } from '../errors.js';
29
29
  export { AuthenticationError };
30
+ /**
31
+ * Load and initialise the ChaCha20 WASM module from `source`.
32
+ * Must be called before constructing any ChaCha20 class.
33
+ * @param source WASM binary — gzip+base64 string, URL, ArrayBuffer, Uint8Array,
34
+ * pre-compiled WebAssembly.Module, Response, or Promise<Response>
35
+ */
30
36
  export async function chacha20Init(source) {
31
37
  return initModule('chacha20', source);
32
38
  }
39
+ /** Returns the raw chacha20 WASM export object. @internal */
33
40
  function getExports() {
34
41
  return getInstance('chacha20').exports;
35
42
  }
36
- // ── ChaCha20 ──────────────────────────────────────────────────────────────────
43
+ // ── ChaCha20 ────────────────────────────────────────────────────────────────
44
+ /**
45
+ * Raw ChaCha20 stream cipher (RFC 8439 §2.4).
46
+ *
47
+ * Holds exclusive access to the `chacha20` WASM module from construction
48
+ * until `dispose()`. Constructing a second ChaCha20 or any other chacha20
49
+ * user while this instance is live throws. Call `dispose()` when done.
50
+ */
37
51
  export class ChaCha20 {
38
52
  x;
53
+ _tok;
39
54
  constructor() {
40
55
  this.x = getExports();
56
+ this._tok = _acquireModule('chacha20');
41
57
  }
58
+ /**
59
+ * Load key and nonce into WASM state and set the block counter to 1.
60
+ * Must be called before each message (RFC 8439 §2.4).
61
+ * @param key 32 bytes
62
+ * @param nonce 12 bytes — must be unique per (key, message)
63
+ */
42
64
  beginEncrypt(key, nonce) {
65
+ if (this._tok === undefined)
66
+ throw new Error('ChaCha20: instance has been disposed');
43
67
  if (key.length !== 32)
44
68
  throw new RangeError(`ChaCha20 key must be 32 bytes (got ${key.length})`);
45
69
  if (nonce.length !== 12)
@@ -50,7 +74,14 @@ export class ChaCha20 {
50
74
  this.x.chachaSetCounter(1);
51
75
  this.x.chachaLoadKey();
52
76
  }
77
+ /**
78
+ * XOR `chunk` with the next keystream block(s). Counter advances automatically.
79
+ * @param chunk Plaintext chunk — must not exceed WASM CHUNK_SIZE
80
+ * @returns Ciphertext of the same length
81
+ */
53
82
  encryptChunk(chunk) {
83
+ if (this._tok === undefined)
84
+ throw new Error('ChaCha20: instance has been disposed');
54
85
  const maxChunk = this.x.getChunkSize();
55
86
  if (chunk.length > maxChunk)
56
87
  throw new RangeError(`chunk exceeds maximum size of ${maxChunk} bytes — split into smaller chunks`);
@@ -61,23 +92,57 @@ export class ChaCha20 {
61
92
  this.x.chachaEncryptChunk_simd(chunk.length);
62
93
  return mem.slice(ctOff, ctOff + chunk.length);
63
94
  }
95
+ /**
96
+ * Alias for `beginEncrypt` — ChaCha20 is a stream cipher (symmetric).
97
+ * @param key 32 bytes
98
+ * @param nonce 12 bytes — must match the value used to encrypt
99
+ */
64
100
  beginDecrypt(key, nonce) {
65
101
  this.beginEncrypt(key, nonce);
66
102
  }
103
+ /**
104
+ * Alias for `encryptChunk` — ChaCha20 is a stream cipher (symmetric).
105
+ * @param chunk Ciphertext chunk
106
+ * @returns Plaintext of the same length
107
+ */
67
108
  decryptChunk(chunk) {
68
109
  return this.encryptChunk(chunk);
69
110
  }
111
+ /** Wipe WASM state and release exclusive module access. Idempotent. */
70
112
  dispose() {
71
- this.x.wipeBuffers();
113
+ if (this._tok === undefined)
114
+ return;
115
+ try {
116
+ this.x.wipeBuffers();
117
+ }
118
+ finally {
119
+ _releaseModule('chacha20', this._tok);
120
+ this._tok = undefined;
121
+ }
72
122
  }
73
123
  }
74
- // ── Poly1305 ──────────────────────────────────────────────────────────────────
124
+ // ── Poly1305 ────────────────────────────────────────────────────────────────
125
+ /**
126
+ * Poly1305 one-time MAC (RFC 8439 §2.5).
127
+ *
128
+ * Atomic (stateless) class: each `mac()` call is independent.
129
+ * The key must never be reused across messages. For most use cases, prefer
130
+ * `ChaCha20Poly1305` or `XChaCha20Poly1305` which manage the one-time key
131
+ * automatically.
132
+ */
75
133
  export class Poly1305 {
76
134
  x;
77
135
  constructor() {
78
136
  this.x = getExports();
79
137
  }
138
+ /**
139
+ * Compute a 16-byte Poly1305 MAC for `msg` using `key`.
140
+ * @param key 32-byte one-time key — must not be reused across messages
141
+ * @param msg Message to authenticate
142
+ * @returns 16-byte Poly1305 tag
143
+ */
80
144
  mac(key, msg) {
145
+ _assertNotOwned('chacha20');
81
146
  if (key.length !== 32)
82
147
  throw new RangeError(`Poly1305 key must be 32 bytes (got ${key.length})`);
83
148
  const mem = new Uint8Array(this.x.memory.buffer);
@@ -96,11 +161,13 @@ export class Poly1305 {
96
161
  this.x.polyFinal();
97
162
  return new Uint8Array(this.x.memory.buffer).slice(tagOff, tagOff + 16);
98
163
  }
164
+ /** Wipe WASM MAC state. */
99
165
  dispose() {
166
+ _assertNotOwned('chacha20');
100
167
  this.x.wipeBuffers();
101
168
  }
102
169
  }
103
- // ── ChaCha20Poly1305 ─────────────────────────────────────────────────────────
170
+ // ── ChaCha20Poly1305 ────────────────────────────────────────────────────────
104
171
  /**
105
172
  * ChaCha20-Poly1305 AEAD (RFC 8439 §2.8).
106
173
  *
@@ -119,23 +186,48 @@ export class ChaCha20Poly1305 {
119
186
  constructor() {
120
187
  this.x = getExports();
121
188
  }
189
+ /**
190
+ * Encrypt and authenticate `plaintext` with ChaCha20-Poly1305 (RFC 8439 §2.8).
191
+ *
192
+ * **Single-use guard:** `encrypt()` may only be called once per instance.
193
+ * Any throw — including validation errors — permanently locks this instance.
194
+ * Always create a new `ChaCha20Poly1305` per message.
195
+ * @param key 32 bytes
196
+ * @param nonce 12 bytes — must be unique per (key, message)
197
+ * @param plaintext Data to encrypt
198
+ * @param aad Additional authenticated data (optional)
199
+ * @returns Ciphertext || 16-byte Poly1305 tag
200
+ */
122
201
  encrypt(key, nonce, plaintext, aad = new Uint8Array(0)) {
123
202
  if (this._used)
124
203
  throw new Error('leviathan-crypto: encrypt() already called on this instance. '
125
204
  + 'Create a new instance for each encryption to prevent nonce reuse.');
205
+ // Strict single-use: lock FIRST, before anything else. Any subsequent
206
+ // throw — including validation errors — terminates the instance.
207
+ this._used = true;
208
+ _assertNotOwned('chacha20');
126
209
  if (key.length !== 32)
127
210
  throw new RangeError(`key must be 32 bytes (got ${key.length})`);
128
211
  if (nonce.length !== 12)
129
212
  throw new RangeError(`nonce must be 12 bytes (got ${nonce.length})`);
130
213
  const { ciphertext, tag } = aeadEncrypt(this.x, key, nonce, plaintext, aad);
131
- this._used = true;
132
214
  const out = new Uint8Array(ciphertext.length + 16);
133
215
  out.set(ciphertext);
134
216
  out.set(tag, ciphertext.length);
135
217
  return out;
136
218
  }
219
+ /**
220
+ * Verify and decrypt a ChaCha20-Poly1305 ciphertext (RFC 8439 §2.8).
221
+ * Throws `AuthenticationError` if the tag does not match.
222
+ * @param key 32 bytes
223
+ * @param nonce 12 bytes — must match the value used to encrypt
224
+ * @param ciphertext Ciphertext || 16-byte tag (combined format from `encrypt`)
225
+ * @param aad Additional authenticated data (optional)
226
+ * @returns Plaintext
227
+ */
137
228
  decrypt(key, nonce, ciphertext, // ciphertext || tag(16) combined
138
229
  aad = new Uint8Array(0)) {
230
+ _assertNotOwned('chacha20');
139
231
  if (key.length !== 32)
140
232
  throw new RangeError(`key must be 32 bytes (got ${key.length})`);
141
233
  if (nonce.length !== 12)
@@ -146,11 +238,13 @@ export class ChaCha20Poly1305 {
146
238
  const tag = ciphertext.subarray(ciphertext.length - 16);
147
239
  return aeadDecrypt(this.x, key, nonce, ct, tag, aad);
148
240
  }
241
+ /** Wipe WASM cipher and MAC state. */
149
242
  dispose() {
243
+ _assertNotOwned('chacha20');
150
244
  this.x.wipeBuffers();
151
245
  }
152
246
  }
153
- // ── XChaCha20Poly1305 ────────────────────────────────────────────────────────
247
+ // ── XChaCha20Poly1305 ───────────────────────────────────────────────────────
154
248
  /**
155
249
  * XChaCha20-Poly1305 AEAD (IETF draft-irtf-cfrg-xchacha).
156
250
  *
@@ -168,19 +262,44 @@ export class XChaCha20Poly1305 {
168
262
  constructor() {
169
263
  this.x = getExports();
170
264
  }
265
+ /**
266
+ * Encrypt and authenticate `plaintext` with XChaCha20-Poly1305
267
+ * (draft-irtf-cfrg-xchacha). Recommended for general-purpose AEAD.
268
+ *
269
+ * **Single-use guard:** `encrypt()` may only be called once per instance.
270
+ * Any throw — including validation errors — permanently locks this instance.
271
+ * Always create a new `XChaCha20Poly1305` per message to prevent nonce reuse.
272
+ * @param key 32 bytes
273
+ * @param nonce 24 bytes — safe to generate randomly via `randomBytes(24)`
274
+ * @param plaintext Data to encrypt
275
+ * @param aad Additional authenticated data (optional)
276
+ * @returns Ciphertext || 16-byte Poly1305 tag
277
+ */
171
278
  encrypt(key, nonce, plaintext, aad = new Uint8Array(0)) {
172
279
  if (this._used)
173
280
  throw new Error('leviathan-crypto: encrypt() already called on this instance. '
174
281
  + 'Create a new instance for each encryption to prevent nonce reuse.');
282
+ // Strict single-use: lock FIRST, before anything else. Any subsequent
283
+ // throw — including validation errors — terminates the instance.
284
+ this._used = true;
285
+ _assertNotOwned('chacha20');
175
286
  if (key.length !== 32)
176
287
  throw new RangeError(`key must be 32 bytes (got ${key.length})`);
177
288
  if (nonce.length !== 24)
178
289
  throw new RangeError(`XChaCha20 nonce must be 24 bytes (got ${nonce.length})`);
179
- const result = xcEncrypt(this.x, key, nonce, plaintext, aad);
180
- this._used = true;
181
- return result;
290
+ return xcEncrypt(this.x, key, nonce, plaintext, aad);
182
291
  }
292
+ /**
293
+ * Verify and decrypt an XChaCha20-Poly1305 ciphertext.
294
+ * Throws `AuthenticationError` if the tag does not match.
295
+ * @param key 32 bytes
296
+ * @param nonce 24 bytes — must match the value used to encrypt
297
+ * @param ciphertext Ciphertext || 16-byte tag (combined format from `encrypt`)
298
+ * @param aad Additional authenticated data (optional)
299
+ * @returns Plaintext
300
+ */
183
301
  decrypt(key, nonce, ciphertext, aad = new Uint8Array(0)) {
302
+ _assertNotOwned('chacha20');
184
303
  if (key.length !== 32)
185
304
  throw new RangeError(`key must be 32 bytes (got ${key.length})`);
186
305
  if (nonce.length !== 24)
@@ -189,12 +308,21 @@ export class XChaCha20Poly1305 {
189
308
  throw new RangeError(`ciphertext too short — must include 16-byte tag (got ${ciphertext.length})`);
190
309
  return xcDecrypt(this.x, key, nonce, ciphertext, aad);
191
310
  }
311
+ /** Wipe WASM cipher and MAC state. */
192
312
  dispose() {
313
+ _assertNotOwned('chacha20');
193
314
  this.x.wipeBuffers();
194
315
  }
195
316
  }
196
317
  export { XChaCha20Cipher } from './cipher-suite.js';
197
- // ── Ready check ──────────────────────────────────────────────────────────────
318
+ // ── ChaCha20Generator ───────────────────────────────────────────────────────
319
+ export { ChaCha20Generator } from './generator.js';
320
+ // ── Ready check ─────────────────────────────────────────────────────────────
321
+ /**
322
+ * Returns `true` if the chacha20 WASM module has been initialised.
323
+ * Used by tests and internal guards; not part of the public API.
324
+ * @internal
325
+ */
198
326
  export function _chachaReady() {
199
327
  try {
200
328
  getInstance('chacha20');
@@ -1,16 +1,67 @@
1
1
  import type { ChaChaExports } from './types.js';
2
- /** ChaCha20-Poly1305 AEAD encrypt (RFC 8439 §2.8). */
2
+ /**
3
+ * ChaCha20-Poly1305 AEAD encrypt (RFC 8439 §2.8).
4
+ * @param x ChaCha20 WASM exports
5
+ * @param key 32-byte key
6
+ * @param nonce 12-byte nonce — must be unique per (key, message)
7
+ * @param plaintext Data to encrypt (must be ≤ WASM CHUNK_SIZE)
8
+ * @param aad Additional authenticated data
9
+ * @returns `{ ciphertext, tag }` — tag is 16 bytes
10
+ */
3
11
  export declare function aeadEncrypt(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): {
4
12
  ciphertext: Uint8Array;
5
13
  tag: Uint8Array;
6
14
  };
7
- /** ChaCha20-Poly1305 AEAD decrypt (RFC 8439 §2.8). Constant-time tag comparison. */
15
+ /**
16
+ * ChaCha20-Poly1305 AEAD decrypt with constant-time tag comparison (RFC 8439 §2.8).
17
+ * Throws `AuthenticationError` on tag mismatch; never returns plaintext on failure.
18
+ * @param x ChaCha20 WASM exports
19
+ * @param key 32-byte key
20
+ * @param nonce 12-byte nonce — must match the value used to encrypt
21
+ * @param ciphertext Ciphertext bytes (must be ≤ WASM CHUNK_SIZE)
22
+ * @param tag 16-byte Poly1305 tag
23
+ * @param aad Additional authenticated data
24
+ * @param cipherName Error label for `AuthenticationError` (default 'chacha20-poly1305')
25
+ * @returns Plaintext
26
+ */
8
27
  export declare function aeadDecrypt(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, tag: Uint8Array, aad: Uint8Array, cipherName?: string): Uint8Array;
9
- /** HChaCha20 subkey derivation — first 16 bytes of nonce. */
28
+ /**
29
+ * Derive a 32-byte HChaCha20 subkey from `key` and the first 16 bytes of `nonce`.
30
+ * Used as the inner key for XChaCha20-Poly1305 (draft-irtf-cfrg-xchacha §2.3).
31
+ * @param x ChaCha20 WASM exports
32
+ * @param key 32-byte master key
33
+ * @param nonce 24-byte XChaCha20 nonce (only bytes 0–15 are used)
34
+ * @returns 32-byte HChaCha20 subkey
35
+ */
10
36
  export declare function deriveSubkey(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array): Uint8Array;
11
- /** Build inner 12-byte nonce from bytes 16–23 of XChaCha nonce. */
37
+ /**
38
+ * Build the inner 12-byte ChaCha20 nonce for XChaCha20 from bytes 16–23 of the
39
+ * 24-byte XChaCha nonce (draft-irtf-cfrg-xchacha §2.3).
40
+ * @param nonce 24-byte XChaCha20 nonce
41
+ * @returns 12-byte inner nonce (bytes 0–3 are zero, bytes 4–11 are nonce[16:24])
42
+ */
12
43
  export declare function innerNonce(nonce: Uint8Array): Uint8Array;
13
- /** XChaCha20-Poly1305 encrypt → ciphertext || tag. */
44
+ /**
45
+ * XChaCha20-Poly1305 encrypt (draft-irtf-cfrg-xchacha).
46
+ * Derives HChaCha20 subkey from `key` + nonce[0:16], then runs
47
+ * ChaCha20-Poly1305 with a 12-byte inner nonce (nonce[16:24]).
48
+ * @param x ChaCha20 WASM exports
49
+ * @param key 32-byte key
50
+ * @param nonce 24-byte nonce
51
+ * @param plaintext Data to encrypt
52
+ * @param aad Additional authenticated data
53
+ * @returns Ciphertext || 16-byte Poly1305 tag
54
+ */
14
55
  export declare function xcEncrypt(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Uint8Array;
15
- /** XChaCha20-Poly1305 decrypt → plaintext (throws on auth failure). */
56
+ /**
57
+ * XChaCha20-Poly1305 decrypt (draft-irtf-cfrg-xchacha).
58
+ * Derives HChaCha20 subkey, verifies the Poly1305 tag, then decrypts.
59
+ * Throws `AuthenticationError` on tag mismatch.
60
+ * @param x ChaCha20 WASM exports
61
+ * @param key 32-byte key
62
+ * @param nonce 24-byte nonce — must match the value used to encrypt
63
+ * @param ciphertext Ciphertext || 16-byte tag (combined format from `xcEncrypt`)
64
+ * @param aad Additional authenticated data
65
+ * @returns Plaintext
66
+ */
16
67
  export declare function xcDecrypt(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad: Uint8Array): Uint8Array;
@@ -5,7 +5,14 @@
5
5
  // and the pool worker (pool.worker.ts), eliminating duplication.
6
6
  import { constantTimeEqual } from '../utils.js';
7
7
  import { AuthenticationError } from '../errors.js';
8
- // ── Module-private helpers ───────────────────────────────────────────────────
8
+ // ── Module-private helpers ──────────────────────────────────────────────────
9
+ /**
10
+ * Feed `data` into the active Poly1305 accumulator via the WASM message buffer.
11
+ * No-op when `data` is empty.
12
+ * @param x ChaCha20 WASM exports
13
+ * @param data Bytes to absorb
14
+ * @internal
15
+ */
9
16
  function polyFeed(x, data) {
10
17
  if (data.length === 0)
11
18
  return;
@@ -19,6 +26,14 @@ function polyFeed(x, data) {
19
26
  pos += chunk;
20
27
  }
21
28
  }
29
+ /**
30
+ * Build the 16-byte Poly1305 length footer from AAD and ciphertext lengths.
31
+ * Both lengths are encoded as 64-bit little-endian integers (RFC 8439 §2.8).
32
+ * @param aadLen AAD byte length
33
+ * @param ctLen Ciphertext byte length
34
+ * @returns 16-byte length block
35
+ * @internal
36
+ */
22
37
  function lenBlock(aadLen, ctLen) {
23
38
  const b = new Uint8Array(16);
24
39
  const dv = new DataView(b.buffer);
@@ -31,8 +46,16 @@ function lenBlock(aadLen, ctLen) {
31
46
  dv.setUint32(12, Math.floor(ctLen / 0x100000000) >>> 0, true);
32
47
  return b;
33
48
  }
34
- // ── Inner AEAD (12-byte nonce) ───────────────────────────────────────────────
35
- /** ChaCha20-Poly1305 AEAD encrypt (RFC 8439 §2.8). */
49
+ // ── Inner AEAD (12-byte nonce) ──────────────────────────────────────────────
50
+ /**
51
+ * ChaCha20-Poly1305 AEAD encrypt (RFC 8439 §2.8).
52
+ * @param x ChaCha20 WASM exports
53
+ * @param key 32-byte key
54
+ * @param nonce 12-byte nonce — must be unique per (key, message)
55
+ * @param plaintext Data to encrypt (must be ≤ WASM CHUNK_SIZE)
56
+ * @param aad Additional authenticated data
57
+ * @returns `{ ciphertext, tag }` — tag is 16 bytes
58
+ */
36
59
  export function aeadEncrypt(x, key, nonce, plaintext, aad) {
37
60
  const maxChunk = x.getChunkSize();
38
61
  if (plaintext.length > maxChunk)
@@ -50,9 +73,13 @@ export function aeadEncrypt(x, key, nonce, plaintext, aad) {
50
73
  const aadPad = (16 - aad.length % 16) % 16;
51
74
  if (aadPad > 0)
52
75
  polyFeed(x, new Uint8Array(aadPad));
53
- // Step 4: Re-init ChaCha20 at counter=1
76
+ // Step 4: Re-init ChaCha20 at counter=1.
77
+ // `chachaGenPolyKey` mutated CHACHA_STATE + 48 (the counter word) to 0 but
78
+ // left every other state word intact (constants, key, nonce). Writing
79
+ // counter=1 via `chachaSetCounter` restores the state for encryption —
80
+ // no second `chachaLoadKey()` is needed (the key/nonce buffers and the
81
+ // non-counter state words are already correct).
54
82
  x.chachaSetCounter(1);
55
- x.chachaLoadKey();
56
83
  // Step 5: Encrypt
57
84
  mem.set(plaintext, x.getChunkPtOffset());
58
85
  x.chachaEncryptChunk_simd(plaintext.length);
@@ -71,7 +98,18 @@ export function aeadEncrypt(x, key, nonce, plaintext, aad) {
71
98
  const tag = new Uint8Array(x.memory.buffer).slice(tagOff, tagOff + 16);
72
99
  return { ciphertext, tag };
73
100
  }
74
- /** ChaCha20-Poly1305 AEAD decrypt (RFC 8439 §2.8). Constant-time tag comparison. */
101
+ /**
102
+ * ChaCha20-Poly1305 AEAD decrypt with constant-time tag comparison (RFC 8439 §2.8).
103
+ * Throws `AuthenticationError` on tag mismatch; never returns plaintext on failure.
104
+ * @param x ChaCha20 WASM exports
105
+ * @param key 32-byte key
106
+ * @param nonce 12-byte nonce — must match the value used to encrypt
107
+ * @param ciphertext Ciphertext bytes (must be ≤ WASM CHUNK_SIZE)
108
+ * @param tag 16-byte Poly1305 tag
109
+ * @param aad Additional authenticated data
110
+ * @param cipherName Error label for `AuthenticationError` (default 'chacha20-poly1305')
111
+ * @returns Plaintext
112
+ */
75
113
  export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad, cipherName = 'chacha20-poly1305') {
76
114
  const maxChunk = x.getChunkSize();
77
115
  if (ciphertext.length > maxChunk)
@@ -97,9 +135,19 @@ export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad, cipherName = 'c
97
135
  const tagOff = x.getPolyTagOffset();
98
136
  const expectedTag = new Uint8Array(x.memory.buffer).slice(tagOff, tagOff + 16);
99
137
  if (!constantTimeEqual(expectedTag, tag)) {
100
- // Wipe the full chunk output buffer — defense-in-depth before throwing
138
+ // Wipe the full chunk output buffer — defense-in-depth before throwing.
101
139
  const ctOff = x.getChunkCtOffset();
102
140
  mem.fill(0, ctOff, ctOff + maxChunk);
141
+ // Also zero the 64-byte chacha block buffer — it holds keystream bytes
142
+ // generated by chachaGenPolyKey() that would otherwise persist until
143
+ // the next op or dispose().
144
+ const blockOff = x.getChachaBlockOffset();
145
+ mem.fill(0, blockOff, blockOff + 64);
146
+ // And the 32-byte Poly1305 one-time subkey copy at POLY_KEY_OFFSET.
147
+ // chachaGenPolyKey copies keystream[0..32] here; wiping CHACHA_BLOCK
148
+ // zeroes the source but not this copy.
149
+ const polyKeyOff = x.getPolyKeyOffset();
150
+ mem.fill(0, polyKeyOff, polyKeyOff + 32);
103
151
  throw new AuthenticationError(cipherName);
104
152
  }
105
153
  // Decrypt only after authentication succeeds
@@ -110,8 +158,15 @@ export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad, cipherName = 'c
110
158
  const ptOff = x.getChunkCtOffset();
111
159
  return new Uint8Array(x.memory.buffer).slice(ptOff, ptOff + ciphertext.length);
112
160
  }
113
- // ── XChaCha20 helpers ────────────────────────────────────────────────────────
114
- /** HChaCha20 subkey derivation — first 16 bytes of nonce. */
161
+ // ── XChaCha20 helpers ───────────────────────────────────────────────────────
162
+ /**
163
+ * Derive a 32-byte HChaCha20 subkey from `key` and the first 16 bytes of `nonce`.
164
+ * Used as the inner key for XChaCha20-Poly1305 (draft-irtf-cfrg-xchacha §2.3).
165
+ * @param x ChaCha20 WASM exports
166
+ * @param key 32-byte master key
167
+ * @param nonce 24-byte XChaCha20 nonce (only bytes 0–15 are used)
168
+ * @returns 32-byte HChaCha20 subkey
169
+ */
115
170
  export function deriveSubkey(x, key, nonce) {
116
171
  const mem = new Uint8Array(x.memory.buffer);
117
172
  mem.set(key, x.getKeyOffset());
@@ -120,14 +175,29 @@ export function deriveSubkey(x, key, nonce) {
120
175
  const off = x.getXChaChaSubkeyOffset();
121
176
  return new Uint8Array(x.memory.buffer).slice(off, off + 32);
122
177
  }
123
- /** Build inner 12-byte nonce from bytes 16–23 of XChaCha nonce. */
178
+ /**
179
+ * Build the inner 12-byte ChaCha20 nonce for XChaCha20 from bytes 16–23 of the
180
+ * 24-byte XChaCha nonce (draft-irtf-cfrg-xchacha §2.3).
181
+ * @param nonce 24-byte XChaCha20 nonce
182
+ * @returns 12-byte inner nonce (bytes 0–3 are zero, bytes 4–11 are nonce[16:24])
183
+ */
124
184
  export function innerNonce(nonce) {
125
185
  const n = new Uint8Array(12);
126
186
  n.set(nonce.subarray(16, 24), 4);
127
187
  return n;
128
188
  }
129
- // ── Full XChaCha20-Poly1305 ──────────────────────────────────────────────────
130
- /** XChaCha20-Poly1305 encrypt → ciphertext || tag. */
189
+ // ── Full XChaCha20-Poly1305 ─────────────────────────────────────────────────
190
+ /**
191
+ * XChaCha20-Poly1305 encrypt (draft-irtf-cfrg-xchacha).
192
+ * Derives HChaCha20 subkey from `key` + nonce[0:16], then runs
193
+ * ChaCha20-Poly1305 with a 12-byte inner nonce (nonce[16:24]).
194
+ * @param x ChaCha20 WASM exports
195
+ * @param key 32-byte key
196
+ * @param nonce 24-byte nonce
197
+ * @param plaintext Data to encrypt
198
+ * @param aad Additional authenticated data
199
+ * @returns Ciphertext || 16-byte Poly1305 tag
200
+ */
131
201
  export function xcEncrypt(x, key, nonce, plaintext, aad) {
132
202
  const subkey = deriveSubkey(x, key, nonce);
133
203
  const inner = innerNonce(nonce);
@@ -137,7 +207,17 @@ export function xcEncrypt(x, key, nonce, plaintext, aad) {
137
207
  result.set(tag, ciphertext.length);
138
208
  return result;
139
209
  }
140
- /** XChaCha20-Poly1305 decrypt → plaintext (throws on auth failure). */
210
+ /**
211
+ * XChaCha20-Poly1305 decrypt (draft-irtf-cfrg-xchacha).
212
+ * Derives HChaCha20 subkey, verifies the Poly1305 tag, then decrypts.
213
+ * Throws `AuthenticationError` on tag mismatch.
214
+ * @param x ChaCha20 WASM exports
215
+ * @param key 32-byte key
216
+ * @param nonce 24-byte nonce — must match the value used to encrypt
217
+ * @param ciphertext Ciphertext || 16-byte tag (combined format from `xcEncrypt`)
218
+ * @param aad Additional authenticated data
219
+ * @returns Plaintext
220
+ */
141
221
  export function xcDecrypt(x, key, nonce, ciphertext, aad) {
142
222
  const ct = ciphertext.subarray(0, ciphertext.length - 16);
143
223
  const tag = ciphertext.subarray(ciphertext.length - 16);