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/docs/types.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
<img src="https://github.com/xero/leviathan-crypto/raw/main/docs/logo.svg" alt="logo" width="120" align="left" margin="10">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
### TypeScript Interfaces
|
|
4
|
+
|
|
5
|
+
Defines the abstract interfaces all leviathan-crypto cryptographic classes implement. These are type-only exports; they contain no runtime code and generate no JavaScript output.
|
|
5
6
|
|
|
6
7
|
> ### Table of Contents
|
|
7
8
|
> - [API Reference](#api-reference)
|
|
8
9
|
> - [Usage Examples](#usage-examples)
|
|
10
|
+
> - [Generator](#generator)
|
|
11
|
+
> - [HashFn](#hashfn)
|
|
9
12
|
> - [WasmSource](#wasmsource)
|
|
10
13
|
> - [CipherSuite](#ciphersuite)
|
|
11
14
|
> - [DerivedKeys](#derivedkeys)
|
|
@@ -188,6 +191,60 @@ function cleanup(ctx: EncryptionContext): void {
|
|
|
188
191
|
|
|
189
192
|
---
|
|
190
193
|
|
|
194
|
+
## Generator
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
interface Generator {
|
|
198
|
+
readonly keySize: number; // bytes
|
|
199
|
+
readonly blockSize: number; // bytes per cipher block
|
|
200
|
+
readonly counterSize: number; // bytes
|
|
201
|
+
readonly wasmModules: readonly string[];
|
|
202
|
+
generate(key: Uint8Array, counter: Uint8Array, n: number): Uint8Array;
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Stateless cipher PRF. Used by `Fortuna` as the generator slot.
|
|
207
|
+
Implementations are plain const objects; they assert that no stateful
|
|
208
|
+
instance owns the underlying WASM module before each call but do not
|
|
209
|
+
acquire it themselves.
|
|
210
|
+
|
|
211
|
+
| Member | Description |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `keySize` | Generator key size in bytes. Must equal the paired `HashFn.outputSize` when used with `Fortuna`. |
|
|
214
|
+
| `blockSize` | Bytes per cipher block. Used by `Fortuna` to compute counter advancement. |
|
|
215
|
+
| `counterSize` | Counter width in bytes. `Fortuna` allocates its `genCnt` of this size. |
|
|
216
|
+
| `wasmModules` | List of WASM module names the generator depends on. Used for `init()` preflight in `Fortuna.create()`. |
|
|
217
|
+
| `generate(key, counter, n)` | Produces `n` bytes of keystream from `(key, counter)`. Stateless; does not mutate either input. |
|
|
218
|
+
|
|
219
|
+
Shipped implementations: `SerpentGenerator` (from `'leviathan-crypto/serpent'`), `ChaCha20Generator` (from `'leviathan-crypto/chacha20'`).
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## HashFn
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
interface HashFn {
|
|
227
|
+
readonly outputSize: number; // bytes
|
|
228
|
+
readonly wasmModules: readonly string[];
|
|
229
|
+
digest(msg: Uint8Array): Uint8Array;
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Stateless hash function. Used by `Fortuna` for the accumulator chain and
|
|
234
|
+
the reseed key derivation. Distinct from the existing `Hash` interface
|
|
235
|
+
above, which describes class-shaped instances that own scratch state and
|
|
236
|
+
require `dispose()`.
|
|
237
|
+
|
|
238
|
+
| Member | Description |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `outputSize` | Digest size in bytes. Must equal the paired `Generator.keySize` when used with `Fortuna`. |
|
|
241
|
+
| `wasmModules` | List of WASM module names the hash depends on. |
|
|
242
|
+
| `digest(msg)` | Produces a digest of the input. Stateless; safe to call concurrently with itself within a single JavaScript turn. |
|
|
243
|
+
|
|
244
|
+
Shipped implementations: `SHA256Hash` (from `'leviathan-crypto/sha2'`), `SHA3_256Hash` (from `'leviathan-crypto/sha3'`).
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
191
248
|
## WasmSource
|
|
192
249
|
|
|
193
250
|
Union type for WASM module sources. Accepted by `init()`, `serpentInit()`, etc.
|
|
@@ -210,15 +267,16 @@ Cipher-specific logic injected into `SealStream` and `OpenStream`.
|
|
|
210
267
|
| `kemCtSize` | `number` | KEM ciphertext byte length appended to the header in the preamble. `0` for symmetric suites |
|
|
211
268
|
| `tagSize` | `number` | Authentication tag size in bytes |
|
|
212
269
|
| `padded` | `boolean` | Whether ciphertext includes padding (PKCS7 for CBC) |
|
|
270
|
+
| `wasmChunkSize` | `number` | WASM buffer capacity for one padded chunk. `SealStreamPool.create()` validates `paddedFull ≤ wasmChunkSize` at startup for padded ciphers and throws `RangeError` if the check fails. `SerpentCipher`: 65552. `XChaCha20Cipher`: 65536. `KyberSuite` forwards from its inner cipher. |
|
|
213
271
|
| `wasmModules` | `readonly string[]` | Cipher-specific WASM modules used by pool workers and per-chunk operations (not transitive dependencies such as HKDF-SHA-256 used by `deriveKeys()`) |
|
|
214
272
|
|
|
215
273
|
| Method | Signature | Description |
|
|
216
274
|
|--------|-----------|-------------|
|
|
217
|
-
| `deriveKeys` | `(masterKey, nonce) → DerivedKeys` | HKDF key derivation |
|
|
275
|
+
| `deriveKeys` | `(masterKey, nonce, kemCt?) → DerivedKeys` | HKDF key derivation. `kemCt` is the KEM ciphertext; present only for hybrid suites. |
|
|
218
276
|
| `sealChunk` | `(keys, counterNonce, chunk, aad?) → Uint8Array` | Encrypt one chunk |
|
|
219
277
|
| `openChunk` | `(keys, counterNonce, chunk, aad?) → Uint8Array` | Decrypt one chunk |
|
|
220
278
|
| `wipeKeys` | `(keys) → void` | Zero derived key material |
|
|
221
|
-
| `createPoolWorker` | `() → Worker` | Create a Web Worker for pool use |
|
|
279
|
+
| `createPoolWorker` | `() → Worker` | Create a Web Worker for pool use. Default spawns a classic worker from a blob URL over a build-time IIFE; override via spread for strict-CSP environments. See [ciphersuite.md](./ciphersuite.md). |
|
|
222
280
|
|
|
223
281
|
Implementations: `XChaCha20Cipher`, `SerpentCipher` (plain `const` objects, not classes), and `KyberSuite` (factory function returning a `CipherSuite`). See [ciphersuite.md](./ciphersuite.md).
|
|
224
282
|
|
|
@@ -235,6 +293,7 @@ Opaque key material returned by `CipherSuite.deriveKeys()`.
|
|
|
235
293
|
| Field | Type | Description |
|
|
236
294
|
|-------|------|-------------|
|
|
237
295
|
| `bytes` | `readonly Uint8Array` | Raw derived key bytes (opaque to the stream layer) |
|
|
296
|
+
| `kemCiphertext?` | `readonly Uint8Array \| undefined` | KEM ciphertext produced during encapsulation. Present only for hybrid KEM suites; absent for symmetric suites. |
|
|
238
297
|
|
|
239
298
|
---
|
|
240
299
|
|
|
@@ -263,13 +322,95 @@ Options for `SealStreamPool.create()`.
|
|
|
263
322
|
|
|
264
323
|
---
|
|
265
324
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
325
|
+
## Ratchet types
|
|
326
|
+
|
|
327
|
+
Shared types for the ratchet KDF module. See [ratchet.md](./ratchet.md) for full API.
|
|
328
|
+
|
|
329
|
+
### MlKemLike
|
|
330
|
+
|
|
331
|
+
Structural interface satisfied by `MlKem512`, `MlKem768`, and `MlKem1024`. Used as the `kem` parameter type for `kemRatchetEncap`, `kemRatchetDecap`, and `RatchetKeypair`.
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
interface MlKemLike {
|
|
335
|
+
readonly params: KyberParams
|
|
336
|
+
keygen(): { encapsulationKey: Uint8Array; decapsulationKey: Uint8Array }
|
|
337
|
+
encapsulate(ek: Uint8Array): { ciphertext: Uint8Array; sharedSecret: Uint8Array }
|
|
338
|
+
decapsulate(dk: Uint8Array, ct: Uint8Array): Uint8Array
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### RatchetInitResult
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
interface RatchetInitResult {
|
|
346
|
+
readonly nextRootKey: Uint8Array // 32 bytes
|
|
347
|
+
readonly sendChainKey: Uint8Array // 32 bytes
|
|
348
|
+
readonly recvChainKey: Uint8Array // 32 bytes
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### KemEncapResult
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
interface KemEncapResult {
|
|
356
|
+
readonly nextRootKey: Uint8Array // 32 bytes
|
|
357
|
+
readonly sendChainKey: Uint8Array // 32 bytes
|
|
358
|
+
readonly recvChainKey: Uint8Array // 32 bytes
|
|
359
|
+
readonly kemCt: Uint8Array // ML-KEM ciphertext — transmit in-band
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### KemDecapResult
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
interface KemDecapResult {
|
|
367
|
+
readonly nextRootKey: Uint8Array // 32 bytes
|
|
368
|
+
readonly sendChainKey: Uint8Array // 32 bytes
|
|
369
|
+
readonly recvChainKey: Uint8Array // 32 bytes
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### RatchetMessageHeader
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
interface RatchetMessageHeader {
|
|
377
|
+
readonly epoch: number // sender's epoch at seal time; starts 0, increments on ratchet step
|
|
378
|
+
readonly counter: number // KDFChain.n at seal time (post-step value, first message = 1)
|
|
379
|
+
readonly pn?: number // previous chain length — present only on the first message of a new epoch
|
|
380
|
+
readonly kemCt?: Uint8Array // ML-KEM ciphertext — present only on the first message of a new epoch (encap side)
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Canonical header shape for a ratchet-protected message. `pn` and `kemCt` are absent on every message except the first one of a new epoch, where both must be present together.
|
|
385
|
+
|
|
386
|
+
### ResolveHandle
|
|
387
|
+
|
|
388
|
+
Return type of `SkippedKeyStore.resolve()`.
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
interface ResolveHandle {
|
|
392
|
+
readonly key: Uint8Array // 32-byte message key — throws after settlement
|
|
393
|
+
commit(): void // wipe key and mark settled — call on successful decrypt
|
|
394
|
+
rollback(): void // return key to store and mark settled — call on auth failure
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
`commit()` and `rollback()` are mutually exclusive; calling either a second time (or calling the other after settling) throws `Error: 'SkippedKeyStore: handle already settled'`. Accessing `.key` after settlement also throws. This enforces the delete-on-use contract: a key is consumed exactly once, either by committing (decrypt succeeded, key wiped) or rolling back (decrypt failed, key returned to the store for a future legitimate delivery at the same counter).
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
## Cross-References
|
|
404
|
+
|
|
405
|
+
| Document | Description |
|
|
406
|
+
| -------- | ----------- |
|
|
407
|
+
| [index](./README.md) | Project Documentation index |
|
|
408
|
+
| [architecture](./architecture.md) | architecture overview, module relationships, buffer layouts, and build pipeline |
|
|
409
|
+
| [utils](./utils.md) | encoding utilities and `constantTimeEqual` for verifying MACs from `KeyedHash` |
|
|
410
|
+
| [serpent](./serpent.md) | Serpent classes implement `Blockcipher`, `Streamcipher`, and `AEAD` |
|
|
411
|
+
| [chacha20](./chacha20.md) | `XChaCha20Cipher` is a `CipherSuite` for `SealStream`/`OpenStream`/`Seal`; `Seal` provides one-shot AEAD over any `CipherSuite`; `ChaCha20`/`ChaCha20Poly1305`/`XChaCha20Poly1305` are stateless primitives |
|
|
412
|
+
| [sha2](./sha2.md) | SHA-2 classes implement `Hash`; HMAC classes implement `KeyedHash` |
|
|
413
|
+
| [sha3](./sha3.md) | SHA-3 classes implement `Hash`; SHAKE classes extend with XOF API |
|
|
414
|
+
| [ratchet](./ratchet.md) | ratchet KDF primitives; `MlKemLike`, `RatchetInitResult`, `KemEncapResult`, `KemDecapResult`, `RatchetMessageHeader`, `ResolveHandle` |
|
|
415
|
+
| [test-suite](./test-suite.md) | test suite structure and vector corpus |
|
|
416
|
+
|
package/dist/docs/utils.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
<img src="https://github.com/xero/leviathan-crypto/raw/main/docs/logo.svg" alt="logo" width="120" align="left" margin="10">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
### Utilities
|
|
4
|
+
|
|
5
|
+
Pure TypeScript utilities that ship alongside the WASM-backed primitives. No `init()` call required; all functions work immediately on import.
|
|
5
6
|
|
|
6
7
|
> ### Table of Contents
|
|
7
8
|
> - [Security Notes](#security-notes)
|
|
@@ -11,125 +12,201 @@
|
|
|
11
12
|
|
|
12
13
|
---
|
|
13
14
|
|
|
14
|
-
The module covers encoding (hex, UTF-8, and base64 conversions between strings
|
|
15
|
+
The module covers encoding (hex, UTF-8, and base64 conversions between strings
|
|
16
|
+
and `Uint8Array`), security (constant-time comparison and secure memory
|
|
17
|
+
wiping), byte manipulation (XOR and concatenation), and random byte generation.
|
|
15
18
|
|
|
16
19
|
## Security Notes
|
|
17
20
|
|
|
18
|
-
**`constantTimeEqual`**
|
|
21
|
+
**`constantTimeEqual`** runs inside a WASM SIMD module that removes the JS JIT
|
|
22
|
+
compiler from the timing picture. Use this function whenever you compare MACs,
|
|
23
|
+
hashes, authentication tags, or any secret-derived values. Never use `===`,
|
|
24
|
+
`Buffer.equals`, or a manual loop-with-break for security comparisons. Those
|
|
25
|
+
leak timing information that attackers can exploit to recover secrets. Inputs
|
|
26
|
+
are limited to [`CT_MAX_BYTES`](#ct_max_bytes) (32768 bytes) per side.
|
|
19
27
|
|
|
20
|
-
The length check in `constantTimeEqual` is not constant-time, because array
|
|
28
|
+
The length check in `constantTimeEqual` is not constant-time, because array
|
|
29
|
+
length is non-secret in all standard protocols. If your use case treats length
|
|
30
|
+
as secret, pad to equal length before comparing.
|
|
21
31
|
|
|
22
|
-
**`wipe`** zeroes a typed array in-place. Call it on keys, plaintext buffers,
|
|
32
|
+
**`wipe`** zeroes a typed array in-place. Call it on keys, plaintext buffers,
|
|
33
|
+
and any other sensitive data as soon as you are done with them. JavaScript's
|
|
34
|
+
garbage collector does not guarantee timely or complete erasure of memory.
|
|
23
35
|
|
|
24
|
-
**`randomBytes`** delegates to `crypto.getRandomValues` (Web Crypto API), which
|
|
36
|
+
**`randomBytes`** delegates to `crypto.getRandomValues` (Web Crypto API), which
|
|
37
|
+
is cryptographically secure in all modern browsers and Node.js 19+. It does not
|
|
38
|
+
fall back to `Math.random` or any insecure source.
|
|
25
39
|
|
|
26
|
-
The encoding functions (`hexToBytes`, `bytesToHex`, `utf8ToBytes`,
|
|
40
|
+
The encoding functions (`hexToBytes`, `bytesToHex`, `utf8ToBytes`,
|
|
41
|
+
`bytesToUtf8`, `base64ToBytes`, `bytesToBase64`) perform no security-sensitive
|
|
42
|
+
operations.
|
|
27
43
|
|
|
28
44
|
---
|
|
29
45
|
|
|
30
46
|
## API Reference
|
|
31
47
|
|
|
32
|
-
###
|
|
48
|
+
### constantTimeEqual
|
|
33
49
|
|
|
34
50
|
```typescript
|
|
35
|
-
|
|
51
|
+
constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean
|
|
36
52
|
```
|
|
37
53
|
|
|
38
|
-
|
|
54
|
+
Returns `true` if `a` and `b` contain identical bytes. Returns `false`
|
|
55
|
+
immediately if the arrays differ in length (length is non-secret in all
|
|
56
|
+
standard protocols).
|
|
57
|
+
|
|
58
|
+
The comparison runs inside a WASM SIMD module, removing the JS JIT compiler
|
|
59
|
+
from the timing picture. Speculative optimisation and branch prediction inside
|
|
60
|
+
the engine cannot short-circuit the loop. This function requires WebAssembly
|
|
61
|
+
SIMD; on runtimes without SIMD support it throws `Error: leviathan-crypto:
|
|
62
|
+
constantTimeEqual requires WebAssembly SIMD — this runtime does not support it`
|
|
63
|
+
at first call. SIMD has been a baseline feature of all major browsers and
|
|
64
|
+
Node.js 16.4+ since 2021, and the library's symmetric and post-quantum
|
|
65
|
+
primitives require SIMD already; this function tightens that posture
|
|
66
|
+
consistently.
|
|
67
|
+
|
|
68
|
+
The WASM module ORs the two 64-bit lanes of the SIMD accumulator into a scalar,
|
|
69
|
+
then folds it to 0/1 via `~((diff | -diff) >> 63) & 1`. The only remaining
|
|
70
|
+
observable control flow is the outer length check, which operates on non-secret
|
|
71
|
+
input.
|
|
72
|
+
|
|
73
|
+
**Zero-copy design.** The WASM module has no internal staging buffers. The
|
|
74
|
+
wrapper writes `a` into WASM linear memory at offset 0 and `b` immediately
|
|
75
|
+
after at offset `a.length`, then passes those two offsets directly to the
|
|
76
|
+
`compare` function. No data is duplicated inside the WASM binary; the
|
|
77
|
+
comparison reads in-place from the caller-owned positions. This eliminates an
|
|
78
|
+
entire class of residual-data risk: there is no intermediate copy that outlives
|
|
79
|
+
the operation.
|
|
80
|
+
|
|
81
|
+
**Guaranteed memory zeroing.** After every comparison, the wrapper clears both
|
|
82
|
+
input regions via `mem.fill(0, 0, a.length * 2)` inside a `finally` block. The
|
|
83
|
+
wipe runs whether the comparison succeeds, throws, or is interrupted. Sensitive
|
|
84
|
+
material written into WASM linear memory does not persist past the end of the
|
|
85
|
+
call.
|
|
86
|
+
|
|
87
|
+
WASM SIMD JITs do not guarantee cycle-level constant time; the branch-free tail
|
|
88
|
+
is best-effort hardening against branch-prediction side channels. For
|
|
89
|
+
defence-in-depth against timing attacks on tag comparisons, pair this primitive
|
|
90
|
+
with an authenticated construction (`SerpentCipher`, `XChaCha20Cipher`) that
|
|
91
|
+
terminates on mismatch with a generic error message.
|
|
92
|
+
|
|
93
|
+
Maximum input size is [`CT_MAX_BYTES`](#ct_max_bytes) (32768 bytes) per side.
|
|
94
|
+
Throws `RangeError` if either array exceeds this limit.
|
|
95
|
+
|
|
96
|
+
See [asm_ct.md](./asm_ct.md) for the full WASM module reference, including the
|
|
97
|
+
SIMD algorithm, reduction technique, instantiation model, and memory layout.
|
|
98
|
+
|
|
99
|
+
Use this function when working with lower-level unauthenticated primitives or
|
|
100
|
+
building custom authenticated protocols on top of the hashing and KDF APIs.
|
|
101
|
+
Common cases:
|
|
102
|
+
|
|
103
|
+
- **Encrypt-then-MAC with `SerpentCbc` or `SerpentCtr`.** If you use the
|
|
104
|
+
`dangerUnauthenticated` primitive directly and compute your own HMAC-SHA256
|
|
105
|
+
tag, compare that tag with `constantTimeEqual`. See the [example
|
|
106
|
+
below](#encrypt-then-mac-with-serpentcbc).
|
|
107
|
+
- **Argon2id key verification.** When re-deriving an Argon2id hash to verify a
|
|
108
|
+
passphrase, the final comparison must be constant-time. See
|
|
109
|
+
[argon2id.md](./argon2id.md#password-hashing-and-verification) for the full
|
|
110
|
+
example.
|
|
111
|
+
- **Custom HMAC protocols.** Any protocol where you derive a MAC with
|
|
112
|
+
`HMAC_SHA256` or `HMAC_SHA512` and compare it against a received value. See
|
|
113
|
+
[examples.md](./examples.md#hmac-sha256-message-authentication) for a
|
|
114
|
+
complete example.
|
|
39
115
|
|
|
40
116
|
---
|
|
41
117
|
|
|
42
|
-
###
|
|
118
|
+
### CT_MAX_BYTES
|
|
43
119
|
|
|
44
120
|
```typescript
|
|
45
|
-
|
|
121
|
+
const CT_MAX_BYTES: 32768
|
|
46
122
|
```
|
|
47
123
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
124
|
+
Maximum input size accepted by [`constantTimeEqual`](#constanttimeequal) per
|
|
125
|
+
side, in bytes. Reflects the physical layout of the WASM comparison module: one
|
|
126
|
+
64 KiB page of linear memory split equally between the two input buffers (32
|
|
127
|
+
KiB each).
|
|
51
128
|
|
|
52
|
-
|
|
129
|
+
In practice the largest comparison performed anywhere in this library is a
|
|
130
|
+
32-byte HMAC-SHA-256 tag. This limit only matters for custom protocols that
|
|
131
|
+
compare unusually large values. Use this constant to guard your own inputs
|
|
132
|
+
rather than hardcoding the magic number:
|
|
53
133
|
|
|
54
134
|
```typescript
|
|
55
|
-
|
|
56
|
-
```
|
|
135
|
+
import { constantTimeEqual, CT_MAX_BYTES } from 'leviathan-crypto'
|
|
57
136
|
|
|
58
|
-
|
|
137
|
+
if (a.length > CT_MAX_BYTES || b.length > CT_MAX_BYTES) {
|
|
138
|
+
throw new RangeError(`comparison input exceeds CT_MAX_BYTES (${CT_MAX_BYTES})`)
|
|
139
|
+
}
|
|
140
|
+
const match = constantTimeEqual(a, b)
|
|
141
|
+
```
|
|
59
142
|
|
|
60
143
|
---
|
|
61
144
|
|
|
62
|
-
###
|
|
145
|
+
### hexToBytes
|
|
63
146
|
|
|
64
147
|
```typescript
|
|
65
|
-
|
|
148
|
+
hexToBytes(hex: string): Uint8Array
|
|
66
149
|
```
|
|
67
150
|
|
|
68
|
-
|
|
151
|
+
Converts a hex string to a `Uint8Array`. Accepts lowercase or uppercase
|
|
152
|
+
characters. An optional `0x` or `0X` prefix is stripped automatically. Throws
|
|
153
|
+
`RangeError` on odd-length or non-hex input.
|
|
69
154
|
|
|
70
155
|
---
|
|
71
156
|
|
|
72
|
-
###
|
|
157
|
+
### bytesToHex
|
|
73
158
|
|
|
74
159
|
```typescript
|
|
75
|
-
|
|
160
|
+
bytesToHex(bytes: Uint8Array): string
|
|
76
161
|
```
|
|
77
162
|
|
|
78
|
-
|
|
163
|
+
Converts a `Uint8Array` to a lowercase hex string (no prefix).
|
|
79
164
|
|
|
80
165
|
---
|
|
81
166
|
|
|
82
|
-
###
|
|
167
|
+
### utf8ToBytes
|
|
83
168
|
|
|
84
169
|
```typescript
|
|
85
|
-
|
|
170
|
+
utf8ToBytes(str: string): Uint8Array
|
|
86
171
|
```
|
|
87
172
|
|
|
88
|
-
Encodes a
|
|
173
|
+
Encodes a JavaScript string as UTF-8 bytes using the platform `TextEncoder`.
|
|
89
174
|
|
|
90
175
|
---
|
|
91
176
|
|
|
92
|
-
###
|
|
177
|
+
### bytesToUtf8
|
|
93
178
|
|
|
94
179
|
```typescript
|
|
95
|
-
|
|
180
|
+
bytesToUtf8(bytes: Uint8Array): string
|
|
96
181
|
```
|
|
97
182
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
When WebAssembly SIMD is available the comparison runs inside a WASM module, removing the JS JIT compiler from the timing picture. Speculative optimisation and branch prediction inside the engine cannot short-circuit the loop. On runtimes without SIMD support the function falls back to an XOR-accumulate loop in JavaScript, which is best-effort but not a hardware-level guarantee. The overall posture is best-available constant-time, not a cryptographic proof of timing safety.
|
|
101
|
-
|
|
102
|
-
Maximum input size is [`CT_MAX_BYTES`](#ct_max_bytes) (32768 bytes) per side. Throws `RangeError` if either array exceeds this limit.
|
|
103
|
-
|
|
104
|
-
Use this function when working with lower-level unauthenticated primitives or building custom authenticated protocols on top of the hashing and KDF APIs. Three common cases:
|
|
105
|
-
|
|
106
|
-
**Encrypt-then-MAC with `SerpentCbc` or `SerpentCtr`.** If you use the `dangerUnauthenticated` primitive directly and compute your own HMAC-SHA256 tag, compare that tag with `constantTimeEqual`. See the [example below](#encrypt-then-mac-with-serpentcbc).
|
|
107
|
-
|
|
108
|
-
**Argon2id key verification.** When re-deriving an Argon2id hash to verify a passphrase, the final comparison must be constant-time. See [argon2id.md](./argon2id.md#password-hashing-and-verification) for the full example.
|
|
109
|
-
|
|
110
|
-
**Custom HMAC protocols.** Any protocol where you derive a MAC with `HMAC_SHA256` or `HMAC_SHA512` and compare it against a received value. See [examples.md](./examples.md#hmac-sha256-message-authentication) for a complete example.
|
|
183
|
+
Decodes UTF-8 bytes to a JavaScript string using the platform `TextDecoder`.
|
|
111
184
|
|
|
112
185
|
---
|
|
113
186
|
|
|
114
|
-
###
|
|
187
|
+
### base64ToBytes
|
|
115
188
|
|
|
116
189
|
```typescript
|
|
117
|
-
|
|
190
|
+
base64ToBytes(b64: string): Uint8Array
|
|
118
191
|
```
|
|
119
192
|
|
|
120
|
-
|
|
193
|
+
Decodes a base64 or base64url string to a `Uint8Array`. Handles padded,
|
|
194
|
+
unpadded, and legacy `%3d` padding. Unpadded base64url input is accepted (RFC
|
|
195
|
+
4648 §5). Throws `RangeError` if the input is not valid base64 (illegal
|
|
196
|
+
characters or `rem=1` length).
|
|
121
197
|
|
|
122
|
-
|
|
198
|
+
---
|
|
123
199
|
|
|
124
|
-
|
|
125
|
-
import { constantTimeEqual, CT_MAX_BYTES } from 'leviathan-crypto'
|
|
200
|
+
### bytesToBase64
|
|
126
201
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
const match = constantTimeEqual(a, b)
|
|
202
|
+
```typescript
|
|
203
|
+
bytesToBase64(bytes: Uint8Array, url?: boolean): string
|
|
131
204
|
```
|
|
132
205
|
|
|
206
|
+
Encodes a `Uint8Array` to a base64 string. Pass `url = true` for base64url (RFC
|
|
207
|
+
4648 §5), which uses `-` and `_` instead of `+` and `/` with no padding
|
|
208
|
+
characters. Defaults to standard base64.
|
|
209
|
+
|
|
133
210
|
---
|
|
134
211
|
|
|
135
212
|
### wipe
|
|
@@ -138,7 +215,8 @@ const match = constantTimeEqual(a, b)
|
|
|
138
215
|
wipe(data: Uint8Array | Uint16Array | Uint32Array): void
|
|
139
216
|
```
|
|
140
217
|
|
|
141
|
-
Zeroes a typed array in-place by calling `fill(0)`. Use this to clear keys,
|
|
218
|
+
Zeroes a typed array in-place by calling `fill(0)`. Use this to clear keys,
|
|
219
|
+
plaintext, or any sensitive material when you are done with it.
|
|
142
220
|
|
|
143
221
|
---
|
|
144
222
|
|
|
@@ -148,7 +226,8 @@ Zeroes a typed array in-place by calling `fill(0)`. Use this to clear keys, plai
|
|
|
148
226
|
xor(a: Uint8Array, b: Uint8Array): Uint8Array
|
|
149
227
|
```
|
|
150
228
|
|
|
151
|
-
Returns a new `Uint8Array` where each byte is `a[i] ^ b[i]`. Both arrays must
|
|
229
|
+
Returns a new `Uint8Array` where each byte is `a[i] ^ b[i]`. Both arrays must
|
|
230
|
+
have the same length. Throws `RangeError` if they differ.
|
|
152
231
|
|
|
153
232
|
---
|
|
154
233
|
|
|
@@ -168,7 +247,8 @@ Concatenates one or more `Uint8Array`s into a new array.
|
|
|
168
247
|
randomBytes(n: number): Uint8Array
|
|
169
248
|
```
|
|
170
249
|
|
|
171
|
-
Returns `n` cryptographically secure random bytes via the Web Crypto API
|
|
250
|
+
Returns `n` cryptographically secure random bytes via the Web Crypto API
|
|
251
|
+
(`crypto.getRandomValues`).
|
|
172
252
|
|
|
173
253
|
---
|
|
174
254
|
|
|
@@ -182,13 +262,12 @@ Returns `true` if the current runtime supports WebAssembly SIMD (the `v128`
|
|
|
182
262
|
type and associated operations). The result is computed once on first call by
|
|
183
263
|
validating a minimal v128 WASM module, then cached for subsequent calls.
|
|
184
264
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
265
|
+
A public utility for consumers who want to feature-detect before calling
|
|
266
|
+
`init()`. The library requires SIMD for `serpent`, `chacha20`, and
|
|
267
|
+
`constantTimeEqual`; calls into those modules throw a branded error on
|
|
268
|
+
non-SIMD runtimes. `sha2` and `sha3` do not require SIMD.
|
|
189
269
|
|
|
190
|
-
Supported in all modern browsers and Node.js 16+.
|
|
191
|
-
environments, which fall back silently to the scalar path.
|
|
270
|
+
Supported in all modern browsers and Node.js 16+.
|
|
192
271
|
|
|
193
272
|
---
|
|
194
273
|
|
|
@@ -229,14 +308,17 @@ console.log(b64url) // "bGV2aWF0aGFuLWNyeXB0bw"
|
|
|
229
308
|
|
|
230
309
|
// Decoding (accepts both standard and url variants)
|
|
231
310
|
const decoded = base64ToBytes(b64)
|
|
232
|
-
|
|
311
|
+
console.log(bytesToUtf8(decoded)) // "leviathan-crypto"
|
|
233
312
|
```
|
|
234
313
|
|
|
235
314
|
---
|
|
236
315
|
|
|
237
316
|
### Encrypt-then-MAC with SerpentCbc
|
|
238
317
|
|
|
239
|
-
If you use `SerpentCbc` or `SerpentCtr` directly with `{ dangerUnauthenticated:
|
|
318
|
+
If you use `SerpentCbc` or `SerpentCtr` directly with `{ dangerUnauthenticated:
|
|
319
|
+
true }`, you are responsible for authentication. The correct pattern is
|
|
320
|
+
Encrypt-then-MAC: encrypt first, then compute HMAC-SHA256 over the ciphertext,
|
|
321
|
+
and use `constantTimeEqual` to verify on decrypt.
|
|
240
322
|
|
|
241
323
|
```typescript
|
|
242
324
|
import {
|
|
@@ -288,7 +370,10 @@ wipe(expectedTag)
|
|
|
288
370
|
```
|
|
289
371
|
|
|
290
372
|
> [!NOTE]
|
|
291
|
-
> `Seal` with `SerpentCipher` does all of this for you
|
|
373
|
+
> `Seal` with `SerpentCipher` does all of this for you; key derivation, IV
|
|
374
|
+
> handling, Encrypt-then-MAC, and constant-time verification, with no manual
|
|
375
|
+
> steps. The pattern above is only relevant if you need direct access to the
|
|
376
|
+
> raw `SerpentCbc` primitive.
|
|
292
377
|
|
|
293
378
|
---
|
|
294
379
|
|
|
@@ -343,8 +428,8 @@ console.log(combined.length) // 32
|
|
|
343
428
|
| Function | Condition | Behavior |
|
|
344
429
|
|---|---|---|
|
|
345
430
|
| `hexToBytes` | Odd-length string | Throws `RangeError` |
|
|
346
|
-
| `hexToBytes` | Invalid hex characters |
|
|
347
|
-
| `base64ToBytes` | Invalid length or characters |
|
|
431
|
+
| `hexToBytes` | Invalid hex characters | Throws `RangeError` |
|
|
432
|
+
| `base64ToBytes` | Invalid length or characters | Throws `RangeError` |
|
|
348
433
|
| `constantTimeEqual` | Arrays differ in length | Returns `false` immediately |
|
|
349
434
|
| `constantTimeEqual` | Either array exceeds `CT_MAX_BYTES` | Throws `RangeError` |
|
|
350
435
|
| `xor` | Arrays differ in length | Throws `RangeError` |
|
|
@@ -353,15 +438,20 @@ console.log(combined.length) // 32
|
|
|
353
438
|
|
|
354
439
|
---
|
|
355
440
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
441
|
+
|
|
442
|
+
## Cross-References
|
|
443
|
+
|
|
444
|
+
| Document | Description |
|
|
445
|
+
| -------- | ----------- |
|
|
446
|
+
| [index](./README.md) | Project Documentation index |
|
|
447
|
+
| [architecture](./architecture.md) | architecture overview, module relationships, buffer layouts, and build pipeline |
|
|
448
|
+
| [asm_ct](./asm_ct.md) | WASM module reference for `constantTimeEqual`: SIMD algorithm, zero-copy layout, instantiation model, and memory zeroing |
|
|
449
|
+
| [serpent](./serpent.md) | Serpent modes consume keys from `randomBytes`; wrappers use `wipe` and `constantTimeEqual` |
|
|
450
|
+
| [chacha20](./chacha20.md) | ChaCha20/Poly1305 classes use `randomBytes` for nonce generation |
|
|
451
|
+
| [sha2](./sha2.md) | SHA-2 and HMAC classes; output often converted with `bytesToHex` |
|
|
452
|
+
| [sha3](./sha3.md) | SHA-3 and SHAKE classes; output often converted with `bytesToHex` |
|
|
453
|
+
| [argon2id](./argon2id.md) | passphrase-based encryption; uses `constantTimeEqual` for hash verification |
|
|
454
|
+
| [examples](./examples.md) | full HMAC-SHA256 custom protocol example using `constantTimeEqual` |
|
|
455
|
+
| [types](./types.md) | public interfaces whose implementations rely on these utilities |
|
|
456
|
+
| [test-suite](./test-suite.md) | test suite structure and vector corpus |
|
|
457
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const WORKER_SOURCE = "\"use strict\";(()=>{var U=new Uint8Array([0,97,115,109,1,0,0,0,1,8,1,96,3,127,127,127,1,127,3,2,1,0,5,4,1,1,1,1,7,20,2,7,99,111,109,112,97,114,101,0,0,6,109,101,109,111,114,121,2,0,10,133,1,1,130,1,3,2,127,1,126,1,123,3,64,32,3,65,16,106,34,4,32,2,76,4,64,32,6,32,0,32,3,106,253,0,4,0,32,1,32,3,106,253,0,4,0,253,81,253,80,33,6,32,4,33,3,12,1,11,11,3,64,32,2,32,3,74,4,64,32,5,32,0,32,3,106,49,0,0,32,1,32,3,106,49,0,0,133,132,33,5,32,3,65,1,106,33,3,12,1,11,11,66,0,32,5,32,6,253,29,0,32,6,253,29,1,132,132,34,5,125,32,5,132,66,63,135,66,127,133,167,65,1,113,11]);var v=null,k=null,O=!1,A=null,T=32768;function P(){if(O){if(A)throw A;return}if(O=!0,!R())throw A=new Error(\"leviathan-crypto: constantTimeEqual requires WebAssembly SIMD \\u2014 this runtime does not support it\"),A;try{let t=U.buffer.slice(U.byteOffset,U.byteOffset+U.byteLength),r=new WebAssembly.Module(t),e=new WebAssembly.Instance(r).exports;k=e.memory,v=e.compare}catch(t){throw A=new Error(`leviathan-crypto: ct WASM module failed to instantiate: ${t.message}`),A}}var B=(t,r)=>{if(t.length!==r.length)return!1;if(t.length>T)throw new RangeError(`constantTimeEqual: max ${T} bytes (got ${t.length})`);P();let n=k,e=v;if(!n||!e)throw new Error(\"leviathan-crypto: ct init invariant violated\");let o=new Uint8Array(n.buffer);o.set(t,0),o.set(r,t.length);try{return e(0,t.length,t.length)===1}finally{o.fill(0,0,t.length*2)}};var m=null;function R(){if(m!==null)return m;if(typeof WebAssembly>\"u\"||typeof WebAssembly.validate!=\"function\")return m=!1,m;try{m=WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11]))}catch{m=!1}return m}var d=class t extends Error{constructor(r){super(`${r}: authentication failed`),this.name=\"AuthenticationError\",Object.setPrototypeOf(this,t.prototype)}};function l(t,r){if(r.length===0)return;let n=new Uint8Array(t.memory.buffer),e=t.getPolyMsgOffset(),o=0;for(;o<r.length;){let a=Math.min(64,r.length-o);n.set(r.subarray(o,o+a),e),t.polyUpdate(a),o+=a}}function M(t,r){let n=new Uint8Array(16),e=new DataView(n.buffer);return e.setUint32(0,t>>>0,!0),e.setUint32(4,Math.floor(t/4294967296)>>>0,!0),e.setUint32(8,r>>>0,!0),e.setUint32(12,Math.floor(r/4294967296)>>>0,!0),n}function W(t,r,n,e,o){let a=t.getChunkSize();if(e.length>a)throw new RangeError(`plaintext exceeds ${a} bytes \\u2014 split into smaller chunks`);let u=new Uint8Array(t.memory.buffer);u.set(r,t.getKeyOffset()),u.set(n,t.getChachaNonceOffset()),t.chachaLoadKey(),t.chachaGenPolyKey(),t.polyInit(),l(t,o);let f=(16-o.length%16)%16;f>0&&l(t,new Uint8Array(f)),t.chachaSetCounter(1),u.set(e,t.getChunkPtOffset()),t.chachaEncryptChunk_simd(e.length);let i=t.getChunkCtOffset(),s=new Uint8Array(t.memory.buffer).slice(i,i+e.length);l(t,s);let g=(16-e.length%16)%16;g>0&&l(t,new Uint8Array(g)),l(t,M(o.length,e.length)),t.polyFinal();let c=t.getPolyTagOffset(),p=new Uint8Array(t.memory.buffer).slice(c,c+16);return{ciphertext:s,tag:p}}function K(t,r,n,e,o,a,u=\"chacha20-poly1305\"){let f=t.getChunkSize();if(e.length>f)throw new RangeError(`ciphertext exceeds ${f} bytes \\u2014 split into smaller chunks`);let i=new Uint8Array(t.memory.buffer);i.set(r,t.getKeyOffset()),i.set(n,t.getChachaNonceOffset()),t.chachaLoadKey(),t.chachaGenPolyKey(),t.polyInit(),l(t,a);let s=(16-a.length%16)%16;s>0&&l(t,new Uint8Array(s)),l(t,e);let g=(16-e.length%16)%16;g>0&&l(t,new Uint8Array(g)),l(t,M(a.length,e.length)),t.polyFinal();let c=t.getPolyTagOffset(),p=new Uint8Array(t.memory.buffer).slice(c,c+16);if(!B(p,o)){let b=t.getChunkCtOffset();i.fill(0,b,b+f);let C=t.getChachaBlockOffset();i.fill(0,C,C+64);let E=t.getPolyKeyOffset();throw i.fill(0,E,E+32),new d(u)}t.chachaSetCounter(1),t.chachaLoadKey(),new Uint8Array(t.memory.buffer).set(e,t.getChunkPtOffset()),t.chachaEncryptChunk_simd(e.length);let w=t.getChunkCtOffset();return new Uint8Array(t.memory.buffer).slice(w,w+e.length)}var y,h;self.onmessage=async t=>{let r=t.data;if(r.type===\"init\"){try{let n=new WebAssembly.Memory({initial:3,maximum:3}),e=r.modules.chacha20;if(y=(await WebAssembly.instantiate(e,{env:{memory:n}})).exports,h=new Uint8Array(r.derivedKeyBytes),h.length!==32)throw new Error(`expected 32 derived key bytes (got ${h.length})`);r.derivedKeyBytes.fill(0),self.postMessage({type:\"ready\"})}catch(n){self.postMessage({type:\"error\",id:-1,message:n.message,isAuthError:!1})}return}if(r.type===\"wipe\"){h&&h.fill(0),h=void 0,y&&y.wipeBuffers(),y=void 0,self.postMessage({type:\"wiped\"});return}if(!y||!h){self.postMessage({type:\"error\",id:r.id,message:\"worker not initialized\",isAuthError:!1});return}try{let{id:n,op:e,counterNonce:o,data:a,aad:u}=r,f=u??new Uint8Array(0),i=r.derivedKeyBytes??h,s;if(e===\"seal\"){let{ciphertext:c,tag:p}=W(y,i,o,a,f);s=new Uint8Array(c.length+16),s.set(c),s.set(p,c.length)}else{let c=a.subarray(0,a.length-16),p=a.subarray(a.length-16);s=K(y,i,o,c,p,f,\"xchacha20-poly1305\")}let g=s.buffer instanceof ArrayBuffer?[s.buffer]:[];self.postMessage({type:\"result\",id:n,data:s},{transfer:g})}catch(n){let e=n instanceof d;self.postMessage({type:\"error\",id:r.id,message:n.message,cipher:e?\"xchacha20-poly1305\":void 0,isAuthError:e})}finally{r.derivedKeyBytes&&r.derivedKeyBytes.fill(0),y&&y.wipeBuffers()}};})();\n";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Generated by scripts/embed-workers.ts — do not edit
|
|
2
|
+
// IIFE-bundled pool worker source. Spawned via blob URL by
|
|
3
|
+
// the cipher-suite.ts createPoolWorker; consumers do not
|
|
4
|
+
// import this directly.
|
|
5
|
+
export const WORKER_SOURCE = "\"use strict\";(()=>{var U=new Uint8Array([0,97,115,109,1,0,0,0,1,8,1,96,3,127,127,127,1,127,3,2,1,0,5,4,1,1,1,1,7,20,2,7,99,111,109,112,97,114,101,0,0,6,109,101,109,111,114,121,2,0,10,133,1,1,130,1,3,2,127,1,126,1,123,3,64,32,3,65,16,106,34,4,32,2,76,4,64,32,6,32,0,32,3,106,253,0,4,0,32,1,32,3,106,253,0,4,0,253,81,253,80,33,6,32,4,33,3,12,1,11,11,3,64,32,2,32,3,74,4,64,32,5,32,0,32,3,106,49,0,0,32,1,32,3,106,49,0,0,133,132,33,5,32,3,65,1,106,33,3,12,1,11,11,66,0,32,5,32,6,253,29,0,32,6,253,29,1,132,132,34,5,125,32,5,132,66,63,135,66,127,133,167,65,1,113,11]);var v=null,k=null,O=!1,A=null,T=32768;function P(){if(O){if(A)throw A;return}if(O=!0,!R())throw A=new Error(\"leviathan-crypto: constantTimeEqual requires WebAssembly SIMD \\u2014 this runtime does not support it\"),A;try{let t=U.buffer.slice(U.byteOffset,U.byteOffset+U.byteLength),r=new WebAssembly.Module(t),e=new WebAssembly.Instance(r).exports;k=e.memory,v=e.compare}catch(t){throw A=new Error(`leviathan-crypto: ct WASM module failed to instantiate: ${t.message}`),A}}var B=(t,r)=>{if(t.length!==r.length)return!1;if(t.length>T)throw new RangeError(`constantTimeEqual: max ${T} bytes (got ${t.length})`);P();let n=k,e=v;if(!n||!e)throw new Error(\"leviathan-crypto: ct init invariant violated\");let o=new Uint8Array(n.buffer);o.set(t,0),o.set(r,t.length);try{return e(0,t.length,t.length)===1}finally{o.fill(0,0,t.length*2)}};var m=null;function R(){if(m!==null)return m;if(typeof WebAssembly>\"u\"||typeof WebAssembly.validate!=\"function\")return m=!1,m;try{m=WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11]))}catch{m=!1}return m}var d=class t extends Error{constructor(r){super(`${r}: authentication failed`),this.name=\"AuthenticationError\",Object.setPrototypeOf(this,t.prototype)}};function l(t,r){if(r.length===0)return;let n=new Uint8Array(t.memory.buffer),e=t.getPolyMsgOffset(),o=0;for(;o<r.length;){let a=Math.min(64,r.length-o);n.set(r.subarray(o,o+a),e),t.polyUpdate(a),o+=a}}function M(t,r){let n=new Uint8Array(16),e=new DataView(n.buffer);return e.setUint32(0,t>>>0,!0),e.setUint32(4,Math.floor(t/4294967296)>>>0,!0),e.setUint32(8,r>>>0,!0),e.setUint32(12,Math.floor(r/4294967296)>>>0,!0),n}function W(t,r,n,e,o){let a=t.getChunkSize();if(e.length>a)throw new RangeError(`plaintext exceeds ${a} bytes \\u2014 split into smaller chunks`);let u=new Uint8Array(t.memory.buffer);u.set(r,t.getKeyOffset()),u.set(n,t.getChachaNonceOffset()),t.chachaLoadKey(),t.chachaGenPolyKey(),t.polyInit(),l(t,o);let f=(16-o.length%16)%16;f>0&&l(t,new Uint8Array(f)),t.chachaSetCounter(1),u.set(e,t.getChunkPtOffset()),t.chachaEncryptChunk_simd(e.length);let i=t.getChunkCtOffset(),s=new Uint8Array(t.memory.buffer).slice(i,i+e.length);l(t,s);let g=(16-e.length%16)%16;g>0&&l(t,new Uint8Array(g)),l(t,M(o.length,e.length)),t.polyFinal();let c=t.getPolyTagOffset(),p=new Uint8Array(t.memory.buffer).slice(c,c+16);return{ciphertext:s,tag:p}}function K(t,r,n,e,o,a,u=\"chacha20-poly1305\"){let f=t.getChunkSize();if(e.length>f)throw new RangeError(`ciphertext exceeds ${f} bytes \\u2014 split into smaller chunks`);let i=new Uint8Array(t.memory.buffer);i.set(r,t.getKeyOffset()),i.set(n,t.getChachaNonceOffset()),t.chachaLoadKey(),t.chachaGenPolyKey(),t.polyInit(),l(t,a);let s=(16-a.length%16)%16;s>0&&l(t,new Uint8Array(s)),l(t,e);let g=(16-e.length%16)%16;g>0&&l(t,new Uint8Array(g)),l(t,M(a.length,e.length)),t.polyFinal();let c=t.getPolyTagOffset(),p=new Uint8Array(t.memory.buffer).slice(c,c+16);if(!B(p,o)){let b=t.getChunkCtOffset();i.fill(0,b,b+f);let C=t.getChachaBlockOffset();i.fill(0,C,C+64);let E=t.getPolyKeyOffset();throw i.fill(0,E,E+32),new d(u)}t.chachaSetCounter(1),t.chachaLoadKey(),new Uint8Array(t.memory.buffer).set(e,t.getChunkPtOffset()),t.chachaEncryptChunk_simd(e.length);let w=t.getChunkCtOffset();return new Uint8Array(t.memory.buffer).slice(w,w+e.length)}var y,h;self.onmessage=async t=>{let r=t.data;if(r.type===\"init\"){try{let n=new WebAssembly.Memory({initial:3,maximum:3}),e=r.modules.chacha20;if(y=(await WebAssembly.instantiate(e,{env:{memory:n}})).exports,h=new Uint8Array(r.derivedKeyBytes),h.length!==32)throw new Error(`expected 32 derived key bytes (got ${h.length})`);r.derivedKeyBytes.fill(0),self.postMessage({type:\"ready\"})}catch(n){self.postMessage({type:\"error\",id:-1,message:n.message,isAuthError:!1})}return}if(r.type===\"wipe\"){h&&h.fill(0),h=void 0,y&&y.wipeBuffers(),y=void 0,self.postMessage({type:\"wiped\"});return}if(!y||!h){self.postMessage({type:\"error\",id:r.id,message:\"worker not initialized\",isAuthError:!1});return}try{let{id:n,op:e,counterNonce:o,data:a,aad:u}=r,f=u??new Uint8Array(0),i=r.derivedKeyBytes??h,s;if(e===\"seal\"){let{ciphertext:c,tag:p}=W(y,i,o,a,f);s=new Uint8Array(c.length+16),s.set(c),s.set(p,c.length)}else{let c=a.subarray(0,a.length-16),p=a.subarray(a.length-16);s=K(y,i,o,c,p,f,\"xchacha20-poly1305\")}let g=s.buffer instanceof ArrayBuffer?[s.buffer]:[];self.postMessage({type:\"result\",id:n,data:s},{transfer:g})}catch(n){let e=n instanceof d;self.postMessage({type:\"error\",id:r.id,message:n.message,cipher:e?\"xchacha20-poly1305\":void 0,isAuthError:e})}finally{r.derivedKeyBytes&&r.derivedKeyBytes.fill(0),y&&y.wipeBuffers()}};})();\n";
|