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
package/dist/utils.js CHANGED
@@ -23,13 +23,17 @@
23
23
  //
24
24
  // Pure TypeScript utilities — no init() dependency.
25
25
  // Ported from leviathan/src/base.ts (Convert namespace, Util namespace, constantTimeEqual).
26
- // ── Encoding ─────────────────────────────────────────────────────────────────
27
- /** Hex string to Uint8Array. Accepts lowercase/uppercase, optional 0x prefix. Throws RangeError on odd-length input. */
26
+ // ── Encoding ────────────────────────────────────────────────────────────────
27
+ /** Hex string to Uint8Array. Accepts lowercase/uppercase, optional 0x prefix. Throws RangeError on odd-length or non-hex input. */
28
28
  export const hexToBytes = (hex) => {
29
29
  if (hex.startsWith('0x') || hex.startsWith('0X'))
30
30
  hex = hex.slice(2);
31
31
  if (hex.length % 2)
32
32
  throw new RangeError(`hexToBytes: odd-length string (${hex.length} chars) — input must be an even-length hex string`);
33
+ // parseInt('0g', 16) returns 0 (not NaN) because it stops at the first
34
+ // invalid char — silent wrong-answer. Reject non-hex chars up front.
35
+ if (hex.length > 0 && !/^[0-9a-fA-F]*$/.test(hex))
36
+ throw new RangeError('hexToBytes: input contains non-hex characters');
33
37
  const bin = new Uint8Array(hex.length >>> 1);
34
38
  for (let i = 0, len = hex.length >>> 1; i < len; i++)
35
39
  bin[i] = parseInt(hex.slice(i << 1, (i << 1) + 2), 16);
@@ -51,20 +55,20 @@ export const utf8ToBytes = (str) => {
51
55
  export const bytesToUtf8 = (bytes) => {
52
56
  return new TextDecoder().decode(bytes);
53
57
  };
54
- /** Base64 or base64url string to Uint8Array. Handles padded, unpadded, and legacy %3d padding. Returns undefined on invalid input. */
58
+ /** Base64 or base64url string to Uint8Array. Handles padded, unpadded, and legacy %3d padding. Throws RangeError on invalid input. */
55
59
  export const base64ToBytes = (b64) => {
56
60
  // Normalise base64url → base64
57
61
  b64 = b64.replace(/-/g, '+').replace(/_/g, '/').replace(/%3d/gi, '=');
58
62
  // Re-pad if unpadded (RFC 4648 §5 base64url omits '=')
59
63
  const rem = b64.length % 4;
60
64
  if (rem === 1)
61
- return undefined; // always invalid no valid b64 produces this
65
+ throw new RangeError('base64ToBytes: invalid base64 input'); // no valid b64 produces this
62
66
  if (rem === 2)
63
67
  b64 += '==';
64
68
  if (rem === 3)
65
69
  b64 += '=';
66
70
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(b64))
67
- return undefined;
71
+ throw new RangeError('base64ToBytes: invalid base64 input');
68
72
  let strlen = b64.length / 4 * 3;
69
73
  if (b64.charAt(b64.length - 1) === '=')
70
74
  strlen--;
@@ -75,7 +79,7 @@ export const base64ToBytes = (b64) => {
75
79
  return new Uint8Array(atob(b64).split('').map(c => c.charCodeAt(0)));
76
80
  }
77
81
  catch {
78
- return undefined;
82
+ throw new RangeError('base64ToBytes: invalid base64 input');
79
83
  }
80
84
  }
81
85
  // Fallback: manual decode
@@ -136,65 +140,90 @@ export const bytesToBase64 = (bytes, url = false) => {
136
140
  }
137
141
  return base64;
138
142
  };
139
- // ── Constant-time comparison ─────────────────────────────────────────────────
143
+ // ── Constant-time comparison ────────────────────────────────────────────────
140
144
  import { CT_WASM } from './ct-wasm.js';
141
145
  let _ctCompare = null;
142
146
  let _ctMem = null;
143
147
  let _ctInit = false;
148
+ let _ctInitError = null;
144
149
  // CT WASM module uses 1 page (64KB) of linear memory with both buffers
145
150
  // laid out side-by-side: a at offset 0, b at offset a.length.
146
151
  // Max per-side = _ctMem.buffer.byteLength >>> 1 = 32768 bytes.
