@push.rocks/smartsecret 1.4.0 → 1.5.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,1195 @@
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
+ } from './smartsecret.kernel.protocol.js';
10
+ import {
11
+ SmartSecretSealedFileStoreError,
12
+ createSmartSecretSealedFileStoreError,
13
+ } from './smartsecret.sealedfile.error.js';
14
+
15
+ export const smartSecretSealedFileMaximumEntryBytes = 512 * 1_024;
16
+
17
+ const schemaVersion = 1 as const;
18
+ const profile = 'smartsecret-kernel-aes-256-gcm-file-v1' as const;
19
+ const manifestName = 'manifest.json';
20
+ const masterKeyBytes = 32;
21
+ const maximumEnvelopeBytes = 768 * 1_024;
22
+ const operationTimeoutMs = 60_000;
23
+ const canonicalDigestPattern = /^[A-Za-z0-9_-]{43}$/;
24
+ const entryFilePattern = /^[A-Za-z0-9_-]{43}\.sealed\.json$/;
25
+ const temporaryFilePattern = /^\.smartsecret-[A-Za-z0-9_-]{22}\.tmp$/;
26
+ const maximumPendingOperations = 64;
27
+ const encoder = new TextEncoder();
28
+
29
+ export interface ISmartSecretSealedFileKernelStore {
30
+ readonly service: string;
31
+ getEntry(
32
+ accountArg: string,
33
+ optionsArg?: ISmartSecretKernelOperationOptions,
34
+ ): Promise<Uint8Array | null>;
35
+ setEntry(
36
+ accountArg: string,
37
+ valueArg: Uint8Array,
38
+ optionsArg?: ISmartSecretKernelOperationOptions,
39
+ ): Promise<void>;
40
+ deleteEntry(
41
+ accountArg: string,
42
+ optionsArg?: ISmartSecretKernelOperationOptions,
43
+ ): Promise<boolean>;
44
+ }
45
+
46
+ export interface ISmartSecretSealedFileStoreOptions {
47
+ kernelStore: ISmartSecretSealedFileKernelStore;
48
+ storeId: string;
49
+ directoryPath: string;
50
+ }
51
+
52
+ interface INormalizedOptions {
53
+ kernelStore: ISmartSecretSealedFileKernelStore;
54
+ service: string;
55
+ storeId: string;
56
+ directoryPath: string;
57
+ masterKeyAccount: string;
58
+ mutexNamespace: string;
59
+ }
60
+
61
+ interface IManifestV1 {
62
+ schemaVersion: 1;
63
+ profile: typeof profile;
64
+ serviceDigest: string;
65
+ storeId: string;
66
+ masterKeyFingerprint: string;
67
+ }
68
+
69
+ interface IEnvelopeV1 {
70
+ schemaVersion: 1;
71
+ profile: typeof profile;
72
+ entryId: string;
73
+ masterKeyFingerprint: string;
74
+ nonce: string;
75
+ ciphertext: string;
76
+ tag: string;
77
+ }
78
+
79
+ type TLifecycleState = 'ready' | 'closing' | 'closed';
80
+ type TBigIntStats = plugins.fs.BigIntStats;
81
+
82
+ const sha256 = (...valuesArg: Uint8Array[]): Uint8Array => {
83
+ const hash = plugins.crypto.createHash('sha256');
84
+ for (const value of valuesArg) hash.update(value);
85
+ return new Uint8Array(hash.digest());
86
+ };
87
+
88
+ const digest = (...valuesArg: Uint8Array[]): string =>
89
+ Buffer.from(sha256(...valuesArg)).toString('base64url');
90
+
91
+ const fingerprint = (keyArg: Uint8Array): string => digest(
92
+ encoder.encode('@push.rocks/smartsecret/sealed-file/master-key/v1\0'),
93
+ keyArg,
94
+ );
95
+
96
+ const normalizeStoreId = (valueArg: unknown): string => {
97
+ if (
98
+ typeof valueArg !== 'string'
99
+ || !/^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(valueArg)
100
+ ) throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
101
+ return valueArg;
102
+ };
103
+
104
+ const normalizeDirectoryPath = (valueArg: unknown): string => {
105
+ if (
106
+ typeof valueArg !== 'string'
107
+ || !plugins.path.isAbsolute(valueArg)
108
+ || plugins.path.resolve(valueArg) !== valueArg
109
+ || valueArg === plugins.path.parse(valueArg).root
110
+ ) throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
111
+ return valueArg;
112
+ };
113
+
114
+ const dataPropertyValue = (objectArg: object, keyArg: string): unknown => {
115
+ let current: object | null = objectArg;
116
+ while (current) {
117
+ const descriptor = Object.getOwnPropertyDescriptor(current, keyArg);
118
+ if (descriptor) {
119
+ if (!('value' in descriptor)) {
120
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
121
+ }
122
+ return descriptor.value;
123
+ }
124
+ current = Object.getPrototypeOf(current) as object | null;
125
+ }
126
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
127
+ };
128
+
129
+ const normalizeOptions = (optionsArg: ISmartSecretSealedFileStoreOptions): INormalizedOptions => {
130
+ try {
131
+ if (!optionsArg || typeof optionsArg !== 'object' || Array.isArray(optionsArg)) {
132
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
133
+ }
134
+ const keys = Reflect.ownKeys(optionsArg);
135
+ if (
136
+ keys.length !== 3
137
+ || keys.some((keyArg) => typeof keyArg !== 'string')
138
+ || !['kernelStore', 'storeId', 'directoryPath'].every((keyArg) => keys.includes(keyArg))
139
+ ) throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
140
+ const values: Record<string, unknown> = Object.create(null);
141
+ for (const key of keys as string[]) {
142
+ const descriptor = Object.getOwnPropertyDescriptor(optionsArg, key);
143
+ if (!descriptor?.enumerable || !('value' in descriptor)) {
144
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
145
+ }
146
+ values[key] = descriptor.value;
147
+ }
148
+ const kernelStoreValue = values.kernelStore;
149
+ if (
150
+ !kernelStoreValue
151
+ || typeof kernelStoreValue !== 'object'
152
+ ) throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
153
+ const service = normalizeSmartSecretKernelService(
154
+ dataPropertyValue(kernelStoreValue, 'service'),
155
+ );
156
+ const getEntry = dataPropertyValue(kernelStoreValue, 'getEntry');
157
+ const setEntry = dataPropertyValue(kernelStoreValue, 'setEntry');
158
+ const deleteEntry = dataPropertyValue(kernelStoreValue, 'deleteEntry');
159
+ if (
160
+ typeof getEntry !== 'function'
161
+ || typeof setEntry !== 'function'
162
+ || typeof deleteEntry !== 'function'
163
+ ) throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
164
+ const kernelStore: ISmartSecretSealedFileKernelStore = {
165
+ service,
166
+ getEntry: (accountArg, operationOptionsArg) => Reflect.apply(
167
+ getEntry,
168
+ kernelStoreValue,
169
+ [accountArg, operationOptionsArg],
170
+ ) as Promise<Uint8Array | null>,
171
+ setEntry: (accountArg, valueArg, operationOptionsArg) => Reflect.apply(
172
+ setEntry,
173
+ kernelStoreValue,
174
+ [accountArg, valueArg, operationOptionsArg],
175
+ ) as Promise<void>,
176
+ deleteEntry: (accountArg, operationOptionsArg) => Reflect.apply(
177
+ deleteEntry,
178
+ kernelStoreValue,
179
+ [accountArg, operationOptionsArg],
180
+ ) as Promise<boolean>,
181
+ };
182
+ const storeId = normalizeStoreId(values.storeId);
183
+ const directoryPath = normalizeDirectoryPath(values.directoryPath);
184
+ const identity = encoder.encode(`${service}\0${storeId}\0${directoryPath}`);
185
+ return {
186
+ kernelStore,
187
+ service,
188
+ storeId,
189
+ directoryPath,
190
+ masterKeyAccount: `sealed-file-master:v1:${digest(identity)}`,
191
+ mutexNamespace: `smartsecret-sealed-file:v1:${digest(encoder.encode(directoryPath))}`,
192
+ };
193
+ } catch {
194
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
195
+ }
196
+ };
197
+
198
+ const exactObject = (
199
+ valueArg: unknown,
200
+ keysArg: readonly string[],
201
+ errorCodeArg: 'ENVELOPE_INVALID' | 'FILESYSTEM_FAILED',
202
+ ): Record<string, unknown> => {
203
+ try {
204
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
205
+ throw createSmartSecretSealedFileStoreError(errorCodeArg);
206
+ }
207
+ const prototype = Object.getPrototypeOf(valueArg);
208
+ const keys = Reflect.ownKeys(valueArg);
209
+ if (
210
+ (prototype !== Object.prototype && prototype !== null)
211
+ || keys.length !== keysArg.length
212
+ || keys.some((keyArg) => typeof keyArg !== 'string')
213
+ || keysArg.some((keyArg) => !keys.includes(keyArg))
214
+ ) throw createSmartSecretSealedFileStoreError(errorCodeArg);
215
+ const result: Record<string, unknown> = Object.create(null);
216
+ for (const key of keys as string[]) {
217
+ const descriptor = Object.getOwnPropertyDescriptor(valueArg, key);
218
+ if (!descriptor?.enumerable || !('value' in descriptor)) {
219
+ throw createSmartSecretSealedFileStoreError(errorCodeArg);
220
+ }
221
+ result[key] = descriptor.value;
222
+ }
223
+ return result;
224
+ } catch (errorArg) {
225
+ if (errorArg instanceof SmartSecretSealedFileStoreError) throw errorArg;
226
+ throw createSmartSecretSealedFileStoreError(errorCodeArg);
227
+ }
228
+ };
229
+
230
+ const decodeBase64Url = (
231
+ valueArg: unknown,
232
+ maximumBytesArg: number,
233
+ exactBytesArg?: number,
234
+ ): Uint8Array => {
235
+ if (
236
+ typeof valueArg !== 'string'
237
+ || !/^[A-Za-z0-9_-]*$/.test(valueArg)
238
+ || valueArg.length % 4 === 1
239
+ || valueArg.length > Math.ceil(maximumBytesArg * 4 / 3)
240
+ ) throw createSmartSecretSealedFileStoreError('ENVELOPE_INVALID');
241
+ const decoded = Buffer.from(valueArg, 'base64url');
242
+ if (
243
+ decoded.byteLength > maximumBytesArg
244
+ || (exactBytesArg !== undefined && decoded.byteLength !== exactBytesArg)
245
+ || decoded.toString('base64url') !== valueArg
246
+ ) {
247
+ decoded.fill(0);
248
+ throw createSmartSecretSealedFileStoreError('ENVELOPE_INVALID');
249
+ }
250
+ const result = new Uint8Array(decoded);
251
+ decoded.fill(0);
252
+ return result;
253
+ };
254
+
255
+ const parseEnvelope = (valueArg: unknown): {
256
+ envelope: IEnvelopeV1;
257
+ encryptedData: plugins.smartcrypto.IAesGcmCiphertext;
258
+ } => {
259
+ const value = exactObject(valueArg, [
260
+ 'schemaVersion',
261
+ 'profile',
262
+ 'entryId',
263
+ 'masterKeyFingerprint',
264
+ 'nonce',
265
+ 'ciphertext',
266
+ 'tag',
267
+ ], 'ENVELOPE_INVALID');
268
+ if (
269
+ value.schemaVersion !== schemaVersion
270
+ || value.profile !== profile
271
+ || typeof value.entryId !== 'string'
272
+ || !canonicalDigestPattern.test(value.entryId)
273
+ || typeof value.masterKeyFingerprint !== 'string'
274
+ || !canonicalDigestPattern.test(value.masterKeyFingerprint)
275
+ ) throw createSmartSecretSealedFileStoreError('ENVELOPE_INVALID');
276
+ let nonce: Uint8Array | undefined;
277
+ let ciphertext: Uint8Array | undefined;
278
+ let tag: Uint8Array | undefined;
279
+ try {
280
+ nonce = decodeBase64Url(value.nonce, 12, 12);
281
+ ciphertext = decodeBase64Url(value.ciphertext, smartSecretSealedFileMaximumEntryBytes);
282
+ tag = decodeBase64Url(value.tag, 16, 16);
283
+ return {
284
+ envelope: {
285
+ schemaVersion,
286
+ profile,
287
+ entryId: value.entryId,
288
+ masterKeyFingerprint: value.masterKeyFingerprint,
289
+ nonce: value.nonce as string,
290
+ ciphertext: value.ciphertext as string,
291
+ tag: value.tag as string,
292
+ },
293
+ encryptedData: { nonce, ciphertext, tag },
294
+ };
295
+ } catch (errorArg) {
296
+ nonce?.fill(0);
297
+ ciphertext?.fill(0);
298
+ tag?.fill(0);
299
+ throw errorArg;
300
+ }
301
+ };
302
+
303
+ const encodeBase64Url = (valueArg: Uint8Array): string => {
304
+ const copy = Buffer.from(valueArg);
305
+ try {
306
+ return copy.toString('base64url');
307
+ } finally {
308
+ copy.fill(0);
309
+ }
310
+ };
311
+
312
+ const encodeEncryptedData = (
313
+ valueArg: plugins.smartcrypto.IAesGcmCiphertext,
314
+ ): Pick<IEnvelopeV1, 'nonce' | 'ciphertext' | 'tag'> => ({
315
+ nonce: encodeBase64Url(valueArg.nonce),
316
+ ciphertext: encodeBase64Url(valueArg.ciphertext),
317
+ tag: encodeBase64Url(valueArg.tag),
318
+ });
319
+
320
+ const entryId = (optionsArg: INormalizedOptions, accountArg: string): string => digest(
321
+ encoder.encode('@push.rocks/smartsecret/sealed-file/entry/v1\0'),
322
+ encoder.encode(optionsArg.service),
323
+ new Uint8Array([0]),
324
+ encoder.encode(optionsArg.storeId),
325
+ new Uint8Array([0]),
326
+ encoder.encode(accountArg),
327
+ );
328
+
329
+ const entryAad = (
330
+ optionsArg: INormalizedOptions,
331
+ accountArg: string,
332
+ entryIdArg: string,
333
+ fingerprintArg: string,
334
+ ): Uint8Array => encoder.encode(JSON.stringify({
335
+ schemaVersion,
336
+ profile,
337
+ service: optionsArg.service,
338
+ storeId: optionsArg.storeId,
339
+ account: accountArg,
340
+ entryId: entryIdArg,
341
+ masterKeyFingerprint: fingerprintArg,
342
+ }));
343
+
344
+ const manifestFor = (optionsArg: INormalizedOptions, fingerprintArg: string): IManifestV1 => ({
345
+ schemaVersion,
346
+ profile,
347
+ serviceDigest: digest(encoder.encode(optionsArg.service)),
348
+ storeId: optionsArg.storeId,
349
+ masterKeyFingerprint: fingerprintArg,
350
+ });
351
+
352
+ const parseManifest = (valueArg: unknown, optionsArg: INormalizedOptions): IManifestV1 => {
353
+ const value = exactObject(valueArg, [
354
+ 'schemaVersion',
355
+ 'profile',
356
+ 'serviceDigest',
357
+ 'storeId',
358
+ 'masterKeyFingerprint',
359
+ ], 'FILESYSTEM_FAILED');
360
+ if (
361
+ value.schemaVersion !== schemaVersion
362
+ || value.profile !== profile
363
+ || value.serviceDigest !== digest(encoder.encode(optionsArg.service))
364
+ || value.storeId !== optionsArg.storeId
365
+ || typeof value.masterKeyFingerprint !== 'string'
366
+ || !canonicalDigestPattern.test(value.masterKeyFingerprint)
367
+ ) throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
368
+ return value as unknown as IManifestV1;
369
+ };
370
+
371
+ const mapKernelError = (errorArg: unknown): SmartSecretSealedFileStoreError => {
372
+ if (!(errorArg instanceof SmartSecretKernelStoreError)) {
373
+ return createSmartSecretSealedFileStoreError('KERNEL_UNAVAILABLE');
374
+ }
375
+ if (errorArg.code === 'MUTATION_OUTCOME_UNKNOWN') {
376
+ return createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN');
377
+ }
378
+ if (errorArg.code === 'INVALID_ARGUMENT') {
379
+ return createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
380
+ }
381
+ return createSmartSecretSealedFileStoreError('KERNEL_UNAVAILABLE');
382
+ };
383
+
384
+ const mapMutexError = (errorArg: unknown): SmartSecretSealedFileStoreError => {
385
+ if (errorArg instanceof plugins.smartipc.NamedMutexError) {
386
+ if (errorArg.code === 'ABORTED' || errorArg.code === 'TIMEOUT') {
387
+ return createSmartSecretSealedFileStoreError('MUTEX_FAILED');
388
+ }
389
+ }
390
+ return createSmartSecretSealedFileStoreError('MUTEX_FAILED');
391
+ };
392
+
393
+ const directoryHandlePath = (
394
+ directoryHandleArg: plugins.fs.promises.FileHandle,
395
+ nameArg?: string,
396
+ ): string => nameArg
397
+ ? plugins.path.join(`/proc/self/fd/${directoryHandleArg.fd}`, nameArg)
398
+ : `/proc/self/fd/${directoryHandleArg.fd}`;
399
+
400
+ const mutationOutcomeUnknown = (errorArg: unknown): boolean =>
401
+ errorArg instanceof SmartSecretSealedFileStoreError
402
+ && errorArg.code === 'MUTATION_OUTCOME_UNKNOWN';
403
+
404
+ const preferCleanupError = (operationErrorArg: unknown, cleanupErrorArg: unknown): unknown =>
405
+ mutationOutcomeUnknown(operationErrorArg) ? operationErrorArg : cleanupErrorArg;
406
+
407
+ const closeFileHandle = async (handleArg: plugins.fs.promises.FileHandle): Promise<void> => {
408
+ try {
409
+ await handleArg.close();
410
+ } catch {
411
+ try {
412
+ await handleArg.close();
413
+ } catch {}
414
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
415
+ }
416
+ };
417
+
418
+ const closeFileHandleWithError = async (
419
+ handleArg: plugins.fs.promises.FileHandle,
420
+ operationErrorArg: unknown,
421
+ ): Promise<unknown> => {
422
+ try {
423
+ await closeFileHandle(handleArg);
424
+ return operationErrorArg;
425
+ } catch (errorArg) {
426
+ return preferCleanupError(operationErrorArg, errorArg);
427
+ }
428
+ };
429
+
430
+ const wipeDiscardedResult = (valueArg: unknown): void => {
431
+ if (valueArg instanceof Uint8Array) valueArg.fill(0);
432
+ };
433
+
434
+ const validateDirectory = (
435
+ statsArg: TBigIntStats,
436
+ effectiveUidArg: bigint,
437
+ finalArg: boolean,
438
+ ): void => {
439
+ if (!statsArg.isDirectory()) {
440
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
441
+ }
442
+ const mode = statsArg.mode & 0o7777n;
443
+ if (finalArg) {
444
+ if (statsArg.uid !== effectiveUidArg || mode !== 0o700n) {
445
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
446
+ }
447
+ return;
448
+ }
449
+ if (statsArg.uid !== 0n && statsArg.uid !== effectiveUidArg) {
450
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
451
+ }
452
+ if (
453
+ (mode & 0o022n) !== 0n
454
+ && !(statsArg.uid === 0n && (mode & 0o1000n) !== 0n)
455
+ ) throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
456
+ };
457
+
458
+ const ensureDirectory = async (directoryPathArg: string): Promise<plugins.fs.promises.FileHandle> => {
459
+ let currentHandle: plugins.fs.promises.FileHandle | undefined;
460
+ try {
461
+ const effectiveUid = process.geteuid?.();
462
+ if (!Number.isSafeInteger(effectiveUid) || effectiveUid! < 0) {
463
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
464
+ }
465
+ const root = plugins.path.parse(directoryPathArg).root;
466
+ const components = plugins.path.relative(root, directoryPathArg)
467
+ .split(plugins.path.sep)
468
+ .filter(Boolean);
469
+ currentHandle = await plugins.fs.promises.open(
470
+ root,
471
+ plugins.fs.constants.O_RDONLY
472
+ | plugins.fs.constants.O_DIRECTORY
473
+ | plugins.fs.constants.O_NOFOLLOW,
474
+ );
475
+ validateDirectory(await currentHandle.stat({ bigint: true }), BigInt(effectiveUid!), false);
476
+ for (let index = 0; index < components.length; index++) {
477
+ const componentPath = directoryHandlePath(currentHandle, components[index]);
478
+ try {
479
+ await plugins.fs.promises.mkdir(componentPath, { mode: 0o700 });
480
+ } catch (errorArg) {
481
+ if ((errorArg as NodeJS.ErrnoException).code !== 'EEXIST') throw errorArg;
482
+ }
483
+ let nextHandle: plugins.fs.promises.FileHandle | undefined;
484
+ try {
485
+ nextHandle = await plugins.fs.promises.open(
486
+ componentPath,
487
+ plugins.fs.constants.O_RDONLY
488
+ | plugins.fs.constants.O_DIRECTORY
489
+ | plugins.fs.constants.O_NOFOLLOW,
490
+ );
491
+ validateDirectory(
492
+ await nextHandle.stat({ bigint: true }),
493
+ BigInt(effectiveUid!),
494
+ index === components.length - 1,
495
+ );
496
+ await closeFileHandle(currentHandle);
497
+ currentHandle = nextHandle;
498
+ nextHandle = undefined;
499
+ } finally {
500
+ if (nextHandle) await closeFileHandle(nextHandle);
501
+ }
502
+ }
503
+ const result = currentHandle;
504
+ currentHandle = undefined;
505
+ return result;
506
+ } catch (errorArg) {
507
+ let operationError: unknown = errorArg instanceof SmartSecretSealedFileStoreError
508
+ ? errorArg
509
+ : createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
510
+ if (currentHandle) {
511
+ operationError = await closeFileHandleWithError(currentHandle, operationError);
512
+ }
513
+ throw operationError;
514
+ }
515
+ };
516
+
517
+ const readStrictJsonFile = async (
518
+ directoryHandleArg: plugins.fs.promises.FileHandle,
519
+ fileNameArg: string,
520
+ maximumBytesArg: number,
521
+ ): Promise<unknown | undefined> => {
522
+ let handle: plugins.fs.promises.FileHandle | undefined;
523
+ let bytes: Buffer | undefined;
524
+ let result: unknown | undefined;
525
+ let operationError: unknown;
526
+ try {
527
+ try {
528
+ handle = await plugins.fs.promises.open(
529
+ directoryHandlePath(directoryHandleArg, fileNameArg),
530
+ plugins.fs.constants.O_RDONLY | plugins.fs.constants.O_NOFOLLOW,
531
+ );
532
+ } catch (errorArg) {
533
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
534
+ throw errorArg;
535
+ }
536
+ const stats = await handle.stat();
537
+ const effectiveUid = process.geteuid?.();
538
+ if (
539
+ !stats.isFile()
540
+ || stats.nlink !== 1
541
+ || stats.size < 1
542
+ || stats.size > maximumBytesArg
543
+ || (effectiveUid !== undefined && stats.uid !== effectiveUid)
544
+ || (stats.mode & 0o7777) !== 0o600
545
+ ) throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
546
+ bytes = await handle.readFile();
547
+ if (bytes.byteLength !== stats.size) {
548
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
549
+ }
550
+ result = JSON.parse(bytes.toString('utf8')) as unknown;
551
+ } catch (errorArg) {
552
+ operationError = errorArg instanceof SmartSecretSealedFileStoreError
553
+ ? errorArg
554
+ : createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
555
+ }
556
+ bytes?.fill(0);
557
+ if (handle) {
558
+ try {
559
+ await closeFileHandle(handle);
560
+ } catch (errorArg) {
561
+ operationError = preferCleanupError(operationError, errorArg);
562
+ }
563
+ }
564
+ if (operationError) throw operationError;
565
+ return result;
566
+ };
567
+
568
+ const atomicWriteJson = async (
569
+ directoryHandleArg: plugins.fs.promises.FileHandle,
570
+ targetNameArg: string,
571
+ valueArg: unknown,
572
+ ): Promise<void> => {
573
+ const temporaryName = `.smartsecret-${plugins.crypto.randomBytes(16).toString('base64url')}.tmp`;
574
+ const temporaryPath = directoryHandlePath(directoryHandleArg, temporaryName);
575
+ const targetPath = directoryHandlePath(directoryHandleArg, targetNameArg);
576
+ let handle: plugins.fs.promises.FileHandle | undefined;
577
+ let bytes: Buffer | undefined;
578
+ let renamed = false;
579
+ let mutationApplied = false;
580
+ let operationError: unknown;
581
+ try {
582
+ bytes = Buffer.from(JSON.stringify(valueArg), 'utf8');
583
+ handle = await plugins.fs.promises.open(
584
+ temporaryPath,
585
+ plugins.fs.constants.O_WRONLY
586
+ | plugins.fs.constants.O_CREAT
587
+ | plugins.fs.constants.O_EXCL
588
+ | plugins.fs.constants.O_NOFOLLOW,
589
+ 0o600,
590
+ );
591
+ const stats = await handle.stat();
592
+ const effectiveUid = process.geteuid?.();
593
+ if (
594
+ !stats.isFile()
595
+ || stats.nlink !== 1
596
+ || (effectiveUid !== undefined && stats.uid !== effectiveUid)
597
+ || (stats.mode & 0o7777) !== 0o600
598
+ ) {
599
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
600
+ }
601
+ await handle.writeFile(bytes);
602
+ await handle.sync();
603
+ await closeFileHandle(handle);
604
+ handle = undefined;
605
+ await plugins.fs.promises.rename(temporaryPath, targetPath);
606
+ renamed = true;
607
+ mutationApplied = true;
608
+ await directoryHandleArg.sync();
609
+ } catch (errorArg) {
610
+ operationError = errorArg instanceof SmartSecretSealedFileStoreError
611
+ ? errorArg
612
+ : createSmartSecretSealedFileStoreError(
613
+ mutationApplied ? 'MUTATION_OUTCOME_UNKNOWN' : 'FILESYSTEM_FAILED',
614
+ );
615
+ }
616
+ bytes?.fill(0);
617
+ if (handle) {
618
+ try {
619
+ await closeFileHandle(handle);
620
+ } catch (errorArg) {
621
+ operationError = preferCleanupError(operationError, errorArg);
622
+ }
623
+ }
624
+ if (!renamed) {
625
+ try {
626
+ await plugins.fs.promises.unlink(temporaryPath);
627
+ await directoryHandleArg.sync();
628
+ } catch (errorArg) {
629
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') {
630
+ operationError = createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN');
631
+ }
632
+ }
633
+ }
634
+ if (operationError) throw operationError;
635
+ };
636
+
637
+ const removeAndSync = async (
638
+ directoryHandleArg: plugins.fs.promises.FileHandle,
639
+ fileNameArg: string,
640
+ ): Promise<boolean> => {
641
+ try {
642
+ await plugins.fs.promises.unlink(directoryHandlePath(directoryHandleArg, fileNameArg));
643
+ try {
644
+ await directoryHandleArg.sync();
645
+ } catch {
646
+ throw createSmartSecretSealedFileStoreError('MUTATION_OUTCOME_UNKNOWN');
647
+ }
648
+ return true;
649
+ } catch (errorArg) {
650
+ if (errorArg instanceof SmartSecretSealedFileStoreError) throw errorArg;
651
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return false;
652
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
653
+ }
654
+ };
655
+
656
+ const listOwnedFiles = async (
657
+ directoryHandleArg: plugins.fs.promises.FileHandle,
658
+ ): Promise<string[]> => {
659
+ try {
660
+ const names = await plugins.fs.promises.readdir(directoryHandlePath(directoryHandleArg));
661
+ if (names.some((nameArg) => (
662
+ nameArg !== manifestName
663
+ && !entryFilePattern.test(nameArg)
664
+ && !temporaryFilePattern.test(nameArg)
665
+ ))) throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
666
+ return names;
667
+ } catch (errorArg) {
668
+ if (errorArg instanceof SmartSecretSealedFileStoreError) throw errorArg;
669
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
670
+ }
671
+ };
672
+
673
+ const acquireLease = async (
674
+ optionsArg: INormalizedOptions,
675
+ deadlineArg = performance.now() + operationTimeoutMs,
676
+ ): Promise<plugins.smartipc.NamedMutexLease> => {
677
+ const remainingMs = deadlineArg - performance.now();
678
+ if (remainingMs <= 0) {
679
+ throw createSmartSecretSealedFileStoreError('MUTEX_FAILED');
680
+ }
681
+ try {
682
+ const mutex = new plugins.smartipc.NamedMutex(optionsArg.mutexNamespace);
683
+ const lease = await mutex.acquire({ timeoutMs: Math.max(0, Math.floor(remainingMs)) });
684
+ if (performance.now() >= deadlineArg) {
685
+ await lease.release().catch(() => undefined);
686
+ throw createSmartSecretSealedFileStoreError('MUTEX_FAILED');
687
+ }
688
+ return lease;
689
+ } catch (errorArg) {
690
+ throw mapMutexError(errorArg);
691
+ }
692
+ };
693
+
694
+ const readMasterKey = async (optionsArg: INormalizedOptions): Promise<Uint8Array | null> => {
695
+ let value: unknown;
696
+ try {
697
+ value = await optionsArg.kernelStore.getEntry(optionsArg.masterKeyAccount);
698
+ } catch (errorArg) {
699
+ throw mapKernelError(errorArg);
700
+ }
701
+ if (value === null) return null;
702
+ let result: Uint8Array | undefined;
703
+ try {
704
+ if (!(value instanceof Uint8Array)) {
705
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
706
+ }
707
+ result = new Uint8Array(value);
708
+ try {
709
+ value.fill(0);
710
+ } catch {}
711
+ if (result.byteLength !== masterKeyBytes) {
712
+ result.fill(0);
713
+ result = undefined;
714
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
715
+ }
716
+ return result;
717
+ } catch (errorArg) {
718
+ result?.fill(0);
719
+ if (errorArg instanceof SmartSecretSealedFileStoreError) throw errorArg;
720
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
721
+ }
722
+ };
723
+
724
+ const createMasterKey = async (optionsArg: INormalizedOptions): Promise<Uint8Array> => {
725
+ const generated = plugins.smartcrypto.generateAes256GcmKey();
726
+ let written = false;
727
+ try {
728
+ try {
729
+ await optionsArg.kernelStore.setEntry(optionsArg.masterKeyAccount, generated);
730
+ written = true;
731
+ } catch (errorArg) {
732
+ throw mapKernelError(errorArg);
733
+ }
734
+ const readback = await readMasterKey(optionsArg);
735
+ if (
736
+ !readback
737
+ || !plugins.crypto.timingSafeEqual(readback, generated)
738
+ ) {
739
+ readback?.fill(0);
740
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
741
+ }
742
+ return readback;
743
+ } catch (errorArg) {
744
+ if (written) {
745
+ try {
746
+ await optionsArg.kernelStore.deleteEntry(optionsArg.masterKeyAccount);
747
+ } catch (deleteErrorArg) {
748
+ throw mapKernelError(deleteErrorArg);
749
+ }
750
+ }
751
+ throw errorArg;
752
+ } finally {
753
+ generated.fill(0);
754
+ }
755
+ };
756
+
757
+ const bootstrapUnderLease = async (optionsArg: INormalizedOptions): Promise<Uint8Array> => {
758
+ const directoryHandle = await ensureDirectory(optionsArg.directoryPath);
759
+ let key: Uint8Array | null = null;
760
+ let result: Uint8Array | undefined;
761
+ let createdMasterKey = false;
762
+ let operationError: unknown;
763
+ try {
764
+ const names = await listOwnedFiles(directoryHandle);
765
+ const manifestValue = await readStrictJsonFile(
766
+ directoryHandle,
767
+ manifestName,
768
+ 4 * 1_024,
769
+ );
770
+ const manifest = manifestValue === undefined ? undefined : parseManifest(manifestValue, optionsArg);
771
+ key = await readMasterKey(optionsArg);
772
+ if (!key) {
773
+ if (names.length > 0) {
774
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_UNAVAILABLE');
775
+ }
776
+ key = await createMasterKey(optionsArg);
777
+ createdMasterKey = true;
778
+ }
779
+ if (key.byteLength !== masterKeyBytes) {
780
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
781
+ }
782
+ const keyFingerprint = fingerprint(key);
783
+ if (manifest && manifest.masterKeyFingerprint !== keyFingerprint) {
784
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
785
+ }
786
+ for (const name of names.filter((nameArg) => temporaryFilePattern.test(nameArg))) {
787
+ await removeAndSync(directoryHandle, name);
788
+ }
789
+ if (!manifest) {
790
+ await atomicWriteJson(
791
+ directoryHandle,
792
+ manifestName,
793
+ manifestFor(optionsArg, keyFingerprint),
794
+ );
795
+ }
796
+ result = new Uint8Array(key);
797
+ key.fill(0);
798
+ key = null;
799
+ } catch (errorArg) {
800
+ operationError = errorArg;
801
+ }
802
+ key?.fill(0);
803
+ operationError = await closeFileHandleWithError(directoryHandle, operationError);
804
+ if (operationError) {
805
+ result?.fill(0);
806
+ result = undefined;
807
+ if (createdMasterKey && !mutationOutcomeUnknown(operationError)) {
808
+ try {
809
+ await optionsArg.kernelStore.deleteEntry(optionsArg.masterKeyAccount);
810
+ } catch (deleteErrorArg) {
811
+ const deleteError = mapKernelError(deleteErrorArg);
812
+ throw preferCleanupError(operationError, deleteError);
813
+ }
814
+ }
815
+ throw operationError;
816
+ }
817
+ return result!;
818
+ };
819
+
820
+ /**
821
+ * Stores one small master key in a strict kernel store and authenticated
822
+ * ciphertext in a caller-owned private directory. The kernel store lifecycle
823
+ * remains owned by the caller.
824
+ */
825
+ export class SmartSecretSealedFileStore {
826
+ public readonly service: string;
827
+ public readonly storeId: string;
828
+ public readonly directoryPath: string;
829
+
830
+ private lifecycleState: TLifecycleState = 'ready';
831
+ private operationTail: Promise<void> = Promise.resolve();
832
+ private pendingOperations = 0;
833
+ private closePromise?: Promise<void>;
834
+
835
+ private constructor(
836
+ private readonly options: INormalizedOptions,
837
+ private readonly masterKey: Uint8Array,
838
+ ) {
839
+ this.service = options.service;
840
+ this.storeId = options.storeId;
841
+ this.directoryPath = options.directoryPath;
842
+ }
843
+
844
+ public static async create(
845
+ optionsArg: ISmartSecretSealedFileStoreOptions,
846
+ ): Promise<SmartSecretSealedFileStore> {
847
+ const options = normalizeOptions(optionsArg);
848
+ if (process.platform !== 'linux' || typeof process.geteuid !== 'function') {
849
+ throw createSmartSecretSealedFileStoreError('KERNEL_UNAVAILABLE');
850
+ }
851
+ const lease = await acquireLease(options);
852
+ let key: Uint8Array | undefined;
853
+ let operationError: unknown;
854
+ try {
855
+ key = await bootstrapUnderLease(options);
856
+ } catch (errorArg) {
857
+ operationError = errorArg;
858
+ }
859
+ try {
860
+ await lease.release();
861
+ } catch {
862
+ operationError = preferCleanupError(
863
+ operationError,
864
+ createSmartSecretSealedFileStoreError('MUTEX_FAILED'),
865
+ );
866
+ }
867
+ if (operationError) {
868
+ key?.fill(0);
869
+ throw operationError;
870
+ }
871
+ const store = new SmartSecretSealedFileStore(options, key!);
872
+ key = undefined;
873
+ return store;
874
+ }
875
+
876
+ /** Explicitly discards this store's ciphertext and kernel master key. */
877
+ public static async reset(
878
+ optionsArg: ISmartSecretSealedFileStoreOptions,
879
+ ): Promise<SmartSecretSealedFileStore> {
880
+ const options = normalizeOptions(optionsArg);
881
+ if (process.platform !== 'linux' || typeof process.geteuid !== 'function') {
882
+ throw createSmartSecretSealedFileStoreError('KERNEL_UNAVAILABLE');
883
+ }
884
+ const lease = await acquireLease(options);
885
+ let key: Uint8Array | undefined;
886
+ let operationError: unknown;
887
+ try {
888
+ const directoryHandle = await ensureDirectory(options.directoryPath);
889
+ try {
890
+ const names = await listOwnedFiles(directoryHandle);
891
+ if (names.some((nameArg) => (
892
+ nameArg === manifestName || entryFilePattern.test(nameArg)
893
+ ))) {
894
+ const manifestValue = await readStrictJsonFile(
895
+ directoryHandle,
896
+ manifestName,
897
+ 4 * 1_024,
898
+ );
899
+ if (manifestValue === undefined) {
900
+ throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
901
+ }
902
+ parseManifest(manifestValue, options);
903
+ }
904
+ try {
905
+ await options.kernelStore.deleteEntry(options.masterKeyAccount);
906
+ } catch (errorArg) {
907
+ throw mapKernelError(errorArg);
908
+ }
909
+ const deletionOrder = names.filter((nameArg) => nameArg !== manifestName);
910
+ if (names.includes(manifestName)) deletionOrder.push(manifestName);
911
+ for (const name of deletionOrder) {
912
+ await removeAndSync(directoryHandle, name);
913
+ }
914
+ } catch (errorArg) {
915
+ operationError = errorArg;
916
+ }
917
+ operationError = await closeFileHandleWithError(directoryHandle, operationError);
918
+ if (operationError) throw operationError;
919
+ key = await bootstrapUnderLease(options);
920
+ } catch (errorArg) {
921
+ operationError = errorArg;
922
+ }
923
+ try {
924
+ await lease.release();
925
+ } catch {
926
+ operationError = preferCleanupError(
927
+ operationError,
928
+ createSmartSecretSealedFileStoreError('MUTEX_FAILED'),
929
+ );
930
+ }
931
+ if (operationError) {
932
+ key?.fill(0);
933
+ throw operationError;
934
+ }
935
+ const store = new SmartSecretSealedFileStore(options, key!);
936
+ key = undefined;
937
+ return store;
938
+ }
939
+
940
+ public getEntry(accountArg: string): Promise<Uint8Array | null> {
941
+ let account: string;
942
+ try {
943
+ account = normalizeSmartSecretKernelAccount(accountArg);
944
+ } catch {
945
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
946
+ }
947
+ return this.reserveOperation((deadlineArg, markAcquiredArg) => this.withLease(
948
+ deadlineArg,
949
+ markAcquiredArg,
950
+ async () => {
951
+ const directoryHandle = await ensureDirectory(this.directoryPath);
952
+ const id = entryId(this.options, account);
953
+ let value: unknown | undefined;
954
+ let operationError: unknown;
955
+ try {
956
+ await this.verifyManifest(directoryHandle);
957
+ value = await readStrictJsonFile(
958
+ directoryHandle,
959
+ `${id}.sealed.json`,
960
+ maximumEnvelopeBytes,
961
+ );
962
+ } catch (errorArg) {
963
+ operationError = errorArg;
964
+ }
965
+ operationError = await closeFileHandleWithError(directoryHandle, operationError);
966
+ if (operationError) throw operationError;
967
+ if (value === undefined) return null;
968
+ const parsed = parseEnvelope(value);
969
+ const aad = entryAad(this.options, account, id, fingerprint(this.masterKey));
970
+ try {
971
+ if (
972
+ parsed.envelope.entryId !== id
973
+ || parsed.envelope.masterKeyFingerprint !== fingerprint(this.masterKey)
974
+ ) throw createSmartSecretSealedFileStoreError('ENVELOPE_INVALID');
975
+ try {
976
+ return await plugins.smartcrypto.aesGcmDecrypt({
977
+ key: this.masterKey,
978
+ encryptedData: parsed.encryptedData,
979
+ aad,
980
+ });
981
+ } catch {
982
+ throw createSmartSecretSealedFileStoreError('AUTHENTICATION_FAILED');
983
+ }
984
+ } finally {
985
+ aad.fill(0);
986
+ parsed.encryptedData.nonce.fill(0);
987
+ parsed.encryptedData.ciphertext.fill(0);
988
+ parsed.encryptedData.tag.fill(0);
989
+ }
990
+ },
991
+ ));
992
+ }
993
+
994
+ public setEntry(accountArg: string, valueArg: Uint8Array): Promise<void> {
995
+ let account: string;
996
+ try {
997
+ account = normalizeSmartSecretKernelAccount(accountArg);
998
+ } catch {
999
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
1000
+ }
1001
+ let value: Uint8Array;
1002
+ try {
1003
+ if (!(valueArg instanceof Uint8Array)) {
1004
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
1005
+ }
1006
+ if (valueArg.byteLength > smartSecretSealedFileMaximumEntryBytes) {
1007
+ throw createSmartSecretSealedFileStoreError('SIZE_LIMIT_EXCEEDED');
1008
+ }
1009
+ value = new Uint8Array(valueArg);
1010
+ } catch (errorArg) {
1011
+ if (errorArg instanceof SmartSecretSealedFileStoreError) throw errorArg;
1012
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
1013
+ }
1014
+ let result: Promise<void>;
1015
+ try {
1016
+ result = this.reserveOperation((deadlineArg, markAcquiredArg) => this.withLease(
1017
+ deadlineArg,
1018
+ markAcquiredArg,
1019
+ async () => {
1020
+ const directoryHandle = await ensureDirectory(this.directoryPath);
1021
+ let encrypted: plugins.smartcrypto.IAesGcmCiphertext | undefined;
1022
+ const id = entryId(this.options, account);
1023
+ const keyFingerprint = fingerprint(this.masterKey);
1024
+ const aad = entryAad(this.options, account, id, keyFingerprint);
1025
+ let operationError: unknown;
1026
+ try {
1027
+ await this.verifyManifest(directoryHandle);
1028
+ try {
1029
+ encrypted = await plugins.smartcrypto.aesGcmEncrypt({
1030
+ key: this.masterKey,
1031
+ plaintext: value,
1032
+ aad,
1033
+ });
1034
+ } catch {
1035
+ throw createSmartSecretSealedFileStoreError('AUTHENTICATION_FAILED');
1036
+ }
1037
+ const envelope: IEnvelopeV1 = {
1038
+ schemaVersion,
1039
+ profile,
1040
+ entryId: id,
1041
+ masterKeyFingerprint: keyFingerprint,
1042
+ ...encodeEncryptedData(encrypted),
1043
+ };
1044
+ await atomicWriteJson(
1045
+ directoryHandle,
1046
+ `${id}.sealed.json`,
1047
+ envelope,
1048
+ );
1049
+ } catch (errorArg) {
1050
+ operationError = errorArg;
1051
+ }
1052
+ try {
1053
+ aad.fill(0);
1054
+ encrypted?.nonce.fill(0);
1055
+ encrypted?.ciphertext.fill(0);
1056
+ encrypted?.tag.fill(0);
1057
+ } finally {
1058
+ operationError = await closeFileHandleWithError(directoryHandle, operationError);
1059
+ }
1060
+ if (operationError) throw operationError;
1061
+ },
1062
+ ));
1063
+ } catch (errorArg) {
1064
+ value.fill(0);
1065
+ throw errorArg;
1066
+ }
1067
+ return result.finally(() => value.fill(0));
1068
+ }
1069
+
1070
+ public deleteEntry(accountArg: string): Promise<boolean> {
1071
+ let account: string;
1072
+ try {
1073
+ account = normalizeSmartSecretKernelAccount(accountArg);
1074
+ } catch {
1075
+ throw createSmartSecretSealedFileStoreError('INVALID_ARGUMENT');
1076
+ }
1077
+ return this.reserveOperation((deadlineArg, markAcquiredArg) => this.withLease(
1078
+ deadlineArg,
1079
+ markAcquiredArg,
1080
+ async () => {
1081
+ const directoryHandle = await ensureDirectory(this.directoryPath);
1082
+ let result: boolean | undefined;
1083
+ let operationError: unknown;
1084
+ try {
1085
+ await this.verifyManifest(directoryHandle);
1086
+ const id = entryId(this.options, account);
1087
+ result = await removeAndSync(directoryHandle, `${id}.sealed.json`);
1088
+ } catch (errorArg) {
1089
+ operationError = errorArg;
1090
+ }
1091
+ operationError = await closeFileHandleWithError(directoryHandle, operationError);
1092
+ if (operationError) throw operationError;
1093
+ return result!;
1094
+ },
1095
+ ));
1096
+ }
1097
+
1098
+ public close(): Promise<void> {
1099
+ if (this.closePromise) return this.closePromise;
1100
+ this.lifecycleState = 'closing';
1101
+ const operationTail = this.operationTail;
1102
+ const closePromise = operationTail.finally(() => {
1103
+ this.masterKey.fill(0);
1104
+ this.lifecycleState = 'closed';
1105
+ });
1106
+ this.closePromise = closePromise;
1107
+ return closePromise;
1108
+ }
1109
+
1110
+ private reserveOperation<T>(
1111
+ operationArg: (deadlineArg: number, markAcquiredArg: () => void) => Promise<T>,
1112
+ ): Promise<T> {
1113
+ if (this.lifecycleState !== 'ready') {
1114
+ throw createSmartSecretSealedFileStoreError('STORE_CLOSED');
1115
+ }
1116
+ if (this.pendingOperations >= maximumPendingOperations) {
1117
+ throw createSmartSecretSealedFileStoreError('OPERATION_LIMIT_REACHED');
1118
+ }
1119
+ this.pendingOperations += 1;
1120
+ const deadline = performance.now() + operationTimeoutMs;
1121
+ let admissionExpired = false;
1122
+ let admissionTimer: NodeJS.Timeout | undefined;
1123
+ let rejectAdmission!: (errorArg: SmartSecretSealedFileStoreError) => void;
1124
+ const admissionTimeout = new Promise<never>((_resolveArg, rejectArg) => {
1125
+ rejectAdmission = rejectArg;
1126
+ });
1127
+ const clearAdmissionTimer = (): void => {
1128
+ if (admissionTimer) {
1129
+ clearTimeout(admissionTimer);
1130
+ admissionTimer = undefined;
1131
+ }
1132
+ };
1133
+ const markAcquired = (): void => clearAdmissionTimer();
1134
+ admissionTimer = setTimeout(() => {
1135
+ admissionExpired = true;
1136
+ admissionTimer = undefined;
1137
+ rejectAdmission(createSmartSecretSealedFileStoreError('MUTEX_FAILED'));
1138
+ }, operationTimeoutMs);
1139
+ const execution = this.operationTail.then(() => {
1140
+ if (admissionExpired || performance.now() >= deadline) {
1141
+ throw createSmartSecretSealedFileStoreError('MUTEX_FAILED');
1142
+ }
1143
+ return operationArg(deadline, markAcquired);
1144
+ });
1145
+ this.operationTail = execution.then(() => undefined, () => undefined);
1146
+ execution.then(() => {
1147
+ clearAdmissionTimer();
1148
+ this.pendingOperations -= 1;
1149
+ }, () => {
1150
+ clearAdmissionTimer();
1151
+ this.pendingOperations -= 1;
1152
+ });
1153
+ return Promise.race([execution, admissionTimeout]);
1154
+ }
1155
+
1156
+ private async withLease<T>(
1157
+ deadlineArg: number,
1158
+ markAcquiredArg: () => void,
1159
+ operationArg: () => Promise<T>,
1160
+ ): Promise<T> {
1161
+ const lease = await acquireLease(this.options, deadlineArg);
1162
+ markAcquiredArg();
1163
+ let result: T | undefined;
1164
+ let operationError: unknown;
1165
+ try {
1166
+ result = await operationArg();
1167
+ } catch (errorArg) {
1168
+ operationError = errorArg;
1169
+ }
1170
+ try {
1171
+ await lease.release();
1172
+ } catch {
1173
+ wipeDiscardedResult(result);
1174
+ if (mutationOutcomeUnknown(operationError)) throw operationError;
1175
+ throw createSmartSecretSealedFileStoreError('MUTEX_FAILED');
1176
+ }
1177
+ if (operationError) throw operationError;
1178
+ return result as T;
1179
+ }
1180
+
1181
+ private async verifyManifest(
1182
+ directoryHandleArg: plugins.fs.promises.FileHandle,
1183
+ ): Promise<void> {
1184
+ const value = await readStrictJsonFile(
1185
+ directoryHandleArg,
1186
+ manifestName,
1187
+ 4 * 1_024,
1188
+ );
1189
+ if (value === undefined) throw createSmartSecretSealedFileStoreError('FILESYSTEM_FAILED');
1190
+ const manifest = parseManifest(value, this.options);
1191
+ if (manifest.masterKeyFingerprint !== fingerprint(this.masterKey)) {
1192
+ throw createSmartSecretSealedFileStoreError('MASTER_KEY_CONFLICT');
1193
+ }
1194
+ }
1195
+ }