leviathan-crypto 2.0.1 → 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 (93) hide show
  1. package/CLAUDE.md +171 -7
  2. package/LICENSE +4 -0
  3. package/README.md +109 -54
  4. package/SECURITY.md +125 -238
  5. package/dist/chacha20/cipher-suite.d.ts +10 -0
  6. package/dist/chacha20/cipher-suite.js +65 -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 +66 -26
  18. package/dist/docs/architecture.md +600 -521
  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 +155 -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/fortuna.d.ts +14 -8
  37. package/dist/fortuna.js +144 -50
  38. package/dist/index.d.ts +8 -6
  39. package/dist/index.js +6 -5
  40. package/dist/init.d.ts +0 -2
  41. package/dist/init.js +83 -3
  42. package/dist/kyber/indcpa.js +4 -4
  43. package/dist/kyber/index.js +25 -5
  44. package/dist/kyber/kem.js +56 -1
  45. package/dist/kyber/suite.d.ts +1 -2
  46. package/dist/kyber/types.d.ts +1 -0
  47. package/dist/kyber/validate.d.ts +8 -4
  48. package/dist/kyber/validate.js +18 -14
  49. package/dist/kyber.wasm +0 -0
  50. package/dist/loader.d.ts +7 -2
  51. package/dist/loader.js +25 -28
  52. package/dist/ratchet/index.d.ts +6 -0
  53. package/dist/ratchet/index.js +37 -0
  54. package/dist/ratchet/kdf-chain.d.ts +13 -0
  55. package/dist/ratchet/kdf-chain.js +85 -0
  56. package/dist/ratchet/ratchet-keypair.d.ts +9 -0
  57. package/dist/ratchet/ratchet-keypair.js +61 -0
  58. package/dist/ratchet/root-kdf.d.ts +4 -0
  59. package/dist/ratchet/root-kdf.js +124 -0
  60. package/dist/ratchet/skipped-key-store.d.ts +14 -0
  61. package/dist/ratchet/skipped-key-store.js +154 -0
  62. package/dist/ratchet/types.d.ts +36 -0
  63. package/dist/ratchet/types.js +26 -0
  64. package/dist/serpent/cipher-suite.d.ts +10 -0
  65. package/dist/serpent/cipher-suite.js +135 -50
  66. package/dist/serpent/generator.d.ts +12 -0
  67. package/dist/serpent/generator.js +97 -0
  68. package/dist/serpent/index.d.ts +61 -1
  69. package/dist/serpent/index.js +92 -7
  70. package/dist/serpent/pool-worker.js +25 -101
  71. package/dist/serpent/serpent-cbc.d.ts +14 -4
  72. package/dist/serpent/serpent-cbc.js +50 -32
  73. package/dist/serpent/shared-ops.d.ts +83 -0
  74. package/dist/serpent/shared-ops.js +213 -0
  75. package/dist/serpent/types.d.ts +1 -5
  76. package/dist/sha2/hash.d.ts +2 -0
  77. package/dist/sha2/hash.js +53 -0
  78. package/dist/sha2/index.d.ts +1 -0
  79. package/dist/sha2/index.js +15 -1
  80. package/dist/sha3/hash.d.ts +2 -0
  81. package/dist/sha3/hash.js +53 -0
  82. package/dist/sha3/index.d.ts +17 -2
  83. package/dist/sha3/index.js +79 -7
  84. package/dist/stream/header.js +5 -5
  85. package/dist/stream/open-stream.js +36 -14
  86. package/dist/stream/seal-stream-pool.d.ts +1 -0
  87. package/dist/stream/seal-stream-pool.js +38 -8
  88. package/dist/stream/seal-stream.js +29 -11
  89. package/dist/types.d.ts +21 -0
  90. package/dist/utils.d.ts +7 -8
  91. package/dist/utils.js +73 -40
  92. package/dist/wasm-source.d.ts +9 -8
  93. package/package.json +79 -64
@@ -24,53 +24,42 @@
24
24
  // SerpentCbc — Serpent-256 CBC + PKCS7, internal module.
25
25
  // Extracted to break the cipher-suite.ts ↔ index.ts circular dependency.
26
26
  // Import from here directly; index.ts re-exports for the public API surface.
27
- import { getInstance } from '../init.js';
27
+ import { getInstance, _acquireModule, _releaseModule } from '../init.js';
28
+ import { pkcs7Pad, pkcs7Strip, PKCS7_INVALID } from './shared-ops.js';
29
+ /** Returns the raw serpent WASM export object. @internal */
28
30
  function getExports() {
29
31
  return getInstance('serpent').exports;
30
32
  }
