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
@@ -0,0 +1,97 @@
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/serpent/generator.ts
23
+ //
24
+ // Practical Cryptography (Ferguson & Schneier, 2003) §9.4 — generator
25
+ // Serpent-256 ECB counter-mode PRF for Fortuna's generator slot.
26
+ import { _assertNotOwned, getInstance } from '../init.js';
27
+ import { wipe } from '../utils.js';
28
+ /**
29
+ * Serpent-256 ECB counter-mode PRF for Fortuna's generator slot.
30
+ *
31
+ * Each 16-byte counter value is encrypted as a plaintext block to produce
32
+ * one block of pseudorandom output. Practical Cryptography (Ferguson &
33
+ * Schneier, 2003) §9.4.
34
+ *
35
+ * Pass to `Fortuna.create({ generator: SerpentGenerator, ... })` — do not
36
+ * call `generate()` directly outside of Fortuna.
37
+ */
38
+ export const SerpentGenerator = {
39
+ keySize: 32,
40
+ blockSize: 16,
41
+ counterSize: 16,
42
+ wasmModules: ['serpent'],
43
+ /**
44
+ * Generate `n` pseudorandom bytes by encrypting successive 16-byte counter
45
+ * values in ECB mode. The counter is incremented as a 128-bit little-endian
46
+ * integer after each block.
47
+ * @param key 32-byte Serpent-256 key
48
+ * @param counter 16-byte initial counter value (little-endian)
49
+ * @param n Number of bytes to generate (0 ≤ n ≤ 2^30)
50
+ * @returns `n` pseudorandom bytes
51
+ */
52
+ generate(key, counter, n) {
53
+ _assertNotOwned('serpent');
54
+ if (key.length !== 32)
55
+ throw new RangeError(`SerpentGenerator: key must be 32 bytes (got ${key.length})`);
56
+ if (counter.length !== 16)
57
+ throw new RangeError(`SerpentGenerator: counter must be 16 bytes (got ${counter.length})`);
58
+ if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 30)
59
+ throw new RangeError(`SerpentGenerator: n must be a non-negative safe integer <= 2^30 (got ${n})`);
60
+ const x = getInstance('serpent').exports;
61
+ const mem = new Uint8Array(x.memory.buffer);
62
+ const c = counter.slice();
63
+ try {
64
+ mem.set(key, x.getKeyOffset());
65
+ if (x.loadKey(32) !== 0)
66
+ throw new Error('SerpentGenerator: loadKey failed');
67
+ const blocks = Math.ceil(n / 16);
68
+ const output = new Uint8Array(n);
69
+ const ptOff = x.getBlockPtOffset();
70
+ const ctOff = x.getBlockCtOffset();
71
+ for (let i = 0; i < blocks; i++) {
72
+ mem.set(c, ptOff);
73
+ x.encryptBlock();
74
+ // Last-block trim: copy only what the caller asked for. The
75
+ // unused tail stays in WASM memory (wiped in finally) instead
76
+ // of landing on the JS heap where callers could reach it via
77
+ // `result.buffer`. Mirrors ChaCha20Generator's exact-size output.
78
+ const offset = i * 16;
79
+ const writeLen = Math.min(16, n - offset);
80
+ output.set(mem.subarray(ctOff, ctOff + writeLen), offset);
81
+ // Increment c as a 16-byte little-endian integer
82
+ for (let j = 0; j < 16; j++) {
83
+ if (++c[j] !== 0)
84
+ break;
85
+ }
86
+ }
87
+ return output;
88
+ }
89
+ finally {
90
+ // Wipe WASM key/key-schedule/last-block scratch and the JS-heap
91
+ // counter copy so secret-derived state does not outlive this call
92
+ // in either the WASM linear memory or the JS heap.
93
+ x.wipeBuffers();
94
+ wipe(c);
95
+ }
96
+ },
97
+ };
@@ -1,12 +1,44 @@
1
1
  import type { WasmSource } from '../wasm-source.js';
