leviathan-crypto 2.0.1 → 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 -238
- package/dist/chacha20/cipher-suite.d.ts +10 -0
- package/dist/chacha20/cipher-suite.js +65 -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 +66 -26
- package/dist/docs/architecture.md +600 -521
- 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 +155 -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/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/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 +135 -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 -101
- package/dist/serpent/serpent-cbc.d.ts +14 -4
- package/dist/serpent/serpent-cbc.js +50 -32
- 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/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 +38 -8
- package/dist/stream/seal-stream.js +29 -11
- 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,106 +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
|
-
const ret = s.cbcEncryptChunk(padded.length);
|
|
89
|
-
if (ret < 0)
|
|
90
|
-
throw new RangeError(`cbcEncryptChunk rejected len=${padded.length}` +
|
|
91
|
-
` (WASM CHUNK_SIZE=${s.getChunkSize()})`);
|
|
92
|
-
return new Uint8Array(s.memory.buffer).slice(s.getChunkCtOffset(), s.getChunkCtOffset() + padded.length);
|
|
93
|
-
}
|
|
94
|
-
function cbcDecrypt(encKey, iv, ct) {
|
|
95
|
-
const s = serpent;
|
|
96
|
-
const mem = new Uint8Array(s.memory.buffer);
|
|
97
|
-
mem.set(encKey, s.getKeyOffset());
|
|
98
|
-
s.loadKey(encKey.length);
|
|
99
|
-
mem.set(iv, s.getCbcIvOffset());
|
|
100
|
-
mem.set(ct, s.getChunkCtOffset());
|
|
101
|
-
const ret = s.cbcDecryptChunk(ct.length);
|
|
102
|
-
if (ret < 0)
|
|
103
|
-
throw new RangeError(`cbcDecryptChunk rejected len=${ct.length}` +
|
|
104
|
-
` (WASM CHUNK_SIZE=${s.getChunkSize()})`);
|
|
105
|
-
const raw = new Uint8Array(s.memory.buffer).slice(s.getChunkPtOffset(), s.getChunkPtOffset() + ct.length);
|
|
106
|
-
return pkcs7Strip(raw);
|
|
107
|
-
}
|
|
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
|
+
*/
|
|
108
31
|
self.onmessage = async (e) => {
|
|
109
32
|
const msg = e.data;
|
|
110
33
|
if (msg.type === 'init') {
|
|
@@ -136,6 +59,7 @@ self.onmessage = async (e) => {
|
|
|
136
59
|
serpent.wipeBuffers();
|
|
137
60
|
sha2 = undefined;
|
|
138
61
|
serpent = undefined;
|
|
62
|
+
self.postMessage({ type: 'wiped' });
|
|
139
63
|
return;
|
|
140
64
|
}
|
|
141
65
|
if (!keys || !sha2 || !serpent) {
|
|
@@ -151,14 +75,14 @@ self.onmessage = async (e) => {
|
|
|
151
75
|
const ivKey = jobKey.subarray(64, 96);
|
|
152
76
|
let result;
|
|
153
77
|
if (op === 'seal') {
|
|
154
|
-
const ivFull = hmacSha256(ivKey, counterNonce);
|
|
78
|
+
const ivFull = hmacSha256(sha2, ivKey, counterNonce);
|
|
155
79
|
const iv = ivFull.slice(0, 16);
|
|
156
80
|
wipe(ivFull);
|
|
157
|
-
const ct =
|
|
81
|
+
const ct = cbcEncryptChunk(serpent, encKey, iv, data);
|
|
158
82
|
const aadLenBuf = new Uint8Array(4);
|
|
159
83
|
new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
|
|
160
84
|
const tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
|
|
161
|
-
const tag = hmacSha256(macKey, tagInput);
|
|
85
|
+
const tag = hmacSha256(sha2, macKey, tagInput);
|
|
162
86
|
result = concat(ct, tag);
|
|
163
87
|
wipe(iv);
|
|
164
88
|
wipe(tagInput);
|
|
@@ -166,13 +90,13 @@ self.onmessage = async (e) => {
|
|
|
166
90
|
else {
|
|
167
91
|
const ct = data.subarray(0, data.length - 32);
|
|
168
92
|
const receivedTag = data.subarray(data.length - 32);
|
|
169
|
-
const ivFull = hmacSha256(ivKey, counterNonce);
|
|
93
|
+
const ivFull = hmacSha256(sha2, ivKey, counterNonce);
|
|
170
94
|
const iv = ivFull.slice(0, 16);
|
|
171
95
|
wipe(ivFull);
|
|
172
96
|
const aadLenBuf = new Uint8Array(4);
|
|
173
97
|
new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
|
|
174
98
|
const tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
|
|
175
|
-
const expectedTag = hmacSha256(macKey, tagInput);
|
|
99
|
+
const expectedTag = hmacSha256(sha2, macKey, tagInput);
|
|
176
100
|
// CRITICAL: verify HMAC before decrypting (Vaudenay 2002)
|
|
177
101
|
if (!constantTimeEqual(expectedTag, receivedTag)) {
|
|
178
102
|
wipe(iv);
|
|
@@ -182,7 +106,7 @@ self.onmessage = async (e) => {
|
|
|
182
106
|
}
|
|
183
107
|
wipe(tagInput);
|
|
184
108
|
wipe(expectedTag);
|
|
185
|
-
result =
|
|
109
|
+
result = cbcDecryptChunk(serpent, encKey, iv, ct);
|
|
186
110
|
wipe(iv);
|
|
187
111
|
}
|
|
188
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
|
}
|