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
package/dist/sha3/index.d.ts
CHANGED
|
@@ -26,13 +26,20 @@ export declare class SHA3_224 {
|
|
|
26
26
|
hash(msg: Uint8Array): Uint8Array;
|
|
27
27
|
dispose(): void;
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* SHAKE128 XOF — extendable output, multi-squeeze capable.
|
|
31
|
+
*
|
|
32
|
+
* Holds exclusive access to the `sha3` WASM module from construction until
|
|
33
|
+
* `dispose()`. Constructing a second SHAKE128/SHAKE256 or any other sha3
|
|
34
|
+
* user while this instance is live throws. Call `dispose()` when done.
|
|
35
|
+
*/
|
|
30
36
|
export declare class SHAKE128 {
|
|
31
37
|
private readonly x;
|
|
32
38
|
private readonly _rate;
|
|
33
39
|
private _squeezing;
|
|
34
40
|
private _block;
|
|
35
41
|
private _blockPos;
|
|
42
|
+
private _tok;
|
|
36
43
|
constructor();
|
|
37
44
|
reset(): this;
|
|
38
45
|
absorb(msg: Uint8Array): this;
|
|
@@ -40,13 +47,20 @@ export declare class SHAKE128 {
|
|
|
40
47
|
hash(msg: Uint8Array, outputLength: number): Uint8Array;
|
|
41
48
|
dispose(): void;
|
|
42
49
|
}
|
|
43
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* SHAKE256 XOF — extendable output, multi-squeeze capable.
|
|
52
|
+
*
|
|
53
|
+
* Holds exclusive access to the `sha3` WASM module from construction until
|
|
54
|
+
* `dispose()`. Constructing a second SHAKE128/SHAKE256 or any other sha3
|
|
55
|
+
* user while this instance is live throws. Call `dispose()` when done.
|
|
56
|
+
*/
|
|
44
57
|
export declare class SHAKE256 {
|
|
45
58
|
private readonly x;
|
|
46
59
|
private readonly _rate;
|
|
47
60
|
private _squeezing;
|
|
48
61
|
private _block;
|
|
49
62
|
private _blockPos;
|
|
63
|
+
private _tok;
|
|
50
64
|
constructor();
|
|
51
65
|
reset(): this;
|
|
52
66
|
absorb(msg: Uint8Array): this;
|
|
@@ -54,3 +68,4 @@ export declare class SHAKE256 {
|
|
|
54
68
|
hash(msg: Uint8Array, outputLength: number): Uint8Array;
|
|
55
69
|
dispose(): void;
|
|
56
70
|
}
|
|
71
|
+
export { SHA3_256Hash } from './hash.js';
|
package/dist/sha3/index.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
//
|
|
24
24
|
// Public API classes for the SHA-3 WASM module.
|
|
25
25
|
// Uses the init() module cache — call sha3Init(source) before constructing.
|
|
26
|
-
import { getInstance, initModule } from '../init.js';
|
|
26
|
+
import { getInstance, initModule, _acquireModule, _releaseModule, _assertNotOwned } from '../init.js';
|
|
27
27
|
export async function sha3Init(source) {
|
|
28
28
|
return initModule('sha3', source);
|
|
29
29
|
}
|
|
@@ -58,6 +58,7 @@ export class SHA3_256 {
|
|
|
58
58
|
this.x = getExports();
|
|
59
59
|
}
|
|
60
60
|
hash(msg) {
|
|
61
|
+
_assertNotOwned('sha3');
|
|
61
62
|
this.x.sha3_256Init();
|
|
62
63
|
absorb(this.x, msg);
|
|
63
64
|
this.x.sha3_256Final();
|
|
@@ -65,6 +66,7 @@ export class SHA3_256 {
|
|
|
65
66
|
return mem.slice(this.x.getOutOffset(), this.x.getOutOffset() + 32);
|
|
66
67
|
}
|
|
67
68
|
dispose() {
|
|
69
|
+
_assertNotOwned('sha3');
|
|
68
70
|
this.x.wipeBuffers();
|
|
69
71
|
}
|
|
70
72
|
}
|
|
@@ -75,6 +77,7 @@ export class SHA3_512 {
|
|
|
75
77
|
this.x = getExports();
|
|
76
78
|
}
|
|
77
79
|
hash(msg) {
|
|
80
|
+
_assertNotOwned('sha3');
|
|
78
81
|
this.x.sha3_512Init();
|
|
79
82
|
absorb(this.x, msg);
|
|
80
83
|
this.x.sha3_512Final();
|
|
@@ -82,6 +85,7 @@ export class SHA3_512 {
|
|
|
82
85
|
return mem.slice(this.x.getOutOffset(), this.x.getOutOffset() + 64);
|
|
83
86
|
}
|
|
84
87
|
dispose() {
|
|
88
|
+
_assertNotOwned('sha3');
|
|
85
89
|
this.x.wipeBuffers();
|
|
86
90
|
}
|
|
87
91
|
}
|
|
@@ -92,6 +96,7 @@ export class SHA3_384 {
|
|
|
92
96
|
this.x = getExports();
|
|
93
97
|
}
|
|
94
98
|
hash(msg) {
|
|
99
|
+
_assertNotOwned('sha3');
|
|
95
100
|
this.x.sha3_384Init();
|
|
96
101
|
absorb(this.x, msg);
|
|
97
102
|
this.x.sha3_384Final();
|
|
@@ -99,6 +104,7 @@ export class SHA3_384 {
|
|
|
99
104
|
return mem.slice(this.x.getOutOffset(), this.x.getOutOffset() + 48);
|
|
100
105
|
}
|
|
101
106
|
dispose() {
|
|
107
|
+
_assertNotOwned('sha3');
|
|
102
108
|
this.x.wipeBuffers();
|
|
103
109
|
}
|
|
104
110
|
}
|
|
@@ -109,6 +115,7 @@ export class SHA3_224 {
|
|
|
109
115
|
this.x = getExports();
|
|
110
116
|
}
|
|
111
117
|
hash(msg) {
|
|
118
|
+
_assertNotOwned('sha3');
|
|
112
119
|
this.x.sha3_224Init();
|
|
113
120
|
absorb(this.x, msg);
|
|
114
121
|
this.x.sha3_224Final();
|
|
@@ -116,22 +123,40 @@ export class SHA3_224 {
|
|
|
116
123
|
return mem.slice(this.x.getOutOffset(), this.x.getOutOffset() + 28);
|
|
117
124
|
}
|
|
118
125
|
dispose() {
|
|
126
|
+
_assertNotOwned('sha3');
|
|
119
127
|
this.x.wipeBuffers();
|
|
120
128
|
}
|
|
121
129
|
}
|
|
122
130
|
// ── SHAKE128 ────────────────────────────────────────────────────────────────
|
|
123
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* SHAKE128 XOF — extendable output, multi-squeeze capable.
|
|
133
|
+
*
|
|
134
|
+
* Holds exclusive access to the `sha3` WASM module from construction until
|
|
135
|
+
* `dispose()`. Constructing a second SHAKE128/SHAKE256 or any other sha3
|
|
136
|
+
* user while this instance is live throws. Call `dispose()` when done.
|
|
137
|
+
*/
|
|
124
138
|
export class SHAKE128 {
|
|
125
139
|
x;
|
|
126
140
|
_rate = 168;
|
|
127
141
|
_squeezing = false;
|
|
128
142
|
_block = new Uint8Array(168);
|
|
129
143
|
_blockPos = 168;
|
|
144
|
+
_tok;
|
|
130
145
|
constructor() {
|
|
131
146
|
this.x = getExports();
|
|
132
|
-
this.
|
|
147
|
+
this._tok = _acquireModule('sha3');
|
|
148
|
+
try {
|
|
149
|
+
this.x.shake128Init();
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
_releaseModule('sha3', this._tok);
|
|
153
|
+
this._tok = undefined;
|
|
154
|
+
throw e;
|
|
155
|
+
}
|
|
133
156
|
}
|
|
134
157
|
reset() {
|
|
158
|
+
if (this._tok === undefined)
|
|
159
|
+
throw new Error('SHAKE128: instance has been disposed');
|
|
135
160
|
this.x.shake128Init();
|
|
136
161
|
this._squeezing = false;
|
|
137
162
|
this._block.fill(0);
|
|
@@ -139,12 +164,16 @@ export class SHAKE128 {
|
|
|
139
164
|
return this;
|
|
140
165
|
}
|
|
141
166
|
absorb(msg) {
|
|
167
|
+
if (this._tok === undefined)
|
|
168
|
+
throw new Error('SHAKE128: instance has been disposed');
|
|
142
169
|
if (this._squeezing)
|
|
143
170
|
throw new Error('SHAKE128: cannot absorb after squeeze — call reset() first');
|
|
144
171
|
absorb(this.x, msg);
|
|
145
172
|
return this;
|
|
146
173
|
}
|
|
147
174
|
squeeze(n) {
|
|
175
|
+
if (this._tok === undefined)
|
|
176
|
+
throw new Error('SHAKE128: instance has been disposed');
|
|
148
177
|
if (n < 1)
|
|
149
178
|
throw new RangeError(`squeeze length must be >= 1 (got ${n})`);
|
|
150
179
|
if (!this._squeezing) {
|
|
@@ -170,6 +199,8 @@ export class SHAKE128 {
|
|
|
170
199
|
return out;
|
|
171
200
|
}
|
|
172
201
|
hash(msg, outputLength) {
|
|
202
|
+
if (this._tok === undefined)
|
|
203
|
+
throw new Error('SHAKE128: instance has been disposed');
|
|
173
204
|
if (outputLength < 1)
|
|
174
205
|
throw new RangeError(`outputLength must be >= 1 (got ${outputLength})`);
|
|
175
206
|
this.reset();
|
|
@@ -177,23 +208,48 @@ export class SHAKE128 {
|
|
|
177
208
|
return this.squeeze(outputLength);
|
|
178
209
|
}
|
|
179
210
|
dispose() {
|
|
211
|
+
if (this._tok === undefined)
|
|
212
|
+
return;
|
|
180
213
|
this._block.fill(0);
|
|
181
|
-
|
|
214
|
+
try {
|
|
215
|
+
this.x.wipeBuffers();
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
_releaseModule('sha3', this._tok);
|
|
219
|
+
this._tok = undefined;
|
|
220
|
+
}
|
|
182
221
|
}
|
|
183
222
|
}
|
|
184
223
|
// ── SHAKE256 ────────────────────────────────────────────────────────────────
|
|
185
|
-
/**
|
|
224
|
+
/**
|
|
225
|
+
* SHAKE256 XOF — extendable output, multi-squeeze capable.
|
|
226
|
+
*
|
|
227
|
+
* Holds exclusive access to the `sha3` WASM module from construction until
|
|
228
|
+
* `dispose()`. Constructing a second SHAKE128/SHAKE256 or any other sha3
|
|
229
|
+
* user while this instance is live throws. Call `dispose()` when done.
|
|
230
|
+
*/
|
|
186
231
|
export class SHAKE256 {
|
|
187
232
|
x;
|
|
188
233
|
_rate = 136;
|
|
189
234
|
_squeezing = false;
|
|
190
235
|
_block = new Uint8Array(136);
|
|
191
236
|
_blockPos = 136;
|
|
237
|
+
_tok;
|
|
192
238
|
constructor() {
|
|
193
239
|
this.x = getExports();
|
|
194
|
-
this.
|
|
240
|
+
this._tok = _acquireModule('sha3');
|
|
241
|
+
try {
|
|
242
|
+
this.x.shake256Init();
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
_releaseModule('sha3', this._tok);
|
|
246
|
+
this._tok = undefined;
|
|
247
|
+
throw e;
|
|
248
|
+
}
|
|
195
249
|
}
|
|
196
250
|
reset() {
|
|
251
|
+
if (this._tok === undefined)
|
|
252
|
+
throw new Error('SHAKE256: instance has been disposed');
|
|
197
253
|
this.x.shake256Init();
|
|
198
254
|
this._squeezing = false;
|
|
199
255
|
this._block.fill(0);
|
|
@@ -201,12 +257,16 @@ export class SHAKE256 {
|
|
|
201
257
|
return this;
|
|
202
258
|
}
|
|
203
259
|
absorb(msg) {
|
|
260
|
+
if (this._tok === undefined)
|
|
261
|
+
throw new Error('SHAKE256: instance has been disposed');
|
|
204
262
|
if (this._squeezing)
|
|
205
263
|
throw new Error('SHAKE256: cannot absorb after squeeze — call reset() first');
|
|
206
264
|
absorb(this.x, msg);
|
|
207
265
|
return this;
|
|
208
266
|
}
|
|
209
267
|
squeeze(n) {
|
|
268
|
+
if (this._tok === undefined)
|
|
269
|
+
throw new Error('SHAKE256: instance has been disposed');
|
|
210
270
|
if (n < 1)
|
|
211
271
|
throw new RangeError(`squeeze length must be >= 1 (got ${n})`);
|
|
212
272
|
if (!this._squeezing) {
|
|
@@ -232,6 +292,8 @@ export class SHAKE256 {
|
|
|
232
292
|
return out;
|
|
233
293
|
}
|
|
234
294
|
hash(msg, outputLength) {
|
|
295
|
+
if (this._tok === undefined)
|
|
296
|
+
throw new Error('SHAKE256: instance has been disposed');
|
|
235
297
|
if (outputLength < 1)
|
|
236
298
|
throw new RangeError(`outputLength must be >= 1 (got ${outputLength})`);
|
|
237
299
|
this.reset();
|
|
@@ -239,7 +301,17 @@ export class SHAKE256 {
|
|
|
239
301
|
return this.squeeze(outputLength);
|
|
240
302
|
}
|
|
241
303
|
dispose() {
|
|
304
|
+
if (this._tok === undefined)
|
|
305
|
+
return;
|
|
242
306
|
this._block.fill(0);
|
|
243
|
-
|
|
307
|
+
try {
|
|
308
|
+
this.x.wipeBuffers();
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
_releaseModule('sha3', this._tok);
|
|
312
|
+
this._tok = undefined;
|
|
313
|
+
}
|
|
244
314
|
}
|
|
245
315
|
}
|
|
316
|
+
// ── SHA3_256Hash ────────────────────────────────────────────────────────────
|
|
317
|
+
export { SHA3_256Hash } from './hash.js';
|
package/dist/stream/header.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// src/ts/stream/header.ts
|
|
23
23
|
//
|
|
24
24
|
// Wire format header encoding/decoding and counter nonce construction.
|
|
25
|
-
import { FLAG_FRAMED, HEADER_SIZE, TAG_DATA, TAG_FINAL } from './constants.js';
|
|
25
|
+
import { CHUNK_MAX, CHUNK_MIN, FLAG_FRAMED, HEADER_SIZE, TAG_DATA, TAG_FINAL } from './constants.js';
|
|
26
26
|
// The 16-byte nonce is a HKDF salt — not a direct cipher nonce.
|
|
27
27
|
// Both XChaCha20Cipher and SerpentCipher derive their actual key material
|
|
28
28
|
// and nonces from this value via HKDF-SHA-256. The 16-byte size is chosen
|
|
@@ -33,8 +33,8 @@ export function writeHeader(formatEnum, framed, nonce, chunkSize) {
|
|
|
33
33
|
throw new RangeError(`formatEnum must be an integer in [0, 0x3f] (got ${formatEnum})`);
|
|
34
34
|
if (nonce.length !== 16)
|
|
35
35
|
throw new RangeError(`nonce must be 16 bytes (got ${nonce.length})`);
|
|
36
|
-
if (!Number.isInteger(chunkSize) || chunkSize <
|
|
37
|
-
throw new RangeError(`chunkSize must be an integer in [
|
|
36
|
+
if (!Number.isInteger(chunkSize) || chunkSize < CHUNK_MIN || chunkSize > CHUNK_MAX)
|
|
37
|
+
throw new RangeError(`chunkSize must be an integer in [${CHUNK_MIN}, ${CHUNK_MAX}] (got ${chunkSize})`);
|
|
38
38
|
const h = new Uint8Array(HEADER_SIZE);
|
|
39
39
|
h[0] = (framed ? FLAG_FRAMED : 0) | formatEnum;
|
|
40
40
|
h.set(nonce, 1);
|
|
@@ -59,8 +59,8 @@ export function readHeader(header) {
|
|
|
59
59
|
}
|
|
60
60
|
/** 12-byte counter nonce: 11-byte BE counter + 1-byte final flag. */
|
|
61
61
|
export function makeCounterNonce(counter, finalFlag) {
|
|
62
|
-
if (!Number.
|
|
63
|
-
throw new RangeError(`counter must be
|
|
62
|
+
if (!Number.isSafeInteger(counter) || counter < 0)
|
|
63
|
+
throw new RangeError(`counter must be a safe integer in [0, ${Number.MAX_SAFE_INTEGER}]`);
|
|
64
64
|
if (finalFlag !== TAG_DATA && finalFlag !== TAG_FINAL)
|
|
65
65
|
throw new RangeError(`finalFlag must be TAG_DATA (0x00) or TAG_FINAL (0x01) (got 0x${finalFlag.toString(16).padStart(2, '0')})`);
|
|
66
66
|
const n = new Uint8Array(12);
|
|
@@ -65,42 +65,64 @@ export class OpenStream {
|
|
|
65
65
|
}
|
|
66
66
|
pull(chunk, opts) {
|
|
67
67
|
if (this.state !== 'ready')
|
|
68
|
-
throw new Error(
|
|
68
|
+
throw new Error(`OpenStream: cannot pull in state '${this.state}'`);
|
|
69
|
+
// Argument and wire-format validation runs before the crypto-failure
|
|
70
|
+
// try/catch so a malformed chunk throws without wiping keys or
|
|
71
|
+
// transitioning to 'failed'. The caller can retry with a corrected
|
|
72
|
+
// chunk. Symmetric with SealStream.push.
|
|
69
73
|
const data = this.framed ? this._stripFrame(chunk) : chunk;
|
|
70
74
|
if (data.length < this.cipher.tagSize)
|
|
71
75
|
throw new RangeError(`chunk too short to contain ${this.cipher.tagSize}-byte tag (got ${data.length} bytes)`);
|
|
72
76
|
if (data.length > this.maxWireChunk)
|
|
73
77
|
throw new RangeError(`chunk exceeds max wire size (${data.length} > ${this.maxWireChunk})`);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
try {
|
|
79
|
+
const nonce = makeCounterNonce(this.counter, TAG_DATA);
|
|
80
|
+
const plaintext = this.cipher.openChunk(this.keys, nonce, data, opts?.aad);
|
|
81
|
+
this.counter++;
|
|
82
|
+
return plaintext;
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
this.cipher.wipeKeys(this.keys);
|
|
86
|
+
this.state = 'failed';
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
78
89
|
}
|
|
79
90
|
finalize(chunk, opts) {
|
|
80
91
|
if (this.state !== 'ready')
|
|
81
|
-
throw new Error(
|
|
92
|
+
throw new Error(`OpenStream: cannot finalize in state '${this.state}'`);
|
|
82
93
|
const data = this.framed ? this._stripFrame(chunk) : chunk;
|
|
83
94
|
if (data.length < this.cipher.tagSize)
|
|
84
95
|
throw new RangeError(`chunk too short to contain ${this.cipher.tagSize}-byte tag (got ${data.length} bytes)`);
|
|
85
96
|
if (data.length > this.maxWireChunk)
|
|
86
97
|
throw new RangeError(`chunk exceeds max wire size (${data.length} > ${this.maxWireChunk})`);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
try {
|
|
99
|
+
const nonce = makeCounterNonce(this.counter, TAG_FINAL);
|
|
100
|
+
const plaintext = this.cipher.openChunk(this.keys, nonce, data, opts?.aad);
|
|
101
|
+
this.cipher.wipeKeys(this.keys);
|
|
102
|
+
this.state = 'finalized';
|
|
103
|
+
return plaintext;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
this.cipher.wipeKeys(this.keys);
|
|
107
|
+
this.state = 'failed';
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
92
110
|
}
|
|
93
111
|
dispose() {
|
|
94
112
|
if (this.state === 'ready') {
|
|
95
113
|
this.cipher.wipeKeys(this.keys);
|
|
96
114
|
this.state = 'finalized';
|
|
97
115
|
}
|
|
116
|
+
// 'failed' already wiped keys; 'finalized' already wiped keys — no-op.
|
|
98
117
|
}
|
|
99
118
|
seek(index) {
|
|
100
119
|
if (this.state !== 'ready')
|
|
101
|
-
throw new Error(
|
|
102
|
-
if (!Number.
|
|
103
|
-
throw new RangeError(`seek index must be a non-negative integer (got ${index})`);
|
|
120
|
+
throw new Error(`OpenStream: cannot seek in state '${this.state}'`);
|
|
121
|
+
if (!Number.isSafeInteger(index) || index < 0)
|
|
122
|
+
throw new RangeError(`seek index must be a non-negative safe integer ≤ Number.MAX_SAFE_INTEGER (got ${index})`);
|
|
123
|
+
if (index < this.counter)
|
|
124
|
+
throw new RangeError(`OpenStream: seek is forward-only — current counter ${this.counter}, requested ${index}. `
|
|
125
|
+
+ 'Backward seeks would permit plaintext replay; construct a new OpenStream to restart.');
|
|
104
126
|
this.counter = index;
|
|
105
127
|
}
|
|
106
128
|
_stripFrame(chunk) {
|
|
@@ -379,15 +379,14 @@ export class SealStreamPool {
|
|
|
379
379
|
reject(error);
|
|
380
380
|
this._pending.clear();
|
|
381
381
|
this._queue.length = 0;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
w.postMessage({ type: 'wipe' });
|
|
385
|
-
}
|
|
386
|
-
catch { /* worker may be terminated */ }
|
|
387
|
-
w.terminate();
|
|
388
|
-
}
|
|
389
|
-
this._workers.length = 0;
|
|
382
|
+
const workers = this._workers;
|
|
383
|
+
this._workers = [];
|
|
390
384
|
this._idle.length = 0;
|
|
385
|
+
// Fire-and-forget: wipe each worker's key material, then terminate.
|
|
386
|
+
// On timeout, terminate anyway — the main-thread key handles are
|
|
387
|
+
// wiped below so the owning surface no longer has access.
|
|
388
|
+
for (const w of workers)
|
|
389
|
+
this._wipeThenTerminate(w);
|
|
391
390
|
if (this._keys) {
|
|
392
391
|
wipe(this._keys.bytes);
|
|
393
392
|
this._keys = null;
|
|
@@ -397,4 +396,35 @@ export class SealStreamPool {
|
|
|
397
396
|
this._masterKey = null;
|
|
398
397
|
}
|
|
399
398
|
}
|
|
399
|
+
_wipeThenTerminate(w) {
|
|
400
|
+
const WIPE_ACK_TIMEOUT_MS = 100;
|
|
401
|
+
let done = false;
|
|
402
|
+
// prefer-const false positive: `finish` (defined below) closes over
|
|
403
|
+
// `t` and is invoked synchronously from the catch path before the
|
|
404
|
+
// `t = setTimeout(...)` assignment runs.
|
|
405
|
+
// eslint-disable-next-line prefer-const
|
|
406
|
+
let t;
|
|
407
|
+
const finish = () => {
|
|
408
|
+
if (done)
|
|
409
|
+
return;
|
|
410
|
+
done = true;
|
|
411
|
+
if (t !== undefined)
|
|
412
|
+
clearTimeout(t);
|
|
413
|
+
w.removeEventListener('message', onMsg);
|
|
414
|
+
w.terminate();
|
|
415
|
+
};
|
|
416
|
+
const onMsg = (e) => {
|
|
417
|
+
if (e.data && e.data.type === 'wiped')
|
|
418
|
+
finish();
|
|
419
|
+
};
|
|
420
|
+
w.addEventListener('message', onMsg);
|
|
421
|
+
try {
|
|
422
|
+
w.postMessage({ type: 'wipe' });
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
finish();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
t = setTimeout(finish, WIPE_ACK_TIMEOUT_MS);
|
|
429
|
+
}
|
|
400
430
|
}
|
|
@@ -80,30 +80,48 @@ export class SealStream {
|
|
|
80
80
|
}
|
|
81
81
|
push(chunk, opts) {
|
|
82
82
|
if (this.state !== 'ready')
|
|
83
|
-
throw new Error(
|
|
83
|
+
throw new Error(`SealStream: cannot push in state '${this.state}'`);
|
|
84
|
+
// Argument validation runs before the crypto-failure try/catch so a
|
|
85
|
+
// too-big chunk throws without wiping keys or transitioning to 'failed'.
|
|
86
|
+
// The caller can retry with a correctly-sized chunk.
|
|
84
87
|
if (chunk.length > this.chunkSize)
|
|
85
88
|
throw new RangeError(`chunk exceeds chunkSize (${chunk.length} > ${this.chunkSize})`);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
try {
|
|
90
|
+
const nonce = makeCounterNonce(this.counter, TAG_DATA);
|
|
91
|
+
const result = this.cipher.sealChunk(this.keys, nonce, chunk, opts?.aad);
|
|
92
|
+
this.counter++;
|
|
93
|
+
return this.framed ? concat(u32beFrame(result.length), result) : result;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
this.cipher.wipeKeys(this.keys);
|
|
97
|
+
this.state = 'failed';
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
90
100
|
}
|
|
91
101
|
finalize(chunk, opts) {
|
|
92
102
|
if (this.state !== 'ready')
|
|
93
|
-
throw new Error(
|
|
103
|
+
throw new Error(`SealStream: cannot finalize in state '${this.state}'`);
|
|
94
104
|
if (chunk.length > this.chunkSize)
|
|
95
105
|
throw new RangeError(`chunk exceeds chunkSize (${chunk.length} > ${this.chunkSize})`);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
try {
|
|
107
|
+
const nonce = makeCounterNonce(this.counter, TAG_FINAL);
|
|
108
|
+
const result = this.cipher.sealChunk(this.keys, nonce, chunk, opts?.aad);
|
|
109
|
+
this.cipher.wipeKeys(this.keys);
|
|
110
|
+
this.state = 'finalized';
|
|
111
|
+
return this.framed ? concat(u32beFrame(result.length), result) : result;
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
this.cipher.wipeKeys(this.keys);
|
|
115
|
+
this.state = 'failed';
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
101
118
|
}
|
|
102
119
|
dispose() {
|
|
103
120
|
if (this.state === 'ready') {
|
|
104
121
|
this.cipher.wipeKeys(this.keys);
|
|
105
122
|
this.state = 'finalized';
|
|
106
123
|
}
|
|
124
|
+
// 'failed' already wiped keys; 'finalized' already wiped keys — no-op.
|
|
107
125
|
}
|
|
108
126
|
toTransformStream() {
|
|
109
127
|
let headerSent = false;
|
package/dist/types.d.ts
CHANGED
|
@@ -22,3 +22,24 @@ export interface AEAD {
|
|
|
22
22
|
decrypt(ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
23
23
|
dispose(): void;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Stateless cipher PRF for Fortuna's generator slot. Produces `n` bytes of
|
|
27
|
+
* keystream from `(key, counter)` without mutating either input. Implementations
|
|
28
|
+
* are plain const objects, not classes.
|
|
29
|
+
*/
|
|
30
|
+
export interface Generator {
|
|
31
|
+
readonly keySize: number;
|
|
32
|
+
readonly blockSize: number;
|
|
33
|
+
readonly counterSize: number;
|
|
34
|
+
readonly wasmModules: readonly string[];
|
|
35
|
+
generate(key: Uint8Array, counter: Uint8Array, n: number): Uint8Array;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Stateless hash function for Fortuna's accumulator and reseed slots. Output
|
|
39
|
+
* size must match the generator's key size when paired in Fortuna.
|
|
40
|
+
*/
|
|
41
|
+
export interface HashFn {
|
|
42
|
+
readonly outputSize: number;
|
|
43
|
+
readonly wasmModules: readonly string[];
|
|
44
|
+
digest(msg: Uint8Array): Uint8Array;
|
|
45
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Hex string to Uint8Array. Accepts lowercase/uppercase, optional 0x prefix. Throws RangeError on odd-length input. */
|
|
1
|
+
/** Hex string to Uint8Array. Accepts lowercase/uppercase, optional 0x prefix. Throws RangeError on odd-length or non-hex input. */
|
|
2
2
|
export declare const hexToBytes: (hex: string) => Uint8Array;
|
|
3
3
|
/** Uint8Array to lowercase hex string. */
|
|
4
4
|
export declare const bytesToHex: (bytes: Uint8Array) => string;
|
|
@@ -6,18 +6,17 @@ export declare const bytesToHex: (bytes: Uint8Array) => string;
|
|
|
6
6
|
export declare const utf8ToBytes: (str: string) => Uint8Array;
|
|
7
7
|
/** Uint8Array to UTF-8 string. */
|
|
8
8
|
export declare const bytesToUtf8: (bytes: Uint8Array) => string;
|
|
9
|
-
/** Base64 or base64url string to Uint8Array. Handles padded, unpadded, and legacy %3d padding.
|
|
10
|
-
export declare const base64ToBytes: (b64: string) => Uint8Array
|
|
9
|
+
/** Base64 or base64url string to Uint8Array. Handles padded, unpadded, and legacy %3d padding. Throws RangeError on invalid input. */
|
|
10
|
+
export declare const base64ToBytes: (b64: string) => Uint8Array;
|
|
11
11
|
/** Uint8Array to base64 string. Pass url=true for base64url (RFC 4648 §5 — no padding characters). */
|
|
12
12
|
export declare const bytesToBase64: (bytes: Uint8Array, url?: boolean) => string;
|
|
13
13
|
export declare const CT_MAX_BYTES = 32768;
|
|
14
14
|
/**
|
|
15
15
|
* Constant-time byte-array equality.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* Max input size: 32768 bytes per side (enforced regardless of code path).
|
|
16
|
+
* Runs entirely inside a WASM SIMD module (v128 XOR accumulate with
|
|
17
|
+
* branch-free reduction). Throws on runtimes without SIMD support —
|
|
18
|
+
* no JS fallback. Length check is not constant-time (length is
|
|
19
|
+
* non-secret in all protocols). Max input size: 32768 bytes per side.
|
|
21
20
|
*/
|
|
22
21
|
export declare const constantTimeEqual: (a: Uint8Array, b: Uint8Array) => boolean;
|
|
23
22
|
/** Zero a typed array in place. */
|