2
+ /**
3
+ * Load and initialise the Serpent WASM module from `source`.
4
+ * Must be called before constructing any Serpent class.
5
+ * @param source WASM binary — gzip+base64 string, URL, ArrayBuffer, Uint8Array,
6
+ * pre-compiled WebAssembly.Module, Response, or Promise<Response>
7
+ */
2
8
  export declare function serpentInit(source: WasmSource): Promise<void>;
3
9
  export type { WasmSource };
10
+ /**
11
+ * Low-level Serpent-256 block cipher — raw ECB encrypt/decrypt.
12
+ *
13
+ * Atomic (stateless) class: each method call is independent.
14
+ * Does not hold exclusive module access; cannot be used while a stateful
15
+ * instance (`SerpentCtr`, `SerpentCbc`, `SerpentCipher`) is alive.
16
+ * Call `dispose()` after use to wipe WASM key material.
17
+ */
4
18
  export declare class Serpent {
5
19
  private readonly x;
6
20
  constructor();
21
+ /**
22
+ * Expand `key` into the WASM key schedule. Must be called before
23
+ * `encryptBlock` / `decryptBlock`.
24
+ * @param key 16, 24, or 32 bytes
25
+ */
7
26
  loadKey(key: Uint8Array): void;
27
+ /**
28
+ * Encrypt one 128-bit block with the previously loaded key schedule.
29
+ * Serpent AES submission §2.2.
30
+ * @param plaintext 16-byte plaintext block
31
+ * @returns 16-byte ciphertext block
32
+ */
8
33
  encryptBlock(plaintext: Uint8Array): Uint8Array;
34
+ /**
35
+ * Decrypt one 128-bit block with the previously loaded key schedule.
36
+ * Serpent AES submission §2.2.
37
+ * @param ciphertext 16-byte ciphertext block
38
+ * @returns 16-byte plaintext block
39
+ */
9
40
  decryptBlock(ciphertext: Uint8Array): Uint8Array;
41
+ /** Wipe WASM key material and release memory. */
10
42
  dispose(): void;
11
43
  }
12
44
  /**
@@ -15,19 +47,47 @@ export declare class Serpent {
15
47
  * **WARNING: CTR mode is unauthenticated.** An attacker can flip ciphertext
16
48
  * bits without detection. Always pair with HMAC-SHA256 (Encrypt-then-MAC)
17
49
  * or use `XChaCha20Poly1305` instead.
50
+ *
51
+ * Holds exclusive access to the `serpent` WASM module from construction
52
+ * until `dispose()`. Constructing a second SerpentCtr/SerpentCbc/
53
+ * SerpentCipher or any other serpent user while this instance is live
54
+ * throws. Call `dispose()` when done.
18
55
  */
19
56
  export declare class SerpentCtr {
20
57
  private readonly x;
58
+ private _tok;
21
59
  constructor(opts?: {
22
60
  dangerUnauthenticated: true;
23
61
  });
62
+ /**
63
+ * Load key and nonce into WASM state and reset the block counter to 0.
64
+ * Must be called before each message.
65
+ * @param key 16, 24, or 32 bytes
66
+ * @param nonce 16 bytes — must be unique per (key, message)
67
+ */
24
68
  beginEncrypt(key: Uint8Array, nonce: Uint8Array): void;
69
+ /**
70
+ * XOR `chunk` with the next keystream block(s). Counter advances automatically.
71
+ * @param chunk Plaintext chunk — must not exceed WASM CHUNK_SIZE
72
+ * @returns Ciphertext of the same length
73
+ */
25
74
  encryptChunk(chunk: Uint8Array): Uint8Array;
75
+ /**
76
+ * Alias for `beginEncrypt` — CTR mode is symmetric.
77
+ * @param key 16, 24, or 32 bytes
78
+ * @param nonce 16 bytes — must match the value used to encrypt
79
+ */
26
80
  beginDecrypt(key: Uint8Array, nonce: Uint8Array): void;
81
+ /**
82
+ * Alias for `encryptChunk` — CTR mode is symmetric.
83
+ * @param chunk Ciphertext chunk
84
+ * @returns Plaintext of the same length
85
+ */
27
86
  decryptChunk(chunk: Uint8Array): Uint8Array;
87
+ /** Wipe WASM state and release exclusive module access. Idempotent. */
28
88
  dispose(): void;
29
89
  }
