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
|
@@ -24,53 +24,42 @@
|
|
|
24
24
|
// SerpentCbc โ Serpent-256 CBC + PKCS7, internal module.
|
|
25
25
|
// Extracted to break the cipher-suite.ts โ index.ts circular dependency.
|
|
26
26
|
// Import from here directly; index.ts re-exports for the public API surface.
|
|
27
|
-
import { getInstance } from '../init.js';
|
|
27
|
+
import { getInstance, _acquireModule, _releaseModule } from '../init.js';
|
|
28
|
+
import { pkcs7Pad, pkcs7Strip, PKCS7_INVALID } from './shared-ops.js';
|
|
29
|
+
/** Returns the raw serpent WASM export object. @internal */
|
|
28
30
|
function getExports() {
|
|
29
31
|
return getInstance('serpent').exports;
|
|
30
32
|
}
|
|
31
|
-
// โโ PKCS7 helpers
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
// pkcs7Strip is only called after HMAC authentication succeeds (verify-then-decrypt).
|
|
40
|
-
// The early throw on invalid padLen is not a padding oracle in this context โ
|
|
41
|
-
// the HMAC check is the oracle gate and runs in constant time before this point.
|
|
42
|
-
// If you move this call to a pre-auth site, revisit the timing properties.
|
|
43
|
-
function pkcs7Strip(data) {
|
|
44
|
-
if (data.length === 0)
|
|
45
|
-
throw new RangeError('empty ciphertext');
|
|
46
|
-
const padLen = data[data.length - 1];
|
|
47
|
-
if (padLen === 0 || padLen > 16)
|
|
48
|
-
throw new RangeError(`invalid PKCS7 padding byte: ${padLen}`);
|
|
49
|
-
if (padLen > data.length)
|
|
50
|
-
throw new RangeError(`invalid PKCS7 padding: pad length ${padLen} exceeds data length ${data.length}`);
|
|
51
|
-
let bad = 0;
|
|
52
|
-
for (let i = data.length - padLen; i < data.length; i++)
|
|
53
|
-
bad |= data[i] ^ padLen;
|
|
54
|
-
if (bad !== 0)
|
|
55
|
-
throw new RangeError('invalid PKCS7 padding');
|
|
56
|
-
return data.subarray(0, data.length - padLen);
|
|
57
|
-
}
|
|
58
|
-
// โโ SerpentCbc โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
33
|
+
// โโ PKCS7 helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
34
|
+
//
|
|
35
|
+
// The canonical `pkcs7Pad` / `pkcs7Strip` live in `./shared-ops.ts`; this
|
|
36
|
+
// file re-uses them so the main-thread class and the pool worker share one
|
|
37
|
+
// implementation. See `shared-ops.ts` for the branch-free, Vaudenay-2002-
|
|
38
|
+
// closed padding check.
|
|
39
|
+
// โโ SerpentCbc โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
59
40
|
/**
|
|
60
41
|
* Serpent-256 in CBC mode with PKCS7 padding.
|
|
61
42
|
*
|
|
62
43
|
* **WARNING: CBC mode is unauthenticated.** Always authenticate the output
|
|
63
44
|
* with HMAC-SHA256 (Encrypt-then-MAC) or use `XChaCha20Poly1305` instead.
|
|
45
|
+
*
|
|
46
|
+
* Holds exclusive access to the `serpent` WASM module from construction
|
|
47
|
+
* until `dispose()`. Constructing a second SerpentCbc/SerpentCtr/
|
|
48
|
+
* SerpentCipher or any other serpent user while this instance is live
|
|
49
|
+
* throws. Call `dispose()` when done.
|
|
64
50
|
*/
|
|
65
51
|
export class SerpentCbc {
|
|
66
52
|
x;
|
|
53
|
+
_tok;
|
|
67
54
|
constructor(opts) {
|
|
68
55
|
if (!opts?.dangerUnauthenticated) {
|
|
69
56
|
throw new Error('leviathan-crypto: SerpentCbc is unauthenticated โ use Seal with SerpentCipher instead. ' +
|
|
70
57
|
'To use SerpentCbc directly, pass { dangerUnauthenticated: true }.');
|
|
71
58
|
}
|
|
72
59
|
this.x = getExports();
|
|
60
|
+
this._tok = _acquireModule('serpent');
|
|
73
61
|
}
|
|
62
|
+
/** View over WASM linear memory. Rebind on every access โ memory can be detached after grow. @internal */
|
|
74
63
|
get mem() {
|
|
75
64
|
return new Uint8Array(this.x.memory.buffer);
|
|
76
65
|
}
|
|
@@ -83,6 +72,8 @@ export class SerpentCbc {
|
|
|
83
72
|
* @returns ciphertext (length = ceil((plaintext.length + 1) / 16) * 16)
|
|
84
73
|
*/
|
|
85
74
|
encrypt(key, iv, plaintext) {
|
|
75
|
+
if (this._tok === undefined)
|
|
76
|
+
throw new Error('SerpentCbc: instance has been disposed');
|
|
86
77
|
this._loadKey(key);
|
|
87
78
|
this._setIv(iv);
|
|
88
79
|
const padded = pkcs7Pad(plaintext);
|
|
@@ -93,18 +84,29 @@ export class SerpentCbc {
|
|
|
93
84
|
for (let off = 0; off < padded.length; off += maxChunk) {
|
|
94
85
|
const chunk = padded.subarray(off, Math.min(off + maxChunk, padded.length));
|
|
95
86
|
this.mem.set(chunk, ptOff);
|
|
96
|
-
this.x.cbcEncryptChunk(chunk.length);
|
|
87
|
+
const ret = this.x.cbcEncryptChunk(chunk.length);
|
|
88
|
+
if (ret < 0)
|
|
89
|
+
throw new RangeError(`cbcEncryptChunk rejected len=${chunk.length}` +
|
|
90
|
+
` (WASM CHUNK_SIZE=${this.x.getChunkSize()})`);
|
|
97
91
|
output.set(new Uint8Array(this.x.memory.buffer).subarray(ctOff, ctOff + chunk.length), off);
|
|
98
92
|
}
|
|
99
93
|
return output;
|
|
100
94
|
}
|
|
101
95
|
/**
|
|
102
96
|
* Decrypt Serpent-256 CBC + PKCS7.
|
|
103
|
-
*
|
|
97
|
+
*
|
|
98
|
+
* All failure modes โ empty input, non-multiple-of-16 length, and any
|
|
99
|
+
* PKCS7 validation failure โ throw the same generic `RangeError` with
|
|
100
|
+
* message `'invalid ciphertext'`. Padding validation runs branch-free
|
|
101
|
+
* over the last 16 bytes regardless of where the mismatch is, closing
|
|
102
|
+
* the Vaudenay 2002 padding-oracle surface for callers using
|
|
103
|
+
* `{ dangerUnauthenticated: true }` without an outer HMAC.
|
|
104
104
|
*/
|
|
105
105
|
decrypt(key, iv, ciphertext) {
|
|
106
|
+
if (this._tok === undefined)
|
|
107
|
+
throw new Error('SerpentCbc: instance has been disposed');
|
|
106
108
|
if (ciphertext.length === 0 || ciphertext.length % 16 !== 0)
|
|
107
|
-
throw new RangeError(
|
|
109
|
+
throw new RangeError(PKCS7_INVALID);
|
|
108
110
|
this._loadKey(key);
|
|
109
111
|
this._setIv(iv);
|
|
110
112
|
const output = new Uint8Array(ciphertext.length);
|
|
@@ -114,20 +116,42 @@ export class SerpentCbc {
|
|
|
114
116
|
for (let off = 0; off < ciphertext.length; off += maxChunk) {
|
|
115
117
|
const chunk = ciphertext.subarray(off, Math.min(off + maxChunk, ciphertext.length));
|
|
116
118
|
this.mem.set(chunk, ctOff);
|
|
117
|
-
this.x.cbcDecryptChunk_simd(chunk.length);
|
|
119
|
+
const ret = this.x.cbcDecryptChunk_simd(chunk.length);
|
|
120
|
+
if (ret < 0)
|
|
121
|
+
throw new RangeError(`cbcDecryptChunk_simd rejected len=${chunk.length}` +
|
|
122
|
+
` (WASM CHUNK_SIZE=${this.x.getChunkSize()})`);
|
|
118
123
|
output.set(new Uint8Array(this.x.memory.buffer).subarray(ptOff, ptOff + chunk.length), off);
|
|
119
124
|
}
|
|
120
125
|
return pkcs7Strip(output);
|
|
121
126
|
}
|
|
127
|
+
/** Wipe WASM state and release exclusive module access. Idempotent. */
|
|
122
128
|
dispose() {
|
|
123
|
-
this.
|
|
129
|
+
if (this._tok === undefined)
|
|
130
|
+
return;
|
|
131
|
+
try {
|
|
132
|
+
this.x.wipeBuffers();
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
_releaseModule('serpent', this._tok);
|
|
136
|
+
this._tok = undefined;
|
|
137
|
+
}
|
|
124
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Validate and load `key` into the WASM key schedule.
|
|
141
|
+
* @param key 16, 24, or 32 bytes
|
|
142
|
+
* @internal
|
|
143
|
+
*/
|
|
125
144
|
_loadKey(key) {
|
|
126
145
|
if (key.length !== 16 && key.length !== 24 && key.length !== 32)
|
|
127
146
|
throw new RangeError(`Serpent key must be 16, 24, or 32 bytes (got ${key.length})`);
|
|
128
147
|
this.mem.set(key, this.x.getKeyOffset());
|
|
129
148
|
this.x.loadKey(key.length);
|
|
130
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Write `iv` into the WASM CBC IV buffer.
|
|
152
|
+
* @param iv 16 bytes
|
|
153
|
+
* @internal
|
|
154
|
+
*/
|
|
131
155
|
_setIv(iv) {
|
|
132
156
|
if (iv.length !== 16)
|
|
133
157
|
throw new RangeError(`CBC IV must be 16 bytes (got ${iv.length})`);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Subset of the sha2 WASM exports used by `hmacSha256`. */
|
|
2
|
+
export interface Sha2OpsExports {
|
|
3
|
+
memory: WebAssembly.Memory;
|
|
4
|
+
getSha256InputOffset: () => number;
|
|
5
|
+
getSha256OutOffset: () => number;
|
|
6
|
+
sha256Init: () => void;
|
|
7
|
+
sha256Update: (len: number) => void;
|
|
8
|
+
sha256Final: () => void;
|
|
9
|
+
hmac256Init: (keyLen: number) => void;
|
|
10
|
+
hmac256Update: (len: number) => void;
|
|
11
|
+
hmac256Final: () => void;
|
|
12
|
+
}
|
|
13
|
+
/** Subset of the serpent WASM exports used by `cbcEncryptChunk`/`cbcDecryptChunk`. */
|
|
14
|
+
export interface SerpentOpsExports {
|
|
15
|
+
memory: WebAssembly.Memory;
|
|
16
|
+
getKeyOffset: () => number;
|
|
17
|
+
getChunkPtOffset: () => number;
|
|
18
|
+
getChunkCtOffset: () => number;
|
|
19
|
+
getChunkSize: () => number;
|
|
20
|
+
getCbcIvOffset: () => number;
|
|
21
|
+
loadKey: (n: number) => number;
|
|
22
|
+
cbcEncryptChunk: (n: number) => number;
|
|
23
|
+
cbcDecryptChunk_simd: (n: number) => number;
|
|
24
|
+
}
|
|
25
|
+
export declare const PKCS7_INVALID = "invalid ciphertext";
|
|
26
|
+
/**
|
|
27
|
+
* Apply PKCS7 padding to `data` so the result length is a multiple of 16.
|
|
28
|
+
* Padding length is always 1โ16 bytes so a full pad block is appended when
|
|
29
|
+
* `data.length` is already block-aligned.
|
|
30
|
+
* @param data Input bytes of any length
|
|
31
|
+
* @returns New Uint8Array padded to the next 16-byte boundary
|
|
32
|
+
*/
|
|
33
|
+
export declare function pkcs7Pad(data: Uint8Array): Uint8Array;
|
|
34
|
+
/**
|
|
35
|
+
* Remove PKCS7 padding from a block-aligned buffer in constant time.
|
|
36
|
+
*
|
|
37
|
+
* Branch-free over all secret bits โ padding length and per-byte comparisons
|
|
38
|
+
* are accumulated into a single `bad` flag with no early exit. Closes the
|
|
39
|
+
* Vaudenay 2002 padding-oracle surface. Throws a single generic
|
|
40
|
+
* `RangeError('invalid ciphertext')` for every failure mode: empty input,
|
|
41
|
+
* non-block-aligned length, padding byte out of range 1โ16, and any per-byte
|
|
42
|
+
* mismatch in the padding region.
|
|
43
|
+
* @param data Block-aligned ciphertext (length must be a multiple of 16)
|
|
44
|
+
* @returns Plaintext with padding removed
|
|
45
|
+
*/
|
|
46
|
+
export declare function pkcs7Strip(data: Uint8Array): Uint8Array;
|
|
47
|
+
/**
|
|
48
|
+
* Compute HMAC-SHA-256 using raw WASM sha2 exports.
|
|
49
|
+
*
|
|
50
|
+
* Keys longer than 64 bytes are pre-hashed per RFC 2104 ยง3. The SHA-256
|
|
51
|
+
* input buffer is fed in 64-byte chunks to match the WASM block size.
|
|
52
|
+
* Does not call `_acquireModule` โ callers must ensure no stateful instance
|
|
53
|
+
* owns the sha2 module before calling.
|
|
54
|
+
* @param sx sha2 WASM exports
|
|
55
|
+
* @param key HMAC key of any length
|
|
56
|
+
* @param msg Message to authenticate
|
|
57
|
+
* @returns 32-byte HMAC-SHA-256 tag
|
|
58
|
+
*/
|
|
59
|
+
export declare function hmacSha256(sx: Sha2OpsExports, key: Uint8Array, msg: Uint8Array): Uint8Array;
|
|
60
|
+
/**
|
|
61
|
+
* Encrypt one plaintext chunk with Serpent-256 CBC + PKCS7 padding.
|
|
62
|
+
*
|
|
63
|
+
* The padded chunk must fit within the WASM CHUNK_SIZE. Callers are
|
|
64
|
+
* responsible for splitting larger payloads before calling.
|
|
65
|
+
* @param kx Serpent WASM exports
|
|
66
|
+
* @param key 16, 24, or 32-byte key
|
|
67
|
+
* @param iv 16-byte CBC initialisation vector
|
|
68
|
+
* @param chunk Plaintext chunk (padded length must be โค WASM CHUNK_SIZE)
|
|
69
|
+
* @returns Ciphertext with PKCS7 padding applied
|
|
70
|
+
*/
|
|
71
|
+
export declare function cbcEncryptChunk(kx: SerpentOpsExports, key: Uint8Array, iv: Uint8Array, chunk: Uint8Array): Uint8Array;
|
|
72
|
+
/**
|
|
73
|
+
* Decrypt one Serpent-256 CBC chunk using the SIMD path and strip PKCS7 padding.
|
|
74
|
+
*
|
|
75
|
+
* Output matches `SerpentCbc.decrypt` byte-for-byte. Throws
|
|
76
|
+
* `RangeError('invalid ciphertext')` on any length or padding failure.
|
|
77
|
+
* @param kx Serpent WASM exports
|
|
78
|
+
* @param key 16, 24, or 32-byte key
|
|
79
|
+
* @param iv 16-byte CBC initialisation vector
|
|
80
|
+
* @param ct Ciphertext (must be non-empty and a multiple of 16 bytes)
|
|
81
|
+
* @returns Plaintext with PKCS7 padding removed
|
|
82
|
+
*/
|
|
83
|
+
export declare function cbcDecryptChunk(kx: SerpentOpsExports, key: Uint8Array, iv: Uint8Array, ct: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,213 @@
|
|
|
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/shared-ops.ts
|
|
23
|
+
//
|
|
24
|
+
// Pure-function primitives shared between the main-thread `SerpentCipher`
|
|
25
|
+
// (cipher-suite.ts) and the `SealStreamPool` worker (pool-worker.ts). Both
|
|
26
|
+
// call sites hold their own WASM exports โ pool workers instantiate modules
|
|
27
|
+
// locally, the main thread fetches via `getInstance()` โ so every function
|
|
28
|
+
// here takes the sha2/serpent exports as parameters. No dependency on
|
|
29
|
+
// `init.ts`, no module-level state, no instance wrappers.
|
|
30
|
+
//
|
|
31
|
+
// These helpers are strictly single-chunk: the caller already divided the
|
|
32
|
+
// payload into chunks โค WASM CHUNK_SIZE. For multi-chunk use, see
|
|
33
|
+
// `SerpentCbc.encrypt`/`decrypt`, which loop over the same WASM exports.
|
|
34
|
+
//
|
|
35
|
+
// This file owns the canonical `pkcs7Pad` / `pkcs7Strip`; `serpent-cbc.ts`
|
|
36
|
+
// re-exports them. A single source of truth keeps the branch-free,
|
|
37
|
+
// Vaudenay-2002-closed padding check identical on the main-thread and
|
|
38
|
+
// pool-worker paths โ divergence between the two would reintroduce an
|
|
39
|
+
// oracle.
|
|
40
|
+
// โโ PKCS7 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
41
|
+
// Generic error string used by every failure mode of `pkcs7Strip` and the
|
|
42
|
+
// length/alignment gate in `SerpentCbc.decrypt`. No numeric leaks, no
|
|
43
|
+
// structural disclosure โ a caller cannot distinguish "bad length" from
|
|
44
|
+
// "bad padding" by message or by timing.
|
|
45
|
+
export const PKCS7_INVALID = 'invalid ciphertext';
|
|
46
|
+
/**
|
|
47
|
+
* Apply PKCS7 padding to `data` so the result length is a multiple of 16.
|
|
48
|
+
* Padding length is always 1โ16 bytes so a full pad block is appended when
|
|
49
|
+
* `data.length` is already block-aligned.
|
|
50
|
+
* @param data Input bytes of any length
|
|
51
|
+
* @returns New Uint8Array padded to the next 16-byte boundary
|
|
52
|
+
*/
|
|
53
|
+
export function pkcs7Pad(data) {
|
|
54
|
+
const padLen = 16 - (data.length % 16); // 1..16
|
|
55
|
+
const out = new Uint8Array(data.length + padLen);
|
|
56
|
+
out.set(data);
|
|
57
|
+
out.fill(padLen, data.length);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Remove PKCS7 padding from a block-aligned buffer in constant time.
|
|
62
|
+
*
|
|
63
|
+
* Branch-free over all secret bits โ padding length and per-byte comparisons
|
|
64
|
+
* are accumulated into a single `bad` flag with no early exit. Closes the
|
|
65
|
+
* Vaudenay 2002 padding-oracle surface. Throws a single generic
|
|
66
|
+
* `RangeError('invalid ciphertext')` for every failure mode: empty input,
|
|
67
|
+
* non-block-aligned length, padding byte out of range 1โ16, and any per-byte
|
|
68
|
+
* mismatch in the padding region.
|
|
69
|
+
* @param data Block-aligned ciphertext (length must be a multiple of 16)
|
|
70
|
+
* @returns Plaintext with padding removed
|
|
71
|
+
*/
|
|
72
|
+
export function pkcs7Strip(data) {
|
|
73
|
+
if (data.length === 0 || data.length % 16 !== 0)
|
|
74
|
+
throw new RangeError(PKCS7_INVALID);
|
|
75
|
+
const padLen = data[data.length - 1];
|
|
76
|
+
let bad = 0;
|
|
77
|
+
bad |= ((padLen - 1) >>> 31); // 1 if padLen == 0
|
|
78
|
+
bad |= ((16 - padLen) >>> 31); // 1 if padLen > 16
|
|
79
|
+
// Per-byte pad-region mask without branches on secret bits.
|
|
80
|
+
// inPadRegion = 0xff when i >= 16 - padLen
|
|
81
|
+
// = 0x00 otherwise
|
|
82
|
+
//
|
|
83
|
+
// (16 - padLen - i - 1) is negative iff i >= 16 - padLen. A signed
|
|
84
|
+
// arithmetic shift by 31 yields -1 for negative, 0 for non-negative;
|
|
85
|
+
// ANDing with 0xff collapses those to 0xff and 0x00.
|
|
86
|
+
for (let i = 0; i < 16; i++) {
|
|
87
|
+
const idx = data.length - 16 + i;
|
|
88
|
+
const mask = ((16 - padLen - i - 1) >> 31) & 0xff;
|
|
89
|
+
bad |= (data[idx] ^ padLen) & mask;
|
|
90
|
+
}
|
|
91
|
+
const invalid = ((bad - 1) >>> 31) ^ 1;
|
|
92
|
+
if (invalid)
|
|
93
|
+
throw new RangeError(PKCS7_INVALID);
|
|
94
|
+
return data.subarray(0, data.length - padLen);
|
|
95
|
+
}
|
|
96
|
+
// โโ HMAC-SHA-256 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
97
|
+
/**
|
|
98
|
+
* Compute HMAC-SHA-256 using raw WASM sha2 exports.
|
|
99
|
+
*
|
|
100
|
+
* Keys longer than 64 bytes are pre-hashed per RFC 2104 ยง3. The SHA-256
|
|
101
|
+
* input buffer is fed in 64-byte chunks to match the WASM block size.
|
|
102
|
+
* Does not call `_acquireModule` โ callers must ensure no stateful instance
|
|
103
|
+
* owns the sha2 module before calling.
|
|
104
|
+
* @param sx sha2 WASM exports
|
|
105
|
+
* @param key HMAC key of any length
|
|
106
|
+
* @param msg Message to authenticate
|
|
107
|
+
* @returns 32-byte HMAC-SHA-256 tag
|
|
108
|
+
*/
|
|
109
|
+
export function hmacSha256(sx, key, msg) {
|
|
110
|
+
const inOff = sx.getSha256InputOffset();
|
|
111
|
+
const outOff = sx.getSha256OutOffset();
|
|
112
|
+
let k = key;
|
|
113
|
+
if (k.length > 64) {
|
|
114
|
+
sx.sha256Init();
|
|
115
|
+
feedMemory(sx.memory, inOff, k, 64, sx.sha256Update);
|
|
116
|
+
sx.sha256Final();
|
|
117
|
+
k = new Uint8Array(sx.memory.buffer).slice(outOff, outOff + 32);
|
|
118
|
+
}
|
|
119
|
+
const mem = new Uint8Array(sx.memory.buffer);
|
|
120
|
+
mem.set(k, inOff);
|
|
121
|
+
sx.hmac256Init(k.length);
|
|
122
|
+
feedMemory(sx.memory, inOff, msg, 64, sx.hmac256Update);
|
|
123
|
+
sx.hmac256Final();
|
|
124
|
+
return new Uint8Array(sx.memory.buffer).slice(outOff, outOff + 32);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Copy `msg` into WASM linear memory at `inputOff` in `chunkSize`-byte
|
|
128
|
+
* increments, calling `update(n)` after each write.
|
|
129
|
+
* @param memory WASM memory object
|
|
130
|
+
* @param inputOff Byte offset of the WASM input buffer
|
|
131
|
+
* @param msg Data to feed
|
|
132
|
+
* @param chunkSize Maximum bytes per write (must match WASM buffer size)
|
|
133
|
+
* @param update WASM update function to call after each write
|
|
134
|
+
* @internal
|
|
135
|
+
*/
|
|
136
|
+
function feedMemory(memory, inputOff, msg, chunkSize, update) {
|
|
137
|
+
const mem = new Uint8Array(memory.buffer);
|
|
138
|
+
let pos = 0;
|
|
139
|
+
while (pos < msg.length) {
|
|
140
|
+
const n = Math.min(msg.length - pos, chunkSize);
|
|
141
|
+
mem.set(msg.subarray(pos, pos + n), inputOff);
|
|
142
|
+
update(n);
|
|
143
|
+
pos += n;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// โโ Serpent-CBC (single chunk) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
147
|
+
/**
|
|
148
|
+
* Encrypt one plaintext chunk with Serpent-256 CBC + PKCS7 padding.
|
|
149
|
+
*
|
|
150
|
+
* The padded chunk must fit within the WASM CHUNK_SIZE. Callers are
|
|
151
|
+
* responsible for splitting larger payloads before calling.
|
|
152
|
+
* @param kx Serpent WASM exports
|
|
153
|
+
* @param key 16, 24, or 32-byte key
|
|
154
|
+
* @param iv 16-byte CBC initialisation vector
|
|
155
|
+
* @param chunk Plaintext chunk (padded length must be โค WASM CHUNK_SIZE)
|
|
156
|
+
* @returns Ciphertext with PKCS7 padding applied
|
|
157
|
+
*/
|
|
158
|
+
export function cbcEncryptChunk(kx, key, iv, chunk) {
|
|
159
|
+
loadKeyAndIv(kx, key, iv);
|
|
160
|
+
const padded = pkcs7Pad(chunk);
|
|
161
|
+
const ptOff = kx.getChunkPtOffset();
|
|
162
|
+
const ctOff = kx.getChunkCtOffset();
|
|
163
|
+
const mem = new Uint8Array(kx.memory.buffer);
|
|
164
|
+
mem.set(padded, ptOff);
|
|
165
|
+
const ret = kx.cbcEncryptChunk(padded.length);
|
|
166
|
+
if (ret < 0)
|
|
167
|
+
throw new RangeError(`cbcEncryptChunk rejected len=${padded.length}` +
|
|
168
|
+
` (WASM CHUNK_SIZE=${kx.getChunkSize()})`);
|
|
169
|
+
return new Uint8Array(kx.memory.buffer).slice(ctOff, ctOff + padded.length);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Decrypt one Serpent-256 CBC chunk using the SIMD path and strip PKCS7 padding.
|
|
173
|
+
*
|
|
174
|
+
* Output matches `SerpentCbc.decrypt` byte-for-byte. Throws
|
|
175
|
+
* `RangeError('invalid ciphertext')` on any length or padding failure.
|
|
176
|
+
* @param kx Serpent WASM exports
|
|
177
|
+
* @param key 16, 24, or 32-byte key
|
|
178
|
+
* @param iv 16-byte CBC initialisation vector
|
|
179
|
+
* @param ct Ciphertext (must be non-empty and a multiple of 16 bytes)
|
|
180
|
+
* @returns Plaintext with PKCS7 padding removed
|
|
181
|
+
*/
|
|
182
|
+
export function cbcDecryptChunk(kx, key, iv, ct) {
|
|
183
|
+
if (ct.length === 0 || ct.length % 16 !== 0)
|
|
184
|
+
throw new RangeError(PKCS7_INVALID);
|
|
185
|
+
loadKeyAndIv(kx, key, iv);
|
|
186
|
+
const ctOff = kx.getChunkCtOffset();
|
|
187
|
+
const ptOff = kx.getChunkPtOffset();
|
|
188
|
+
const mem = new Uint8Array(kx.memory.buffer);
|
|
189
|
+
mem.set(ct, ctOff);
|
|
190
|
+
const ret = kx.cbcDecryptChunk_simd(ct.length);
|
|
191
|
+
if (ret < 0)
|
|
192
|
+
throw new RangeError(`cbcDecryptChunk_simd rejected len=${ct.length}` +
|
|
193
|
+
` (WASM CHUNK_SIZE=${kx.getChunkSize()})`);
|
|
194
|
+
const raw = new Uint8Array(kx.memory.buffer).slice(ptOff, ptOff + ct.length);
|
|
195
|
+
return pkcs7Strip(raw);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Validate, then write `key` and `iv` into the WASM buffers and expand the key schedule.
|
|
199
|
+
* @param kx Serpent WASM exports
|
|
200
|
+
* @param key 16, 24, or 32-byte Serpent key
|
|
201
|
+
* @param iv 16-byte CBC initialisation vector
|
|
202
|
+
* @internal
|
|
203
|
+
*/
|
|
204
|
+
function loadKeyAndIv(kx, key, iv) {
|
|
205
|
+
if (key.length !== 16 && key.length !== 24 && key.length !== 32)
|
|
206
|
+
throw new RangeError(`Serpent key must be 16, 24, or 32 bytes (got ${key.length})`);
|
|
207
|
+
if (iv.length !== 16)
|
|
208
|
+
throw new RangeError(`CBC IV must be 16 bytes (got ${iv.length})`);
|
|
209
|
+
const mem = new Uint8Array(kx.memory.buffer);
|
|
210
|
+
mem.set(key, kx.getKeyOffset());
|
|
211
|
+
kx.loadKey(key.length);
|
|
212
|
+
mem.set(iv, kx.getCbcIvOffset());
|
|
213
|
+
}
|
package/dist/serpent/types.d.ts
CHANGED
package/dist/serpent.wasm
CHANGED
|
Binary file
|
|
@@ -0,0 +1,53 @@
|
|
|
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/sha2/hash.ts
|
|
23
|
+
//
|
|
24
|
+
// Stateless SHA-256 HashFn for Fortuna's accumulator and reseed slots.
|
|
25
|
+
import { _assertNotOwned, getInstance } from '../init.js';
|
|
26
|
+
export const SHA256Hash = {
|
|
27
|
+
outputSize: 32,
|
|
28
|
+
wasmModules: ['sha2'],
|
|
29
|
+
digest(msg) {
|
|
30
|
+
_assertNotOwned('sha2');
|
|
31
|
+
const x = getInstance('sha2').exports;
|
|
32
|
+
const mem = new Uint8Array(x.memory.buffer);
|
|
33
|
+
try {
|
|
34
|
+
x.sha256Init();
|
|
35
|
+
const inOff = x.getSha256InputOffset();
|
|
36
|
+
let pos = 0;
|
|
37
|
+
while (pos < msg.length) {
|
|
38
|
+
const n = Math.min(msg.length - pos, 64);
|
|
39
|
+
mem.set(msg.subarray(pos, pos + n), inOff);
|
|
40
|
+
x.sha256Update(n);
|
|
41
|
+
pos += n;
|
|
42
|
+
}
|
|
43
|
+
x.sha256Final();
|
|
44
|
+
const outOff = x.getSha256OutOffset();
|
|
45
|
+
return mem.slice(outOff, outOff + 32);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
// Wipe the SHA-256 input/output/state scratch so secret-derived
|
|
49
|
+
// inputs (e.g. Fortuna pool entropy) do not outlive this call.
|
|
50
|
+
x.wipeBuffers();
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
package/dist/sha2/index.d.ts
CHANGED
package/dist/sha2/index.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
//
|
|
24
24
|
// Public API classes for the SHA-2 WASM module.
|
|
25
25
|
// Uses the init() module cache โ call sha2Init(source) before constructing.
|
|
26
|
-
import { getInstance, initModule } from '../init.js';
|
|
26
|
+
import { getInstance, initModule, _assertNotOwned } from '../init.js';
|
|
27
27
|
export async function sha2Init(source) {
|
|
28
28
|
return initModule('sha2', source);
|
|
29
29
|
}
|
|
@@ -57,6 +57,7 @@ export class SHA256 {
|
|
|
57
57
|
this.x = getExports();
|
|
58
58
|
}
|
|
59
59
|
hash(msg) {
|
|
60
|
+
_assertNotOwned('sha2');
|
|
60
61
|
this.x.sha256Init();
|
|
61
62
|
feedHash(this.x, msg, this.x.getSha256InputOffset(), 64, this.x.sha256Update);
|
|
62
63
|
this.x.sha256Final();
|
|
@@ -64,6 +65,7 @@ export class SHA256 {
|
|
|
64
65
|
return mem.slice(this.x.getSha256OutOffset(), this.x.getSha256OutOffset() + 32);
|
|
65
66
|
}
|
|
66
67
|
dispose() {
|
|
68
|
+
_assertNotOwned('sha2');
|
|
67
69
|
this.x.wipeBuffers();
|
|
68
70
|
}
|
|
69
71
|
}
|
|
@@ -74,6 +76,7 @@ export class SHA512 {
|
|
|
74
76
|
this.x = getExports();
|
|
75
77
|
}
|
|
76
78
|
hash(msg) {
|
|
79
|
+
_assertNotOwned('sha2');
|
|
77
80
|
this.x.sha512Init();
|
|
78
81
|
feedHash(this.x, msg, this.x.getSha512InputOffset(), 128, this.x.sha512Update);
|
|
79
82
|
this.x.sha512Final();
|
|
@@ -81,6 +84,7 @@ export class SHA512 {
|
|
|
81
84
|
return mem.slice(this.x.getSha512OutOffset(), this.x.getSha512OutOffset() + 64);
|
|
82
85
|
}
|
|
83
86
|
dispose() {
|
|
87
|
+
_assertNotOwned('sha2');
|
|
84
88
|
this.x.wipeBuffers();
|
|
85
89
|
}
|
|
86
90
|
}
|
|
@@ -91,6 +95,7 @@ export class SHA384 {
|
|
|
91
95
|
this.x = getExports();
|
|
92
96
|
}
|
|
93
97
|
hash(msg) {
|
|
98
|
+
_assertNotOwned('sha2');
|
|
94
99
|
this.x.sha384Init();
|
|
95
100
|
feedHash(this.x, msg, this.x.getSha512InputOffset(), 128, this.x.sha512Update);
|
|
96
101
|
this.x.sha384Final();
|
|
@@ -98,6 +103,7 @@ export class SHA384 {
|
|
|
98
103
|
return mem.slice(this.x.getSha512OutOffset(), this.x.getSha512OutOffset() + 48);
|
|
99
104
|
}
|
|
100
105
|
dispose() {
|
|
106
|
+
_assertNotOwned('sha2');
|
|
101
107
|
this.x.wipeBuffers();
|
|
102
108
|
}
|
|
103
109
|
}
|
|
@@ -108,6 +114,7 @@ export class HMAC_SHA256 {
|
|
|
108
114
|
this.x = getExports();
|
|
109
115
|
}
|
|
110
116
|
hash(key, msg) {
|
|
117
|
+
_assertNotOwned('sha2');
|
|
111
118
|
let k = key;
|
|
112
119
|
// RFC 2104 ยง3: keys longer than block size are pre-hashed
|
|
113
120
|
if (k.length > 64) {
|
|
@@ -126,6 +133,7 @@ export class HMAC_SHA256 {
|
|
|
126
133
|
return out.slice(this.x.getSha256OutOffset(), this.x.getSha256OutOffset() + 32);
|
|
127
134
|
}
|
|
128
135
|
dispose() {
|
|
136
|
+
_assertNotOwned('sha2');
|
|
129
137
|
this.x.wipeBuffers();
|
|
130
138
|
}
|
|
131
139
|
}
|
|
@@ -136,6 +144,7 @@ export class HMAC_SHA512 {
|
|
|
136
144
|
this.x = getExports();
|
|
137
145
|
}
|
|
138
146
|
hash(key, msg) {
|
|
147
|
+
_assertNotOwned('sha2');
|
|
139
148
|
let k = key;
|
|
140
149
|
// RFC 2104 ยง3: keys longer than block size (128) are pre-hashed
|
|
141
150
|
if (k.length > 128) {
|
|
@@ -154,6 +163,7 @@ export class HMAC_SHA512 {
|
|
|
154
163
|
return out.slice(this.x.getSha512OutOffset(), this.x.getSha512OutOffset() + 64);
|
|
155
164
|
}
|
|
156
165
|
dispose() {
|
|
166
|
+
_assertNotOwned('sha2');
|
|
157
167
|
this.x.wipeBuffers();
|
|
158
168
|
}
|
|
159
169
|
}
|
|
@@ -164,6 +174,7 @@ export class HMAC_SHA384 {
|
|
|
164
174
|
this.x = getExports();
|
|
165
175
|
}
|
|
166
176
|
hash(key, msg) {
|
|
177
|
+
_assertNotOwned('sha2');
|
|
167
178
|
let k = key;
|
|
168
179
|
// RFC 2104 ยง3: keys longer than block size (128) are pre-hashed with SHA-384
|
|
169
180
|
if (k.length > 128) {
|
|
@@ -182,8 +193,11 @@ export class HMAC_SHA384 {
|
|
|
182
193
|
return out.slice(this.x.getSha512OutOffset(), this.x.getSha512OutOffset() + 48);
|
|
183
194
|
}
|
|
184
195
|
dispose() {
|
|
196
|
+
_assertNotOwned('sha2');
|
|
185
197
|
this.x.wipeBuffers();
|
|
186
198
|
}
|
|
187
199
|
}
|
|
188
200
|
// โโ HKDF โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
189
201
|
export { HKDF_SHA256, HKDF_SHA512 } from './hkdf.js';
|
|
202
|
+
// โโ SHA256Hash โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
203
|
+
export { SHA256Hash } from './hash.js';
|