spd-lib 1.3.5 → 1.3.7

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