leviathan-crypto 1.4.0 → 2.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 +129 -94
- package/README.md +166 -223
- package/SECURITY.md +85 -45
- package/dist/chacha20/cipher-suite.d.ts +4 -0
- package/dist/chacha20/cipher-suite.js +78 -0
- package/dist/chacha20/embedded.d.ts +1 -0
- package/dist/chacha20/embedded.js +27 -0
- package/dist/chacha20/index.d.ts +20 -27
- package/dist/chacha20/index.js +40 -59
- package/dist/chacha20/ops.d.ts +1 -1
- package/dist/chacha20/ops.js +19 -18
- package/dist/chacha20/pool-worker.js +77 -0
- package/dist/ct-wasm.d.ts +1 -0
- package/dist/ct-wasm.js +3 -0
- package/dist/ct.wasm +0 -0
- package/dist/docs/aead.md +320 -0
- package/dist/docs/architecture.md +419 -285
- package/dist/docs/argon2id.md +42 -30
- package/dist/docs/chacha20.md +192 -266
- package/dist/docs/exports.md +241 -0
- package/dist/docs/fortuna.md +60 -69
- package/dist/docs/init.md +172 -178
- package/dist/docs/loader.md +87 -142
- package/dist/docs/serpent.md +134 -583
- package/dist/docs/sha2.md +91 -103
- package/dist/docs/sha3.md +70 -36
- package/dist/docs/types.md +93 -16
- package/dist/docs/utils.md +109 -32
- package/dist/embedded/kyber.d.ts +1 -0
- package/dist/embedded/kyber.js +3 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +38 -0
- package/dist/fortuna.d.ts +0 -6
- package/dist/fortuna.js +5 -5
- package/dist/index.d.ts +25 -9
- package/dist/index.js +36 -7
- package/dist/init.d.ts +3 -7
- package/dist/init.js +18 -35
- package/dist/keccak/embedded.d.ts +1 -0
- package/dist/keccak/embedded.js +27 -0
- package/dist/keccak/index.d.ts +4 -0
- package/dist/keccak/index.js +31 -0
- package/dist/kyber/embedded.d.ts +1 -0
- package/dist/kyber/embedded.js +27 -0
- package/dist/kyber/indcpa.d.ts +49 -0
- package/dist/kyber/indcpa.js +352 -0
- package/dist/kyber/index.d.ts +38 -0
- package/dist/kyber/index.js +150 -0
- package/dist/kyber/kem.d.ts +21 -0
- package/dist/kyber/kem.js +160 -0
- package/dist/kyber/params.d.ts +14 -0
- package/dist/kyber/params.js +37 -0
- package/dist/kyber/suite.d.ts +13 -0
- package/dist/kyber/suite.js +93 -0
- package/dist/kyber/types.d.ts +98 -0
- package/dist/kyber/types.js +25 -0
- package/dist/kyber/validate.d.ts +19 -0
- package/dist/kyber/validate.js +68 -0
- package/dist/kyber.wasm +0 -0
- package/dist/loader.d.ts +15 -6
- package/dist/loader.js +65 -21
- package/dist/serpent/cipher-suite.d.ts +4 -0
- package/dist/serpent/cipher-suite.js +121 -0
- package/dist/serpent/embedded.d.ts +1 -0
- package/dist/serpent/embedded.js +27 -0
- package/dist/serpent/index.d.ts +6 -37
- package/dist/serpent/index.js +9 -118
- package/dist/serpent/pool-worker.d.ts +1 -0
- package/dist/serpent/pool-worker.js +202 -0
- package/dist/serpent/serpent-cbc.d.ts +30 -0
- package/dist/serpent/serpent-cbc.js +136 -0
- package/dist/sha2/embedded.d.ts +1 -0
- package/dist/sha2/embedded.js +27 -0
- package/dist/sha2/hkdf.js +6 -2
- package/dist/sha2/index.d.ts +3 -2
- package/dist/sha2/index.js +3 -4
- package/dist/sha3/embedded.d.ts +1 -0
- package/dist/sha3/embedded.js +27 -0
- package/dist/sha3/index.d.ts +3 -2
- package/dist/sha3/index.js +3 -4
- package/dist/stream/constants.d.ts +6 -0
- package/dist/stream/constants.js +30 -0
- package/dist/stream/header.d.ts +9 -0
- package/dist/stream/header.js +77 -0
- package/dist/stream/index.d.ts +7 -0
- package/dist/stream/index.js +27 -0
- package/dist/stream/open-stream.d.ts +21 -0
- package/dist/stream/open-stream.js +146 -0
- package/dist/stream/seal-stream-pool.d.ts +38 -0
- package/dist/stream/seal-stream-pool.js +391 -0
- package/dist/stream/seal-stream.d.ts +20 -0
- package/dist/stream/seal-stream.js +142 -0
- package/dist/stream/seal.d.ts +9 -0
- package/dist/stream/seal.js +75 -0
- package/dist/stream/types.d.ts +24 -0
- package/dist/stream/types.js +26 -0
- package/dist/utils.d.ts +7 -2
- package/dist/utils.js +49 -3
- package/dist/wasm-source.d.ts +12 -0
- package/dist/wasm-source.js +26 -0
- package/package.json +13 -5
- package/dist/chacha20/pool.d.ts +0 -52
- package/dist/chacha20/pool.js +0 -178
- package/dist/chacha20/pool.worker.js +0 -37
- package/dist/chacha20/stream-sealer.d.ts +0 -49
- package/dist/chacha20/stream-sealer.js +0 -327
- package/dist/docs/chacha20_pool.md +0 -309
- package/dist/docs/wasm.md +0 -194
- package/dist/serpent/seal.d.ts +0 -8
- package/dist/serpent/seal.js +0 -72
- package/dist/serpent/stream-pool.d.ts +0 -48
- package/dist/serpent/stream-pool.js +0 -275
- package/dist/serpent/stream-sealer.d.ts +0 -55
- package/dist/serpent/stream-sealer.js +0 -342
- package/dist/serpent/stream.d.ts +0 -28
- package/dist/serpent/stream.js +0 -205
- package/dist/serpent/stream.worker.d.ts +0 -32
- package/dist/serpent/stream.worker.js +0 -117
- /package/dist/chacha20/{pool.worker.d.ts → pool-worker.d.ts} +0 -0
package/dist/chacha20/ops.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// ChaChaExports explicitly. Used by both the class wrappers (index.ts)
|
|
5
5
|
// and the pool worker (pool.worker.ts), eliminating duplication.
|
|
6
6
|
import { constantTimeEqual } from '../utils.js';
|
|
7
|
+
import { AuthenticationError } from '../errors.js';
|
|
7
8
|
// ── Module-private helpers ───────────────────────────────────────────────────
|
|
8
9
|
function polyFeed(x, data) {
|
|
9
10
|
if (data.length === 0)
|
|
@@ -20,16 +21,14 @@ function polyFeed(x, data) {
|
|
|
20
21
|
}
|
|
21
22
|
function lenBlock(aadLen, ctLen) {
|
|
22
23
|
const b = new Uint8Array(16);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
n >>>= 8;
|
|
32
|
-
}
|
|
24
|
+
const dv = new DataView(b.buffer);
|
|
25
|
+
// RFC 8439 §2.8 — 64-bit LE lengths.
|
|
26
|
+
// JS numbers are f64 — write low 32 bits directly, high bits via
|
|
27
|
+
// Math.floor(n / 2^32). Safe for n ≤ Number.MAX_SAFE_INTEGER.
|
|
28
|
+
dv.setUint32(0, aadLen >>> 0, true);
|
|
29
|
+
dv.setUint32(4, Math.floor(aadLen / 0x100000000) >>> 0, true);
|
|
30
|
+
dv.setUint32(8, ctLen >>> 0, true);
|
|
31
|
+
dv.setUint32(12, Math.floor(ctLen / 0x100000000) >>> 0, true);
|
|
33
32
|
return b;
|
|
34
33
|
}
|
|
35
34
|
// ── Inner AEAD (12-byte nonce) ───────────────────────────────────────────────
|
|
@@ -42,7 +41,6 @@ export function aeadEncrypt(x, key, nonce, plaintext, aad) {
|
|
|
42
41
|
// Step 1: Generate Poly1305 one-time key at counter=0 (RFC 8439 §2.6)
|
|
43
42
|
mem.set(key, x.getKeyOffset());
|
|
44
43
|
mem.set(nonce, x.getChachaNonceOffset());
|
|
45
|
-
x.chachaSetCounter(1);
|
|
46
44
|
x.chachaLoadKey();
|
|
47
45
|
x.chachaGenPolyKey();
|
|
48
46
|
// Step 2: Initialise Poly1305
|
|
@@ -57,7 +55,7 @@ export function aeadEncrypt(x, key, nonce, plaintext, aad) {
|
|
|
57
55
|
x.chachaLoadKey();
|
|
58
56
|
// Step 5: Encrypt
|
|
59
57
|
mem.set(plaintext, x.getChunkPtOffset());
|
|
60
|
-
x.
|
|
58
|
+
x.chachaEncryptChunk_simd(plaintext.length);
|
|
61
59
|
const ctOff = x.getChunkCtOffset();
|
|
62
60
|
const ciphertext = new Uint8Array(x.memory.buffer).slice(ctOff, ctOff + plaintext.length);
|
|
63
61
|
// Step 6: MAC ciphertext + pad
|
|
@@ -74,7 +72,7 @@ export function aeadEncrypt(x, key, nonce, plaintext, aad) {
|
|
|
74
72
|
return { ciphertext, tag };
|
|
75
73
|
}
|
|
76
74
|
/** ChaCha20-Poly1305 AEAD decrypt (RFC 8439 §2.8). Constant-time tag comparison. */
|
|
77
|
-
export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad) {
|
|
75
|
+
export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad, cipherName = 'chacha20-poly1305') {
|
|
78
76
|
const maxChunk = x.getChunkSize();
|
|
79
77
|
if (ciphertext.length > maxChunk)
|
|
80
78
|
throw new RangeError(`ciphertext exceeds ${maxChunk} bytes — split into smaller chunks`);
|
|
@@ -82,7 +80,6 @@ export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad) {
|
|
|
82
80
|
// Compute expected tag
|
|
83
81
|
mem.set(key, x.getKeyOffset());
|
|
84
82
|
mem.set(nonce, x.getChachaNonceOffset());
|
|
85
|
-
x.chachaSetCounter(1);
|
|
86
83
|
x.chachaLoadKey();
|
|
87
84
|
x.chachaGenPolyKey();
|
|
88
85
|
x.polyInit();
|
|
@@ -99,13 +96,17 @@ export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad) {
|
|
|
99
96
|
// Constant-time tag comparison
|
|
100
97
|
const tagOff = x.getPolyTagOffset();
|
|
101
98
|
const expectedTag = new Uint8Array(x.memory.buffer).slice(tagOff, tagOff + 16);
|
|
102
|
-
if (!constantTimeEqual(expectedTag, tag))
|
|
103
|
-
|
|
99
|
+
if (!constantTimeEqual(expectedTag, tag)) {
|
|
100
|
+
// Wipe the full chunk output buffer — defense-in-depth before throwing
|
|
101
|
+
const ctOff = x.getChunkCtOffset();
|
|
102
|
+
mem.fill(0, ctOff, ctOff + maxChunk);
|
|
103
|
+
throw new AuthenticationError(cipherName);
|
|
104
|
+
}
|
|
104
105
|
// Decrypt only after authentication succeeds
|
|
105
106
|
x.chachaSetCounter(1);
|
|
106
107
|
x.chachaLoadKey();
|
|
107
108
|
new Uint8Array(x.memory.buffer).set(ciphertext, x.getChunkPtOffset());
|
|
108
|
-
x.
|
|
109
|
+
x.chachaEncryptChunk_simd(ciphertext.length);
|
|
109
110
|
const ptOff = x.getChunkCtOffset();
|
|
110
111
|
return new Uint8Array(x.memory.buffer).slice(ptOff, ptOff + ciphertext.length);
|
|
111
112
|
}
|
|
@@ -142,5 +143,5 @@ export function xcDecrypt(x, key, nonce, ciphertext, aad) {
|
|
|
142
143
|
const tag = ciphertext.subarray(ciphertext.length - 16);
|
|
143
144
|
const subkey = deriveSubkey(x, key, nonce);
|
|
144
145
|
const inner = innerNonce(nonce);
|
|
145
|
-
return aeadDecrypt(x, subkey, inner, ct, tag, aad);
|
|
146
|
+
return aeadDecrypt(x, subkey, inner, ct, tag, aad, 'xchacha20-poly1305');
|
|
146
147
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// / <reference lib="webworker" />
|
|
2
|
+
// src/ts/chacha20/pool-worker.ts
|
|
3
|
+
//
|
|
4
|
+
// Worker for SealStreamPool with XChaCha20Cipher.
|
|
5
|
+
// Holds derived subkey and ChaCha20 WASM instance for the pool's lifetime.
|
|
6
|
+
// Per-job: ChaCha20-Poly1305 AEAD with 12-byte counter nonce.
|
|
7
|
+
import { aeadEncrypt, aeadDecrypt } from './ops.js';
|
|
8
|
+
import { AuthenticationError } from '../errors.js';
|
|
9
|
+
let x;
|
|
10
|
+
let subkey;
|
|
11
|
+
self.onmessage = async (e) => {
|
|
12
|
+
const msg = e.data;
|
|
13
|
+
if (msg.type === 'init') {
|
|
14
|
+
try {
|
|
15
|
+
const mem = new WebAssembly.Memory({ initial: 3, maximum: 3 });
|
|
16
|
+
const mod = msg.modules.chacha20;
|
|
17
|
+
const inst = await WebAssembly.instantiate(mod, { env: { memory: mem } });
|
|
18
|
+
x = inst.exports;
|
|
19
|
+
subkey = new Uint8Array(msg.derivedKeyBytes);
|
|
20
|
+
if (subkey.length !== 32)
|
|
21
|
+
throw new Error(`expected 32 derived key bytes (got ${subkey.length})`);
|
|
22
|
+
msg.derivedKeyBytes.fill(0);
|
|
23
|
+
self.postMessage({ type: 'ready' });
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
self.postMessage({ type: 'error', id: -1, message: err.message, isAuthError: false });
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (msg.type === 'wipe') {
|
|
31
|
+
if (subkey)
|
|
32
|
+
subkey.fill(0);
|
|
33
|
+
subkey = undefined;
|
|
34
|
+
if (x)
|
|
35
|
+
x.wipeBuffers();
|
|
36
|
+
x = undefined;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!x || !subkey) {
|
|
40
|
+
self.postMessage({ type: 'error', id: msg.id, message: 'worker not initialized', isAuthError: false });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const { id, op, counterNonce, data, aad } = msg;
|
|
45
|
+
const aadBytes = aad ?? new Uint8Array(0);
|
|
46
|
+
const jobKey = msg.derivedKeyBytes ?? subkey;
|
|
47
|
+
let result;
|
|
48
|
+
if (op === 'seal') {
|
|
49
|
+
const { ciphertext, tag } = aeadEncrypt(x, jobKey, counterNonce, data, aadBytes);
|
|
50
|
+
result = new Uint8Array(ciphertext.length + 16);
|
|
51
|
+
result.set(ciphertext);
|
|
52
|
+
result.set(tag, ciphertext.length);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const ct = data.subarray(0, data.length - 16);
|
|
56
|
+
const tag = data.subarray(data.length - 16);
|
|
57
|
+
result = aeadDecrypt(x, jobKey, counterNonce, ct, tag, aadBytes, 'xchacha20-poly1305');
|
|
58
|
+
}
|
|
59
|
+
const transfer = result.buffer instanceof ArrayBuffer ? [result.buffer] : [];
|
|
60
|
+
self.postMessage({ type: 'result', id, data: result }, { transfer });
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const isAuth = err instanceof AuthenticationError;
|
|
64
|
+
self.postMessage({
|
|
65
|
+
type: 'error', id: msg.id,
|
|
66
|
+
message: err.message,
|
|
67
|
+
cipher: isAuth ? 'xchacha20-poly1305' : undefined,
|
|
68
|
+
isAuthError: isAuth,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (msg.derivedKeyBytes)
|
|
73
|
+
msg.derivedKeyBytes.fill(0);
|
|
74
|
+
if (x)
|
|
75
|
+
x.wipeBuffers();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CT_WASM: Uint8Array<ArrayBuffer>;
|
package/dist/ct-wasm.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// auto-generated — do not edit
|
|
2
|
+
// raw WASM bytes for constant-time comparison module
|
|
3
|
+
export const CT_WASM = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 8, 1, 96, 3, 127, 127, 127, 1, 127, 2, 16, 1, 3, 101, 110, 118, 6, 109, 101, 109, 111, 114, 121, 2, 1, 1, 1, 3, 2, 1, 0, 7, 20, 2, 7, 99, 111, 109, 112, 97, 114, 101, 0, 0, 6, 109, 101, 109, 111, 114, 121, 2, 0, 10, 111, 1, 109, 2, 3, 127, 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, 45, 0, 0, 32, 1, 32, 3, 106, 45, 0, 0, 115, 114, 33, 5, 32, 3, 65, 1, 106, 33, 3, 12, 1, 11, 11, 32, 6, 253, 83, 4, 64, 65, 0, 15, 11, 32, 5, 69, 11]);
|
package/dist/ct.wasm
ADDED
|
Binary file
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# Authenticated Encryption
|
|
2
|
+
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> Cipher-agnostic authenticated encryption for any scale. One-shot with `Seal`, chunked with `SealStream` and `OpenStream`, or parallel with `SealStreamPool`. All four share a wire format and accept any `CipherSuite`.
|
|
5
|
+
|
|
6
|
+
> ### Table of Contents
|
|
7
|
+
> - [Overview](#overview)
|
|
8
|
+
> - [Security Model](#security-model)
|
|
9
|
+
> - [Wire Format](#wire-format)
|
|
10
|
+
> - [API Reference](#api-reference)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
`Seal`, `SealStream`, `OpenStream`, and `SealStreamPool` are the primary API for authenticated encryption in leviathan-crypto. They are cipher-agnostic: you pass a `CipherSuite` object at construction and the implementation handles key derivation, nonce management, and authentication for you.
|
|
17
|
+
|
|
18
|
+
The four classes form a natural progression. `Seal` handles data that fits in memory. `SealStream` and `OpenStream` handle data that arrives in chunks or is too large to buffer. `SealStreamPool` parallelizes the chunked approach across Web Workers. All four produce and consume the same wire format, so a `Seal` blob can be opened by `OpenStream` and vice versa.
|
|
19
|
+
|
|
20
|
+
Two cipher suites are included. A third wraps either with ML-KEM for post-quantum hybrid encryption.
|
|
21
|
+
|
|
22
|
+
| Suite | Cipher | Tag | Modules |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `SerpentCipher` | Serpent-256 CBC + HMAC-SHA-256 | 32 B | `serpent`, `sha2` |
|
|
25
|
+
| `XChaCha20Cipher` | XChaCha20-Poly1305 | 16 B | `chacha20`, `sha2` |
|
|
26
|
+
| `KyberSuite` | ML-KEM + inner cipher | depends | `kyber`, `sha3`, + inner |
|
|
27
|
+
|
|
28
|
+
See [ciphersuite.md](./ciphersuite.md) for full cipher suite documentation.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Security Model
|
|
33
|
+
|
|
34
|
+
The STREAM construction is based on [Hoang, Reyhanitabar, Rogaway, and Vizár (CRYPTO 2015)](https://eprint.iacr.org/2015/189.pdf). It provides online authenticated encryption with four guarantees.
|
|
35
|
+
|
|
36
|
+
**Per-chunk authentication.** Each chunk is individually authenticated. A tampered chunk is rejected immediately without decrypting anything that follows.
|
|
37
|
+
|
|
38
|
+
**Counter binding.** Each chunk's nonce includes a monotonic counter. Reordering or duplicating chunks produces a counter mismatch and authentication fails.
|
|
39
|
+
|
|
40
|
+
**Final-chunk detection.** The last chunk uses a distinct nonce flag (`TAG_FINAL` vs `TAG_DATA`). Truncating the stream by dropping the final chunk is detected because the opener expects a chunk marked final.
|
|
41
|
+
|
|
42
|
+
**Stream isolation.** Each stream generates a fresh 16-byte random nonce on construction. Two streams with the same key derive independent subkeys via HKDF and cannot interfere with each other.
|
|
43
|
+
|
|
44
|
+
> [!IMPORTANT]
|
|
45
|
+
> `SealStream` is single-use. After `finalize()` is called the derived keys are wiped and no further chunks can be sealed. Create a new `SealStream` for each message. `SealStreamPool.seal()` enforces this with a guard that throws on subsequent calls.
|
|
46
|
+
|
|
47
|
+
### WASM Side-Channel Posture
|
|
48
|
+
|
|
49
|
+
All cryptographic computation runs in WASM outside the JavaScript JIT. Serpent's bitsliced S-box implementation and ChaCha20's quarter-round construction are both branchless and table-free, which eliminates data-dependent timing variation at the algorithm level. WASM lacks hardware-level constant-time guarantees, so this provides stronger posture than pure JavaScript but weaker than native constant-time code. If timing side channels are your primary threat model, a native cryptographic library with verified constant-time guarantees is more appropriate.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Wire Format
|
|
54
|
+
|
|
55
|
+
### Header (20 bytes)
|
|
56
|
+
|
|
57
|
+
Every stream begins with a 20-byte header:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
bytes:
|
|
61
|
+
0: compound enum (bit 7 = framed flag, bit 6 = reserved, bits 0-5 = format ID)
|
|
62
|
+
1-16: random nonce (16 bytes)
|
|
63
|
+
17-19: chunk size as u24 big-endian
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Format IDs:** `0x01` = XChaCha20-Poly1305, `0x02` = Serpent-256. KEM suites encode both the parameter set and inner cipher in a single byte. See [ciphersuite.md](./ciphersuite.md#kybersuite) for the full format enum table.
|
|
67
|
+
|
|
68
|
+
The 16-byte nonce is a HKDF salt, not a direct cipher nonce. `XChaCha20Cipher` passes it to HChaCha20 for subkey derivation. `SerpentCipher` uses it as the HKDF-SHA-256 salt to derive 96 bytes of enc/mac/iv key material.
|
|
69
|
+
|
|
70
|
+
The framed flag (bit 7) prefixes each chunk with a `u32be` length. Use framed mode for flat byte streams where chunks are concatenated without an external framing layer. Leave it off when the transport provides its own message boundaries such as WebSocket frames or IPC messages.
|
|
71
|
+
|
|
72
|
+
### Counter Nonce (12 bytes)
|
|
73
|
+
|
|
74
|
+
Each chunk is encrypted with a 12-byte nonce:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
bytes:
|
|
78
|
+
0-10: 11-byte big-endian counter (monotonically increasing)
|
|
79
|
+
11: final flag (0x00 = TAG_DATA, 0x01 = TAG_FINAL)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The counter starts at 0 and increments with each chunk. The final chunk uses `TAG_FINAL` instead of `TAG_DATA`. A data chunk at counter N and a final chunk at counter N produce distinct nonces, so the construction never reuses a nonce.
|
|
83
|
+
|
|
84
|
+
### Key Derivation
|
|
85
|
+
|
|
86
|
+
HKDF-SHA-256 derives cipher-specific key material from the master key and the random nonce at stream construction:
|
|
87
|
+
|
|
88
|
+
| Cipher | HKDF info | Output | Structure |
|
|
89
|
+
|---|---|---|---|
|
|
90
|
+
| XChaCha20 | `xchacha20-sealstream-v2` | 32 B | HKDF → streamKey → HChaCha20 → subkey |
|
|
91
|
+
| Serpent | `serpent-sealstream-v2` | 96 B | `enc_key[0:32] \| mac_key[32:64] \| iv_key[64:96]` |
|
|
92
|
+
|
|
93
|
+
XChaCha20 performs an additional HChaCha20 subkey derivation step using the first 16 bytes of the nonce. The intermediate streamKey is wiped immediately after use.
|
|
94
|
+
|
|
95
|
+
Serpent derives three keys: an encryption key for CBC, a MAC key for HMAC-SHA-256, and an IV key for per-chunk IV derivation via `HMAC-SHA-256(iv_key, counterNonce)[0:16]`. The CBC IV is derived deterministically on both sides and never transmitted.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## API Reference
|
|
100
|
+
|
|
101
|
+
### Seal
|
|
102
|
+
|
|
103
|
+
`Seal` is a static class, never instantiated. It handles one-shot authenticated encryption and decryption. A `Seal` blob is structurally identical to a single-chunk `SealStream` output: `preamble || finalChunk(counter=0, TAG_FINAL)`. `OpenStream.finalize()` can open a `Seal` blob directly, and `Seal.decrypt()` can open a single-chunk `SealStream`.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { init, Seal, XChaCha20Cipher } from 'leviathan-crypto'
|
|
107
|
+
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
|
|
108
|
+
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
|
|
109
|
+
|
|
110
|
+
await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
|
|
111
|
+
|
|
112
|
+
const key = XChaCha20Cipher.keygen()
|
|
113
|
+
const blob = Seal.encrypt(XChaCha20Cipher, key, plaintext)
|
|
114
|
+
const pt = Seal.decrypt(XChaCha20Cipher, key, blob) // throws AuthenticationError on tamper
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| Method | Returns | Description |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `Seal.encrypt(suite, key, plaintext, opts?)` | `Uint8Array` | One-shot encrypt. Returns `preamble \|\| chunk`. |
|
|
120
|
+
| `Seal.decrypt(suite, key, blob, opts?)` | `Uint8Array` | One-shot decrypt. Throws `AuthenticationError` on tamper. |
|
|
121
|
+
|
|
122
|
+
**`opts.aad`** — optional `Uint8Array`. Additional Authenticated Data: authenticated but not encrypted. Pass the same value to both `encrypt` and `decrypt`.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### SealStream
|
|
127
|
+
|
|
128
|
+
> [!NOTE]
|
|
129
|
+
> All stream classes require `sha2` for HKDF key derivation. Load it alongside your cipher module before constructing any stream.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { init, SealStream } from 'leviathan-crypto'
|
|
133
|
+
import { XChaCha20Cipher } from 'leviathan-crypto/chacha20'
|
|
134
|
+
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
|
|
135
|
+
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
|
|
136
|
+
|
|
137
|
+
await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
|
|
138
|
+
|
|
139
|
+
const key = XChaCha20Cipher.keygen()
|
|
140
|
+
const sealer = new SealStream(XChaCha20Cipher, key, { chunkSize: 65536 })
|
|
141
|
+
const preamble = sealer.preamble // send first
|
|
142
|
+
|
|
143
|
+
const ct0 = sealer.push(chunk0)
|
|
144
|
+
const ct1 = sealer.push(chunk1)
|
|
145
|
+
const ctLast = sealer.finalize(lastChunk) // keys wiped
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Constructor:** `new SealStream(cipher, key, opts?)`
|
|
149
|
+
|
|
150
|
+
| Parameter | Type | Description |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `cipher` | `CipherSuite` | `XChaCha20Cipher`, `SerpentCipher`, or a `KyberSuite` instance. |
|
|
153
|
+
| `key` | `Uint8Array` | Master key. Must be `cipher.keySize` bytes (32 for both symmetric suites). |
|
|
154
|
+
| `opts.chunkSize` | `number` | Max plaintext bytes per chunk. Range: [1024, 16777215]. Default: 65536. |
|
|
155
|
+
| `opts.framed` | `boolean` | Prepend `u32be` length prefix to each chunk. Default: false. |
|
|
156
|
+
|
|
157
|
+
| Method | Returns | Description |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `push(chunk, { aad? })` | `Uint8Array` | Encrypt a data chunk. Must be ≤ chunkSize bytes. |
|
|
160
|
+
| `finalize(chunk, { aad? })` | `Uint8Array` | Encrypt the final chunk and wipe keys. Must be ≤ chunkSize bytes. |
|
|
161
|
+
| `toTransformStream()` | `TransformStream` | Web Streams API wrapper. Emits preamble first, then sealed chunks. Finalizes on stream close. |
|
|
162
|
+
| `preamble` | `Uint8Array` | The stream preamble (read-only). 20 bytes for symmetric suites. 20B header + KEM ciphertext for KEM suites. |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### OpenStream
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import { OpenStream } from 'leviathan-crypto/stream'
|
|
170
|
+
import { XChaCha20Cipher } from 'leviathan-crypto/chacha20'
|
|
171
|
+
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
|
|
172
|
+
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
|
|
173
|
+
|
|
174
|
+
// init already called — preamble, key, and ciphertext chunks received from sender
|
|
175
|
+
const opener = new OpenStream(XChaCha20Cipher, key, preamble)
|
|
176
|
+
|
|
177
|
+
const pt0 = opener.pull(ct0)
|
|
178
|
+
const pt1 = opener.pull(ct1)
|
|
179
|
+
const ptLast = opener.finalize(ctLast) // keys wiped
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Constructor:** `new OpenStream(cipher, key, preamble)`
|
|
183
|
+
|
|
184
|
+
Throws if the preamble format enum doesn't match the cipher, or if the preamble is too short.
|
|
185
|
+
|
|
186
|
+
| Parameter | Type | Description |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| `cipher` | `CipherSuite` | Must match the cipher that produced the preamble. |
|
|
189
|
+
| `key` | `Uint8Array` | Same master key used for sealing. |
|
|
190
|
+
| `preamble` | `Uint8Array` | The preamble from `SealStream.preamble`. Pass it directly. |
|
|
191
|
+
|
|
192
|
+
| Method | Returns | Description |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `pull(chunk, { aad? })` | `Uint8Array` | Decrypt a data chunk. Throws `AuthenticationError` on tamper. |
|
|
195
|
+
| `finalize(chunk, { aad? })` | `Uint8Array` | Decrypt the final chunk and wipe keys. |
|
|
196
|
+
| `seek(index)` | `void` | Set the counter to `index`. Enables random access decryption. Must be a non-negative integer. |
|
|
197
|
+
| `toTransformStream()` | `TransformStream` | Web Streams API wrapper. Buffers one chunk to detect the final chunk. |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### SealStreamPool
|
|
202
|
+
|
|
203
|
+
Parallel batch encryption and decryption using Web Workers. Each worker holds its own WASM instance and a copy of the derived keys.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { init, SealStreamPool } from 'leviathan-crypto'
|
|
207
|
+
import { XChaCha20Cipher } from 'leviathan-crypto/chacha20'
|
|
208
|
+
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
|
|
209
|
+
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
|
|
210
|
+
|
|
211
|
+
await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
|
|
212
|
+
|
|
213
|
+
const pool = await SealStreamPool.create(XChaCha20Cipher, key, {
|
|
214
|
+
wasm: chacha20Wasm,
|
|
215
|
+
workers: 4,
|
|
216
|
+
chunkSize: 65536,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const ciphertext = await pool.seal(plaintext)
|
|
220
|
+
const decrypted = await pool.open(ciphertext)
|
|
221
|
+
pool.destroy()
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**`SealStreamPool.create(cipher, key, opts)`** — async factory.
|
|
225
|
+
|
|
226
|
+
| Option | Type | Default | Description |
|
|
227
|
+
|---|---|---|---|
|
|
228
|
+
| `wasm` | `WasmSource` or `Record<string, WasmSource>` | required | WASM source(s). Single value for XChaCha20. Record for Serpent: `{ serpent, sha2 }`. |
|
|
229
|
+
| `workers` | `number` | `navigator.hardwareConcurrency` (4 if unset) | Worker count. |
|
|
230
|
+
| `chunkSize` | `number` | `65536` | Chunk size in bytes. |
|
|
231
|
+
| `framed` | `boolean` | `false` | Framed mode. |
|
|
232
|
+
| `jobTimeout` | `number` | `30000` | Per-job timeout in ms. |
|
|
233
|
+
|
|
234
|
+
**Failure model.** Any error is fatal. Authentication failure, worker crash, and timeout all terminate every worker, wipe all keys, and mark the pool permanently dead. Pending promises reject. There is no retry and no worker replacement. Create a new pool for the next operation.
|
|
235
|
+
|
|
236
|
+
| Method / Property | Description |
|
|
237
|
+
|---|---|
|
|
238
|
+
| `seal(plaintext)` | Encrypt. Returns `Promise<Uint8Array>`. Single-use. Throws on subsequent calls. |
|
|
239
|
+
| `open(ciphertext)` | Decrypt. Returns `Promise<Uint8Array>`. Rejects empty ciphertext. |
|
|
240
|
+
| `destroy()` | Wipes keys and terminates workers. Safe to call multiple times. |
|
|
241
|
+
| `header` | The 20-byte stream header. `SealStreamPool` exposes `.header` while `SealStream` exposes `.preamble`, which also supports KEM preambles. |
|
|
242
|
+
| `dead` | `true` after any fatal error or `destroy()`. |
|
|
243
|
+
| `size` | Number of workers. |
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
### KyberSuite
|
|
248
|
+
|
|
249
|
+
`KyberSuite` wraps an ML-KEM instance and an inner `CipherSuite` into a hybrid post-quantum construction. The result plugs into `Seal`, `SealStream`, `OpenStream`, and `SealStreamPool` identically to a symmetric suite.
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { init, SealStream, OpenStream } from 'leviathan-crypto'
|
|
253
|
+
import { KyberSuite, MlKem768 } from 'leviathan-crypto/kyber'
|
|
254
|
+
import { XChaCha20Cipher } from 'leviathan-crypto/chacha20'
|
|
255
|
+
import { kyberWasm } from 'leviathan-crypto/kyber/embedded'
|
|
256
|
+
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
|
|
257
|
+
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
|
|
258
|
+
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
|
|
259
|
+
|
|
260
|
+
await init({ kyber: kyberWasm, sha3: sha3Wasm, chacha20: chacha20Wasm, sha2: sha2Wasm })
|
|
261
|
+
|
|
262
|
+
const suite = KyberSuite(new MlKem768(), XChaCha20Cipher)
|
|
263
|
+
const { encapsulationKey: ek, decapsulationKey: dk } = suite.keygen()
|
|
264
|
+
|
|
265
|
+
// sender — encrypts with the public key
|
|
266
|
+
const sealer = new SealStream(suite, ek)
|
|
267
|
+
const preamble = sealer.preamble // 1108 bytes for MlKem768
|
|
268
|
+
const ct0 = sealer.push(chunk0)
|
|
269
|
+
const ctLast = sealer.finalize(lastChunk)
|
|
270
|
+
|
|
271
|
+
// recipient — decrypts with the private key
|
|
272
|
+
const opener = new OpenStream(suite, dk, preamble)
|
|
273
|
+
const pt0 = opener.pull(ct0)
|
|
274
|
+
const ptLast = opener.finalize(ctLast)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
See [kyber.md](./kyber.md) for key management, parameter set selection, and the full ML-KEM reference. See [ciphersuite.md](./ciphersuite.md#kybersuite) for format enum values and key derivation details.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
### Per-chunk AAD
|
|
282
|
+
|
|
283
|
+
`push()` and `finalize()` on `SealStream` and `pull()` and `finalize()` on `OpenStream` all accept an optional `{ aad }` parameter for Additional Authenticated Data. AAD is authenticated but not encrypted. It binds each chunk to external context such as sequence numbers, metadata, or routing information without including that data in the ciphertext.
|
|
284
|
+
|
|
285
|
+
AAD applies per chunk, not per stream. Each chunk can carry different AAD. If you sealed a chunk with AAD you must provide the same value when opening it. A mismatch causes authentication to fail.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### AuthenticationError
|
|
290
|
+
|
|
291
|
+
`AuthenticationError` is thrown by `Seal.decrypt()`, `OpenStream.pull()`, `OpenStream.finalize()`, and `SealStreamPool.open()` when authentication fails. It extends `Error` and carries the cipher name in the message.
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { AuthenticationError } from 'leviathan-crypto'
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const pt = Seal.decrypt(XChaCha20Cipher, key, tampered)
|
|
298
|
+
} catch (e) {
|
|
299
|
+
if (e instanceof AuthenticationError) {
|
|
300
|
+
// ciphertext was modified
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Never attempt to recover plaintext after an `AuthenticationError`. The stream layer wipes output buffers before throwing.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
> ## Cross-References
|
|
310
|
+
>
|
|
311
|
+
> - [index](./README.md) — Project Documentation index
|
|
312
|
+
> - [lexicon](./lexicon.md) — Glossary of cryptographic terms
|
|
313
|
+
> - [architecture](./architecture.md) — architecture overview, module relationships, buffer layouts, and build pipeline
|
|
314
|
+
> - [ciphersuite](./ciphersuite.md) — `SerpentCipher`, `XChaCha20Cipher`, `KyberSuite`, and the `CipherSuite` interface
|
|
315
|
+
> - [kyber](./kyber.md) — ML-KEM key encapsulation, parameter sets, and key management
|
|
316
|
+
> - [serpent](./serpent.md) — Serpent-256 raw primitives
|
|
317
|
+
> - [chacha20](./chacha20.md) — ChaCha20 raw primitives
|
|
318
|
+
> - [stream_audit](./stream_audit.md) — streaming AEAD composition audit
|
|
319
|
+
> - [exports](./exports.md) — complete export reference
|
|
320
|
+
> - [init](./init.md) — WASM loading and `WasmSource`
|