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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// ▄▄▄▄▄▄▄▄▄▄
|
|
2
|
+
// ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
|
|
3
|
+
// ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
|
|
4
|
+
// ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
|
|
5
|
+
// ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
|
|
6
|
+
// ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
|
|
7
|
+
// ███████▌ ▀██▀ ███
|
|
8
|
+
// ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
|
|
9
|
+
// ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
|
|
10
|
+
// ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
|
|
11
|
+
// ▀████▄ ▄██▄
|
|
12
|
+
// ▐████ ▐███ Author: xero (https://x-e.ro)
|
|
13
|
+
// ▄▄██████████ ▐███ ▄▄ License: MIT
|
|
14
|
+
// ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
|
|
15
|
+
// ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
|
|
16
|
+
// ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
|
|
17
|
+
// ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
|
|
18
|
+
// █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
|
|
19
|
+
// ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
|
|
20
|
+
// ▀█████▀▀
|
|
21
|
+
//
|
|
22
|
+
// src/ts/serpent/generator.ts
|
|
23
|
+
//
|
|
24
|
+
// Practical Cryptography (Ferguson & Schneier, 2003) §9.4 — generator
|
|
25
|
+
// Serpent-256 ECB counter-mode PRF for Fortuna's generator slot.
|
|
26
|
+
import { _assertNotOwned, getInstance } from '../init.js';
|
|
27
|
+
import { wipe } from '../utils.js';
|
|
28
|
+
/**
|
|
29
|
+
* Serpent-256 ECB counter-mode PRF for Fortuna's generator slot.
|
|
30
|
+
*
|
|
31
|
+
* Each 16-byte counter value is encrypted as a plaintext block to produce
|
|
32
|
+
* one block of pseudorandom output. Practical Cryptography (Ferguson &
|
|
33
|
+
* Schneier, 2003) §9.4.
|
|
34
|
+
*
|
|
35
|
+
* Pass to `Fortuna.create({ generator: SerpentGenerator, ... })` — do not
|
|
36
|
+
* call `generate()` directly outside of Fortuna.
|
|
37
|
+
*/
|
|
38
|
+
export const SerpentGenerator = {
|
|
39
|
+
keySize: 32,
|
|
40
|
+
blockSize: 16,
|
|
41
|
+
counterSize: 16,
|
|
42
|
+
wasmModules: ['serpent'],
|
|
43
|
+
/**
|
|
44
|
+
* Generate `n` pseudorandom bytes by encrypting successive 16-byte counter
|
|
45
|
+
* values in ECB mode. The counter is incremented as a 128-bit little-endian
|
|
46
|
+
* integer after each block.
|
|
47
|
+
* @param key 32-byte Serpent-256 key
|
|
48
|
+
* @param counter 16-byte initial counter value (little-endian)
|
|
49
|
+
* @param n Number of bytes to generate (0 ≤ n ≤ 2^30)
|
|
50
|
+
* @returns `n` pseudorandom bytes
|
|
51
|
+
*/
|
|
52
|
+
generate(key, counter, n) {
|
|
53
|
+
_assertNotOwned('serpent');
|
|
54
|
+
if (key.length !== 32)
|
|
55
|
+
throw new RangeError(`SerpentGenerator: key must be 32 bytes (got ${key.length})`);
|
|
56
|
+
if (counter.length !== 16)
|
|
57
|
+
throw new RangeError(`SerpentGenerator: counter must be 16 bytes (got ${counter.length})`);
|
|
58
|
+
if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 30)
|
|
59
|
+
throw new RangeError(`SerpentGenerator: n must be a non-negative safe integer <= 2^30 (got ${n})`);
|
|
60
|
+
const x = getInstance('serpent').exports;
|
|
61
|
+
const mem = new Uint8Array(x.memory.buffer);
|
|
62
|
+
const c = counter.slice();
|
|
63
|
+
try {
|
|
64
|
+
mem.set(key, x.getKeyOffset());
|
|
65
|
+
if (x.loadKey(32) !== 0)
|
|
66
|
+
throw new Error('SerpentGenerator: loadKey failed');
|
|
67
|
+
const blocks = Math.ceil(n / 16);
|
|
68
|
+
const output = new Uint8Array(n);
|
|
69
|
+
const ptOff = x.getBlockPtOffset();
|
|
70
|
+
const ctOff = x.getBlockCtOffset();
|
|
71
|
+
for (let i = 0; i < blocks; i++) {
|
|
72
|
+
mem.set(c, ptOff);
|
|
73
|
+
x.encryptBlock();
|
|
74
|
+
// Last-block trim: copy only what the caller asked for. The
|
|
75
|
+
// unused tail stays in WASM memory (wiped in finally) instead
|
|
76
|
+
// of landing on the JS heap where callers could reach it via
|
|
77
|
+
// `result.buffer`. Mirrors ChaCha20Generator's exact-size output.
|
|
78
|
+
const offset = i * 16;
|
|
79
|
+
const writeLen = Math.min(16, n - offset);
|
|
80
|
+
output.set(mem.subarray(ctOff, ctOff + writeLen), offset);
|
|
81
|
+
// Increment c as a 16-byte little-endian integer
|
|
82
|
+
for (let j = 0; j < 16; j++) {
|
|
83
|
+
if (++c[j] !== 0)
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return output;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
// Wipe WASM key/key-schedule/last-block scratch and the JS-heap
|
|
91
|
+
// counter copy so secret-derived state does not outlive this call
|
|
92
|
+
// in either the WASM linear memory or the JS heap.
|
|
93
|
+
x.wipeBuffers();
|
|
94
|
+
wipe(c);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
package/dist/serpent/index.d.ts
CHANGED
|
@@ -1,12 +1,44 @@
|
|
|
1
1
|
import type { WasmSource } from '../wasm-source.js';
|
|
2
|
+
/**
|
|
3
|
+
* Load and initialise the Serpent WASM module from `source`.
|
|
4
|
+
* Must be called before constructing any Serpent class.
|
|
5
|
+
* @param source WASM binary — gzip+base64 string, URL, ArrayBuffer, Uint8Array,
|
|
6
|
+
* pre-compiled WebAssembly.Module, Response, or Promise<Response>
|
|
7
|
+
*/
|
|
2
8
|
export declare function serpentInit(source: WasmSource): Promise<void>;
|
|
3
9
|
export type { WasmSource };
|
|
10
|
+
/**
|
|
11
|
+
* Low-level Serpent-256 block cipher — raw ECB encrypt/decrypt.
|
|
12
|
+
*
|
|
13
|
+
* Atomic (stateless) class: each method call is independent.
|
|
14
|
+
* Does not hold exclusive module access; cannot be used while a stateful
|
|
15
|
+
* instance (`SerpentCtr`, `SerpentCbc`, `SerpentCipher`) is alive.
|
|
16
|
+
* Call `dispose()` after use to wipe WASM key material.
|
|
17
|
+
*/
|
|
4
18
|
export declare class Serpent {
|
|
5
19
|
private readonly x;
|
|
6
20
|
constructor();
|
|
21
|
+
/**
|
|
22
|
+
* Expand `key` into the WASM key schedule. Must be called before
|
|
23
|
+
* `encryptBlock` / `decryptBlock`.
|
|
24
|
+
* @param key 16, 24, or 32 bytes
|
|
25
|
+
*/
|
|
7
26
|
loadKey(key: Uint8Array): void;
|
|
27
|
+
/**
|
|
28
|
+
* Encrypt one 128-bit block with the previously loaded key schedule.
|
|
29
|
+
* Serpent AES submission §2.2.
|
|
30
|
+
* @param plaintext 16-byte plaintext block
|
|
31
|
+
* @returns 16-byte ciphertext block
|
|
32
|
+
*/
|
|
8
33
|
encryptBlock(plaintext: Uint8Array): Uint8Array;
|
|
34
|
+
/**
|
|
35
|
+
* Decrypt one 128-bit block with the previously loaded key schedule.
|
|
36
|
+
* Serpent AES submission §2.2.
|
|
37
|
+
* @param ciphertext 16-byte ciphertext block
|
|
38
|
+
* @returns 16-byte plaintext block
|
|
39
|
+
*/
|
|
9
40
|
decryptBlock(ciphertext: Uint8Array): Uint8Array;
|
|
41
|
+
/** Wipe WASM key material and release memory. */
|
|
10
42
|
dispose(): void;
|
|
11
43
|
}
|
|
12
44
|
/**
|
|
@@ -15,19 +47,47 @@ export declare class Serpent {
|
|
|
15
47
|
* **WARNING: CTR mode is unauthenticated.** An attacker can flip ciphertext
|
|
16
48
|
* bits without detection. Always pair with HMAC-SHA256 (Encrypt-then-MAC)
|
|
17
49
|
* or use `XChaCha20Poly1305` instead.
|
|
50
|
+
*
|
|
51
|
+
* Holds exclusive access to the `serpent` WASM module from construction
|
|
52
|
+
* until `dispose()`. Constructing a second SerpentCtr/SerpentCbc/
|
|
53
|
+
* SerpentCipher or any other serpent user while this instance is live
|
|
54
|
+
* throws. Call `dispose()` when done.
|
|
18
55
|
*/
|
|
19
56
|
export declare class SerpentCtr {
|
|
20
57
|
private readonly x;
|
|
58
|
+
private _tok;
|
|
21
59
|
constructor(opts?: {
|
|
22
60
|
dangerUnauthenticated: true;
|
|
23
61
|
});
|
|
62
|
+
/**
|
|
63
|
+
* Load key and nonce into WASM state and reset the block counter to 0.
|
|
64
|
+
* Must be called before each message.
|
|
65
|
+
* @param key 16, 24, or 32 bytes
|
|
66
|
+
* @param nonce 16 bytes — must be unique per (key, message)
|
|
67
|
+
*/
|
|
24
68
|
beginEncrypt(key: Uint8Array, nonce: Uint8Array): void;
|
|
69
|
+
/**
|
|
70
|
+
* XOR `chunk` with the next keystream block(s). Counter advances automatically.
|
|
71
|
+
* @param chunk Plaintext chunk — must not exceed WASM CHUNK_SIZE
|
|
72
|
+
* @returns Ciphertext of the same length
|
|
73
|
+
*/
|
|
25
74
|
encryptChunk(chunk: Uint8Array): Uint8Array;
|
|
75
|
+
/**
|
|
76
|
+
* Alias for `beginEncrypt` — CTR mode is symmetric.
|
|
77
|
+
* @param key 16, 24, or 32 bytes
|
|
78
|
+
* @param nonce 16 bytes — must match the value used to encrypt
|
|
79
|
+
*/
|
|
26
80
|
beginDecrypt(key: Uint8Array, nonce: Uint8Array): void;
|
|
81
|
+
/**
|
|
82
|
+
* Alias for `encryptChunk` — CTR mode is symmetric.
|
|
83
|
+
* @param chunk Ciphertext chunk
|
|
84
|
+
* @returns Plaintext of the same length
|
|
85
|
+
*/
|
|
27
86
|
decryptChunk(chunk: Uint8Array): Uint8Array;
|
|
87
|
+
/** Wipe WASM state and release exclusive module access. Idempotent. */
|
|
28
88
|
dispose(): void;
|
|
29
89
|
}
|
|
30
90
|
export { SerpentCbc } from './serpent-cbc.js';
|
|
31
91
|
export { AuthenticationError } from '../errors.js';
|
|
32
92
|
export { SerpentCipher } from './cipher-suite.js';
|
|
33
|
-
export
|
|
93
|
+
export { SerpentGenerator } from './generator.js';
|
package/dist/serpent/index.js
CHANGED
|
@@ -23,20 +23,41 @@
|
|
|
23
23
|
//
|
|
24
24
|
// Public API classes for the Serpent-256 WASM module.
|
|
25
25
|
// Uses the init() module cache — call serpentInit(source) before constructing.
|
|
26
|
-
import { getInstance, initModule } from '../init.js';
|
|
26
|
+
import { getInstance, initModule, _acquireModule, _releaseModule, _assertNotOwned } from '../init.js';
|
|
27
|
+
/**
|
|
28
|
+
* Load and initialise the Serpent WASM module from `source`.
|
|
29
|
+
* Must be called before constructing any Serpent class.
|
|
30
|
+
* @param source WASM binary — gzip+base64 string, URL, ArrayBuffer, Uint8Array,
|
|
31
|
+
* pre-compiled WebAssembly.Module, Response, or Promise<Response>
|
|
32
|
+
*/
|
|
27
33
|
export async function serpentInit(source) {
|
|
28
34
|
return initModule('serpent', source);
|
|
29
35
|
}
|
|
36
|
+
/** Returns the raw serpent WASM export object. @internal */
|
|
30
37
|
function getExports() {
|
|
31
38
|
return getInstance('serpent').exports;
|
|
32
39
|
}
|
|
33
|
-
// ── Serpent
|
|
40
|
+
// ── Serpent ─────────────────────────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Low-level Serpent-256 block cipher — raw ECB encrypt/decrypt.
|
|
43
|
+
*
|
|
44
|
+
* Atomic (stateless) class: each method call is independent.
|
|
45
|
+
* Does not hold exclusive module access; cannot be used while a stateful
|
|
46
|
+
* instance (`SerpentCtr`, `SerpentCbc`, `SerpentCipher`) is alive.
|
|
47
|
+
* Call `dispose()` after use to wipe WASM key material.
|
|
48
|
+
*/
|
|
34
49
|
export class Serpent {
|
|
35
50
|
x;
|
|
36
51
|
constructor() {
|
|
37
52
|
this.x = getExports();
|
|
38
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Expand `key` into the WASM key schedule. Must be called before
|
|
56
|
+
* `encryptBlock` / `decryptBlock`.
|
|
57
|
+
* @param key 16, 24, or 32 bytes
|
|
58
|
+
*/
|
|
39
59
|
loadKey(key) {
|
|
60
|
+
_assertNotOwned('serpent');
|
|
40
61
|
if (key.length !== 16 && key.length !== 24 && key.length !== 32)
|
|
41
62
|
throw new RangeError(`key must be 16, 24, or 32 bytes (got ${key.length})`);
|
|
42
63
|
const mem = new Uint8Array(this.x.memory.buffer);
|
|
@@ -44,7 +65,14 @@ export class Serpent {
|
|
|
44
65
|
if (this.x.loadKey(key.length) !== 0)
|
|
45
66
|
throw new Error('loadKey failed');
|
|
46
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Encrypt one 128-bit block with the previously loaded key schedule.
|
|
70
|
+
* Serpent AES submission §2.2.
|
|
71
|
+
* @param plaintext 16-byte plaintext block
|
|
72
|
+
* @returns 16-byte ciphertext block
|
|
73
|
+
*/
|
|
47
74
|
encryptBlock(plaintext) {
|
|
75
|
+
_assertNotOwned('serpent');
|
|
48
76
|
if (plaintext.length !== 16)
|
|
49
77
|
throw new RangeError(`block must be 16 bytes (got ${plaintext.length})`);
|
|
50
78
|
const mem = new Uint8Array(this.x.memory.buffer);
|
|
@@ -54,7 +82,14 @@ export class Serpent {
|
|
|
54
82
|
this.x.encryptBlock();
|
|
55
83
|
return mem.slice(ctOff, ctOff + 16);
|
|
56
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Decrypt one 128-bit block with the previously loaded key schedule.
|
|
87
|
+
* Serpent AES submission §2.2.
|
|
88
|
+
* @param ciphertext 16-byte ciphertext block
|
|
89
|
+
* @returns 16-byte plaintext block
|
|
90
|
+
*/
|
|
57
91
|
decryptBlock(ciphertext) {
|
|
92
|
+
_assertNotOwned('serpent');
|
|
58
93
|
if (ciphertext.length !== 16)
|
|
59
94
|
throw new RangeError(`block must be 16 bytes (got ${ciphertext.length})`);
|
|
60
95
|
const mem = new Uint8Array(this.x.memory.buffer);
|
|
@@ -64,28 +99,45 @@ export class Serpent {
|
|
|
64
99
|
this.x.decryptBlock();
|
|
65
100
|
return mem.slice(ptOff, ptOff + 16);
|
|
66
101
|
}
|
|
102
|
+
/** Wipe WASM key material and release memory. */
|
|
67
103
|
dispose() {
|
|
104
|
+
_assertNotOwned('serpent');
|
|
68
105
|
this.x.wipeBuffers();
|
|
69
106
|
}
|
|
70
107
|
}
|
|
71
|
-
// ── SerpentCtr
|
|
108
|
+
// ── SerpentCtr ──────────────────────────────────────────────────────────────
|
|
72
109
|
/**
|
|
73
110
|
* Serpent-256 in CTR mode.
|
|
74
111
|
*
|
|
75
112
|
* **WARNING: CTR mode is unauthenticated.** An attacker can flip ciphertext
|
|
76
113
|
* bits without detection. Always pair with HMAC-SHA256 (Encrypt-then-MAC)
|
|
77
114
|
* or use `XChaCha20Poly1305` instead.
|
|
115
|
+
*
|
|
116
|
+
* Holds exclusive access to the `serpent` WASM module from construction
|
|
117
|
+
* until `dispose()`. Constructing a second SerpentCtr/SerpentCbc/
|
|
118
|
+
* SerpentCipher or any other serpent user while this instance is live
|
|
119
|
+
* throws. Call `dispose()` when done.
|
|
78
120
|
*/
|
|
79
121
|
export class SerpentCtr {
|
|
80
122
|
x;
|
|
123
|
+
_tok;
|
|
81
124
|
constructor(opts) {
|
|
82
125
|
if (!opts?.dangerUnauthenticated) {
|
|
83
126
|
throw new Error('leviathan-crypto: SerpentCtr is unauthenticated — use Seal with SerpentCipher instead. ' +
|
|
84
127
|
'To use SerpentCtr directly, pass { dangerUnauthenticated: true }.');
|
|
85
128
|
}
|
|
86
129
|
this.x = getExports();
|
|
130
|
+
this._tok = _acquireModule('serpent');
|
|
87
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Load key and nonce into WASM state and reset the block counter to 0.
|
|
134
|
+
* Must be called before each message.
|
|
135
|
+
* @param key 16, 24, or 32 bytes
|
|
136
|
+
* @param nonce 16 bytes — must be unique per (key, message)
|
|
137
|
+
*/
|
|
88
138
|
beginEncrypt(key, nonce) {
|
|
139
|
+
if (this._tok === undefined)
|
|
140
|
+
throw new Error('SerpentCtr: instance has been disposed');
|
|
89
141
|
if (key.length !== 16 && key.length !== 24 && key.length !== 32)
|
|
90
142
|
throw new RangeError('key must be 16, 24, or 32 bytes');
|
|
91
143
|
if (nonce.length !== 16)
|
|
@@ -96,7 +148,14 @@ export class SerpentCtr {
|
|
|
96
148
|
this.x.loadKey(key.length);
|
|
97
149
|
this.x.resetCounter();
|
|
98
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* XOR `chunk` with the next keystream block(s). Counter advances automatically.
|
|
153
|
+
* @param chunk Plaintext chunk — must not exceed WASM CHUNK_SIZE
|
|
154
|
+
* @returns Ciphertext of the same length
|
|
155
|
+
*/
|
|
99
156
|
encryptChunk(chunk) {
|
|
157
|
+
if (this._tok === undefined)
|
|
158
|
+
throw new Error('SerpentCtr: instance has been disposed');
|
|
100
159
|
const maxChunk = this.x.getChunkSize();
|
|
101
160
|
if (chunk.length > maxChunk)
|
|
102
161
|
throw new RangeError(`chunk exceeds maximum size of ${maxChunk} bytes — split into smaller chunks`);
|
|
@@ -107,22 +166,48 @@ export class SerpentCtr {
|
|
|
107
166
|
this.x.encryptChunk_simd(chunk.length);
|
|
108
167
|
return mem.slice(ctOff, ctOff + chunk.length);
|
|
109
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Alias for `beginEncrypt` — CTR mode is symmetric.
|
|
171
|
+
* @param key 16, 24, or 32 bytes
|
|
172
|
+
* @param nonce 16 bytes — must match the value used to encrypt
|
|
173
|
+
*/
|
|
110
174
|
beginDecrypt(key, nonce) {
|
|
111
175
|
this.beginEncrypt(key, nonce);
|
|
112
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Alias for `encryptChunk` — CTR mode is symmetric.
|
|
179
|
+
* @param chunk Ciphertext chunk
|
|
180
|
+
* @returns Plaintext of the same length
|
|
181
|
+
*/
|
|
113
182
|
decryptChunk(chunk) {
|
|
114
183
|
return this.encryptChunk(chunk);
|
|
115
184
|
}
|
|
185
|
+
/** Wipe WASM state and release exclusive module access. Idempotent. */
|
|
116
186
|
dispose() {
|
|
117
|
-
this.
|
|
187
|
+
if (this._tok === undefined)
|
|
188
|
+
return;
|
|
189
|
+
try {
|
|
190
|
+
this.x.wipeBuffers();
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
_releaseModule('serpent', this._tok);
|
|
194
|
+
this._tok = undefined;
|
|
195
|
+
}
|
|
118
196
|
}
|
|
119
197
|
}
|
|
120
|
-
// ── SerpentCbc
|
|
198
|
+
// ── SerpentCbc ──────────────────────────────────────────────────────────────
|
|
121
199
|
export { SerpentCbc } from './serpent-cbc.js';
|
|
122
200
|
export { AuthenticationError } from '../errors.js';
|
|
123
|
-
// ── SerpentCipher re-export
|
|
201
|
+
// ── SerpentCipher re-export ─────────────────────────────────────────────────
|
|
124
202
|
export { SerpentCipher } from './cipher-suite.js';
|
|
125
|
-
// ──
|
|
203
|
+
// ── SerpentGenerator ────────────────────────────────────────────────────────
|
|
204
|
+
export { SerpentGenerator } from './generator.js';
|
|
205
|
+
// ── Ready check ─────────────────────────────────────────────────────────────
|
|
206
|
+
/**
|
|
207
|
+
* Returns `true` if the serpent WASM module has been initialised.
|
|
208
|
+
* Used by tests and internal guards; not part of the public API.
|
|
209
|
+
* @internal
|
|
210
|
+
*/
|
|
126
211
|
export function _serpentReady() {
|
|
127
212
|
try {
|
|
128
213
|
getInstance('serpent');
|
|
@@ -5,100 +5,29 @@
|
|
|
5
5
|
// Holds 3 derived keys (enc/mac/iv) and raw WASM instances.
|
|
6
6
|
// Direct WASM calls — no initModule (avoids same-thread module cache conflicts
|
|
7
7
|
// in @vitest/web-worker test environment).
|
|
8
|
+
//
|
|
9
|
+
// All HMAC / CBC / PKCS7 primitives come from `./shared-ops.js` — the same
|
|
10
|
+
// module the main-thread `SerpentCipher` uses. Byte-identical output with
|
|
11
|
+
// the main thread is the regression guard (see
|
|
12
|
+
// test/unit/stream/pool-byte-exact.test.ts). Must NOT import from `../init.js`:
|
|
13
|
+
// workers have their own isolated WASM instances, no shared-state registry.
|
|
8
14
|
import { constantTimeEqual, wipe, concat } from '../utils.js';
|
|
9
15
|
import { AuthenticationError } from '../errors.js';
|
|
16
|
+
import { hmacSha256, cbcEncryptChunk, cbcDecryptChunk, } from './shared-ops.js';
|
|
10
17
|
let sha2;
|
|
11
18
|
let serpent;
|
|
12
19
|
let keys;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
mem.set(k, x.getSha256InputOffset());
|
|
25
|
-
x.hmac256Init(k.length);
|
|
26
|
-
feedHmac(msg);
|
|
27
|
-
x.hmac256Final();
|
|
28
|
-
return new Uint8Array(x.memory.buffer).slice(x.getSha256OutOffset(), x.getSha256OutOffset() + 32);
|
|
29
|
-
}
|
|
30
|
-
function feedSha2(data) {
|
|
31
|
-
const x = sha2;
|
|
32
|
-
const mem = new Uint8Array(x.memory.buffer);
|
|
33
|
-
const off = x.getSha256InputOffset();
|
|
34
|
-
let pos = 0;
|
|
35
|
-
while (pos < data.length) {
|
|
36
|
-
const n = Math.min(data.length - pos, 64);
|
|
37
|
-
mem.set(data.subarray(pos, pos + n), off);
|
|
38
|
-
x.sha256Update(n);
|
|
39
|
-
pos += n;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
function feedHmac(data) {
|
|
43
|
-
const x = sha2;
|
|
44
|
-
const mem = new Uint8Array(x.memory.buffer);
|
|
45
|
-
const off = x.getSha256InputOffset();
|
|
46
|
-
let pos = 0;
|
|
47
|
-
while (pos < data.length) {
|
|
48
|
-
const n = Math.min(data.length - pos, 64);
|
|
49
|
-
mem.set(data.subarray(pos, pos + n), off);
|
|
50
|
-
x.hmac256Update(n);
|
|
51
|
-
pos += n;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
function pkcs7Pad(data) {
|
|
55
|
-
const padLen = 16 - (data.length % 16);
|
|
56
|
-
const out = new Uint8Array(data.length + padLen);
|
|
57
|
-
out.set(data);
|
|
58
|
-
out.fill(padLen, data.length);
|
|
59
|
-
return out;
|
|
60
|
-
}
|
|
61
|
-
// pkcs7Strip is only called after HMAC authentication succeeds (verify-then-decrypt).
|
|
62
|
-
// The early throw on invalid padLen is not a padding oracle in this context —
|
|
63
|
-
// the HMAC check is the oracle gate and runs in constant time before this point.
|
|
64
|
-
// If you move this call to a pre-auth site, revisit the timing properties.
|
|
65
|
-
function pkcs7Strip(data) {
|
|
66
|
-
if (data.length === 0)
|
|
67
|
-
throw new RangeError('empty ciphertext');
|
|
68
|
-
const padLen = data[data.length - 1];
|
|
69
|
-
if (padLen === 0 || padLen > 16)
|
|
70
|
-
throw new RangeError('invalid PKCS7 padding');
|
|
71
|
-
if (padLen > data.length)
|
|
72
|
-
throw new RangeError('invalid PKCS7 padding');
|
|
73
|
-
let bad = 0;
|
|
74
|
-
for (let i = data.length - padLen; i < data.length; i++)
|
|
75
|
-
bad |= data[i] ^ padLen;
|
|
76
|
-
if (bad !== 0)
|
|
77
|
-
throw new RangeError('invalid PKCS7 padding');
|
|
78
|
-
return data.subarray(0, data.length - padLen);
|
|
79
|
-
}
|
|
80
|
-
function cbcEncrypt(encKey, iv, plaintext) {
|
|
81
|
-
const s = serpent;
|
|
82
|
-
const mem = new Uint8Array(s.memory.buffer);
|
|
83
|
-
mem.set(encKey, s.getKeyOffset());
|
|
84
|
-
s.loadKey(encKey.length);
|
|
85
|
-
mem.set(iv, s.getCbcIvOffset());
|
|
86
|
-
const padded = pkcs7Pad(plaintext);
|
|
87
|
-
mem.set(padded, s.getChunkPtOffset());
|
|
88
|
-
s.cbcEncryptChunk(padded.length);
|
|
89
|
-
return new Uint8Array(s.memory.buffer).slice(s.getChunkCtOffset(), s.getChunkCtOffset() + padded.length);
|
|
90
|
-
}
|
|
91
|
-
function cbcDecrypt(encKey, iv, ct) {
|
|
92
|
-
const s = serpent;
|
|
93
|
-
const mem = new Uint8Array(s.memory.buffer);
|
|
94
|
-
mem.set(encKey, s.getKeyOffset());
|
|
95
|
-
s.loadKey(encKey.length);
|
|
96
|
-
mem.set(iv, s.getCbcIvOffset());
|
|
97
|
-
mem.set(ct, s.getChunkCtOffset());
|
|
98
|
-
s.cbcDecryptChunk(ct.length);
|
|
99
|
-
const raw = new Uint8Array(s.memory.buffer).slice(s.getChunkPtOffset(), s.getChunkPtOffset() + ct.length);
|
|
100
|
-
return pkcs7Strip(raw);
|
|
101
|
-
}
|
|
20
|
+
/**
|
|
21
|
+
* Message handler for the Serpent pool worker.
|
|
22
|
+
*
|
|
23
|
+
* Accepts three message types:
|
|
24
|
+
* - `'init'` — instantiate sha2 + serpent WASM modules and store derived keys
|
|
25
|
+
* - `'wipe'` — zero keys and WASM buffers, then post `{ type: 'wiped' }`
|
|
26
|
+
* - `{ op: 'seal' | 'open', ... }` — encrypt or decrypt one chunk
|
|
27
|
+
*
|
|
28
|
+
* Replies with `{ type: 'result', id, data }` on success or
|
|
29
|
+
* `{ type: 'error', id, message, isAuthError }` on failure.
|
|
30
|
+
*/
|
|
102
31
|
self.onmessage = async (e) => {
|
|
103
32
|
const msg = e.data;
|
|
104
33
|
if (msg.type === 'init') {
|
|
@@ -130,6 +59,7 @@ self.onmessage = async (e) => {
|
|
|
130
59
|
serpent.wipeBuffers();
|
|
131
60
|
sha2 = undefined;
|
|
132
61
|
serpent = undefined;
|
|
62
|
+
self.postMessage({ type: 'wiped' });
|
|
133
63
|
return;
|
|
134
64
|
}
|
|
135
65
|
if (!keys || !sha2 || !serpent) {
|
|
@@ -145,14 +75,14 @@ self.onmessage = async (e) => {
|
|
|
145
75
|
const ivKey = jobKey.subarray(64, 96);
|
|
146
76
|
let result;
|
|
147
77
|
if (op === 'seal') {
|
|
148
|
-
const ivFull = hmacSha256(ivKey, counterNonce);
|
|
78
|
+
const ivFull = hmacSha256(sha2, ivKey, counterNonce);
|
|
149
79
|
const iv = ivFull.slice(0, 16);
|
|
150
80
|
wipe(ivFull);
|
|
151
|
-
const ct =
|
|
81
|
+
const ct = cbcEncryptChunk(serpent, encKey, iv, data);
|
|
152
82
|
const aadLenBuf = new Uint8Array(4);
|
|
153
83
|
new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
|
|
154
84
|
const tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
|
|
155
|
-
const tag = hmacSha256(macKey, tagInput);
|
|
85
|
+
const tag = hmacSha256(sha2, macKey, tagInput);
|
|
156
86
|
result = concat(ct, tag);
|
|
157
87
|
wipe(iv);
|
|
158
88
|
wipe(tagInput);
|
|
@@ -160,13 +90,13 @@ self.onmessage = async (e) => {
|
|
|
160
90
|
else {
|
|
161
91
|
const ct = data.subarray(0, data.length - 32);
|
|
162
92
|
const receivedTag = data.subarray(data.length - 32);
|
|
163
|
-
const ivFull = hmacSha256(ivKey, counterNonce);
|
|
93
|
+
const ivFull = hmacSha256(sha2, ivKey, counterNonce);
|
|
164
94
|
const iv = ivFull.slice(0, 16);
|
|
165
95
|
wipe(ivFull);
|
|
166
96
|
const aadLenBuf = new Uint8Array(4);
|
|
167
97
|
new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
|
|
168
98
|
const tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
|
|
169
|
-
const expectedTag = hmacSha256(macKey, tagInput);
|
|
99
|
+
const expectedTag = hmacSha256(sha2, macKey, tagInput);
|
|
170
100
|
// CRITICAL: verify HMAC before decrypting (Vaudenay 2002)
|
|
171
101
|
if (!constantTimeEqual(expectedTag, receivedTag)) {
|
|
172
102
|
wipe(iv);
|
|
@@ -176,7 +106,7 @@ self.onmessage = async (e) => {
|
|
|
176
106
|
}
|
|
177
107
|
wipe(tagInput);
|
|
178
108
|
wipe(expectedTag);
|
|
179
|
-
result =
|
|
109
|
+
result = cbcDecryptChunk(serpent, encKey, iv, ct);
|
|
180
110
|
wipe(iv);
|
|
181
111
|
}
|
|
182
112
|
const transfer = result.buffer instanceof ArrayBuffer ? [result.buffer] : [];
|
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* **WARNING: CBC mode is unauthenticated.** Always authenticate the output
|
|
5
5
|
* with HMAC-SHA256 (Encrypt-then-MAC) or use `XChaCha20Poly1305` instead.
|
|
6
|
+
*
|
|
7
|
+
* Holds exclusive access to the `serpent` WASM module from construction
|
|
8
|
+
* until `dispose()`. Constructing a second SerpentCbc/SerpentCtr/
|
|
9
|
+
* SerpentCipher or any other serpent user while this instance is live
|
|
10
|
+
* throws. Call `dispose()` when done.
|
|
6
11
|
*/
|
|
7
12
|
export declare class SerpentCbc {
|
|
8
13
|
private readonly x;
|
|
14
|
+
private _tok;
|
|
9
15
|
constructor(opts?: {
|
|
10
16
|
dangerUnauthenticated: true;
|
|
11
17
|
});
|
|
12
|
-
private get mem();
|
|
13
18
|
/**
|
|
14
19
|
* Encrypt plaintext with Serpent-256 CBC + PKCS7 padding.
|
|
15
20
|
*
|
|
@@ -21,10 +26,15 @@ export declare class SerpentCbc {
|
|
|
21
26
|
encrypt(key: Uint8Array, iv: Uint8Array, plaintext: Uint8Array): Uint8Array;
|
|
22
27
|
/**
|
|
23
28
|
* Decrypt Serpent-256 CBC + PKCS7.
|
|
24
|
-
*
|
|
29
|
+
*
|
|
30
|
+
* All failure modes — empty input, non-multiple-of-16 length, and any
|
|
31
|
+
* PKCS7 validation failure — throw the same generic `RangeError` with
|
|
32
|
+
* message `'invalid ciphertext'`. Padding validation runs branch-free
|
|
33
|
+
* over the last 16 bytes regardless of where the mismatch is, closing
|
|
34
|
+
* the Vaudenay 2002 padding-oracle surface for callers using
|
|
35
|
+
* `{ dangerUnauthenticated: true }` without an outer HMAC.
|
|
25
36
|
*/
|
|
26
37
|
decrypt(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Uint8Array;
|
|
38
|
+
/** Wipe WASM state and release exclusive module access. Idempotent. */
|
|
27
39
|
dispose(): void;
|
|
28
|
-
private _loadKey;
|
|
29
|
-
private _setIv;
|
|
30
40
|
}
|