spd-lib 1.3.1 → 1.3.3

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 ADDED
@@ -0,0 +1,396 @@
1
+ # spd-lib-ts
2
+
3
+ **SPD (Secure Packaged Data)** — a post-quantum-hardened encrypted data format for Node.js.
4
+
5
+ Encrypt any JavaScript value (strings, numbers, objects, typed arrays, Maps, Sets, Dates, etc.) into a compressed, authenticated, tamper-proof container. Supports file storage, base64 string transport, chunked internet transfer, and streaming I/O for files larger than 2 GB.
6
+
7
+ ---
8
+
9
+ ## Security model
10
+
11
+ | Layer | Algorithm | Post-quantum strength |
12
+ |---|---|---|
13
+ | Key derivation | Argon2id (128 MiB, 6 iterations) | Raises brute-force cost against quantum search |
14
+ | Encryption | XChaCha20-Poly1305 (256-bit key) | 128-bit PQ security via Grover's algorithm |
15
+ | Authentication | HMAC-SHA3-512 (256-bit key, domain-separated) | 128-bit PQ security |
16
+ | Salt wrapping | Argon2id-derived key + XChaCha20-Poly1305 | Same as above |
17
+
18
+ Keys are domain-separated: the 512-bit Argon2id master secret is split — the first 32 bytes go to encryption, the last 32 bytes go to authentication. They are never used interchangeably.
19
+
20
+ ---
21
+
22
+ ## Install
23
+
24
+ ```sh
25
+ npm install spd-lib-ts
26
+ ```
27
+
28
+ > **Requires Node.js ≥ 18** and a C++ build toolchain for the `argon2` native module.
29
+
30
+ ---
31
+
32
+ ## Quick start
33
+
34
+ ```ts
35
+ import { SPD } from 'spd-lib-ts';
36
+
37
+ const spd = new SPD();
38
+ await spd.setPassKey('MyStr0ng!Passphrase#2024');
39
+
40
+ await spd.addData('username', 'alice');
41
+ await spd.addData('apiKey', 'sk-abc123');
42
+ await spd.addData('config', { theme: 'dark', retries: 3 });
43
+
44
+ // Save to file
45
+ await spd.saveToFile('./vault.spd', 'MyStr0ng!Passphrase#2024');
46
+
47
+ // Load from file
48
+ const loaded = await SPD.loadFromFile('./vault.spd', 'MyStr0ng!Passphrase#2024');
49
+ const data = await loaded.extractData();
50
+ console.log(data.username); // 'alice'
51
+ ```
52
+
53
+ ---
54
+
55
+ ## API reference
56
+
57
+ ### `new SPD()`
58
+
59
+ Creates a new empty SPD instance. You must call `setPassKey()` before adding or saving data.
60
+
61
+ ---
62
+
63
+ ### `spd.setPassKey(passcode: string): Promise<void>`
64
+
65
+ Derives encryption and authentication keys from the passcode using Argon2id. Must be called before any data operation.
66
+
67
+ **Passcode requirements:** minimum 12 characters, must contain at least 3 of: lowercase, uppercase, digits, special characters.
68
+
69
+ ```ts
70
+ await spd.setPassKey('MyStr0ng!Passphrase#2024');
71
+ ```
72
+
73
+ ---
74
+
75
+ ### `spd.addData(name: string, value: unknown): Promise<void>`
76
+
77
+ Encrypts and stores a single value. The name is sanitized (lowercased, non-alphanumeric chars replaced with `_`).
78
+
79
+ **Supported types:** `string`, `number`, `boolean`, `null`, `object`, `Array`, `Uint8Array`, `Uint16Array`, `Uint32Array`, `BigInt64Array`, `BigUint64Array`, `Float32Array`, `Float64Array`, `Map`, `Set`, `Date`, `RegExp`, `Error`
80
+
81
+ ```ts
82
+ await spd.addData('score', 42);
83
+ await spd.addData('tags', ['a', 'b', 'c']);
84
+ await spd.addData('created', new Date());
85
+ await spd.addData('raw', new Uint8Array([1, 2, 3]));
86
+ await spd.addData('lookup', new Map([['key', 'val']]));
87
+ ```
88
+
89
+ ---
90
+
91
+ ### `spd.addMany(items: { name: string; data: unknown }[]): Promise<void>`
92
+
93
+ Batch version of `addData`. All items are encrypted in parallel.
94
+
95
+ ```ts
96
+ await spd.addMany([
97
+ { name: 'firstName', data: 'Alice' },
98
+ { name: 'age', data: 30 },
99
+ { name: 'prefs', data: { darkMode: true } },
100
+ ]);
101
+ ```
102
+
103
+ ---
104
+
105
+ ### `spd.extractData(): Promise<Record<string, unknown>>`
106
+
107
+ Decrypts and returns all stored entries as a plain object.
108
+
109
+ ```ts
110
+ const data = await spd.extractData();
111
+ console.log(data.firstName); // 'Alice'
112
+ ```
113
+
114
+ ---
115
+
116
+ ### `spd.saveToFile(path: string, passcode: string): Promise<void>`
117
+
118
+ Saves the encrypted payload to a file (mode `0600`). Suitable for files up to ~1–2 GB.
119
+
120
+ ```ts
121
+ await spd.saveToFile('./secrets.spd', 'MyStr0ng!Passphrase#2024');
122
+ ```
123
+
124
+ ---
125
+
126
+ ### `SPD.loadFromFile(path: string, passcode: string): Promise<SPD>`
127
+
128
+ Loads and verifies an SPD file. The hash algorithm and Argon2 parameters are read from the file itself — no need to pass them.
129
+
130
+ ```ts
131
+ const spd = await SPD.loadFromFile('./secrets.spd', 'MyStr0ng!Passphrase#2024');
132
+ ```
133
+
134
+ ---
135
+
136
+ ### `spd.saveToFileStreaming(path: string, passcode: string): Promise<void>`
137
+ ### `SPD.loadFromFileStreaming(path: string, passcode: string): Promise<SPD>`
138
+
139
+ Streaming variants for files larger than 2 GB. Uses a binary wire format with 64 MB write chunks — heap usage stays bounded regardless of file size.
140
+
141
+ ```ts
142
+ await spd.saveToFileStreaming('./large.spd', 'MyStr0ng!Passphrase#2024');
143
+ const spd = await SPD.loadFromFileStreaming('./large.spd', 'MyStr0ng!Passphrase#2024');
144
+ ```
145
+
146
+ ---
147
+
148
+ ### `spd.extractDataStreaming(tmpDir: string, callback): Promise<void>`
149
+
150
+ Memory-efficient extraction using `.ssf` skeleton files. For each encrypted entry:
151
+
152
+ 1. Decrypts + decompresses the entry into `<tmpDir>/<name>.ssf` (mode `0600`)
153
+ 2. Reads the `.ssf` file and calls your callback with the name and value
154
+ 3. Deletes the `.ssf` file before moving to the next entry
155
+
156
+ At most one entry's bytes exist on disk or in RAM at a time.
157
+
158
+ ```ts
159
+ import * as fs from 'fs';
160
+
161
+ fs.mkdirSync('/tmp/spd_scratch', { recursive: true });
162
+
163
+ await spd.extractDataStreaming('/tmp/spd_scratch', async (name, value) => {
164
+ console.log(name, value);
165
+ });
166
+ ```
167
+
168
+ > `tmpDir` must exist before calling. Mode `0700` is recommended.
169
+
170
+ ---
171
+
172
+ ### `SPD.extractFromFileStreaming(filePath, passcode, tmpDir, callback): Promise<void>`
173
+
174
+ 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
+
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
+ Peak RAM usage is proportional to the largest single entry, not the file size.
183
+
184
+ ```ts
185
+ await SPD.extractFromFileStreaming(
186
+ './huge.spd',
187
+ 'MyStr0ng!Passphrase#2024',
188
+ '/tmp/spd_scratch',
189
+ async (name, value) => {
190
+ console.log(name, typeof value);
191
+ }
192
+ );
193
+ ```
194
+
195
+ ---
196
+
197
+ ### `spd.saveData(passcode: string): Promise<string>`
198
+
199
+ Serializes the payload to a base64 string for in-memory or network transport.
200
+
201
+ ```ts
202
+ const b64 = await spd.saveData('MyStr0ng!Passphrase#2024');
203
+ ```
204
+
205
+ ---
206
+
207
+ ### `SPD.loadFromString(data: string, passcode: string): Promise<SPD>`
208
+
209
+ Loads from a base64 string produced by `saveData`.
210
+
211
+ ```ts
212
+ const spd = await SPD.loadFromString(b64, 'MyStr0ng!Passphrase#2024');
213
+ ```
214
+
215
+ ---
216
+
217
+ ### Chunked internet transfer
218
+
219
+ Split a payload into chunks for HTTP uploads or any transport with a body-size limit.
220
+
221
+ ```ts
222
+ // Sender
223
+ const chunks = await spd.saveDataChunked('MyStr0ng!Passphrase#2024', 512 * 1024); // 512 KB chunks
224
+ // chunks is string[] — send each element, then the manifest (last element) last
225
+
226
+ // Receiver — pass all chunks in order including the manifest
227
+ const spd = await SPD.loadFromChunks(chunks, 'MyStr0ng!Passphrase#2024');
228
+ ```
229
+
230
+ 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.
231
+
232
+ Default chunk size is **1 MB**. Tune down for strict API body limits.
233
+
234
+ ---
235
+
236
+ ### `spd.changePasscode(oldPasscode: string, newPasscode: string): Promise<void>`
237
+
238
+ Re-derives keys under the new passcode and re-encrypts all data. Verifies the old passcode via MAC before doing anything.
239
+
240
+ ```ts
241
+ await spd.changePasscode('MyStr0ng!Passphrase#2024', 'EvenStr0nger!Pass#9999');
242
+ ```
243
+
244
+ ---
245
+
246
+ ### `spd.setHash(hash: 'sha3-512' | 'sha256' | 'sha512'): void`
247
+
248
+ Sets the hash algorithm used for per-entry integrity checks. Default is `'sha3-512'`. Must be set before adding data. The chosen algorithm is embedded in the payload and restored automatically on load.
249
+
250
+ ```ts
251
+ spd.setHash('sha3-512'); // default, recommended
252
+ ```
253
+
254
+ ---
255
+
256
+ ### `spd.setCompressionLevel(level: number): void`
257
+
258
+ Sets the zlib deflate compression level (1–9). Default is `9` (maximum compression).
259
+
260
+ ```ts
261
+ spd.setCompressionLevel(6); // faster, slightly larger
262
+ ```
263
+
264
+ ---
265
+
266
+ ### `spd.destroy()` / `spd.clearCache()`
267
+
268
+ Securely zeros all key material in place using `sodium.memzero()`, then clears all stored data. Call this when you are done with a session.
269
+
270
+ ```ts
271
+ spd.destroy();
272
+ ```
273
+
274
+ ---
275
+
276
+ ### `SPD.deriveKeys(passcode, salt)` / `SPD.derivePBK(passcode, salt)`
277
+
278
+ Low-level static helpers exposed for advanced use cases (e.g. deriving keys outside of an SPD container).
279
+
280
+ ---
281
+
282
+ ## SPDVault — in-memory key vault
283
+
284
+ `SPDVault` is a time-limited in-memory store for passcodes or keys. Keys expire automatically after a configurable timeout and are cleared on access renewal.
285
+
286
+ ```ts
287
+ import { SPDVault } from 'spd-lib-ts';
288
+
289
+ const vault = new SPDVault(300_000); // 5-minute TTL
290
+
291
+ // Generate a random high-entropy key and store it
292
+ vault.genKey('session');
293
+
294
+ // Store your own key
295
+ vault.pushKey('myKey', 'MyStr0ng!Passphrase#2024');
296
+
297
+ // Retrieve (resets TTL)
298
+ const key = vault.pullKey('myKey'); // 'MyStr0ng!Passphrase#2024'
299
+
300
+ // Update (requires old value to match)
301
+ vault.updateKey('myKey', 'MyStr0ng!Passphrase#2024', 'NewPass!99');
302
+
303
+ // Delete one key
304
+ vault.destroyKey('myKey');
305
+
306
+ // Stop all timers (keys stay in memory)
307
+ vault.stop();
308
+
309
+ // Wipe everything
310
+ vault.destroy();
311
+ ```
312
+
313
+ Generated keys are 500–699 characters of cryptographically random characters drawn from a 91-character charset using rejection sampling (no modulo bias).
314
+
315
+ ---
316
+
317
+ ## SPDLegacy
318
+
319
+ `SPDLegacy` is a backwards-compatible class for reading files produced by SPD v1.x. It uses `crypto_secretbox_easy` (XSalsa20-Poly1305) and PBKDF2 key derivation. **Do not use for new data** — migrate to `SPD` instead.
320
+
321
+ ```ts
322
+ import { SPDLegacy } from 'spd-lib-ts';
323
+
324
+ const spd = await SPDLegacy.loadFromFile('./old.spd', 'passcode');
325
+ const data = await spd.extractData();
326
+ ```
327
+
328
+ ---
329
+
330
+ ## Full example — save and load a config
331
+
332
+ ```ts
333
+ import { SPD } from 'spd-lib-ts';
334
+ import * as path from 'path';
335
+
336
+ const FILE = path.join(process.env.HOME!, '.myapp', 'config.spd');
337
+ const PASS = process.env.APP_PASS!;
338
+
339
+ async function saveConfig(config: Record<string, unknown>) {
340
+ const spd = new SPD();
341
+ await spd.setPassKey(PASS);
342
+ await spd.addMany(Object.entries(config).map(([name, data]) => ({ name, data })));
343
+ await spd.saveToFile(FILE, PASS);
344
+ spd.destroy();
345
+ }
346
+
347
+ async function loadConfig(): Promise<Record<string, unknown>> {
348
+ const spd = await SPD.loadFromFile(FILE, PASS);
349
+ const data = await spd.extractData();
350
+ spd.destroy();
351
+ return data;
352
+ }
353
+ ```
354
+
355
+ ---
356
+
357
+ ## Full example — chunked HTTP upload
358
+
359
+ ```ts
360
+ // server sends:
361
+ const spd = new SPD();
362
+ await spd.setPassKey(PASS);
363
+ await spd.addData('payload', largeObject);
364
+ const chunks = await spd.saveDataChunked(PASS, 256 * 1024); // 256 KB per chunk
365
+ spd.destroy();
366
+
367
+ for (let i = 0; i < chunks.length; i++) {
368
+ await fetch('https://example.com/upload', {
369
+ method: 'POST',
370
+ body: JSON.stringify({ index: i, chunk: chunks[i] }),
371
+ });
372
+ }
373
+
374
+ // receiver collects all chunks in order and loads:
375
+ const spd = await SPD.loadFromChunks(receivedChunks, PASS);
376
+ const data = await spd.extractData();
377
+ spd.destroy();
378
+ ```
379
+
380
+ ---
381
+
382
+ ## Versioning
383
+
384
+ | npm version | SPD format version | Notes |
385
+ |---|---|---|
386
+ | 1.3.1 | v26 | Hash algo + Argon2 params embedded in payload, secure key zeroing, `null` type support |
387
+ | 1.3.0 | v25 | 512-bit Argon2id master secret, domain-separated AEAD + HMAC keys, HMAC-SHA3-512 |
388
+ | < 1.3.0 | v24 | 256-bit Argon2id, single key for AEAD + MAC |
389
+
390
+ SPD format versions are not cross-compatible. Use `SPDLegacy` to read v24 and earlier files.
391
+
392
+ ---
393
+
394
+ ## License
395
+
396
+ ISC
package/index.d.mts CHANGED
@@ -98,6 +98,29 @@ declare class SPD {
98
98
  addData(dataName: string, value: unknown): Promise<void>;
99
99
  addMany(items: DataInput[]): Promise<void>;
100
100
  extractData(): Promise<Record<string, unknown>>;
101
+ /**
102
+ * Memory-efficient streaming extraction.
103
+ *
104
+ * For each encrypted entry, this method:
105
+ * 1. Decrypts and decompresses the entry into a temp file in `tmpDir`
106
+ * (named `<entryName>.ssf`, mode 0600)
107
+ * 2. Reads the .ssf file and calls your `callback` with the name + value
108
+ * 3. Immediately deletes the .ssf file before moving to the next entry
109
+ *
110
+ * At most one entry's decrypted bytes exist on disk or in RAM at a time,
111
+ * so this works for arbitrarily large SPD files as long as a single entry
112
+ * fits in memory (which is always the case — entries are discrete values).
113
+ *
114
+ * @param tmpDir Directory to write temp files into (must exist, mode 0700 recommended)
115
+ * @param callback Called once per entry in order. Await is respected — the next
116
+ * entry won't be processed until your callback resolves.
117
+ *
118
+ * @example
119
+ * await spd.extractDataStreaming('/tmp/spd_scratch', async (name, value) => {
120
+ * console.log(name, value);
121
+ * });
122
+ */
123
+ extractDataStreaming(tmpDir: string, callback: (name: string, value: unknown) => Promise<void> | void): Promise<void>;
101
124
  destroy(): void;
102
125
  clearCache(): void;
103
126
  saveToFile(outputPath: string, passcode: string): Promise<void>;
@@ -115,6 +138,33 @@ declare class SPD {
115
138
  saveDataChunked(passcode: string, chunkSize?: number): Promise<string[]>;
116
139
  static loadFromFile(filePath: string, passcode: string): Promise<SPD>;
117
140
  static loadFromFileStreaming(filePath: string, passcode: string): Promise<SPD>;
141
+ /**
142
+ * True constant-memory streaming extract from a binary SPD file.
143
+ *
144
+ * Unlike `loadFromFileStreaming` (which buffers the entire plaintext into RAM),
145
+ * this method pipes the inflate stream through a byte-by-byte parser that:
146
+ * 1. Reads and verifies the file header + MAC
147
+ * 2. Parses the metadata block to derive keys
148
+ * 3. Processes one entry at a time:
149
+ * a. Writes the encrypted entry to `<name>_enc.ssf` in `tmpDir` (mode 0600)
150
+ * b. Decrypts it, writes the plaintext to `<name>_dec.ssf`
151
+ * c. Reads the `.ssf` file and calls your `callback`
152
+ * d. Deletes both `.ssf` files before touching the next entry
153
+ *
154
+ * Peak RAM usage is proportional to the largest single entry, not the file size.
155
+ * Files of any size are supported as long as individual entries fit in memory.
156
+ *
157
+ * @param filePath Path to the `.spd` file written by `saveToFileStreaming`
158
+ * @param passcode Passcode used when the file was saved
159
+ * @param tmpDir Scratch directory for temp files (must exist, mode 0700 recommended)
160
+ * @param callback Called once per entry in order. Next entry waits for this to resolve.
161
+ *
162
+ * @example
163
+ * await SPD.extractFromFileStreaming('./huge.spd', 'MyPass!', '/tmp/spd', async (name, value) => {
164
+ * console.log(name, typeof value);
165
+ * });
166
+ */
167
+ static extractFromFileStreaming(filePath: string, passcode: string, tmpDir: string, callback: (name: string, value: unknown) => Promise<void> | void): Promise<void>;
118
168
  static loadFromString(data: string, passcode: string): Promise<SPD>;
119
169
  static loadFromChunks(chunks: string[], passcode: string): Promise<SPD>;
120
170
  static derivePBK(passcode: string, salt: Uint8Array, memory?: number, time?: number): Promise<PBKResult>;
package/index.d.ts CHANGED
@@ -98,6 +98,29 @@ declare class SPD {
98
98
  addData(dataName: string, value: unknown): Promise<void>;
99
99
  addMany(items: DataInput[]): Promise<void>;
100
100
  extractData(): Promise<Record<string, unknown>>;
101
+ /**
102
+ * Memory-efficient streaming extraction.
103
+ *
104
+ * For each encrypted entry, this method:
105
+ * 1. Decrypts and decompresses the entry into a temp file in `tmpDir`
106
+ * (named `<entryName>.ssf`, mode 0600)
107
+ * 2. Reads the .ssf file and calls your `callback` with the name + value
108
+ * 3. Immediately deletes the .ssf file before moving to the next entry
109
+ *
110
+ * At most one entry's decrypted bytes exist on disk or in RAM at a time,
111
+ * so this works for arbitrarily large SPD files as long as a single entry
112
+ * fits in memory (which is always the case — entries are discrete values).
113
+ *
114
+ * @param tmpDir Directory to write temp files into (must exist, mode 0700 recommended)
115
+ * @param callback Called once per entry in order. Await is respected — the next
116
+ * entry won't be processed until your callback resolves.
117
+ *
118
+ * @example
119
+ * await spd.extractDataStreaming('/tmp/spd_scratch', async (name, value) => {
120
+ * console.log(name, value);
121
+ * });
122
+ */
123
+ extractDataStreaming(tmpDir: string, callback: (name: string, value: unknown) => Promise<void> | void): Promise<void>;
101
124
  destroy(): void;
102
125
  clearCache(): void;
103
126
  saveToFile(outputPath: string, passcode: string): Promise<void>;
@@ -115,6 +138,33 @@ declare class SPD {
115
138
  saveDataChunked(passcode: string, chunkSize?: number): Promise<string[]>;
116
139
  static loadFromFile(filePath: string, passcode: string): Promise<SPD>;
117
140
  static loadFromFileStreaming(filePath: string, passcode: string): Promise<SPD>;
141
+ /**
142
+ * True constant-memory streaming extract from a binary SPD file.
143
+ *
144
+ * Unlike `loadFromFileStreaming` (which buffers the entire plaintext into RAM),
145
+ * this method pipes the inflate stream through a byte-by-byte parser that:
146
+ * 1. Reads and verifies the file header + MAC
147
+ * 2. Parses the metadata block to derive keys
148
+ * 3. Processes one entry at a time:
149
+ * a. Writes the encrypted entry to `<name>_enc.ssf` in `tmpDir` (mode 0600)
150
+ * b. Decrypts it, writes the plaintext to `<name>_dec.ssf`
151
+ * c. Reads the `.ssf` file and calls your `callback`
152
+ * d. Deletes both `.ssf` files before touching the next entry
153
+ *
154
+ * Peak RAM usage is proportional to the largest single entry, not the file size.
155
+ * Files of any size are supported as long as individual entries fit in memory.
156
+ *
157
+ * @param filePath Path to the `.spd` file written by `saveToFileStreaming`
158
+ * @param passcode Passcode used when the file was saved
159
+ * @param tmpDir Scratch directory for temp files (must exist, mode 0700 recommended)
160
+ * @param callback Called once per entry in order. Next entry waits for this to resolve.
161
+ *
162
+ * @example
163
+ * await SPD.extractFromFileStreaming('./huge.spd', 'MyPass!', '/tmp/spd', async (name, value) => {
164
+ * console.log(name, typeof value);
165
+ * });
166
+ */
167
+ static extractFromFileStreaming(filePath: string, passcode: string, tmpDir: string, callback: (name: string, value: unknown) => Promise<void> | void): Promise<void>;
118
168
  static loadFromString(data: string, passcode: string): Promise<SPD>;
119
169
  static loadFromChunks(chunks: string[], passcode: string): Promise<SPD>;
120
170
  static derivePBK(passcode: string, salt: Uint8Array, memory?: number, time?: number): Promise<PBKResult>;