@push.rocks/smartsecret 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1834 @@
1
+ import * as plugins from './smartsecret.plugins.js';
2
+ import {
3
+ SmartSecretKernelStoreError,
4
+ } from './smartsecret.kernel.error.js';
5
+ import {
6
+ normalizeSmartSecretKernelAccount,
7
+ normalizeSmartSecretKernelService,
8
+ type ISmartSecretKernelOperationOptions,
9
+ type TSmartSecretKernelMoveStatus,
10
+ } from './smartsecret.kernel.protocol.js';
11
+ import {
12
+ SmartSecretSealedFileStoreError,
13
+ createSmartSecretSealedFileStoreError,
14
+ } from './smartsecret.sealedfile.error.js';
15
+
16
+ export const smartSecretSealedFileMaximumEntryBytes = 512 * 1_024;
17
+
18
+ const schemaVersion = 1 as const;
19
+ const profile = 'smartsecret-kernel-aes-256-gcm-file-v1' as const;
20
+ const manifestName = 'manifest.json';
21
+ const relocationReceiptName = '.smartsecret-relocation.json';
22
+ const masterKeyBytes = 32;
23
+ const maximumEnvelopeBytes = 768 * 1_024;
24
+ const operationTimeoutMs = 60_000;
25
+ const canonicalDigestPattern = /^[A-Za-z0-9_-]{43}$/;
26
+ const entryFilePattern = /^[A-Za-z0-9_-]{43}\.sealed\.json$/;
27
+ const temporaryFilePattern = /^\.smartsecret-[A-Za-z0-9_-]{22}\.tmp$/;
28
+ const maximumPendingOperations = 64;
29
+ const encoder = new TextEncoder();
30
+
31
+ export interface ISmartSecretSealedFileKernelStore {
32
+ readonly service: string;
33
+ getEntry(
34
+ accountArg: string,
35
+ optionsArg?: ISmartSecretKernelOperationOptions,
36
+ ): Promise<Uint8Array | null>;
37
+ setEntry(
38
+ accountArg: string,
39
+ valueArg: Uint8Array,
40
+ optionsArg?: ISmartSecretKernelOperationOptions,
41
+ ): Promise<void>;
42
+ deleteEntry(
43
+ accountArg: string,
44
+ optionsArg?: ISmartSecretKernelOperationOptions,
45
+ ): Promise<boolean>;
46
+ }
47
+
48
+ export interface ISmartSecretSealedFileRelocationKernelStore
49
+ extends ISmartSecretSealedFileKernelStore {
50
+ moveEntry(
51
+ sourceAccountArg: string,
52
+ destinationAccountArg: string,
53
+ optionsArg?: ISmartSecretKernelOperationOptions,
54
+ ): Promise<TSmartSecretKernelMoveStatus>;
55
+ }
56
+
57
+ export interface ISmartSecretSealedFileStoreOptions {
58
+ kernelStore: ISmartSecretSealedFileKernelStore;
59
+ storeId: string;
60
+ directoryPath: string;
61
+ }
62
+
63
+ export interface ISmartSecretSealedFileStoreRelocationOptions {
64
+ kernelStore: ISmartSecretSealedFileRelocationKernelStore;
65
+ storeId: string;
66
+ sourceDirectoryPath: string;
67
+ destinationDirectoryPath: string;
68
+ }
69
+
70
+ interface INormalizedRelocationOptions {
71
+ kernelStore: ISmartSecretSealedFileRelocationKernelStore;
72
+ source: INormalizedOptions;
73
+ destination: INormalizedOptions;
74
+ }
75
+
76
+ interface INormalizedOptions {
77
+ kernelStore: ISmartSecretSealedFileKernelStore;
78
+ service: string;
79
+ storeId: string;
80
+ directoryPath: string;
81
+ masterKeyAccount: string;
82
+ mutexNamespace: string;
83
+ }
84
+
85
+ interface IManifestV1 {
86
+ schemaVersion: 1;
87
+ profile: typeof profile;
88
+ serviceDigest: string;
89
+ storeId: string;
90
+ masterKeyFingerprint: string;
91
+ }
92
+
93
+ interface IEnvelopeV1 {
94
+ schemaVersion: 1;
95
+ profile: typeof profile;
96
+ entryId: string;
97
+ masterKeyFingerprint: string;
98
+ nonce: string;
99
+ ciphertext: string;
100
+ tag: string;
101
+ }
102
+
103
+ interface IDirectoryIdentity {
104
+ device: string;
105
+ inode: string;
106
+ }
107
+
108
+ interface IRelocationReceiptV1 extends IDirectoryIdentity {
109
+ schemaVersion: 1;
110
+ kind: 'relocation';
111
+ profile: typeof profile;
112
+ serviceDigest: string;
113
+ storeId: string;
114
+ sourcePathDigest: string;
115
+ destinationPathDigest: string;
116
+ masterKeyFingerprint: string;
117
+ }
118
+
119
+ interface IRelocationDirectoryInspection {
120
+ manifest: IManifestV1;
121
+ receiptValue?: unknown;
122
+ }
123
+
124
+ interface IRelocationMasterKeyState {
125
+ key: Uint8Array;
126
+ sourcePresent: boolean;
127
+ destinationPresent: boolean;
128
+ }
129
+
130
+ type TLifecycleState = 'ready' | 'closing' | 'closed';
131
+ type TBigIntStats = plugins.fs.BigIntStats;
132
+
133
+ const sha256 = (...valuesArg: Uint8Array[]): Uint8Array => {
134
+ const hash = plugins.crypto.createHash('sha256');
135
+ for (const value of valuesArg) hash.update(value);
136
+ return new Uint8Array(hash.digest());
137
+ };
138
+
139
+ const digest = (...valuesArg: Uint8Array[]): string =>
140
+ Buffer.from(sha256(...valuesArg)).toString('base64url');
141
+
142
+ const fingerprint = (keyArg: Uint8Array): string => digest(
143
+ encoder.encode('@push.rocks/smartsecret/sealed-file/master-key/v1\0'),
144
+ keyArg,
145
+ );
146
+
147
+ const normalizeStoreId = (valueArg: unknown): string => {
148
+ if (
149
+ typeof valueArg !== 'string'
150
+ || !/^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(valueArg)
151
+ ) throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
152
+ return valueArg;
153
+ };
154
+
155
+ const normalizeDirectoryPath = (valueArg: unknown): string => {
156
+ if (
157
+ typeof valueArg !== 'string'
158
+ || !plugins.path.isAbsolute(valueArg)
159
+ || plugins.path.resolve(valueArg) !== valueArg
160
+ || valueArg === plugins.path.parse(valueArg).root
161
+ ) throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
162
+ return valueArg;
163
+ };
164
+
165
+ const dataPropertyValue = (objectArg: object, keyArg: string): unknown => {
166
+ let current: object | null = objectArg;
167
+ while (current) {
168
+ const descriptor = Object.getOwnPropertyDescriptor(current, keyArg);
169
+ if (descriptor) {
170
+ if (!('value' in descriptor)) {
171
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
172
+ }
173
+ return descriptor.value;
174
+ }
175
+ current = Object.getPrototypeOf(current) as object | null;
176
+ }
177
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
178
+ };
179
+
180
+ const normalizeOptions = (optionsArg: ISmartSecretSealedFileStoreOptions): INormalizedOptions => {
181
+ try {
182
+ if (!optionsArg || typeof optionsArg !== 'object' || Array.isArray(optionsArg)) {
183
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
184
+ }
185
+ const keys = Reflect.ownKeys(optionsArg);
186
+ if (
187
+ keys.length !== 3
188
+ || keys.some((keyArg) => typeof keyArg !== 'string')
189
+ || !['kernelStore', 'storeId', 'directoryPath'].every((keyArg) => keys.includes(keyArg))
190
+ ) throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
191
+ const values: Record<string, unknown> = Object.create(null);
192
+ for (const key of keys as string[]) {
193
+ const descriptor = Object.getOwnPropertyDescriptor(optionsArg, key);
194
+ if (!descriptor?.enumerable || !('value' in descriptor)) {
195
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
196
+ }
197
+ values[key] = descriptor.value;
198
+ }
199
+ const kernelStoreValue = values.kernelStore;
200
+ if (
201
+ !kernelStoreValue
202
+ || typeof kernelStoreValue !== 'object'
203
+ ) throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
204
+ const service = normalizeSmartSecretKernelService(
205
+ dataPropertyValue(kernelStoreValue, 'service'),
206
+ );
207
+ const getEntry = dataPropertyValue(kernelStoreValue, 'getEntry');
208
+ const setEntry = dataPropertyValue(kernelStoreValue, 'setEntry');
209
+ const deleteEntry = dataPropertyValue(kernelStoreValue, 'deleteEntry');
210
+ if (
211
+ typeof getEntry !== 'function'
212
+ || typeof setEntry !== 'function'
213
+ || typeof deleteEntry !== 'function'
214
+ ) throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
215
+ const kernelStore: ISmartSecretSealedFileKernelStore = {
216
+ service,
217
+ getEntry: (accountArg, operationOptionsArg) => Reflect.apply(
218
+ getEntry,
219
+ kernelStoreValue,
220
+ [accountArg, operationOptionsArg],
221
+ ) as Promise<Uint8Array | null>,
222
+ setEntry: (accountArg, valueArg, operationOptionsArg) => Reflect.apply(
223
+ setEntry,
224
+ kernelStoreValue,
225
+ [accountArg, valueArg, operationOptionsArg],
226
+ ) as Promise<void>,
227
+ deleteEntry: (accountArg, operationOptionsArg) => Reflect.apply(
228
+ deleteEntry,
229
+ kernelStoreValue,
230
+ [accountArg, operationOptionsArg],
231
+ ) as Promise<boolean>,
232
+ };
233
+ const storeId = normalizeStoreId(values.storeId);
234
+ const directoryPath = normalizeDirectoryPath(values.directoryPath);
235
+ const identity = encoder.encode(`${service}\0${storeId}\0${directoryPath}`);
236
+ return {
237
+ kernelStore,
238
+ service,
239
+ storeId,
240
+ directoryPath,
241
+ masterKeyAccount: `sealed-file-master:v1:${digest(identity)}`,
242
+ mutexNamespace: `smartsecret-sealed-file:v1:${digest(encoder.encode(directoryPath))}`,
243
+ };
244
+ } catch {
245
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
246
+ }
247
+ };
248
+
249
+ const normalizeRelocationOptions = (
250
+ optionsArg: ISmartSecretSealedFileStoreRelocationOptions,
251
+ ): INormalizedRelocationOptions => {
252
+ try {
253
+ if (!optionsArg || typeof optionsArg !== 'object' || Array.isArray(optionsArg)) {
254
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
255
+ }
256
+ const expectedKeys = [
257
+ 'kernelStore',
258
+ 'storeId',
259
+ 'sourceDirectoryPath',
260
+ 'destinationDirectoryPath',
261
+ ];
262
+ const keys = Reflect.ownKeys(optionsArg);
263
+ if (
264
+ keys.length !== expectedKeys.length
265
+ || keys.some((keyArg) => typeof keyArg !== 'string')
266
+ || expectedKeys.some((keyArg) => !keys.includes(keyArg))
267
+ ) throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
268
+ const values: Record<string, unknown> = Object.create(null);
269
+ for (const key of keys as string[]) {
270
+ const descriptor = Object.getOwnPropertyDescriptor(optionsArg, key);
271
+ if (!descriptor?.enumerable || !('value' in descriptor)) {
272
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
273
+ }
274
+ values[key] = descriptor.value;
275
+ }
276
+ const kernelStoreValue = values.kernelStore;
277
+ if (!kernelStoreValue || typeof kernelStoreValue !== 'object') {
278
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
279
+ }
280
+ const moveEntry = dataPropertyValue(kernelStoreValue, 'moveEntry');
281
+ if (typeof moveEntry !== 'function') {
282
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
283
+ }
284
+ const source = normalizeOptions({
285
+ kernelStore: kernelStoreValue as ISmartSecretSealedFileKernelStore,
286
+ storeId: values.storeId as string,
287
+ directoryPath: values.sourceDirectoryPath as string,
288
+ });
289
+ const destination = normalizeOptions({
290
+ kernelStore: kernelStoreValue as ISmartSecretSealedFileKernelStore,
291
+ storeId: values.storeId as string,
292
+ directoryPath: values.destinationDirectoryPath as string,
293
+ });
294
+ if (source.directoryPath === destination.directoryPath) {
295
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
296
+ }
297
+ const kernelStore: ISmartSecretSealedFileRelocationKernelStore = {
298
+ ...source.kernelStore,
299
+ moveEntry: (sourceAccountArg, destinationAccountArg, operationOptionsArg) => Reflect.apply(
300
+ moveEntry,
301
+ kernelStoreValue,
302
+ [sourceAccountArg, destinationAccountArg, operationOptionsArg],
303
+ ) as Promise<TSmartSecretKernelMoveStatus>,
304
+ };
305
+ source.kernelStore = kernelStore;
306
+ destination.kernelStore = kernelStore;
307
+ return { kernelStore, source, destination };
308
+ } catch (errorArg) {
309
+ if (errorArg instanceof SmartSecretSealedFileStoreError) throw errorArg;
310
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
311
+ }
312
+ };
313
+
314
+ const exactObject = (
315
+ valueArg: unknown,
316
+ keysArg: readonly string[],
317
+ errorCodeArg: 'ENVELOPE_INVALID' | 'FILESYSTEM_FAILED',
318
+ ): Record<string, unknown> => {
319
+ try {
320
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
321
+ throw createSmartSecretSealedFileStoreError(errorCodeArg);
322
+ }
323
+ const prototype = Object.getPrototypeOf(valueArg);
324
+ const keys = Reflect.ownKeys(valueArg);
325
+ if (
326
+ (prototype !== Object.prototype && prototype !== null)
327
+ || keys.length !== keysArg.length
328
+ || keys.some((keyArg) => typeof keyArg !== 'string')
329
+ || keysArg.some((keyArg) => !keys.includes(keyArg))
330
+ ) throw createSmartSecretSealedFileStoreError(errorCodeArg);
331
+ const result: Record<string, unknown> = Object.create(null);
332
+ for (const key of keys as string[]) {
333
+ const descriptor = Object.getOwnPropertyDescriptor(valueArg, key);
334
+ if (!descriptor?.enumerable || !('value' in descriptor)) {
335
+ throw createSmartSecretSealedFileStoreError(errorCodeArg);
336
+ }
337
+ result[key] = descriptor.value;
338
+ }
339
+ return result;
340
+ } catch (errorArg) {
341
+ if (errorArg instanceof SmartSecretSealedFileStoreError) throw errorArg;
342
+ throw createSmartSecretSealedFileStoreError(errorCodeArg);
343
+ }
344
+ };
345
+
346
+ const decodeBase64Url = (
347
+ valueArg: unknown,
348
+ maximumBytesArg: number,
349
+ exactBytesArg?: number,
350
+ ): Uint8Array => {
351
+ if (
352
+ typeof valueArg !== 'string'
353
+ || !/^[A-Za-z0-9_-]*$/.test(valueArg)
354
+ || valueArg.length % 4 === 1
355
+ || valueArg.length > Math.ceil(maximumBytesArg * 4 / 3)
356
+ ) throw createSmartSecretSealedFileStoreError('ENVELOPE_INVALID');
357
+ const decoded = Buffer.from(valueArg, 'base64url');
358
+ if (
359
+ decoded.byteLength > maximumBytesArg
360
+ || (exactBytesArg !== undefined && decoded.byteLength !== exactBytesArg)
361
+ || decoded.toString('base64url') !== valueArg
362
+ ) {
363
+ decoded.fill(0);
364
+ throw createSmartSecretSealedFileStoreError('ENVELOPE_INVALID');
365
+ }
366
+ const result = new Uint8Array(decoded);
367
+ decoded.fill(0);
368
+ return result;
369
+ };
370
+
371
+ const parseEnvelope = (valueArg: unknown): {
372
+ envelope: IEnvelopeV1;
373
+ encryptedData: plugins.smartcrypto.IAesGcmCiphertext;
374
+ } => {
375
+ const value = exactObject(valueArg, [
376
+ 'schemaVersion',
377
+ 'profile',
378
+ 'entryId',
379
+ 'masterKeyFingerprint',
380
+ 'nonce',
381
+ 'ciphertext',
382
+ 'tag',
383
+ ], 'ENVELOPE_INVALID');
384
+ if (
385
+ value.schemaVersion !== schemaVersion
386
+ || value.profile !== profile
387
+ || typeof value.entryId !== 'string'
388
+ || !canonicalDigestPattern.test(value.entryId)
389
+ || typeof value.masterKeyFingerprint !== 'string'
390
+ || !canonicalDigestPattern.test(value.masterKeyFingerprint)
391
+ ) throw createSmartSecretSealedFileStoreError('ENVELOPE_INVALID');
392
+ let nonce: Uint8Array | undefined;
393
+ let ciphertext: Uint8Array | undefined;
394
+ let tag: Uint8Array | undefined;
395
+ try {
396
+ nonce = decodeBase64Url(value.nonce, 12, 12);
397
+ ciphertext = decodeBase64Url(value.ciphertext, smartSecretSealedFileMaximumEntryBytes);
398
+ tag = decodeBase64Url(value.tag, 16, 16);
399
+ return {
400
+ envelope: {
401
+ schemaVersion,
402
+ profile,
403
+ entryId: value.entryId,
404
+ masterKeyFingerprint: value.masterKeyFingerprint,
405
+ nonce: value.nonce as string,
406
+ ciphertext: value.ciphertext as string,
407
+ tag: value.tag as string,
408
+ },
409
+ encryptedData: { nonce, ciphertext, tag },
410
+ };
411
+ } catch (errorArg) {
412
+ nonce?.fill(0);
413
+ ciphertext?.fill(0);
414
+ tag?.fill(0);
415
+ throw errorArg;
416
+ }
417
+ };
418
+
419
+ const encodeBase64Url = (valueArg: Uint8Array): string => {
420
+ const copy = Buffer.from(valueArg);
421
+ try {
422
+ return copy.toString('base64url');
423
+ } finally {
424
+ copy.fill(0);
425
+ }
426
+ };
427
+
428
+ const encodeEncryptedData = (
429
+ valueArg: plugins.smartcrypto.IAesGcmCiphertext,
430
+ ): Pick<IEnvelopeV1, 'nonce' | 'ciphertext' | 'tag'> => ({
431
+ nonce: encodeBase64Url(valueArg.nonce),
432
+ ciphertext: encodeBase64Url(valueArg.ciphertext),
433
+ tag: encodeBase64Url(valueArg.tag),
434
+ });
435
+
436
+ const entryId = (optionsArg: INormalizedOptions, accountArg: string): string => digest(
437
+ encoder.encode('@push.rocks/smartsecret/sealed-file/entry/v1\0'),
438
+ encoder.encode(optionsArg.service),
439
+ new Uint8Array([0]),
440
+ encoder.encode(optionsArg.storeId),
441
+ new Uint8Array([0]),
442
+ encoder.encode(accountArg),
443
+ );
444
+
445
+ const entryAad = (
446
+ optionsArg: INormalizedOptions,
447
+ accountArg: string,
448
+ entryIdArg: string,
449
+ fingerprintArg: string,
450
+ ): Uint8Array => encoder.encode(JSON.stringify({
451
+ schemaVersion,
452
+ profile,
453
+ service: optionsArg.service,
454
+ storeId: optionsArg.storeId,
455
+ account: accountArg,
456
+ entryId: entryIdArg,
457
+ masterKeyFingerprint: fingerprintArg,
458
+ }));
459
+
460
+ const manifestFor = (optionsArg: INormalizedOptions, fingerprintArg: string): IManifestV1 => ({
461
+ schemaVersion,
462
+ profile,
463
+ serviceDigest: digest(encoder.encode(optionsArg.service)),
464
+ storeId: optionsArg.storeId,
465
+ masterKeyFingerprint: fingerprintArg,
466
+ });
467
+
468
+ const directoryIdentity = (statsArg: TBigIntStats): IDirectoryIdentity => ({
469
+ device: statsArg.dev.toString(10),
470
+ inode: statsArg.ino.toString(10),
471
+ });
472
+
473
+ const directoryIdentitiesEqual = (
474
+ leftArg: IDirectoryIdentity,
475
+ rightArg: IDirectoryIdentity,
476
+ ): boolean => leftArg.device === rightArg.device && leftArg.inode === rightArg.inode;
477
+
478
+ const relocationReceiptFor = (
479
+ optionsArg: INormalizedRelocationOptions,
480
+ fingerprintArg: string,
481
+ identityArg: IDirectoryIdentity,
482
+ ): IRelocationReceiptV1 => ({
483
+ schemaVersion,
484
+ kind: 'relocation',
485
+ profile,
486
+ serviceDigest: digest(encoder.encode(optionsArg.source.service)),
487
+ storeId: optionsArg.source.storeId,
488
+ sourcePathDigest: digest(encoder.encode(optionsArg.source.directoryPath)),
489
+ destinationPathDigest: digest(encoder.encode(optionsArg.destination.directoryPath)),
490
+ masterKeyFingerprint: fingerprintArg,
491
+ ...identityArg,
492
+ });
493
+
494
+ const parseRelocationReceipt = (
495
+ valueArg: unknown,
496
+ optionsArg: INormalizedRelocationOptions,
497
+ fingerprintArg: string,
498
+ identityArg: IDirectoryIdentity,
499
+ ): IRelocationReceiptV1 => {
500
+ const value = exactObject(valueArg, [
501
+ 'schemaVersion',
502
+ 'kind',
503
+ 'profile',
504
+ 'serviceDigest',
505
+ 'storeId',
506
+ 'sourcePathDigest',
507
+ 'destinationPathDigest',
508
+ 'masterKeyFingerprint',
509
+ 'device',
510
+ 'inode',
511
+ ], 'FILESYSTEM_FAILED');
512
+ const expected = relocationReceiptFor(optionsArg, fingerprintArg, identityArg);
513
+ if (
514
+ value.schemaVersion !== expected.schemaVersion
515
+ || value.kind !== expected.kind
516
+ || value.profile !== expected.profile
517
+ || value.serviceDigest !== expected.serviceDigest
518
+ || value.storeId !== expected.storeId
519
+ || value.sourcePathDigest !== expected.sourcePathDigest
520
+ || value.destinationPathDigest !== expected.destinationPathDigest
521
+ || value.masterKeyFingerprint !== expected.masterKeyFingerprint
522
+ || value.device !== expected.device
523
+ || value.inode !== expected.inode
524
+ ) throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
525
+ return value as unknown as IRelocationReceiptV1;
526
+ };
527
+
528
+ const parseManifest = (valueArg: unknown, optionsArg: INormalizedOptions): IManifestV1 => {
529
+ const value = exactObject(valueArg, [
530
+ 'schemaVersion',
531
+ 'profile',
532
+ 'serviceDigest',
533
+ 'storeId',
534
+ 'masterKeyFingerprint',
535
+ ], 'FILESYSTEM_FAILED');
536
+ if (
537
+ value.schemaVersion !== schemaVersion
538
+ || value.profile !== profile
539
+ || value.serviceDigest !== digest(encoder.encode(optionsArg.service))
540
+ || value.storeId !== optionsArg.storeId
541
+ || typeof value.masterKeyFingerprint !== 'string'
542
+ || !canonicalDigestPattern.test(value.masterKeyFingerprint)
543
+ ) throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
544
+ return value as unknown as IManifestV1;
545
+ };
546
+
547
+ const mapKernelError = (errorArg: unknown): SmartSecretSealedFileStoreError => {
548
+ if (!(errorArg instanceof SmartSecretKernelStoreError)) {
549
+ return createSmartSecretSealedFileStoreError('KERNEL_UNAVAILABLE');
550
+ }
551
+ if (errorArg.code === 'MUTATION_OUTCOME_UNKNOWN') {
552
+ return createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN');
553
+ }
554
+ if (errorArg.code === 'INVALID_ARGUMENT') {
555
+ return createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
556
+ }
557
+ return createSmartSecretSealedFileStoreError('KERNEL_UNAVAILABLE');
558
+ };
559
+
560
+ const mapMutexError = (errorArg: unknown): SmartSecretSealedFileStoreError => {
561
+ if (errorArg instanceof plugins.smartipc.NamedMutexError) {
562
+ if (errorArg.code === 'ABORTED' || errorArg.code === 'TIMEOUT') {
563
+ return createSmartSecretSealedFileStoreError('MUTEX_FAILED');
564
+ }
565
+ }
566
+ return createSmartSecretSealedFileStoreError('MUTEX_FAILED');
567
+ };
568
+
569
+ const directoryHandlePath = (
570
+ directoryHandleArg: plugins.fs.promises.FileHandle,
571
+ nameArg?: string,
572
+ ): string => nameArg
573
+ ? plugins.path.join(`/proc/self/fd/${directoryHandleArg.fd}`, nameArg)
574
+ : `/proc/self/fd/${directoryHandleArg.fd}`;
575
+
576
+ const mutationOutcomeUnknown = (errorArg: unknown): boolean =>
577
+ errorArg instanceof SmartSecretSealedFileStoreError
578
+ && errorArg.code === 'MUTATION_OUTCOME_UNKNOWN';
579
+
580
+ const preferCleanupError = (operationErrorArg: unknown, cleanupErrorArg: unknown): unknown =>
581
+ mutationOutcomeUnknown(operationErrorArg) ? operationErrorArg : cleanupErrorArg;
582
+
583
+ const closeFileHandle = async (handleArg: plugins.fs.promises.FileHandle): Promise<void> => {
584
+ try {
585
+ await handleArg.close();
586
+ } catch {
587
+ try {
588
+ await handleArg.close();
589
+ } catch {}
590
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
591
+ }
592
+ };
593
+
594
+ const closeFileHandleWithError = async (
595
+ handleArg: plugins.fs.promises.FileHandle,
596
+ operationErrorArg: unknown,
597
+ ): Promise<unknown> => {
598
+ try {
599
+ await closeFileHandle(handleArg);
600
+ return operationErrorArg;
601
+ } catch (errorArg) {
602
+ return preferCleanupError(operationErrorArg, errorArg);
603
+ }
604
+ };
605
+
606
+ const wipeDiscardedResult = (valueArg: unknown): void => {
607
+ if (valueArg instanceof Uint8Array) valueArg.fill(0);
608
+ };
609
+
610
+ const validateDirectory = (
611
+ statsArg: TBigIntStats,
612
+ effectiveUidArg: bigint,
613
+ finalArg: boolean,
614
+ ): void => {
615
+ if (!statsArg.isDirectory()) {
616
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
617
+ }
618
+ const mode = statsArg.mode & 0o7777n;
619
+ if (finalArg) {
620
+ if (statsArg.uid !== effectiveUidArg || mode !== 0o700n) {
621
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
622
+ }
623
+ return;
624
+ }
625
+ if (statsArg.uid !== 0n && statsArg.uid !== effectiveUidArg) {
626
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
627
+ }
628
+ if (
629
+ (mode & 0o022n) !== 0n
630
+ && !(statsArg.uid === 0n && (mode & 0o1000n) !== 0n)
631
+ ) throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
632
+ };
633
+
634
+ const openDirectory = async (
635
+ directoryPathArg: string,
636
+ createArg: boolean,
637
+ ): Promise<plugins.fs.promises.FileHandle> => {
638
+ let currentHandle: plugins.fs.promises.FileHandle | undefined;
639
+ try {
640
+ const effectiveUid = process.geteuid?.();
641
+ if (!Number.isSafeInteger(effectiveUid) || effectiveUid! < 0) {
642
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
643
+ }
644
+ const root = plugins.path.parse(directoryPathArg).root;
645
+ const components = plugins.path.relative(root, directoryPathArg)
646
+ .split(plugins.path.sep)
647
+ .filter(Boolean);
648
+ currentHandle = await plugins.fs.promises.open(
649
+ root,
650
+ plugins.fs.constants.O_RDONLY
651
+ | plugins.fs.constants.O_DIRECTORY
652
+ | plugins.fs.constants.O_NOFOLLOW,
653
+ );
654
+ validateDirectory(await currentHandle.stat({ bigint: true }), BigInt(effectiveUid!), false);
655
+ for (let index = 0; index < components.length; index++) {
656
+ const componentPath = directoryHandlePath(currentHandle, components[index]);
657
+ if (createArg) {
658
+ try {
659
+ await plugins.fs.promises.mkdir(componentPath, { mode: 0o700 });
660
+ } catch (errorArg) {
661
+ if ((errorArg as NodeJS.ErrnoException).code !== 'EEXIST') throw errorArg;
662
+ }
663
+ }
664
+ let nextHandle: plugins.fs.promises.FileHandle | undefined;
665
+ try {
666
+ nextHandle = await plugins.fs.promises.open(
667
+ componentPath,
668
+ plugins.fs.constants.O_RDONLY
669
+ | plugins.fs.constants.O_DIRECTORY
670
+ | plugins.fs.constants.O_NOFOLLOW,
671
+ );
672
+ validateDirectory(
673
+ await nextHandle.stat({ bigint: true }),
674
+ BigInt(effectiveUid!),
675
+ index === components.length - 1,
676
+ );
677
+ await closeFileHandle(currentHandle);
678
+ currentHandle = nextHandle;
679
+ nextHandle = undefined;
680
+ } finally {
681
+ if (nextHandle) await closeFileHandle(nextHandle);
682
+ }
683
+ }
684
+ const result = currentHandle;
685
+ currentHandle = undefined;
686
+ return result;
687
+ } catch (errorArg) {
688
+ let operationError: unknown = errorArg instanceof SmartSecretSealedFileStoreError
689
+ ? errorArg
690
+ : createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
691
+ if (currentHandle) {
692
+ operationError = await closeFileHandleWithError(currentHandle, operationError);
693
+ }
694
+ throw operationError;
695
+ }
696
+ };
697
+
698
+ const ensureDirectory = (directoryPathArg: string): Promise<plugins.fs.promises.FileHandle> =>
699
+ openDirectory(directoryPathArg, true);
700
+
701
+ const openExistingDirectory = (
702
+ directoryPathArg: string,
703
+ ): Promise<plugins.fs.promises.FileHandle> => openDirectory(directoryPathArg, false);
704
+
705
+ const openChildDirectoryIfPresent = async (
706
+ parentHandleArg: plugins.fs.promises.FileHandle,
707
+ nameArg: string,
708
+ ): Promise<plugins.fs.promises.FileHandle | undefined> => {
709
+ let handle: plugins.fs.promises.FileHandle | undefined;
710
+ try {
711
+ try {
712
+ handle = await plugins.fs.promises.open(
713
+ directoryHandlePath(parentHandleArg, nameArg),
714
+ plugins.fs.constants.O_RDONLY
715
+ | plugins.fs.constants.O_DIRECTORY
716
+ | plugins.fs.constants.O_NOFOLLOW,
717
+ );
718
+ } catch (errorArg) {
719
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
720
+ throw errorArg;
721
+ }
722
+ const effectiveUid = process.geteuid?.();
723
+ if (!Number.isSafeInteger(effectiveUid) || effectiveUid! < 0) {
724
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
725
+ }
726
+ validateDirectory(await handle.stat({ bigint: true }), BigInt(effectiveUid!), true);
727
+ const result = handle;
728
+ handle = undefined;
729
+ return result;
730
+ } catch (errorArg) {
731
+ let operationError: unknown = errorArg instanceof SmartSecretSealedFileStoreError
732
+ ? errorArg
733
+ : createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
734
+ if (handle) operationError = await closeFileHandleWithError(handle, operationError);
735
+ throw operationError;
736
+ }
737
+ };
738
+
739
+ const readStrictJsonFile = async (
740
+ directoryHandleArg: plugins.fs.promises.FileHandle,
741
+ fileNameArg: string,
742
+ maximumBytesArg: number,
743
+ ): Promise<unknown | undefined> => {
744
+ let handle: plugins.fs.promises.FileHandle | undefined;
745
+ let bytes: Buffer | undefined;
746
+ let result: unknown | undefined;
747
+ let operationError: unknown;
748
+ try {
749
+ try {
750
+ handle = await plugins.fs.promises.open(
751
+ directoryHandlePath(directoryHandleArg, fileNameArg),
752
+ plugins.fs.constants.O_RDONLY | plugins.fs.constants.O_NOFOLLOW,
753
+ );
754
+ } catch (errorArg) {
755
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
756
+ throw errorArg;
757
+ }
758
+ const stats = await handle.stat();
759
+ const effectiveUid = process.geteuid?.();
760
+ if (
761
+ !stats.isFile()
762
+ || stats.nlink !== 1
763
+ || stats.size < 1
764
+ || stats.size > maximumBytesArg
765
+ || (effectiveUid !== undefined && stats.uid !== effectiveUid)
766
+ || (stats.mode & 0o7777) !== 0o600
767
+ ) throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
768
+ bytes = await handle.readFile();
769
+ if (bytes.byteLength !== stats.size) {
770
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
771
+ }
772
+ result = JSON.parse(bytes.toString('utf8')) as unknown;
773
+ } catch (errorArg) {
774
+ operationError = errorArg instanceof SmartSecretSealedFileStoreError
775
+ ? errorArg
776
+ : createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
777
+ }
778
+ bytes?.fill(0);
779
+ if (handle) {
780
+ try {
781
+ await closeFileHandle(handle);
782
+ } catch (errorArg) {
783
+ operationError = preferCleanupError(operationError, errorArg);
784
+ }
785
+ }
786
+ if (operationError) throw operationError;
787
+ return result;
788
+ };
789
+
790
+ const atomicWriteJson = async (
791
+ directoryHandleArg: plugins.fs.promises.FileHandle,
792
+ targetNameArg: string,
793
+ valueArg: unknown,
794
+ ): Promise<void> => {
795
+ const temporaryName = `.smartsecret-${plugins.crypto.randomBytes(16).toString('base64url')}.tmp`;
796
+ const temporaryPath = directoryHandlePath(directoryHandleArg, temporaryName);
797
+ const targetPath = directoryHandlePath(directoryHandleArg, targetNameArg);
798
+ let handle: plugins.fs.promises.FileHandle | undefined;
799
+ let bytes: Buffer | undefined;
800
+ let renamed = false;
801
+ let mutationApplied = false;
802
+ let operationError: unknown;
803
+ try {
804
+ bytes = Buffer.from(JSON.stringify(valueArg), 'utf8');
805
+ handle = await plugins.fs.promises.open(
806
+ temporaryPath,
807
+ plugins.fs.constants.O_WRONLY
808
+ | plugins.fs.constants.O_CREAT
809
+ | plugins.fs.constants.O_EXCL
810
+ | plugins.fs.constants.O_NOFOLLOW,
811
+ 0o600,
812
+ );
813
+ const stats = await handle.stat();
814
+ const effectiveUid = process.geteuid?.();
815
+ if (
816
+ !stats.isFile()
817
+ || stats.nlink !== 1
818
+ || (effectiveUid !== undefined && stats.uid !== effectiveUid)
819
+ || (stats.mode & 0o7777) !== 0o600
820
+ ) {
821
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
822
+ }
823
+ await handle.writeFile(bytes);
824
+ await handle.sync();
825
+ await closeFileHandle(handle);
826
+ handle = undefined;
827
+ await plugins.fs.promises.rename(temporaryPath, targetPath);
828
+ renamed = true;
829
+ mutationApplied = true;
830
+ await directoryHandleArg.sync();
831
+ } catch (errorArg) {
832
+ operationError = errorArg instanceof SmartSecretSealedFileStoreError
833
+ ? errorArg
834
+ : createSmartSecretSealedFileStoreError(
835
+ mutationApplied ? 'MUTATION_OUTCOME_UNKNOWN' : 'FILESYSTEM_FAILED',
836
+ );
837
+ }
838
+ bytes?.fill(0);
839
+ if (handle) {
840
+ try {
841
+ await closeFileHandle(handle);
842
+ } catch (errorArg) {
843
+ operationError = preferCleanupError(operationError, errorArg);
844
+ }
845
+ }
846
+ if (!renamed) {
847
+ try {
848
+ await plugins.fs.promises.unlink(temporaryPath);
849
+ await directoryHandleArg.sync();
850
+ } catch (errorArg) {
851
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') {
852
+ operationError = createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN');
853
+ }
854
+ }
855
+ }
856
+ if (operationError) throw operationError;
857
+ };
858
+
859
+ const removeAndSync = async (
860
+ directoryHandleArg: plugins.fs.promises.FileHandle,
861
+ fileNameArg: string,
862
+ ): Promise<boolean> => {
863
+ try {
864
+ await plugins.fs.promises.unlink(directoryHandlePath(directoryHandleArg, fileNameArg));
865
+ try {
866
+ await directoryHandleArg.sync();
867
+ } catch {
868
+ throw createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN');
869
+ }
870
+ return true;
871
+ } catch (errorArg) {
872
+ if (errorArg instanceof SmartSecretSealedFileStoreError) throw errorArg;
873
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return false;
874
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
875
+ }
876
+ };
877
+
878
+ const listOwnedFiles = async (
879
+ directoryHandleArg: plugins.fs.promises.FileHandle,
880
+ allowRelocationReceiptArg = false,
881
+ ): Promise<string[]> => {
882
+ try {
883
+ const names = await plugins.fs.promises.readdir(directoryHandlePath(directoryHandleArg));
884
+ if (names.some((nameArg) => (
885
+ nameArg !== manifestName
886
+ && !(allowRelocationReceiptArg && nameArg === relocationReceiptName)
887
+ && !entryFilePattern.test(nameArg)
888
+ && !temporaryFilePattern.test(nameArg)
889
+ ))) throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
890
+ return names;
891
+ } catch (errorArg) {
892
+ if (errorArg instanceof SmartSecretSealedFileStoreError) throw errorArg;
893
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
894
+ }
895
+ };
896
+
897
+ const acquireLease = async (
898
+ optionsArg: INormalizedOptions,
899
+ deadlineArg = performance.now() + operationTimeoutMs,
900
+ ): Promise<plugins.smartipc.NamedMutexLease> => {
901
+ const remainingMs = deadlineArg - performance.now();
902
+ if (remainingMs <= 0) {
903
+ throw createSmartSecretSealedFileStoreError('MUTEX_FAILED');
904
+ }
905
+ try {
906
+ const mutex = new plugins.smartipc.NamedMutex(optionsArg.mutexNamespace);
907
+ const lease = await mutex.acquire({ timeoutMs: Math.max(0, Math.floor(remainingMs)) });
908
+ if (performance.now() >= deadlineArg) {
909
+ await lease.release().catch(() => undefined);
910
+ throw createSmartSecretSealedFileStoreError('MUTEX_FAILED');
911
+ }
912
+ return lease;
913
+ } catch (errorArg) {
914
+ throw mapMutexError(errorArg);
915
+ }
916
+ };
917
+
918
+ const readMasterKey = async (optionsArg: INormalizedOptions): Promise<Uint8Array | null> => {
919
+ let value: unknown;
920
+ try {
921
+ value = await optionsArg.kernelStore.getEntry(optionsArg.masterKeyAccount);
922
+ } catch (errorArg) {
923
+ throw mapKernelError(errorArg);
924
+ }
925
+ if (value === null) return null;
926
+ let result: Uint8Array | undefined;
927
+ try {
928
+ if (!(value instanceof Uint8Array)) {
929
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
930
+ }
931
+ result = new Uint8Array(value);
932
+ try {
933
+ value.fill(0);
934
+ } catch {}
935
+ if (result.byteLength !== masterKeyBytes) {
936
+ result.fill(0);
937
+ result = undefined;
938
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
939
+ }
940
+ return result;
941
+ } catch (errorArg) {
942
+ result?.fill(0);
943
+ if (errorArg instanceof SmartSecretSealedFileStoreError) throw errorArg;
944
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
945
+ }
946
+ };
947
+
948
+ const mapRelocationKernelError = (errorArg: unknown): SmartSecretSealedFileStoreError => {
949
+ if (!(errorArg instanceof SmartSecretKernelStoreError)) {
950
+ return createSmartSecretSealedFileStoreError('KERNEL_UNAVAILABLE');
951
+ }
952
+ if (errorArg.code === 'MUTATION_OUTCOME_UNKNOWN') {
953
+ return createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN');
954
+ }
955
+ if (errorArg.code === 'TARGET_CONFLICT' || errorArg.code === 'SOURCE_CHANGED') {
956
+ return createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
957
+ }
958
+ if (errorArg.code === 'INVALID_ARGUMENT') {
959
+ return createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
960
+ }
961
+ return createSmartSecretSealedFileStoreError('KERNEL_UNAVAILABLE');
962
+ };
963
+
964
+ const acquireRelocationLeases = async (
965
+ optionsArg: INormalizedRelocationOptions,
966
+ ): Promise<plugins.smartipc.NamedMutexLease[]> => {
967
+ const deadline = performance.now() + operationTimeoutMs;
968
+ const ordered = [optionsArg.source, optionsArg.destination]
969
+ .sort((leftArg, rightArg) => leftArg.mutexNamespace < rightArg.mutexNamespace
970
+ ? -1
971
+ : leftArg.mutexNamespace > rightArg.mutexNamespace
972
+ ? 1
973
+ : 0);
974
+ const leases: plugins.smartipc.NamedMutexLease[] = [];
975
+ try {
976
+ for (const options of ordered) leases.push(await acquireLease(options, deadline));
977
+ return leases;
978
+ } catch (errorArg) {
979
+ let operationError: unknown = errorArg;
980
+ for (const lease of leases.reverse()) {
981
+ try {
982
+ await lease.release();
983
+ } catch {
984
+ operationError = preferCleanupError(
985
+ operationError,
986
+ createSmartSecretSealedFileStoreError('MUTEX_FAILED'),
987
+ );
988
+ }
989
+ }
990
+ throw operationError;
991
+ }
992
+ };
993
+
994
+ const inspectRelocationDirectory = async (
995
+ directoryHandleArg: plugins.fs.promises.FileHandle,
996
+ optionsArg: INormalizedOptions,
997
+ ): Promise<IRelocationDirectoryInspection> => {
998
+ const names = await listOwnedFiles(directoryHandleArg, true);
999
+ if (!names.includes(manifestName)) {
1000
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1001
+ }
1002
+ const manifestValue = await readStrictJsonFile(
1003
+ directoryHandleArg,
1004
+ manifestName,
1005
+ 4 * 1_024,
1006
+ );
1007
+ if (manifestValue === undefined) {
1008
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1009
+ }
1010
+ const receiptValue = names.includes(relocationReceiptName)
1011
+ ? await readStrictJsonFile(directoryHandleArg, relocationReceiptName, 4 * 1_024)
1012
+ : undefined;
1013
+ if (names.includes(relocationReceiptName) && receiptValue === undefined) {
1014
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1015
+ }
1016
+ return {
1017
+ manifest: parseManifest(manifestValue, optionsArg),
1018
+ ...(receiptValue === undefined ? {} : { receiptValue }),
1019
+ };
1020
+ };
1021
+
1022
+ const readRelocationMasterKey = async (
1023
+ sourceArg: INormalizedOptions,
1024
+ destinationArg: INormalizedOptions,
1025
+ expectedFingerprintArg: string,
1026
+ ): Promise<IRelocationMasterKeyState> => {
1027
+ let sourceKey: Uint8Array | null = null;
1028
+ let destinationKey: Uint8Array | null = null;
1029
+ try {
1030
+ sourceKey = await readMasterKey(sourceArg);
1031
+ destinationKey = await readMasterKey(destinationArg);
1032
+ if (!sourceKey && !destinationKey) {
1033
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_UNAVAILABLE');
1034
+ }
1035
+ if (
1036
+ (sourceKey && fingerprint(sourceKey) !== expectedFingerprintArg)
1037
+ || (destinationKey && fingerprint(destinationKey) !== expectedFingerprintArg)
1038
+ || (
1039
+ sourceKey
1040
+ && destinationKey
1041
+ && !plugins.crypto.timingSafeEqual(sourceKey, destinationKey)
1042
+ )
1043
+ ) throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
1044
+ return {
1045
+ key: new Uint8Array(sourceKey ?? destinationKey!),
1046
+ sourcePresent: sourceKey !== null,
1047
+ destinationPresent: destinationKey !== null,
1048
+ };
1049
+ } finally {
1050
+ sourceKey?.fill(0);
1051
+ destinationKey?.fill(0);
1052
+ }
1053
+ };
1054
+
1055
+ const syncRelocationParents = async (
1056
+ sourceParentHandleArg: plugins.fs.promises.FileHandle,
1057
+ sourceParentPathArg: string,
1058
+ destinationParentHandleArg: plugins.fs.promises.FileHandle,
1059
+ destinationParentPathArg: string,
1060
+ ): Promise<void> => {
1061
+ try {
1062
+ await sourceParentHandleArg.sync();
1063
+ if (sourceParentPathArg !== destinationParentPathArg) {
1064
+ await destinationParentHandleArg.sync();
1065
+ }
1066
+ } catch {
1067
+ throw createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN');
1068
+ }
1069
+ };
1070
+
1071
+ const assertOpenChildIdentity = async (
1072
+ parentHandleArg: plugins.fs.promises.FileHandle,
1073
+ nameArg: string,
1074
+ childHandleArg: plugins.fs.promises.FileHandle,
1075
+ ): Promise<IDirectoryIdentity> => {
1076
+ try {
1077
+ const [pathStats, handleStats] = await Promise.all([
1078
+ plugins.fs.promises.lstat(directoryHandlePath(parentHandleArg, nameArg), { bigint: true }),
1079
+ childHandleArg.stat({ bigint: true }),
1080
+ ]);
1081
+ if (
1082
+ pathStats.isSymbolicLink()
1083
+ || !pathStats.isDirectory()
1084
+ || pathStats.dev !== handleStats.dev
1085
+ || pathStats.ino !== handleStats.ino
1086
+ ) throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1087
+ return directoryIdentity(handleStats);
1088
+ } catch (errorArg) {
1089
+ if (errorArg instanceof SmartSecretSealedFileStoreError) throw errorArg;
1090
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1091
+ }
1092
+ };
1093
+
1094
+ const readChildIdentity = async (
1095
+ parentHandleArg: plugins.fs.promises.FileHandle,
1096
+ nameArg: string,
1097
+ ): Promise<IDirectoryIdentity | undefined> => {
1098
+ try {
1099
+ const stats = await plugins.fs.promises.lstat(
1100
+ directoryHandlePath(parentHandleArg, nameArg),
1101
+ { bigint: true },
1102
+ );
1103
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
1104
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1105
+ }
1106
+ return directoryIdentity(stats);
1107
+ } catch (errorArg) {
1108
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
1109
+ if (errorArg instanceof SmartSecretSealedFileStoreError) throw errorArg;
1110
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1111
+ }
1112
+ };
1113
+
1114
+ const assertChildAbsent = async (
1115
+ parentHandleArg: plugins.fs.promises.FileHandle,
1116
+ nameArg: string,
1117
+ ): Promise<void> => {
1118
+ try {
1119
+ await plugins.fs.promises.lstat(directoryHandlePath(parentHandleArg, nameArg));
1120
+ } catch (errorArg) {
1121
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return;
1122
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1123
+ }
1124
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1125
+ };
1126
+
1127
+ const createMasterKey = async (optionsArg: INormalizedOptions): Promise<Uint8Array> => {
1128
+ const generated = plugins.smartcrypto.generateAes256GcmKey();
1129
+ let written = false;
1130
+ try {
1131
+ try {
1132
+ await optionsArg.kernelStore.setEntry(optionsArg.masterKeyAccount, generated);
1133
+ written = true;
1134
+ } catch (errorArg) {
1135
+ throw mapKernelError(errorArg);
1136
+ }
1137
+ const readback = await readMasterKey(optionsArg);
1138
+ if (
1139
+ !readback
1140
+ || !plugins.crypto.timingSafeEqual(readback, generated)
1141
+ ) {
1142
+ readback?.fill(0);
1143
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
1144
+ }
1145
+ return readback;
1146
+ } catch (errorArg) {
1147
+ if (written) {
1148
+ try {
1149
+ await optionsArg.kernelStore.deleteEntry(optionsArg.masterKeyAccount);
1150
+ } catch (deleteErrorArg) {
1151
+ throw mapKernelError(deleteErrorArg);
1152
+ }
1153
+ }
1154
+ throw errorArg;
1155
+ } finally {
1156
+ generated.fill(0);
1157
+ }
1158
+ };
1159
+
1160
+ const bootstrapUnderLease = async (
1161
+ optionsArg: INormalizedOptions,
1162
+ existingIdentityArg?: IDirectoryIdentity,
1163
+ allowRelocationReceiptArg = false,
1164
+ ): Promise<Uint8Array> => {
1165
+ const directoryHandle = existingIdentityArg
1166
+ ? await openExistingDirectory(optionsArg.directoryPath)
1167
+ : await ensureDirectory(optionsArg.directoryPath);
1168
+ let key: Uint8Array | null = null;
1169
+ let result: Uint8Array | undefined;
1170
+ let createdMasterKey = false;
1171
+ let operationError: unknown;
1172
+ try {
1173
+ if (existingIdentityArg) {
1174
+ const currentIdentity = directoryIdentity(await directoryHandle.stat({ bigint: true }));
1175
+ if (!directoryIdentitiesEqual(currentIdentity, existingIdentityArg)) {
1176
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1177
+ }
1178
+ }
1179
+ const names = await listOwnedFiles(directoryHandle, allowRelocationReceiptArg);
1180
+ const manifestValue = await readStrictJsonFile(
1181
+ directoryHandle,
1182
+ manifestName,
1183
+ 4 * 1_024,
1184
+ );
1185
+ const manifest = manifestValue === undefined ? undefined : parseManifest(manifestValue, optionsArg);
1186
+ key = await readMasterKey(optionsArg);
1187
+ if (!key) {
1188
+ if (names.length > 0) {
1189
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_UNAVAILABLE');
1190
+ }
1191
+ key = await createMasterKey(optionsArg);
1192
+ createdMasterKey = true;
1193
+ }
1194
+ if (key.byteLength !== masterKeyBytes) {
1195
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
1196
+ }
1197
+ const keyFingerprint = fingerprint(key);
1198
+ if (manifest && manifest.masterKeyFingerprint !== keyFingerprint) {
1199
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
1200
+ }
1201
+ for (const name of names.filter((nameArg) => temporaryFilePattern.test(nameArg))) {
1202
+ await removeAndSync(directoryHandle, name);
1203
+ }
1204
+ if (!manifest) {
1205
+ await atomicWriteJson(
1206
+ directoryHandle,
1207
+ manifestName,
1208
+ manifestFor(optionsArg, keyFingerprint),
1209
+ );
1210
+ }
1211
+ result = new Uint8Array(key);
1212
+ key.fill(0);
1213
+ key = null;
1214
+ } catch (errorArg) {
1215
+ operationError = errorArg;
1216
+ }
1217
+ key?.fill(0);
1218
+ operationError = await closeFileHandleWithError(directoryHandle, operationError);
1219
+ if (operationError) {
1220
+ result?.fill(0);
1221
+ result = undefined;
1222
+ if (createdMasterKey && !mutationOutcomeUnknown(operationError)) {
1223
+ try {
1224
+ await optionsArg.kernelStore.deleteEntry(optionsArg.masterKeyAccount);
1225
+ } catch (deleteErrorArg) {
1226
+ const deleteError = mapKernelError(deleteErrorArg);
1227
+ throw preferCleanupError(operationError, deleteError);
1228
+ }
1229
+ }
1230
+ throw operationError;
1231
+ }
1232
+ return result!;
1233
+ };
1234
+
1235
+ /**
1236
+ * Stores one small master key in a strict kernel store and authenticated
1237
+ * ciphertext in a caller-owned private directory. The kernel store lifecycle
1238
+ * remains owned by the caller.
1239
+ */
1240
+ export class SmartSecretSealedFileStore {
1241
+ public readonly service: string;
1242
+ public readonly storeId: string;
1243
+ public readonly directoryPath: string;
1244
+
1245
+ private lifecycleState: TLifecycleState = 'ready';
1246
+ private operationTail: Promise<void> = Promise.resolve();
1247
+ private pendingOperations = 0;
1248
+ private closePromise?: Promise<void>;
1249
+
1250
+ private constructor(
1251
+ private readonly options: INormalizedOptions,
1252
+ private readonly masterKey: Uint8Array,
1253
+ ) {
1254
+ this.service = options.service;
1255
+ this.storeId = options.storeId;
1256
+ this.directoryPath = options.directoryPath;
1257
+ }
1258
+
1259
+ public static async create(
1260
+ optionsArg: ISmartSecretSealedFileStoreOptions,
1261
+ ): Promise<SmartSecretSealedFileStore> {
1262
+ const options = normalizeOptions(optionsArg);
1263
+ if (process.platform !== 'linux' || typeof process.geteuid !== 'function') {
1264
+ throw createSmartSecretSealedFileStoreError('KERNEL_UNAVAILABLE');
1265
+ }
1266
+ const lease = await acquireLease(options);
1267
+ let key: Uint8Array | undefined;
1268
+ let operationError: unknown;
1269
+ try {
1270
+ key = await bootstrapUnderLease(options);
1271
+ } catch (errorArg) {
1272
+ operationError = errorArg;
1273
+ }
1274
+ try {
1275
+ await lease.release();
1276
+ } catch {
1277
+ operationError = preferCleanupError(
1278
+ operationError,
1279
+ createSmartSecretSealedFileStoreError('MUTEX_FAILED'),
1280
+ );
1281
+ }
1282
+ if (operationError) {
1283
+ key?.fill(0);
1284
+ throw operationError;
1285
+ }
1286
+ const store = new SmartSecretSealedFileStore(options, key!);
1287
+ key = undefined;
1288
+ return store;
1289
+ }
1290
+
1291
+ /** Moves an initialized store and rebinds its exact kernel master key. */
1292
+ public static async relocate(
1293
+ optionsArg: ISmartSecretSealedFileStoreRelocationOptions,
1294
+ ): Promise<SmartSecretSealedFileStore> {
1295
+ const options = normalizeRelocationOptions(optionsArg);
1296
+ if (process.platform !== 'linux' || typeof process.geteuid !== 'function') {
1297
+ throw createSmartSecretSealedFileStoreError('KERNEL_UNAVAILABLE');
1298
+ }
1299
+ const leases = await acquireRelocationLeases(options);
1300
+ let sourceParentHandle: plugins.fs.promises.FileHandle | undefined;
1301
+ let destinationParentHandle: plugins.fs.promises.FileHandle | undefined;
1302
+ let sourceDirectoryHandle: plugins.fs.promises.FileHandle | undefined;
1303
+ let destinationDirectoryHandle: plugins.fs.promises.FileHandle | undefined;
1304
+ let expectedKey: Uint8Array | undefined;
1305
+ let expectedIdentity: IDirectoryIdentity | undefined;
1306
+ let resultKey: Uint8Array | undefined;
1307
+ let mutationPossible = false;
1308
+ let operationError: unknown;
1309
+ const sourceParentPath = plugins.path.dirname(options.source.directoryPath);
1310
+ const destinationParentPath = plugins.path.dirname(options.destination.directoryPath);
1311
+ const sourceName = plugins.path.basename(options.source.directoryPath);
1312
+ const destinationName = plugins.path.basename(options.destination.directoryPath);
1313
+ try {
1314
+ sourceParentHandle = await openExistingDirectory(sourceParentPath);
1315
+ destinationParentHandle = await openExistingDirectory(destinationParentPath);
1316
+ const [sourceParentStats, destinationParentStats] = await Promise.all([
1317
+ sourceParentHandle.stat({ bigint: true }),
1318
+ destinationParentHandle.stat({ bigint: true }),
1319
+ ]);
1320
+ if (sourceParentStats.dev !== destinationParentStats.dev) {
1321
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1322
+ }
1323
+ sourceDirectoryHandle = await openChildDirectoryIfPresent(sourceParentHandle, sourceName);
1324
+ destinationDirectoryHandle = await openChildDirectoryIfPresent(
1325
+ destinationParentHandle,
1326
+ destinationName,
1327
+ );
1328
+ if (Boolean(sourceDirectoryHandle) === Boolean(destinationDirectoryHandle)) {
1329
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1330
+ }
1331
+ const storeAtSource = sourceDirectoryHandle !== undefined;
1332
+ const currentOptions = storeAtSource ? options.source : options.destination;
1333
+ const currentHandle = sourceDirectoryHandle ?? destinationDirectoryHandle!;
1334
+ expectedIdentity = await assertOpenChildIdentity(
1335
+ storeAtSource ? sourceParentHandle : destinationParentHandle,
1336
+ storeAtSource ? sourceName : destinationName,
1337
+ currentHandle,
1338
+ );
1339
+ if (expectedIdentity.device !== sourceParentStats.dev.toString(10)) {
1340
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1341
+ }
1342
+ const inspection = await inspectRelocationDirectory(currentHandle, currentOptions);
1343
+ const keyState = await readRelocationMasterKey(
1344
+ options.source,
1345
+ options.destination,
1346
+ inspection.manifest.masterKeyFingerprint,
1347
+ );
1348
+ expectedKey = keyState.key;
1349
+ let receipt = inspection.receiptValue === undefined
1350
+ ? undefined
1351
+ : parseRelocationReceipt(
1352
+ inspection.receiptValue,
1353
+ options,
1354
+ inspection.manifest.masterKeyFingerprint,
1355
+ expectedIdentity,
1356
+ );
1357
+ if (storeAtSource) {
1358
+ if (!keyState.sourcePresent) {
1359
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_UNAVAILABLE');
1360
+ }
1361
+ if (!receipt) {
1362
+ await atomicWriteJson(
1363
+ currentHandle,
1364
+ relocationReceiptName,
1365
+ relocationReceiptFor(
1366
+ options,
1367
+ inspection.manifest.masterKeyFingerprint,
1368
+ expectedIdentity,
1369
+ ),
1370
+ );
1371
+ mutationPossible = true;
1372
+ const receiptValue = await readStrictJsonFile(
1373
+ currentHandle,
1374
+ relocationReceiptName,
1375
+ 4 * 1_024,
1376
+ );
1377
+ if (receiptValue === undefined) {
1378
+ throw createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN');
1379
+ }
1380
+ receipt = parseRelocationReceipt(
1381
+ receiptValue,
1382
+ options,
1383
+ inspection.manifest.masterKeyFingerprint,
1384
+ expectedIdentity,
1385
+ );
1386
+ }
1387
+ await assertOpenChildIdentity(sourceParentHandle, sourceName, sourceDirectoryHandle!);
1388
+ await assertChildAbsent(destinationParentHandle, destinationName);
1389
+ try {
1390
+ await plugins.fs.promises.rename(
1391
+ directoryHandlePath(sourceParentHandle, sourceName),
1392
+ directoryHandlePath(destinationParentHandle, destinationName),
1393
+ );
1394
+ mutationPossible = true;
1395
+ } catch {
1396
+ const [sourceAfter, destinationAfter] = await Promise.all([
1397
+ readChildIdentity(sourceParentHandle, sourceName),
1398
+ readChildIdentity(destinationParentHandle, destinationName),
1399
+ ]);
1400
+ if (
1401
+ sourceAfter
1402
+ && directoryIdentitiesEqual(sourceAfter, expectedIdentity)
1403
+ && destinationAfter === undefined
1404
+ ) {
1405
+ try {
1406
+ if (!await removeAndSync(currentHandle, relocationReceiptName)) {
1407
+ throw createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN');
1408
+ }
1409
+ } catch (errorArg) {
1410
+ if (mutationOutcomeUnknown(errorArg)) throw errorArg;
1411
+ throw createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN');
1412
+ }
1413
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1414
+ }
1415
+ if (
1416
+ sourceAfter === undefined
1417
+ && destinationAfter
1418
+ && directoryIdentitiesEqual(destinationAfter, expectedIdentity)
1419
+ ) {
1420
+ mutationPossible = true;
1421
+ } else {
1422
+ throw createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN');
1423
+ }
1424
+ }
1425
+ } else {
1426
+ if (!receipt && (keyState.sourcePresent || !keyState.destinationPresent)) {
1427
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1428
+ }
1429
+ }
1430
+ await syncRelocationParents(
1431
+ sourceParentHandle,
1432
+ sourceParentPath,
1433
+ destinationParentHandle,
1434
+ destinationParentPath,
1435
+ );
1436
+ mutationPossible = true;
1437
+ let moveStatus: TSmartSecretKernelMoveStatus;
1438
+ try {
1439
+ moveStatus = await options.kernelStore.moveEntry(
1440
+ options.source.masterKeyAccount,
1441
+ options.destination.masterKeyAccount,
1442
+ );
1443
+ } catch (errorArg) {
1444
+ throw mapRelocationKernelError(errorArg);
1445
+ }
1446
+ if (moveStatus === 'sourceAbsent') {
1447
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_UNAVAILABLE');
1448
+ }
1449
+ const destinationKey = await readMasterKey(options.destination);
1450
+ try {
1451
+ if (!destinationKey || !plugins.crypto.timingSafeEqual(destinationKey, expectedKey)) {
1452
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
1453
+ }
1454
+ } finally {
1455
+ destinationKey?.fill(0);
1456
+ }
1457
+ resultKey = await bootstrapUnderLease(options.destination, expectedIdentity, Boolean(receipt));
1458
+ if (!plugins.crypto.timingSafeEqual(resultKey, expectedKey)) {
1459
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
1460
+ }
1461
+ if (receipt) {
1462
+ try {
1463
+ if (!await removeAndSync(currentHandle, relocationReceiptName)) {
1464
+ throw createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN');
1465
+ }
1466
+ } catch (errorArg) {
1467
+ if (mutationOutcomeUnknown(errorArg)) throw errorArg;
1468
+ throw createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN');
1469
+ }
1470
+ }
1471
+ } catch (errorArg) {
1472
+ operationError = errorArg;
1473
+ }
1474
+
1475
+ for (const handle of [
1476
+ sourceDirectoryHandle,
1477
+ destinationDirectoryHandle,
1478
+ sourceParentHandle,
1479
+ destinationParentHandle,
1480
+ ]) {
1481
+ if (!handle) continue;
1482
+ try {
1483
+ await closeFileHandle(handle);
1484
+ } catch (errorArg) {
1485
+ operationError = preferCleanupError(
1486
+ operationError,
1487
+ mutationPossible
1488
+ ? createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN')
1489
+ : errorArg,
1490
+ );
1491
+ }
1492
+ }
1493
+ for (const lease of leases.reverse()) {
1494
+ try {
1495
+ await lease.release();
1496
+ } catch {
1497
+ operationError = preferCleanupError(
1498
+ operationError,
1499
+ createSmartSecretSealedFileStoreError(
1500
+ mutationPossible ? 'MUTATION_OUTCOME_UNKNOWN' : 'MUTEX_FAILED',
1501
+ ),
1502
+ );
1503
+ }
1504
+ }
1505
+ expectedKey?.fill(0);
1506
+ if (operationError) {
1507
+ resultKey?.fill(0);
1508
+ throw operationError;
1509
+ }
1510
+ const store = new SmartSecretSealedFileStore(options.destination, resultKey!);
1511
+ resultKey = undefined;
1512
+ return store;
1513
+ }
1514
+
1515
+ /** Explicitly discards this store's ciphertext and kernel master key. */
1516
+ public static async reset(
1517
+ optionsArg: ISmartSecretSealedFileStoreOptions,
1518
+ ): Promise<SmartSecretSealedFileStore> {
1519
+ const options = normalizeOptions(optionsArg);
1520
+ if (process.platform !== 'linux' || typeof process.geteuid !== 'function') {
1521
+ throw createSmartSecretSealedFileStoreError('KERNEL_UNAVAILABLE');
1522
+ }
1523
+ const lease = await acquireLease(options);
1524
+ let key: Uint8Array | undefined;
1525
+ let operationError: unknown;
1526
+ try {
1527
+ const directoryHandle = await ensureDirectory(options.directoryPath);
1528
+ try {
1529
+ const names = await listOwnedFiles(directoryHandle);
1530
+ if (names.some((nameArg) => (
1531
+ nameArg === manifestName || entryFilePattern.test(nameArg)
1532
+ ))) {
1533
+ const manifestValue = await readStrictJsonFile(
1534
+ directoryHandle,
1535
+ manifestName,
1536
+ 4 * 1_024,
1537
+ );
1538
+ if (manifestValue === undefined) {
1539
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1540
+ }
1541
+ parseManifest(manifestValue, options);
1542
+ }
1543
+ try {
1544
+ await options.kernelStore.deleteEntry(options.masterKeyAccount);
1545
+ } catch (errorArg) {
1546
+ throw mapKernelError(errorArg);
1547
+ }
1548
+ const deletionOrder = names.filter((nameArg) => nameArg !== manifestName);
1549
+ if (names.includes(manifestName)) deletionOrder.push(manifestName);
1550
+ for (const name of deletionOrder) {
1551
+ await removeAndSync(directoryHandle, name);
1552
+ }
1553
+ } catch (errorArg) {
1554
+ operationError = errorArg;
1555
+ }
1556
+ operationError = await closeFileHandleWithError(directoryHandle, operationError);
1557
+ if (operationError) throw operationError;
1558
+ key = await bootstrapUnderLease(options);
1559
+ } catch (errorArg) {
1560
+ operationError = errorArg;
1561
+ }
1562
+ try {
1563
+ await lease.release();
1564
+ } catch {
1565
+ operationError = preferCleanupError(
1566
+ operationError,
1567
+ createSmartSecretSealedFileStoreError('MUTEX_FAILED'),
1568
+ );
1569
+ }
1570
+ if (operationError) {
1571
+ key?.fill(0);
1572
+ throw operationError;
1573
+ }
1574
+ const store = new SmartSecretSealedFileStore(options, key!);
1575
+ key = undefined;
1576
+ return store;
1577
+ }
1578
+
1579
+ public getEntry(accountArg: string): Promise<Uint8Array | null> {
1580
+ let account: string;
1581
+ try {
1582
+ account = normalizeSmartSecretKernelAccount(accountArg);
1583
+ } catch {
1584
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
1585
+ }
1586
+ return this.reserveOperation((deadlineArg, markAcquiredArg) => this.withLease(
1587
+ deadlineArg,
1588
+ markAcquiredArg,
1589
+ async () => {
1590
+ const directoryHandle = await openExistingDirectory(this.directoryPath);
1591
+ const id = entryId(this.options, account);
1592
+ let value: unknown | undefined;
1593
+ let operationError: unknown;
1594
+ try {
1595
+ await this.verifyManifest(directoryHandle);
1596
+ value = await readStrictJsonFile(
1597
+ directoryHandle,
1598
+ `${id}.sealed.json`,
1599
+ maximumEnvelopeBytes,
1600
+ );
1601
+ } catch (errorArg) {
1602
+ operationError = errorArg;
1603
+ }
1604
+ operationError = await closeFileHandleWithError(directoryHandle, operationError);
1605
+ if (operationError) throw operationError;
1606
+ if (value === undefined) return null;
1607
+ const parsed = parseEnvelope(value);
1608
+ const aad = entryAad(this.options, account, id, fingerprint(this.masterKey));
1609
+ try {
1610
+ if (
1611
+ parsed.envelope.entryId !== id
1612
+ || parsed.envelope.masterKeyFingerprint !== fingerprint(this.masterKey)
1613
+ ) throw createSmartSecretSealedFileStoreError('ENVELOPE_INVALID');
1614
+ try {
1615
+ return await plugins.smartcrypto.aesGcmDecrypt({
1616
+ key: this.masterKey,
1617
+ encryptedData: parsed.encryptedData,
1618
+ aad,
1619
+ });
1620
+ } catch {
1621
+ throw createSmartSecretSealedFileStoreError('AUTHENTICATION_FAILED');
1622
+ }
1623
+ } finally {
1624
+ aad.fill(0);
1625
+ parsed.encryptedData.nonce.fill(0);
1626
+ parsed.encryptedData.ciphertext.fill(0);
1627
+ parsed.encryptedData.tag.fill(0);
1628
+ }
1629
+ },
1630
+ ));
1631
+ }
1632
+
1633
+ public setEntry(accountArg: string, valueArg: Uint8Array): Promise<void> {
1634
+ let account: string;
1635
+ try {
1636
+ account = normalizeSmartSecretKernelAccount(accountArg);
1637
+ } catch {
1638
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
1639
+ }
1640
+ let value: Uint8Array;
1641
+ try {
1642
+ if (!(valueArg instanceof Uint8Array)) {
1643
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
1644
+ }
1645
+ if (valueArg.byteLength > smartSecretSealedFileMaximumEntryBytes) {
1646
+ throw createSmartSecretSealedFileStoreError('SIZE_LIMIT_EXCEEDED');
1647
+ }
1648
+ value = new Uint8Array(valueArg);
1649
+ } catch (errorArg) {
1650
+ if (errorArg instanceof SmartSecretSealedFileStoreError) throw errorArg;
1651
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
1652
+ }
1653
+ let result: Promise<void>;
1654
+ try {
1655
+ result = this.reserveOperation((deadlineArg, markAcquiredArg) => this.withLease(
1656
+ deadlineArg,
1657
+ markAcquiredArg,
1658
+ async () => {
1659
+ const directoryHandle = await openExistingDirectory(this.directoryPath);
1660
+ let encrypted: plugins.smartcrypto.IAesGcmCiphertext | undefined;
1661
+ const id = entryId(this.options, account);
1662
+ const keyFingerprint = fingerprint(this.masterKey);
1663
+ const aad = entryAad(this.options, account, id, keyFingerprint);
1664
+ let operationError: unknown;
1665
+ try {
1666
+ await this.verifyManifest(directoryHandle);
1667
+ try {
1668
+ encrypted = await plugins.smartcrypto.aesGcmEncrypt({
1669
+ key: this.masterKey,
1670
+ plaintext: value,
1671
+ aad,
1672
+ });
1673
+ } catch {
1674
+ throw createSmartSecretSealedFileStoreError('AUTHENTICATION_FAILED');
1675
+ }
1676
+ const envelope: IEnvelopeV1 = {
1677
+ schemaVersion,
1678
+ profile,
1679
+ entryId: id,
1680
+ masterKeyFingerprint: keyFingerprint,
1681
+ ...encodeEncryptedData(encrypted),
1682
+ };
1683
+ await atomicWriteJson(
1684
+ directoryHandle,
1685
+ `${id}.sealed.json`,
1686
+ envelope,
1687
+ );
1688
+ } catch (errorArg) {
1689
+ operationError = errorArg;
1690
+ }
1691
+ try {
1692
+ aad.fill(0);
1693
+ encrypted?.nonce.fill(0);
1694
+ encrypted?.ciphertext.fill(0);
1695
+ encrypted?.tag.fill(0);
1696
+ } finally {
1697
+ operationError = await closeFileHandleWithError(directoryHandle, operationError);
1698
+ }
1699
+ if (operationError) throw operationError;
1700
+ },
1701
+ ));
1702
+ } catch (errorArg) {
1703
+ value.fill(0);
1704
+ throw errorArg;
1705
+ }
1706
+ return result.finally(() => value.fill(0));
1707
+ }
1708
+
1709
+ public deleteEntry(accountArg: string): Promise<boolean> {
1710
+ let account: string;
1711
+ try {
1712
+ account = normalizeSmartSecretKernelAccount(accountArg);
1713
+ } catch {
1714
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
1715
+ }
1716
+ return this.reserveOperation((deadlineArg, markAcquiredArg) => this.withLease(
1717
+ deadlineArg,
1718
+ markAcquiredArg,
1719
+ async () => {
1720
+ const directoryHandle = await openExistingDirectory(this.directoryPath);
1721
+ let result: boolean | undefined;
1722
+ let operationError: unknown;
1723
+ try {
1724
+ await this.verifyManifest(directoryHandle);
1725
+ const id = entryId(this.options, account);
1726
+ result = await removeAndSync(directoryHandle, `${id}.sealed.json`);
1727
+ } catch (errorArg) {
1728
+ operationError = errorArg;
1729
+ }
1730
+ operationError = await closeFileHandleWithError(directoryHandle, operationError);
1731
+ if (operationError) throw operationError;
1732
+ return result!;
1733
+ },
1734
+ ));
1735
+ }
1736
+
1737
+ public close(): Promise<void> {
1738
+ if (this.closePromise) return this.closePromise;
1739
+ this.lifecycleState = 'closing';
1740
+ const operationTail = this.operationTail;
1741
+ const closePromise = operationTail.finally(() => {
1742
+ this.masterKey.fill(0);
1743
+ this.lifecycleState = 'closed';
1744
+ });
1745
+ this.closePromise = closePromise;
1746
+ return closePromise;
1747
+ }
1748
+
1749
+ private reserveOperation<T>(
1750
+ operationArg: (deadlineArg: number, markAcquiredArg: () => void) => Promise<T>,
1751
+ ): Promise<T> {
1752
+ if (this.lifecycleState !== 'ready') {
1753
+ throw createSmartSecretSealedFileStoreError('STORE_CLOSED');
1754
+ }
1755
+ if (this.pendingOperations >= maximumPendingOperations) {
1756
+ throw createSmartSecretSealedFileStoreError('OPERATION_LIMIT_REACHED');
1757
+ }
1758
+ this.pendingOperations += 1;
1759
+ const deadline = performance.now() + operationTimeoutMs;
1760
+ let admissionExpired = false;
1761
+ let admissionTimer: NodeJS.Timeout | undefined;
1762
+ let rejectAdmission!: (errorArg: SmartSecretSealedFileStoreError) => void;
1763
+ const admissionTimeout = new Promise<never>((_resolveArg, rejectArg) => {
1764
+ rejectAdmission = rejectArg;
1765
+ });
1766
+ const clearAdmissionTimer = (): void => {
1767
+ if (admissionTimer) {
1768
+ clearTimeout(admissionTimer);
1769
+ admissionTimer = undefined;
1770
+ }
1771
+ };
1772
+ const markAcquired = (): void => clearAdmissionTimer();
1773
+ admissionTimer = setTimeout(() => {
1774
+ admissionExpired = true;
1775
+ admissionTimer = undefined;
1776
+ rejectAdmission(createSmartSecretSealedFileStoreError('MUTEX_FAILED'));
1777
+ }, operationTimeoutMs);
1778
+ const execution = this.operationTail.then(() => {
1779
+ if (admissionExpired || performance.now() >= deadline) {
1780
+ throw createSmartSecretSealedFileStoreError('MUTEX_FAILED');
1781
+ }
1782
+ return operationArg(deadline, markAcquired);
1783
+ });
1784
+ this.operationTail = execution.then(() => undefined, () => undefined);
1785
+ execution.then(() => {
1786
+ clearAdmissionTimer();
1787
+ this.pendingOperations -= 1;
1788
+ }, () => {
1789
+ clearAdmissionTimer();
1790
+ this.pendingOperations -= 1;
1791
+ });
1792
+ return Promise.race([execution, admissionTimeout]);
1793
+ }
1794
+
1795
+ private async withLease<T>(
1796
+ deadlineArg: number,
1797
+ markAcquiredArg: () => void,
1798
+ operationArg: () => Promise<T>,
1799
+ ): Promise<T> {
1800
+ const lease = await acquireLease(this.options, deadlineArg);
1801
+ markAcquiredArg();
1802
+ let result: T | undefined;
1803
+ let operationError: unknown;
1804
+ try {
1805
+ result = await operationArg();
1806
+ } catch (errorArg) {
1807
+ operationError = errorArg;
1808
+ }
1809
+ try {
1810
+ await lease.release();
1811
+ } catch {
1812
+ wipeDiscardedResult(result);
1813
+ if (mutationOutcomeUnknown(operationError)) throw operationError;
1814
+ throw createSmartSecretSealedFileStoreError('MUTEX_FAILED');
1815
+ }
1816
+ if (operationError) throw operationError;
1817
+ return result as T;
1818
+ }
1819
+
1820
+ private async verifyManifest(
1821
+ directoryHandleArg: plugins.fs.promises.FileHandle,
1822
+ ): Promise<void> {
1823
+ const value = await readStrictJsonFile(
1824
+ directoryHandleArg,
1825
+ manifestName,
1826
+ 4 * 1_024,
1827
+ );
1828
+ if (value === undefined) throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1829
+ const manifest = parseManifest(value, this.options);
1830
+ if (manifest.masterKeyFingerprint !== fingerprint(this.masterKey)) {
1831
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
1832
+ }
1833
+ }
1834
+ }