leviathan-crypto 2.1.0 → 3.0.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 +86 -443
- package/README.md +198 -65
- package/dist/aes/aes-cbc.d.ts +40 -0
- package/dist/aes/aes-cbc.js +158 -0
- package/dist/aes/aes-ctr.d.ts +50 -0
- package/dist/aes/aes-ctr.js +141 -0
- package/dist/aes/aes-gcm-siv.d.ts +67 -0
- package/dist/aes/aes-gcm-siv.js +217 -0
- package/dist/aes/aes-gcm.d.ts +61 -0
- package/dist/aes/aes-gcm.js +226 -0
- package/dist/aes/cipher-suite.d.ts +21 -0
- package/dist/aes/cipher-suite.js +179 -0
- package/dist/aes/embedded.d.ts +1 -0
- package/dist/aes/embedded.js +26 -0
- package/dist/aes/generator.d.ts +14 -0
- package/dist/aes/generator.js +103 -0
- package/dist/aes/index.d.ts +58 -0
- package/dist/aes/index.js +125 -0
- package/dist/aes/ops.d.ts +60 -0
- package/dist/aes/ops.js +164 -0
- package/dist/aes/pool-worker.d.ts +1 -0
- package/dist/aes/pool-worker.js +92 -0
- package/dist/aes/types.d.ts +1 -0
- package/dist/aes/types.js +23 -0
- package/dist/aes.wasm +0 -0
- package/dist/blake3/embedded.d.ts +1 -0
- package/dist/blake3/embedded.js +26 -0
- package/dist/blake3/index.d.ts +143 -0
- package/dist/blake3/index.js +620 -0
- package/dist/blake3/types.d.ts +102 -0
- package/dist/blake3/types.js +31 -0
- package/dist/blake3/validate.d.ts +29 -0
- package/dist/blake3/validate.js +80 -0
- package/dist/blake3.wasm +0 -0
- package/dist/chacha20/cipher-suite.js +47 -25
- package/dist/chacha20/generator.d.ts +2 -2
- package/dist/chacha20/generator.js +4 -4
- package/dist/chacha20/index.d.ts +16 -15
- package/dist/chacha20/index.js +52 -46
- package/dist/chacha20/ops.d.ts +7 -7
- package/dist/chacha20/ops.js +34 -34
- package/dist/chacha20/pool-worker.js +5 -3
- package/dist/cte-wasm.d.ts +1 -0
- package/dist/cte-wasm.js +3 -0
- package/dist/curve25519.wasm +0 -0
- package/dist/ecdsa/der.d.ts +23 -0
- package/dist/ecdsa/der.js +192 -0
- package/dist/ecdsa/ecprivatekey-der.d.ts +32 -0
- package/dist/ecdsa/ecprivatekey-der.js +230 -0
- package/dist/ecdsa/embedded.d.ts +1 -0
- package/dist/ecdsa/embedded.js +25 -0
- package/dist/ecdsa/index.d.ts +124 -0
- package/dist/ecdsa/index.js +366 -0
- package/dist/ecdsa/types.d.ts +31 -0
- package/dist/ecdsa/types.js +28 -0
- package/dist/ecdsa/validate.d.ts +18 -0
- package/dist/ecdsa/validate.js +92 -0
- package/dist/ed25519/embedded.d.ts +1 -0
- package/dist/ed25519/embedded.js +31 -0
- package/dist/ed25519/index.d.ts +70 -0
- package/dist/ed25519/index.js +308 -0
- package/dist/ed25519/types.d.ts +27 -0
- package/dist/ed25519/types.js +27 -0
- package/dist/ed25519/validate.d.ts +7 -0
- package/dist/ed25519/validate.js +77 -0
- package/dist/embedded/aes-pool-worker.d.ts +1 -0
- package/dist/embedded/aes-pool-worker.js +5 -0
- package/dist/embedded/aes.d.ts +1 -0
- package/dist/embedded/aes.js +3 -0
- package/dist/embedded/blake3.d.ts +1 -0
- package/dist/embedded/blake3.js +3 -0
- package/dist/embedded/chacha20-pool-worker.d.ts +1 -1
- package/dist/embedded/chacha20-pool-worker.js +2 -2
- package/dist/embedded/chacha20.d.ts +1 -1
- package/dist/embedded/chacha20.js +2 -2
- package/dist/embedded/curve25519.d.ts +1 -0
- package/dist/embedded/curve25519.js +3 -0
- package/dist/embedded/mldsa.d.ts +1 -0
- package/dist/embedded/mldsa.js +3 -0
- package/dist/embedded/mlkem.d.ts +1 -0
- package/dist/embedded/mlkem.js +3 -0
- package/dist/embedded/p256.d.ts +1 -0
- package/dist/embedded/p256.js +3 -0
- package/dist/embedded/serpent-pool-worker.d.ts +1 -1
- package/dist/embedded/serpent-pool-worker.js +2 -2
- package/dist/embedded/serpent.d.ts +1 -1
- package/dist/embedded/serpent.js +2 -2
- package/dist/embedded/sha2.d.ts +1 -1
- package/dist/embedded/sha2.js +2 -2
- package/dist/embedded/sha3.d.ts +1 -1
- package/dist/embedded/sha3.js +2 -2
- package/dist/embedded/slhdsa.d.ts +1 -0
- package/dist/embedded/slhdsa.js +3 -0
- package/dist/errors.d.ts +92 -1
- package/dist/errors.js +111 -1
- package/dist/fortuna.d.ts +5 -5
- package/dist/fortuna.js +37 -64
- package/dist/index.d.ts +38 -9
- package/dist/index.js +63 -19
- package/dist/init.d.ts +1 -1
- package/dist/init.js +11 -25
- package/dist/keccak/embedded.js +1 -1
- package/dist/keccak/index.d.ts +2 -0
- package/dist/keccak/index.js +4 -2
- package/dist/loader.d.ts +1 -24
- package/dist/loader.js +13 -16
- package/dist/merkle/blake3-tree.d.ts +35 -0
- package/dist/merkle/blake3-tree.js +187 -0
- package/dist/merkle/checkpoint.d.ts +58 -0
- package/dist/merkle/checkpoint.js +217 -0
- package/dist/merkle/index.d.ts +19 -0
- package/dist/merkle/index.js +37 -0
- package/dist/merkle/merkle-log.d.ts +130 -0
- package/dist/merkle/merkle-log.js +207 -0
- package/dist/merkle/merkle-verifier.d.ts +126 -0
- package/dist/merkle/merkle-verifier.js +296 -0
- package/dist/merkle/proof.d.ts +70 -0
- package/dist/merkle/proof.js +300 -0
- package/dist/merkle/sha256-tree.d.ts +33 -0
- package/dist/merkle/sha256-tree.js +145 -0
- package/dist/merkle/signed-log.d.ts +156 -0
- package/dist/merkle/signed-log.js +356 -0
- package/dist/merkle/signed-note.d.ts +309 -0
- package/dist/merkle/signed-note.js +648 -0
- package/dist/merkle/sth.d.ts +31 -0
- package/dist/merkle/sth.js +31 -0
- package/dist/merkle/storage.d.ts +40 -0
- package/dist/merkle/storage.js +71 -0
- package/dist/merkle/tree.d.ts +68 -0
- package/dist/merkle/tree.js +94 -0
- package/dist/mldsa/embedded.d.ts +1 -0
- package/dist/{kyber → mldsa}/embedded.js +5 -5
- package/dist/mldsa/expand.d.ts +53 -0
- package/dist/mldsa/expand.js +188 -0
- package/dist/mldsa/format.d.ts +16 -0
- package/dist/mldsa/format.js +68 -0
- package/dist/mldsa/hashvariant.d.ts +32 -0
- package/dist/mldsa/hashvariant.js +248 -0
- package/dist/mldsa/index.d.ts +142 -0
- package/dist/mldsa/index.js +463 -0
- package/dist/mldsa/keygen.d.ts +16 -0
- package/dist/mldsa/keygen.js +232 -0
- package/dist/mldsa/params.d.ts +21 -0
- package/dist/mldsa/params.js +55 -0
- package/dist/mldsa/sha3-helpers.d.ts +30 -0
- package/dist/mldsa/sha3-helpers.js +124 -0
- package/dist/mldsa/sign.d.ts +36 -0
- package/dist/mldsa/sign.js +380 -0
- package/dist/mldsa/types.d.ts +91 -0
- package/dist/mldsa/types.js +25 -0
- package/dist/mldsa/validate.d.ts +55 -0
- package/dist/mldsa/validate.js +125 -0
- package/dist/mldsa/verify.d.ts +29 -0
- package/dist/mldsa/verify.js +269 -0
- package/dist/mldsa.wasm +0 -0
- package/dist/mlkem/embedded.d.ts +1 -0
- package/dist/mlkem/embedded.js +27 -0
- package/dist/mlkem/indcpa.d.ts +49 -0
- package/dist/{kyber → mlkem}/indcpa.js +44 -44
- package/dist/mlkem/index.d.ts +37 -0
- package/dist/{kyber → mlkem}/index.js +24 -34
- package/dist/mlkem/kem.d.ts +21 -0
- package/dist/{kyber → mlkem}/kem.js +44 -64
- package/dist/{kyber → mlkem}/params.d.ts +4 -4
- package/dist/{kyber → mlkem}/params.js +2 -2
- package/dist/mlkem/suite.d.ts +12 -0
- package/dist/{kyber → mlkem}/suite.js +17 -12
- package/dist/{kyber → mlkem}/types.d.ts +3 -3
- package/dist/{kyber → mlkem}/types.js +1 -1
- package/dist/{kyber → mlkem}/validate.d.ts +7 -7
- package/dist/{kyber → mlkem}/validate.js +7 -7
- package/dist/{kyber.wasm → mlkem.wasm} +0 -0
- package/dist/p256.wasm +0 -0
- package/dist/ratchet/index.d.ts +2 -0
- package/dist/ratchet/index.js +1 -0
- package/dist/ratchet/kdf-chain.js +3 -3
- package/dist/ratchet/ratchet-keypair.js +2 -2
- package/dist/ratchet/root-kdf.js +7 -7
- package/dist/ratchet/skipped-key-store.js +4 -4
- package/dist/ratchet/types.d.ts +1 -1
- package/dist/serpent/cipher-suite.js +20 -17
- package/dist/serpent/generator.d.ts +1 -1
- package/dist/serpent/generator.js +2 -2
- package/dist/serpent/index.d.ts +8 -7
- package/dist/serpent/index.js +18 -27
- package/dist/serpent/pool-worker.js +7 -5
- package/dist/serpent/serpent-cbc.d.ts +4 -4
- package/dist/serpent/serpent-cbc.js +11 -8
- package/dist/serpent/shared-ops.d.ts +3 -23
- package/dist/serpent/shared-ops.js +50 -85
- package/dist/serpent.wasm +0 -0
- package/dist/sha2/hkdf.js +5 -5
- package/dist/sha2/index.d.ts +21 -1
- package/dist/sha2/index.js +65 -10
- package/dist/sha2/types.d.ts +41 -2
- package/dist/sha2.wasm +0 -0
- package/dist/sha3/index.d.ts +72 -3
- package/dist/sha3/index.js +240 -14
- package/dist/sha3/kmac.d.ts +121 -0
- package/dist/sha3/kmac.js +800 -0
- package/dist/sha3.wasm +0 -0
- package/dist/shared/pkcs7.d.ts +22 -0
- package/dist/shared/pkcs7.js +84 -0
- package/dist/sign/ctx.d.ts +41 -0
- package/dist/sign/ctx.js +102 -0
- package/dist/sign/envelope.d.ts +45 -0
- package/dist/sign/envelope.js +152 -0
- package/dist/sign/hasher.d.ts +9 -0
- package/dist/sign/hasher.js +132 -0
- package/dist/sign/index.d.ts +11 -0
- package/dist/sign/index.js +34 -0
- package/dist/sign/sign-stream.d.ts +25 -0
- package/dist/sign/sign-stream.js +112 -0
- package/dist/sign/suites/ecdsa-p256.d.ts +2 -0
- package/dist/sign/suites/ecdsa-p256.js +120 -0
- package/dist/sign/suites/ed25519.d.ts +3 -0
- package/dist/sign/suites/ed25519.js +165 -0
- package/dist/sign/suites/hybrid-classical.d.ts +23 -0
- package/dist/sign/suites/hybrid-classical.js +526 -0
- package/dist/sign/suites/hybrid-pq.d.ts +4 -0
- package/dist/sign/suites/hybrid-pq.js +234 -0
- package/dist/sign/suites/mldsa.d.ts +7 -0
- package/dist/sign/suites/mldsa.js +161 -0
- package/dist/sign/suites/slhdsa.d.ts +7 -0
- package/dist/sign/suites/slhdsa.js +176 -0
- package/dist/sign/types.d.ts +106 -0
- package/dist/sign/types.js +28 -0
- package/dist/sign/verify-stream.d.ts +30 -0
- package/dist/sign/verify-stream.js +227 -0
- package/dist/slhdsa/embedded.d.ts +1 -0
- package/dist/slhdsa/embedded.js +26 -0
- package/dist/slhdsa/index.d.ts +149 -0
- package/dist/slhdsa/index.js +493 -0
- package/dist/slhdsa/params.d.ts +26 -0
- package/dist/slhdsa/params.js +70 -0
- package/dist/slhdsa/prehash.d.ts +68 -0
- package/dist/slhdsa/prehash.js +307 -0
- package/dist/slhdsa/sign.d.ts +39 -0
- package/dist/slhdsa/sign.js +116 -0
- package/dist/slhdsa/types.d.ts +129 -0
- package/dist/slhdsa/types.js +27 -0
- package/dist/slhdsa/validate.d.ts +60 -0
- package/dist/slhdsa/validate.js +127 -0
- package/dist/slhdsa/verify.d.ts +32 -0
- package/dist/slhdsa/verify.js +107 -0
- package/dist/slhdsa.wasm +0 -0
- package/dist/stream/header.js +3 -3
- package/dist/stream/index.d.ts +1 -0
- package/dist/stream/index.js +1 -0
- package/dist/stream/open-stream.js +31 -10
- package/dist/stream/seal-stream-pool.d.ts +1 -0
- package/dist/stream/seal-stream-pool.js +63 -26
- package/dist/stream/seal-stream.d.ts +1 -1
- package/dist/stream/seal-stream.js +20 -9
- package/dist/stream/seal.js +6 -6
- package/dist/stream/types.d.ts +3 -1
- package/dist/stream/types.js +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js +46 -54
- package/dist/wasm-source.d.ts +7 -7
- package/dist/wasm-source.js +1 -1
- package/dist/x25519/embedded.d.ts +1 -0
- package/dist/x25519/embedded.js +31 -0
- package/dist/x25519/index.d.ts +43 -0
- package/dist/x25519/index.js +159 -0
- package/dist/x25519/types.d.ts +25 -0
- package/dist/x25519/types.js +27 -0
- package/dist/x25519/validate.d.ts +2 -0
- package/dist/x25519/validate.js +39 -0
- package/package.json +70 -26
- package/SECURITY.md +0 -163
- package/dist/ct-wasm.d.ts +0 -1
- package/dist/ct-wasm.js +0 -3
- package/dist/docs/aead.md +0 -363
- package/dist/docs/architecture.md +0 -1011
- package/dist/docs/argon2id.md +0 -305
- package/dist/docs/chacha20.md +0 -781
- package/dist/docs/exports.md +0 -277
- package/dist/docs/fortuna.md +0 -530
- package/dist/docs/init.md +0 -301
- package/dist/docs/loader.md +0 -256
- package/dist/docs/serpent.md +0 -617
- package/dist/docs/sha2.md +0 -671
- package/dist/docs/sha3.md +0 -612
- package/dist/docs/types.md +0 -416
- package/dist/docs/utils.md +0 -457
- package/dist/embedded/kyber.d.ts +0 -1
- package/dist/embedded/kyber.js +0 -3
- package/dist/kyber/embedded.d.ts +0 -1
- package/dist/kyber/indcpa.d.ts +0 -49
- package/dist/kyber/index.d.ts +0 -38
- package/dist/kyber/kem.d.ts +0 -21
- package/dist/kyber/suite.d.ts +0 -12
- /package/dist/{ct.wasm → cte.wasm} +0 -0
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
//
|
|
22
22
|
// src/ts/stream/seal-stream-pool.ts
|
|
23
23
|
//
|
|
24
|
-
// SealStreamPool
|
|
24
|
+
// SealStreamPool, parallel batch encryption/decryption using the STREAM
|
|
25
25
|
// construction. Dispatches per-chunk seal/open jobs across Web Workers.
|
|
26
26
|
// Any error is fatal: auth failure, crash, or timeout kills all workers,
|
|
27
27
|
// wipes keys, and rejects all pending promises.
|
|
28
|
-
import { randomBytes, wipe } from '../utils.js';
|
|
28
|
+
import { randomBytes, wipe, constantTimeEqual } from '../utils.js';
|
|
29
29
|
import { isInitialized } from '../init.js';
|
|
30
30
|
import { compileWasm } from '../loader.js';
|
|
31
31
|
import { AuthenticationError } from '../errors.js';
|
|
@@ -44,6 +44,7 @@ export class SealStreamPool {
|
|
|
44
44
|
_framed;
|
|
45
45
|
_timeout;
|
|
46
46
|
_header;
|
|
47
|
+
_commitment;
|
|
47
48
|
_workers;
|
|
48
49
|
_idle;
|
|
49
50
|
_queue;
|
|
@@ -53,7 +54,7 @@ export class SealStreamPool {
|
|
|
53
54
|
_sealed;
|
|
54
55
|
_keys;
|
|
55
56
|
_masterKey;
|
|
56
|
-
constructor(cipher, workers, keys, masterKey, header, chunkSize, framed, timeout) {
|
|
57
|
+
constructor(cipher, workers, keys, masterKey, header, commitment, chunkSize, framed, timeout) {
|
|
57
58
|
this._cipher = cipher;
|
|
58
59
|
this._workers = workers;
|
|
59
60
|
this._idle = [...workers];
|
|
@@ -65,6 +66,7 @@ export class SealStreamPool {
|
|
|
65
66
|
this._keys = keys;
|
|
66
67
|
this._masterKey = masterKey;
|
|
67
68
|
this._header = header;
|
|
69
|
+
this._commitment = commitment;
|
|
68
70
|
this._chunkSize = chunkSize;
|
|
69
71
|
this._framed = framed;
|
|
70
72
|
this._timeout = timeout;
|
|
@@ -75,10 +77,10 @@ export class SealStreamPool {
|
|
|
75
77
|
}
|
|
76
78
|
static async create(cipher, key, opts) {
|
|
77
79
|
if (!isInitialized('sha2'))
|
|
78
|
-
throw new Error('leviathan-crypto: stream layer requires sha2 for key derivation
|
|
80
|
+
throw new Error('leviathan-crypto: stream layer requires sha2 for key derivation, '
|
|
79
81
|
+ 'call init({ sha2: ... }) before creating a SealStreamPool');
|
|
80
82
|
if (cipher.kemCtSize > 0)
|
|
81
|
-
throw new Error('leviathan-crypto: SealStreamPool does not support KEM-enabled cipher suites
|
|
83
|
+
throw new Error('leviathan-crypto: SealStreamPool does not support KEM-enabled cipher suites, '
|
|
82
84
|
+ 'KEM encryption is asymmetric (seal uses encapsulation key, open requires decapsulation key) '
|
|
83
85
|
+ 'and cannot share a single key across both directions. '
|
|
84
86
|
+ 'Use SealStream / OpenStream directly for hybrid KEM encryption.');
|
|
@@ -101,7 +103,7 @@ export class SealStreamPool {
|
|
|
101
103
|
}
|
|
102
104
|
else {
|
|
103
105
|
if (required.length > 1)
|
|
104
|
-
throw new Error(`leviathan-crypto: cipher requires ${required.length} WASM modules (${required.join(', ')})
|
|
106
|
+
throw new Error(`leviathan-crypto: cipher requires ${required.length} WASM modules (${required.join(', ')}), provide a Record`);
|
|
105
107
|
modules[required[0]] = await compileWasm(opts.wasm);
|
|
106
108
|
}
|
|
107
109
|
// For padded ciphers, validate that a full plaintext chunk fits in the WASM
|
|
@@ -115,10 +117,20 @@ export class SealStreamPool {
|
|
|
115
117
|
}
|
|
116
118
|
if (key.length !== cipher.keySize)
|
|
117
119
|
throw new RangeError(`key must be ${cipher.keySize} bytes (got ${key.length})`);
|
|
118
|
-
// Generate nonce and
|
|
120
|
+
// Generate nonce and build header before deriveKeys, XChaCha20 binds
|
|
121
|
+
// the header into HKDF info; SerpentCipher accepts and ignores it.
|
|
119
122
|
const nonce = randomBytes(16);
|
|
120
|
-
const keys = cipher.deriveKeys(key, nonce);
|
|
121
123
|
const header = writeHeader(cipher.formatEnum, framed, nonce, chunkSize);
|
|
124
|
+
const keys = cipher.deriveKeys(key, nonce, undefined, header);
|
|
125
|
+
let commitment = null;
|
|
126
|
+
if (cipher.commitmentSize > 0) {
|
|
127
|
+
if (!keys.commitment || keys.commitment.length !== cipher.commitmentSize) {
|
|
128
|
+
cipher.wipeKeys(keys);
|
|
129
|
+
throw new Error(`leviathan-crypto: ${cipher.formatName}.deriveKeys returned `
|
|
130
|
+
+ `${keys.commitment?.length ?? 'no'} commitment bytes, expected ${cipher.commitmentSize}`);
|
|
131
|
+
}
|
|
132
|
+
commitment = keys.commitment;
|
|
133
|
+
}
|
|
122
134
|
// Spawn workers sequentially (compatible with @vitest/web-worker)
|
|
123
135
|
const workers = [];
|
|
124
136
|
for (let i = 0; i < n; i++) {
|
|
@@ -147,7 +159,7 @@ export class SealStreamPool {
|
|
|
147
159
|
});
|
|
148
160
|
workers.push(w);
|
|
149
161
|
}
|
|
150
|
-
return new SealStreamPool(cipher, workers, keys, key.slice(), header, chunkSize, framed, timeout);
|
|
162
|
+
return new SealStreamPool(cipher, workers, keys, key.slice(), header, commitment, chunkSize, framed, timeout);
|
|
151
163
|
}
|
|
152
164
|
get header() {
|
|
153
165
|
return this._header;
|
|
@@ -176,12 +188,17 @@ export class SealStreamPool {
|
|
|
176
188
|
}
|
|
177
189
|
try {
|
|
178
190
|
const results = await Promise.all(jobs);
|
|
179
|
-
|
|
191
|
+
const commitmentLen = this._cipher.commitmentSize;
|
|
192
|
+
let totalLen = HEADER_SIZE + commitmentLen;
|
|
180
193
|
for (const r of results)
|
|
181
194
|
totalLen += this._framed ? r.length + 4 : r.length;
|
|
182
195
|
const ciphertext = new Uint8Array(totalLen);
|
|
183
196
|
ciphertext.set(this._header, 0);
|
|
184
197
|
let pos = HEADER_SIZE;
|
|
198
|
+
if (this._commitment) {
|
|
199
|
+
ciphertext.set(this._commitment, pos);
|
|
200
|
+
pos += commitmentLen;
|
|
201
|
+
}
|
|
185
202
|
for (const r of results) {
|
|
186
203
|
if (this._framed) {
|
|
187
204
|
new DataView(ciphertext.buffer, pos).setUint32(0, r.length, false);
|
|
@@ -202,30 +219,52 @@ export class SealStreamPool {
|
|
|
202
219
|
if (this._dead)
|
|
203
220
|
throw new Error('leviathan-crypto: pool is dead');
|
|
204
221
|
if (ciphertext.length < HEADER_SIZE)
|
|
205
|
-
throw new RangeError(`leviathan-crypto: ciphertext too short
|
|
222
|
+
throw new RangeError(`leviathan-crypto: ciphertext too short, need at least ${HEADER_SIZE} bytes for header`);
|
|
206
223
|
// Validate header before splitting chunks
|
|
207
224
|
const h = readHeader(ciphertext.subarray(0, HEADER_SIZE));
|
|
208
225
|
if (h.formatEnum !== this._cipher.formatEnum)
|
|
209
226
|
throw new Error(`leviathan-crypto: pool expected format 0x${this._cipher.formatEnum.toString(16).padStart(2, '0')}, `
|
|
210
227
|
+ `got 0x${h.formatEnum.toString(16).padStart(2, '0')}`);
|
|
211
228
|
if (h.chunkSize !== this._chunkSize)
|
|
212
|
-
throw new RangeError(`leviathan-crypto: pool chunkSize mismatch
|
|
229
|
+
throw new RangeError(`leviathan-crypto: pool chunkSize mismatch, pool expects ${this._chunkSize}, `
|
|
213
230
|
+ `header says ${h.chunkSize}`);
|
|
214
231
|
if (h.framed !== this._framed)
|
|
215
|
-
throw new Error(`leviathan-crypto: pool framing mismatch
|
|
232
|
+
throw new Error(`leviathan-crypto: pool framing mismatch, pool is ${this._framed ? 'framed' : 'unframed'}, `
|
|
216
233
|
+ `header says ${h.framed ? 'framed' : 'unframed'}`);
|
|
217
234
|
// Re-derive keys from the nonce embedded in this ciphertext's header.
|
|
218
|
-
// The pool's _keys are tied to its own seal nonce
|
|
235
|
+
// The pool's _keys are tied to its own seal nonce, for arbitrary incoming
|
|
219
236
|
// ciphertext the nonce may differ, so we derive fresh keys here.
|
|
220
237
|
if (!this._masterKey)
|
|
221
238
|
throw new Error('leviathan-crypto: pool master key has been wiped');
|
|
222
|
-
const
|
|
239
|
+
const headerBytes = ciphertext.subarray(0, HEADER_SIZE);
|
|
240
|
+
const commitmentLen = this._cipher.commitmentSize;
|
|
241
|
+
const minLen = HEADER_SIZE + commitmentLen;
|
|
242
|
+
if (ciphertext.length < minLen)
|
|
243
|
+
throw new RangeError(`leviathan-crypto: ciphertext too short, need at least ${minLen} bytes for header + commitment`);
|
|
244
|
+
const openKeys = this._cipher.deriveKeys(this._masterKey, h.nonce, undefined, headerBytes);
|
|
223
245
|
let openKeysWiped = false;
|
|
246
|
+
// Verify commitment before any chunk dispatch. Wrong key fails fast
|
|
247
|
+
// with AuthenticationError, before Poly1305 is consulted.
|
|
248
|
+
if (commitmentLen > 0) {
|
|
249
|
+
const derivedCommitment = openKeys.commitment;
|
|
250
|
+
if (!derivedCommitment || derivedCommitment.length !== commitmentLen) {
|
|
251
|
+
this._cipher.wipeKeys(openKeys);
|
|
252
|
+
throw new Error(`leviathan-crypto: ${this._cipher.formatName}.deriveKeys returned `
|
|
253
|
+
+ `${derivedCommitment?.length ?? 'no'} commitment bytes, expected ${commitmentLen}`);
|
|
254
|
+
}
|
|
255
|
+
const recvCommitment = ciphertext.subarray(HEADER_SIZE, HEADER_SIZE + commitmentLen);
|
|
256
|
+
if (!constantTimeEqual(derivedCommitment, recvCommitment)) {
|
|
257
|
+
this._cipher.wipeKeys(openKeys);
|
|
258
|
+
const err = new AuthenticationError(`commitment-${this._cipher.formatName}`);
|
|
259
|
+
this._killAll(err);
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
224
263
|
try {
|
|
225
|
-
// Strip header before chunk splitting
|
|
226
|
-
const body = ciphertext.subarray(HEADER_SIZE);
|
|
264
|
+
// Strip header + commitment before chunk splitting
|
|
265
|
+
const body = ciphertext.subarray(HEADER_SIZE + commitmentLen);
|
|
227
266
|
if (body.length === 0)
|
|
228
|
-
throw new RangeError('leviathan-crypto: empty ciphertext
|
|
267
|
+
throw new RangeError('leviathan-crypto: empty ciphertext, seal() always produces at least one chunk');
|
|
229
268
|
// Compute max wire chunk size for per-chunk validation
|
|
230
269
|
const tagSize = this._cipher.tagSize;
|
|
231
270
|
const paddedSize = this._cipher.padded
|
|
@@ -265,7 +304,7 @@ export class SealStreamPool {
|
|
|
265
304
|
const jobs = [];
|
|
266
305
|
for (let i = 0; i < chunks.length; i++) {
|
|
267
306
|
if (chunks[i].length < tagSize)
|
|
268
|
-
throw new RangeError(`leviathan-crypto: chunk ${i} too short
|
|
307
|
+
throw new RangeError(`leviathan-crypto: chunk ${i} too short, need at least ${tagSize} bytes for tag `
|
|
269
308
|
+ `(got ${chunks[i].length})`);
|
|
270
309
|
if (chunks[i].length > maxWireChunk)
|
|
271
310
|
throw new RangeError(`leviathan-crypto: chunk ${i} exceeds max wire size `
|
|
@@ -277,10 +316,8 @@ export class SealStreamPool {
|
|
|
277
316
|
derivedKeyBytes: openKeys.bytes.slice(),
|
|
278
317
|
}));
|
|
279
318
|
}
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
// finally below only fires on pre-dispatch throws (empty body, frame errors,
|
|
283
|
-
// chunk validation), not as a redundant second call on the normal path.
|
|
319
|
+
// Per-job copies made; wipe main-thread openKeys immediately. openKeysWiped
|
|
320
|
+
// guards the finally so it only fires on pre-dispatch throws.
|
|
284
321
|
this._cipher.wipeKeys(openKeys);
|
|
285
322
|
openKeysWiped = true;
|
|
286
323
|
const results = await Promise.all(jobs);
|
|
@@ -329,7 +366,7 @@ export class SealStreamPool {
|
|
|
329
366
|
_send(worker, job) {
|
|
330
367
|
const transfer = [];
|
|
331
368
|
// Only transfer data.buffer when the Uint8Array owns the buffer exclusively.
|
|
332
|
-
// Subarrays from open() share the caller's ciphertext buffer
|
|
369
|
+
// Subarrays from open() share the caller's ciphertext buffer, transferring
|
|
333
370
|
// one would detach all sibling views dispatched as parallel jobs.
|
|
334
371
|
if (job.data.buffer instanceof ArrayBuffer
|
|
335
372
|
&& job.data.byteOffset === 0
|
|
@@ -340,7 +377,7 @@ export class SealStreamPool {
|
|
|
340
377
|
transfer.push(job.counterNonce.buffer);
|
|
341
378
|
if (job.derivedKeyBytes?.buffer instanceof ArrayBuffer)
|
|
342
379
|
transfer.push(job.derivedKeyBytes.buffer);
|
|
343
|
-
// aad is intentionally not transferred
|
|
380
|
+
// aad is intentionally not transferred, caller may retain the reference
|
|
344
381
|
worker.postMessage(job, { transfer });
|
|
345
382
|
}
|
|
346
383
|
_onMessage(worker, e) {
|
|
@@ -383,7 +420,7 @@ export class SealStreamPool {
|
|
|
383
420
|
this._workers = [];
|
|
384
421
|
this._idle.length = 0;
|
|
385
422
|
// Fire-and-forget: wipe each worker's key material, then terminate.
|
|
386
|
-
// On timeout, terminate anyway
|
|
423
|
+
// On timeout, terminate anyway, the main-thread key handles are
|
|
387
424
|
// wiped below so the owning surface no longer has access.
|
|
388
425
|
for (const w of workers)
|
|
389
426
|
this._wipeThenTerminate(w);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CipherSuite, SealStreamOpts } from './types.js';
|
|
2
2
|
export declare class SealStream {
|
|
3
|
-
/** Preamble sent before the first chunk: header [|| kemCiphertext]. */
|
|
3
|
+
/** Preamble sent before the first chunk: header [|| kemCiphertext] [|| commitment]. */
|
|
4
4
|
readonly preamble: Uint8Array;
|
|
5
5
|
private readonly cipher;
|
|
6
6
|
private readonly keys;
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
//
|
|
22
22
|
// src/ts/stream/seal-stream.ts
|
|
23
23
|
//
|
|
24
|
-
// SealStream
|
|
24
|
+
// SealStream, cipher-agnostic streaming encryption using the STREAM
|
|
25
25
|
// construction (Hoang/Reyhanitabar/Rogaway/Vizár, CRYPTO 2015).
|
|
26
26
|
import { randomBytes, concat } from '../utils.js';
|
|
27
27
|
import { isInitialized } from '../init.js';
|
|
@@ -32,11 +32,11 @@ function u32beFrame(n) {
|
|
|
32
32
|
new DataView(b.buffer).setUint32(0, n, false);
|
|
33
33
|
return b;
|
|
34
34
|
}
|
|
35
|
-
// Module-level nonce injection slot
|
|
35
|
+
// Module-level nonce injection slot, used only by _fromNonce for KAT tests.
|
|
36
36
|
// Set immediately before constructing, cleared inside the constructor.
|
|
37
37
|
let _injectNonce;
|
|
38
38
|
export class SealStream {
|
|
39
|
-
/** Preamble sent before the first chunk: header [|| kemCiphertext]. */
|
|
39
|
+
/** Preamble sent before the first chunk: header [|| kemCiphertext] [|| commitment]. */
|
|
40
40
|
preamble;
|
|
41
41
|
cipher;
|
|
42
42
|
keys;
|
|
@@ -49,7 +49,7 @@ export class SealStream {
|
|
|
49
49
|
this.chunkSize = opts?.chunkSize ?? 65536;
|
|
50
50
|
this.framed = opts?.framed ?? false;
|
|
51
51
|
if (!isInitialized('sha2'))
|
|
52
|
-
throw new Error('leviathan-crypto: stream layer requires sha2 for key derivation
|
|
52
|
+
throw new Error('leviathan-crypto: stream layer requires sha2 for key derivation, '
|
|
53
53
|
+ 'call init({ sha2: ... }) before creating a SealStream');
|
|
54
54
|
if (key.length !== cipher.keySize)
|
|
55
55
|
throw new RangeError(`key must be ${cipher.keySize} bytes (got ${key.length})`);
|
|
@@ -57,14 +57,25 @@ export class SealStream {
|
|
|
57
57
|
throw new RangeError(`chunkSize must be in [${CHUNK_MIN}, ${CHUNK_MAX}] (got ${this.chunkSize})`);
|
|
58
58
|
const nonce = _injectNonce ?? randomBytes(16);
|
|
59
59
|
_injectNonce = undefined;
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// Header must be built before deriveKeys, XChaCha20 binds it into
|
|
61
|
+
// the HKDF info string. SerpentCipher accepts and ignores it.
|
|
62
62
|
const header = writeHeader(cipher.formatEnum, this.framed, nonce, this.chunkSize);
|
|
63
|
-
this.
|
|
63
|
+
this.keys = cipher.deriveKeys(key, nonce, undefined, header);
|
|
64
|
+
const kemCt = this.keys.kemCiphertext;
|
|
65
|
+
const commitment = cipher.commitmentSize > 0 ? this.keys.commitment : undefined;
|
|
66
|
+
if (cipher.commitmentSize > 0 && (!commitment || commitment.length !== cipher.commitmentSize))
|
|
67
|
+
throw new Error(`leviathan-crypto: ${cipher.formatName}.deriveKeys returned `
|
|
68
|
+
+ `${commitment?.length ?? 'no'} commitment bytes, expected ${cipher.commitmentSize}`);
|
|
69
|
+
const parts = [header];
|
|
70
|
+
if (kemCt)
|
|
71
|
+
parts.push(kemCt);
|
|
72
|
+
if (commitment)
|
|
73
|
+
parts.push(commitment);
|
|
74
|
+
this.preamble = parts.length === 1 ? header : concat(...parts);
|
|
64
75
|
}
|
|
65
76
|
/**
|
|
66
77
|
* @internal
|
|
67
|
-
* KAT-only factory
|
|
78
|
+
* KAT-only factory, injects a fixed nonce so seal output is deterministic.
|
|
68
79
|
* Stripped from published `.d.ts` by `stripInternal`. Do not use in production.
|
|
69
80
|
*/
|
|
70
81
|
static _fromNonce(cipher, key, opts, nonce) {
|
|
@@ -121,7 +132,7 @@ export class SealStream {
|
|
|
121
132
|
this.cipher.wipeKeys(this.keys);
|
|
122
133
|
this.state = 'finalized';
|
|
123
134
|
}
|
|
124
|
-
// 'failed' already wiped keys; 'finalized' already wiped keys
|
|
135
|
+
// 'failed' already wiped keys; 'finalized' already wiped keys, no-op.
|
|
125
136
|
}
|
|
126
137
|
toTransformStream() {
|
|
127
138
|
let headerSent = false;
|
package/dist/stream/seal.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
//
|
|
22
22
|
// src/ts/stream/seal.ts
|
|
23
23
|
//
|
|
24
|
-
// Seal
|
|
24
|
+
// Seal, unified single-shot encrypt/decrypt using the STREAM construction.
|
|
25
25
|
// Seal blobs are valid SealStream blobs with a single final chunk.
|
|
26
26
|
// OpenStream can decrypt a Seal blob without modification.
|
|
27
27
|
import { concat } from '../utils.js';
|
|
@@ -32,7 +32,7 @@ import { HEADER_SIZE, CHUNK_MAX, CHUNK_MIN } from './constants.js';
|
|
|
32
32
|
export class Seal {
|
|
33
33
|
static encrypt(suite, key, pt, opts) {
|
|
34
34
|
if (pt.length > CHUNK_MAX)
|
|
35
|
-
throw new RangeError(`Seal.encrypt: plaintext exceeds maximum (${CHUNK_MAX} bytes)
|
|
35
|
+
throw new RangeError(`Seal.encrypt: plaintext exceeds maximum (${CHUNK_MAX} bytes), use SealStream for large data`);
|
|
36
36
|
const sealer = new SealStream(suite, key, { chunkSize: Math.max(pt.length, CHUNK_MIN) });
|
|
37
37
|
try {
|
|
38
38
|
const ct = sealer.finalize(pt, opts);
|
|
@@ -43,9 +43,9 @@ export class Seal {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
static decrypt(suite, key, blob, opts) {
|
|
46
|
-
const preambleLen = HEADER_SIZE + suite.kemCtSize;
|
|
46
|
+
const preambleLen = HEADER_SIZE + suite.kemCtSize + suite.commitmentSize;
|
|
47
47
|
if (blob.length < preambleLen)
|
|
48
|
-
throw new RangeError(`Seal.decrypt: blob too short
|
|
48
|
+
throw new RangeError(`Seal.decrypt: blob too short, need at least ${preambleLen} bytes (got ${blob.length})`);
|
|
49
49
|
const preamble = blob.subarray(0, preambleLen);
|
|
50
50
|
const opener = new OpenStream(suite, key, preamble);
|
|
51
51
|
try {
|
|
@@ -57,12 +57,12 @@ export class Seal {
|
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* @internal
|
|
60
|
-
* KAT-only
|
|
60
|
+
* KAT-only, injects a fixed nonce so output is deterministic.
|
|
61
61
|
* Stripped from published `.d.ts` by `stripInternal`. Do not use in production.
|
|
62
62
|
*/
|
|
63
63
|
static _fromNonce(suite, key, pt, nonce, opts) {
|
|
64
64
|
if (pt.length > CHUNK_MAX)
|
|
65
|
-
throw new RangeError(`Seal._fromNonce: plaintext exceeds maximum (${CHUNK_MAX} bytes)
|
|
65
|
+
throw new RangeError(`Seal._fromNonce: plaintext exceeds maximum (${CHUNK_MAX} bytes), use SealStream for large data`);
|
|
66
66
|
const sealer = SealStream._fromNonce(suite, key, { chunkSize: Math.max(pt.length, CHUNK_MIN) }, nonce);
|
|
67
67
|
try {
|
|
68
68
|
const ct = sealer.finalize(pt, opts);
|
package/dist/stream/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface DerivedKeys {
|
|
2
2
|
readonly bytes: Uint8Array;
|
|
3
3
|
readonly kemCiphertext?: Uint8Array;
|
|
4
|
+
readonly commitment?: Uint8Array;
|
|
4
5
|
}
|
|
5
6
|
export interface CipherSuite {
|
|
6
7
|
readonly formatEnum: number;
|
|
@@ -9,10 +10,11 @@ export interface CipherSuite {
|
|
|
9
10
|
readonly keySize: number;
|
|
10
11
|
readonly decKeySize?: number;
|
|
11
12
|
readonly kemCtSize: number;
|
|
13
|
+
readonly commitmentSize: number;
|
|
12
14
|
readonly tagSize: number;
|
|
13
15
|
readonly padded: boolean;
|
|
14
16
|
readonly wasmChunkSize: number;
|
|
15
|
-
deriveKeys(key: Uint8Array, nonce: Uint8Array, kemCt?: Uint8Array): DerivedKeys;
|
|
17
|
+
deriveKeys(key: Uint8Array, nonce: Uint8Array, kemCt?: Uint8Array, header?: Uint8Array): DerivedKeys;
|
|
16
18
|
sealChunk(keys: DerivedKeys, counterNonce: Uint8Array, chunk: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
17
19
|
openChunk(keys: DerivedKeys, counterNonce: Uint8Array, chunk: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
18
20
|
wipeKeys(keys: DerivedKeys): void;
|
package/dist/stream/types.js
CHANGED
|
@@ -21,6 +21,6 @@
|
|
|
21
21
|
//
|
|
22
22
|
// src/ts/stream/types.ts
|
|
23
23
|
//
|
|
24
|
-
// CipherSuite interface
|
|
24
|
+
// CipherSuite interface, cipher-specific logic injected into SealStream
|
|
25
25
|
// and OpenStream. Implementations are plain objects (not classes).
|
|
26
26
|
export {};
|
package/dist/types.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface Streamcipher {
|
|
|
18
18
|
}
|
|
19
19
|
export interface AEAD {
|
|
20
20
|
encrypt(msg: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
21
|
-
/** Decrypt and authenticate. Throws `Error` on authentication failure
|
|
21
|
+
/** Decrypt and authenticate. Throws `Error` on authentication failure, never returns null. */
|
|
22
22
|
decrypt(ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
23
23
|
dispose(): void;
|
|
24
24
|
}
|
package/dist/types.js
CHANGED
package/dist/utils.d.ts
CHANGED
|
@@ -8,13 +8,13 @@ export declare const utf8ToBytes: (str: string) => Uint8Array;
|
|
|
8
8
|
export declare const bytesToUtf8: (bytes: Uint8Array) => string;
|
|
9
9
|
/** Base64 or base64url string to Uint8Array. Handles padded, unpadded, and legacy %3d padding. Throws RangeError on invalid input. */
|
|
10
10
|
export declare const base64ToBytes: (b64: string) => Uint8Array;
|
|
11
|
-
/** Uint8Array to base64 string. Pass url=true for base64url (RFC 4648 §5
|
|
11
|
+
/** Uint8Array to base64 string. Pass url=true for base64url (RFC 4648 §5, no padding characters). */
|
|
12
12
|
export declare const bytesToBase64: (bytes: Uint8Array, url?: boolean) => string;
|
|
13
|
-
export declare const
|
|
13
|
+
export declare const CTE_MAX_BYTES = 32768;
|
|
14
14
|
/**
|
|
15
15
|
* Constant-time byte-array equality.
|
|
16
16
|
* Runs entirely inside a WASM SIMD module (v128 XOR accumulate with
|
|
17
|
-
* branch-free reduction). Throws on runtimes without SIMD support
|
|
17
|
+
* branch-free reduction). Throws on runtimes without SIMD support,
|
|
18
18
|
* no JS fallback. Length check is not constant-time (length is
|
|
19
19
|
* non-secret in all protocols). Max input size: 32768 bytes per side.
|
|
20
20
|
*/
|
package/dist/utils.js
CHANGED
|
@@ -21,17 +21,16 @@
|
|
|
21
21
|
//
|
|
22
22
|
// src/ts/utils.ts
|
|
23
23
|
//
|
|
24
|
-
// Pure TypeScript utilities
|
|
25
|
-
// Ported from leviathan/src/base.ts (Convert namespace, Util namespace, constantTimeEqual).
|
|
24
|
+
// Pure TypeScript utilities, no init() dependency.
|
|
26
25
|
// ── Encoding ────────────────────────────────────────────────────────────────
|
|
27
26
|
/** Hex string to Uint8Array. Accepts lowercase/uppercase, optional 0x prefix. Throws RangeError on odd-length or non-hex input. */
|
|
28
27
|
export const hexToBytes = (hex) => {
|
|
29
28
|
if (hex.startsWith('0x') || hex.startsWith('0X'))
|
|
30
29
|
hex = hex.slice(2);
|
|
31
30
|
if (hex.length % 2)
|
|
32
|
-
throw new RangeError(`hexToBytes: odd-length string (${hex.length} chars)
|
|
31
|
+
throw new RangeError(`hexToBytes: odd-length string (${hex.length} chars), input must be an even-length hex string`);
|
|
33
32
|
// parseInt('0g', 16) returns 0 (not NaN) because it stops at the first
|
|
34
|
-
// invalid char
|
|
33
|
+
// invalid char, silent wrong-answer. Reject non-hex chars up front.
|
|
35
34
|
if (hex.length > 0 && !/^[0-9a-fA-F]*$/.test(hex))
|
|
36
35
|
throw new RangeError('hexToBytes: input contains non-hex characters');
|
|
37
36
|
const bin = new Uint8Array(hex.length >>> 1);
|
|
@@ -114,7 +113,7 @@ export const base64ToBytes = (b64) => {
|
|
|
114
113
|
}
|
|
115
114
|
return bin;
|
|
116
115
|
};
|
|
117
|
-
/** Uint8Array to base64 string. Pass url=true for base64url (RFC 4648 §5
|
|
116
|
+
/** Uint8Array to base64 string. Pass url=true for base64url (RFC 4648 §5, no padding characters). */
|
|
118
117
|
export const bytesToBase64 = (bytes, url = false) => {
|
|
119
118
|
if (typeof btoa !== 'undefined') {
|
|
120
119
|
const raw = btoa(String.fromCharCode.apply(null, Array.from(bytes)));
|
|
@@ -141,88 +140,81 @@ export const bytesToBase64 = (bytes, url = false) => {
|
|
|
141
140
|
return base64;
|
|
142
141
|
};
|
|
143
142
|
// ── Constant-time comparison ────────────────────────────────────────────────
|
|
144
|
-
import {
|
|
145
|
-
let
|
|
146
|
-
let
|
|
147
|
-
let
|
|
148
|
-
let
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
*/
|
|
160
|
-
function _initCt() {
|
|
161
|
-
if (_ctInit) {
|
|
162
|
-
if (_ctInitError)
|
|
163
|
-
throw _ctInitError;
|
|
143
|
+
import { CTE_WASM } from './cte-wasm.js';
|
|
144
|
+
let _cteCompare = null;
|
|
145
|
+
let _cteMem = null;
|
|
146
|
+
let _cteMemView = null;
|
|
147
|
+
let _cteInit = false;
|
|
148
|
+
let _cteInitError = null;
|
|
149
|
+
export const CTE_MAX_BYTES = 32768;
|
|
150
|
+
function _initCte() {
|
|
151
|
+
if (_cteInit) {
|
|
152
|
+
if (_cteInitError)
|
|
153
|
+
throw _cteInitError;
|
|
164
154
|
return;
|
|
165
155
|
}
|
|
166
|
-
|
|
156
|
+
_cteInit = true;
|
|
167
157
|
if (!hasSIMD()) {
|
|
168
|
-
|
|
158
|
+
_cteInitError = new Error('leviathan-crypto: constantTimeEqual requires WebAssembly SIMD, '
|
|
169
159
|
+ 'this runtime does not support it');
|
|
170
|
-
throw
|
|
160
|
+
throw _cteInitError;
|
|
171
161
|
}
|
|
172
162
|
try {
|
|
173
|
-
const buf =
|
|
163
|
+
const buf = CTE_WASM.buffer.slice(CTE_WASM.byteOffset, CTE_WASM.byteOffset + CTE_WASM.byteLength);
|
|
174
164
|
const mod = new WebAssembly.Module(buf);
|
|
175
165
|
const inst = new WebAssembly.Instance(mod);
|
|
176
166
|
const exports = inst.exports;
|
|
177
|
-
|
|
178
|
-
|
|
167
|
+
_cteMem = exports.memory;
|
|
168
|
+
_cteMemView = new Uint8Array(_cteMem.buffer);
|
|
169
|
+
_cteCompare = exports.compare;
|
|
179
170
|
}
|
|
180
171
|
catch (cause) {
|
|
181
|
-
|
|
182
|
-
throw
|
|
172
|
+
_cteInitError = new Error(`leviathan-crypto: cte WASM module failed to instantiate: ${cause.message}`);
|
|
173
|
+
throw _cteInitError;
|
|
183
174
|
}
|
|
184
175
|
}
|
|
185
176
|
/**
|
|
186
177
|
* Constant-time byte-array equality.
|
|
187
178
|
* Runs entirely inside a WASM SIMD module (v128 XOR accumulate with
|
|
188
|
-
* branch-free reduction). Throws on runtimes without SIMD support
|
|
179
|
+
* branch-free reduction). Throws on runtimes without SIMD support,
|
|
189
180
|
* no JS fallback. Length check is not constant-time (length is
|
|
190
181
|
* non-secret in all protocols). Max input size: 32768 bytes per side.
|
|
191
182
|
*/
|
|
192
183
|
export const constantTimeEqual = (a, b) => {
|
|
193
184
|
if (a.length !== b.length)
|
|
194
185
|
return false;
|
|
195
|
-
if (a.length >
|
|
196
|
-
throw new RangeError(`constantTimeEqual: max ${
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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);
|
|
186
|
+
if (a.length > CTE_MAX_BYTES)
|
|
187
|
+
throw new RangeError(`constantTimeEqual: max ${CTE_MAX_BYTES} bytes (got ${a.length})`);
|
|
188
|
+
_initCte();
|
|
189
|
+
const mem = _cteMemView;
|
|
190
|
+
const compare = _cteCompare;
|
|
191
|
+
if (!mem || !compare)
|
|
192
|
+
throw new Error('leviathan-crypto: cte init invariant violated');
|
|
206
193
|
mem.set(a, 0);
|
|
207
194
|
mem.set(b, a.length);
|
|
208
195
|
try {
|
|
209
196
|
return compare(0, a.length, a.length) === 1;
|
|
210
197
|
}
|
|
211
198
|
finally {
|
|
212
|
-
|
|
199
|
+
// Wipe the full cte memory region, not just the bytes we wrote.
|
|
200
|
+
// Defense in depth against stale residue from longer prior calls.
|
|
201
|
+
// The module-private WASM memory is never read from outside this
|
|
202
|
+
// function, but we keep the surface clean regardless.
|
|
203
|
+
mem.fill(0, 0, CTE_MAX_BYTES * 2);
|
|
213
204
|
}
|
|
214
205
|
};
|
|
215
206
|
/**
|
|
216
|
-
* Reset the internal
|
|
207
|
+
* Reset the internal CTE WASM cache, including any cached initialization
|
|
217
208
|
* error. Exists so the test suite can force re-instantiation across
|
|
218
209
|
* describe blocks.
|
|
219
210
|
* @internal
|
|
220
211
|
*/
|
|
221
|
-
export function
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
212
|
+
export function _cteResetForTesting() {
|
|
213
|
+
_cteInit = false;
|
|
214
|
+
_cteCompare = null;
|
|
215
|
+
_cteMem = null;
|
|
216
|
+
_cteMemView = null;
|
|
217
|
+
_cteInitError = null;
|
|
226
218
|
}
|
|
227
219
|
/** Zero a typed array in place. */
|
|
228
220
|
export const wipe = (data) => {
|
|
@@ -249,7 +241,7 @@ export const concat = (...arrays) => {
|
|
|
249
241
|
export const randomBytes = (n) => {
|
|
250
242
|
if (typeof globalThis.crypto === 'undefined'
|
|
251
243
|
|| typeof globalThis.crypto.getRandomValues !== 'function')
|
|
252
|
-
throw new Error('leviathan-crypto: crypto.getRandomValues is required
|
|
244
|
+
throw new Error('leviathan-crypto: crypto.getRandomValues is required, '
|
|
253
245
|
+ 'this runtime does not expose the Web Crypto API');
|
|
254
246
|
const buf = new Uint8Array(n);
|
|
255
247
|
globalThis.crypto.getRandomValues(buf);
|
|
@@ -269,7 +261,7 @@ export function hasSIMD() {
|
|
|
269
261
|
_simd = false;
|
|
270
262
|
return _simd;
|
|
271
263
|
}
|
|
272
|
-
// Minimal WASM module using v128
|
|
264
|
+
// Minimal WASM module using v128, validates iff SIMD is supported
|
|
273
265
|
try {
|
|
274
266
|
_simd = WebAssembly.validate(new Uint8Array([
|
|
275
267
|
0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123,
|
package/dist/wasm-source.d.ts
CHANGED
|
@@ -1,13 +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
|
-
* - `PromiseLike<WasmSource>`
|
|
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
11
|
* is resolved recursively (max depth 3).
|
|
12
12
|
*/
|
|
13
13
|
export type WasmSource = string | URL | ArrayBuffer | Uint8Array | WebAssembly.Module | Response | PromiseLike<WasmSource>;
|
package/dist/wasm-source.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WASM_GZ_BASE64 as curve25519Wasm, WASM_GZ_BASE64 as x25519Wasm, } from '../embedded/curve25519.js';
|