31
- // ── PKCS7 helpers ────────────────────────────────────────────────────────────
32
- function pkcs7Pad(data) {
33
- const padLen = 16 - (data.length % 16); // 1..16
34
- const out = new Uint8Array(data.length + padLen);
35
- out.set(data);
36
- out.fill(padLen, data.length);
37
- return out;
38
- }
39
- // pkcs7Strip is only called after HMAC authentication succeeds (verify-then-decrypt).
40
- // The early throw on invalid padLen is not a padding oracle in this context —
41
- // the HMAC check is the oracle gate and runs in constant time before this point.
42
- // If you move this call to a pre-auth site, revisit the timing properties.
43
- function pkcs7Strip(data) {
44
- if (data.length === 0)
45
- throw new RangeError('empty ciphertext');
46
- const padLen = data[data.length - 1];
47
- if (padLen === 0 || padLen > 16)
48
- throw new RangeError(`invalid PKCS7 padding byte: ${padLen}`);
49
- if (padLen > data.length)
50
- throw new RangeError(`invalid PKCS7 padding: pad length ${padLen} exceeds data length ${data.length}`);
51
- let bad = 0;
52
- for (let i = data.length - padLen; i < data.length; i++)
53
- bad |= data[i] ^ padLen;
54
- if (bad !== 0)
55
- throw new RangeError('invalid PKCS7 padding');
56
- return data.subarray(0, data.length - padLen);
57
- }
58
- // ── SerpentCbc ───────────────────────────────────────────────────────────────
33
+ // ── PKCS7 helpers ───────────────────────────────────────────────────────────
34
+ //
35
+ // The canonical `pkcs7Pad` / `pkcs7Strip` live in `./shared-ops.ts`; this
36
+ // file re-uses them so the main-thread class and the pool worker share one
37
+ // implementation. See `shared-ops.ts` for the branch-free, Vaudenay-2002-
38
+ // closed padding check.
39
+ // ── SerpentCbc ──────────────────────────────────────────────────────────────
59
40
  /**
60
41
  * Serpent-256 in CBC mode with PKCS7 padding.
61
42
  *
62
43
  * **WARNING: CBC mode is unauthenticated.** Always authenticate the output
63
44
  * with HMAC-SHA256 (Encrypt-then-MAC) or use `XChaCha20Poly1305` instead.
45
+ *
46
+ * Holds exclusive access to the `serpent` WASM module from construction
47
+ * until `dispose()`. Constructing a second SerpentCbc/SerpentCtr/
48
+ * SerpentCipher or any other serpent user while this instance is live
49
+ * throws. Call `dispose()` when done.
64
50
  */
