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.
- package/CLAUDE.md +171 -7
- package/LICENSE +4 -0
- package/README.md +109 -54
- package/SECURITY.md +125 -233
- package/dist/chacha20/cipher-suite.d.ts +10 -0
- package/dist/chacha20/cipher-suite.js +66 -2
- package/dist/chacha20/generator.d.ts +12 -0
- package/dist/chacha20/generator.js +91 -0
- package/dist/chacha20/index.d.ts +97 -1
- package/dist/chacha20/index.js +139 -11
- package/dist/chacha20/ops.d.ts +57 -6
- package/dist/chacha20/ops.js +93 -13
- package/dist/chacha20/pool-worker.js +12 -0
- package/dist/chacha20/types.d.ts +1 -32
- package/dist/ct-wasm.js +1 -1
- package/dist/ct.wasm +0 -0
- package/dist/docs/aead.md +69 -26
- package/dist/docs/architecture.md +600 -520
- package/dist/docs/argon2id.md +17 -14
- package/dist/docs/chacha20.md +146 -39
- package/dist/docs/exports.md +46 -10
- package/dist/docs/fortuna.md +339 -122
- package/dist/docs/init.md +24 -25
- package/dist/docs/loader.md +142 -47
- package/dist/docs/serpent.md +139 -41
- package/dist/docs/sha2.md +77 -19
- package/dist/docs/sha3.md +81 -15
- package/dist/docs/types.md +156 -15
- package/dist/docs/utils.md +171 -81
- package/dist/embedded/chacha20-pool-worker.d.ts +1 -0
- package/dist/embedded/chacha20-pool-worker.js +5 -0
- package/dist/embedded/kyber.d.ts +1 -1
- package/dist/embedded/kyber.js +1 -1
- package/dist/embedded/serpent-pool-worker.d.ts +1 -0
- package/dist/embedded/serpent-pool-worker.js +5 -0
- package/dist/embedded/serpent.d.ts +1 -1
- package/dist/embedded/serpent.js +1 -1
- package/dist/fortuna.d.ts +14 -8
- package/dist/fortuna.js +144 -50
- package/dist/index.d.ts +8 -6
- package/dist/index.js +6 -5
- package/dist/init.d.ts +0 -2
- package/dist/init.js +83 -3
- package/dist/kyber/indcpa.js +4 -4
- package/dist/kyber/index.js +25 -5
- package/dist/kyber/kem.js +56 -1
- package/dist/kyber/suite.d.ts +1 -2
- package/dist/kyber/suite.js +1 -0
- package/dist/kyber/types.d.ts +1 -0
- package/dist/kyber/validate.d.ts +8 -4
- package/dist/kyber/validate.js +18 -14
- package/dist/kyber.wasm +0 -0
- package/dist/loader.d.ts +7 -2
- package/dist/loader.js +25 -28
- package/dist/ratchet/index.d.ts +6 -0
- package/dist/ratchet/index.js +37 -0
- package/dist/ratchet/kdf-chain.d.ts +13 -0
- package/dist/ratchet/kdf-chain.js +85 -0
- package/dist/ratchet/ratchet-keypair.d.ts +9 -0
- package/dist/ratchet/ratchet-keypair.js +61 -0
- package/dist/ratchet/root-kdf.d.ts +4 -0
- package/dist/ratchet/root-kdf.js +124 -0
- package/dist/ratchet/skipped-key-store.d.ts +14 -0
- package/dist/ratchet/skipped-key-store.js +154 -0
- package/dist/ratchet/types.d.ts +36 -0
- package/dist/ratchet/types.js +26 -0
- package/dist/serpent/cipher-suite.d.ts +10 -0
- package/dist/serpent/cipher-suite.js +136 -50
- package/dist/serpent/generator.d.ts +12 -0
- package/dist/serpent/generator.js +97 -0
- package/dist/serpent/index.d.ts +61 -1
- package/dist/serpent/index.js +92 -7
- package/dist/serpent/pool-worker.js +25 -95
- package/dist/serpent/serpent-cbc.d.ts +14 -4
- package/dist/serpent/serpent-cbc.js +58 -34
- package/dist/serpent/shared-ops.d.ts +83 -0
- package/dist/serpent/shared-ops.js +213 -0
- package/dist/serpent/types.d.ts +1 -5
- package/dist/serpent.wasm +0 -0
- package/dist/sha2/hash.d.ts +2 -0
- package/dist/sha2/hash.js +53 -0
- package/dist/sha2/index.d.ts +1 -0
- package/dist/sha2/index.js +15 -1
- package/dist/sha3/hash.d.ts +2 -0
- package/dist/sha3/hash.js +53 -0
- package/dist/sha3/index.d.ts +17 -2
- package/dist/sha3/index.js +79 -7
- package/dist/stream/header.js +5 -5
- package/dist/stream/open-stream.js +36 -14
- package/dist/stream/seal-stream-pool.d.ts +1 -0
- package/dist/stream/seal-stream-pool.js +47 -8
- package/dist/stream/seal-stream.js +29 -11
- package/dist/stream/types.d.ts +1 -0
- package/dist/types.d.ts +21 -0
- package/dist/utils.d.ts +7 -8
- package/dist/utils.js +73 -40
- package/dist/wasm-source.d.ts +9 -8
- 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
|
-
|
|
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)
|
package/dist/kyber/suite.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/kyber/suite.js
CHANGED
|
@@ -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;
|
package/dist/kyber/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/kyber/validate.d.ts
CHANGED
|
@@ -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
|
|
7
|
-
* 2.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
/**
|
package/dist/kyber/validate.js
CHANGED
|
@@ -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
|
|
31
|
-
* 2.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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.
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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,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;
|