leviathan-crypto 1.1.0 → 1.3.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 +19 -17
- package/README.md +153 -82
- package/SECURITY.md +100 -55
- package/dist/chacha.wasm +0 -0
- package/dist/chacha20/index.js +3 -1
- package/dist/chacha20/types.d.ts +2 -0
- package/dist/docs/architecture.md +4 -2
- package/dist/docs/serpent.md +38 -4
- package/dist/docs/utils.md +21 -0
- package/dist/embedded/chacha.d.ts +1 -1
- package/dist/embedded/chacha.js +1 -1
- package/dist/embedded/serpent.d.ts +1 -1
- package/dist/embedded/serpent.js +1 -1
- package/dist/embedded/sha2.d.ts +1 -1
- package/dist/embedded/sha2.js +1 -1
- package/dist/embedded/sha3.d.ts +1 -1
- package/dist/embedded/sha3.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/serpent/index.d.ts +0 -1
- package/dist/serpent/index.js +5 -4
- package/dist/serpent/stream-sealer.d.ts +18 -2
- package/dist/serpent/stream-sealer.js +122 -4
- package/dist/serpent.wasm +0 -0
- package/dist/sha2.wasm +0 -0
- package/dist/sha3.wasm +0 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +25 -0
- package/package.json +1 -1
- package/dist/serpent/stream-encoder.d.ts +0 -20
- package/dist/serpent/stream-encoder.js +0 -167
|
@@ -5,11 +5,19 @@ export declare class SerpentStreamSealer {
|
|
|
5
5
|
private readonly _cbc;
|
|
6
6
|
private readonly _hmac;
|
|
7
7
|
private readonly _hkdf;
|
|
8
|
+
private readonly _framed;
|
|
8
9
|
private readonly _ivs;
|
|
9
10
|
private _ivIdx;
|
|
10
11
|
private _index;
|
|
11
12
|
private _state;
|
|
12
|
-
|
|
13
|
+
/** Public: consumers use this 3-param form. */
|
|
14
|
+
constructor(key: Uint8Array, chunkSize?: number, opts?: {
|
|
15
|
+
framed?: boolean;
|
|
16
|
+
});
|
|
17
|
+
/** @internal Test-only overload to inject fixed nonce/IVs for deterministic KAT vectors. */
|
|
18
|
+
constructor(key: Uint8Array, chunkSize: number | undefined, opts: {
|
|
19
|
+
framed?: boolean;
|
|
20
|
+
} | undefined, _nonce: Uint8Array, _ivs?: Uint8Array[]);
|
|
13
21
|
header(): Uint8Array;
|
|
14
22
|
seal(plaintext: Uint8Array): Uint8Array;
|
|
15
23
|
final(plaintext: Uint8Array): Uint8Array;
|
|
@@ -24,11 +32,19 @@ export declare class SerpentStreamOpener {
|
|
|
24
32
|
private readonly _cbc;
|
|
25
33
|
private readonly _hmac;
|
|
26
34
|
private readonly _hkdf;
|
|
35
|
+
private readonly _framed;
|
|
36
|
+
private readonly _buf;
|
|
37
|
+
private readonly _maxFrame;
|
|
38
|
+
private _bufLen;
|
|
27
39
|
private _index;
|
|
28
40
|
private _dead;
|
|
29
|
-
constructor(key: Uint8Array, header: Uint8Array
|
|
41
|
+
constructor(key: Uint8Array, header: Uint8Array, opts?: {
|
|
42
|
+
framed?: boolean;
|
|
43
|
+
});
|
|
30
44
|
get closed(): boolean;
|
|
31
45
|
open(chunk: Uint8Array): Uint8Array;
|
|
46
|
+
private _openRaw;
|
|
47
|
+
feed(bytes: Uint8Array): Uint8Array[];
|
|
32
48
|
private _wipe;
|
|
33
49
|
dispose(): void;
|
|
34
50
|
}
|
|
@@ -63,12 +63,12 @@ export class SerpentStreamSealer {
|
|
|
63
63
|
_cbc;
|
|
64
64
|
_hmac;
|
|
65
65
|
_hkdf;
|
|
66
|
+
_framed;
|
|
66
67
|
_ivs; // test seam: fixed IVs
|
|
67
68
|
_ivIdx;
|
|
68
69
|
_index;
|
|
69
70
|
_state;
|
|
70
|
-
|
|
71
|
-
constructor(key, chunkSize, _nonce, _ivs) {
|
|
71
|
+
constructor(key, chunkSize, opts, _nonce, _ivs) {
|
|
72
72
|
if (!_serpentReady())
|
|
73
73
|
throw new Error('leviathan-crypto: call init([\'serpent\']) before using SerpentStreamSealer');
|
|
74
74
|
if (!_sha2Ready())
|
|
@@ -80,6 +80,7 @@ export class SerpentStreamSealer {
|
|
|
80
80
|
throw new RangeError(`SerpentStreamSealer chunkSize must be ${CHUNK_MIN}..${CHUNK_MAX} (got ${cs})`);
|
|
81
81
|
this._key = key.slice();
|
|
82
82
|
this._cs = cs;
|
|
83
|
+
this._framed = opts?.framed ?? false;
|
|
83
84
|
this._nonce = new Uint8Array(16);
|
|
84
85
|
if (_nonce && _nonce.length === 16) {
|
|
85
86
|
this._nonce.set(_nonce);
|
|
@@ -138,7 +139,13 @@ export class SerpentStreamSealer {
|
|
|
138
139
|
const ciphertext = this._cbc.encrypt(encKey, iv, plaintext);
|
|
139
140
|
const tag = this._hmac.hash(macKey, concat(iv, ciphertext));
|
|
140
141
|
this._index++;
|
|
141
|
-
|
|
142
|
+
const sealed = concat(concat(iv, ciphertext), tag);
|
|
143
|
+
if (!this._framed)
|
|
144
|
+
return sealed;
|
|
145
|
+
const out = new Uint8Array(4 + sealed.length);
|
|
146
|
+
out.set(u32be(sealed.length), 0);
|
|
147
|
+
out.set(sealed, 4);
|
|
148
|
+
return out;
|
|
142
149
|
}
|
|
143
150
|
_wipe() {
|
|
144
151
|
wipe(this._key);
|
|
@@ -160,9 +167,13 @@ export class SerpentStreamOpener {
|
|
|
160
167
|
_cbc;
|
|
161
168
|
_hmac;
|
|
162
169
|
_hkdf;
|
|
170
|
+
_framed;
|
|
171
|
+
_buf;
|
|
172
|
+
_maxFrame;
|
|
173
|
+
_bufLen;
|
|
163
174
|
_index;
|
|
164
175
|
_dead;
|
|
165
|
-
constructor(key, header) {
|
|
176
|
+
constructor(key, header, opts) {
|
|
166
177
|
if (!_serpentReady())
|
|
167
178
|
throw new Error('leviathan-crypto: call init([\'serpent\']) before using SerpentStreamOpener');
|
|
168
179
|
if (!_sha2Ready())
|
|
@@ -174,11 +185,21 @@ export class SerpentStreamOpener {
|
|
|
174
185
|
this._key = key.slice();
|
|
175
186
|
this._nonce = header.slice(0, 16);
|
|
176
187
|
this._cs = (header[16] << 24 | header[17] << 16 | header[18] << 8 | header[19]) >>> 0;
|
|
188
|
+
if (this._cs < CHUNK_MIN || this._cs > CHUNK_MAX)
|
|
189
|
+
throw new RangeError(`SerpentStreamOpener: header contains invalid chunkSize ${this._cs} (expected ${CHUNK_MIN}..${CHUNK_MAX})`);
|
|
190
|
+
this._framed = opts?.framed ?? false;
|
|
177
191
|
this._cbc = new SerpentCbc({ dangerUnauthenticated: true });
|
|
178
192
|
this._hmac = new HMAC_SHA256();
|
|
179
193
|
this._hkdf = new HKDF_SHA256();
|
|
180
194
|
this._index = 0;
|
|
181
195
|
this._dead = false;
|
|
196
|
+
this._bufLen = 0;
|
|
197
|
+
if (this._framed) {
|
|
198
|
+
const cs = this._cs;
|
|
199
|
+
const maxSealed = 16 + (cs + (16 - (cs % 16))) + 32;
|
|
200
|
+
this._maxFrame = 4 + maxSealed;
|
|
201
|
+
this._buf = new Uint8Array(this._maxFrame);
|
|
202
|
+
}
|
|
182
203
|
}
|
|
183
204
|
get closed() {
|
|
184
205
|
return this._dead;
|
|
@@ -186,6 +207,11 @@ export class SerpentStreamOpener {
|
|
|
186
207
|
open(chunk) {
|
|
187
208
|
if (this._dead)
|
|
188
209
|
throw new Error('SerpentStreamOpener: stream is closed');
|
|
210
|
+
if (this._framed)
|
|
211
|
+
throw new Error('SerpentStreamOpener: call feed() on framed openers — open() expects raw sealed chunks without length prefix');
|
|
212
|
+
return this._openRaw(chunk);
|
|
213
|
+
}
|
|
214
|
+
_openRaw(chunk) {
|
|
189
215
|
// Try isLast = true first, then false.
|
|
190
216
|
// Whichever passes auth is the correct interpretation.
|
|
191
217
|
for (const isLast of [true, false]) {
|
|
@@ -208,12 +234,104 @@ export class SerpentStreamOpener {
|
|
|
208
234
|
}
|
|
209
235
|
throw new Error('SerpentStreamOpener: authentication failed');
|
|
210
236
|
}
|
|
237
|
+
feed(bytes) {
|
|
238
|
+
if (!this._framed)
|
|
239
|
+
throw new Error('SerpentStreamOpener: feed() requires { framed: true }');
|
|
240
|
+
if (this._dead)
|
|
241
|
+
throw new Error('SerpentStreamOpener: stream is closed');
|
|
242
|
+
const buf = this._buf;
|
|
243
|
+
const maxFrame = this._maxFrame;
|
|
244
|
+
const results = [];
|
|
245
|
+
let consumed = 0;
|
|
246
|
+
// ── Phase 1: drain carry-over ─────────────────────────────────────
|
|
247
|
+
if (this._bufLen > 0) {
|
|
248
|
+
// Sub-case A: partial prefix — we have < 4 bytes buffered
|
|
249
|
+
if (this._bufLen < 4) {
|
|
250
|
+
const need = 4 - this._bufLen;
|
|
251
|
+
const take = Math.min(need, bytes.length - consumed);
|
|
252
|
+
buf.set(bytes.subarray(consumed, consumed + take), this._bufLen);
|
|
253
|
+
this._bufLen += take;
|
|
254
|
+
consumed += take;
|
|
255
|
+
if (this._bufLen < 4)
|
|
256
|
+
return results;
|
|
257
|
+
}
|
|
258
|
+
const sealedLen = (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
|
|
259
|
+
if (sealedLen === 0 || sealedLen > (maxFrame - 4)) {
|
|
260
|
+
this._wipe();
|
|
261
|
+
throw new Error('SerpentStreamOpener: invalid sealed chunk length');
|
|
262
|
+
}
|
|
263
|
+
const frameLen = 4 + sealedLen;
|
|
264
|
+
// Sub-case B: partial frame body
|
|
265
|
+
const haveInBuf = this._bufLen;
|
|
266
|
+
const needMore = frameLen - haveInBuf;
|
|
267
|
+
if (needMore > 0) {
|
|
268
|
+
const take = Math.min(needMore, bytes.length - consumed);
|
|
269
|
+
buf.set(bytes.subarray(consumed, consumed + take), haveInBuf);
|
|
270
|
+
this._bufLen += take;
|
|
271
|
+
consumed += take;
|
|
272
|
+
if (this._bufLen < frameLen)
|
|
273
|
+
return results;
|
|
274
|
+
}
|
|
275
|
+
const plaintext = this._openRaw(buf.subarray(4, frameLen));
|
|
276
|
+
results.push(plaintext);
|
|
277
|
+
this._bufLen = 0;
|
|
278
|
+
if (this._dead) {
|
|
279
|
+
if (consumed < bytes.length) {
|
|
280
|
+
this._wipe();
|
|
281
|
+
throw new Error('SerpentStreamOpener: unexpected bytes after final chunk');
|
|
282
|
+
}
|
|
283
|
+
return results;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// ── Phase 2: parse complete frames directly from bytes ────────────
|
|
287
|
+
let pos = consumed;
|
|
288
|
+
while (true) {
|
|
289
|
+
if (bytes.length - pos < 4)
|
|
290
|
+
break;
|
|
291
|
+
const sealedLen = (bytes[pos] << 24 | bytes[pos + 1] << 16 | bytes[pos + 2] << 8 | bytes[pos + 3]) >>> 0;
|
|
292
|
+
if (sealedLen === 0 || sealedLen > (maxFrame - 4)) {
|
|
293
|
+
this._wipe();
|
|
294
|
+
throw new Error('SerpentStreamOpener: invalid sealed chunk length');
|
|
295
|
+
}
|
|
296
|
+
const frameLen = 4 + sealedLen;
|
|
297
|
+
if (bytes.length - pos < frameLen)
|
|
298
|
+
break;
|
|
299
|
+
const plaintext = this._openRaw(bytes.subarray(pos + 4, pos + frameLen));
|
|
300
|
+
results.push(plaintext);
|
|
301
|
+
if (this._dead) {
|
|
302
|
+
const remaining = bytes.length - pos - frameLen;
|
|
303
|
+
if (remaining > 0) {
|
|
304
|
+
this._wipe();
|
|
305
|
+
throw new Error('SerpentStreamOpener: unexpected bytes after final chunk');
|
|
306
|
+
}
|
|
307
|
+
return results;
|
|
308
|
+
}
|
|
309
|
+
pos += frameLen;
|
|
310
|
+
}
|
|
311
|
+
// ── Carry over any incomplete trailing bytes into _buf ────────────
|
|
312
|
+
const leftover = bytes.length - pos;
|
|
313
|
+
if (leftover > 0) {
|
|
314
|
+
if (leftover > maxFrame) {
|
|
315
|
+
this._wipe();
|
|
316
|
+
throw new Error('SerpentStreamOpener: input exceeds maximum frame size');
|
|
317
|
+
}
|
|
318
|
+
buf.set(bytes.subarray(pos), 0);
|
|
319
|
+
this._bufLen = leftover;
|
|
320
|
+
}
|
|
321
|
+
return results;
|
|
322
|
+
}
|
|
211
323
|
_wipe() {
|
|
324
|
+
if (this._dead)
|
|
325
|
+
return;
|
|
212
326
|
wipe(this._key);
|
|
213
327
|
wipe(this._nonce);
|
|
214
328
|
this._cbc.dispose();
|
|
215
329
|
this._hmac.dispose();
|
|
216
330
|
this._hkdf.dispose();
|
|
331
|
+
if (this._framed) {
|
|
332
|
+
wipe(this._buf);
|
|
333
|
+
this._bufLen = 0;
|
|
334
|
+
}
|
|
217
335
|
this._dead = true;
|
|
218
336
|
}
|
|
219
337
|
dispose() {
|
package/dist/serpent.wasm
CHANGED
|
Binary file
|
package/dist/sha2.wasm
CHANGED
|
Binary file
|
package/dist/sha3.wasm
CHANGED
|
Binary file
|
package/dist/utils.d.ts
CHANGED
|
@@ -24,3 +24,8 @@ export declare const xor: (a: Uint8Array, b: Uint8Array) => Uint8Array;
|
|
|
24
24
|
export declare const concat: (a: Uint8Array, b: Uint8Array) => Uint8Array;
|
|
25
25
|
/** Cryptographically secure random bytes via Web Crypto API. */
|
|
26
26
|
export declare const randomBytes: (n: number) => Uint8Array;
|
|
27
|
+
/**
|
|
28
|
+
* Detects WASM SIMD support once and caches the result.
|
|
29
|
+
* Gates CTR/CBC-decrypt dispatch in Serpent and encryptChunk dispatch in ChaCha20.
|
|
30
|
+
*/
|
|
31
|
+
export declare function hasSIMD(): boolean;
|
package/dist/utils.js
CHANGED
|
@@ -167,3 +167,28 @@ export const randomBytes = (n) => {
|
|
|
167
167
|
crypto.getRandomValues(buf);
|
|
168
168
|
return buf;
|
|
169
169
|
};
|
|
170
|
+
// ── SIMD detection ───────────────────────────────────────────────────────────
|
|
171
|
+
let _simd = null;
|
|
172
|
+
/**
|
|
173
|
+
* Detects WASM SIMD support once and caches the result.
|
|
174
|
+
* Gates CTR/CBC-decrypt dispatch in Serpent and encryptChunk dispatch in ChaCha20.
|
|
175
|
+
*/
|
|
176
|
+
export function hasSIMD() {
|
|
177
|
+
if (_simd !== null)
|
|
178
|
+
return _simd;
|
|
179
|
+
if (typeof WebAssembly === 'undefined' || typeof WebAssembly.validate !== 'function') {
|
|
180
|
+
_simd = false;
|
|
181
|
+
return _simd;
|
|
182
|
+
}
|
|
183
|
+
// Minimal WASM module using v128 — validates iff SIMD is supported
|
|
184
|
+
try {
|
|
185
|
+
_simd = WebAssembly.validate(new Uint8Array([
|
|
186
|
+
0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123,
|
|
187
|
+
3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11,
|
|
188
|
+
]));
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
_simd = false;
|
|
192
|
+
}
|
|
193
|
+
return _simd;
|
|
194
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leviathan-crypto",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"author": "xero (https://x-e.ro)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Zero-dependency WebAssembly cryptography library for TypeScript: Serpent-256, XChaCha20-Poly1305, SHA-2/3, HMAC, HKDF, and Fortuna CSPRNG, with a strictly typed API built on vector-verified primitives.",
|
|
@@ -1,20 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
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
|
-
}
|