65
51
  export class SerpentCbc {
66
52
  x;
53
+ _tok;
67
54
  constructor(opts) {
68
55
  if (!opts?.dangerUnauthenticated) {
69
56
  throw new Error('leviathan-crypto: SerpentCbc is unauthenticated — use Seal with SerpentCipher instead. ' +
70
57
  'To use SerpentCbc directly, pass { dangerUnauthenticated: true }.');
71
58
  }
72
59
  this.x = getExports();
60
+ this._tok = _acquireModule('serpent');
73
61
  }
62
+ /** View over WASM linear memory. Rebind on every access — memory can be detached after grow. @internal */
74
63
  get mem() {
75
64
  return new Uint8Array(this.x.memory.buffer);
76
65
  }
@@ -83,6 +72,8 @@ export class SerpentCbc {
83
72
  * @returns ciphertext (length = ceil((plaintext.length + 1) / 16) * 16)
84
73
  */
85
74
  encrypt(key, iv, plaintext) {
75
+ if (this._tok === undefined)
76
+ throw new Error('SerpentCbc: instance has been disposed');
86
77
  this._loadKey(key);
87
78
  this._setIv(iv);
88
79
  const padded = pkcs7Pad(plaintext);
@@ -103,11 +94,19 @@ export class SerpentCbc {
103
94
  }
104
95
  /**
105
96
  * Decrypt Serpent-256 CBC + PKCS7.
106
- * Throws if ciphertext length is not a non-zero multiple of 16 or PKCS7 is invalid.
97
+ *
98
+ * All failure modes — empty input, non-multiple-of-16 length, and any
99
+ * PKCS7 validation failure — throw the same generic `RangeError` with
100
+ * message `'invalid ciphertext'`. Padding validation runs branch-free
101
+ * over the last 16 bytes regardless of where the mismatch is, closing
102
+ * the Vaudenay 2002 padding-oracle surface for callers using
103
+ * `{ dangerUnauthenticated: true }` without an outer HMAC.
107
104
  */
108
105
  decrypt(key, iv, ciphertext) {
106
+ if (this._tok === undefined)
107
+ throw new Error('SerpentCbc: instance has been disposed');
109
108
  if (ciphertext.length === 0 || ciphertext.length % 16 !== 0)
110
- throw new RangeError('ciphertext length must be a non-zero multiple of 16');
109
+ throw new RangeError(PKCS7_INVALID);
111
110
  this._loadKey(key);
112
111
  this._setIv(iv);
113
112
  const output = new Uint8Array(ciphertext.length);
@@ -125,15 +124,34 @@ export class SerpentCbc {
125
124
  }
126
125
  return pkcs7Strip(output);
127
126
  }
127
+ /** Wipe WASM state and release exclusive module access. Idempotent. */
128
128
  dispose() {
129
- this.x.wipeBuffers();
129
+ if (this._tok === undefined)
130
+ return;
131
+ try {
132
+ this.x.wipeBuffers();
133
+ }
134
+ finally {
135
+ _releaseModule('serpent', this._tok);
136
+ this._tok = undefined;
137
+ }
130
138
  }
139
+ /**
140
+ * Validate and load `key` into the WASM key schedule.
141
+ * @param key 16, 24, or 32 bytes
142
+ * @internal
143
+ */
131
144
  _loadKey(key) {
132
145
  if (key.length !== 16 && key.length !== 24 && key.length !== 32)
133
146
  throw new RangeError(`Serpent key must be 16, 24, or 32 bytes (got ${key.length})`);
134
147
  this.mem.set(key, this.x.getKeyOffset());
135
148
  this.x.loadKey(key.length);
136
149
  }
150
+ /**
151
+ * Write `iv` into the WASM CBC IV buffer.
152
+ * @param iv 16 bytes
153
+ * @internal
154
+ */
137
155
  _setIv(iv) {
138
156
  if (iv.length !== 16)
139
157
  throw new RangeError(`CBC IV must be 16 bytes (got ${iv.length})`);
@@ -0,0 +1,83 @@
1
+ /** Subset of the sha2 WASM exports used by `hmacSha256`. */
2
+ export interface Sha2OpsExports {
3
+ memory: WebAssembly.Memory;
4
+ getSha256InputOffset: () => number;
5
+ getSha256OutOffset: () => number;
6
+ sha256Init: () => void;
7
+ sha256Update: (len: number) => void;
8
+ sha256Final: () => void;
9
+ hmac256Init: (keyLen: number) => void;
10
+ hmac256Update: (len: number) => void;
11
+ hmac256Final: () => void;
12
+ }
13
+ /** Subset of the serpent WASM exports used by `cbcEncryptChunk`/`cbcDecryptChunk`. */
14
+ export interface SerpentOpsExports {
15
+ memory: WebAssembly.Memory;
16
+ getKeyOffset: () => number;
17
+ getChunkPtOffset: () => number;
18
+ getChunkCtOffset: () => number;
19
+ getChunkSize: () => number;
20
+ getCbcIvOffset: () => number;
21
+ loadKey: (n: number) => number;
22
+ cbcEncryptChunk: (n: number) => number;
23
+ cbcDecryptChunk_simd: (n: number) => number;
24
+ }
25
+ export declare const PKCS7_INVALID = "invalid ciphertext";
26
+ /**
27
+ * Apply PKCS7 padding to `data` so the result length is a multiple of 16.
28
+ * Padding length is always 1–16 bytes so a full pad block is appended when
29
+ * `data.length` is already block-aligned.
30
+ * @param data Input bytes of any length
31
+ * @returns New Uint8Array padded to the next 16-byte boundary
32
+ */
33
+ export declare function pkcs7Pad(data: Uint8Array): Uint8Array;
34
+ /**
35
+ * Remove PKCS7 padding from a block-aligned buffer in constant time.
36
+ *
37
+ * Branch-free over all secret bits — padding length and per-byte comparisons
38
+ * are accumulated into a single `bad` flag with no early exit. Closes the
39
+ * Vaudenay 2002 padding-oracle surface. Throws a single generic
40
+ * `RangeError('invalid ciphertext')` for every failure mode: empty input,
41
+ * non-block-aligned length, padding byte out of range 1–16, and any per-byte
42
+ * mismatch in the padding region.
43
+ * @param data Block-aligned ciphertext (length must be a multiple of 16)
44
+ * @returns Plaintext with padding removed
45
+ */
46
+ export declare function pkcs7Strip(data: Uint8Array): Uint8Array;
47
+ /**
48
+ * Compute HMAC-SHA-256 using raw WASM sha2 exports.
49
+ *
50
+ * Keys longer than 64 bytes are pre-hashed per RFC 2104 §3. The SHA-256
51
+ * input buffer is fed in 64-byte chunks to match the WASM block size.
52
+ * Does not call `_acquireModule` — callers must ensure no stateful instance
53
+ * owns the sha2 module before calling.
54
+ * @param sx sha2 WASM exports
55
+ * @param key HMAC key of any length
56
+ * @param msg Message to authenticate
57
+ * @returns 32-byte HMAC-SHA-256 tag
58
+ */
59
+ export declare function hmacSha256(sx: Sha2OpsExports, key: Uint8Array, msg: Uint8Array): Uint8Array;
60
+ /**
61
+ * Encrypt one plaintext chunk with Serpent-256 CBC + PKCS7 padding.
62
+ *
63
+ * The padded chunk must fit within the WASM CHUNK_SIZE. Callers are
64
+ * responsible for splitting larger payloads before calling.
65
+ * @param kx Serpent WASM exports
66
+ * @param key 16, 24, or 32-byte key
67
+ * @param iv 16-byte CBC initialisation vector
68
+ * @param chunk Plaintext chunk (padded length must be ≤ WASM CHUNK_SIZE)
69
+ * @returns Ciphertext with PKCS7 padding applied
70
+ */
71
+ export declare function cbcEncryptChunk(kx: SerpentOpsExports, key: Uint8Array, iv: Uint8Array, chunk: Uint8Array): Uint8Array;
72
+ /**
73
+ * Decrypt one Serpent-256 CBC chunk using the SIMD path and strip PKCS7 padding.
74
+ *
75
+ * Output matches `SerpentCbc.decrypt` byte-for-byte. Throws
76
+ * `RangeError('invalid ciphertext')` on any length or padding failure.
77
+ * @param kx Serpent WASM exports
78
+ * @param key 16, 24, or 32-byte key
79
+ * @param iv 16-byte CBC initialisation vector
80
+ * @param ct Ciphertext (must be non-empty and a multiple of 16 bytes)
81
+ * @returns Plaintext with PKCS7 padding removed
82
+ */
83
+ export declare function cbcDecryptChunk(kx: SerpentOpsExports, key: Uint8Array, iv: Uint8Array, ct: Uint8Array): Uint8Array;
@@ -0,0 +1,213 @@
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/shared-ops.ts
23
+ //
24
+ // Pure-function primitives shared between the main-thread `SerpentCipher`
25
+ // (cipher-suite.ts) and the `SealStreamPool` worker (pool-worker.ts). Both
26
+ // call sites hold their own WASM exports — pool workers instantiate modules
27
+ // locally, the main thread fetches via `getInstance()` — so every function
28
+ // here takes the sha2/serpent exports as parameters. No dependency on
29
+ // `init.ts`, no module-level state, no instance wrappers.
30
+ //
31
+ // These helpers are strictly single-chunk: the caller already divided the
32
+ // payload into chunks ≤ WASM CHUNK_SIZE. For multi-chunk use, see
33
+ // `SerpentCbc.encrypt`/`decrypt`, which loop over the same WASM exports.
34
+ //
35
+ // This file owns the canonical `pkcs7Pad` / `pkcs7Strip`; `serpent-cbc.ts`
36
+ // re-exports them. A single source of truth keeps the branch-free,
37
+ // Vaudenay-2002-closed padding check identical on the main-thread and
38
+ // pool-worker paths — divergence between the two would reintroduce an
39
+ // oracle.
40
+ // ── PKCS7 ───────────────────────────────────────────────────────────────────
41
+ // Generic error string used by every failure mode of `pkcs7Strip` and the
42
+ // length/alignment gate in `SerpentCbc.decrypt`. No numeric leaks, no
43
+ // structural disclosure — a caller cannot distinguish "bad length" from
44
+ // "bad padding" by message or by timing.
45
+ export const PKCS7_INVALID = 'invalid ciphertext';
46
+ /**
47
+ * Apply PKCS7 padding to `data` so the result length is a multiple of 16.
48
+ * Padding length is always 1–16 bytes so a full pad block is appended when
49
+ * `data.length` is already block-aligned.
50
+ * @param data Input bytes of any length
51
+ * @returns New Uint8Array padded to the next 16-byte boundary
52
+ */
53
+ export function pkcs7Pad(data) {
54
+ const padLen = 16 - (data.length % 16); // 1..16
55
+ const out = new Uint8Array(data.length + padLen);
56
+ out.set(data);
57
+ out.fill(padLen, data.length);
58
+ return out;
59
+ }
60
+ /**
61
+ * Remove PKCS7 padding from a block-aligned buffer in constant time.
62
+ *
63
+ * Branch-free over all secret bits — padding length and per-byte comparisons
64
+ * are accumulated into a single `bad` flag with no early exit. Closes the
65
+ * Vaudenay 2002 padding-oracle surface. Throws a single generic
66
+ * `RangeError('invalid ciphertext')` for every failure mode: empty input,
67
+ * non-block-aligned length, padding byte out of range 1–16, and any per-byte
68
+ * mismatch in the padding region.
69
+ * @param data Block-aligned ciphertext (length must be a multiple of 16)
70
+ * @returns Plaintext with padding removed
71
+ */
72
+ export function pkcs7Strip(data) {
73
+ if (data.length === 0 || data.length % 16 !== 0)
74
+ throw new RangeError(PKCS7_INVALID);
75
+ const padLen = data[data.length - 1];
76
+ let bad = 0;
77
+ bad |= ((padLen - 1) >>> 31); // 1 if padLen == 0
78
+ bad |= ((16 - padLen) >>> 31); // 1 if padLen > 16
79
+ // Per-byte pad-region mask without branches on secret bits.
80
+ // inPadRegion = 0xff when i >= 16 - padLen
81
+ // = 0x00 otherwise
82
+ //
83
+ // (16 - padLen - i - 1) is negative iff i >= 16 - padLen. A signed
84
+ // arithmetic shift by 31 yields -1 for negative, 0 for non-negative;
85
+ // ANDing with 0xff collapses those to 0xff and 0x00.
86
+ for (let i = 0; i < 16; i++) {
87
+ const idx = data.length - 16 + i;
88
+ const mask = ((16 - padLen - i - 1) >> 31) & 0xff;
89
+ bad |= (data[idx] ^ padLen) & mask;
90
+ }
91
+ const invalid = ((bad - 1) >>> 31) ^ 1;
92
+ if (invalid)
93
+ throw new RangeError(PKCS7_INVALID);
94
+ return data.subarray(0, data.length - padLen);
95
+ }
96
+ // ── HMAC-SHA-256 ────────────────────────────────────────────────────────────
97
+ /**
98
+ * Compute HMAC-SHA-256 using raw WASM sha2 exports.
99
+ *
100
+ * Keys longer than 64 bytes are pre-hashed per RFC 2104 §3. The SHA-256
101
+ * input buffer is fed in 64-byte chunks to match the WASM block size.
102
+ * Does not call `_acquireModule` — callers must ensure no stateful instance
103
+ * owns the sha2 module before calling.
104
+ * @param sx sha2 WASM exports
105
+ * @param key HMAC key of any length
106
+ * @param msg Message to authenticate
107
+ * @returns 32-byte HMAC-SHA-256 tag
108
+ */
109
+ export function hmacSha256(sx, key, msg) {
110
+ const inOff = sx.getSha256InputOffset();
111
+ const outOff = sx.getSha256OutOffset();
112
+ let k = key;
113
+ if (k.length > 64) {
114
+ sx.sha256Init();
115
+ feedMemory(sx.memory, inOff, k, 64, sx.sha256Update);
116
+ sx.sha256Final();
117
+ k = new Uint8Array(sx.memory.buffer).slice(outOff, outOff + 32);
118
+ }
119
+ const mem = new Uint8Array(sx.memory.buffer);
120
+ mem.set(k, inOff);
121
+ sx.hmac256Init(k.length);
122
+ feedMemory(sx.memory, inOff, msg, 64, sx.hmac256Update);
123
+ sx.hmac256Final();
124
+ return new Uint8Array(sx.memory.buffer).slice(outOff, outOff + 32);
125
+ }
126
+ /**
127
+ * Copy `msg` into WASM linear memory at `inputOff` in `chunkSize`-byte
128
+ * increments, calling `update(n)` after each write.
129
+ * @param memory WASM memory object
130
+ * @param inputOff Byte offset of the WASM input buffer
131
+ * @param msg Data to feed
132
+ * @param chunkSize Maximum bytes per write (must match WASM buffer size)
133
+ * @param update WASM update function to call after each write
134
+ * @internal
135
+ */
136
+ function feedMemory(memory, inputOff, msg, chunkSize, update) {
137
+ const mem = new Uint8Array(memory.buffer);
138
+ let pos = 0;
139
+ while (pos < msg.length) {
140
+ const n = Math.min(msg.length - pos, chunkSize);
141
+ mem.set(msg.subarray(pos, pos + n), inputOff);
142
+ update(n);
143
+ pos += n;
144
+ }
145
+ }
146
+ // ── Serpent-CBC (single chunk) ──────────────────────────────────────────────
147
+ /**
148
+ * Encrypt one plaintext chunk with Serpent-256 CBC + PKCS7 padding.
149
+ *
150
+ * The padded chunk must fit within the WASM CHUNK_SIZE. Callers are
151
+ * responsible for splitting larger payloads before calling.
152
+ * @param kx Serpent WASM exports
153
+ * @param key 16, 24, or 32-byte key
154
+ * @param iv 16-byte CBC initialisation vector
155
+ * @param chunk Plaintext chunk (padded length must be ≤ WASM CHUNK_SIZE)
156
+ * @returns Ciphertext with PKCS7 padding applied
157
+ */
158
+ export function cbcEncryptChunk(kx, key, iv, chunk) {
159
+ loadKeyAndIv(kx, key, iv);
160
+ const padded = pkcs7Pad(chunk);
161
+ const ptOff = kx.getChunkPtOffset();
162
+ const ctOff = kx.getChunkCtOffset();
163
+ const mem = new Uint8Array(kx.memory.buffer);
164
+ mem.set(padded, ptOff);
165
+ const ret = kx.cbcEncryptChunk(padded.length);
166
+ if (ret < 0)
167
+ throw new RangeError(`cbcEncryptChunk rejected len=${padded.length}` +
168
+ ` (WASM CHUNK_SIZE=${kx.getChunkSize()})`);
169
+ return new Uint8Array(kx.memory.buffer).slice(ctOff, ctOff + padded.length);
170
+ }
171
+ /**
172
+ * Decrypt one Serpent-256 CBC chunk using the SIMD path and strip PKCS7 padding.
173
+ *
174
+ * Output matches `SerpentCbc.decrypt` byte-for-byte. Throws
175
+ * `RangeError('invalid ciphertext')` on any length or padding failure.
176
+ * @param kx Serpent WASM exports
177
+ * @param key 16, 24, or 32-byte key
178
+ * @param iv 16-byte CBC initialisation vector
179
+ * @param ct Ciphertext (must be non-empty and a multiple of 16 bytes)
180
+ * @returns Plaintext with PKCS7 padding removed
181
+ */
182
+ export function cbcDecryptChunk(kx, key, iv, ct) {
183
+ if (ct.length === 0 || ct.length % 16 !== 0)
184
+ throw new RangeError(PKCS7_INVALID);
185
+ loadKeyAndIv(kx, key, iv);
186
+ const ctOff = kx.getChunkCtOffset();
187
+ const ptOff = kx.getChunkPtOffset();
188
+ const mem = new Uint8Array(kx.memory.buffer);
189
+ mem.set(ct, ctOff);
190
+ const ret = kx.cbcDecryptChunk_simd(ct.length);
191
+ if (ret < 0)
192
+ throw new RangeError(`cbcDecryptChunk_simd rejected len=${ct.length}` +
193
+ ` (WASM CHUNK_SIZE=${kx.getChunkSize()})`);
194
+ const raw = new Uint8Array(kx.memory.buffer).slice(ptOff, ptOff + ct.length);
195
+ return pkcs7Strip(raw);
196
+ }
197
+ /**
198
+ * Validate, then write `key` and `iv` into the WASM buffers and expand the key schedule.
199
+ * @param kx Serpent WASM exports
200
+ * @param key 16, 24, or 32-byte Serpent key
201
+ * @param iv 16-byte CBC initialisation vector
202
+ * @internal
203
+ */
204
+ function loadKeyAndIv(kx, key, iv) {
205
+ if (key.length !== 16 && key.length !== 24 && key.length !== 32)
206
+ throw new RangeError(`Serpent key must be 16, 24, or 32 bytes (got ${key.length})`);
207
+ if (iv.length !== 16)
208
+ throw new RangeError(`CBC IV must be 16 bytes (got ${iv.length})`);
209
+ const mem = new Uint8Array(kx.memory.buffer);
210
+ mem.set(key, kx.getKeyOffset());
211
+ kx.loadKey(key.length);
212
+ mem.set(iv, kx.getCbcIvOffset());
213
+ }
@@ -1,5 +1 @@
1
- /** WASM exports for the serpent module */
2
- export interface SerpentExports {
3
- memory: WebAssembly.Memory;
4
- getModuleId(): number;
5
- }
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { HashFn } from '../types.js';
2
+ export declare const SHA256Hash: HashFn;
@@ -0,0 +1,53 @@
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/sha2/hash.ts
23
+ //
24
+ // Stateless SHA-256 HashFn for Fortuna's accumulator and reseed slots.
25
+ import { _assertNotOwned, getInstance } from '../init.js';
26
+ export const SHA256Hash = {
27
+ outputSize: 32,
28
+ wasmModules: ['sha2'],
29
+ digest(msg) {
30
+ _assertNotOwned('sha2');
31
+ const x = getInstance('sha2').exports;
32
+ const mem = new Uint8Array(x.memory.buffer);
33
+ try {
34
+ x.sha256Init();
35
+ const inOff = x.getSha256InputOffset();
36
+ let pos = 0;
37
+ while (pos < msg.length) {
38
+ const n = Math.min(msg.length - pos, 64);
39
+ mem.set(msg.subarray(pos, pos + n), inOff);
40
+ x.sha256Update(n);
41
+ pos += n;
42
+ }
43
+ x.sha256Final();
44
+ const outOff = x.getSha256OutOffset();
45
+ return mem.slice(outOff, outOff + 32);
46
+ }
47
+ finally {
48
+ // Wipe the SHA-256 input/output/state scratch so secret-derived
49
+ // inputs (e.g. Fortuna pool entropy) do not outlive this call.
50
+ x.wipeBuffers();
51
+ }
52
+ },
53
+ };
@@ -39,3 +39,4 @@ export declare class HMAC_SHA384 {
39
39
  dispose(): void;
40
40
  }
41
41
  export { HKDF_SHA256, HKDF_SHA512 } from './hkdf.js';
42
+ export { SHA256Hash } from './hash.js';
@@ -23,7 +23,7 @@
23
23
  //
24
24
  // Public API classes for the SHA-2 WASM module.
25
25
  // Uses the init() module cache — call sha2Init(source) before constructing.
26
- import { getInstance, initModule } from '../init.js';
26
+ import { getInstance, initModule, _assertNotOwned } from '../init.js';
27
27
  export async function sha2Init(source) {
28
28
  return initModule('sha2', source);
29
29
  }
@@ -57,6 +57,7 @@ export class SHA256 {
57
57
  this.x = getExports();
58
58
  }
59
59
  hash(msg) {
60
+ _assertNotOwned('sha2');
60
61
  this.x.sha256Init();
61
62
  feedHash(this.x, msg, this.x.getSha256InputOffset(), 64, this.x.sha256Update);
62
63
  this.x.sha256Final();
@@ -64,6 +65,7 @@ export class SHA256 {
64
65
  return mem.slice(this.x.getSha256OutOffset(), this.x.getSha256OutOffset() + 32);
65
66
  }
66
67
  dispose() {
68
+ _assertNotOwned('sha2');
67
69
  this.x.wipeBuffers();
68
70
  }
69
71
  }
@@ -74,6 +76,7 @@ export class SHA512 {
74
76
  this.x = getExports();
75
77
  }
76
78
  hash(msg) {
79
+ _assertNotOwned('sha2');
77
80
  this.x.sha512Init();
78
81
  feedHash(this.x, msg, this.x.getSha512InputOffset(), 128, this.x.sha512Update);
79
82
  this.x.sha512Final();
@@ -81,6 +84,7 @@ export class SHA512 {
81
84
  return mem.slice(this.x.getSha512OutOffset(), this.x.getSha512OutOffset() + 64);
82
85
  }
83
86
  dispose() {
87
+ _assertNotOwned('sha2');
84
88
  this.x.wipeBuffers();
85
89
  }
86
90
  }
@@ -91,6 +95,7 @@ export class SHA384 {
91
95
  this.x = getExports();
92
96
  }
93
97
  hash(msg) {
98
+ _assertNotOwned('sha2');
94
99
  this.x.sha384Init();
95
100
  feedHash(this.x, msg, this.x.getSha512InputOffset(), 128, this.x.sha512Update);
96
101
  this.x.sha384Final();
@@ -98,6 +103,7 @@ export class SHA384 {
98
103
  return mem.slice(this.x.getSha512OutOffset(), this.x.getSha512OutOffset() + 48);
99
104
  }
100
105
  dispose() {
106
+ _assertNotOwned('sha2');
101
107
  this.x.wipeBuffers();
102
108
  }
103
109
  }
@@ -108,6 +114,7 @@ export class HMAC_SHA256 {
108
114
  this.x = getExports();
109
115
  }
110
116
  hash(key, msg) {
117
+ _assertNotOwned('sha2');
111
118
  let k = key;
112
119
  // RFC 2104 §3: keys longer than block size are pre-hashed
113
120
  if (k.length > 64) {
@@ -126,6 +133,7 @@ export class HMAC_SHA256 {
126
133
  return out.slice(this.x.getSha256OutOffset(), this.x.getSha256OutOffset() + 32);
127
134
  }
128
135
  dispose() {
136
+ _assertNotOwned('sha2');
129
137
  this.x.wipeBuffers();
130
138
  }
131
139
  }
@@ -136,6 +144,7 @@ export class HMAC_SHA512 {
136
144
  this.x = getExports();
137
145
  }
138
146
  hash(key, msg) {
147
+ _assertNotOwned('sha2');
139
148
  let k = key;
140
149
  // RFC 2104 §3: keys longer than block size (128) are pre-hashed
141
150
  if (k.length > 128) {
@@ -154,6 +163,7 @@ export class HMAC_SHA512 {
154
163
  return out.slice(this.x.getSha512OutOffset(), this.x.getSha512OutOffset() + 64);
155
164
  }
156
165
  dispose() {
166
+ _assertNotOwned('sha2');
157
167
  this.x.wipeBuffers();
158
168
  }
159
169
  }
@@ -164,6 +174,7 @@ export class HMAC_SHA384 {
164
174
  this.x = getExports();
165
175
  }
166
176
  hash(key, msg) {
177
+ _assertNotOwned('sha2');
167
178
  let k = key;
168
179
  // RFC 2104 §3: keys longer than block size (128) are pre-hashed with SHA-384
169
180
  if (k.length > 128) {
@@ -182,8 +193,11 @@ export class HMAC_SHA384 {
182
193
  return out.slice(this.x.getSha512OutOffset(), this.x.getSha512OutOffset() + 48);
183
194
  }
184
195
  dispose() {
196
+ _assertNotOwned('sha2');
185
197
  this.x.wipeBuffers();
186
198
  }
187
199
  }
188
200
  // ── HKDF ────────────────────────────────────────────────────────────────────
189
201
  export { HKDF_SHA256, HKDF_SHA512 } from './hkdf.js';
202
+ // ── SHA256Hash ──────────────────────────────────────────────────────────────
203
+ export { SHA256Hash } from './hash.js';
@@ -0,0 +1,2 @@
1
+ import type { HashFn } from '../types.js';
2
+ export declare const SHA3_256Hash: HashFn;
@@ -0,0 +1,53 @@
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/sha3/hash.ts
23
+ //
24
+ // Stateless SHA3-256 HashFn for Fortuna's accumulator and reseed slots.
25
+ import { _assertNotOwned, getInstance } from '../init.js';
26
+ export const SHA3_256Hash = {
27
+ outputSize: 32,
28
+ wasmModules: ['sha3'],
29
+ digest(msg) {
30
+ _assertNotOwned('sha3');
31
+ const x = getInstance('sha3').exports;
32
+ const mem = new Uint8Array(x.memory.buffer);
33
+ try {
34
+ x.sha3_256Init();
35
+ const inOff = x.getInputOffset();
36
+ let pos = 0;
37
+ while (pos < msg.length) {
38
+ const n = Math.min(msg.length - pos, 168);
39
+ mem.set(msg.subarray(pos, pos + n), inOff);
40
+ x.keccakAbsorb(n);
41
+ pos += n;
42
+ }
43
+ x.sha3_256Final();
44
+ const outOff = x.getOutOffset();
45
+ return mem.slice(outOff, outOff + 32);
46
+ }
47
+ finally {
48
+ // Wipe the keccak input/output/sponge state so secret-derived
49
+ // inputs (e.g. Fortuna pool entropy) do not outlive this call.
50
+ x.wipeBuffers();
51
+ }
52
+ },
53
+ };