spd-lib 1.3.5 → 1.3.6
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/README.md +158 -38
- package/index.d.mts +925 -19
- package/index.d.ts +925 -19
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/package.json +2 -1
- package/web.d.mts +111 -0
- package/web.mjs +195 -0
package/README.md
CHANGED
|
@@ -6,16 +6,41 @@ Encrypt any JavaScript value (strings, numbers, objects, typed arrays, Maps, Set
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## Security model
|
|
9
|
+
## Security model (v29)
|
|
10
10
|
|
|
11
|
-
| Layer | Algorithm |
|
|
11
|
+
| Layer | Algorithm | Notes |
|
|
12
12
|
|---|---|---|
|
|
13
13
|
| Key derivation | Argon2id (128 MiB, 6 iterations) | Raises brute-force cost against quantum search |
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
14
|
+
| Key expansion | HKDF-SHA3-512 (domain-separated) | Separate AEAD and MAC keys from one master secret |
|
|
15
|
+
| Encryption | XChaCha20-Poly1305 (256-bit key, per-entry) | 128-bit PQ security via Grover's algorithm |
|
|
16
|
+
| Key commitment | CMT-4: SHA3-256(key ∥ nonce) prefix | Prevents partitioning oracle / multi-key attacks |
|
|
17
|
+
| Authentication | HMAC-SHA3-512 (256-bit key, full-file) | 128-bit PQ security, domain-separated |
|
|
18
|
+
| Name encryption | XChaCha20-Poly1305 (dedicated name key) | Entry names are never stored in plaintext |
|
|
19
|
+
| Compression privacy | Pad-to-256B blocks before deflate | Mitigates CRIME/BREACH length-based oracle |
|
|
20
|
+
| Salt wrapping | Argon2id + XChaCha20-Poly1305 | Encrypts the KDF salt under the passcode |
|
|
21
|
+
|
|
22
|
+
**Key derivation chain:**
|
|
23
|
+
1. Argon2id(passcode, random 16-byte salt) → 96-byte master secret
|
|
24
|
+
2. HKDF-SHA3-512(master, `'spd-aead-key-v1'`) → 32-byte AEAD key
|
|
25
|
+
3. HKDF-SHA3-512(master, `'spd-mac-key-v1'`) → 64-byte MAC key
|
|
26
|
+
4. Per-entry key: HKDF-SHA3-512(AEAD key, entry name) → 32-byte entry key
|
|
17
27
|
|
|
18
|
-
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Performance (Node.js 22, Apple M3, standard profile)
|
|
31
|
+
|
|
32
|
+
| Operation | Throughput / Latency |
|
|
33
|
+
|---|---|
|
|
34
|
+
| Key derivation (Argon2id, 128 MiB) | ~380 ms (one-time per session) |
|
|
35
|
+
| addData ~36 B | ~10k ops/s |
|
|
36
|
+
| addData 10 KB | ~3k ops/s |
|
|
37
|
+
| addData 100 KB | ~750 ops/s |
|
|
38
|
+
| addData 1 MB | ~100 ops/s |
|
|
39
|
+
| saveToFileStreaming / loadFromFileStreaming 10×10 KB | ~400 ms (dominated by KDF) |
|
|
40
|
+
| getEntry (random-access lookup, binary file) | ~400 ms (dominated by KDF) |
|
|
41
|
+
| addMany 1000 items (parallel workers) | ~1–2 s |
|
|
42
|
+
|
|
43
|
+
The dominant cost in all file operations is the single Argon2id call (~380 ms). CMT-4 commitment, compression padding, HKDF, and name encryption add < 5 µs per entry.
|
|
19
44
|
|
|
20
45
|
---
|
|
21
46
|
|
|
@@ -41,13 +66,14 @@ await spd.addData('username', 'alice');
|
|
|
41
66
|
await spd.addData('apiKey', 'sk-abc123');
|
|
42
67
|
await spd.addData('config', { theme: 'dark', retries: 3 });
|
|
43
68
|
|
|
44
|
-
// Save to file
|
|
45
|
-
await spd.
|
|
69
|
+
// Save to file (binary streaming format — recommended)
|
|
70
|
+
await spd.saveToFileStreaming('./vault.spd', 'MyStr0ng!Passphrase#2024');
|
|
46
71
|
|
|
47
72
|
// Load from file
|
|
48
|
-
const loaded = await SPD.
|
|
73
|
+
const loaded = await SPD.loadFromFileStreaming('./vault.spd', 'MyStr0ng!Passphrase#2024');
|
|
49
74
|
const data = await loaded.extractData();
|
|
50
75
|
console.log(data.username); // 'alice'
|
|
76
|
+
loaded.destroy();
|
|
51
77
|
```
|
|
52
78
|
|
|
53
79
|
---
|
|
@@ -62,9 +88,9 @@ Creates a new empty SPD instance. You must call `setPassKey()` before adding or
|
|
|
62
88
|
|
|
63
89
|
### `spd.setPassKey(passcode: string): Promise<void>`
|
|
64
90
|
|
|
65
|
-
Derives encryption and authentication keys from the passcode using Argon2id. Must be called before any data operation.
|
|
91
|
+
Derives encryption and authentication keys from the passcode using Argon2id + HKDF. Must be called before any data operation.
|
|
66
92
|
|
|
67
|
-
**Passcode requirements:** minimum 12 characters, must contain at least 3 of: lowercase, uppercase, digits, special characters.
|
|
93
|
+
**Passcode requirements:** minimum 12 characters, must contain at least 3 of: lowercase, uppercase, digits, special characters. Alternatively, a passphrase of 5+ space-separated lowercase words (e.g. from `generatePassphrase()`) is accepted.
|
|
68
94
|
|
|
69
95
|
```ts
|
|
70
96
|
await spd.setPassKey('MyStr0ng!Passphrase#2024');
|
|
@@ -72,9 +98,36 @@ await spd.setPassKey('MyStr0ng!Passphrase#2024');
|
|
|
72
98
|
|
|
73
99
|
---
|
|
74
100
|
|
|
101
|
+
### `spd.setKeyProfile(profile: 'standard' | 'paranoid'): void`
|
|
102
|
+
|
|
103
|
+
Sets the Argon2id memory and time parameters before calling `setPassKey`.
|
|
104
|
+
|
|
105
|
+
| Profile | Memory | Iterations | Use case |
|
|
106
|
+
|---|---|---|---|
|
|
107
|
+
| `'standard'` | 128 MiB | 6 | Default — interactive applications |
|
|
108
|
+
| `'paranoid'` | 2048 MiB | 16 | Long-term archive, server-side, offline |
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
spd.setKeyProfile('standard'); // default
|
|
112
|
+
spd.setKeyProfile('paranoid'); // maximum resistance
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### `SPD.generatePassphrase(wordCount?: number): string`
|
|
118
|
+
|
|
119
|
+
Generates a cryptographically random space-separated word passphrase using the EFF large wordlist. Default is 7 words.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const pass = SPD.generatePassphrase(); // 7 words, ~56 bits entropy
|
|
123
|
+
const pass = SPD.generatePassphrase(10); // 10 words, ~80 bits entropy
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
75
128
|
### `spd.addData(name: string, value: unknown): Promise<void>`
|
|
76
129
|
|
|
77
|
-
Encrypts and stores a single value. The name is sanitized (lowercased, non-alphanumeric chars replaced with `_`).
|
|
130
|
+
Encrypts and stores a single value. The name is sanitized (lowercased, non-alphanumeric chars replaced with `_`). Entry names are encrypted and never stored in plaintext (v29+).
|
|
78
131
|
|
|
79
132
|
**Supported types:** `string`, `number`, `boolean`, `null`, `object`, `Array`, `Uint8Array`, `Uint16Array`, `Uint32Array`, `BigInt64Array`, `BigUint64Array`, `Float32Array`, `Float64Array`, `Map`, `Set`, `Date`, `RegExp`, `Error`
|
|
80
133
|
|
|
@@ -90,7 +143,7 @@ await spd.addData('lookup', new Map([['key', 'val']]));
|
|
|
90
143
|
|
|
91
144
|
### `spd.addMany(items: { name: string; data: unknown }[]): Promise<void>`
|
|
92
145
|
|
|
93
|
-
Batch version of `addData`.
|
|
146
|
+
Batch version of `addData`. Large batches (≥ 256 items) are encrypted in parallel using worker threads.
|
|
94
147
|
|
|
95
148
|
```ts
|
|
96
149
|
await spd.addMany([
|
|
@@ -113,34 +166,38 @@ console.log(data.firstName); // 'Alice'
|
|
|
113
166
|
|
|
114
167
|
---
|
|
115
168
|
|
|
116
|
-
### `spd.
|
|
169
|
+
### `spd.saveToFileStreaming(path: string, passcode: string): Promise<void>`
|
|
170
|
+
### `SPD.loadFromFileStreaming(path: string, passcode: string): Promise<SPD>`
|
|
117
171
|
|
|
118
|
-
|
|
172
|
+
**Recommended for all file I/O.** Writes/reads the binary SPD wire format. Heap usage stays bounded regardless of file size. Required format for `getEntry` random-access lookups.
|
|
173
|
+
|
|
174
|
+
The file embeds an offset index tail (`SPDx`) for O(1) entry lookups without a sidecar file.
|
|
119
175
|
|
|
120
176
|
```ts
|
|
121
|
-
await spd.
|
|
177
|
+
await spd.saveToFileStreaming('./large.spd', 'MyStr0ng!Passphrase#2024');
|
|
178
|
+
const spd = await SPD.loadFromFileStreaming('./large.spd', 'MyStr0ng!Passphrase#2024');
|
|
122
179
|
```
|
|
123
180
|
|
|
124
181
|
---
|
|
125
182
|
|
|
183
|
+
### `spd.saveToFile(path: string, passcode: string): Promise<void>`
|
|
126
184
|
### `SPD.loadFromFile(path: string, passcode: string): Promise<SPD>`
|
|
127
185
|
|
|
128
|
-
|
|
186
|
+
JSON-envelope file format. Suitable for files up to ~1–2 GB. Not compatible with `getEntry`.
|
|
129
187
|
|
|
130
188
|
```ts
|
|
189
|
+
await spd.saveToFile('./secrets.spd', 'MyStr0ng!Passphrase#2024');
|
|
131
190
|
const spd = await SPD.loadFromFile('./secrets.spd', 'MyStr0ng!Passphrase#2024');
|
|
132
191
|
```
|
|
133
192
|
|
|
134
193
|
---
|
|
135
194
|
|
|
136
|
-
### `
|
|
137
|
-
### `SPD.loadFromFileStreaming(path: string, passcode: string): Promise<SPD>`
|
|
195
|
+
### `SPD.getEntry(filePath: string, passcode: string, entryName: string): Promise<unknown>`
|
|
138
196
|
|
|
139
|
-
|
|
197
|
+
Random-access lookup of a single entry from a binary SPD file (written by `saveToFileStreaming`). Uses the embedded `SPDx` index tail to jump directly to the entry — no full scan. MAC-verified before decryption.
|
|
140
198
|
|
|
141
199
|
```ts
|
|
142
|
-
await
|
|
143
|
-
const spd = await SPD.loadFromFileStreaming('./large.spd', 'MyStr0ng!Passphrase#2024');
|
|
200
|
+
const value = await SPD.getEntry('./vault.spd', 'MyStr0ng!Passphrase#2024', 'username');
|
|
144
201
|
```
|
|
145
202
|
|
|
146
203
|
---
|
|
@@ -173,12 +230,6 @@ await spd.extractDataStreaming('/tmp/spd_scratch', async (name, value) => {
|
|
|
173
230
|
|
|
174
231
|
True constant-memory extraction from a binary `.spd` file (written by `saveToFileStreaming`). Processes one entry at a time using `.ssf` skeleton files — the full plaintext never lives in RAM.
|
|
175
232
|
|
|
176
|
-
Steps per entry:
|
|
177
|
-
1. Writes encrypted blob to `<name>_enc.ssf`
|
|
178
|
-
2. Decrypts into `<name>_dec.ssf`
|
|
179
|
-
3. Reads the `.ssf` file and calls your callback
|
|
180
|
-
4. Deletes both `.ssf` files before the next entry
|
|
181
|
-
|
|
182
233
|
Peak RAM usage is proportional to the largest single entry, not the file size.
|
|
183
234
|
|
|
184
235
|
```ts
|
|
@@ -232,7 +283,7 @@ const spd = await SPD.loadFromChunks(chunks, 'MyStr0ng!Passphrase#2024');
|
|
|
232
283
|
|
|
233
284
|
The last element of the array is always a JSON manifest (`{ totalChunks, chunkSize, totalBytes, version }`). The receiver validates chunk count and byte count before decrypting.
|
|
234
285
|
|
|
235
|
-
**Auto chunk size** (when `chunkSize` is omitted): targets ~32 chunks, clamped between 64 KB and 8 MB, rounded to the nearest 64 KB boundary.
|
|
286
|
+
**Auto chunk size** (when `chunkSize` is omitted): targets ~32 chunks, clamped between 64 KB and 8 MB, rounded to the nearest 64 KB boundary.
|
|
236
287
|
|
|
237
288
|
---
|
|
238
289
|
|
|
@@ -258,14 +309,41 @@ spd.setHash('sha3-512'); // default, recommended
|
|
|
258
309
|
|
|
259
310
|
### `spd.setCompressionLevel(level: number): void`
|
|
260
311
|
|
|
261
|
-
Sets the zlib deflate compression level (1–9). Default is `
|
|
312
|
+
Sets the zlib deflate compression level (1–9). Default is `6` (balanced speed/size).
|
|
262
313
|
|
|
263
314
|
```ts
|
|
264
|
-
spd.setCompressionLevel(
|
|
315
|
+
spd.setCompressionLevel(9); // maximum compression
|
|
316
|
+
spd.setCompressionLevel(1); // fastest, largest output
|
|
265
317
|
```
|
|
266
318
|
|
|
267
319
|
---
|
|
268
320
|
|
|
321
|
+
### `spd.setSigningKey(privateKeyPem: string): void` / `spd.setVerifyKey(publicKeyPem: string): void`
|
|
322
|
+
|
|
323
|
+
Attach an Ed25519 signing key to the session. When set, `saveToFile` / `saveData` / `saveToFileStreaming` append an Ed25519 signature over the entire payload, and `loadFromFile` / `loadFromString` / `loadFromFileStreaming` verify it before decryption.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
|
|
327
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
328
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
spd.setSigningKey(privateKey);
|
|
332
|
+
await spd.saveToFileStreaming('./signed.spd', PASS);
|
|
333
|
+
|
|
334
|
+
const loaded = await SPD.loadFromFileStreaming('./signed.spd', PASS);
|
|
335
|
+
loaded.setVerifyKey(publicKey);
|
|
336
|
+
const data = await loaded.extractData(); // throws if signature invalid
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
### `spd.setEpochSize(bytes: number): void`
|
|
342
|
+
|
|
343
|
+
Controls the epoch size (in bytes) for the key-ratcheting mechanism used in chunked exports. Defaults to 4 MB. Smaller values ratchet keys more frequently (higher security, lower throughput).
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
269
347
|
### `spd.destroy()` / `spd.clearCache()`
|
|
270
348
|
|
|
271
349
|
Securely zeros all key material in place using `sodium.memzero()`, then clears all stored data. Call this when you are done with a session.
|
|
@@ -276,9 +354,35 @@ spd.destroy();
|
|
|
276
354
|
|
|
277
355
|
---
|
|
278
356
|
|
|
279
|
-
|
|
357
|
+
## SPDWriter — streaming disk-backed writer
|
|
358
|
+
|
|
359
|
+
`SPDWriter` encrypts and writes entries one at a time to disk as they are produced — the full plaintext never exists in RAM simultaneously. Suitable for constructing large SPD files from a stream of records.
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
import { SPDWriter } from 'spd-lib-ts';
|
|
363
|
+
|
|
364
|
+
const writer = new SPDWriter('./output.spd', 'MyStr0ng!Passphrase#2024');
|
|
365
|
+
await writer.init();
|
|
280
366
|
|
|
281
|
-
|
|
367
|
+
await writer.addEntry('username', 'alice');
|
|
368
|
+
await writer.addEntry('config', { theme: 'dark' });
|
|
369
|
+
// ... add as many entries as needed
|
|
370
|
+
|
|
371
|
+
await writer.finalize();
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
After `finalize()`, the file contains an embedded `SPDx` index tail compatible with `SPD.getEntry()` — no sidecar file needed.
|
|
375
|
+
|
|
376
|
+
**Options** (passed as third argument to constructor):
|
|
377
|
+
|
|
378
|
+
| Option | Default | Description |
|
|
379
|
+
|---|---|---|
|
|
380
|
+
| `compressionLevel` | `9` | zlib deflate level |
|
|
381
|
+
| `hashAlgorithm` | `'sha3-512'` | Per-entry hash |
|
|
382
|
+
| `argon2Memory` | `131072` (128 MiB) | Argon2id memory cost |
|
|
383
|
+
| `argon2Time` | `6` | Argon2id time cost |
|
|
384
|
+
|
|
385
|
+
Call `writer.destroy()` to zero keys and delete the partial file if `finalize()` was never called.
|
|
282
386
|
|
|
283
387
|
---
|
|
284
388
|
|
|
@@ -343,12 +447,12 @@ async function saveConfig(config: Record<string, unknown>) {
|
|
|
343
447
|
const spd = new SPD();
|
|
344
448
|
await spd.setPassKey(PASS);
|
|
345
449
|
await spd.addMany(Object.entries(config).map(([name, data]) => ({ name, data })));
|
|
346
|
-
await spd.
|
|
450
|
+
await spd.saveToFileStreaming(FILE, PASS);
|
|
347
451
|
spd.destroy();
|
|
348
452
|
}
|
|
349
453
|
|
|
350
454
|
async function loadConfig(): Promise<Record<string, unknown>> {
|
|
351
|
-
const spd = await SPD.
|
|
455
|
+
const spd = await SPD.loadFromFileStreaming(FILE, PASS);
|
|
352
456
|
const data = await spd.extractData();
|
|
353
457
|
spd.destroy();
|
|
354
458
|
return data;
|
|
@@ -360,7 +464,7 @@ async function loadConfig(): Promise<Record<string, unknown>> {
|
|
|
360
464
|
## Full example — chunked HTTP upload
|
|
361
465
|
|
|
362
466
|
```ts
|
|
363
|
-
//
|
|
467
|
+
// sender:
|
|
364
468
|
const spd = new SPD();
|
|
365
469
|
await spd.setPassKey(PASS);
|
|
366
470
|
await spd.addData('payload', largeObject);
|
|
@@ -374,7 +478,7 @@ for (let i = 0; i < chunks.length; i++) {
|
|
|
374
478
|
});
|
|
375
479
|
}
|
|
376
480
|
|
|
377
|
-
// receiver
|
|
481
|
+
// receiver:
|
|
378
482
|
const spd = await SPD.loadFromChunks(receivedChunks, PASS);
|
|
379
483
|
const data = await spd.extractData();
|
|
380
484
|
spd.destroy();
|
|
@@ -386,11 +490,27 @@ spd.destroy();
|
|
|
386
490
|
|
|
387
491
|
| npm version | SPD format version | Notes |
|
|
388
492
|
|---|---|---|
|
|
493
|
+
| 1.4.0 | v29 | CMT-4 key commitment, HKDF key expansion, encrypted entry names, 256-byte compression padding |
|
|
389
494
|
| 1.3.1 | v26 | Hash algo + Argon2 params embedded in payload, secure key zeroing, `null` type support |
|
|
390
495
|
| 1.3.0 | v25 | 512-bit Argon2id master secret, domain-separated AEAD + HMAC keys, HMAC-SHA3-512 |
|
|
391
496
|
| < 1.3.0 | v24 | 256-bit Argon2id, single key for AEAD + MAC |
|
|
392
497
|
|
|
393
|
-
SPD format versions are not cross-compatible. Use `SPDLegacy` to read v24 and earlier files.
|
|
498
|
+
SPD format versions are not cross-compatible. v29 files cannot be read by older library versions. Use `SPDLegacy` to read v24 and earlier files.
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## Wire format (v29 binary)
|
|
503
|
+
|
|
504
|
+
Each entry in the binary file uses the following layout (all integers little-endian):
|
|
505
|
+
|
|
506
|
+
```
|
|
507
|
+
[2B encNameLen][encName][1B typeLen][type][2B hashLen][hash][3B nonceLen][nonce][8B dataLen][data]
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
- `encName` = `[24B XChaCha20 nonce || CMT-4 ciphertext of entry name]`
|
|
511
|
+
- `data` = `[32B CMT block || XChaCha20-Poly1305 ciphertext of padded+compressed plaintext]`
|
|
512
|
+
|
|
513
|
+
The full file is zlib-deflated and HMAC-SHA3-512 authenticated. A 72-byte header stores the plaintext length and MAC. An `SPDx` index tail is appended after the compressed stream for O(1) lookups.
|
|
394
514
|
|
|
395
515
|
---
|
|
396
516
|
|