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/index.d.mts
CHANGED
|
@@ -22,11 +22,45 @@ interface SPDPayload {
|
|
|
22
22
|
saltNonce: number[];
|
|
23
23
|
wrapSalt: number[];
|
|
24
24
|
version: number;
|
|
25
|
+
writeCounter?: number;
|
|
25
26
|
hashAlgorithm?: string;
|
|
26
27
|
argon2Memory?: number;
|
|
27
28
|
argon2Time?: number;
|
|
28
29
|
argon2HashLen?: number;
|
|
29
30
|
hashKeyLen?: number;
|
|
31
|
+
entryRoles?: Record<string, string[]>;
|
|
32
|
+
maxOpenCount?: number;
|
|
33
|
+
openCount?: number;
|
|
34
|
+
keyProfile?: string;
|
|
35
|
+
keyCommitment?: string;
|
|
36
|
+
kdfChain?: boolean;
|
|
37
|
+
scryptN?: number;
|
|
38
|
+
scryptR?: number;
|
|
39
|
+
scryptP?: number;
|
|
40
|
+
wrapArgon2Memory?: number;
|
|
41
|
+
wrapArgon2Time?: number;
|
|
42
|
+
fileSignature?: number[];
|
|
43
|
+
signingPubKey?: number[];
|
|
44
|
+
ratchetEnabled?: boolean;
|
|
45
|
+
ratchetCounter?: number;
|
|
46
|
+
epochNumber?: number;
|
|
47
|
+
epochSize?: number;
|
|
48
|
+
merkleRoot?: string;
|
|
49
|
+
counterHMAC?: string;
|
|
50
|
+
}
|
|
51
|
+
/** PKCS#11 / HSM key provider abstraction. Implement this interface to use hardware keys. */
|
|
52
|
+
interface SPDKeyProvider {
|
|
53
|
+
/** Derive the 96-byte master secret from a passcode and salt (replaces Argon2id). */
|
|
54
|
+
deriveKey(passcode: string, salt: Uint8Array, memory: number, time: number): Promise<Uint8Array>;
|
|
55
|
+
/** Optional: wrap (encrypt) a key for storage — used when exporting key material. */
|
|
56
|
+
wrapKey?(key: Uint8Array): Promise<Uint8Array>;
|
|
57
|
+
/** Optional: unwrap (decrypt) a key retrieved from storage. */
|
|
58
|
+
unwrapKey?(wrapped: Uint8Array): Promise<Uint8Array>;
|
|
59
|
+
}
|
|
60
|
+
/** ML-DSA-65 signing key pair for file authentication. Signatures are 3309 bytes. */
|
|
61
|
+
interface SPDSigningKeyPair {
|
|
62
|
+
privateKey: Uint8Array;
|
|
63
|
+
publicKey: Uint8Array;
|
|
30
64
|
}
|
|
31
65
|
interface SPDLegacyPayload {
|
|
32
66
|
data: SerializedDataEntry[];
|
|
@@ -82,11 +116,62 @@ interface SPDGetEntryResult {
|
|
|
82
116
|
dataType: SupportedDataType;
|
|
83
117
|
name: string;
|
|
84
118
|
}
|
|
119
|
+
interface SPDInspectResult {
|
|
120
|
+
version: number;
|
|
121
|
+
entryCount: number;
|
|
122
|
+
writeCounter: number;
|
|
123
|
+
hashAlgorithm: string;
|
|
124
|
+
argon2Memory: number;
|
|
125
|
+
argon2Time: number;
|
|
126
|
+
fileSize: number;
|
|
127
|
+
hasIndex: boolean;
|
|
128
|
+
format: 'binary' | 'json';
|
|
129
|
+
}
|
|
130
|
+
interface SPDVerifyResult {
|
|
131
|
+
valid: boolean;
|
|
132
|
+
entryCount: number;
|
|
133
|
+
indexPresent: boolean;
|
|
134
|
+
writeCounter: number;
|
|
135
|
+
}
|
|
136
|
+
interface SPDDiffResult {
|
|
137
|
+
added: string[];
|
|
138
|
+
removed: string[];
|
|
139
|
+
modified: string[];
|
|
140
|
+
}
|
|
141
|
+
interface SPDMergeOptions {
|
|
142
|
+
strategy: 'a-wins' | 'b-wins' | 'newer';
|
|
143
|
+
}
|
|
144
|
+
interface SPDSnapshot {
|
|
145
|
+
/** Encrypted entries captured at snapshot time (deep copies). */
|
|
146
|
+
data: EncryptedDataEntry[];
|
|
147
|
+
writeCounter: number;
|
|
148
|
+
ts: number;
|
|
149
|
+
}
|
|
150
|
+
interface SPDLogEvent {
|
|
151
|
+
event: 'key_derived' | 'key_cache_hit' | 'entry_added' | 'entry_updated' | 'entry_deleted' | 'save_start' | 'save_end' | 'load_start' | 'load_end' | 'rollback_detected' | 'lock_acquired' | 'lock_released' | 'wal_flushed' | 'auto_destruct' | 'repair_complete' | 'benchmark_complete' | 'keyfile_generated' | 'keyfile_loaded';
|
|
152
|
+
ts: number;
|
|
153
|
+
detail?: Record<string, unknown>;
|
|
154
|
+
}
|
|
155
|
+
interface SPDBenchmarkResult {
|
|
156
|
+
argon2Ms: number;
|
|
157
|
+
encryptMs: number;
|
|
158
|
+
decryptMs: number;
|
|
159
|
+
macMs: number;
|
|
160
|
+
entriesPerSecond: number;
|
|
161
|
+
}
|
|
162
|
+
interface SPDRepairResult {
|
|
163
|
+
recovered: number;
|
|
164
|
+
skipped: number;
|
|
165
|
+
}
|
|
85
166
|
type TypedArray = Uint8Array | Uint16Array | Uint32Array | BigInt64Array | BigUint64Array | Float32Array | Float64Array;
|
|
86
167
|
type SupportedValue = string | number | boolean | unknown[] | TypedArray | Map<unknown, unknown> | Set<unknown> | Date | RegExp | Error | Record<string, unknown>;
|
|
87
168
|
|
|
88
169
|
declare const ARGON2_MEMORY_HIGH = 524288;
|
|
89
170
|
declare const ARGON2_TIME_HIGH = 8;
|
|
171
|
+
declare const ARGON2_MEMORY_PARANOID = 1048576;
|
|
172
|
+
declare const ARGON2_TIME_PARANOID = 12;
|
|
173
|
+
/** Key stretching profile names */
|
|
174
|
+
type SPDKeyProfile = 'standard' | 'high' | 'paranoid';
|
|
90
175
|
declare class SPD {
|
|
91
176
|
private data;
|
|
92
177
|
private keyPair?;
|
|
@@ -95,8 +180,118 @@ declare class SPD {
|
|
|
95
180
|
private salt?;
|
|
96
181
|
private hash;
|
|
97
182
|
private compressionLevel;
|
|
183
|
+
private fileVersion;
|
|
184
|
+
private writeCounter;
|
|
185
|
+
private walPath?;
|
|
186
|
+
private walRecordCount;
|
|
187
|
+
private static readonly WAL_COMPACT_RECORDS;
|
|
188
|
+
private static readonly WAL_COMPACT_BYTES;
|
|
189
|
+
private static globalPepper;
|
|
190
|
+
private static keyfileBytes;
|
|
191
|
+
private entryRoles;
|
|
192
|
+
private currentRole;
|
|
193
|
+
private maxOpenCount;
|
|
194
|
+
private openCount;
|
|
195
|
+
private schemas;
|
|
196
|
+
private _plogPath?;
|
|
197
|
+
private keyProfile;
|
|
198
|
+
private static machineBindingEnabled;
|
|
199
|
+
private _keyMask?;
|
|
200
|
+
private _blindedKey?;
|
|
201
|
+
private _blindedMacKey?;
|
|
202
|
+
private _keyBlindingEnabled;
|
|
203
|
+
private _signingKey?;
|
|
204
|
+
private _verifyKey?;
|
|
205
|
+
private _ratchetEnabled;
|
|
206
|
+
private _epochSize;
|
|
207
|
+
private _epochNumber;
|
|
208
|
+
private static _keyProvider?;
|
|
209
|
+
private _kdfChain;
|
|
210
|
+
private _scryptN;
|
|
211
|
+
private _scryptR;
|
|
212
|
+
private _scryptP;
|
|
213
|
+
private _wrapArgon2Memory;
|
|
214
|
+
private _wrapArgon2Time;
|
|
215
|
+
private static globalLogger;
|
|
216
|
+
private static keyCache;
|
|
217
|
+
private static keyCacheGet;
|
|
218
|
+
private static keyCacheSet;
|
|
219
|
+
/** Build a non-secret cache key from the passcode and wrapSalt bytes. */
|
|
220
|
+
private static buildCacheKey;
|
|
221
|
+
/** Evict all cached key material immediately (e.g. on pepper change). */
|
|
222
|
+
static clearKeyCache(): void;
|
|
223
|
+
private static nameCollisionWarnings;
|
|
224
|
+
/**
|
|
225
|
+
* Enable developer warnings when two raw entry names map to the same
|
|
226
|
+
* sanitized name (e.g. "user.id" and "user_id" → both become "user_id").
|
|
227
|
+
* Logs to console.warn. Off by default to avoid noise in production.
|
|
228
|
+
*/
|
|
229
|
+
static enableNameCollisionWarnings(): void;
|
|
230
|
+
static disableNameCollisionWarnings(): void;
|
|
231
|
+
/**
|
|
232
|
+
* Install a global log handler. Called for key lifecycle events so the host
|
|
233
|
+
* app can feed SPD activity into its own observability pipeline.
|
|
234
|
+
* Pass `undefined` to remove the handler.
|
|
235
|
+
*/
|
|
236
|
+
static setLogger(fn: ((e: SPDLogEvent) => void) | undefined): void;
|
|
237
|
+
private static log;
|
|
98
238
|
private assertReady;
|
|
99
239
|
init(): Promise<void>;
|
|
240
|
+
/**
|
|
241
|
+
* Enable in-memory key blinding: keys are stored XOR'd with a random mask.
|
|
242
|
+
* This reduces the risk of key extraction from memory dumps or cold-boot attacks.
|
|
243
|
+
* Must be called before `setPassKey()`.
|
|
244
|
+
*/
|
|
245
|
+
enableKeyBlinding(): void;
|
|
246
|
+
disableKeyBlinding(): void;
|
|
247
|
+
private static _xorBufs;
|
|
248
|
+
/** Blind the current userKey and macKey with a fresh random mask. */
|
|
249
|
+
private _blindKeys;
|
|
250
|
+
/** Unblind keys into temporaries, run fn, then re-blind. */
|
|
251
|
+
private _withKeys;
|
|
252
|
+
private _withKeysAsync;
|
|
253
|
+
/**
|
|
254
|
+
* Set an ML-DSA-65 signing private key. When set, every `saveToFile` /
|
|
255
|
+
* `saveToFileStreaming` call will embed an ML-DSA-65 signature over the file MAC.
|
|
256
|
+
*/
|
|
257
|
+
setSigningKey(privateKey: Uint8Array): void;
|
|
258
|
+
/**
|
|
259
|
+
* Set an ML-DSA-65 verify public key. When set, `loadFromFile` /
|
|
260
|
+
* `loadFromFileStreaming` will verify the embedded signature and reject
|
|
261
|
+
* files that fail verification.
|
|
262
|
+
*/
|
|
263
|
+
setVerifyKey(publicKey: Uint8Array): void;
|
|
264
|
+
/** Generate a fresh ML-DSA-65 signing key pair. */
|
|
265
|
+
static generateSigningKeyPair(): SPDSigningKeyPair;
|
|
266
|
+
/** Enable the per-save ratchet. Each save derives a new sub-key from the previous. */
|
|
267
|
+
enableRatchet(): void;
|
|
268
|
+
disableRatchet(): void;
|
|
269
|
+
/**
|
|
270
|
+
* Set the epoch size: every `n` saves, a new random salt is derived and all
|
|
271
|
+
* entries are re-encrypted under a fresh key. Pass 0 to disable.
|
|
272
|
+
*/
|
|
273
|
+
setEpochSize(n: number): void;
|
|
274
|
+
/**
|
|
275
|
+
* Enable KDF chaining: after Argon2id, pipe output through scrypt for
|
|
276
|
+
* double-KDF protection. Enabled by default for `high` / `paranoid` profiles.
|
|
277
|
+
* @param n scrypt CPU/memory cost (power of 2, default 16384)
|
|
278
|
+
* @param r block size (default 8)
|
|
279
|
+
* @param p parallelism (default 1)
|
|
280
|
+
*/
|
|
281
|
+
enableKdfChain(n?: number, r?: number, p?: number): void;
|
|
282
|
+
disableKdfChain(): void;
|
|
283
|
+
/**
|
|
284
|
+
* Override the Argon2id parameters used to derive the salt-wrapping key.
|
|
285
|
+
* By default, standard params (128 MiB / 6 passes) are used for backward compatibility.
|
|
286
|
+
* Changing these breaks compatibility with existing files unless you also change the reader.
|
|
287
|
+
*/
|
|
288
|
+
setWrapKeyProfile(memory: number, time: number): void;
|
|
289
|
+
/**
|
|
290
|
+
* Install a global PKCS#11 / HSM key provider. When set, SPD delegates
|
|
291
|
+
* all key derivation to `provider.deriveKey()` instead of Argon2id+scrypt.
|
|
292
|
+
* Pass `undefined` to revert to the built-in KDF.
|
|
293
|
+
*/
|
|
294
|
+
static setKeyProvider(provider: SPDKeyProvider | undefined): void;
|
|
100
295
|
changePasscode(oldPasscode: string, newPasscode: string): Promise<void>;
|
|
101
296
|
/**
|
|
102
297
|
* Derives a master secret via Argon2id and splits it into:
|
|
@@ -106,20 +301,191 @@ declare class SPD {
|
|
|
106
301
|
* v27+: hashLen=96, macKeyLen=64 (full SHA3-512 output width = 128-bit PQ forgery resistance)
|
|
107
302
|
* v26: hashLen=64, macKeyLen=32 (legacy — pass these explicitly when reading old files)
|
|
108
303
|
*/
|
|
109
|
-
static deriveKeys(passcode: string, salt: Uint8Array, memory?: number, time?: number, hashLen?: number, macKeyLen?: number): Promise<{
|
|
304
|
+
static deriveKeys(passcode: string, salt: Uint8Array, memory?: number, time?: number, hashLen?: number, macKeyLen?: number, kdfChain?: boolean, scryptN?: number, scryptR?: number, scryptP?: number): Promise<{
|
|
110
305
|
aeadKey: Uint8Array;
|
|
111
306
|
macKey: Uint8Array;
|
|
112
307
|
}>;
|
|
308
|
+
/**
|
|
309
|
+
* Compute a key commitment token: HMAC-SHA3-256(aeadKey, "spd-key-commit-v1").
|
|
310
|
+
* Stored in the file meta and verified on load before decryption begins.
|
|
311
|
+
* Closes key-commitment attacks where an adversary crafts a ciphertext that
|
|
312
|
+
* "decrypts" under two different keys.
|
|
313
|
+
*/
|
|
314
|
+
static computeKeyCommitment(aeadKey: Uint8Array): string;
|
|
113
315
|
private static computeMAC;
|
|
316
|
+
/**
|
|
317
|
+
* Derives a per-entry 32-byte AEAD key from the master AEAD key using HKDF-SHA3-512.
|
|
318
|
+
*
|
|
319
|
+
* Each entry is encrypted under a unique key = HKDF(masterKey, salt=entryName, info="spd-entry-v27").
|
|
320
|
+
* This means:
|
|
321
|
+
* - Compromise of one entry's ciphertext reveals nothing about other entries' keys
|
|
322
|
+
* - The master key never touches AEAD directly
|
|
323
|
+
* - Zero performance impact: HKDF is ~microseconds vs ~1s for Argon2id
|
|
324
|
+
*/
|
|
325
|
+
static deriveEntryKey(masterAeadKey: Uint8Array, entryName: string): Uint8Array;
|
|
114
326
|
checkPasscodeStrength(passcode: string): boolean;
|
|
115
327
|
setPassKey(passcode: string): Promise<void>;
|
|
116
328
|
setHash(hash?: HashAlgorithm): void;
|
|
117
329
|
setCompressionLevel(level?: number): void;
|
|
118
330
|
getSodium(): typeof sodium;
|
|
331
|
+
/**
|
|
332
|
+
* Restrict an entry to one or more roles. `extractData`, `getEntry`, and the
|
|
333
|
+
* lazy `entries()` iterator will throw if `setRole()` has not been called with
|
|
334
|
+
* a matching role. Pass an empty array to remove all restrictions.
|
|
335
|
+
*/
|
|
336
|
+
restrictEntry(name: string, roles: string[]): void;
|
|
337
|
+
/**
|
|
338
|
+
* Set the active role for this instance. Entries restricted to roles that
|
|
339
|
+
* don't include this value will be inaccessible until the role is changed.
|
|
340
|
+
* Pass `undefined` to clear the role (only unrestricted entries will be readable).
|
|
341
|
+
*/
|
|
342
|
+
setRole(role: string | undefined): void;
|
|
343
|
+
/**
|
|
344
|
+
* Set the Argon2id cost profile for this instance.
|
|
345
|
+
* - `'standard'` — 128 MiB / 6 passes (default, ~500 ms on modern hardware)
|
|
346
|
+
* - `'high'` — 512 MiB / 8 passes (~2 s)
|
|
347
|
+
* - `'paranoid'` — 1 GiB / 12 passes + machine-secret post-KDF (~4–8 s)
|
|
348
|
+
*
|
|
349
|
+
* Must be set before `setPassKey()`. The chosen profile's params are embedded
|
|
350
|
+
* in the file so loads always re-derive with the same cost.
|
|
351
|
+
*/
|
|
352
|
+
setKeyProfile(profile: SPDKeyProfile): void;
|
|
353
|
+
/**
|
|
354
|
+
* Enable machine binding: mixes a stable machine-specific secret (derived
|
|
355
|
+
* from `/etc/machine-id`, `hostname`, or a UUID sidecar) into every key
|
|
356
|
+
* derivation via an extra HMAC step. The resulting key is only reproducible
|
|
357
|
+
* on the originating machine, dramatically raising the cost of offline attacks
|
|
358
|
+
* with a stolen file.
|
|
359
|
+
*
|
|
360
|
+
* Enabling this globally affects all SPD instances in the process.
|
|
361
|
+
* Must be called before `setPassKey()` / `loadFromFile()` / `getEntry()`.
|
|
362
|
+
*/
|
|
363
|
+
static enableMachineBinding(): void;
|
|
364
|
+
static disableMachineBinding(): void;
|
|
365
|
+
/** Reads or creates the machine secret (16-byte hex stored in ~/.spd-machine-id). */
|
|
366
|
+
private static getMachineSecret;
|
|
367
|
+
/**
|
|
368
|
+
* Post-KDF machine binding: HMAC-SHA3-512(machineSecret, rawKey)[0..keyLen].
|
|
369
|
+
* Applied to both aeadKey and macKey when machine binding is enabled.
|
|
370
|
+
*/
|
|
371
|
+
private static applyMachineBinding;
|
|
372
|
+
/** Resolve Argon2id params from a profile name. */
|
|
373
|
+
static profileParams(profile: SPDKeyProfile): {
|
|
374
|
+
memory: number;
|
|
375
|
+
time: number;
|
|
376
|
+
};
|
|
377
|
+
/** Returns true if the current role is allowed to read `name`. */
|
|
378
|
+
private canReadEntry;
|
|
379
|
+
/**
|
|
380
|
+
* Configure a maximum open count for auto-destruct.
|
|
381
|
+
* After this many successful `loadFromFile` / `loadFromFileStreaming` calls the
|
|
382
|
+
* file is securely wiped and an error is thrown on the exceeding open.
|
|
383
|
+
* Set to 0 to disable (default).
|
|
384
|
+
*/
|
|
385
|
+
setMaxOpenCount(n: number): void;
|
|
386
|
+
/**
|
|
387
|
+
* Persist the max-open-count limit to a `.maxcount` sidecar alongside `filePath`.
|
|
388
|
+
* Must be called after `saveToFile`/`saveToFileStreaming` so the sidecar is
|
|
389
|
+
* co-located with the file that was actually written.
|
|
390
|
+
* Callers who set a limit should call this immediately after saving.
|
|
391
|
+
*/
|
|
392
|
+
static setFileMaxOpenCount(filePath: string, n: number): void;
|
|
393
|
+
/**
|
|
394
|
+
* Set the path to the passcode rotation audit log (.plog) sidecar file.
|
|
395
|
+
* A JSON-lines record `{ts, event}` is appended whenever `changePasscode` is
|
|
396
|
+
* called. The file is created if it does not exist (mode 0600).
|
|
397
|
+
* Pass `undefined` to disable.
|
|
398
|
+
*/
|
|
399
|
+
setPlogPath(p: string | undefined): void;
|
|
400
|
+
/**
|
|
401
|
+
* Attach a validator function to an entry name.
|
|
402
|
+
* Called before encryption in `addData` and `updateEntry`.
|
|
403
|
+
* Throw or return `false` to reject the value.
|
|
404
|
+
*/
|
|
405
|
+
setSchema(name: string, validator: (v: unknown) => boolean): void;
|
|
406
|
+
/** Remove a previously registered schema validator. */
|
|
407
|
+
removeSchema(name: string): void;
|
|
408
|
+
private applySchema;
|
|
409
|
+
/**
|
|
410
|
+
* Returns an immutable snapshot of the current encrypted entries.
|
|
411
|
+
* No key material is included — use `restoreSnapshot` to apply.
|
|
412
|
+
*/
|
|
413
|
+
snapshot(): SPDSnapshot;
|
|
414
|
+
/**
|
|
415
|
+
* Replaces the current encrypted entries with those from a snapshot.
|
|
416
|
+
* Key material is unchanged — the snapshot must have been taken from an
|
|
417
|
+
* instance with the same passcode.
|
|
418
|
+
*/
|
|
419
|
+
restoreSnapshot(snap: SPDSnapshot): void;
|
|
119
420
|
sanitizeName(dataName: string): string;
|
|
120
421
|
addData(dataName: string, value: unknown): Promise<void>;
|
|
121
422
|
addMany(items: DataInput[]): Promise<void>;
|
|
122
423
|
extractData(): Promise<Record<string, unknown>>;
|
|
424
|
+
/** Returns true if an entry with the given name exists. No decryption. */
|
|
425
|
+
has(name: string): boolean;
|
|
426
|
+
/** Returns all entry names in insertion order. No decryption. */
|
|
427
|
+
keys(): string[];
|
|
428
|
+
/**
|
|
429
|
+
* Lazy decryption iterator — yields `{name, value, dataType}` one entry at a
|
|
430
|
+
* time without loading all entries into memory simultaneously. Respects entry
|
|
431
|
+
* ACL roles; skips (does not throw) decoy entries silently.
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* for await (const {name, value} of spd.entries()) {
|
|
435
|
+
* console.log(name, value);
|
|
436
|
+
* }
|
|
437
|
+
*/
|
|
438
|
+
entries(): AsyncGenerator<{
|
|
439
|
+
name: string;
|
|
440
|
+
value: unknown;
|
|
441
|
+
dataType: string;
|
|
442
|
+
}>;
|
|
443
|
+
/**
|
|
444
|
+
* Insert `count` fake encrypted entries whose names are derived from `prefix`.
|
|
445
|
+
* Decoys are indistinguishable from real entries in the binary/JSON file.
|
|
446
|
+
* They are silently skipped by `entries()` and flagged as `_decoy` type so
|
|
447
|
+
* callers that inspect `dataType` can filter them.
|
|
448
|
+
*/
|
|
449
|
+
addDecoy(prefix: string, count?: number): Promise<void>;
|
|
450
|
+
/**
|
|
451
|
+
* Removes an entry by name. Throws if not found.
|
|
452
|
+
* Does NOT re-save to disk — call saveToFile/saveToFileStreaming after.
|
|
453
|
+
*/
|
|
454
|
+
deleteEntry(name: string): void;
|
|
455
|
+
/**
|
|
456
|
+
* Renames an entry. Re-encrypts the blob under the new name's derived key + AAD.
|
|
457
|
+
* Throws if `oldName` is not found or `newName` already exists.
|
|
458
|
+
* Does NOT re-save to disk — call saveToFile/saveToFileStreaming after.
|
|
459
|
+
*/
|
|
460
|
+
renameEntry(oldName: string, newName: string): Promise<void>;
|
|
461
|
+
/**
|
|
462
|
+
* Replaces an entry's value in place. Equivalent to deleteEntry + addData
|
|
463
|
+
* but preserves insertion order.
|
|
464
|
+
* Throws if the entry is not found.
|
|
465
|
+
* Does NOT re-save to disk — call saveToFile/saveToFileStreaming after.
|
|
466
|
+
*/
|
|
467
|
+
updateEntry(name: string, value: unknown): Promise<void>;
|
|
468
|
+
/**
|
|
469
|
+
* Returns a new SPD instance (with the same key material) containing only the
|
|
470
|
+
* entries whose sanitized names match `pattern`. The returned instance is
|
|
471
|
+
* ready for `saveToFile` / `saveToFileStreaming` — it shares no mutable state
|
|
472
|
+
* with the original.
|
|
473
|
+
*
|
|
474
|
+
* @param pattern RegExp tested against each sanitized entry name.
|
|
475
|
+
* @returns A new SPD instance carrying the matching encrypted entries.
|
|
476
|
+
*/
|
|
477
|
+
exportEntries(pattern: RegExp): SPD;
|
|
478
|
+
/**
|
|
479
|
+
* Copies all entries from `source` into this instance.
|
|
480
|
+
* - Both instances must use the same passcode / key material.
|
|
481
|
+
* - If an entry name already exists it is overwritten.
|
|
482
|
+
* - Does NOT re-save to disk.
|
|
483
|
+
*
|
|
484
|
+
* @param source Another SPD instance (must share the same AEAD / HMAC keys).
|
|
485
|
+
*/
|
|
486
|
+
importEntries(source: SPD): void;
|
|
487
|
+
/** Returns the number of stored entries. */
|
|
488
|
+
count(): number;
|
|
123
489
|
/**
|
|
124
490
|
* Memory-efficient streaming extraction.
|
|
125
491
|
*
|
|
@@ -143,32 +509,174 @@ declare class SPD {
|
|
|
143
509
|
* });
|
|
144
510
|
*/
|
|
145
511
|
extractDataStreaming(tmpDir: string, callback: (name: string, value: unknown) => Promise<void> | void): Promise<void>;
|
|
512
|
+
/**
|
|
513
|
+
* Enables the write-ahead log. Once enabled, every `addData` / `addMany` call
|
|
514
|
+
* appends the encrypted entry to `walPath` atomically. On the next
|
|
515
|
+
* `loadFromFile` / `loadFromFileStreaming`, any existing WAL is replayed
|
|
516
|
+
* automatically before returning. `saveToFile` / `saveToFileStreaming` merges
|
|
517
|
+
* and then deletes the WAL.
|
|
518
|
+
*
|
|
519
|
+
* WAL record format (binary, appended):
|
|
520
|
+
* [4B magic "SPDW"][1B version][1B nameLen][nameLen B name]
|
|
521
|
+
* [1B typeLen][typeLen B type][2B hashLen][hashLen B hash]
|
|
522
|
+
* [3B nonceLen][nonceLen B nonce][8B dataLen][dataLen B encData]
|
|
523
|
+
*/
|
|
524
|
+
enableWal(walPath: string): void;
|
|
525
|
+
/** Disable WAL without deleting the file. */
|
|
526
|
+
disableWal(): void;
|
|
527
|
+
/** Append a single encrypted entry to the WAL file. Auto-compacts when thresholds are exceeded. */
|
|
528
|
+
private appendToWal;
|
|
529
|
+
/**
|
|
530
|
+
* Internal: called by setImmediate when WAL thresholds are exceeded.
|
|
531
|
+
* Requires a `walCompactPath` to be set; does nothing otherwise.
|
|
532
|
+
* Users should call `flushDelta(outputPath, passcode)` explicitly —
|
|
533
|
+
* auto-compaction only fires when `walCompactPath` is configured via `enableWal`.
|
|
534
|
+
*/
|
|
535
|
+
private _walAutoCompact;
|
|
536
|
+
private _walCompactPath?;
|
|
537
|
+
private _walCompactPasscode?;
|
|
538
|
+
/**
|
|
539
|
+
* Configures the output path and passcode used for WAL auto-compaction.
|
|
540
|
+
* Must be called after `enableWal()` for auto-compaction to fire.
|
|
541
|
+
*/
|
|
542
|
+
setWalCompactTarget(outputPath: string, passcode: string): void;
|
|
543
|
+
/**
|
|
544
|
+
* Reads and replays all entries from an existing WAL file into `this.data`.
|
|
545
|
+
* Skips duplicate names (WAL entry wins over existing). Returns entry count replayed.
|
|
546
|
+
*/
|
|
547
|
+
static replayWal(walPath: string, target: SPD): number;
|
|
548
|
+
/**
|
|
549
|
+
* Merges any pending WAL entries, saves the full file, then deletes the WAL.
|
|
550
|
+
* Use this instead of `saveToFile` when WAL is enabled to ensure the WAL
|
|
551
|
+
* is atomically cleared only after a successful save.
|
|
552
|
+
*/
|
|
553
|
+
flushDelta(outputPath: string, passcode: string, streaming?: boolean): Promise<void>;
|
|
146
554
|
destroy(): void;
|
|
147
555
|
clearCache(): void;
|
|
556
|
+
/**
|
|
557
|
+
* Sets a global pepper mixed into all Argon2id key derivations.
|
|
558
|
+
* Never stored in the file — both save and load sides must set the same pepper.
|
|
559
|
+
* Must be set before setPassKey / loadFromFile / loadFromString / getEntry.
|
|
560
|
+
*/
|
|
561
|
+
static setPepper(pepper: string | Buffer): void;
|
|
562
|
+
/**
|
|
563
|
+
* Generate a cryptographically random 64-byte (512-bit) keyfile and write it
|
|
564
|
+
* to `filePath`. The keyfile should be stored separately from the SPD file
|
|
565
|
+
* (e.g. USB drive, password manager, or HSM) so that an attacker who obtains
|
|
566
|
+
* the SPD file alone cannot attempt password guessing — the keyfile bytes are
|
|
567
|
+
* required as a second factor and are mixed into every Argon2id derivation.
|
|
568
|
+
*
|
|
569
|
+
* Never store the keyfile in the same directory as the SPD file.
|
|
570
|
+
*
|
|
571
|
+
* @example
|
|
572
|
+
* await SPD.generateKeyfile('/mnt/usb/my.key');
|
|
573
|
+
* SPD.loadKeyfile('/mnt/usb/my.key');
|
|
574
|
+
* const spd = new SPD();
|
|
575
|
+
* await spd.setPassKey('anything'); // passcode quality no longer matters
|
|
576
|
+
*/
|
|
577
|
+
static generateKeyfile(filePath: string): void;
|
|
578
|
+
/**
|
|
579
|
+
* Load a keyfile previously created with `generateKeyfile()`.
|
|
580
|
+
* Must be called before `setPassKey()`, `loadFromFile()`, and any other
|
|
581
|
+
* operation that derives key material.
|
|
582
|
+
*
|
|
583
|
+
* The keyfile bytes are mixed into every Argon2id derivation via
|
|
584
|
+
* HMAC-SHA3-512(keyfileBytes, fileSalt || passcode), so:
|
|
585
|
+
* - The same keyfile + any passcode ≠ the same keyfile + different passcode
|
|
586
|
+
* - The same keyfile + same passcode on a different SPD file (different salt) ≠ same key
|
|
587
|
+
* - Without the keyfile, no amount of passcode guessing succeeds
|
|
588
|
+
*
|
|
589
|
+
* @param filePath Path to the keyfile (64 bytes of random data).
|
|
590
|
+
*/
|
|
591
|
+
static loadKeyfile(filePath: string): void;
|
|
592
|
+
/**
|
|
593
|
+
* Unload the current keyfile so subsequent operations use passcode-only derivation.
|
|
594
|
+
* Call this when you are done with the SPD instance and want to remove the
|
|
595
|
+
* keyfile material from memory.
|
|
596
|
+
*/
|
|
597
|
+
static unloadKeyfile(): void;
|
|
598
|
+
/**
|
|
599
|
+
* Generate a cryptographically random diceware passphrase from the EFF short
|
|
600
|
+
* wordlist (1296 words, ~10.3 bits of entropy per word).
|
|
601
|
+
*
|
|
602
|
+
* Default: 9 words → ~72 bits of entropy (256-word list, 8 bits/word) — resistant
|
|
603
|
+
* Argon2id-capable ASICs, and **centuries** at the `high` or `paranoid` profile.
|
|
604
|
+
*
|
|
605
|
+
* With a keyfile loaded alongside, the passphrase entropy is irrelevant — the
|
|
606
|
+
* keyfile provides 512 bits regardless. The passphrase then acts only as a
|
|
607
|
+
* second authentication factor.
|
|
608
|
+
*
|
|
609
|
+
* @param words Number of words (minimum 5 = ~51 bits; default 7 = ~72 bits).
|
|
610
|
+
* @returns A space-separated passphrase string, e.g. "apple cargo drum fence grain helm iris"
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* const pass = SPD.generatePassphrase();
|
|
614
|
+
* // → "timber rivet angle plumb comet drift spark"
|
|
615
|
+
* const spd = new SPD();
|
|
616
|
+
* await spd.setPassKey(pass);
|
|
617
|
+
*/
|
|
618
|
+
static generatePassphrase(words?: number): string;
|
|
619
|
+
/**
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Acquires an advisory lock for `filePath` by creating `filePath.lock`.
|
|
623
|
+
* The lock file contains JSON with PID and timestamp so stale locks
|
|
624
|
+
* (process dead or >30 s old) are automatically broken.
|
|
625
|
+
*
|
|
626
|
+
* If the lock is held by a live process, retries every 50 ms until
|
|
627
|
+
* `lockTimeoutMs` has elapsed (default 5 000 ms) before throwing `ESPD_LOCKED`.
|
|
628
|
+
*/
|
|
629
|
+
static acquireLock(filePath: string, lockTimeoutMs?: number): void;
|
|
630
|
+
/** Releases the advisory lock for `filePath` (deletes `filePath.lock`). */
|
|
631
|
+
static releaseLock(filePath: string): void;
|
|
632
|
+
/** Path of the sequence sidecar file for `filePath`. */
|
|
633
|
+
private static seqPath;
|
|
634
|
+
/**
|
|
635
|
+
* Reads the last-seen write counter from the `.seq` sidecar.
|
|
636
|
+
* Returns -1 if no sidecar exists (first load — any counter is accepted).
|
|
637
|
+
*/
|
|
638
|
+
private static readSeqCounter;
|
|
639
|
+
/** Writes the new write counter to the `.seq` sidecar (atomic rename). */
|
|
640
|
+
private static writeSeqCounter;
|
|
641
|
+
/**
|
|
642
|
+
* Verifies that a file's embedded write counter is ≥ the last-seen counter
|
|
643
|
+
* stored in the `.seq` sidecar. Call after key derivation (needs plaintext
|
|
644
|
+
* meta) but before returning the loaded SPD instance.
|
|
645
|
+
* Throws if the counter indicates a rollback attack.
|
|
646
|
+
*/
|
|
647
|
+
private static checkRollback;
|
|
148
648
|
saveToFile(outputPath: string, passcode: string): Promise<void>;
|
|
149
649
|
/**
|
|
150
650
|
* Saves to file using streaming I/O — supports files larger than 2 GB.
|
|
151
651
|
* Format: [8B LE uint64 plaintext length][64B HMAC-SHA3-512][zlib(plaintext)]
|
|
152
652
|
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
653
|
+
* Embeds a tail index (SPDx) for fast on-demand `getEntry()` lookups.
|
|
654
|
+
* Acquires an advisory lock on `outputPath` and releases it after save.
|
|
655
|
+
*
|
|
656
|
+
* @param onProgress Optional callback — called with (bytesWritten, totalBytes)
|
|
657
|
+
* as plaintext bytes are fed through the deflate pipeline.
|
|
155
658
|
*/
|
|
156
|
-
saveToFileStreaming(outputPath: string, passcode: string): Promise<void>;
|
|
659
|
+
saveToFileStreaming(outputPath: string, passcode: string, onProgress?: (bytesWritten: number, totalBytes: number) => void): Promise<void>;
|
|
157
660
|
/**
|
|
158
661
|
* Computes the decompressed byte offset of each entry within a binary plaintext buffer.
|
|
159
662
|
* The offset points to the start of that entry's nameLen byte.
|
|
160
663
|
*/
|
|
161
664
|
private static buildOffsetIndex;
|
|
162
665
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* for each entry: [1B nameLen][name bytes][8B LE uint64 decompressedOffset]
|
|
666
|
+
* Appends the entry offset index to the end of an already-written .spd file.
|
|
667
|
+
* Tail format: [32B HMAC-SHA3-256 of body][body: N×(1B nameLen + name + 8B offset)][8B indexStart][4B "SPDx"]
|
|
668
|
+
* The indexStart is the byte offset from the start of the file to the 32B HMAC.
|
|
167
669
|
*/
|
|
168
|
-
private static
|
|
670
|
+
private static embedIndexInFile;
|
|
169
671
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
672
|
+
* Writes a `.spdi` sidecar index file (same body format as the embedded tail
|
|
673
|
+
* but stored externally so it can be pre-warmed without modifying the `.spd`).
|
|
674
|
+
* Format: [4B "SPDI"][32B HMAC-SHA3-256 of body][body]
|
|
675
|
+
*/
|
|
676
|
+
private static writeSidecarIndex;
|
|
677
|
+
/**
|
|
678
|
+
* Reads and validates the embedded tail index from a `.spd` file.
|
|
679
|
+
* Returns null if the file doesn't exist, has no embedded index, or the HMAC fails (falls back to scan).
|
|
172
680
|
*/
|
|
173
681
|
private static readIndexFile;
|
|
174
682
|
saveData(passcode?: string): Promise<string>;
|
|
@@ -183,8 +691,13 @@ declare class SPD {
|
|
|
183
691
|
* - Rounded up to the nearest 64 KB boundary for alignment
|
|
184
692
|
*/
|
|
185
693
|
saveDataChunked(passcode: string, chunkSize?: number): Promise<string[]>;
|
|
186
|
-
static loadFromFile(filePath: string, passcode: string): Promise<SPD>;
|
|
187
|
-
static loadFromFileStreaming(filePath: string, passcode: string): Promise<SPD>;
|
|
694
|
+
static loadFromFile(filePath: string, passcode: string, verifyKey?: Uint8Array): Promise<SPD>;
|
|
695
|
+
static loadFromFileStreaming(filePath: string, passcode: string, onProgress?: (bytesRead: number, totalBytes: number) => void, verifyKey?: Uint8Array): Promise<SPD>;
|
|
696
|
+
/**
|
|
697
|
+
* Auto-destruct helper: reads/increments the open count sidecar (.ocount).
|
|
698
|
+
* If `maxOpenCount` is set and the new count exceeds it, wipes the file and throws.
|
|
699
|
+
*/
|
|
700
|
+
private static checkAndIncrementOpenCount;
|
|
188
701
|
/**
|
|
189
702
|
* True constant-memory streaming extract from a binary SPD file.
|
|
190
703
|
*
|
|
@@ -211,7 +724,7 @@ declare class SPD {
|
|
|
211
724
|
* console.log(name, typeof value);
|
|
212
725
|
* });
|
|
213
726
|
*/
|
|
214
|
-
static extractFromFileStreaming(filePath: string, passcode: string, tmpDir: string, callback: (name: string, value: unknown) => Promise<void> | void): Promise<void>;
|
|
727
|
+
static extractFromFileStreaming(filePath: string, passcode: string, tmpDir: string, callback: (name: string, value: unknown) => Promise<void> | void, onProgress?: (bytesRead: number, totalBytes: number) => void): Promise<void>;
|
|
215
728
|
/**
|
|
216
729
|
* On-demand disk lookup — decrypts and returns a single named entry from a
|
|
217
730
|
* binary `.spd` file (written by `saveToFileStreaming`) without loading the
|
|
@@ -227,13 +740,48 @@ declare class SPD {
|
|
|
227
740
|
* RAM usage is bounded to ~one entry's worth of bytes at any time.
|
|
228
741
|
*/
|
|
229
742
|
static getEntry(filePath: string, passcode: string, name: string): Promise<SPDGetEntryResult>;
|
|
743
|
+
/**
|
|
744
|
+
* Reads file metadata without any key derivation or decryption.
|
|
745
|
+
*
|
|
746
|
+
* For binary `.spd` files (written by `saveToFileStreaming` / `SPDWriter`):
|
|
747
|
+
* reads the 72-byte header + decompresses just the meta block.
|
|
748
|
+
* For JSON `.spd` files (written by `saveToFile`):
|
|
749
|
+
* decompresses and parses the outer JSON + payload JSON.
|
|
750
|
+
*
|
|
751
|
+
* No passcode required. Returns format, version, entry count, Argon2 params,
|
|
752
|
+
* file size, and whether a tail index (SPDx) is embedded.
|
|
753
|
+
*/
|
|
754
|
+
static inspect(filePath: string): Promise<SPDInspectResult>;
|
|
755
|
+
/**
|
|
756
|
+
* Verifies file integrity (MAC check) without decrypting any entry values.
|
|
757
|
+
* Derives keys from passcode, verifies the HMAC over the entire plaintext,
|
|
758
|
+
* and returns a result object. Does not call any decrypt operation on entries.
|
|
759
|
+
*
|
|
760
|
+
* Useful for backup health checks: fast, safe, no plaintext produced.
|
|
761
|
+
*/
|
|
762
|
+
static verify(filePath: string, passcode: string): Promise<SPDVerifyResult>;
|
|
230
763
|
static loadFromString(data: string, passcode: string): Promise<SPD>;
|
|
231
764
|
static loadFromChunks(chunks: string[], passcode: string): Promise<SPD>;
|
|
232
765
|
static derivePBK(passcode: string, salt: Uint8Array, memory?: number, time?: number, hashLen?: number): Promise<PBKResult>;
|
|
233
766
|
static decryptSalt(encryptedSalt: number[], saltNonce: number[], wrapSalt: number[], passcode: string, memory?: number, time?: number): Promise<Uint8Array>;
|
|
234
|
-
static encryptSalt(salt: Uint8Array, passcode: string): Promise<EncryptedSaltResult>;
|
|
767
|
+
static encryptSalt(salt: Uint8Array, passcode: string, memory?: number, time?: number): Promise<EncryptedSaltResult>;
|
|
235
768
|
static toBase64(data: Uint8Array | Buffer): string;
|
|
236
769
|
static fromBase64(data: string): Uint8Array;
|
|
770
|
+
/**
|
|
771
|
+
* Build a SHA3-256 Merkle tree over an array of leaf hashes (hex strings).
|
|
772
|
+
* Returns the root hash as a hex string. Empty input returns the hash of "".
|
|
773
|
+
*/
|
|
774
|
+
private static buildMerkleRoot;
|
|
775
|
+
private static computeCounterHMAC;
|
|
776
|
+
private _applyRatchet;
|
|
777
|
+
/** When ratchet is enabled, rotate to a fresh salt+keys before each save,
|
|
778
|
+
* re-encrypting all entries under the new key.
|
|
779
|
+
* Forward secrecy is achieved because each save uses a different salt.
|
|
780
|
+
*/
|
|
781
|
+
private _maybeRatchetSalt;
|
|
782
|
+
private _maybeRotateEpoch;
|
|
783
|
+
/** Get the effective AEAD and MAC keys (handles blinding). */
|
|
784
|
+
private _getKeys;
|
|
237
785
|
private buildSerializedPayload;
|
|
238
786
|
private static parseSerializedPayload;
|
|
239
787
|
private buildBinaryPayload;
|
|
@@ -243,6 +791,64 @@ declare class SPD {
|
|
|
243
791
|
private isCollectionType;
|
|
244
792
|
private convertInputToString;
|
|
245
793
|
private convertStringToInput;
|
|
794
|
+
/**
|
|
795
|
+
* Compares two SPD files by entry name and hash — no entry decryption.
|
|
796
|
+
* Only key derivation is performed (to verify MACs before trusting hashes).
|
|
797
|
+
*
|
|
798
|
+
* @returns `{added, removed, modified}` arrays of sanitized entry names.
|
|
799
|
+
* `added` — names present in `fileB` but not `fileA`
|
|
800
|
+
* `removed` — names present in `fileA` but not `fileB`
|
|
801
|
+
* `modified` — names present in both but with different ciphertext hashes
|
|
802
|
+
*/
|
|
803
|
+
static diff(fileA: string, fileB: string, passcode: string): Promise<SPDDiffResult>;
|
|
804
|
+
/**
|
|
805
|
+
* Merges two SPD files into a new in-memory instance.
|
|
806
|
+
* - `'a-wins'` : conflicting entries keep file A's version.
|
|
807
|
+
* - `'b-wins'` : conflicting entries keep file B's version.
|
|
808
|
+
* - `'newer'` : keeps the version from the file with the higher `writeCounter`
|
|
809
|
+
* (file-level granularity, not per-entry).
|
|
810
|
+
*
|
|
811
|
+
* The returned instance uses file A's key material. Call `saveToFile` /
|
|
812
|
+
* `saveToFileStreaming` to persist.
|
|
813
|
+
*/
|
|
814
|
+
static merge(fileA: string, fileB: string, passcode: string, options?: SPDMergeOptions): Promise<SPD>;
|
|
815
|
+
/**
|
|
816
|
+
* Attempts partial recovery of a corrupt SPD file.
|
|
817
|
+
* Reads entries sequentially until the first parse/decrypt failure, then saves
|
|
818
|
+
* whatever was recovered to `outputPath`.
|
|
819
|
+
*
|
|
820
|
+
* Returns `{recovered, skipped}` counts. If the file is a valid JSON payload
|
|
821
|
+
* it uses the JSON loader; otherwise it attempts binary streaming entry-by-entry.
|
|
822
|
+
*/
|
|
823
|
+
static repair(filePath: string, passcode: string, outputPath: string): Promise<SPDRepairResult>;
|
|
824
|
+
/**
|
|
825
|
+
* Runs an in-memory self-test and returns timing/throughput numbers.
|
|
826
|
+
* Useful for choosing `argon2Memory`/`argon2Time` on the target hardware.
|
|
827
|
+
*
|
|
828
|
+
* Does NOT write any files or use any persistent state.
|
|
829
|
+
*/
|
|
830
|
+
static benchmark(): Promise<SPDBenchmarkResult>;
|
|
831
|
+
/**
|
|
832
|
+
* Watches `filePath` for external changes and calls `cb` with the freshly
|
|
833
|
+
* loaded SPD instance and a diff report each time the file is modified.
|
|
834
|
+
*
|
|
835
|
+
* ```ts
|
|
836
|
+
* const stop = SPD.watch('/data/secrets.spd', 'pass', (spd, diff) => {
|
|
837
|
+
* console.log('added:', diff.added);
|
|
838
|
+
* });
|
|
839
|
+
* // … later:
|
|
840
|
+
* stop();
|
|
841
|
+
* ```
|
|
842
|
+
*
|
|
843
|
+
* - Uses `fs.watchFile` (stat polling, 1 s interval) so it works on NFS /
|
|
844
|
+
* Docker volumes where `fs.watch` inotify events are unreliable.
|
|
845
|
+
* - Re-loads via `SPD.loadFromFile` (JSON) with a `SPD.loadFromFileStreaming`
|
|
846
|
+
* fallback for binary files.
|
|
847
|
+
* - If the reload throws, the error is swallowed and `cb` is **not** called
|
|
848
|
+
* (avoids crashing the host process on transient FS errors).
|
|
849
|
+
* - Returns a `stop()` function; call it to un-watch and stop polling.
|
|
850
|
+
*/
|
|
851
|
+
static watch(filePath: string, passcode: string, cb: (spd: SPD, diff: SPDDiffResult) => void): () => void;
|
|
246
852
|
}
|
|
247
853
|
|
|
248
854
|
/**
|
|
@@ -336,8 +942,8 @@ declare class SPDVault {
|
|
|
336
942
|
* await writer.addEntry('config', { theme: 'dark' });
|
|
337
943
|
* await writer.finalize();
|
|
338
944
|
*
|
|
339
|
-
* After finalize(),
|
|
340
|
-
*
|
|
945
|
+
* After finalize(), an index tail is embedded in the file itself so that
|
|
946
|
+
* `SPD.getEntry()` can locate entries without a full scan (no sidecar file).
|
|
341
947
|
*/
|
|
342
948
|
|
|
343
949
|
declare class SPDWriter {
|
|
@@ -374,7 +980,307 @@ declare class SPDWriter {
|
|
|
374
980
|
destroy(): void;
|
|
375
981
|
/** Write bytes to the deflate stream and update the rolling HMAC + offset. */
|
|
376
982
|
private writeToPlaintext;
|
|
377
|
-
|
|
983
|
+
/**
|
|
984
|
+
* Appends the entry offset index to the end of the output file.
|
|
985
|
+
* Format: [32B HMAC-SHA3-256 of body][body][8B indexStart][4B "SPDx"]
|
|
986
|
+
* NOTE: macKey is still valid here — zeroed by finalize() AFTER this call.
|
|
987
|
+
*/
|
|
988
|
+
private embedIndexTail;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* SPDTransport — authenticated, forward-secret client↔server secure channel.
|
|
993
|
+
*
|
|
994
|
+
* ## Protocol versions
|
|
995
|
+
*
|
|
996
|
+
* V1 (original): ephemeral Curve25519 ECDH only
|
|
997
|
+
* V2 (v29+): hybrid Curve25519 + ML-KEM-768, ML-DSA-65 certificate pinning,
|
|
998
|
+
* PSK session resumption
|
|
999
|
+
*
|
|
1000
|
+
* ## What it provides
|
|
1001
|
+
*
|
|
1002
|
+
* - **Ephemeral ECDH handshake** (Curve25519 via libsodium `crypto_kx`) so
|
|
1003
|
+
* every session produces a fresh pair of symmetric session keys.
|
|
1004
|
+
* - **ML-KEM-768 hybrid**: client additionally encapsulates to the server's
|
|
1005
|
+
* ML-KEM public key; the KEM shared secret is XOR-combined with the ECDH
|
|
1006
|
+
* secret so that breaking EITHER primitive does not compromise the session.
|
|
1007
|
+
* - **ML-DSA-65 certificate pinning**: the server's long-term identity can
|
|
1008
|
+
* include an ML-DSA-65 signing key pair; clients pin the ML-DSA public key
|
|
1009
|
+
* and the server signs each ServerHello, preventing MitM.
|
|
1010
|
+
* - **PSK session resumption**: a 32-byte pre-shared key can be mixed into
|
|
1011
|
+
* the session key derivation, allowing clients to resume a prior session
|
|
1012
|
+
* with an additional secret layer.
|
|
1013
|
+
* - **Forward secrecy**: all ephemeral key material is zeroed after handshake.
|
|
1014
|
+
* - **Two independent session streams**: `clientToServer` and `serverToClient`,
|
|
1015
|
+
* each with a monotonic 64-bit counter mixed into the XChaCha20-Poly1305
|
|
1016
|
+
* nonce — prevents nonce reuse and blocks replay attacks.
|
|
1017
|
+
* - **Message authentication**: every frame carries a Poly1305 tag.
|
|
1018
|
+
* - **Optional SPD payload wrapping**: `wrapSPD` / `unwrapSPD` let you
|
|
1019
|
+
* transmit an in-memory SPD binary payload over the channel.
|
|
1020
|
+
*
|
|
1021
|
+
* ## Threat model
|
|
1022
|
+
*
|
|
1023
|
+
* ✅ Network eavesdropper — cannot read any session payload
|
|
1024
|
+
* ✅ Replay attack — monotonic counter prevents replaying old frames
|
|
1025
|
+
* ✅ Past-session decryption after key compromise — forward secrecy
|
|
1026
|
+
* ✅ Active MitM — ML-DSA-65 certificate pinning + ECDH server auth
|
|
1027
|
+
* ✅ Harvest-now-decrypt-later (quantum) — ML-KEM-768 hybrid
|
|
1028
|
+
* ✅ PSK gives additional session binding layer for high-security deployments
|
|
1029
|
+
*
|
|
1030
|
+
* ## V2 ClientHello wire format (1249 bytes)
|
|
1031
|
+
*
|
|
1032
|
+
* [1B version ] = TRANSPORT_VERSION_V2 (2)
|
|
1033
|
+
* [32B ecdhEphemeralPub] client's ephemeral Curve25519 public key
|
|
1034
|
+
* [32B sessionSalt ] random HKDF salt (public)
|
|
1035
|
+
* [1184B kemEphemeralPub] client's ephemeral ML-KEM-768 public key
|
|
1036
|
+
*
|
|
1037
|
+
* ## V2 ServerHello wire format (1 + 1088 + 3309 = 4398 bytes)
|
|
1038
|
+
*
|
|
1039
|
+
* [1B version ] = TRANSPORT_VERSION_V2 (2)
|
|
1040
|
+
* [1088B kemCiphertext ] ML-KEM-768 ciphertext (encapsulated by server to client's kemPub)
|
|
1041
|
+
* [3309B signature ] ML-DSA-65 signature over (kemCiphertext || sessionSalt || ecdhEphemeralPub)
|
|
1042
|
+
* — present only if server has a signing key; all zeros if absent
|
|
1043
|
+
*
|
|
1044
|
+
* ## Usage
|
|
1045
|
+
*
|
|
1046
|
+
* ```ts
|
|
1047
|
+
* // ── Server side ──────────────────────────────────────────────────────────
|
|
1048
|
+
* const serverIdentity = await SPDTransport.generateServerIdentity();
|
|
1049
|
+
* // Optional: add ML-DSA-65 signing
|
|
1050
|
+
* const signingPair = SPDTransport.generateCertKeyPair();
|
|
1051
|
+
* serverIdentity.signingPrivKey = signingPair.privateKey;
|
|
1052
|
+
* serverIdentity.signingPubKey = signingPair.publicKey;
|
|
1053
|
+
*
|
|
1054
|
+
* const { session, serverHello } = await SPDTransport.serverAccept(serverIdentity, clientHello);
|
|
1055
|
+
* session.decrypt(encryptedFrame);
|
|
1056
|
+
* session.encrypt(responseBytes);
|
|
1057
|
+
*
|
|
1058
|
+
* // ── Client side ──────────────────────────────────────────────────────────
|
|
1059
|
+
* const { session, clientHello } = await SPDTransport.clientConnect(serverPublicKey, {
|
|
1060
|
+
* pinnedSigningKey: serverSigningPubKey, // optional cert pin
|
|
1061
|
+
* psk: sharedSecret32, // optional PSK
|
|
1062
|
+
* });
|
|
1063
|
+
* send(clientHello);
|
|
1064
|
+
* const serverHello = receive(); // get serverHello from server
|
|
1065
|
+
* await session.processServerHello(serverHello);
|
|
1066
|
+
* session.encrypt(payload);
|
|
1067
|
+
*
|
|
1068
|
+
* // ── PSK session resumption ────────────────────────────────────────────────
|
|
1069
|
+
* const psk = session.exportPSK(); // save after first handshake
|
|
1070
|
+
* // next connection:
|
|
1071
|
+
* const { session: session2, clientHello: ch2 } = await SPDTransport.clientConnect(
|
|
1072
|
+
* serverPublicKey, { psk }
|
|
1073
|
+
* );
|
|
1074
|
+
* ```
|
|
1075
|
+
*/
|
|
1076
|
+
|
|
1077
|
+
interface SPDServerIdentity {
|
|
1078
|
+
/** Curve25519 public key — distribute to clients out-of-band. */
|
|
1079
|
+
publicKey: Uint8Array;
|
|
1080
|
+
/** Curve25519 private key — keep secret, never transmit. */
|
|
1081
|
+
privateKey: Uint8Array;
|
|
1082
|
+
/** Optional ML-DSA-65 signing private key for certificate authentication. */
|
|
1083
|
+
signingPrivKey?: Uint8Array;
|
|
1084
|
+
/** Optional ML-DSA-65 signing public key — pin this on the client side. */
|
|
1085
|
+
signingPubKey?: Uint8Array;
|
|
1086
|
+
}
|
|
1087
|
+
/** Options for `clientConnect`. */
|
|
1088
|
+
interface SPDClientConnectOptions {
|
|
1089
|
+
/**
|
|
1090
|
+
* Use V2 protocol (ML-KEM hybrid). Default: true when server supports it.
|
|
1091
|
+
* Set to false to force V1 (legacy interop).
|
|
1092
|
+
*/
|
|
1093
|
+
v2?: boolean;
|
|
1094
|
+
/**
|
|
1095
|
+
* Pinned ML-DSA-65 public key. When set, the client will verify the
|
|
1096
|
+
* server's signature in the ServerHello and reject if verification fails.
|
|
1097
|
+
*/
|
|
1098
|
+
pinnedSigningKey?: Uint8Array;
|
|
1099
|
+
/**
|
|
1100
|
+
* Pre-shared key (32 bytes) mixed into session key derivation.
|
|
1101
|
+
* Provides an extra secret layer for PSK session resumption.
|
|
1102
|
+
*/
|
|
1103
|
+
psk?: Uint8Array;
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* A live, established session between two peers.
|
|
1107
|
+
* Maintains independent monotonic counters for each direction.
|
|
1108
|
+
*/
|
|
1109
|
+
interface SPDSession {
|
|
1110
|
+
/**
|
|
1111
|
+
* Encrypt `plaintext` for transmission in this session's direction.
|
|
1112
|
+
* Returns a self-contained frame: [1B version][8B counter][24B nonce][ciphertext+tag]
|
|
1113
|
+
*/
|
|
1114
|
+
encrypt(plaintext: Uint8Array | Buffer): Buffer;
|
|
1115
|
+
/**
|
|
1116
|
+
* Decrypt a frame received from the remote peer.
|
|
1117
|
+
* Throws if the tag is invalid, the version is wrong, or the counter is not
|
|
1118
|
+
* strictly greater than the last seen counter (replay protection).
|
|
1119
|
+
*/
|
|
1120
|
+
decrypt(frame: Buffer): Buffer;
|
|
1121
|
+
/**
|
|
1122
|
+
* Serialize `spd` to its binary format in memory and encrypt the result as a
|
|
1123
|
+
* single transport frame. The SPD is NOT written to disk.
|
|
1124
|
+
*/
|
|
1125
|
+
wrapSPD(spd: SPD, passcode: string): Promise<Buffer>;
|
|
1126
|
+
/**
|
|
1127
|
+
* Decrypt a frame produced by `wrapSPD` and load the embedded SPD binary.
|
|
1128
|
+
* Returns a ready-to-use SPD instance.
|
|
1129
|
+
*/
|
|
1130
|
+
unwrapSPD(frame: Buffer, passcode: string): Promise<SPD>;
|
|
1131
|
+
/**
|
|
1132
|
+
* Export a 32-byte PSK derived from the session key material.
|
|
1133
|
+
* Store this to enable fast PSK resumption on the next connection.
|
|
1134
|
+
*/
|
|
1135
|
+
exportPSK(): Uint8Array;
|
|
1136
|
+
/** Zero all session key material. Call when the session is complete. */
|
|
1137
|
+
destroy(): void;
|
|
1138
|
+
}
|
|
1139
|
+
declare class SPDTransport {
|
|
1140
|
+
/**
|
|
1141
|
+
* Generate a long-term Curve25519 server identity key pair.
|
|
1142
|
+
* Store `privateKey` securely (e.g. in a hardware key store or SPD vault).
|
|
1143
|
+
* Distribute `publicKey` to clients out-of-band.
|
|
1144
|
+
*/
|
|
1145
|
+
static generateServerIdentity(): Promise<SPDServerIdentity>;
|
|
1146
|
+
/**
|
|
1147
|
+
* Generate an ML-DSA-65 certificate key pair for server signing.
|
|
1148
|
+
* Set `signingPrivKey` + `signingPubKey` on the server identity, and
|
|
1149
|
+
* distribute `publicKey` to clients as a pinned cert.
|
|
1150
|
+
*/
|
|
1151
|
+
static generateCertKeyPair(): {
|
|
1152
|
+
privateKey: Uint8Array;
|
|
1153
|
+
publicKey: Uint8Array;
|
|
1154
|
+
};
|
|
1155
|
+
/**
|
|
1156
|
+
* Save a server identity to files:
|
|
1157
|
+
* `<path>.pub` (Curve25519 public key, safe to distribute)
|
|
1158
|
+
* `<path>.key` (Curve25519 private key, mode 0600)
|
|
1159
|
+
* `<path>.cert` (ML-DSA-65 public key, safe to distribute; optional)
|
|
1160
|
+
* `<path>.csig` (ML-DSA-65 private key, mode 0600; optional)
|
|
1161
|
+
*/
|
|
1162
|
+
static saveServerIdentity(basePath: string, identity: SPDServerIdentity): void;
|
|
1163
|
+
/**
|
|
1164
|
+
* Load a server identity previously saved with `saveServerIdentity`.
|
|
1165
|
+
*/
|
|
1166
|
+
static loadServerIdentity(basePath: string): SPDServerIdentity;
|
|
1167
|
+
/**
|
|
1168
|
+
* Initiate a new V2 session as the client (ML-KEM-768 hybrid + optional PSK + cert pinning).
|
|
1169
|
+
*
|
|
1170
|
+
* Returns `{ session, clientHello }`. After receiving the ServerHello, call
|
|
1171
|
+
* `await session.processServerHello(serverHello)` to complete the handshake.
|
|
1172
|
+
*
|
|
1173
|
+
* @param serverPublicKey The server's Curve25519 long-term public key.
|
|
1174
|
+
* @param options Optional: pinnedSigningKey, psk, v2 flag.
|
|
1175
|
+
*/
|
|
1176
|
+
static clientConnect(serverPublicKey: Uint8Array, options?: SPDClientConnectOptions): Promise<{
|
|
1177
|
+
session: SPDClientHandshake;
|
|
1178
|
+
clientHello: Buffer;
|
|
1179
|
+
}>;
|
|
1180
|
+
/** V1 legacy clientConnect */
|
|
1181
|
+
private static _clientConnectV1;
|
|
1182
|
+
/**
|
|
1183
|
+
* Accept an incoming client connection (supports both V1 and V2 ClientHello).
|
|
1184
|
+
*
|
|
1185
|
+
* For V2: returns `{ session, serverHello }`. Send `serverHello` to the client
|
|
1186
|
+
* so it can finalize key derivation.
|
|
1187
|
+
*
|
|
1188
|
+
* @param serverIdentity The server's long-term key pair.
|
|
1189
|
+
* @param clientHello The raw bytes sent by the client.
|
|
1190
|
+
* @param psk Optional PSK (must match client's PSK for session to work).
|
|
1191
|
+
*/
|
|
1192
|
+
static serverAccept(serverIdentity: SPDServerIdentity, clientHello: Buffer, psk?: Uint8Array): Promise<{
|
|
1193
|
+
session: SPDSession;
|
|
1194
|
+
serverHello?: Buffer;
|
|
1195
|
+
}>;
|
|
1196
|
+
/** V1 legacy serverAccept */
|
|
1197
|
+
private static _serverAcceptV1;
|
|
1198
|
+
/** V2 serverAccept: hybrid ECDH + ML-KEM, optional ML-DSA-65 signing */
|
|
1199
|
+
private static _serverAcceptV2;
|
|
1200
|
+
/**
|
|
1201
|
+
* Convenience: generate a random 32-byte session token suitable for use as
|
|
1202
|
+
* an application-level session ID (e.g. HTTP cookie, WebSocket session key).
|
|
1203
|
+
*/
|
|
1204
|
+
static generateSessionToken(): string;
|
|
1205
|
+
/**
|
|
1206
|
+
* Verify that a ClientHello buffer has the correct structure and version
|
|
1207
|
+
* without performing the full key exchange.
|
|
1208
|
+
*/
|
|
1209
|
+
static validateClientHello(clientHello: Buffer): boolean;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Intermediate object returned by `clientConnect` (V2).
|
|
1213
|
+
* Call `processServerHello(serverHello)` to finalize key derivation.
|
|
1214
|
+
* After that, use `encrypt` / `decrypt` normally.
|
|
1215
|
+
*/
|
|
1216
|
+
interface SPDClientHandshake extends SPDSession {
|
|
1217
|
+
/**
|
|
1218
|
+
* Process the ServerHello received from the server and finalize session keys.
|
|
1219
|
+
* Must be called before `encrypt` or `decrypt`.
|
|
1220
|
+
* @param serverHello Raw bytes received from `serverAccept`.
|
|
1221
|
+
*/
|
|
1222
|
+
processServerHello(serverHello: Buffer): void;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* SPDShamir — Shamir's Secret Sharing over GF(256).
|
|
1227
|
+
*
|
|
1228
|
+
* Splits a byte-array secret into `n` shares, any `k` of which can
|
|
1229
|
+
* reconstruct the original (k-of-n threshold scheme).
|
|
1230
|
+
*
|
|
1231
|
+
* ## Security properties
|
|
1232
|
+
* - Information-theoretically secure: fewer than `k` shares reveal
|
|
1233
|
+
* nothing about the secret.
|
|
1234
|
+
* - Each share is the same length as the secret + 1 byte (x-coordinate).
|
|
1235
|
+
* - Arithmetic is done in GF(2^8) with the irreducible polynomial
|
|
1236
|
+
* x^8 + x^4 + x^3 + x + 1 (0x11b — same as AES).
|
|
1237
|
+
*
|
|
1238
|
+
* ## Usage
|
|
1239
|
+
*
|
|
1240
|
+
* ```ts
|
|
1241
|
+
* import { SPDShamir } from 'spd-lib-ts';
|
|
1242
|
+
*
|
|
1243
|
+
* const secret = Buffer.from('my 32-byte secret key material!');
|
|
1244
|
+
* const shares = SPDShamir.split(secret, 5, 3); // 5 shares, 3 required
|
|
1245
|
+
* const rebuilt = SPDShamir.combine(shares.slice(1, 4)); // any 3
|
|
1246
|
+
* // rebuilt.equals(secret) === true
|
|
1247
|
+
* ```
|
|
1248
|
+
*
|
|
1249
|
+
* ## Wire format
|
|
1250
|
+
*
|
|
1251
|
+
* Each `SPDShamirShare` is a `Uint8Array` where:
|
|
1252
|
+
* - `share[0]` = x-coordinate (1..255, never 0)
|
|
1253
|
+
* - `share[1..]` = f(x) evaluated for each byte of the secret
|
|
1254
|
+
*
|
|
1255
|
+
* Shares may be base64-encoded for storage / transmission.
|
|
1256
|
+
*/
|
|
1257
|
+
type SPDShamirShare = Uint8Array;
|
|
1258
|
+
declare class SPDShamir {
|
|
1259
|
+
/**
|
|
1260
|
+
* Split `secret` into `n` shares with threshold `k`.
|
|
1261
|
+
* Any `k` shares can reconstruct the secret; fewer than `k` reveal nothing.
|
|
1262
|
+
*
|
|
1263
|
+
* @param secret The byte-array to protect (any length > 0).
|
|
1264
|
+
* @param n Total number of shares to generate (2 ≤ n ≤ 255).
|
|
1265
|
+
* @param k Reconstruction threshold (2 ≤ k ≤ n).
|
|
1266
|
+
* @returns Array of `n` shares, each with `x` in `[0]` and `f(x)` in `[1..]`.
|
|
1267
|
+
*/
|
|
1268
|
+
static split(secret: Uint8Array | Buffer, n: number, k: number): SPDShamirShare[];
|
|
1269
|
+
/**
|
|
1270
|
+
* Combine `k` (or more) shares to reconstruct the original secret.
|
|
1271
|
+
*
|
|
1272
|
+
* @param shares Array of at least `k` shares. Order does not matter.
|
|
1273
|
+
* @returns The reconstructed secret as a `Uint8Array`.
|
|
1274
|
+
*/
|
|
1275
|
+
static combine(shares: SPDShamirShare[]): Uint8Array;
|
|
1276
|
+
/**
|
|
1277
|
+
* Encode a share to a base64url string for safe storage/transmission.
|
|
1278
|
+
*/
|
|
1279
|
+
static encodeShare(share: SPDShamirShare): string;
|
|
1280
|
+
/**
|
|
1281
|
+
* Decode a base64url-encoded share back to `Uint8Array`.
|
|
1282
|
+
*/
|
|
1283
|
+
static decodeShare(encoded: string): SPDShamirShare;
|
|
378
1284
|
}
|
|
379
1285
|
|
|
380
|
-
export { ARGON2_MEMORY_HIGH, ARGON2_TIME_HIGH, type DataInput, type EncryptedDataEntry, type EncryptedSaltResult, type HashAlgorithm, type PBKResult, type PQCKey, type PQCKeyResult, SPD, type SPDChunkManifest, type SPDGetEntryResult, type SPDIndexEntry, SPDLegacy, type SPDLegacyPayload, type SPDPayload, SPDVault, SPDWriter, type SPDWriterOptions, SPDLegacy as SPD_LEG, SPDVault as SPD_Vault, type SerializedDataEntry, type SerializedWrappedPayload, type SupportedDataType, type SupportedValue, type TypedArray, type WrappedPayload };
|
|
1286
|
+
export { ARGON2_MEMORY_HIGH, ARGON2_MEMORY_PARANOID, ARGON2_TIME_HIGH, ARGON2_TIME_PARANOID, type DataInput, type EncryptedDataEntry, type EncryptedSaltResult, type HashAlgorithm, type PBKResult, type PQCKey, type PQCKeyResult, SPD, type SPDBenchmarkResult, type SPDChunkManifest, type SPDClientConnectOptions, type SPDClientHandshake, type SPDDiffResult, type SPDGetEntryResult, type SPDIndexEntry, type SPDInspectResult, type SPDKeyProfile, type SPDKeyProvider, SPDLegacy, type SPDLegacyPayload, type SPDLogEvent, type SPDMergeOptions, type SPDPayload, type SPDRepairResult, type SPDServerIdentity, type SPDSession, SPDShamir, type SPDShamirShare, type SPDSigningKeyPair, type SPDSnapshot, SPDTransport, SPDVault, type SPDVerifyResult, SPDWriter, type SPDWriterOptions, SPDLegacy as SPD_LEG, SPDVault as SPD_Vault, type SerializedDataEntry, type SerializedWrappedPayload, type SupportedDataType, type SupportedValue, type TypedArray, type WrappedPayload };
|