@push.rocks/smartsecret 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.smartconfig.json +8 -1
- package/dist_rust/smartsecret-kernel_linux_amd64 +0 -0
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/smartsecret.kernel.error.d.ts +17 -0
- package/dist_ts/smartsecret.kernel.error.js +47 -0
- package/dist_ts/smartsecret.kernel.protocol.d.ts +159 -0
- package/dist_ts/smartsecret.kernel.protocol.js +424 -0
- package/dist_ts/smartsecret.kernelstore.d.ts +49 -0
- package/dist_ts/smartsecret.kernelstore.js +442 -0
- package/dist_ts/smartsecret.plugins.d.ts +4 -1
- package/dist_ts/smartsecret.plugins.js +5 -2
- package/package.json +11 -6
- package/readme.hints.md +11 -0
- package/readme.md +73 -1
- package/third-party-notices.md +177 -0
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/index.ts +6 -0
- package/ts/smartsecret.kernel.error.ts +78 -0
- package/ts/smartsecret.kernel.protocol.ts +657 -0
- package/ts/smartsecret.kernelstore.ts +664 -0
- package/ts/smartsecret.plugins.ts +4 -1
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import * as plugins from './smartsecret.plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
SmartSecretKernelStoreError,
|
|
4
|
+
createSmartSecretKernelStoreError,
|
|
5
|
+
} from './smartsecret.kernel.error.js';
|
|
6
|
+
|
|
7
|
+
export const smartSecretKernelDefaultTimeoutMs = 5_000;
|
|
8
|
+
export const smartSecretKernelMaximumTimeoutMs = 60_000;
|
|
9
|
+
export const smartSecretKernelMaximumEntryBytes = 1_024;
|
|
10
|
+
export const smartSecretKernelMaximumLegacyBytes = 16 * 1_024;
|
|
11
|
+
export const smartSecretKernelMaximumMessageBytes = 32 * 1_024;
|
|
12
|
+
export const smartSecretKernelLegacyService = 'global.idp.devidp';
|
|
13
|
+
|
|
14
|
+
const serviceDomain = Buffer.from(
|
|
15
|
+
'@push.rocks/smartsecret/kernel-service/v1\u0000',
|
|
16
|
+
'utf8',
|
|
17
|
+
);
|
|
18
|
+
const entryDomain = Buffer.from(
|
|
19
|
+
'@push.rocks/smartsecret/kernel-entry/v1\u0000',
|
|
20
|
+
'utf8',
|
|
21
|
+
);
|
|
22
|
+
const mutexDomain = Buffer.from(
|
|
23
|
+
'@push.rocks/smartsecret/kernel-mutex/v1\u0000',
|
|
24
|
+
'utf8',
|
|
25
|
+
);
|
|
26
|
+
const canonicalDigestPattern = /^[A-Za-z0-9_-]{43}$/;
|
|
27
|
+
const legacyAccountPattern = /^v1:[a-f0-9]{64}$/;
|
|
28
|
+
|
|
29
|
+
export type TSmartSecretKernelDomainErrorCode =
|
|
30
|
+
| 'ENTRY_CORRUPT'
|
|
31
|
+
| 'INVALID_REQUEST'
|
|
32
|
+
| 'KERNEL_UNAVAILABLE'
|
|
33
|
+
| 'MUTATION_OUTCOME_UNKNOWN'
|
|
34
|
+
| 'ROOT_CONFLICT'
|
|
35
|
+
| 'SOURCE_CHANGED';
|
|
36
|
+
|
|
37
|
+
export interface ISmartSecretKernelDomainErrorResult {
|
|
38
|
+
outcome: 'domainError';
|
|
39
|
+
code: TSmartSecretKernelDomainErrorCode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ISmartSecretKernelOperationOptions {
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ISmartSecretKernelStoreOptions {
|
|
48
|
+
service: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface INormalizedSmartSecretKernelOperationOptions {
|
|
52
|
+
timeoutMs: number;
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ISmartSecretKernelGetSuccessAbsent {
|
|
57
|
+
outcome: 'success';
|
|
58
|
+
found: false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ISmartSecretKernelGetSuccessPresent {
|
|
62
|
+
outcome: 'success';
|
|
63
|
+
found: true;
|
|
64
|
+
value: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type TSmartSecretKernelGetResult =
|
|
68
|
+
| ISmartSecretKernelDomainErrorResult
|
|
69
|
+
| ISmartSecretKernelGetSuccessAbsent
|
|
70
|
+
| ISmartSecretKernelGetSuccessPresent;
|
|
71
|
+
|
|
72
|
+
export interface ISmartSecretKernelMutationSuccess {
|
|
73
|
+
outcome: 'success';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type TSmartSecretKernelSetResult =
|
|
77
|
+
| ISmartSecretKernelDomainErrorResult
|
|
78
|
+
| ISmartSecretKernelMutationSuccess;
|
|
79
|
+
|
|
80
|
+
export interface ISmartSecretKernelDeleteSuccess {
|
|
81
|
+
outcome: 'success';
|
|
82
|
+
deleted: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type TSmartSecretKernelDeleteResult =
|
|
86
|
+
| ISmartSecretKernelDomainErrorResult
|
|
87
|
+
| ISmartSecretKernelDeleteSuccess;
|
|
88
|
+
|
|
89
|
+
export interface ISmartSecretKernelLegacySuccessAbsent {
|
|
90
|
+
outcome: 'success';
|
|
91
|
+
found: false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ISmartSecretKernelLegacySuccessPresent {
|
|
95
|
+
outcome: 'success';
|
|
96
|
+
found: true;
|
|
97
|
+
value: string;
|
|
98
|
+
sourceSerial: number;
|
|
99
|
+
sessionRootSerial: number | null;
|
|
100
|
+
persistentRootSerial: number;
|
|
101
|
+
foundInSession: boolean;
|
|
102
|
+
foundInPersistent: boolean;
|
|
103
|
+
byteLength: number;
|
|
104
|
+
sha256: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type TSmartSecretKernelLegacyReadResult =
|
|
108
|
+
| ISmartSecretKernelDomainErrorResult
|
|
109
|
+
| ISmartSecretKernelLegacySuccessAbsent
|
|
110
|
+
| ISmartSecretKernelLegacySuccessPresent;
|
|
111
|
+
|
|
112
|
+
export interface ISmartSecretKernelLegacyDeleteSuccess {
|
|
113
|
+
outcome: 'success';
|
|
114
|
+
status: 'deleted' | 'alreadyAbsent';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type TSmartSecretKernelLegacyDeleteResult =
|
|
118
|
+
| ISmartSecretKernelDomainErrorResult
|
|
119
|
+
| ISmartSecretKernelLegacyDeleteSuccess;
|
|
120
|
+
|
|
121
|
+
export interface ISmartSecretKernelLegacyDeleteParams {
|
|
122
|
+
account: string;
|
|
123
|
+
sourceSerial: number;
|
|
124
|
+
sessionRootSerial: number | null;
|
|
125
|
+
persistentRootSerial: number;
|
|
126
|
+
foundInSession: boolean;
|
|
127
|
+
foundInPersistent: boolean;
|
|
128
|
+
byteLength: number;
|
|
129
|
+
sha256: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type TSmartSecretKernelCommands = {
|
|
133
|
+
getEntry: {
|
|
134
|
+
params: { description: string };
|
|
135
|
+
result: TSmartSecretKernelGetResult;
|
|
136
|
+
};
|
|
137
|
+
setEntry: {
|
|
138
|
+
params: { description: string; value: string };
|
|
139
|
+
result: TSmartSecretKernelSetResult;
|
|
140
|
+
};
|
|
141
|
+
deleteEntry: {
|
|
142
|
+
params: { description: string };
|
|
143
|
+
result: TSmartSecretKernelDeleteResult;
|
|
144
|
+
};
|
|
145
|
+
readDevIdpV1Legacy: {
|
|
146
|
+
params: { account: string };
|
|
147
|
+
result: TSmartSecretKernelLegacyReadResult;
|
|
148
|
+
};
|
|
149
|
+
deleteDevIdpV1Legacy: {
|
|
150
|
+
params: ISmartSecretKernelLegacyDeleteParams;
|
|
151
|
+
result: TSmartSecretKernelLegacyDeleteResult;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const domainErrorCodes: ReadonlySet<string> = new Set([
|
|
156
|
+
'ENTRY_CORRUPT',
|
|
157
|
+
'INVALID_REQUEST',
|
|
158
|
+
'KERNEL_UNAVAILABLE',
|
|
159
|
+
'MUTATION_OUTCOME_UNKNOWN',
|
|
160
|
+
'ROOT_CONFLICT',
|
|
161
|
+
'SOURCE_CHANGED',
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
const isSmartSecretKernelDomainErrorCode = (
|
|
165
|
+
valueArg: string,
|
|
166
|
+
): valueArg is TSmartSecretKernelDomainErrorCode => domainErrorCodes.has(valueArg);
|
|
167
|
+
|
|
168
|
+
const requireExactObject = (
|
|
169
|
+
valueArg: unknown,
|
|
170
|
+
keysArg: readonly string[],
|
|
171
|
+
inputArg: boolean,
|
|
172
|
+
): Record<string, unknown> => {
|
|
173
|
+
const fail = (): never => {
|
|
174
|
+
throw createSmartSecretKernelStoreError(
|
|
175
|
+
inputArg ? 'INVALID_ARGUMENT' : 'WORKER_INTEGRITY_FAILED',
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
try {
|
|
179
|
+
if (valueArg === null || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
|
|
180
|
+
return fail();
|
|
181
|
+
}
|
|
182
|
+
const prototype = Object.getPrototypeOf(valueArg);
|
|
183
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
184
|
+
return fail();
|
|
185
|
+
}
|
|
186
|
+
const ownKeys = Reflect.ownKeys(valueArg);
|
|
187
|
+
if (
|
|
188
|
+
ownKeys.length !== keysArg.length
|
|
189
|
+
|| ownKeys.some((keyArg) => typeof keyArg !== 'string' || !keysArg.includes(keyArg))
|
|
190
|
+
|| keysArg.some((keyArg) => !ownKeys.includes(keyArg))
|
|
191
|
+
) {
|
|
192
|
+
return fail();
|
|
193
|
+
}
|
|
194
|
+
const result: Record<string, unknown> = Object.create(null);
|
|
195
|
+
for (const key of keysArg) {
|
|
196
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, key);
|
|
197
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) {
|
|
198
|
+
return fail();
|
|
199
|
+
}
|
|
200
|
+
result[key] = descriptor.value;
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
} catch {
|
|
204
|
+
return fail();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const requireAllowedInputObject = (
|
|
209
|
+
valueArg: unknown,
|
|
210
|
+
allowedKeysArg: readonly string[],
|
|
211
|
+
): Record<string, unknown> => {
|
|
212
|
+
const fail = (): never => {
|
|
213
|
+
throw createSmartSecretKernelStoreError('INVALID_ARGUMENT');
|
|
214
|
+
};
|
|
215
|
+
try {
|
|
216
|
+
if (valueArg === null || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
|
|
217
|
+
return fail();
|
|
218
|
+
}
|
|
219
|
+
const prototype = Object.getPrototypeOf(valueArg);
|
|
220
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
221
|
+
return fail();
|
|
222
|
+
}
|
|
223
|
+
const ownKeys = Reflect.ownKeys(valueArg);
|
|
224
|
+
if (
|
|
225
|
+
ownKeys.some((keyArg) =>
|
|
226
|
+
typeof keyArg !== 'string' || !allowedKeysArg.includes(keyArg)
|
|
227
|
+
)
|
|
228
|
+
) {
|
|
229
|
+
return fail();
|
|
230
|
+
}
|
|
231
|
+
const result: Record<string, unknown> = Object.create(null);
|
|
232
|
+
for (const key of ownKeys) {
|
|
233
|
+
if (typeof key !== 'string') return fail();
|
|
234
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, key);
|
|
235
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) {
|
|
236
|
+
return fail();
|
|
237
|
+
}
|
|
238
|
+
result[key] = descriptor.value;
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error instanceof SmartSecretKernelStoreError) throw error;
|
|
243
|
+
return fail();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const requireValidScalarString = (valueArg: unknown): string => {
|
|
248
|
+
if (typeof valueArg !== 'string') {
|
|
249
|
+
throw createSmartSecretKernelStoreError('INVALID_ARGUMENT');
|
|
250
|
+
}
|
|
251
|
+
for (let index = 0; index < valueArg.length; index++) {
|
|
252
|
+
const codeUnit = valueArg.charCodeAt(index);
|
|
253
|
+
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
254
|
+
const next = valueArg.charCodeAt(index + 1);
|
|
255
|
+
if (index + 1 >= valueArg.length || next < 0xdc00 || next > 0xdfff) {
|
|
256
|
+
throw createSmartSecretKernelStoreError('INVALID_ARGUMENT');
|
|
257
|
+
}
|
|
258
|
+
index++;
|
|
259
|
+
} else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
|
260
|
+
throw createSmartSecretKernelStoreError('INVALID_ARGUMENT');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const byteLength = Buffer.byteLength(valueArg, 'utf8');
|
|
264
|
+
if (byteLength < 1 || byteLength > 1_024) {
|
|
265
|
+
throw createSmartSecretKernelStoreError('INVALID_ARGUMENT');
|
|
266
|
+
}
|
|
267
|
+
return valueArg;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const lengthPrefix = (lengthArg: number): Buffer => {
|
|
271
|
+
const prefix = Buffer.alloc(4);
|
|
272
|
+
prefix.writeUInt32BE(lengthArg, 0);
|
|
273
|
+
return prefix;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const digestDescription = (
|
|
277
|
+
prefixArg: string,
|
|
278
|
+
partsArg: readonly Uint8Array[],
|
|
279
|
+
): string => {
|
|
280
|
+
const hash = plugins.crypto.createHash('sha256');
|
|
281
|
+
for (const part of partsArg) hash.update(part);
|
|
282
|
+
return `${prefixArg}${hash.digest('base64url')}`;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export const normalizeSmartSecretKernelService = (valueArg: unknown): string =>
|
|
286
|
+
requireValidScalarString(valueArg);
|
|
287
|
+
|
|
288
|
+
export const normalizeSmartSecretKernelStoreOptions = (
|
|
289
|
+
valueArg: unknown,
|
|
290
|
+
): ISmartSecretKernelStoreOptions => {
|
|
291
|
+
const value = requireExactObject(valueArg, ['service'], true);
|
|
292
|
+
return { service: normalizeSmartSecretKernelService(value.service) };
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
export const normalizeSmartSecretKernelAccount = (valueArg: unknown): string =>
|
|
296
|
+
requireValidScalarString(valueArg);
|
|
297
|
+
|
|
298
|
+
export const normalizeDevIdpV1LegacyAccount = (valueArg: unknown): string => {
|
|
299
|
+
const account = requireValidScalarString(valueArg);
|
|
300
|
+
if (!legacyAccountPattern.test(account)) {
|
|
301
|
+
throw createSmartSecretKernelStoreError('INVALID_ARGUMENT');
|
|
302
|
+
}
|
|
303
|
+
return account;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
export const deriveSmartSecretKernelServiceDescription = (serviceArg: string): string => {
|
|
307
|
+
const service = Buffer.from(normalizeSmartSecretKernelService(serviceArg), 'utf8');
|
|
308
|
+
return digestDescription('smartsecret-service:v1:', [
|
|
309
|
+
serviceDomain,
|
|
310
|
+
lengthPrefix(service.byteLength),
|
|
311
|
+
service,
|
|
312
|
+
]);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
export const deriveSmartSecretKernelEntryDescription = (
|
|
316
|
+
serviceArg: string,
|
|
317
|
+
accountArg: string,
|
|
318
|
+
): string => {
|
|
319
|
+
const service = Buffer.from(normalizeSmartSecretKernelService(serviceArg), 'utf8');
|
|
320
|
+
const account = Buffer.from(normalizeSmartSecretKernelAccount(accountArg), 'utf8');
|
|
321
|
+
return digestDescription('smartsecret-entry:v1:', [
|
|
322
|
+
entryDomain,
|
|
323
|
+
lengthPrefix(service.byteLength),
|
|
324
|
+
service,
|
|
325
|
+
lengthPrefix(account.byteLength),
|
|
326
|
+
account,
|
|
327
|
+
]);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
export const deriveSmartSecretKernelMutexNamespace = (serviceArg: string): string => {
|
|
331
|
+
const service = Buffer.from(normalizeSmartSecretKernelService(serviceArg), 'utf8');
|
|
332
|
+
return digestDescription('smartsecret-kernel:v1:', [
|
|
333
|
+
mutexDomain,
|
|
334
|
+
lengthPrefix(service.byteLength),
|
|
335
|
+
service,
|
|
336
|
+
]);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
export const deriveDevIdpV1LegacyDescription = (accountArg: string): string =>
|
|
340
|
+
`keyring:${normalizeDevIdpV1LegacyAccount(accountArg)}@${smartSecretKernelLegacyService}`;
|
|
341
|
+
|
|
342
|
+
export const normalizeSmartSecretKernelValue = (valueArg: unknown): Uint8Array => {
|
|
343
|
+
try {
|
|
344
|
+
if (
|
|
345
|
+
!(valueArg instanceof Uint8Array)
|
|
346
|
+
|| valueArg.byteLength > smartSecretKernelMaximumEntryBytes
|
|
347
|
+
) {
|
|
348
|
+
throw createSmartSecretKernelStoreError('INVALID_ARGUMENT');
|
|
349
|
+
}
|
|
350
|
+
return new Uint8Array(valueArg);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (error instanceof SmartSecretKernelStoreError) throw error;
|
|
353
|
+
throw createSmartSecretKernelStoreError('INVALID_ARGUMENT');
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
export const encodeCanonicalBase64Url = (valueArg: Uint8Array): string => {
|
|
358
|
+
const value = Buffer.from(valueArg);
|
|
359
|
+
try {
|
|
360
|
+
return value.toString('base64url');
|
|
361
|
+
} finally {
|
|
362
|
+
value.fill(0);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const decodeCanonicalBase64Url = (
|
|
367
|
+
valueArg: unknown,
|
|
368
|
+
maximumBytesArg: number,
|
|
369
|
+
minimumBytesArg = 0,
|
|
370
|
+
): Uint8Array => {
|
|
371
|
+
const maximumEncodedLength = Math.ceil(maximumBytesArg * 4 / 3);
|
|
372
|
+
if (
|
|
373
|
+
typeof valueArg !== 'string'
|
|
374
|
+
|| valueArg.length > maximumEncodedLength
|
|
375
|
+
|| valueArg.length % 4 === 1
|
|
376
|
+
|| !/^[A-Za-z0-9_-]*$/.test(valueArg)
|
|
377
|
+
) {
|
|
378
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
379
|
+
}
|
|
380
|
+
const decoded = Buffer.from(valueArg, 'base64url');
|
|
381
|
+
if (
|
|
382
|
+
decoded.byteLength < minimumBytesArg
|
|
383
|
+
|| decoded.byteLength > maximumBytesArg
|
|
384
|
+
|| decoded.toString('base64url') !== valueArg
|
|
385
|
+
) {
|
|
386
|
+
decoded.fill(0);
|
|
387
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
388
|
+
}
|
|
389
|
+
const result = new Uint8Array(decoded);
|
|
390
|
+
decoded.fill(0);
|
|
391
|
+
return result;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
export const normalizeSmartSecretKernelOperationOptions = (
|
|
395
|
+
valueArg: ISmartSecretKernelOperationOptions | undefined,
|
|
396
|
+
): INormalizedSmartSecretKernelOperationOptions => {
|
|
397
|
+
if (valueArg === undefined) return { timeoutMs: smartSecretKernelDefaultTimeoutMs };
|
|
398
|
+
const options = requireAllowedInputObject(valueArg, ['timeoutMs', 'signal']);
|
|
399
|
+
const timeoutMs = Object.hasOwn(options, 'timeoutMs')
|
|
400
|
+
? options.timeoutMs
|
|
401
|
+
: smartSecretKernelDefaultTimeoutMs;
|
|
402
|
+
if (
|
|
403
|
+
typeof timeoutMs !== 'number'
|
|
404
|
+
|| !Number.isSafeInteger(timeoutMs)
|
|
405
|
+
|| timeoutMs < 1
|
|
406
|
+
|| timeoutMs > smartSecretKernelMaximumTimeoutMs
|
|
407
|
+
) {
|
|
408
|
+
throw createSmartSecretKernelStoreError('INVALID_ARGUMENT');
|
|
409
|
+
}
|
|
410
|
+
if (!Object.hasOwn(options, 'signal')) return { timeoutMs };
|
|
411
|
+
let signal: AbortSignal;
|
|
412
|
+
try {
|
|
413
|
+
if (
|
|
414
|
+
!(options.signal instanceof AbortSignal)
|
|
415
|
+
|| Object.getPrototypeOf(options.signal) !== AbortSignal.prototype
|
|
416
|
+
) {
|
|
417
|
+
throw createSmartSecretKernelStoreError('INVALID_ARGUMENT');
|
|
418
|
+
}
|
|
419
|
+
signal = options.signal;
|
|
420
|
+
void signal.aborted;
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (error instanceof SmartSecretKernelStoreError) throw error;
|
|
423
|
+
throw createSmartSecretKernelStoreError('INVALID_ARGUMENT');
|
|
424
|
+
}
|
|
425
|
+
return { timeoutMs, signal };
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const parseDomainError = (
|
|
429
|
+
valueArg: Record<string, unknown>,
|
|
430
|
+
): ISmartSecretKernelDomainErrorResult | undefined => {
|
|
431
|
+
if (valueArg.outcome !== 'domainError') return undefined;
|
|
432
|
+
const result = requireExactObject(valueArg, ['outcome', 'code'], false);
|
|
433
|
+
if (
|
|
434
|
+
typeof result.code !== 'string'
|
|
435
|
+
|| !isSmartSecretKernelDomainErrorCode(result.code)
|
|
436
|
+
) {
|
|
437
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
outcome: 'domainError',
|
|
441
|
+
code: result.code,
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const requireResultObject = (valueArg: unknown): Record<string, unknown> => {
|
|
446
|
+
try {
|
|
447
|
+
if (valueArg === null || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
|
|
448
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
449
|
+
}
|
|
450
|
+
const prototype = Object.getPrototypeOf(valueArg);
|
|
451
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
452
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
453
|
+
}
|
|
454
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, 'outcome');
|
|
455
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) {
|
|
456
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
457
|
+
}
|
|
458
|
+
const result: Record<string, unknown> = Object.create(null);
|
|
459
|
+
for (const key of Reflect.ownKeys(valueArg)) {
|
|
460
|
+
if (typeof key !== 'string') {
|
|
461
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
462
|
+
}
|
|
463
|
+
const property = Object.getOwnPropertyDescriptor(valueArg, key);
|
|
464
|
+
if (!property?.enumerable || !('value' in property)) {
|
|
465
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
466
|
+
}
|
|
467
|
+
result[key] = property.value;
|
|
468
|
+
}
|
|
469
|
+
return result;
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (error instanceof SmartSecretKernelStoreError) throw error;
|
|
472
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const requirePositiveSerial = (valueArg: unknown): number => {
|
|
477
|
+
if (
|
|
478
|
+
typeof valueArg !== 'number'
|
|
479
|
+
|| !Number.isSafeInteger(valueArg)
|
|
480
|
+
|| valueArg < 1
|
|
481
|
+
|| valueArg > 2_147_483_647
|
|
482
|
+
) {
|
|
483
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
484
|
+
}
|
|
485
|
+
return valueArg;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
export const parseSmartSecretKernelGetResult = (
|
|
489
|
+
valueArg: unknown,
|
|
490
|
+
):
|
|
491
|
+
| { kind: 'domainError'; domainError: ISmartSecretKernelDomainErrorResult }
|
|
492
|
+
| { kind: 'success'; found: false }
|
|
493
|
+
| { kind: 'success'; found: true; value: Uint8Array } => {
|
|
494
|
+
const value = requireResultObject(valueArg);
|
|
495
|
+
const domainError = parseDomainError(value);
|
|
496
|
+
if (domainError) return { kind: 'domainError', domainError };
|
|
497
|
+
if (value.outcome !== 'success' || typeof value.found !== 'boolean') {
|
|
498
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
499
|
+
}
|
|
500
|
+
if (!value.found) {
|
|
501
|
+
requireExactObject(value, ['outcome', 'found'], false);
|
|
502
|
+
return { kind: 'success', found: false };
|
|
503
|
+
}
|
|
504
|
+
const result = requireExactObject(value, ['outcome', 'found', 'value'], false);
|
|
505
|
+
return {
|
|
506
|
+
kind: 'success',
|
|
507
|
+
found: true,
|
|
508
|
+
value: decodeCanonicalBase64Url(result.value, smartSecretKernelMaximumEntryBytes),
|
|
509
|
+
};
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
export const parseSmartSecretKernelSetResult = (
|
|
513
|
+
valueArg: unknown,
|
|
514
|
+
):
|
|
515
|
+
| { kind: 'domainError'; domainError: ISmartSecretKernelDomainErrorResult }
|
|
516
|
+
| { kind: 'success' } => {
|
|
517
|
+
const value = requireResultObject(valueArg);
|
|
518
|
+
const domainError = parseDomainError(value);
|
|
519
|
+
if (domainError) return { kind: 'domainError', domainError };
|
|
520
|
+
const result = requireExactObject(value, ['outcome'], false);
|
|
521
|
+
if (result.outcome !== 'success') {
|
|
522
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
523
|
+
}
|
|
524
|
+
return { kind: 'success' };
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
export const parseSmartSecretKernelDeleteResult = (
|
|
528
|
+
valueArg: unknown,
|
|
529
|
+
):
|
|
530
|
+
| { kind: 'domainError'; domainError: ISmartSecretKernelDomainErrorResult }
|
|
531
|
+
| { kind: 'success'; deleted: boolean } => {
|
|
532
|
+
const value = requireResultObject(valueArg);
|
|
533
|
+
const domainError = parseDomainError(value);
|
|
534
|
+
if (domainError) return { kind: 'domainError', domainError };
|
|
535
|
+
const result = requireExactObject(value, ['outcome', 'deleted'], false);
|
|
536
|
+
if (result.outcome !== 'success' || typeof result.deleted !== 'boolean') {
|
|
537
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
538
|
+
}
|
|
539
|
+
return { kind: 'success', deleted: result.deleted };
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
export type TParsedSmartSecretKernelLegacyRead =
|
|
543
|
+
| { kind: 'domainError'; domainError: ISmartSecretKernelDomainErrorResult }
|
|
544
|
+
| { kind: 'success'; found: false }
|
|
545
|
+
| {
|
|
546
|
+
kind: 'success';
|
|
547
|
+
found: true;
|
|
548
|
+
value: Uint8Array;
|
|
549
|
+
receiptParams: ISmartSecretKernelLegacyDeleteParams;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
export const parseSmartSecretKernelLegacyReadResult = (
|
|
553
|
+
valueArg: unknown,
|
|
554
|
+
accountArg: string,
|
|
555
|
+
): TParsedSmartSecretKernelLegacyRead => {
|
|
556
|
+
const account = normalizeDevIdpV1LegacyAccount(accountArg);
|
|
557
|
+
const value = requireResultObject(valueArg);
|
|
558
|
+
const domainError = parseDomainError(value);
|
|
559
|
+
if (domainError) return { kind: 'domainError', domainError };
|
|
560
|
+
if (value.outcome !== 'success' || typeof value.found !== 'boolean') {
|
|
561
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
562
|
+
}
|
|
563
|
+
if (!value.found) {
|
|
564
|
+
requireExactObject(value, ['outcome', 'found'], false);
|
|
565
|
+
return { kind: 'success', found: false };
|
|
566
|
+
}
|
|
567
|
+
const result = requireExactObject(value, [
|
|
568
|
+
'outcome',
|
|
569
|
+
'found',
|
|
570
|
+
'value',
|
|
571
|
+
'sourceSerial',
|
|
572
|
+
'sessionRootSerial',
|
|
573
|
+
'persistentRootSerial',
|
|
574
|
+
'foundInSession',
|
|
575
|
+
'foundInPersistent',
|
|
576
|
+
'byteLength',
|
|
577
|
+
'sha256',
|
|
578
|
+
], false);
|
|
579
|
+
const decoded = decodeCanonicalBase64Url(
|
|
580
|
+
result.value,
|
|
581
|
+
smartSecretKernelMaximumLegacyBytes,
|
|
582
|
+
1,
|
|
583
|
+
);
|
|
584
|
+
let actualDigest: Buffer | undefined;
|
|
585
|
+
let claimedDigest: Buffer | undefined;
|
|
586
|
+
let didSucceed = false;
|
|
587
|
+
try {
|
|
588
|
+
if (
|
|
589
|
+
typeof result.byteLength !== 'number'
|
|
590
|
+
|| !Number.isSafeInteger(result.byteLength)
|
|
591
|
+
|| result.byteLength !== decoded.byteLength
|
|
592
|
+
|| typeof result.foundInSession !== 'boolean'
|
|
593
|
+
|| typeof result.foundInPersistent !== 'boolean'
|
|
594
|
+
|| (!result.foundInSession && !result.foundInPersistent)
|
|
595
|
+
|| typeof result.sha256 !== 'string'
|
|
596
|
+
|| !canonicalDigestPattern.test(result.sha256)
|
|
597
|
+
) {
|
|
598
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
599
|
+
}
|
|
600
|
+
const sourceSerial = requirePositiveSerial(result.sourceSerial);
|
|
601
|
+
const persistentRootSerial = requirePositiveSerial(result.persistentRootSerial);
|
|
602
|
+
const sessionRootSerial = result.sessionRootSerial === null
|
|
603
|
+
? null
|
|
604
|
+
: requirePositiveSerial(result.sessionRootSerial);
|
|
605
|
+
if (result.foundInSession && sessionRootSerial === null) {
|
|
606
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
607
|
+
}
|
|
608
|
+
actualDigest = plugins.crypto.createHash('sha256').update(decoded).digest();
|
|
609
|
+
claimedDigest = Buffer.from(result.sha256, 'base64url');
|
|
610
|
+
if (
|
|
611
|
+
claimedDigest.byteLength !== 32
|
|
612
|
+
|| claimedDigest.toString('base64url') !== result.sha256
|
|
613
|
+
|| !plugins.crypto.timingSafeEqual(actualDigest, claimedDigest)
|
|
614
|
+
) {
|
|
615
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
616
|
+
}
|
|
617
|
+
const parsed: TParsedSmartSecretKernelLegacyRead = {
|
|
618
|
+
kind: 'success',
|
|
619
|
+
found: true,
|
|
620
|
+
value: decoded,
|
|
621
|
+
receiptParams: {
|
|
622
|
+
account,
|
|
623
|
+
sourceSerial,
|
|
624
|
+
sessionRootSerial,
|
|
625
|
+
persistentRootSerial,
|
|
626
|
+
foundInSession: result.foundInSession,
|
|
627
|
+
foundInPersistent: result.foundInPersistent,
|
|
628
|
+
byteLength: result.byteLength,
|
|
629
|
+
sha256: result.sha256,
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
didSucceed = true;
|
|
633
|
+
return parsed;
|
|
634
|
+
} finally {
|
|
635
|
+
actualDigest?.fill(0);
|
|
636
|
+
claimedDigest?.fill(0);
|
|
637
|
+
if (!didSucceed) decoded.fill(0);
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
export const parseSmartSecretKernelLegacyDeleteResult = (
|
|
642
|
+
valueArg: unknown,
|
|
643
|
+
):
|
|
644
|
+
| { kind: 'domainError'; domainError: ISmartSecretKernelDomainErrorResult }
|
|
645
|
+
| { kind: 'success'; status: 'deleted' | 'alreadyAbsent' } => {
|
|
646
|
+
const value = requireResultObject(valueArg);
|
|
647
|
+
const domainError = parseDomainError(value);
|
|
648
|
+
if (domainError) return { kind: 'domainError', domainError };
|
|
649
|
+
const result = requireExactObject(value, ['outcome', 'status'], false);
|
|
650
|
+
if (
|
|
651
|
+
result.outcome !== 'success'
|
|
652
|
+
|| (result.status !== 'deleted' && result.status !== 'alreadyAbsent')
|
|
653
|
+
) {
|
|
654
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
655
|
+
}
|
|
656
|
+
return { kind: 'success', status: result.status };
|
|
657
|
+
};
|