30
90
  export { SerpentCbc } from './serpent-cbc.js';
31
91
  export { AuthenticationError } from '../errors.js';
32
92
  export { SerpentCipher } from './cipher-suite.js';
33
- export declare function _serpentReady(): boolean;
93
+ export { SerpentGenerator } from './generator.js';
@@ -23,20 +23,41 @@
23
23
  //
24
24
  // Public API classes for the Serpent-256 WASM module.
25
25
  // Uses the init() module cache — call serpentInit(source) before constructing.
26
- import { getInstance, initModule } from '../init.js';
26
+ import { getInstance, initModule, _acquireModule, _releaseModule, _assertNotOwned } from '../init.js';
27
+ /**
28
+ * Load and initialise the Serpent WASM module from `source`.
29
+ * Must be called before constructing any Serpent class.
30
+ * @param source WASM binary — gzip+base64 string, URL, ArrayBuffer, Uint8Array,
31
+ * pre-compiled WebAssembly.Module, Response, or Promise<Response>
32
+ */
27
33
  export async function serpentInit(source) {
28
34
  return initModule('serpent', source);
29
35
  }
36
+ /** Returns the raw serpent WASM export object. @internal */
30
37
  function getExports() {
31
38
  return getInstance('serpent').exports;
32
39
  }
