@push.rocks/smartsecret 1.0.2 → 1.2.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.
- package/.smartconfig.json +55 -0
- package/dist_rust/smartsecret-kernel_linux_amd64 +0 -0
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/index.d.ts +7 -0
- package/dist_ts/index.js +5 -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.keyring.d.ts +81 -0
- package/dist_ts/smartsecret.keyring.error.d.ts +17 -0
- package/dist_ts/smartsecret.keyring.error.js +46 -0
- package/dist_ts/smartsecret.keyring.js +758 -0
- package/dist_ts/smartsecret.keyring.protocol.d.ts +57 -0
- package/dist_ts/smartsecret.keyring.protocol.js +290 -0
- package/dist_ts/smartsecret.plugins.d.ts +5 -1
- package/dist_ts/smartsecret.plugins.js +7 -2
- package/package.json +23 -15
- package/readme.hints.md +19 -1
- package/readme.md +182 -4
- package/third-party-notices.md +177 -0
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/index.ts +16 -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.keyring.error.ts +76 -0
- package/ts/smartsecret.keyring.protocol.ts +377 -0
- package/ts/smartsecret.keyring.ts +955 -0
- package/ts/smartsecret.plugins.ts +7 -1
- package/npmextra.json +0 -44
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
import * as plugins from './smartsecret.plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
SmartSecretKeyringError,
|
|
4
|
+
createSmartSecretKeyringError,
|
|
5
|
+
} from './smartsecret.keyring.error.js';
|
|
6
|
+
import {
|
|
7
|
+
SMARTSECRET_KEYRING_MAX_CONTEXT_BYTES,
|
|
8
|
+
SMARTSECRET_KEYRING_MAX_PAYLOAD_BYTES,
|
|
9
|
+
SMARTSECRET_KEYRING_MIN_CONTEXT_BYTES,
|
|
10
|
+
SMARTSECRET_KEYRING_PROFILE,
|
|
11
|
+
SMARTSECRET_KEYRING_SCHEMA_VERSION,
|
|
12
|
+
createSmartSecretPayloadDigest,
|
|
13
|
+
createSmartSecretWrapAad,
|
|
14
|
+
encodeCanonicalBase64Url,
|
|
15
|
+
fingerprintToBytes,
|
|
16
|
+
parseSmartSecretEnvelope,
|
|
17
|
+
requireEpochMilliseconds,
|
|
18
|
+
requireFingerprint,
|
|
19
|
+
requireKekVersion,
|
|
20
|
+
requireSmartSecretIdentifier,
|
|
21
|
+
sha256Bytes,
|
|
22
|
+
timingSafeEqual,
|
|
23
|
+
wipeCiphertext,
|
|
24
|
+
type ISmartSecretEnvelopeV1,
|
|
25
|
+
type IParsedSmartSecretEnvelopeV1,
|
|
26
|
+
} from './smartsecret.keyring.protocol.js';
|
|
27
|
+
|
|
28
|
+
export type TSmartSecretKekStatus = 'active' | 'retired' | 'revoked';
|
|
29
|
+
|
|
30
|
+
export interface ISmartSecretKekDescriptor {
|
|
31
|
+
version: number;
|
|
32
|
+
status: TSmartSecretKekStatus;
|
|
33
|
+
credentialPath?: string;
|
|
34
|
+
expectedFingerprint?: `sha256:${string}`;
|
|
35
|
+
createdAt: number;
|
|
36
|
+
activatedAt: number;
|
|
37
|
+
retiredAt?: number;
|
|
38
|
+
revokedAt?: number;
|
|
39
|
+
escrowReference?: string;
|
|
40
|
+
escrowedAt?: number;
|
|
41
|
+
recoveryTestedAt?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ISmartSecretKeyringConfig {
|
|
45
|
+
keyringId: string;
|
|
46
|
+
keys: readonly ISmartSecretKekDescriptor[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ISmartSecretKekInspection {
|
|
50
|
+
version: number;
|
|
51
|
+
status: TSmartSecretKekStatus;
|
|
52
|
+
expectedFingerprint?: `sha256:${string}`;
|
|
53
|
+
createdAt: number;
|
|
54
|
+
activatedAt: number;
|
|
55
|
+
retiredAt?: number;
|
|
56
|
+
revokedAt?: number;
|
|
57
|
+
escrowReference?: string;
|
|
58
|
+
escrowedAt?: number;
|
|
59
|
+
recoveryTestedAt?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ISmartSecretKeyringInspection {
|
|
63
|
+
keyringId: string;
|
|
64
|
+
activeVersion: number;
|
|
65
|
+
keys: ISmartSecretKekInspection[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface IEncryptSmartSecretEnvelopeOptions {
|
|
69
|
+
envelopeId: string;
|
|
70
|
+
plaintext: Uint8Array;
|
|
71
|
+
context: Uint8Array;
|
|
72
|
+
createdAt?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface IDecryptSmartSecretEnvelopeOptions {
|
|
76
|
+
envelope: unknown;
|
|
77
|
+
expectedEnvelopeId: string;
|
|
78
|
+
context: Uint8Array;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface IRewrapSmartSecretEnvelopeOptions {
|
|
82
|
+
envelope: unknown;
|
|
83
|
+
expectedEnvelopeId: string;
|
|
84
|
+
context: Uint8Array;
|
|
85
|
+
targetVersion?: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ISmartSecretRecoverySelfTestResult {
|
|
89
|
+
keyringId: string;
|
|
90
|
+
version: number;
|
|
91
|
+
status: 'active' | 'retired';
|
|
92
|
+
fingerprint: `sha256:${string}`;
|
|
93
|
+
testedAt: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface INormalizedSmartSecretKekDescriptor extends ISmartSecretKekDescriptor {
|
|
97
|
+
credentialPath?: string;
|
|
98
|
+
expectedFingerprint?: `sha256:${string}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface ILoadedSmartSecretKek {
|
|
102
|
+
descriptor: INormalizedSmartSecretKekDescriptor;
|
|
103
|
+
key: Uint8Array;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type TBigIntStats = plugins.fs.BigIntStats;
|
|
107
|
+
|
|
108
|
+
const descriptorKeys = [
|
|
109
|
+
'version',
|
|
110
|
+
'status',
|
|
111
|
+
'credentialPath',
|
|
112
|
+
'expectedFingerprint',
|
|
113
|
+
'createdAt',
|
|
114
|
+
'activatedAt',
|
|
115
|
+
'retiredAt',
|
|
116
|
+
'revokedAt',
|
|
117
|
+
'escrowReference',
|
|
118
|
+
'escrowedAt',
|
|
119
|
+
'recoveryTestedAt',
|
|
120
|
+
] as const;
|
|
121
|
+
|
|
122
|
+
const requireConfigObject = (
|
|
123
|
+
valueArg: unknown,
|
|
124
|
+
allowedKeysArg: readonly string[],
|
|
125
|
+
requiredKeysArg: readonly string[],
|
|
126
|
+
errorCodeArg: 'CONFIG_INVALID' | 'INVALID_ARGUMENT' = 'CONFIG_INVALID',
|
|
127
|
+
): Record<string, unknown> => {
|
|
128
|
+
try {
|
|
129
|
+
if (valueArg === null || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
|
|
130
|
+
throw createSmartSecretKeyringError(errorCodeArg);
|
|
131
|
+
}
|
|
132
|
+
const prototype = Object.getPrototypeOf(valueArg);
|
|
133
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
134
|
+
throw createSmartSecretKeyringError(errorCodeArg);
|
|
135
|
+
}
|
|
136
|
+
const ownKeys = Reflect.ownKeys(valueArg);
|
|
137
|
+
if (
|
|
138
|
+
ownKeys.length < requiredKeysArg.length
|
|
139
|
+
|| ownKeys.length > allowedKeysArg.length
|
|
140
|
+
|| ownKeys.some((keyArg) => typeof keyArg !== 'string')
|
|
141
|
+
) {
|
|
142
|
+
throw createSmartSecretKeyringError(errorCodeArg);
|
|
143
|
+
}
|
|
144
|
+
const keys = ownKeys as string[];
|
|
145
|
+
if (
|
|
146
|
+
keys.some((keyArg) => !allowedKeysArg.includes(keyArg))
|
|
147
|
+
|| requiredKeysArg.some((keyArg) => !keys.includes(keyArg))
|
|
148
|
+
) {
|
|
149
|
+
throw createSmartSecretKeyringError(errorCodeArg);
|
|
150
|
+
}
|
|
151
|
+
const result: Record<string, unknown> = Object.create(null);
|
|
152
|
+
for (const key of keys) {
|
|
153
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, key);
|
|
154
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) {
|
|
155
|
+
throw createSmartSecretKeyringError(errorCodeArg);
|
|
156
|
+
}
|
|
157
|
+
result[key] = descriptor.value;
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error instanceof SmartSecretKeyringError) {
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
throw createSmartSecretKeyringError(errorCodeArg);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const requireDescriptorArray = (valueArg: unknown): readonly unknown[] => {
|
|
169
|
+
try {
|
|
170
|
+
if (!Array.isArray(valueArg) || Object.getPrototypeOf(valueArg) !== Array.prototype) {
|
|
171
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
172
|
+
}
|
|
173
|
+
const ownKeys = Reflect.ownKeys(valueArg);
|
|
174
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(valueArg, 'length');
|
|
175
|
+
if (
|
|
176
|
+
!lengthDescriptor
|
|
177
|
+
|| !('value' in lengthDescriptor)
|
|
178
|
+
|| !Number.isSafeInteger(lengthDescriptor.value)
|
|
179
|
+
|| lengthDescriptor.value < 0
|
|
180
|
+
|| ownKeys.length !== lengthDescriptor.value + 1
|
|
181
|
+
) {
|
|
182
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
183
|
+
}
|
|
184
|
+
if (ownKeys.some((keyArg) => typeof keyArg === 'symbol')) {
|
|
185
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
186
|
+
}
|
|
187
|
+
const result = new Array<unknown>(lengthDescriptor.value);
|
|
188
|
+
for (let index = 0; index < lengthDescriptor.value; index++) {
|
|
189
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, String(index));
|
|
190
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) {
|
|
191
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
192
|
+
}
|
|
193
|
+
result[index] = descriptor.value;
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error instanceof SmartSecretKeyringError) {
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const requireOptionalTimestamp = (
|
|
205
|
+
valueArg: Record<string, unknown>,
|
|
206
|
+
keyArg: string,
|
|
207
|
+
): number | undefined => {
|
|
208
|
+
if (!Object.hasOwn(valueArg, keyArg)) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
return requireEpochMilliseconds(valueArg[keyArg]);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const normalizeDescriptor = (valueArg: unknown): INormalizedSmartSecretKekDescriptor => {
|
|
215
|
+
const value = requireConfigObject(valueArg, descriptorKeys, [
|
|
216
|
+
'version',
|
|
217
|
+
'status',
|
|
218
|
+
'createdAt',
|
|
219
|
+
'activatedAt',
|
|
220
|
+
]);
|
|
221
|
+
const version = requireKekVersion(value.version);
|
|
222
|
+
if (value.status !== 'active' && value.status !== 'retired' && value.status !== 'revoked') {
|
|
223
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
224
|
+
}
|
|
225
|
+
const status = value.status;
|
|
226
|
+
const createdAt = requireEpochMilliseconds(value.createdAt);
|
|
227
|
+
const activatedAt = requireEpochMilliseconds(value.activatedAt);
|
|
228
|
+
const retiredAt = requireOptionalTimestamp(value, 'retiredAt');
|
|
229
|
+
const revokedAt = requireOptionalTimestamp(value, 'revokedAt');
|
|
230
|
+
const escrowedAt = requireOptionalTimestamp(value, 'escrowedAt');
|
|
231
|
+
const recoveryTestedAt = requireOptionalTimestamp(value, 'recoveryTestedAt');
|
|
232
|
+
if (createdAt > activatedAt) {
|
|
233
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (status === 'active' && (retiredAt !== undefined || revokedAt !== undefined)) {
|
|
237
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
238
|
+
}
|
|
239
|
+
if (status === 'retired' && (retiredAt === undefined || revokedAt !== undefined)) {
|
|
240
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
241
|
+
}
|
|
242
|
+
if (status === 'revoked' && revokedAt === undefined) {
|
|
243
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
244
|
+
}
|
|
245
|
+
if (retiredAt !== undefined && retiredAt < activatedAt) {
|
|
246
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
247
|
+
}
|
|
248
|
+
if (revokedAt !== undefined && revokedAt < (retiredAt ?? activatedAt)) {
|
|
249
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const hasEscrowReference = Object.hasOwn(value, 'escrowReference');
|
|
253
|
+
const hasEscrowedAt = escrowedAt !== undefined;
|
|
254
|
+
if (hasEscrowReference !== hasEscrowedAt) {
|
|
255
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
256
|
+
}
|
|
257
|
+
let escrowReference: string | undefined;
|
|
258
|
+
if (hasEscrowReference) {
|
|
259
|
+
escrowReference = requireSmartSecretIdentifier(value.escrowReference);
|
|
260
|
+
}
|
|
261
|
+
if (escrowedAt !== undefined && escrowedAt < createdAt) {
|
|
262
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
263
|
+
}
|
|
264
|
+
if (recoveryTestedAt !== undefined) {
|
|
265
|
+
if (escrowedAt === undefined || recoveryTestedAt < escrowedAt) {
|
|
266
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
267
|
+
}
|
|
268
|
+
if (revokedAt !== undefined && recoveryTestedAt > revokedAt) {
|
|
269
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let credentialPath: string | undefined;
|
|
274
|
+
let expectedFingerprint: `sha256:${string}` | undefined;
|
|
275
|
+
if (status === 'revoked') {
|
|
276
|
+
if (Object.hasOwn(value, 'credentialPath') || Object.hasOwn(value, 'expectedFingerprint')) {
|
|
277
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
if (typeof value.credentialPath !== 'string') {
|
|
281
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
282
|
+
}
|
|
283
|
+
credentialPath = value.credentialPath;
|
|
284
|
+
if (
|
|
285
|
+
!plugins.path.isAbsolute(credentialPath)
|
|
286
|
+
|| plugins.path.resolve(credentialPath) !== credentialPath
|
|
287
|
+
) {
|
|
288
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
289
|
+
}
|
|
290
|
+
expectedFingerprint = requireFingerprint(value.expectedFingerprint);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return Object.freeze({
|
|
294
|
+
version,
|
|
295
|
+
status,
|
|
296
|
+
...(credentialPath === undefined ? {} : { credentialPath }),
|
|
297
|
+
...(expectedFingerprint === undefined ? {} : { expectedFingerprint }),
|
|
298
|
+
createdAt,
|
|
299
|
+
activatedAt,
|
|
300
|
+
...(retiredAt === undefined ? {} : { retiredAt }),
|
|
301
|
+
...(revokedAt === undefined ? {} : { revokedAt }),
|
|
302
|
+
...(escrowReference === undefined ? {} : { escrowReference }),
|
|
303
|
+
...(escrowedAt === undefined ? {} : { escrowedAt }),
|
|
304
|
+
...(recoveryTestedAt === undefined ? {} : { recoveryTestedAt }),
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const normalizeConfig = (valueArg: unknown): {
|
|
309
|
+
keyringId: string;
|
|
310
|
+
descriptors: readonly INormalizedSmartSecretKekDescriptor[];
|
|
311
|
+
activeVersion: number;
|
|
312
|
+
} => {
|
|
313
|
+
const value = requireConfigObject(valueArg, ['keyringId', 'keys'], ['keyringId', 'keys']);
|
|
314
|
+
const keyringId = requireSmartSecretIdentifier(value.keyringId);
|
|
315
|
+
const descriptorValues = requireDescriptorArray(value.keys);
|
|
316
|
+
const descriptors = descriptorValues.map((descriptorArg) => normalizeDescriptor(descriptorArg));
|
|
317
|
+
if (descriptors.length === 0) {
|
|
318
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
319
|
+
}
|
|
320
|
+
for (let index = 1; index < descriptors.length; index++) {
|
|
321
|
+
if (descriptors[index - 1].version >= descriptors[index].version) {
|
|
322
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const active = descriptors.filter((descriptorArg) => descriptorArg.status === 'active');
|
|
326
|
+
if (active.length !== 1) {
|
|
327
|
+
throw createSmartSecretKeyringError('CONFIG_INVALID');
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
keyringId,
|
|
331
|
+
descriptors: Object.freeze(descriptors),
|
|
332
|
+
activeVersion: active[0].version,
|
|
333
|
+
};
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const statsMatch = (leftArg: TBigIntStats, rightArg: TBigIntStats): boolean =>
|
|
337
|
+
leftArg.dev === rightArg.dev
|
|
338
|
+
&& leftArg.ino === rightArg.ino
|
|
339
|
+
&& leftArg.mode === rightArg.mode
|
|
340
|
+
&& leftArg.nlink === rightArg.nlink
|
|
341
|
+
&& leftArg.uid === rightArg.uid
|
|
342
|
+
&& leftArg.gid === rightArg.gid
|
|
343
|
+
&& leftArg.rdev === rightArg.rdev
|
|
344
|
+
&& leftArg.size === rightArg.size
|
|
345
|
+
&& leftArg.mtimeNs === rightArg.mtimeNs
|
|
346
|
+
&& leftArg.ctimeNs === rightArg.ctimeNs;
|
|
347
|
+
|
|
348
|
+
const statsIdentityMatches = (leftArg: TBigIntStats, rightArg: TBigIntStats): boolean =>
|
|
349
|
+
leftArg.dev === rightArg.dev
|
|
350
|
+
&& leftArg.ino === rightArg.ino
|
|
351
|
+
&& leftArg.mode === rightArg.mode
|
|
352
|
+
&& leftArg.uid === rightArg.uid
|
|
353
|
+
&& leftArg.gid === rightArg.gid;
|
|
354
|
+
|
|
355
|
+
const lstatPathComponents = async (
|
|
356
|
+
credentialPathArg: string,
|
|
357
|
+
): Promise<Array<{ path: string; stats: TBigIntStats }>> => {
|
|
358
|
+
const root = plugins.path.parse(credentialPathArg).root;
|
|
359
|
+
const relative = plugins.path.relative(root, credentialPathArg);
|
|
360
|
+
const components = relative.split(plugins.path.sep).filter(Boolean);
|
|
361
|
+
const result: Array<{ path: string; stats: TBigIntStats }> = [];
|
|
362
|
+
let currentPath = root;
|
|
363
|
+
const rootStats = await plugins.fs.promises.lstat(root, { bigint: true });
|
|
364
|
+
if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) {
|
|
365
|
+
throw createSmartSecretKeyringError('CREDENTIAL_FILE_INVALID');
|
|
366
|
+
}
|
|
367
|
+
result.push({ path: root, stats: rootStats });
|
|
368
|
+
for (let index = 0; index < components.length; index++) {
|
|
369
|
+
currentPath = plugins.path.join(currentPath, components[index]);
|
|
370
|
+
const stats = await plugins.fs.promises.lstat(currentPath, { bigint: true });
|
|
371
|
+
const isFinal = index === components.length - 1;
|
|
372
|
+
if (
|
|
373
|
+
stats.isSymbolicLink()
|
|
374
|
+
|| (isFinal ? !stats.isFile() : !stats.isDirectory())
|
|
375
|
+
) {
|
|
376
|
+
throw createSmartSecretKeyringError('CREDENTIAL_FILE_INVALID');
|
|
377
|
+
}
|
|
378
|
+
result.push({ path: currentPath, stats });
|
|
379
|
+
}
|
|
380
|
+
return result;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const loadCredential = async (
|
|
384
|
+
descriptorArg: INormalizedSmartSecretKekDescriptor,
|
|
385
|
+
): Promise<Uint8Array> => {
|
|
386
|
+
let handle: plugins.fs.promises.FileHandle | undefined;
|
|
387
|
+
let key: Uint8Array | undefined;
|
|
388
|
+
let readBuffer: Buffer | undefined;
|
|
389
|
+
let didFail = false;
|
|
390
|
+
try {
|
|
391
|
+
const credentialPath = descriptorArg.credentialPath!;
|
|
392
|
+
const beforeComponents = await lstatPathComponents(credentialPath);
|
|
393
|
+
const beforeFinal = beforeComponents[beforeComponents.length - 1].stats;
|
|
394
|
+
if (typeof plugins.fs.constants.O_NOFOLLOW !== 'number') {
|
|
395
|
+
throw createSmartSecretKeyringError('PLATFORM_UNSUPPORTED');
|
|
396
|
+
}
|
|
397
|
+
handle = await plugins.fs.promises.open(
|
|
398
|
+
credentialPath,
|
|
399
|
+
plugins.fs.constants.O_RDONLY | plugins.fs.constants.O_NOFOLLOW,
|
|
400
|
+
);
|
|
401
|
+
const beforeFd = await handle.stat({ bigint: true });
|
|
402
|
+
if (
|
|
403
|
+
!beforeFd.isFile()
|
|
404
|
+
|| beforeFd.size !== 32n
|
|
405
|
+
|| (beforeFd.mode & 0o022n) !== 0n
|
|
406
|
+
|| !statsMatch(beforeFinal, beforeFd)
|
|
407
|
+
) {
|
|
408
|
+
throw createSmartSecretKeyringError('CREDENTIAL_FILE_INVALID');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
readBuffer = Buffer.alloc(33);
|
|
412
|
+
let totalBytesRead = 0;
|
|
413
|
+
while (totalBytesRead < readBuffer.byteLength) {
|
|
414
|
+
const { bytesRead } = await handle.read(
|
|
415
|
+
readBuffer,
|
|
416
|
+
totalBytesRead,
|
|
417
|
+
readBuffer.byteLength - totalBytesRead,
|
|
418
|
+
totalBytesRead,
|
|
419
|
+
);
|
|
420
|
+
if (bytesRead === 0) {
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
totalBytesRead += bytesRead;
|
|
424
|
+
}
|
|
425
|
+
if (totalBytesRead !== 32) {
|
|
426
|
+
throw createSmartSecretKeyringError('CREDENTIAL_FILE_INVALID');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const afterFd = await handle.stat({ bigint: true });
|
|
430
|
+
const afterComponents = await lstatPathComponents(credentialPath);
|
|
431
|
+
if (beforeComponents.length !== afterComponents.length) {
|
|
432
|
+
throw createSmartSecretKeyringError('CREDENTIAL_FILE_INVALID');
|
|
433
|
+
}
|
|
434
|
+
for (let index = 0; index < beforeComponents.length - 1; index++) {
|
|
435
|
+
if (!statsIdentityMatches(beforeComponents[index].stats, afterComponents[index].stats)) {
|
|
436
|
+
throw createSmartSecretKeyringError('CREDENTIAL_FILE_INVALID');
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const afterFinal = afterComponents[afterComponents.length - 1].stats;
|
|
440
|
+
if (!statsMatch(beforeFd, afterFd) || !statsMatch(afterFd, afterFinal)) {
|
|
441
|
+
throw createSmartSecretKeyringError('CREDENTIAL_FILE_INVALID');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
key = new Uint8Array(readBuffer.subarray(0, 32));
|
|
445
|
+
const actualFingerprint = sha256Bytes(key);
|
|
446
|
+
const expectedFingerprint = fingerprintToBytes(descriptorArg.expectedFingerprint!);
|
|
447
|
+
try {
|
|
448
|
+
if (!timingSafeEqual(actualFingerprint, expectedFingerprint)) {
|
|
449
|
+
throw createSmartSecretKeyringError('CREDENTIAL_FILE_INVALID');
|
|
450
|
+
}
|
|
451
|
+
} finally {
|
|
452
|
+
actualFingerprint.fill(0);
|
|
453
|
+
expectedFingerprint.fill(0);
|
|
454
|
+
}
|
|
455
|
+
} catch {
|
|
456
|
+
didFail = true;
|
|
457
|
+
} finally {
|
|
458
|
+
readBuffer?.fill(0);
|
|
459
|
+
if (handle) {
|
|
460
|
+
try {
|
|
461
|
+
await handle.close();
|
|
462
|
+
} catch {
|
|
463
|
+
didFail = true;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (didFail || !key) {
|
|
468
|
+
key?.fill(0);
|
|
469
|
+
throw createSmartSecretKeyringError('CREDENTIAL_FILE_INVALID');
|
|
470
|
+
}
|
|
471
|
+
return key;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const copyBytes = (
|
|
475
|
+
valueArg: unknown,
|
|
476
|
+
minimumBytesArg: number,
|
|
477
|
+
maximumBytesArg: number,
|
|
478
|
+
): Uint8Array => {
|
|
479
|
+
if (!(valueArg instanceof Uint8Array)) {
|
|
480
|
+
throw createSmartSecretKeyringError('INVALID_ARGUMENT');
|
|
481
|
+
}
|
|
482
|
+
if (valueArg.byteLength < minimumBytesArg || valueArg.byteLength > maximumBytesArg) {
|
|
483
|
+
throw createSmartSecretKeyringError('SIZE_LIMIT_EXCEEDED');
|
|
484
|
+
}
|
|
485
|
+
return new Uint8Array(valueArg);
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const copyIdentifier = (valueArg: unknown): string => {
|
|
489
|
+
try {
|
|
490
|
+
return requireSmartSecretIdentifier(valueArg);
|
|
491
|
+
} catch {
|
|
492
|
+
throw createSmartSecretKeyringError('INVALID_ARGUMENT');
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const encodeCiphertext = (
|
|
497
|
+
valueArg: plugins.smartcrypto.IAesGcmCiphertext,
|
|
498
|
+
): { nonce: string; ciphertext: string; tag: string } => ({
|
|
499
|
+
nonce: encodeCanonicalBase64Url(valueArg.nonce),
|
|
500
|
+
ciphertext: encodeCanonicalBase64Url(valueArg.ciphertext),
|
|
501
|
+
tag: encodeCanonicalBase64Url(valueArg.tag),
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const wipeParsedEnvelope = (valueArg: IParsedSmartSecretEnvelopeV1): void => {
|
|
505
|
+
valueArg.contextDigest.fill(0);
|
|
506
|
+
valueArg.kekFingerprint.fill(0);
|
|
507
|
+
wipeCiphertext(valueArg.payload);
|
|
508
|
+
wipeCiphertext(valueArg.wrappedDek);
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* A Linux-only, immutable KEK snapshot for strict DEK/KEK envelopes.
|
|
513
|
+
* Envelope persistence and compare-and-swap coordination remain consumer responsibilities.
|
|
514
|
+
*/
|
|
515
|
+
export class SmartSecretKeyring {
|
|
516
|
+
private destroyed = false;
|
|
517
|
+
|
|
518
|
+
private constructor(
|
|
519
|
+
private readonly keyringId: string,
|
|
520
|
+
private readonly descriptors: readonly INormalizedSmartSecretKekDescriptor[],
|
|
521
|
+
private readonly activeVersion: number,
|
|
522
|
+
private readonly loadedKeys: ReadonlyMap<number, ILoadedSmartSecretKek>,
|
|
523
|
+
) {}
|
|
524
|
+
|
|
525
|
+
public static async create(configArg: ISmartSecretKeyringConfig): Promise<SmartSecretKeyring> {
|
|
526
|
+
if (process.platform !== 'linux') {
|
|
527
|
+
throw createSmartSecretKeyringError('PLATFORM_UNSUPPORTED');
|
|
528
|
+
}
|
|
529
|
+
const config = normalizeConfig(configArg);
|
|
530
|
+
const loadedKeys = new Map<number, ILoadedSmartSecretKek>();
|
|
531
|
+
try {
|
|
532
|
+
for (const descriptor of config.descriptors) {
|
|
533
|
+
if (descriptor.status === 'revoked') {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
let key: Uint8Array | undefined;
|
|
537
|
+
try {
|
|
538
|
+
key = await loadCredential(descriptor);
|
|
539
|
+
loadedKeys.set(descriptor.version, { descriptor, key });
|
|
540
|
+
key = undefined;
|
|
541
|
+
} finally {
|
|
542
|
+
key?.fill(0);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return new SmartSecretKeyring(
|
|
546
|
+
config.keyringId,
|
|
547
|
+
config.descriptors,
|
|
548
|
+
config.activeVersion,
|
|
549
|
+
loadedKeys,
|
|
550
|
+
);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
for (const loaded of loadedKeys.values()) {
|
|
553
|
+
loaded.key.fill(0);
|
|
554
|
+
}
|
|
555
|
+
if (error instanceof SmartSecretKeyringError) {
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
throw createSmartSecretKeyringError('CREDENTIAL_FILE_INVALID');
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private requireAlive(): void {
|
|
563
|
+
if (this.destroyed) {
|
|
564
|
+
throw createSmartSecretKeyringError('DESTROYED');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private requireEnvelopeKey(
|
|
569
|
+
envelopeArg: ISmartSecretEnvelopeV1,
|
|
570
|
+
): Uint8Array {
|
|
571
|
+
if (envelopeArg.kek.keyringId !== this.keyringId) {
|
|
572
|
+
throw createSmartSecretKeyringError('KEK_UNAVAILABLE');
|
|
573
|
+
}
|
|
574
|
+
const loaded = this.loadedKeys.get(envelopeArg.kek.version);
|
|
575
|
+
if (!loaded || loaded.descriptor.status === 'revoked') {
|
|
576
|
+
throw createSmartSecretKeyringError('KEK_UNAVAILABLE');
|
|
577
|
+
}
|
|
578
|
+
if (envelopeArg.kek.fingerprint !== loaded.descriptor.expectedFingerprint) {
|
|
579
|
+
throw createSmartSecretKeyringError('KEK_FINGERPRINT_MISMATCH');
|
|
580
|
+
}
|
|
581
|
+
return new Uint8Array(loaded.key);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
public inspect(): ISmartSecretKeyringInspection {
|
|
585
|
+
this.requireAlive();
|
|
586
|
+
return {
|
|
587
|
+
keyringId: this.keyringId,
|
|
588
|
+
activeVersion: this.activeVersion,
|
|
589
|
+
keys: this.descriptors.map((descriptorArg) => ({
|
|
590
|
+
version: descriptorArg.version,
|
|
591
|
+
status: descriptorArg.status,
|
|
592
|
+
...(descriptorArg.expectedFingerprint === undefined
|
|
593
|
+
? {}
|
|
594
|
+
: { expectedFingerprint: descriptorArg.expectedFingerprint }),
|
|
595
|
+
createdAt: descriptorArg.createdAt,
|
|
596
|
+
activatedAt: descriptorArg.activatedAt,
|
|
597
|
+
...(descriptorArg.retiredAt === undefined ? {} : { retiredAt: descriptorArg.retiredAt }),
|
|
598
|
+
...(descriptorArg.revokedAt === undefined ? {} : { revokedAt: descriptorArg.revokedAt }),
|
|
599
|
+
...(descriptorArg.escrowReference === undefined
|
|
600
|
+
? {}
|
|
601
|
+
: { escrowReference: descriptorArg.escrowReference }),
|
|
602
|
+
...(descriptorArg.escrowedAt === undefined ? {} : { escrowedAt: descriptorArg.escrowedAt }),
|
|
603
|
+
...(descriptorArg.recoveryTestedAt === undefined
|
|
604
|
+
? {}
|
|
605
|
+
: { recoveryTestedAt: descriptorArg.recoveryTestedAt }),
|
|
606
|
+
})),
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
public destroy(): void {
|
|
611
|
+
if (this.destroyed) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
this.destroyed = true;
|
|
615
|
+
for (const loaded of this.loadedKeys.values()) {
|
|
616
|
+
loaded.key.fill(0);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
public async encryptEnvelope(
|
|
621
|
+
optionsArg: IEncryptSmartSecretEnvelopeOptions,
|
|
622
|
+
): Promise<ISmartSecretEnvelopeV1> {
|
|
623
|
+
this.requireAlive();
|
|
624
|
+
const options = requireConfigObject(
|
|
625
|
+
optionsArg,
|
|
626
|
+
['envelopeId', 'plaintext', 'context', 'createdAt'],
|
|
627
|
+
['envelopeId', 'plaintext', 'context'],
|
|
628
|
+
'INVALID_ARGUMENT',
|
|
629
|
+
);
|
|
630
|
+
const envelopeId = copyIdentifier(options.envelopeId);
|
|
631
|
+
let createdAt: number;
|
|
632
|
+
try {
|
|
633
|
+
createdAt = options.createdAt === undefined
|
|
634
|
+
? Date.now()
|
|
635
|
+
: requireEpochMilliseconds(options.createdAt);
|
|
636
|
+
} catch {
|
|
637
|
+
throw createSmartSecretKeyringError('INVALID_ARGUMENT');
|
|
638
|
+
}
|
|
639
|
+
let plaintext: Uint8Array | undefined;
|
|
640
|
+
let context: Uint8Array | undefined;
|
|
641
|
+
let kek: Uint8Array | undefined;
|
|
642
|
+
let kekFingerprint: Uint8Array | undefined;
|
|
643
|
+
let contextDigest: Uint8Array | undefined;
|
|
644
|
+
let dek: Uint8Array | undefined;
|
|
645
|
+
let payload: plugins.smartcrypto.IAesGcmCiphertext | undefined;
|
|
646
|
+
let wrappedDek: plugins.smartcrypto.IAesGcmCiphertext | undefined;
|
|
647
|
+
let payloadDigest: Uint8Array | undefined;
|
|
648
|
+
let wrapAad: Uint8Array | undefined;
|
|
649
|
+
try {
|
|
650
|
+
plaintext = copyBytes(options.plaintext, 0, SMARTSECRET_KEYRING_MAX_PAYLOAD_BYTES);
|
|
651
|
+
context = copyBytes(
|
|
652
|
+
options.context,
|
|
653
|
+
SMARTSECRET_KEYRING_MIN_CONTEXT_BYTES,
|
|
654
|
+
SMARTSECRET_KEYRING_MAX_CONTEXT_BYTES,
|
|
655
|
+
);
|
|
656
|
+
const active = this.loadedKeys.get(this.activeVersion)!;
|
|
657
|
+
kek = new Uint8Array(active.key);
|
|
658
|
+
kekFingerprint = fingerprintToBytes(active.descriptor.expectedFingerprint!);
|
|
659
|
+
contextDigest = sha256Bytes(context);
|
|
660
|
+
dek = plugins.smartcrypto.generateDek();
|
|
661
|
+
try {
|
|
662
|
+
payload = await plugins.smartcrypto.aesGcmEncrypt({
|
|
663
|
+
key: dek,
|
|
664
|
+
plaintext,
|
|
665
|
+
aad: context,
|
|
666
|
+
});
|
|
667
|
+
payloadDigest = createSmartSecretPayloadDigest(payload);
|
|
668
|
+
wrapAad = createSmartSecretWrapAad({
|
|
669
|
+
envelopeId,
|
|
670
|
+
createdAt,
|
|
671
|
+
contextDigest,
|
|
672
|
+
payloadDigest,
|
|
673
|
+
keyringId: this.keyringId,
|
|
674
|
+
kekVersion: active.descriptor.version,
|
|
675
|
+
kekFingerprint,
|
|
676
|
+
});
|
|
677
|
+
wrappedDek = await plugins.smartcrypto.wrapDek({ dek, kek, wrapAad });
|
|
678
|
+
} catch (error) {
|
|
679
|
+
if (error instanceof SmartSecretKeyringError) {
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
682
|
+
throw createSmartSecretKeyringError('CRYPTO_OPERATION_FAILED');
|
|
683
|
+
}
|
|
684
|
+
return {
|
|
685
|
+
schemaVersion: SMARTSECRET_KEYRING_SCHEMA_VERSION,
|
|
686
|
+
profile: SMARTSECRET_KEYRING_PROFILE,
|
|
687
|
+
envelopeId,
|
|
688
|
+
createdAt,
|
|
689
|
+
contextDigest: encodeCanonicalBase64Url(contextDigest),
|
|
690
|
+
kek: {
|
|
691
|
+
keyringId: this.keyringId,
|
|
692
|
+
version: active.descriptor.version,
|
|
693
|
+
fingerprint: active.descriptor.expectedFingerprint!,
|
|
694
|
+
},
|
|
695
|
+
payload: encodeCiphertext(payload),
|
|
696
|
+
wrappedDek: encodeCiphertext(wrappedDek),
|
|
697
|
+
};
|
|
698
|
+
} finally {
|
|
699
|
+
plaintext?.fill(0);
|
|
700
|
+
context?.fill(0);
|
|
701
|
+
contextDigest?.fill(0);
|
|
702
|
+
dek?.fill(0);
|
|
703
|
+
kek?.fill(0);
|
|
704
|
+
kekFingerprint?.fill(0);
|
|
705
|
+
payloadDigest?.fill(0);
|
|
706
|
+
wrapAad?.fill(0);
|
|
707
|
+
wipeCiphertext(payload);
|
|
708
|
+
wipeCiphertext(wrappedDek);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
public async decryptEnvelope(
|
|
713
|
+
optionsArg: IDecryptSmartSecretEnvelopeOptions,
|
|
714
|
+
): Promise<Uint8Array> {
|
|
715
|
+
this.requireAlive();
|
|
716
|
+
const options = requireConfigObject(
|
|
717
|
+
optionsArg,
|
|
718
|
+
['envelope', 'expectedEnvelopeId', 'context'],
|
|
719
|
+
['envelope', 'expectedEnvelopeId', 'context'],
|
|
720
|
+
'INVALID_ARGUMENT',
|
|
721
|
+
);
|
|
722
|
+
const expectedEnvelopeId = copyIdentifier(options.expectedEnvelopeId);
|
|
723
|
+
const parsed = parseSmartSecretEnvelope(options.envelope);
|
|
724
|
+
let context: Uint8Array | undefined;
|
|
725
|
+
let envelopeKey: Uint8Array | undefined;
|
|
726
|
+
let contextDigest: Uint8Array | undefined;
|
|
727
|
+
let payloadDigest: Uint8Array | undefined;
|
|
728
|
+
let wrapAad: Uint8Array | undefined;
|
|
729
|
+
let dek: Uint8Array | undefined;
|
|
730
|
+
try {
|
|
731
|
+
context = copyBytes(
|
|
732
|
+
options.context,
|
|
733
|
+
SMARTSECRET_KEYRING_MIN_CONTEXT_BYTES,
|
|
734
|
+
SMARTSECRET_KEYRING_MAX_CONTEXT_BYTES,
|
|
735
|
+
);
|
|
736
|
+
envelopeKey = this.requireEnvelopeKey(parsed.envelope);
|
|
737
|
+
contextDigest = sha256Bytes(context);
|
|
738
|
+
payloadDigest = createSmartSecretPayloadDigest(parsed.payload);
|
|
739
|
+
wrapAad = createSmartSecretWrapAad({
|
|
740
|
+
envelopeId: parsed.envelope.envelopeId,
|
|
741
|
+
createdAt: parsed.envelope.createdAt,
|
|
742
|
+
contextDigest: parsed.contextDigest,
|
|
743
|
+
payloadDigest,
|
|
744
|
+
keyringId: parsed.envelope.kek.keyringId,
|
|
745
|
+
kekVersion: parsed.envelope.kek.version,
|
|
746
|
+
kekFingerprint: parsed.kekFingerprint,
|
|
747
|
+
});
|
|
748
|
+
if (parsed.envelope.envelopeId !== expectedEnvelopeId) {
|
|
749
|
+
throw createSmartSecretKeyringError('ENVELOPE_ID_MISMATCH');
|
|
750
|
+
}
|
|
751
|
+
if (!timingSafeEqual(contextDigest, parsed.contextDigest)) {
|
|
752
|
+
throw createSmartSecretKeyringError('CONTEXT_MISMATCH');
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
dek = await plugins.smartcrypto.unwrapDek({
|
|
756
|
+
wrappedDek: parsed.wrappedDek,
|
|
757
|
+
kek: envelopeKey,
|
|
758
|
+
wrapAad,
|
|
759
|
+
});
|
|
760
|
+
return await plugins.smartcrypto.aesGcmDecrypt({
|
|
761
|
+
key: dek,
|
|
762
|
+
encryptedData: parsed.payload,
|
|
763
|
+
aad: context,
|
|
764
|
+
});
|
|
765
|
+
} catch {
|
|
766
|
+
throw createSmartSecretKeyringError('AUTHENTICATION_FAILED');
|
|
767
|
+
}
|
|
768
|
+
} finally {
|
|
769
|
+
context?.fill(0);
|
|
770
|
+
contextDigest?.fill(0);
|
|
771
|
+
payloadDigest?.fill(0);
|
|
772
|
+
wrapAad?.fill(0);
|
|
773
|
+
envelopeKey?.fill(0);
|
|
774
|
+
dek?.fill(0);
|
|
775
|
+
wipeParsedEnvelope(parsed);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
public async rewrapEnvelope(
|
|
780
|
+
optionsArg: IRewrapSmartSecretEnvelopeOptions,
|
|
781
|
+
): Promise<ISmartSecretEnvelopeV1> {
|
|
782
|
+
this.requireAlive();
|
|
783
|
+
const options = requireConfigObject(
|
|
784
|
+
optionsArg,
|
|
785
|
+
['envelope', 'expectedEnvelopeId', 'context', 'targetVersion'],
|
|
786
|
+
['envelope', 'expectedEnvelopeId', 'context'],
|
|
787
|
+
'INVALID_ARGUMENT',
|
|
788
|
+
);
|
|
789
|
+
const expectedEnvelopeId = copyIdentifier(options.expectedEnvelopeId);
|
|
790
|
+
const parsed = parseSmartSecretEnvelope(options.envelope);
|
|
791
|
+
let context: Uint8Array | undefined;
|
|
792
|
+
let currentKey: Uint8Array | undefined;
|
|
793
|
+
let targetKey: Uint8Array | undefined;
|
|
794
|
+
let targetFingerprint: Uint8Array | undefined;
|
|
795
|
+
let contextDigest: Uint8Array | undefined;
|
|
796
|
+
let payloadDigest: Uint8Array | undefined;
|
|
797
|
+
let currentWrapAad: Uint8Array | undefined;
|
|
798
|
+
let newWrapAad: Uint8Array | undefined;
|
|
799
|
+
let wrappedDek: plugins.smartcrypto.IAesGcmCiphertext | undefined;
|
|
800
|
+
try {
|
|
801
|
+
context = copyBytes(
|
|
802
|
+
options.context,
|
|
803
|
+
SMARTSECRET_KEYRING_MIN_CONTEXT_BYTES,
|
|
804
|
+
SMARTSECRET_KEYRING_MAX_CONTEXT_BYTES,
|
|
805
|
+
);
|
|
806
|
+
currentKey = this.requireEnvelopeKey(parsed.envelope);
|
|
807
|
+
if (options.targetVersion !== undefined) {
|
|
808
|
+
let targetVersion: number;
|
|
809
|
+
try {
|
|
810
|
+
targetVersion = requireKekVersion(options.targetVersion);
|
|
811
|
+
} catch {
|
|
812
|
+
throw createSmartSecretKeyringError('TARGET_VERSION_MISMATCH');
|
|
813
|
+
}
|
|
814
|
+
if (targetVersion !== this.activeVersion) {
|
|
815
|
+
throw createSmartSecretKeyringError('TARGET_VERSION_MISMATCH');
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const active = this.loadedKeys.get(this.activeVersion)!;
|
|
819
|
+
targetKey = new Uint8Array(active.key);
|
|
820
|
+
targetFingerprint = fingerprintToBytes(active.descriptor.expectedFingerprint!);
|
|
821
|
+
contextDigest = sha256Bytes(context);
|
|
822
|
+
payloadDigest = createSmartSecretPayloadDigest(parsed.payload);
|
|
823
|
+
currentWrapAad = createSmartSecretWrapAad({
|
|
824
|
+
envelopeId: parsed.envelope.envelopeId,
|
|
825
|
+
createdAt: parsed.envelope.createdAt,
|
|
826
|
+
contextDigest: parsed.contextDigest,
|
|
827
|
+
payloadDigest,
|
|
828
|
+
keyringId: parsed.envelope.kek.keyringId,
|
|
829
|
+
kekVersion: parsed.envelope.kek.version,
|
|
830
|
+
kekFingerprint: parsed.kekFingerprint,
|
|
831
|
+
});
|
|
832
|
+
newWrapAad = createSmartSecretWrapAad({
|
|
833
|
+
envelopeId: parsed.envelope.envelopeId,
|
|
834
|
+
createdAt: parsed.envelope.createdAt,
|
|
835
|
+
contextDigest: parsed.contextDigest,
|
|
836
|
+
payloadDigest,
|
|
837
|
+
keyringId: this.keyringId,
|
|
838
|
+
kekVersion: active.descriptor.version,
|
|
839
|
+
kekFingerprint: targetFingerprint,
|
|
840
|
+
});
|
|
841
|
+
if (parsed.envelope.envelopeId !== expectedEnvelopeId) {
|
|
842
|
+
throw createSmartSecretKeyringError('ENVELOPE_ID_MISMATCH');
|
|
843
|
+
}
|
|
844
|
+
if (!timingSafeEqual(contextDigest, parsed.contextDigest)) {
|
|
845
|
+
throw createSmartSecretKeyringError('CONTEXT_MISMATCH');
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
wrappedDek = await plugins.smartcrypto.rewrapDek({
|
|
849
|
+
wrappedDek: parsed.wrappedDek,
|
|
850
|
+
currentKek: currentKey,
|
|
851
|
+
currentWrapAad,
|
|
852
|
+
newKek: targetKey,
|
|
853
|
+
newWrapAad,
|
|
854
|
+
});
|
|
855
|
+
} catch {
|
|
856
|
+
throw createSmartSecretKeyringError('AUTHENTICATION_FAILED');
|
|
857
|
+
}
|
|
858
|
+
return {
|
|
859
|
+
schemaVersion: parsed.envelope.schemaVersion,
|
|
860
|
+
profile: parsed.envelope.profile,
|
|
861
|
+
envelopeId: parsed.envelope.envelopeId,
|
|
862
|
+
createdAt: parsed.envelope.createdAt,
|
|
863
|
+
contextDigest: parsed.envelope.contextDigest,
|
|
864
|
+
kek: {
|
|
865
|
+
keyringId: this.keyringId,
|
|
866
|
+
version: active.descriptor.version,
|
|
867
|
+
fingerprint: active.descriptor.expectedFingerprint!,
|
|
868
|
+
},
|
|
869
|
+
payload: { ...parsed.envelope.payload },
|
|
870
|
+
wrappedDek: encodeCiphertext(wrappedDek),
|
|
871
|
+
};
|
|
872
|
+
} finally {
|
|
873
|
+
context?.fill(0);
|
|
874
|
+
contextDigest?.fill(0);
|
|
875
|
+
payloadDigest?.fill(0);
|
|
876
|
+
currentWrapAad?.fill(0);
|
|
877
|
+
newWrapAad?.fill(0);
|
|
878
|
+
currentKey?.fill(0);
|
|
879
|
+
targetKey?.fill(0);
|
|
880
|
+
targetFingerprint?.fill(0);
|
|
881
|
+
wipeCiphertext(wrappedDek);
|
|
882
|
+
wipeParsedEnvelope(parsed);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
public async recoverySelfTest(versionArg: number): Promise<ISmartSecretRecoverySelfTestResult> {
|
|
887
|
+
this.requireAlive();
|
|
888
|
+
let version: number;
|
|
889
|
+
try {
|
|
890
|
+
version = requireKekVersion(versionArg);
|
|
891
|
+
} catch {
|
|
892
|
+
throw createSmartSecretKeyringError('INVALID_ARGUMENT');
|
|
893
|
+
}
|
|
894
|
+
const loaded = this.loadedKeys.get(version);
|
|
895
|
+
if (!loaded || loaded.descriptor.status === 'revoked') {
|
|
896
|
+
throw createSmartSecretKeyringError('KEK_UNAVAILABLE');
|
|
897
|
+
}
|
|
898
|
+
let key: Uint8Array | undefined;
|
|
899
|
+
let fingerprint: Uint8Array | undefined;
|
|
900
|
+
let dek: Uint8Array | undefined;
|
|
901
|
+
let keyringId: Buffer | undefined;
|
|
902
|
+
let length: Buffer | undefined;
|
|
903
|
+
let versionBytes: Buffer | undefined;
|
|
904
|
+
let aad: Buffer | undefined;
|
|
905
|
+
let wrappedDek: plugins.smartcrypto.IAesGcmCiphertext | undefined;
|
|
906
|
+
let unwrappedDek: Uint8Array | undefined;
|
|
907
|
+
try {
|
|
908
|
+
key = new Uint8Array(loaded.key);
|
|
909
|
+
fingerprint = fingerprintToBytes(loaded.descriptor.expectedFingerprint!);
|
|
910
|
+
dek = plugins.smartcrypto.generateDek();
|
|
911
|
+
keyringId = Buffer.from(this.keyringId, 'utf8');
|
|
912
|
+
length = Buffer.allocUnsafe(4);
|
|
913
|
+
length.writeUInt32BE(keyringId.byteLength, 0);
|
|
914
|
+
versionBytes = Buffer.allocUnsafe(8);
|
|
915
|
+
versionBytes.writeBigUInt64BE(BigInt(version), 0);
|
|
916
|
+
aad = Buffer.concat([
|
|
917
|
+
Buffer.from('smartsecret-keyring-recovery-aad-v1', 'ascii'),
|
|
918
|
+
length,
|
|
919
|
+
keyringId,
|
|
920
|
+
versionBytes,
|
|
921
|
+
fingerprint,
|
|
922
|
+
]);
|
|
923
|
+
try {
|
|
924
|
+
wrappedDek = await plugins.smartcrypto.wrapDek({ dek, kek: key, wrapAad: aad });
|
|
925
|
+
unwrappedDek = await plugins.smartcrypto.unwrapDek({
|
|
926
|
+
wrappedDek,
|
|
927
|
+
kek: key,
|
|
928
|
+
wrapAad: aad,
|
|
929
|
+
});
|
|
930
|
+
} catch {
|
|
931
|
+
throw createSmartSecretKeyringError('CRYPTO_OPERATION_FAILED');
|
|
932
|
+
}
|
|
933
|
+
if (!timingSafeEqual(dek, unwrappedDek)) {
|
|
934
|
+
throw createSmartSecretKeyringError('CRYPTO_OPERATION_FAILED');
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
keyringId: this.keyringId,
|
|
938
|
+
version,
|
|
939
|
+
status: loaded.descriptor.status,
|
|
940
|
+
fingerprint: loaded.descriptor.expectedFingerprint!,
|
|
941
|
+
testedAt: Date.now(),
|
|
942
|
+
};
|
|
943
|
+
} finally {
|
|
944
|
+
key?.fill(0);
|
|
945
|
+
fingerprint?.fill(0);
|
|
946
|
+
dek?.fill(0);
|
|
947
|
+
keyringId?.fill(0);
|
|
948
|
+
length?.fill(0);
|
|
949
|
+
versionBytes?.fill(0);
|
|
950
|
+
aad?.fill(0);
|
|
951
|
+
wipeCiphertext(wrappedDek);
|
|
952
|
+
unwrappedDek?.fill(0);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|