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/kyber/kem.js CHANGED
@@ -33,6 +33,20 @@ import { wipe } from '../utils.js';
33
33
  export function kemKeypairDerand(kx, sx, params, d, z) {
34
34
  // indcpaKeypairDerand handles its own sigma wipe
35
35
  const { ekCpa, skCpa } = indcpaKeypairDerand(kx, sx, params, d);
36
+ // Wipe kyber WASM scratch regions that held the CPA secret key and the
37
+ // keygen noise. After kemKeypairDerand returns, no secret or secret-
38
+ // derived data persists in kyber linear memory until the next kyber op
39
+ // or MlKem.dispose(). SK_OFFSET holds skCpa packed via polyvec_tobytes
40
+ // — same severity class as the decap-side SK_OFFSET residual (R-028):
41
+ // long-lived key material whose disclosure compromises every ciphertext
42
+ // under the corresponding ek. POLYVEC_SLOT_1/2 hold ŝ and ê in NTT
43
+ // domain. XOF_PRF_OFFSET holds the last PRF output block. POLYVEC_SLOT_3
44
+ // (t̂) and POLYVEC_SLOT_0 (Â rows) are public and intentionally skipped.
45
+ const kyberMem = new Uint8Array(kx.memory.buffer);
46
+ kyberMem.fill(0, kx.getSkOffset(), kx.getSkOffset() + params.skCpaBytes);
47
+ kyberMem.fill(0, kx.getPolyvecSlot1(), kx.getPolyvecSlot1() + 2048);
48
+ kyberMem.fill(0, kx.getPolyvecSlot2(), kx.getPolyvecSlot2() + 2048);
49
+ kyberMem.fill(0, kx.getXofPrfOffset(), kx.getXofPrfOffset() + 1024);
36
50
  const h = sha3_256Hash(sx, ekCpa);
37
51
  try {
38
52
  const dk = new Uint8Array(params.dkBytes);
@@ -40,6 +54,7 @@ export function kemKeypairDerand(kx, sx, params, d, z) {
40
54
  dk.set(ekCpa, params.skCpaBytes);
41
55
  dk.set(h, params.skCpaBytes + params.ekBytes);
42
56
  dk.set(z, params.skCpaBytes + params.ekBytes + 32);
57
+ sx.wipeBuffers();
43
58
  return {
44
59
  encapsulationKey: ekCpa,
45
60
  decapsulationKey: dk,
@@ -68,6 +83,27 @@ export function kemEncapsulateDerand(kx, sx, params, ek, m) {
68
83
  const K = gOut.slice(0, 32);
69
84
  r = gOut.slice(32, 64);
70
85
  const c = indcpaEncrypt(kx, sx, params, ek, m, r);
86
+ // Wipe kyber WASM scratch regions that held m / r / e₁ / e₂ / u / v /
87
+ // m-poly / PRF output. After kemEncapsulateDerand returns, no secret
88
+ // or secret-derived data persists in kyber linear memory until the
89
+ // next kyber op or MlKem.dispose(). MSG_OFFSET holds raw m —
90
+ // reproducing the shared secret K = G(m ‖ H(ek))[0..32] only needs m
91
+ // plus the public ek, so this is the highest-severity encap residual.
92
+ // POLYVEC_SLOT_1/2/3 hold r, e₁, and uncompressed u (u compression is
93
+ // lossy for du ∈ {10,11} — uncompressed u reveals low-order bits the
94
+ // public ciphertext hides). POLY_SLOT_1/2/3 hold e₂ (full 512B), v,
95
+ // and the m-polynomial. XOF_PRF_OFFSET holds the last PRF block.
96
+ // PK_OFFSET, CT_OFFSET, POLYVEC_SLOT_0/4 are public — skipped.
97
+ const kyberMem = new Uint8Array(kx.memory.buffer);
98
+ kyberMem.fill(0, kx.getMsgOffset(), kx.getMsgOffset() + 32);
99
+ kyberMem.fill(0, kx.getPolyvecSlot1(), kx.getPolyvecSlot1() + 2048);
100
+ kyberMem.fill(0, kx.getPolyvecSlot2(), kx.getPolyvecSlot2() + 2048);
101
+ kyberMem.fill(0, kx.getPolyvecSlot3(), kx.getPolyvecSlot3() + 2048);
102
+ kyberMem.fill(0, kx.getPolySlot1(), kx.getPolySlot1() + 512);
103
+ kyberMem.fill(0, kx.getPolySlot2(), kx.getPolySlot2() + 512);
104
+ kyberMem.fill(0, kx.getPolySlot3(), kx.getPolySlot3() + 512);
105
+ kyberMem.fill(0, kx.getXofPrfOffset(), kx.getXofPrfOffset() + 1024);
106
+ sx.wipeBuffers();
71
107
  return { ciphertext: c, sharedSecret: K };
72
108
  }
73
109
  finally {
@@ -133,7 +169,26 @@ export function kemDecapsulate(kx, sx, params, dk, c) {
133
169
  const fail = kx.ct_verify(ctOff, ctPrimeOff, ctBytes);
134
170
  // If fail != 0 (mismatch): K' ← K̄
135
171
  kx.ct_cmov(kPrimeOff, kBarOff, 32, fail);
136
- return kyberMem.slice(kPrimeOff, kPrimeOff + 32);
172
+ const sharedSecret = kyberMem.slice(kPrimeOff, kPrimeOff + 32);
173
+ // Wipe kyber WASM scratch regions that held the CPA secret key (skCpa),
174
+ // m' / K' / K̄ / e₂ / r / e₁ / u, and the PRF output buffer. Without
175
+ // this, residual secret and secret-derived bytes persist in linear
176
+ // memory until the next kyber op or MlKem.dispose() — a window during
177
+ // which any other code with a handle to the kyber exports could read
178
+ // them. skCpa is the highest-severity residual: it compromises every
179
+ // ciphertext under the corresponding ek, not just this message.
180
+ kyberMem.fill(0, kx.getMsgOffset(), kx.getMsgOffset() + 32); // m' (bytes)
181
+ kyberMem.fill(0, kPrimeOff, kPrimeOff + 32); // K' (final shared secret)
182
+ kyberMem.fill(0, kBarOff, kBarOff + 512); // K̄ (first 32B) + e₂ poly tail
183
+ kyberMem.fill(0, kx.getPolySlot2(), kx.getPolySlot2() + 512); // m'-poly / v residual
184
+ kyberMem.fill(0, kx.getPolySlot3(), kx.getPolySlot3() + 512); // indcpa message poly
185
+ kyberMem.fill(0, kx.getPolyvecSlot1(), kx.getPolyvecSlot1() + 2048); // r (NTT-domain noise polyvec)
186
+ kyberMem.fill(0, kx.getPolyvecSlot2(), kx.getPolyvecSlot2() + 2048); // e₁ (noise polyvec for u)
187
+ kyberMem.fill(0, kx.getPolyvecSlot3(), kx.getPolyvecSlot3() + 2048); // uncompressed u polyvec from FO re-encryption
188
+ kyberMem.fill(0, kx.getXofPrfOffset(), kx.getXofPrfOffset() + 1024); // last PRF output block
189
+ kyberMem.fill(0, kx.getSkOffset(), kx.getSkOffset() + skCpaBytes); // CPA secret key (long-lived — highest severity residual)
190
+ sx.wipeBuffers();
191
+ return sharedSecret;
137
192
  }
138
193
  finally {
139
194
  if (mPrime)
@@ -1,7 +1,7 @@
1
1
  import type { CipherSuite } from '../stream/types.js';
2
2
  import type { KyberKeyPair, KyberEncapsulation } from './types.js';
3
3
  import type { KyberParams } from './params.js';
4
- interface MlKemLike {
4
+ export interface MlKemLike {
5
5
  readonly params: KyberParams;
6
6
  encapsulate(ek: Uint8Array): KyberEncapsulation;
7
7
  decapsulate(dk: Uint8Array, c: Uint8Array): Uint8Array;
@@ -10,4 +10,3 @@ interface MlKemLike {
10
10
  export declare function KyberSuite(kem: MlKemLike, inner: CipherSuite): CipherSuite & {
11
11
  keygen(): KyberKeyPair;
12
12
  };
13
- export {};
@@ -52,6 +52,7 @@ export function KyberSuite(kem, inner) {
52
52
  kemCtSize: p.ctBytes,
53
53
  tagSize: inner.tagSize,
54
54
  padded: inner.padded,
55
+ wasmChunkSize: inner.wasmChunkSize,
55
56
  wasmModules: [...inner.wasmModules, 'kyber', 'sha3'],
56
57
  deriveKeys(key, nonce, kemCt) {
57
58
  let sharedSecret;
@@ -63,6 +63,7 @@ export interface KyberExports {
63
63
  polyvec_reduce: (pvOffset: number, k: number) => void;
64
64
  polyvec_add: (rOffset: number, aOffset: number, bOffset: number, k: number) => void;
65
65
  polyvec_basemul_acc_montgomery: (rOffset: number, aOffset: number, bOffset: number, k: number) => void;
66
+ polyvec_modulus_check: (pvOffset: number, k: number) => number;
66
67
  rej_uniform: (polyOffset: number, ctrStart: number, bufOffset: number, buflen: number) => number;
67
68
  ct_verify: (aOffset: number, bOffset: number, len: number) => number;
68
69
  ct_cmov: (rOffset: number, xOffset: number, len: number, b: number) => void;
@@ -3,10 +3,14 @@ import type { KyberParams } from './params.js';
3
3
  /**
4
4
  * Encapsulation key check — FIPS 203 §7.2 (EncapsulationKeyCheck).
5
5
  *
6
- * 1. Length check: ek.length == params.ekBytes
7
- * 2. ByteDecode₁₂ ByteEncode₁₂ round-trip check on the polyvec portion.
8
- * Any coefficient q stored modulo 2^12 survives frombytes, but tobytes
9
- * re-encodes it differently so the round-trip fails iff any coeff was ≥ q.
6
+ * 1. Length gate: ek.length must equal params.ekBytes.
7
+ * 2. Decode the polyvec portion via ByteDecode₁₂ (polyvec_frombytes). The
8
+ * decoded coefficients are raw 12-bit values in [0, 4095] — frombytes
9
+ * does not reduce mod q.
10
+ * 3. Modulus scan: every coefficient must satisfy c < Q = 3329.
11
+ *
12
+ * Returns true iff both gates pass. The seed ρ (final 32 bytes of ek) is
13
+ * not checked; any 32-byte value is a valid ρ per FIPS 203.
10
14
  */
11
15
  export declare function checkEncapsulationKey(kx: KyberExports, params: KyberParams, ek: Uint8Array): boolean;
12
16
  /**
@@ -27,10 +27,14 @@ import { constantTimeEqual } from '../utils.js';
27
27
  /**
28
28
  * Encapsulation key check — FIPS 203 §7.2 (EncapsulationKeyCheck).
29
29
  *
30
- * 1. Length check: ek.length == params.ekBytes
31
- * 2. ByteDecode₁₂ ByteEncode₁₂ round-trip check on the polyvec portion.
32
- * Any coefficient q stored modulo 2^12 survives frombytes, but tobytes
33
- * re-encodes it differently so the round-trip fails iff any coeff was ≥ q.
30
+ * 1. Length gate: ek.length must equal params.ekBytes.
31
+ * 2. Decode the polyvec portion via ByteDecode₁₂ (polyvec_frombytes). The
32
+ * decoded coefficients are raw 12-bit values in [0, 4095] — frombytes
33
+ * does not reduce mod q.
34
+ * 3. Modulus scan: every coefficient must satisfy c < Q = 3329.
35
+ *
36
+ * Returns true iff both gates pass. The seed ρ (final 32 bytes of ek) is
37
+ * not checked; any 32-byte value is a valid ρ per FIPS 203.
34
38
  */
35
39
  export function checkEncapsulationKey(kx, params, ek) {
36
40
  if (ek.length !== params.ekBytes)
@@ -38,15 +42,10 @@ export function checkEncapsulationKey(kx, params, ek) {
38
42
  const { k } = params;
39
43
  const kyberMem = new Uint8Array(kx.memory.buffer);
40
44
  const pkOff = kx.getPkOffset();
41
- const skOff = kx.getSkOffset();
42
45
  const pvecOff = kx.getPolyvecSlot0();
43
- // Write the polyvec portion of ek into PK buffer, decode, re-encode
44
46
  kyberMem.set(ek.subarray(0, k * 384), pkOff);
45
47
  kx.polyvec_frombytes(pvecOff, pkOff, k);
46
- kx.polyvec_tobytes(skOff, pvecOff, k);
47
- // orig is at pkOff (written above); reEnc is at skOff (polyvec_tobytes output)
48
- const mismatch = kx.ct_verify(pkOff, skOff, k * 384);
49
- return mismatch === 0;
48
+ return kx.polyvec_modulus_check(pvecOff, k) === 0;
50
49
  }
51
50
  /**
52
51
  * Decapsulation key check — FIPS 203 §7.3 (DecapsulationKeyCheck).
@@ -61,8 +60,13 @@ export function checkDecapsulationKey(kx, sx, params, dk) {
61
60
  const { skCpaBytes, ekBytes } = params;
62
61
  const ek = dk.slice(skCpaBytes, skCpaBytes + ekBytes);
63
62
  const h = dk.slice(skCpaBytes + ekBytes, skCpaBytes + ekBytes + 32);
64
- const hComputed = sha3_256Hash(sx, ek);
65
- if (!constantTimeEqual(hComputed, h))
66
- return false;
67
- return checkEncapsulationKey(kx, params, ek);
63
+ try {
64
+ const hComputed = sha3_256Hash(sx, ek);
65
+ if (!constantTimeEqual(hComputed, h))
66
+ return false;
67
+ return checkEncapsulationKey(kx, params, ek);
68
+ }
69
+ finally {
70
+ sx.wipeBuffers();
71
+ }
68
72
  }
package/dist/kyber.wasm CHANGED
Binary file
package/dist/loader.d.ts CHANGED
@@ -8,12 +8,17 @@ export declare function decodeWasm(b64: string): Promise<Uint8Array>;
8
8
  /**
9
9
  * Compile a WASM source to a Module without instantiating.
10
10
  * Used by pool infrastructure to send compiled modules to workers.
11
+ *
12
+ * Thenable sources (Promise<Response>, Promise<ArrayBuffer>, etc.) are
13
+ * resolved and then re-dispatched by the runtime type of the resolved value.
14
+ * Depth is capped at `MAX_THENABLE_DEPTH` to prevent runaway recursion.
11
15
  */
12
- export declare function compileWasm(source: WasmSource): Promise<WebAssembly.Module>;
16
+ export declare function compileWasm(source: WasmSource, _depth?: number): Promise<WebAssembly.Module>;
13
17
  /**
14
18
  * Load a WASM module from any accepted source type.
15
19
  * The loading strategy is inferred from the argument type — no mode string.
16
20
  *
17
- * Throws `TypeError` for null, numeric, or unrecognised inputs.
21
+ * Throws `TypeError` for null, numeric, or unrecognised inputs, or if a
22
+ * thenable source nests deeper than `MAX_THENABLE_DEPTH`.
18
23
  */
19
24
  export declare function loadWasm(source: WasmSource): Promise<WebAssembly.Instance>;
package/dist/loader.js CHANGED
@@ -1,8 +1,4 @@
1
1
  import { base64ToBytes as _b64 } from './utils.js';
2
- // Each WASM module gets its own fresh Memory — never shared between instances.
3
- function makeImports() {
4
- return { env: { memory: new WebAssembly.Memory({ initial: 3, maximum: 3 }) } };
5
- }
6
2
  // TS 5.9 generified Uint8Array<TArrayBuffer> with default ArrayBufferLike, which
7
3
  // no longer satisfies BufferSource = ArrayBufferView<ArrayBuffer> | ArrayBuffer.
8
4
  // Convert Uint8Array to a proper ArrayBuffer before calling WebAssembly APIs.
@@ -22,9 +18,8 @@ export async function decodeWasm(b64) {
22
18
  if (typeof DecompressionStream === 'undefined')
23
19
  throw new Error('leviathan-crypto: DecompressionStream not available — '
24
20
  + 'use a URL, ArrayBuffer, or WebAssembly.Module source in this runtime');
21
+ // _b64 throws RangeError on invalid base64 — no nullish check required.
25
22
  const compressed = _b64(b64);
26
- if (!compressed)
27
- throw new Error('leviathan-crypto: corrupt embedded WASM — base64 decode failed');
28
23
  const ds = new DecompressionStream('gzip');
29
24
  const writer = ds.writable.getWriter();
30
25
  const reader = ds.readable.getReader();
@@ -44,11 +39,22 @@ export async function decodeWasm(b64) {
44
39
  }
45
40
  return out;
46
41
  }
42
+ // Max thenable nesting depth. A caller can pass `Promise<Response>` or even
43
+ // `Promise<Promise<Response>>` (e.g. deferred fetch wrapped in another async
44
+ // layer), but arbitrary `Promise<Promise<Promise<...>>>` chains would indicate
45
+ // a caller bug — cap at 3 levels and throw a clear error beyond that.
46
+ const MAX_THENABLE_DEPTH = 3;
47
47
  /**
48
48
  * Compile a WASM source to a Module without instantiating.
49
49
  * Used by pool infrastructure to send compiled modules to workers.
50
+ *
51
+ * Thenable sources (Promise<Response>, Promise<ArrayBuffer>, etc.) are
52
+ * resolved and then re-dispatched by the runtime type of the resolved value.
53
+ * Depth is capped at `MAX_THENABLE_DEPTH` to prevent runaway recursion.
50
54
  */
51
- export async function compileWasm(source) {
55
+ export async function compileWasm(source, _depth = 0) {
56
+ if (_depth > MAX_THENABLE_DEPTH)
57
+ throw new TypeError(`leviathan-crypto: thenable nesting too deep (max ${MAX_THENABLE_DEPTH})`);
52
58
  if (typeof source === 'string') {
53
59
  if (source.length === 0)
54
60
  throw new TypeError('leviathan-crypto: invalid WasmSource — empty string');
@@ -64,33 +70,24 @@ export async function compileWasm(source) {
64
70
  return source;
65
71
  if (typeof Response !== 'undefined' && source instanceof Response)
66
72
  return WebAssembly.compileStreaming(source);
67
- if (source != null && typeof source.then === 'function')
68
- return WebAssembly.compileStreaming(source);
73
+ if (source != null && typeof source.then === 'function') {
74
+ const resolved = await source;
75
+ return compileWasm(resolved, _depth + 1);
76
+ }
69
77
  throw new TypeError(`leviathan-crypto: invalid WasmSource — got ${source === null ? 'null' : typeof source}`);
70
78
  }
71
79
  /**
72
80
  * Load a WASM module from any accepted source type.
73
81
  * The loading strategy is inferred from the argument type — no mode string.
74
82
  *
75
- * Throws `TypeError` for null, numeric, or unrecognised inputs.
83
+ * Throws `TypeError` for null, numeric, or unrecognised inputs, or if a
84
+ * thenable source nests deeper than `MAX_THENABLE_DEPTH`.
76
85
  */
77
86
  export async function loadWasm(source) {
78
- if (typeof source === 'string') {
79
- if (source.length === 0)
80
- throw new TypeError('leviathan-crypto: invalid WasmSource — empty string');
81
- return (await WebAssembly.instantiate(toArrayBuffer(await decodeWasm(source)), makeImports())).instance;
82
- }
83
- if (source instanceof URL)
84
- return (await WebAssembly.instantiateStreaming(fetch(source.href), makeImports())).instance;
85
- if (source instanceof ArrayBuffer)
86
- return (await WebAssembly.instantiate(source, makeImports())).instance;
87
- if (source instanceof Uint8Array)
88
- return (await WebAssembly.instantiate(toArrayBuffer(source), makeImports())).instance;
89
- if (source instanceof WebAssembly.Module)
90
- return WebAssembly.instantiate(source, makeImports());
91
- if (typeof Response !== 'undefined' && source instanceof Response)
92
- return (await WebAssembly.instantiateStreaming(source, makeImports())).instance;
93
- if (source != null && typeof source.then === 'function')
94
- return (await WebAssembly.instantiateStreaming(source, makeImports())).instance;
95
- throw new TypeError(`leviathan-crypto: invalid WasmSource — got ${source === null ? 'null' : typeof source}`);
87
+ // All leviathan-crypto WASM modules export their own memory and import
88
+ // nothing from the host. If a future module needs imports, they would be
89
+ // computed and passed here.
90
+ // compileWasm already handles thenable resolution + depth capping.
91
+ const mod = await compileWasm(source);
92
+ return WebAssembly.instantiate(mod);
96
93
  }
@@ -0,0 +1,6 @@
1
+ export { KDFChain } from './kdf-chain.js';
2
+ export { ratchetInit, kemRatchetEncap, kemRatchetDecap } from './root-kdf.js';
3
+ export { SkippedKeyStore } from './skipped-key-store.js';
4
+ export { RatchetKeypair } from './ratchet-keypair.js';
5
+ export type { RatchetInitResult, KemEncapResult, KemDecapResult, MlKemLike, RatchetMessageHeader, ResolveHandle, SkippedKeyStoreOpts, } from './types.js';
6
+ export declare function ratchetReady(): boolean;
@@ -0,0 +1,37 @@
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/ratchet/index.ts
23
+ //
24
+ // Public barrel for the ratchet module.
25
+ export { KDFChain } from './kdf-chain.js';
26
+ export { ratchetInit, kemRatchetEncap, kemRatchetDecap } from './root-kdf.js';
27
+ export { SkippedKeyStore } from './skipped-key-store.js';
28
+ export { RatchetKeypair } from './ratchet-keypair.js';
29
+ import { isInitialized } from '../init.js';
30
+ export function ratchetReady() {
31
+ try {
32
+ return isInitialized('sha2');
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
@@ -0,0 +1,13 @@
1
+ export declare class KDFChain {
2
+ private _ck;
3
+ private _n;
4
+ private _disposed;
5
+ constructor(ck: Uint8Array);
6
+ step(): Uint8Array;
7
+ stepWithCounter(): {
8
+ key: Uint8Array;
9
+ counter: number;
10
+ };
11
+ get n(): number;
12
+ dispose(): void;
13
+ }
@@ -0,0 +1,85 @@
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/ratchet/kdf-chain.ts
23
+ //
24
+ // KDFChain — stateful symmetric ratchet chain step (spec §5.2, KDF_SCKA_CK).
25
+ // Each step derives a message key and advances the chain key via HKDF-SHA-256.
26
+ import { HKDF_SHA256 } from '../sha2/index.js';
27
+ import { isInitialized } from '../init.js';
28
+ import { wipe, concat, utf8ToBytes } from '../utils.js';
29
+ // Signal Double Ratchet §7.2 — chain step info string
30
+ const INFO_CHAIN_BYTES = utf8ToBytes('leviathan-ratchet-v1 Chain Step');
31
+ const ZERO_SALT = new Uint8Array(32);
32
+ export class KDFChain {
33
+ _ck;
34
+ _n;
35
+ _disposed;
36
+ constructor(ck) {
37
+ if (!isInitialized('sha2'))
38
+ throw new Error('leviathan-crypto: call init({ sha2: ... }) before using KDFChain');
39
+ if (ck.length !== 32)
40
+ throw new RangeError('KDFChain: ck must be 32 bytes');
41
+ this._ck = ck.slice();
42
+ this._n = 0;
43
+ this._disposed = false;
44
+ }
45
+ step() {
46
+ if (this._disposed)
47
+ throw new Error('KDFChain: instance has been disposed');
48
+ if (this._n >= Number.MAX_SAFE_INTEGER)
49
+ throw new RangeError('KDFChain: counter exceeds maximum safe integer');
50
+ const nextN = this._n + 1;
51
+ // Encode counter as big-endian uint64 — two u32 calls, no BigInt
52
+ const ctrBuf = new Uint8Array(8);
53
+ const dv = new DataView(ctrBuf.buffer);
54
+ dv.setUint32(0, Math.floor(nextN / 0x100000000), false);
55
+ dv.setUint32(4, nextN >>> 0, false);
56
+ const info = concat(INFO_CHAIN_BYTES, ctrBuf);
57
+ const h = new HKDF_SHA256();
58
+ try {
59
+ const okm = h.derive(this._ck, ZERO_SALT, info, 64);
60
+ const nextCk = okm.slice(0, 32);
61
+ const msgKey = okm.slice(32, 64);
62
+ wipe(this._ck);
63
+ this._ck = nextCk;
64
+ this._n = nextN;
65
+ wipe(okm);
66
+ return msgKey;
67
+ }
68
+ finally {
69
+ h.dispose();
70
+ }
71
+ }
72
+ // Returns both the message key and the post-step counter atomically.
73
+ // Eliminates the two-step step() + .n read pattern and the off-by-one risk.
74
+ stepWithCounter() {
75
+ const key = this.step();
76
+ return { key, counter: this._n };
77
+ }
78
+ get n() {
79
+ return this._n;
80
+ }
81
+ dispose() {
82
+ wipe(this._ck);
83
+ this._disposed = true;
84
+ }
85
+ }
@@ -0,0 +1,9 @@
1
+ import type { MlKemLike, KemDecapResult } from './types.js';
2
+ export declare class RatchetKeypair {
3
+ readonly ek: Uint8Array;
4
+ private _dk;
5
+ private _used;
6
+ constructor(kem: MlKemLike);
7
+ decap(kem: MlKemLike, rk: Uint8Array, kemCt: Uint8Array, context?: Uint8Array): KemDecapResult;
8
+ dispose(): void;
9
+ }
@@ -0,0 +1,61 @@
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/ratchet/ratchet-keypair.ts
23
+ //
24
+ // RatchetKeypair — single-use ek/dk lifecycle for one KEM ratchet step.
25
+ // Enforces the DR spec requirement that both parties rotate encapsulation
26
+ // keys after each KEM ratchet step.
27
+ import { wipe } from '../utils.js';
28
+ import { kemRatchetDecap } from './root-kdf.js';
29
+ export class RatchetKeypair {
30
+ ek;
31
+ _dk;
32
+ _used;
33
+ constructor(kem) {
34
+ const { encapsulationKey, decapsulationKey } = kem.keygen();
35
+ this.ek = encapsulationKey;
36
+ this._dk = decapsulationKey;
37
+ this._used = false;
38
+ }
39
+ // Decapsulate using the stored dk. May only be called once per instance.
40
+ // Wipes the dk immediately after decap — the dk never leaves this class.
41
+ // The stored ek is passed as `ownEk` so both sides bind the identical
42
+ // (peerEk, kemCt) pair into the HKDF info string.
43
+ decap(kem, rk, kemCt, context) {
44
+ if (this._used)
45
+ throw new Error('RatchetKeypair: already consumed or disposed. generate a new keypair for the next ratchet step');
46
+ this._used = true;
47
+ try {
48
+ return kemRatchetDecap(kem, rk, this._dk, kemCt, this.ek, context);
49
+ }
50
+ finally {
51
+ wipe(this._dk);
52
+ }
53
+ }
54
+ // Wipe the dk if not already wiped by decap. Idempotent.
55
+ dispose() {
56
+ if (!this._used) {
57
+ wipe(this._dk);
58
+ this._used = true;
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,4 @@
1
+ import type { MlKemLike, RatchetInitResult, KemEncapResult, KemDecapResult } from './types.js';
2
+ export declare function ratchetInit(sk: Uint8Array, context?: Uint8Array): RatchetInitResult;
3
+ export declare function kemRatchetEncap(kem: MlKemLike, rk: Uint8Array, peerEk: Uint8Array, context?: Uint8Array): KemEncapResult;
4
+ export declare function kemRatchetDecap(kem: MlKemLike, rk: Uint8Array, dk: Uint8Array, kemCt: Uint8Array, ownEk: Uint8Array, context?: Uint8Array): KemDecapResult;