33
- // ── Serpent ──────────────────────────────────────────────────────────────────
40
+ // ── Serpent ─────────────────────────────────────────────────────────────────
41
+ /**
42
+ * Low-level Serpent-256 block cipher — raw ECB encrypt/decrypt.
43
+ *
44
+ * Atomic (stateless) class: each method call is independent.
45
+ * Does not hold exclusive module access; cannot be used while a stateful
46
+ * instance (`SerpentCtr`, `SerpentCbc`, `SerpentCipher`) is alive.
47
+ * Call `dispose()` after use to wipe WASM key material.
48
+ */
34
49
  export class Serpent {
35
50
  x;
36
51
  constructor() {
37
52
  this.x = getExports();
38
53
  }
54
+ /**
55
+ * Expand `key` into the WASM key schedule. Must be called before
56
+ * `encryptBlock` / `decryptBlock`.
57
+ * @param key 16, 24, or 32 bytes
58
+ */
39
59
  loadKey(key) {
60
+ _assertNotOwned('serpent');
40
61
  if (key.length !== 16 && key.length !== 24 && key.length !== 32)
41
62
  throw new RangeError(`key must be 16, 24, or 32 bytes (got ${key.length})`);
42
63
  const mem = new Uint8Array(this.x.memory.buffer);
@@ -44,7 +65,14 @@ export class Serpent {
44
65
  if (this.x.loadKey(key.length) !== 0)
45
66
  throw new Error('loadKey failed');
46
67
  }
68
+ /**
69
+ * Encrypt one 128-bit block with the previously loaded key schedule.
70
+ * Serpent AES submission §2.2.
71
+ * @param plaintext 16-byte plaintext block
72
+ * @returns 16-byte ciphertext block
73
+ */
47
74
  encryptBlock(plaintext) {
75
+ _assertNotOwned('serpent');
48
76
  if (plaintext.length !== 16)
49
77
  throw new RangeError(`block must be 16 bytes (got ${plaintext.length})`);
50
78
  const mem = new Uint8Array(this.x.memory.buffer);
@@ -54,7 +82,14 @@ export class Serpent {
54
82
  this.x.encryptBlock();
55
83
  return mem.slice(ctOff, ctOff + 16);
56
84
  }
85
+ /**
86
+ * Decrypt one 128-bit block with the previously loaded key schedule.
87
+ * Serpent AES submission §2.2.
88
+ * @param ciphertext 16-byte ciphertext block
89
+ * @returns 16-byte plaintext block
90
+ */
57
91
  decryptBlock(ciphertext) {
92
+ _assertNotOwned('serpent');
58
93
  if (ciphertext.length !== 16)
59
94
  throw new RangeError(`block must be 16 bytes (got ${ciphertext.length})`);
60
95
  const mem = new Uint8Array(this.x.memory.buffer);
@@ -64,28 +99,45 @@ export class Serpent {
64
99
  this.x.decryptBlock();
65
100
  return mem.slice(ptOff, ptOff + 16);
66
101
  }
102
+ /** Wipe WASM key material and release memory. */
67
103
  dispose() {
104
+ _assertNotOwned('serpent');
68
105
  this.x.wipeBuffers();
69
106
  }
70
107
  }
71
- // ── SerpentCtr ───────────────────────────────────────────────────────────────
108
+ // ── SerpentCtr ──────────────────────────────────────────────────────────────
72
109
  /**
73
110
  * Serpent-256 in CTR mode.
74
111
  *
75
112
  * **WARNING: CTR mode is unauthenticated.** An attacker can flip ciphertext
76
113
  * bits without detection. Always pair with HMAC-SHA256 (Encrypt-then-MAC)
77
114
  * or use `XChaCha20Poly1305` instead.
115
+ *
116
+ * Holds exclusive access to the `serpent` WASM module from construction
117
+ * until `dispose()`. Constructing a second SerpentCtr/SerpentCbc/
118
+ * SerpentCipher or any other serpent user while this instance is live
119
+ * throws. Call `dispose()` when done.
78
120
  */
79
121
  export class SerpentCtr {
80
122
  x;
123
+ _tok;
81
124
  constructor(opts) {
82
125
  if (!opts?.dangerUnauthenticated) {
83
126
  throw new Error('leviathan-crypto: SerpentCtr is unauthenticated — use Seal with SerpentCipher instead. ' +
84
127
  'To use SerpentCtr directly, pass { dangerUnauthenticated: true }.');
85
128
  }
86
129
  this.x = getExports();
130
+ this._tok = _acquireModule('serpent');
87
131
  }
132
+ /**
133
+ * Load key and nonce into WASM state and reset the block counter to 0.
134
+ * Must be called before each message.
135
+ * @param key 16, 24, or 32 bytes
136
+ * @param nonce 16 bytes — must be unique per (key, message)
137
+ */
88
138
  beginEncrypt(key, nonce) {
139
+ if (this._tok === undefined)
140
+ throw new Error('SerpentCtr: instance has been disposed');
89
141
  if (key.length !== 16 && key.length !== 24 && key.length !== 32)
90
142
  throw new RangeError('key must be 16, 24, or 32 bytes');
91
143
  if (nonce.length !== 16)
@@ -96,7 +148,14 @@ export class SerpentCtr {
96
148
  this.x.loadKey(key.length);
97
149
  this.x.resetCounter();
98
150
  }
151
+ /**
152
+ * XOR `chunk` with the next keystream block(s). Counter advances automatically.
153
+ * @param chunk Plaintext chunk — must not exceed WASM CHUNK_SIZE
154
+ * @returns Ciphertext of the same length
155
+ */
99
156
  encryptChunk(chunk) {
157
+ if (this._tok === undefined)
158
+ throw new Error('SerpentCtr: instance has been disposed');
100
159
  const maxChunk = this.x.getChunkSize();
101
160
  if (chunk.length > maxChunk)
102
161
  throw new RangeError(`chunk exceeds maximum size of ${maxChunk} bytes — split into smaller chunks`);
@@ -107,22 +166,48 @@ export class SerpentCtr {
107
166
  this.x.encryptChunk_simd(chunk.length);
108
167
  return mem.slice(ctOff, ctOff + chunk.length);
109
168
  }
169
+ /**
170
+ * Alias for `beginEncrypt` — CTR mode is symmetric.
171
+ * @param key 16, 24, or 32 bytes
172
+ * @param nonce 16 bytes — must match the value used to encrypt
173
+ */
110
174
  beginDecrypt(key, nonce) {
111
175
  this.beginEncrypt(key, nonce);
112
176
  }
177
+ /**
178
+ * Alias for `encryptChunk` — CTR mode is symmetric.
179
+ * @param chunk Ciphertext chunk
180
+ * @returns Plaintext of the same length
181
+ */
113
182
  decryptChunk(chunk) {
114
183
  return this.encryptChunk(chunk);
115
184
  }
185
+ /** Wipe WASM state and release exclusive module access. Idempotent. */
116
186
  dispose() {
117
- this.x.wipeBuffers();
187
+ if (this._tok === undefined)
188
+ return;
189
+ try {
190
+ this.x.wipeBuffers();
191
+ }
192
+ finally {
193
+ _releaseModule('serpent', this._tok);
194
+ this._tok = undefined;
195
+ }
118
196
  }
119
197
  }
120
- // ── SerpentCbc ───────────────────────────────────────────────────────────────
198
+ // ── SerpentCbc ──────────────────────────────────────────────────────────────
121
199
  export { SerpentCbc } from './serpent-cbc.js';
122
200
  export { AuthenticationError } from '../errors.js';
123
- // ── SerpentCipher re-export ───────────────────────────────────────────────────
201
+ // ── SerpentCipher re-export ─────────────────────────────────────────────────
124
202
  export { SerpentCipher } from './cipher-suite.js';
125
- // ── Ready check ──────────────────────────────────────────────────────────────
203
+ // ── SerpentGenerator ────────────────────────────────────────────────────────
204
+ export { SerpentGenerator } from './generator.js';
205
+ // ── Ready check ─────────────────────────────────────────────────────────────
206
+ /**
207
+ * Returns `true` if the serpent WASM module has been initialised.
208
+ * Used by tests and internal guards; not part of the public API.
209
+ * @internal
210
+ */
126
211
  export function _serpentReady() {
127
212
  try {
128
213
  getInstance('serpent');
@@ -5,100 +5,29 @@
5
5
  // Holds 3 derived keys (enc/mac/iv) and raw WASM instances.
6
6
  // Direct WASM calls — no initModule (avoids same-thread module cache conflicts
7
7
  // in @vitest/web-worker test environment).
8
+ //
9
+ // All HMAC / CBC / PKCS7 primitives come from `./shared-ops.js` — the same
10
+ // module the main-thread `SerpentCipher` uses. Byte-identical output with
11
+ // the main thread is the regression guard (see
12
+ // test/unit/stream/pool-byte-exact.test.ts). Must NOT import from `../init.js`:
13
+ // workers have their own isolated WASM instances, no shared-state registry.
8
14
  import { constantTimeEqual, wipe, concat } from '../utils.js';
9
15
  import { AuthenticationError } from '../errors.js';
16
+ import { hmacSha256, cbcEncryptChunk, cbcDecryptChunk, } from './shared-ops.js';
10
17
  let sha2;
11
18
  let serpent;
12
19
  let keys;
13
- function hmacSha256(key, msg) {
14
- const x = sha2;
15
- let k = key;
16
- if (k.length > 64) {
17
- x.sha256Init();
18
- feedSha2(k);
19
- x.sha256Final();
20
- const mem = new Uint8Array(x.memory.buffer);
21
- k = mem.slice(x.getSha256OutOffset(), x.getSha256OutOffset() + 32);
22
- }
23
- const mem = new Uint8Array(x.memory.buffer);
24
- mem.set(k, x.getSha256InputOffset());
25
- x.hmac256Init(k.length);
26
- feedHmac(msg);
27
- x.hmac256Final();
28
- return new Uint8Array(x.memory.buffer).slice(x.getSha256OutOffset(), x.getSha256OutOffset() + 32);
29
- }
30
- function feedSha2(data) {
31
- const x = sha2;
32
- const mem = new Uint8Array(x.memory.buffer);
33
- const off = x.getSha256InputOffset();
34
- let pos = 0;
35
- while (pos < data.length) {
36
- const n = Math.min(data.length - pos, 64);
37
- mem.set(data.subarray(pos, pos + n), off);
38
- x.sha256Update(n);
39
- pos += n;
40
- }
41
- }
42
- function feedHmac(data) {
43
- const x = sha2;
44
- const mem = new Uint8Array(x.memory.buffer);
45
- const off = x.getSha256InputOffset();
46
- let pos = 0;
47
- while (pos < data.length) {
48
- const n = Math.min(data.length - pos, 64);
49
- mem.set(data.subarray(pos, pos + n), off);
50
- x.hmac256Update(n);
51
- pos += n;
52
- }
53
- }
54
- function pkcs7Pad(data) {
55
- const padLen = 16 - (data.length % 16);
56
- const out = new Uint8Array(data.length + padLen);
57
- out.set(data);
58
- out.fill(padLen, data.length);
59
- return out;
60
- }
61
- // pkcs7Strip is only called after HMAC authentication succeeds (verify-then-decrypt).
62
- // The early throw on invalid padLen is not a padding oracle in this context —
63
- // the HMAC check is the oracle gate and runs in constant time before this point.
64
- // If you move this call to a pre-auth site, revisit the timing properties.
65
- function pkcs7Strip(data) {
66
- if (data.length === 0)
67
- throw new RangeError('empty ciphertext');
68
- const padLen = data[data.length - 1];
69
- if (padLen === 0 || padLen > 16)
70
- throw new RangeError('invalid PKCS7 padding');
71
- if (padLen > data.length)
72
- throw new RangeError('invalid PKCS7 padding');
73
- let bad = 0;
74
- for (let i = data.length - padLen; i < data.length; i++)
75
- bad |= data[i] ^ padLen;
76
- if (bad !== 0)
77
- throw new RangeError('invalid PKCS7 padding');
78
- return data.subarray(0, data.length - padLen);
79
- }
80
- function cbcEncrypt(encKey, iv, plaintext) {
81
- const s = serpent;
82
- const mem = new Uint8Array(s.memory.buffer);
83
- mem.set(encKey, s.getKeyOffset());
84
- s.loadKey(encKey.length);
85
- mem.set(iv, s.getCbcIvOffset());
86
- const padded = pkcs7Pad(plaintext);
87
- mem.set(padded, s.getChunkPtOffset());
88
- s.cbcEncryptChunk(padded.length);
89
- return new Uint8Array(s.memory.buffer).slice(s.getChunkCtOffset(), s.getChunkCtOffset() + padded.length);
90
- }
91
- function cbcDecrypt(encKey, iv, ct) {
92
- const s = serpent;
93
- const mem = new Uint8Array(s.memory.buffer);
94
- mem.set(encKey, s.getKeyOffset());
95
- s.loadKey(encKey.length);
96
- mem.set(iv, s.getCbcIvOffset());
97
- mem.set(ct, s.getChunkCtOffset());
98
- s.cbcDecryptChunk(ct.length);
99
- const raw = new Uint8Array(s.memory.buffer).slice(s.getChunkPtOffset(), s.getChunkPtOffset() + ct.length);
100
- return pkcs7Strip(raw);
101
- }
20
+ /**
21
+ * Message handler for the Serpent pool worker.
22
+ *
23
+ * Accepts three message types:
24
+ * - `'init'` — instantiate sha2 + serpent WASM modules and store derived keys
25
+ * - `'wipe'` — zero keys and WASM buffers, then post `{ type: 'wiped' }`
26
+ * - `{ op: 'seal' | 'open', ... }` — encrypt or decrypt one chunk
27
+ *
28
+ * Replies with `{ type: 'result', id, data }` on success or
29
+ * `{ type: 'error', id, message, isAuthError }` on failure.
30
+ */
102
31
  self.onmessage = async (e) => {
103
32
  const msg = e.data;
104
33
  if (msg.type === 'init') {
@@ -130,6 +59,7 @@ self.onmessage = async (e) => {
130
59
  serpent.wipeBuffers();
131
60
  sha2 = undefined;
132
61
  serpent = undefined;
62
+ self.postMessage({ type: 'wiped' });
133
63
  return;
134
64
  }
135
65
  if (!keys || !sha2 || !serpent) {
@@ -145,14 +75,14 @@ self.onmessage = async (e) => {
145
75
  const ivKey = jobKey.subarray(64, 96);
146
76
  let result;
147
77
  if (op === 'seal') {
148
- const ivFull = hmacSha256(ivKey, counterNonce);
78
+ const ivFull = hmacSha256(sha2, ivKey, counterNonce);
149
79
  const iv = ivFull.slice(0, 16);
150
80
  wipe(ivFull);
151
- const ct = cbcEncrypt(encKey, iv, data);
81
+ const ct = cbcEncryptChunk(serpent, encKey, iv, data);
152
82
  const aadLenBuf = new Uint8Array(4);
153
83
  new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
154
84
  const tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
155
- const tag = hmacSha256(macKey, tagInput);
85
+ const tag = hmacSha256(sha2, macKey, tagInput);
156
86
  result = concat(ct, tag);
157
87
  wipe(iv);
158
88
  wipe(tagInput);
@@ -160,13 +90,13 @@ self.onmessage = async (e) => {
160
90
  else {
161
91
  const ct = data.subarray(0, data.length - 32);
162
92
  const receivedTag = data.subarray(data.length - 32);
163
- const ivFull = hmacSha256(ivKey, counterNonce);
93
+ const ivFull = hmacSha256(sha2, ivKey, counterNonce);
164
94
  const iv = ivFull.slice(0, 16);
165
95
  wipe(ivFull);
166
96
  const aadLenBuf = new Uint8Array(4);
167
97
  new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
168
98
  const tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
169
- const expectedTag = hmacSha256(macKey, tagInput);
99
+ const expectedTag = hmacSha256(sha2, macKey, tagInput);
170
100
  // CRITICAL: verify HMAC before decrypting (Vaudenay 2002)
171
101
  if (!constantTimeEqual(expectedTag, receivedTag)) {
172
102
  wipe(iv);
@@ -176,7 +106,7 @@ self.onmessage = async (e) => {
176
106
  }
177
107
  wipe(tagInput);
178
108
  wipe(expectedTag);
179
- result = cbcDecrypt(encKey, iv, ct);
109
+ result = cbcDecryptChunk(serpent, encKey, iv, ct);
180
110
  wipe(iv);
181
111
  }
182
112
  const transfer = result.buffer instanceof ArrayBuffer ? [result.buffer] : [];
@@ -3,13 +3,18 @@
3
3
  *
4
4
  * **WARNING: CBC mode is unauthenticated.** Always authenticate the output
5
5
  * with HMAC-SHA256 (Encrypt-then-MAC) or use `XChaCha20Poly1305` instead.
6
+ *
7
+ * Holds exclusive access to the `serpent` WASM module from construction
8
+ * until `dispose()`. Constructing a second SerpentCbc/SerpentCtr/
9
+ * SerpentCipher or any other serpent user while this instance is live
10
+ * throws. Call `dispose()` when done.
6
11
  */
7
12
  export declare class SerpentCbc {
8
13
  private readonly x;
14
+ private _tok;
9
15
  constructor(opts?: {
10
16
  dangerUnauthenticated: true;
11
17
  });
12
- private get mem();
13
18
  /**
14
19
  * Encrypt plaintext with Serpent-256 CBC + PKCS7 padding.
15
20
  *
@@ -21,10 +26,15 @@ export declare class SerpentCbc {
21
26
  encrypt(key: Uint8Array, iv: Uint8Array, plaintext: Uint8Array): Uint8Array;
22
27
  /**
23
28
  * Decrypt Serpent-256 CBC + PKCS7.
24
- * Throws if ciphertext length is not a non-zero multiple of 16 or PKCS7 is invalid.
29
+ *
30
+ * All failure modes — empty input, non-multiple-of-16 length, and any
31
+ * PKCS7 validation failure — throw the same generic `RangeError` with
32
+ * message `'invalid ciphertext'`. Padding validation runs branch-free
33
+ * over the last 16 bytes regardless of where the mismatch is, closing
34
+ * the Vaudenay 2002 padding-oracle surface for callers using
35
+ * `{ dangerUnauthenticated: true }` without an outer HMAC.
25
36
  */
26
37
  decrypt(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Uint8Array;
38
+ /** Wipe WASM state and release exclusive module access. Idempotent. */
27
39
  dispose(): void;
28
- private _loadKey;
29
- private _setIv;
30
40
  }