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/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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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.
|
package/dist/wasm-source.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* All accepted forms of WASM input for init functions.
|
|
3
3
|
*
|
|
4
|
-
* - `string`
|
|
5
|
-
* - `URL`
|
|
6
|
-
* - `ArrayBuffer`
|
|
7
|
-
* - `Uint8Array`
|
|
8
|
-
* - `WebAssembly.Module`
|
|
9
|
-
* - `Response`
|
|
10
|
-
* - `
|
|
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 |
|
|
13
|
+
export type WasmSource = string | URL | ArrayBuffer | Uint8Array | WebAssembly.Module | Response | PromiseLike<WasmSource>;
|
package/package.json
CHANGED
|
@@ -1,66 +1,81 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
}
|