leviathan-crypto 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +265 -0
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/SECURITY.md +174 -0
- package/dist/chacha.wasm +0 -0
- package/dist/chacha20/index.d.ts +49 -0
- package/dist/chacha20/index.js +177 -0
- package/dist/chacha20/ops.d.ts +16 -0
- package/dist/chacha20/ops.js +146 -0
- package/dist/chacha20/pool.d.ts +52 -0
- package/dist/chacha20/pool.js +188 -0
- package/dist/chacha20/pool.worker.d.ts +1 -0
- package/dist/chacha20/pool.worker.js +37 -0
- package/dist/chacha20/types.d.ts +30 -0
- package/dist/chacha20/types.js +1 -0
- package/dist/docs/architecture.md +795 -0
- package/dist/docs/argon2id.md +290 -0
- package/dist/docs/chacha20.md +602 -0
- package/dist/docs/chacha20_pool.md +306 -0
- package/dist/docs/fortuna.md +322 -0
- package/dist/docs/init.md +308 -0
- package/dist/docs/loader.md +206 -0
- package/dist/docs/serpent.md +914 -0
- package/dist/docs/sha2.md +620 -0
- package/dist/docs/sha3.md +509 -0
- package/dist/docs/types.md +198 -0
- package/dist/docs/utils.md +273 -0
- package/dist/docs/wasm.md +193 -0
- package/dist/embedded/chacha.d.ts +1 -0
- package/dist/embedded/chacha.js +2 -0
- package/dist/embedded/serpent.d.ts +1 -0
- package/dist/embedded/serpent.js +2 -0
- package/dist/embedded/sha2.d.ts +1 -0
- package/dist/embedded/sha2.js +2 -0
- package/dist/embedded/sha3.d.ts +1 -0
- package/dist/embedded/sha3.js +2 -0
- package/dist/fortuna.d.ts +72 -0
- package/dist/fortuna.js +445 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +44 -0
- package/dist/init.d.ts +11 -0
- package/dist/init.js +49 -0
- package/dist/loader.d.ts +4 -0
- package/dist/loader.js +30 -0
- package/dist/serpent/index.d.ts +65 -0
- package/dist/serpent/index.js +242 -0
- package/dist/serpent/seal.d.ts +8 -0
- package/dist/serpent/seal.js +70 -0
- package/dist/serpent/stream-encoder.d.ts +20 -0
- package/dist/serpent/stream-encoder.js +167 -0
- package/dist/serpent/stream-pool.d.ts +48 -0
- package/dist/serpent/stream-pool.js +285 -0
- package/dist/serpent/stream-sealer.d.ts +34 -0
- package/dist/serpent/stream-sealer.js +223 -0
- package/dist/serpent/stream.d.ts +28 -0
- package/dist/serpent/stream.js +205 -0
- package/dist/serpent/stream.worker.d.ts +32 -0
- package/dist/serpent/stream.worker.js +117 -0
- package/dist/serpent/types.d.ts +5 -0
- package/dist/serpent/types.js +1 -0
- package/dist/serpent.wasm +0 -0
- package/dist/sha2/hkdf.d.ts +16 -0
- package/dist/sha2/hkdf.js +108 -0
- package/dist/sha2/index.d.ts +40 -0
- package/dist/sha2/index.js +190 -0
- package/dist/sha2/types.d.ts +5 -0
- package/dist/sha2/types.js +1 -0
- package/dist/sha2.wasm +0 -0
- package/dist/sha3/index.d.ts +55 -0
- package/dist/sha3/index.js +246 -0
- package/dist/sha3/types.d.ts +5 -0
- package/dist/sha3/types.js +1 -0
- package/dist/sha3.wasm +0 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.js +26 -0
- package/dist/utils.d.ts +26 -0
- package/dist/utils.js +169 -0
- package/package.json +90 -0
|
@@ -0,0 +1,242 @@
|
|
|
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/index.ts
|
|
23
|
+
//
|
|
24
|
+
// Public API classes for the Serpent-256 WASM module.
|
|
25
|
+
// Uses the init() module cache — call init('serpent') before constructing.
|
|
26
|
+
import { getInstance, initModule } from '../init.js';
|
|
27
|
+
const _embedded = () => import('../embedded/serpent.js').then(m => m.WASM_BASE64);
|
|
28
|
+
export async function serpentInit(mode = 'embedded', opts) {
|
|
29
|
+
return initModule('serpent', _embedded, mode, opts);
|
|
30
|
+
}
|
|
31
|
+
function getExports() {
|
|
32
|
+
return getInstance('serpent').exports;
|
|
33
|
+
}
|
|
34
|
+
// ── Serpent ──────────────────────────────────────────────────────────────────
|
|
35
|
+
export class Serpent {
|
|
36
|
+
x;
|
|
37
|
+
constructor() {
|
|
38
|
+
this.x = getExports();
|
|
39
|
+
}
|
|
40
|
+
loadKey(key) {
|
|
41
|
+
if (key.length !== 16 && key.length !== 24 && key.length !== 32)
|
|
42
|
+
throw new RangeError(`key must be 16, 24, or 32 bytes (got ${key.length})`);
|
|
43
|
+
const mem = new Uint8Array(this.x.memory.buffer);
|
|
44
|
+
mem.set(key, this.x.getKeyOffset());
|
|
45
|
+
if (this.x.loadKey(key.length) !== 0)
|
|
46
|
+
throw new Error('loadKey failed');
|
|
47
|
+
}
|
|
48
|
+
encryptBlock(plaintext) {
|
|
49
|
+
if (plaintext.length !== 16)
|
|
50
|
+
throw new RangeError(`block must be 16 bytes (got ${plaintext.length})`);
|
|
51
|
+
const mem = new Uint8Array(this.x.memory.buffer);
|
|
52
|
+
const ptOff = this.x.getBlockPtOffset();
|
|
53
|
+
const ctOff = this.x.getBlockCtOffset();
|
|
54
|
+
mem.set(plaintext, ptOff);
|
|
55
|
+
this.x.encryptBlock();
|
|
56
|
+
return mem.slice(ctOff, ctOff + 16);
|
|
57
|
+
}
|
|
58
|
+
decryptBlock(ciphertext) {
|
|
59
|
+
if (ciphertext.length !== 16)
|
|
60
|
+
throw new RangeError(`block must be 16 bytes (got ${ciphertext.length})`);
|
|
61
|
+
const mem = new Uint8Array(this.x.memory.buffer);
|
|
62
|
+
const ptOff = this.x.getBlockPtOffset();
|
|
63
|
+
const ctOff = this.x.getBlockCtOffset();
|
|
64
|
+
mem.set(ciphertext, ctOff);
|
|
65
|
+
this.x.decryptBlock();
|
|
66
|
+
return mem.slice(ptOff, ptOff + 16);
|
|
67
|
+
}
|
|
68
|
+
dispose() {
|
|
69
|
+
this.x.wipeBuffers();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ── SerpentCtr ───────────────────────────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* Serpent-256 in CTR mode.
|
|
75
|
+
*
|
|
76
|
+
* **WARNING: CTR mode is unauthenticated.** An attacker can flip ciphertext
|
|
77
|
+
* bits without detection. Always pair with HMAC-SHA256 (Encrypt-then-MAC)
|
|
78
|
+
* or use `XChaCha20Poly1305` instead.
|
|
79
|
+
*/
|
|
80
|
+
export class SerpentCtr {
|
|
81
|
+
x;
|
|
82
|
+
constructor(opts) {
|
|
83
|
+
if (!opts?.dangerUnauthenticated) {
|
|
84
|
+
throw new Error('leviathan-crypto: SerpentCtr is unauthenticated — use SerpentSeal instead. ' +
|
|
85
|
+
'To use SerpentCtr directly, pass { dangerUnauthenticated: true }.');
|
|
86
|
+
}
|
|
87
|
+
this.x = getExports();
|
|
88
|
+
}
|
|
89
|
+
beginEncrypt(key, nonce) {
|
|
90
|
+
if (key.length !== 16 && key.length !== 24 && key.length !== 32)
|
|
91
|
+
throw new RangeError('key must be 16, 24, or 32 bytes');
|
|
92
|
+
if (nonce.length !== 16)
|
|
93
|
+
throw new RangeError(`nonce must be 16 bytes (got ${nonce.length})`);
|
|
94
|
+
const mem = new Uint8Array(this.x.memory.buffer);
|
|
95
|
+
mem.set(key, this.x.getKeyOffset());
|
|
96
|
+
mem.set(nonce, this.x.getNonceOffset());
|
|
97
|
+
this.x.loadKey(key.length);
|
|
98
|
+
this.x.resetCounter();
|
|
99
|
+
}
|
|
100
|
+
encryptChunk(chunk) {
|
|
101
|
+
const maxChunk = this.x.getChunkSize();
|
|
102
|
+
if (chunk.length > maxChunk)
|
|
103
|
+
throw new RangeError(`chunk exceeds maximum size of ${maxChunk} bytes — split into smaller chunks`);
|
|
104
|
+
const mem = new Uint8Array(this.x.memory.buffer);
|
|
105
|
+
const ptOff = this.x.getChunkPtOffset();
|
|
106
|
+
const ctOff = this.x.getChunkCtOffset();
|
|
107
|
+
mem.set(chunk, ptOff);
|
|
108
|
+
this.x.encryptChunk(chunk.length);
|
|
109
|
+
return mem.slice(ctOff, ctOff + chunk.length);
|
|
110
|
+
}
|
|
111
|
+
beginDecrypt(key, nonce) {
|
|
112
|
+
this.beginEncrypt(key, nonce);
|
|
113
|
+
}
|
|
114
|
+
decryptChunk(chunk) {
|
|
115
|
+
return this.encryptChunk(chunk);
|
|
116
|
+
}
|
|
117
|
+
dispose() {
|
|
118
|
+
this.x.wipeBuffers();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// ── PKCS7 helpers ────────────────────────────────────────────────────────────
|
|
122
|
+
function pkcs7Pad(data) {
|
|
123
|
+
const padLen = 16 - (data.length % 16); // 1..16
|
|
124
|
+
const out = new Uint8Array(data.length + padLen);
|
|
125
|
+
out.set(data);
|
|
126
|
+
out.fill(padLen, data.length);
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
function pkcs7Strip(data) {
|
|
130
|
+
if (data.length === 0)
|
|
131
|
+
throw new RangeError('empty ciphertext');
|
|
132
|
+
const padLen = data[data.length - 1];
|
|
133
|
+
if (padLen === 0 || padLen > 16)
|
|
134
|
+
throw new RangeError(`invalid PKCS7 padding byte: ${padLen}`);
|
|
135
|
+
if (padLen > data.length)
|
|
136
|
+
throw new RangeError(`invalid PKCS7 padding: pad length ${padLen} exceeds data length ${data.length}`);
|
|
137
|
+
let bad = 0;
|
|
138
|
+
for (let i = data.length - padLen; i < data.length; i++)
|
|
139
|
+
bad |= data[i] ^ padLen;
|
|
140
|
+
if (bad !== 0)
|
|
141
|
+
throw new RangeError('invalid PKCS7 padding');
|
|
142
|
+
return data.subarray(0, data.length - padLen);
|
|
143
|
+
}
|
|
144
|
+
// ── SerpentCbc ───────────────────────────────────────────────────────────────
|
|
145
|
+
/**
|
|
146
|
+
* Serpent-256 in CBC mode with PKCS7 padding.
|
|
147
|
+
*
|
|
148
|
+
* **WARNING: CBC mode is unauthenticated.** Always authenticate the output
|
|
149
|
+
* with HMAC-SHA256 (Encrypt-then-MAC) or use `XChaCha20Poly1305` instead.
|
|
150
|
+
*/
|
|
151
|
+
export class SerpentCbc {
|
|
152
|
+
x;
|
|
153
|
+
constructor(opts) {
|
|
154
|
+
if (!opts?.dangerUnauthenticated) {
|
|
155
|
+
throw new Error('leviathan-crypto: SerpentCbc is unauthenticated — use SerpentSeal instead. ' +
|
|
156
|
+
'To use SerpentCbc directly, pass { dangerUnauthenticated: true }.');
|
|
157
|
+
}
|
|
158
|
+
this.x = getExports();
|
|
159
|
+
}
|
|
160
|
+
get mem() {
|
|
161
|
+
return new Uint8Array(this.x.memory.buffer);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Encrypt plaintext with Serpent-256 CBC + PKCS7 padding.
|
|
165
|
+
*
|
|
166
|
+
* @param key 16, 24, or 32 bytes
|
|
167
|
+
* @param iv 16 bytes — must be random and unique per (key, message)
|
|
168
|
+
* @param plaintext any length — PKCS7 padding applied automatically
|
|
169
|
+
* @returns ciphertext (length = ceil((plaintext.length + 1) / 16) * 16)
|
|
170
|
+
*/
|
|
171
|
+
encrypt(key, iv, plaintext) {
|
|
172
|
+
this._loadKey(key);
|
|
173
|
+
this._setIv(iv);
|
|
174
|
+
const padded = pkcs7Pad(plaintext);
|
|
175
|
+
const output = new Uint8Array(padded.length);
|
|
176
|
+
const ptOff = this.x.getChunkPtOffset();
|
|
177
|
+
const ctOff = this.x.getChunkCtOffset();
|
|
178
|
+
const maxChunk = 65536;
|
|
179
|
+
for (let off = 0; off < padded.length; off += maxChunk) {
|
|
180
|
+
const chunk = padded.subarray(off, Math.min(off + maxChunk, padded.length));
|
|
181
|
+
this.mem.set(chunk, ptOff);
|
|
182
|
+
this.x.cbcEncryptChunk(chunk.length);
|
|
183
|
+
output.set(new Uint8Array(this.x.memory.buffer).subarray(ctOff, ctOff + chunk.length), off);
|
|
184
|
+
}
|
|
185
|
+
return output;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Decrypt Serpent-256 CBC + PKCS7.
|
|
189
|
+
* Throws if ciphertext length is not a non-zero multiple of 16 or PKCS7 is invalid.
|
|
190
|
+
*/
|
|
191
|
+
decrypt(key, iv, ciphertext) {
|
|
192
|
+
if (ciphertext.length === 0 || ciphertext.length % 16 !== 0)
|
|
193
|
+
throw new RangeError('ciphertext length must be a non-zero multiple of 16');
|
|
194
|
+
this._loadKey(key);
|
|
195
|
+
this._setIv(iv);
|
|
196
|
+
const output = new Uint8Array(ciphertext.length);
|
|
197
|
+
const ctOff = this.x.getChunkCtOffset();
|
|
198
|
+
const ptOff = this.x.getChunkPtOffset();
|
|
199
|
+
const maxChunk = 65536;
|
|
200
|
+
for (let off = 0; off < ciphertext.length; off += maxChunk) {
|
|
201
|
+
const chunk = ciphertext.subarray(off, Math.min(off + maxChunk, ciphertext.length));
|
|
202
|
+
this.mem.set(chunk, ctOff);
|
|
203
|
+
this.x.cbcDecryptChunk(chunk.length);
|
|
204
|
+
output.set(new Uint8Array(this.x.memory.buffer).subarray(ptOff, ptOff + chunk.length), off);
|
|
205
|
+
}
|
|
206
|
+
return pkcs7Strip(output);
|
|
207
|
+
}
|
|
208
|
+
dispose() {
|
|
209
|
+
this.x.wipeBuffers();
|
|
210
|
+
}
|
|
211
|
+
_loadKey(key) {
|
|
212
|
+
if (key.length !== 16 && key.length !== 24 && key.length !== 32)
|
|
213
|
+
throw new RangeError(`Serpent key must be 16, 24, or 32 bytes (got ${key.length})`);
|
|
214
|
+
this.mem.set(key, this.x.getKeyOffset());
|
|
215
|
+
this.x.loadKey(key.length);
|
|
216
|
+
}
|
|
217
|
+
_setIv(iv) {
|
|
218
|
+
if (iv.length !== 16)
|
|
219
|
+
throw new RangeError(`CBC IV must be 16 bytes (got ${iv.length})`);
|
|
220
|
+
this.mem.set(iv, this.x.getCbcIvOffset());
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// ── SerpentSeal re-export ─────────────────────────────────────────────────────
|
|
224
|
+
export { SerpentSeal } from './seal.js';
|
|
225
|
+
// ── SerpentStream re-export ───────────────────────────────────────────────────
|
|
226
|
+
export { SerpentStream, sealChunk, openChunk } from './stream.js';
|
|
227
|
+
// ── SerpentStreamPool re-export ───────────────────────────────────────────────
|
|
228
|
+
export { SerpentStreamPool } from './stream-pool.js';
|
|
229
|
+
// ── SerpentStreamSealer / SerpentStreamOpener re-export ───────────────────────
|
|
230
|
+
export { SerpentStreamSealer, SerpentStreamOpener } from './stream-sealer.js';
|
|
231
|
+
// ── SerpentStreamEncoder / SerpentStreamDecoder re-export ─────────────────────
|
|
232
|
+
export { SerpentStreamEncoder, SerpentStreamDecoder } from './stream-encoder.js';
|
|
233
|
+
// ── Ready check ──────────────────────────────────────────────────────────────
|
|
234
|
+
export function _serpentReady() {
|
|
235
|
+
try {
|
|
236
|
+
getInstance('serpent');
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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/seal.ts
|
|
23
|
+
//
|
|
24
|
+
// SerpentSeal — authenticated Serpent-256 encryption.
|
|
25
|
+
// Encrypt-then-MAC: SerpentCbc + HMAC-SHA256. Tier 2 pure-TS composition.
|
|
26
|
+
import { SerpentCbc, _serpentReady } from './index.js';
|
|
27
|
+
import { HMAC_SHA256, _sha2Ready } from '../sha2/index.js';
|
|
28
|
+
import { concat, constantTimeEqual } from '../utils.js';
|
|
29
|
+
export class SerpentSeal {
|
|
30
|
+
_cbc;
|
|
31
|
+
_hmac;
|
|
32
|
+
constructor() {
|
|
33
|
+
if (!_serpentReady() || !_sha2Ready())
|
|
34
|
+
throw new Error('leviathan-crypto: call init([\'serpent\', \'sha2\']) before using SerpentSeal');
|
|
35
|
+
this._cbc = new SerpentCbc({ dangerUnauthenticated: true });
|
|
36
|
+
this._hmac = new HMAC_SHA256();
|
|
37
|
+
}
|
|
38
|
+
// _iv: test seam only — inject a fixed IV for deterministic KAT vectors
|
|
39
|
+
encrypt(key, plaintext, _iv) {
|
|
40
|
+
if (key.length !== 64)
|
|
41
|
+
throw new RangeError(`SerpentSeal key must be 64 bytes (got ${key.length})`);
|
|
42
|
+
const encKey = key.subarray(0, 32);
|
|
43
|
+
const macKey = key.subarray(32, 64);
|
|
44
|
+
const iv = (_iv && _iv.length === 16) ? _iv : new Uint8Array(16);
|
|
45
|
+
if (!_iv || _iv.length !== 16)
|
|
46
|
+
crypto.getRandomValues(iv);
|
|
47
|
+
const ciphertext = this._cbc.encrypt(encKey, iv, plaintext);
|
|
48
|
+
const tag = this._hmac.hash(macKey, concat(iv, ciphertext));
|
|
49
|
+
return concat(concat(iv, ciphertext), tag);
|
|
50
|
+
}
|
|
51
|
+
decrypt(key, data) {
|
|
52
|
+
if (key.length !== 64)
|
|
53
|
+
throw new RangeError(`SerpentSeal key must be 64 bytes (got ${key.length})`);
|
|
54
|
+
if (data.length < 64)
|
|
55
|
+
throw new RangeError('SerpentSeal ciphertext too short');
|
|
56
|
+
const encKey = key.subarray(0, 32);
|
|
57
|
+
const macKey = key.subarray(32, 64);
|
|
58
|
+
const iv = data.subarray(0, 16);
|
|
59
|
+
const tag = data.subarray(data.length - 32);
|
|
60
|
+
const ciphertext = data.subarray(16, data.length - 32);
|
|
61
|
+
const expectedTag = this._hmac.hash(macKey, concat(iv, ciphertext));
|
|
62
|
+
if (!constantTimeEqual(tag, expectedTag))
|
|
63
|
+
throw new Error('SerpentSeal: authentication failed');
|
|
64
|
+
return this._cbc.decrypt(encKey, iv, ciphertext);
|
|
65
|
+
}
|
|
66
|
+
dispose() {
|
|
67
|
+
this._cbc.dispose();
|
|
68
|
+
this._hmac.dispose();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare class SerpentStreamEncoder {
|
|
2
|
+
private readonly _sealer;
|
|
3
|
+
private _state;
|
|
4
|
+
constructor(key: Uint8Array, chunkSize?: number, _nonce?: Uint8Array, _ivs?: Uint8Array[]);
|
|
5
|
+
header(): Uint8Array;
|
|
6
|
+
encode(plaintext: Uint8Array): Uint8Array;
|
|
7
|
+
encodeFinal(plaintext: Uint8Array): Uint8Array;
|
|
8
|
+
dispose(): void;
|
|
9
|
+
}
|
|
10
|
+
export declare class SerpentStreamDecoder {
|
|
11
|
+
private readonly _opener;
|
|
12
|
+
private readonly _buf;
|
|
13
|
+
private readonly _maxFrame;
|
|
14
|
+
private _bufLen;
|
|
15
|
+
private _dead;
|
|
16
|
+
constructor(key: Uint8Array, header: Uint8Array);
|
|
17
|
+
feed(bytes: Uint8Array): Uint8Array[];
|
|
18
|
+
private _wipe;
|
|
19
|
+
dispose(): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
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/stream-encoder.ts
|
|
23
|
+
//
|
|
24
|
+
// Tier 2 pure-TS composition: SerpentStreamSealer / SerpentStreamOpener
|
|
25
|
+
// with u32be length-prefixed framing.
|
|
26
|
+
//
|
|
27
|
+
// SerpentStreamEncoder: wraps SerpentStreamSealer, prepends u32be(sealedLen)
|
|
28
|
+
// SerpentStreamDecoder: wraps SerpentStreamOpener, buffers input and assembles
|
|
29
|
+
// complete frames before dispatching to opener.open()
|
|
30
|
+
//
|
|
31
|
+
// Wire format per chunk:
|
|
32
|
+
// u32be(sealedLen) || IV(16) || CBC_ciphertext(padded) || HMAC(32)
|
|
33
|
+
//
|
|
34
|
+
// Use these classes when chunks will be concatenated into a flat byte array
|
|
35
|
+
// (files, buffered TCP, etc). Use SerpentStreamSealer/Opener directly when
|
|
36
|
+
// the transport already frames messages (WebSocket, IPC, etc).
|
|
37
|
+
import { SerpentStreamSealer, SerpentStreamOpener } from './stream-sealer.js';
|
|
38
|
+
import { _serpentReady } from './index.js';
|
|
39
|
+
import { _sha2Ready } from '../sha2/index.js';
|
|
40
|
+
import { wipe } from '../utils.js';
|
|
41
|
+
import { u32be } from './stream.js';
|
|
42
|
+
export class SerpentStreamEncoder {
|
|
43
|
+
_sealer;
|
|
44
|
+
_state;
|
|
45
|
+
// _nonce, _ivs: test seams — passed through to SerpentStreamSealer
|
|
46
|
+
constructor(key, chunkSize, _nonce, _ivs) {
|
|
47
|
+
if (!_serpentReady())
|
|
48
|
+
throw new Error('leviathan-crypto: call init([\'serpent\']) before using SerpentStreamEncoder');
|
|
49
|
+
if (!_sha2Ready())
|
|
50
|
+
throw new Error('leviathan-crypto: call init([\'sha2\']) before using SerpentStreamEncoder');
|
|
51
|
+
this._sealer = new SerpentStreamSealer(key, chunkSize, _nonce, _ivs);
|
|
52
|
+
this._state = 'fresh';
|
|
53
|
+
}
|
|
54
|
+
header() {
|
|
55
|
+
if (this._state === 'encoding')
|
|
56
|
+
throw new Error('SerpentStreamEncoder: header() already called');
|
|
57
|
+
if (this._state === 'dead')
|
|
58
|
+
throw new Error('SerpentStreamEncoder: stream is closed');
|
|
59
|
+
this._state = 'encoding';
|
|
60
|
+
return this._sealer.header();
|
|
61
|
+
}
|
|
62
|
+
encode(plaintext) {
|
|
63
|
+
if (this._state === 'fresh')
|
|
64
|
+
throw new Error('SerpentStreamEncoder: call header() first');
|
|
65
|
+
if (this._state === 'dead')
|
|
66
|
+
throw new Error('SerpentStreamEncoder: stream is closed');
|
|
67
|
+
const sealed = this._sealer.seal(plaintext);
|
|
68
|
+
return _prependLen(sealed);
|
|
69
|
+
}
|
|
70
|
+
encodeFinal(plaintext) {
|
|
71
|
+
if (this._state === 'fresh')
|
|
72
|
+
throw new Error('SerpentStreamEncoder: call header() first');
|
|
73
|
+
if (this._state === 'dead')
|
|
74
|
+
throw new Error('SerpentStreamEncoder: stream is closed');
|
|
75
|
+
const sealed = this._sealer.final(plaintext);
|
|
76
|
+
this._state = 'dead';
|
|
77
|
+
return _prependLen(sealed);
|
|
78
|
+
}
|
|
79
|
+
dispose() {
|
|
80
|
+
if (this._state !== 'dead') {
|
|
81
|
+
this._sealer.dispose();
|
|
82
|
+
this._state = 'dead';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ── SerpentStreamDecoder ─────────────────────────────────────────────────────
|
|
87
|
+
export class SerpentStreamDecoder {
|
|
88
|
+
_opener;
|
|
89
|
+
_buf; // fixed-size accumulation buffer
|
|
90
|
+
_maxFrame; // 4 + max sealed chunk size
|
|
91
|
+
_bufLen; // valid bytes currently in _buf
|
|
92
|
+
_dead;
|
|
93
|
+
constructor(key, header) {
|
|
94
|
+
if (!_serpentReady())
|
|
95
|
+
throw new Error('leviathan-crypto: call init([\'serpent\']) before using SerpentStreamDecoder');
|
|
96
|
+
if (!_sha2Ready())
|
|
97
|
+
throw new Error('leviathan-crypto: call init([\'sha2\']) before using SerpentStreamDecoder');
|
|
98
|
+
this._opener = new SerpentStreamOpener(key, header);
|
|
99
|
+
// Parse chunkSize from stream header (bytes 16..20, u32be)
|
|
100
|
+
const cs = (header[16] << 24 | header[17] << 16 | header[18] << 8 | header[19]) >>> 0;
|
|
101
|
+
// Max sealed chunk size: IV(16) + PKCS7-padded ciphertext + HMAC(32)
|
|
102
|
+
// PKCS7: plaintext always padded to next multiple of 16 (minimum 1 pad byte)
|
|
103
|
+
const maxSealed = 16 + (cs + (16 - (cs % 16))) + 32;
|
|
104
|
+
this._maxFrame = 4 + maxSealed;
|
|
105
|
+
this._buf = new Uint8Array(this._maxFrame);
|
|
106
|
+
this._bufLen = 0;
|
|
107
|
+
this._dead = false;
|
|
108
|
+
}
|
|
109
|
+
feed(bytes) {
|
|
110
|
+
if (this._dead)
|
|
111
|
+
throw new Error('SerpentStreamDecoder: stream is closed');
|
|
112
|
+
// Append incoming bytes to accumulation buffer
|
|
113
|
+
if (this._bufLen + bytes.length > this._maxFrame) {
|
|
114
|
+
throw new Error('SerpentStreamDecoder: input exceeds maximum frame size');
|
|
115
|
+
}
|
|
116
|
+
this._buf.set(bytes, this._bufLen);
|
|
117
|
+
this._bufLen += bytes.length;
|
|
118
|
+
const results = [];
|
|
119
|
+
while (true) {
|
|
120
|
+
// Need at least 4 bytes for the length prefix
|
|
121
|
+
if (this._bufLen < 4)
|
|
122
|
+
break;
|
|
123
|
+
const sealedLen = ((this._buf[0] << 24 | this._buf[1] << 16 |
|
|
124
|
+
this._buf[2] << 8 | this._buf[3]) >>> 0);
|
|
125
|
+
const frameLen = 4 + sealedLen;
|
|
126
|
+
// Need the full frame
|
|
127
|
+
if (this._bufLen < frameLen)
|
|
128
|
+
break;
|
|
129
|
+
// Complete frame — dispatch to opener
|
|
130
|
+
const sealedChunk = this._buf.subarray(4, frameLen);
|
|
131
|
+
const plaintext = this._opener.open(sealedChunk);
|
|
132
|
+
results.push(plaintext);
|
|
133
|
+
if (this._opener.closed) {
|
|
134
|
+
// After final chunk: any leftover bytes are a protocol error
|
|
135
|
+
const remaining = this._bufLen - frameLen;
|
|
136
|
+
if (remaining > 0) {
|
|
137
|
+
this._wipe();
|
|
138
|
+
throw new Error('SerpentStreamDecoder: unexpected bytes after final chunk');
|
|
139
|
+
}
|
|
140
|
+
this._wipe();
|
|
141
|
+
return results;
|
|
142
|
+
}
|
|
143
|
+
// Shift remaining bytes to front of buffer — no allocation
|
|
144
|
+
const remaining = this._bufLen - frameLen;
|
|
145
|
+
this._buf.copyWithin(0, frameLen, frameLen + remaining);
|
|
146
|
+
this._bufLen = remaining;
|
|
147
|
+
}
|
|
148
|
+
return results;
|
|
149
|
+
}
|
|
150
|
+
_wipe() {
|
|
151
|
+
wipe(this._buf);
|
|
152
|
+
this._bufLen = 0;
|
|
153
|
+
this._opener.dispose();
|
|
154
|
+
this._dead = true;
|
|
155
|
+
}
|
|
156
|
+
dispose() {
|
|
157
|
+
if (!this._dead)
|
|
158
|
+
this._wipe();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
162
|
+
function _prependLen(chunk) {
|
|
163
|
+
const out = new Uint8Array(4 + chunk.length);
|
|
164
|
+
out.set(u32be(chunk.length), 0);
|
|
165
|
+
out.set(chunk, 4);
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface StreamPoolOpts {
|
|
2
|
+
/** Number of workers. Default: navigator.hardwareConcurrency ?? 4 */
|
|
3
|
+
workers?: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Parallel worker pool for SerpentStream chunked authenticated encryption.
|
|
7
|
+
*
|
|
8
|
+
* Each worker owns its own `serpent.wasm` and `sha2.wasm` instances with
|
|
9
|
+
* isolated linear memory. Key derivation happens on the main thread; workers
|
|
10
|
+
* receive pre-derived encKey/macKey per chunk.
|
|
11
|
+
*
|
|
12
|
+
* Produces the same wire format as `SerpentStream` -- either can decrypt
|
|
13
|
+
* the other's output.
|
|
14
|
+
*/
|
|
15
|
+
export declare class SerpentStreamPool {
|
|
16
|
+
private readonly _workers;
|
|
17
|
+
private readonly _idle;
|
|
18
|
+
private readonly _queue;
|
|
19
|
+
private readonly _pending;
|
|
20
|
+
private readonly _hkdf;
|
|
21
|
+
private _nextId;
|
|
22
|
+
private _disposed;
|
|
23
|
+
private constructor();
|
|
24
|
+
/**
|
|
25
|
+
* Create a new pool. Requires `init(['serpent', 'sha2'])` to have been called.
|
|
26
|
+
* Compiles both WASM modules once and distributes them to all workers.
|
|
27
|
+
*/
|
|
28
|
+
static create(opts?: StreamPoolOpts): Promise<SerpentStreamPool>;
|
|
29
|
+
/**
|
|
30
|
+
* Encrypt plaintext with SerpentStream chunked authenticated encryption.
|
|
31
|
+
* Returns the complete wire format (header + encrypted chunks).
|
|
32
|
+
*/
|
|
33
|
+
seal(key: Uint8Array, plaintext: Uint8Array, chunkSize?: number): Promise<Uint8Array>;
|
|
34
|
+
/**
|
|
35
|
+
* Decrypt SerpentStream wire format.
|
|
36
|
+
* If any chunk fails authentication, rejects immediately -- no partial plaintext.
|
|
37
|
+
*/
|
|
38
|
+
open(key: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array>;
|
|
39
|
+
/** Terminates all workers. Rejects all pending and queued jobs. */
|
|
40
|
+
dispose(): void;
|
|
41
|
+
/** Number of workers in the pool. */
|
|
42
|
+
get size(): number;
|
|
43
|
+
/** Number of jobs currently queued (waiting for a free worker). */
|
|
44
|
+
get queueDepth(): number;
|
|
45
|
+
private _dispatch;
|
|
46
|
+
private _send;
|
|
47
|
+
private _onMessage;
|
|
48
|
+
}
|