147
152
  // In practice the largest comparison is a 32-byte HMAC-SHA-256 tag.
148
153
  export const CT_MAX_BYTES = 32768;
149
- /** Try to compile the SIMD WASM ct module. Returns false if unavailable. */
154
+ /**
155
+ * Compile and instantiate the SIMD WASM ct module. On failure, caches the
156
+ * branded error and re-throws on every subsequent call; no retries, no
157
+ * fallback. Throws on runtimes without WebAssembly SIMD and on any
158
+ * instantiation error.
159
+ */
150
160
  function _initCt() {
151
- if (_ctInit)
152
- return _ctCompare !== null;
161
+ if (_ctInit) {
162
+ if (_ctInitError)
163
+ throw _ctInitError;
164
+ return;
165
+ }
153
166
  _ctInit = true;
167
+ if (!hasSIMD()) {
168
+ _ctInitError = new Error('leviathan-crypto: constantTimeEqual requires WebAssembly SIMD — '
169
+ + 'this runtime does not support it');
170
+ throw _ctInitError;
171
+ }
154
172
  try {
155
- if (!hasSIMD())
156
- return false;
157
- _ctMem = new WebAssembly.Memory({ initial: 1, maximum: 1 });
158
173
  const buf = CT_WASM.buffer.slice(CT_WASM.byteOffset, CT_WASM.byteOffset + CT_WASM.byteLength);
159
174
  const mod = new WebAssembly.Module(buf);
160
- const inst = new WebAssembly.Instance(mod, { env: { memory: _ctMem } });
161
- _ctCompare = inst.exports.compare;
162
- return true;
175
+ const inst = new WebAssembly.Instance(mod);
176
+ const exports = inst.exports;
177
+ _ctMem = exports.memory;
178
+ _ctCompare = exports.compare;
163
179
  }
164
- catch {
165
- return false;
180
+ catch (cause) {
181
+ _ctInitError = new Error(`leviathan-crypto: ct WASM module failed to instantiate: ${cause.message}`);
182
+ throw _ctInitError;
166
183
  }
167
184
  }
168
185
  /**
169
186
  * Constant-time byte-array equality.
170
- * Uses WASM SIMD when available (no JIT short-circuiting, no speculative
171
- * optimization). Falls back to a JS XOR-accumulate loop on runtimes
172
- * without SIMD support.
173
- * Length check is not constant-time (length is non-secret in all protocols).
174
- * Max input size: 32768 bytes per side (enforced regardless of code path).
187
+ * Runs entirely inside a WASM SIMD module (v128 XOR accumulate with
188
+ * branch-free reduction). Throws on runtimes without SIMD support
189
+ * no JS fallback. Length check is not constant-time (length is
190
+ * non-secret in all protocols). Max input size: 32768 bytes per side.
175
191
  */
176
192
  export const constantTimeEqual = (a, b) => {
177
193
  if (a.length !== b.length)
178
194
  return false;
179
195
  if (a.length > CT_MAX_BYTES)
180
196
  throw new RangeError(`constantTimeEqual: max ${CT_MAX_BYTES} bytes (got ${a.length})`);
181
- if (_initCt() && _ctMem && _ctCompare) {
182
- const mem = new Uint8Array(_ctMem.buffer);
183
- mem.set(a, 0);
184
- mem.set(b, a.length);
185
- try {
186
- return _ctCompare(0, a.length, a.length) === 1;
187
- }
188
- finally {
189
- mem.fill(0, 0, a.length * 2);
190
- }
197
+ _initCt();
198
+ // Copy module-level refs to locals. _initCt() either populates both
199
+ // _ctMem and _ctCompare or throws; the null check below is a defensive
200
+ // invariant guard that is unreachable on a correctly-initialized module.
201
+ const memObj = _ctMem;
202
+ const compare = _ctCompare;
203
+ if (!memObj || !compare)
204
+ throw new Error('leviathan-crypto: ct init invariant violated');
205
+ const mem = new Uint8Array(memObj.buffer);
206
+ mem.set(a, 0);
207
+ mem.set(b, a.length);
208
+ try {
209
+ return compare(0, a.length, a.length) === 1;
210
+ }
211
+ finally {
212
+ mem.fill(0, 0, a.length * 2);
191
213
  }
192
- // JS fallback — best-effort constant-time via XOR accumulate
193
- let diff = 0;
194
- for (let i = 0; i < a.length; i++)
195
- diff |= a[i] ^ b[i];
196
- return diff === 0;
197
214
  };
215
+ /**
216
+ * Reset the internal CT WASM cache, including any cached initialization
217
+ * error. Exists so the test suite can force re-instantiation across
218
+ * describe blocks.
219
+ * @internal
220
+ */
221
+ export function _ctResetForTesting() {
222
+ _ctInit = false;
223
+ _ctCompare = null;
224
+ _ctMem = null;
225
+ _ctInitError = null;
226
+ }
198
227
  /** Zero a typed array in place. */
199
228
  export const wipe = (data) => {
200
229
  data.fill(0);
@@ -218,11 +247,15 @@ export const concat = (...arrays) => {
218
247
  };
219
248
  /** Cryptographically secure random bytes via Web Crypto API. */
220
249
  export const randomBytes = (n) => {
250
+ if (typeof globalThis.crypto === 'undefined'
251
+ || typeof globalThis.crypto.getRandomValues !== 'function')
252
+ throw new Error('leviathan-crypto: crypto.getRandomValues is required — '
253
+ + 'this runtime does not expose the Web Crypto API');
221
254
  const buf = new Uint8Array(n);
222
- crypto.getRandomValues(buf);
255
+ globalThis.crypto.getRandomValues(buf);
223
256
  return buf;
224
257
  };
225
- // ── SIMD detection ───────────────────────────────────────────────────────────
258
+ // ── SIMD detection ──────────────────────────────────────────────────────────
226
259
  let _simd = null;
227
260
  /**
228
261
  * Detects WASM SIMD support once and caches the result.
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * All accepted forms of WASM input for init functions.
3
3
  *
4
- * - `string` — gzip+base64 embedded blob (from `/embedded` subpath)
5
- * - `URL` fetched via `WebAssembly.instantiateStreaming`
6
- * - `ArrayBuffer` — raw WASM bytes, compiled inline
7
- * - `Uint8Array` — raw WASM bytes, compiled inline
8
- * - `WebAssembly.Module` — pre-compiled module (Cloudflare Workers, edge runtimes)
9
- * - `Response` — streaming instantiation from an in-flight fetch response
10
- * - `Promise<Response>` streaming instantiation from a deferred fetch
4
+ * - `string` — gzip+base64 embedded blob (from `/embedded` subpath)
5
+ * - `URL` streaming-compiled from `fetch(url)`
6
+ * - `ArrayBuffer` — raw WASM bytes, compiled inline
7
+ * - `Uint8Array` — raw WASM bytes, compiled inline
8
+ * - `WebAssembly.Module` — pre-compiled module (Cloudflare Workers, edge runtimes)
9
+ * - `Response` — streaming-compiled from an in-flight fetch
10
+ * - `PromiseLike<WasmSource>` any thenable resolving to another `WasmSource`; nesting
11
+ * is resolved recursively (max depth 3).
11
12
  */
12
- export type WasmSource = string | URL | ArrayBuffer | Uint8Array | WebAssembly.Module | Response | Promise<Response>;
13
+ export type WasmSource = string | URL | ArrayBuffer | Uint8Array | WebAssembly.Module | Response | PromiseLike<WasmSource>;
package/package.json CHANGED
@@ -1,66 +1,81 @@
1
1
  {
2
- "name": "leviathan-crypto",
3
- "version": "2.0.0",
4
- "author": "xero (https://x-e.ro)",
5
- "license": "MIT",
6
- "description": "Zero-dependency WASM cryptography for TypeScript. Paranoid ciphers, post-quantum key encapsulation, and Fortuna CSPRNG behind a strictly typed API. All computation runs outside the JS JIT on vector-verified primitives.",
7
- "type": "module",
8
- "sideEffects": false,
9
- "exports": {
10
- ".": "./dist/index.js",
11
- "./stream": "./dist/stream/index.js",
12
- "./serpent": "./dist/serpent/index.js",
13
- "./serpent/embedded": "./dist/serpent/embedded.js",
14
- "./chacha20": "./dist/chacha20/index.js",
15
- "./chacha20/embedded": "./dist/chacha20/embedded.js",
16
- "./sha2": "./dist/sha2/index.js",
17
- "./sha2/embedded": "./dist/sha2/embedded.js",
18
- "./sha3": "./dist/sha3/index.js",
19
- "./sha3/embedded": "./dist/sha3/embedded.js",
20
- "./keccak": "./dist/keccak/index.js",
21
- "./keccak/embedded": "./dist/keccak/embedded.js",
22
- "./kyber": "./dist/kyber/index.js",
23
- "./kyber/embedded": "./dist/kyber/embedded.js"
24
- },
25
- "types": "./dist/index.d.ts",
26
- "files": [
27
- "dist",
28
- "SECURITY.md",
29
- "CLAUDE.md"
30
- ],
31
- "repository": {
32
- "type": "git",
33
- "url": "git+https://github.com/xero/leviathan-crypto.git"
34
- },
35
- "homepage": "https://leviathan.3xi.club",
36
- "bugs": {
37
- "url": "https://github.com/xero/leviathan-crypto/issues"
38
- },
39
- "keywords": [
40
- "cryptography",
41
- "encryption",
42
- "serpent",
43
- "serpent-256",
44
- "cipher",
45
- "crypto",
46
- "typescript",
47
- "wasm",
48
- "webassembly",
49
- "chacha20",
50
- "xchacha20",
51
- "poly1305",
52
- "xchacha20-poly1305",
53
- "aead",
54
- "sha",
55
- "sha-256",
56
- "sha-512",
57
- "sha-3",
58
- "keccak",
59
- "shake",
60
- "hmac",
61
- "hkdf",
62
- "fortuna",
63
- "csprng",
64
- "entropy"
65
- ]
2
+ "name": "leviathan-crypto",
3
+ "version": "2.1.0",
4
+ "author": "xero (https://x-e.ro)",
5
+ "license": "MIT",
6
+ "description": "Post-quantum WASM cryptography with paranoid ciphers (Serpent-256, XChaCha20-Poly1305), ML-KEM, and forward-secret ratcheting. Zero dependencies. Constant-time verified and strictly typed.",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": "./dist/index.js",
11
+ "./stream": "./dist/stream/index.js",
12
+ "./serpent": "./dist/serpent/index.js",
13
+ "./serpent/embedded": "./dist/serpent/embedded.js",
14
+ "./chacha20": "./dist/chacha20/index.js",
15
+ "./chacha20/embedded": "./dist/chacha20/embedded.js",
16
+ "./sha2": "./dist/sha2/index.js",
17
+ "./sha2/embedded": "./dist/sha2/embedded.js",
18
+ "./sha3": "./dist/sha3/index.js",
19
+ "./sha3/embedded": "./dist/sha3/embedded.js",
20
+ "./keccak": "./dist/keccak/index.js",
21
+ "./keccak/embedded": "./dist/keccak/embedded.js",
22
+ "./kyber": "./dist/kyber/index.js",
23
+ "./kyber/embedded": "./dist/kyber/embedded.js",
24
+ "./ratchet": "./dist/ratchet/index.js"
25
+ },
26
+ "types": "./dist/index.d.ts",
27
+ "files": [
28
+ "dist",
29
+ "SECURITY.md",
30
+ "CLAUDE.md"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/xero/leviathan-crypto.git"
35
+ },
36
+ "homepage": "https://leviathan.3xi.club",
37
+ "bugs": {
38
+ "url": "https://github.com/xero/leviathan-crypto/issues"
39
+ },
40
+ "keywords": [
41
+ "cryptography",
42
+ "encryption",
43
+ "crypto",
44
+ "typescript",
45
+ "wasm",
46
+ "webassembly",
47
+ "zero-dependency",
48
+ "constant-time",
49
+ "side-channel",
50
+ "serpent",
51
+ "serpent-256",
52
+ "cipher",
53
+ "aead",
54
+ "chacha20",
55
+ "xchacha20",
56
+ "poly1305",
57
+ "xchacha20-poly1305",
58
+ "kyber",
59
+ "ml-kem",
60
+ "mlkem",
61
+ "pqc",
62
+ "post-quantum",
63
+ "key-encapsulation",
64
+ "kem",
65
+ "double-ratchet",
66
+ "ratchet",
67
+ "forward-secrecy",
68
+ "spqr",
69
+ "sha",
70
+ "sha-256",
71
+ "sha-512",
72
+ "sha-3",
73
+ "keccak",
74
+ "shake",
75
+ "hmac",
76
+ "hkdf",
77
+ "fortuna",
78
+ "csprng",
79
+ "entropy"
80
+ ]
66
81
  }