@metamask-previews/mfa-recovery-controller 0.0.0-preview-a2f6118d2
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/CHANGELOG.md +26 -0
- package/LICENSE +6 -0
- package/LICENSE.APACHE2 +201 -0
- package/LICENSE.MIT +21 -0
- package/README.md +22 -0
- package/dist/MfaRecoveryController-method-action-types.d.ts +88 -0
- package/dist/MfaRecoveryController-method-action-types.d.ts.map +1 -0
- package/dist/MfaRecoveryController-method-action-types.js +6 -0
- package/dist/MfaRecoveryController-method-action-types.js.map +1 -0
- package/dist/MfaRecoveryController.d.ts +119 -0
- package/dist/MfaRecoveryController.d.ts.map +1 -0
- package/dist/MfaRecoveryController.js +456 -0
- package/dist/MfaRecoveryController.js.map +1 -0
- package/dist/crypto.d.ts +111 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +268 -0
- package/dist/crypto.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -0
- package/dist/escrow-utils.d.ts +42 -0
- package/dist/escrow-utils.d.ts.map +1 -0
- package/dist/escrow-utils.js +77 -0
- package/dist/escrow-utils.js.map +1 -0
- package/dist/identifier-auth.d.ts +83 -0
- package/dist/identifier-auth.d.ts.map +1 -0
- package/dist/identifier-auth.js +119 -0
- package/dist/identifier-auth.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/pending-operation-validation.d.ts +21 -0
- package/dist/pending-operation-validation.d.ts.map +1 -0
- package/dist/pending-operation-validation.js +181 -0
- package/dist/pending-operation-validation.js.map +1 -0
- package/dist/state-machine.d.ts +23 -0
- package/dist/state-machine.d.ts.map +1 -0
- package/dist/state-machine.js +34 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/types.d.ts +235 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import { BaseController } from '@metamask/base-controller';
|
|
2
|
+
import { bytesToHex } from '@metamask/utils';
|
|
3
|
+
import { decodeHex, decryptFromPublic, encryptToPublic, generateSigningKey, hash, randomId, unixNow, wrapKeyId, } from './crypto.js';
|
|
4
|
+
import { IncompleteMutationError, MfaRecoveryError } from './errors.js';
|
|
5
|
+
import { isFulfilledResult, isMutationReceipt, selectHighestConsistentVersion, verifyMutationReceipt, } from './escrow-utils.js';
|
|
6
|
+
import { authorizeKeyBoundIdentifier, authorizeWithKeyBoundToken, countDistinctIdentifiers, getIdentifierAuthMode, MIN_IDENTIFIERS, requestKeyBoundIdentifierToken, } from './identifier-auth.js';
|
|
7
|
+
import { assertValidPendingOperation, pendingPayloadHasIdentifiers, pendingPayloadHasRecoverySecret, } from './pending-operation-validation.js';
|
|
8
|
+
import { assertAbortAllowed, getRecoveryPhase } from './state-machine.js';
|
|
9
|
+
const CONTROLLER_NAME = 'MfaRecoveryController';
|
|
10
|
+
const MESSENGER_EXPOSED_METHODS = [
|
|
11
|
+
'register',
|
|
12
|
+
'updateRecoverySecret',
|
|
13
|
+
'updateIdentifiers',
|
|
14
|
+
'authenticateIdentifier',
|
|
15
|
+
'getRecoverySecret',
|
|
16
|
+
'resume',
|
|
17
|
+
'abort',
|
|
18
|
+
'getPhase',
|
|
19
|
+
];
|
|
20
|
+
const mfaRecoveryControllerMetadata = {
|
|
21
|
+
pendingOperation: {
|
|
22
|
+
persist: true,
|
|
23
|
+
includeInDebugSnapshot: false,
|
|
24
|
+
includeInStateLogs: false,
|
|
25
|
+
usedInUi: true,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* @returns The default {@link MfaRecoveryController} state.
|
|
30
|
+
*/
|
|
31
|
+
export function getDefaultMfaRecoveryControllerState() {
|
|
32
|
+
return { pendingOperation: null };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Coordinates MFA recovery secret replication across independent escrows.
|
|
36
|
+
*/
|
|
37
|
+
export class MfaRecoveryController extends BaseController {
|
|
38
|
+
#authProvider;
|
|
39
|
+
#identifierAuthProvider;
|
|
40
|
+
#escrows;
|
|
41
|
+
#encryptor;
|
|
42
|
+
#now;
|
|
43
|
+
#lock = Promise.resolve();
|
|
44
|
+
constructor({ messenger, state, authProvider, identifierAuthProvider, escrows, pendingOperationEncryptor, now = unixNow, }) {
|
|
45
|
+
if (escrows.length === 0) {
|
|
46
|
+
throw new MfaRecoveryError('At least one escrow is required', 'empty_escrow_set');
|
|
47
|
+
}
|
|
48
|
+
const escrowIds = escrows.map((escrow) => escrow.id);
|
|
49
|
+
if (new Set(escrowIds).size !== escrowIds.length) {
|
|
50
|
+
throw new MfaRecoveryError('Duplicate escrow id', 'duplicate_escrow_id');
|
|
51
|
+
}
|
|
52
|
+
super({
|
|
53
|
+
messenger,
|
|
54
|
+
metadata: mfaRecoveryControllerMetadata,
|
|
55
|
+
name: CONTROLLER_NAME,
|
|
56
|
+
state: {
|
|
57
|
+
...getDefaultMfaRecoveryControllerState(),
|
|
58
|
+
...state,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
this.#authProvider = authProvider;
|
|
62
|
+
this.#identifierAuthProvider = identifierAuthProvider;
|
|
63
|
+
this.#escrows = [...escrows];
|
|
64
|
+
this.#encryptor = pendingOperationEncryptor;
|
|
65
|
+
this.#now = now;
|
|
66
|
+
this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Creates version 1 of a recovery record.
|
|
70
|
+
*
|
|
71
|
+
* @param recoverySecret - Secret replicated in full to every escrow.
|
|
72
|
+
* @param identifiers - Ownership-approved identifier set. Must contain at
|
|
73
|
+
* least two identifiers.
|
|
74
|
+
*/
|
|
75
|
+
async register(recoverySecret, identifiers) {
|
|
76
|
+
this.#assertKnownIdentifierTypes(identifiers);
|
|
77
|
+
this.#assertMinIdentifiers(identifiers);
|
|
78
|
+
await this.#mutate({
|
|
79
|
+
operation: 'register',
|
|
80
|
+
payload: {
|
|
81
|
+
identifiers,
|
|
82
|
+
recoverySecret: bytesToHex(recoverySecret),
|
|
83
|
+
},
|
|
84
|
+
identifier: null,
|
|
85
|
+
epoch: 0,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Replaces the recovery secret.
|
|
90
|
+
*
|
|
91
|
+
* @param identifier - Currently registered identifier used to authorize.
|
|
92
|
+
* @param recoverySecret - New secret.
|
|
93
|
+
* @param epoch - Current recovery version, used as `expectedVersion`.
|
|
94
|
+
*/
|
|
95
|
+
async updateRecoverySecret(identifier, recoverySecret, epoch) {
|
|
96
|
+
this.#assertKnownIdentifierTypes([identifier]);
|
|
97
|
+
await this.#mutate({
|
|
98
|
+
operation: 'updateRecoverySecret',
|
|
99
|
+
payload: {
|
|
100
|
+
recoverySecret: bytesToHex(recoverySecret),
|
|
101
|
+
},
|
|
102
|
+
identifier,
|
|
103
|
+
epoch,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Replaces the complete identifier set.
|
|
108
|
+
*
|
|
109
|
+
* @param identifier - Currently registered identifier used to authorize.
|
|
110
|
+
* @param identifiers - New identifier set. Must contain at least two
|
|
111
|
+
* identifiers.
|
|
112
|
+
* @param epoch - Current recovery version, used as `expectedVersion`.
|
|
113
|
+
*/
|
|
114
|
+
async updateIdentifiers(identifier, identifiers, epoch) {
|
|
115
|
+
this.#assertKnownIdentifierTypes([identifier, ...identifiers]);
|
|
116
|
+
this.#assertMinIdentifiers(identifiers);
|
|
117
|
+
await this.#mutate({
|
|
118
|
+
operation: 'updateIdentifiers',
|
|
119
|
+
payload: { identifiers },
|
|
120
|
+
identifier,
|
|
121
|
+
epoch,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Authenticates an identifier for a subsequent recovery-secret read.
|
|
126
|
+
*
|
|
127
|
+
* @param identifier - Identifier to authenticate.
|
|
128
|
+
* @returns Ephemeral session data required by {@link getRecoverySecret}.
|
|
129
|
+
*/
|
|
130
|
+
async authenticateIdentifier(identifier) {
|
|
131
|
+
this.#assertKnownIdentifierTypes([identifier]);
|
|
132
|
+
const requestId = randomId();
|
|
133
|
+
const ephemeral = generateSigningKey();
|
|
134
|
+
const pkE = JSON.parse(ephemeral.publicKey);
|
|
135
|
+
const requestHash = hash({
|
|
136
|
+
operation: 'getRecoverySecret',
|
|
137
|
+
requestId,
|
|
138
|
+
pkE,
|
|
139
|
+
});
|
|
140
|
+
const { token, proofPrivateKey } = await requestKeyBoundIdentifierToken({
|
|
141
|
+
identifier,
|
|
142
|
+
requestHash,
|
|
143
|
+
identifierAuthProvider: this.#identifierAuthProvider,
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
token,
|
|
147
|
+
proofPrivateKey,
|
|
148
|
+
requestId,
|
|
149
|
+
ephemeralPrivateKey: ephemeral.privateKey,
|
|
150
|
+
pkE,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Reads the recovery secret from available escrows and returns the highest
|
|
155
|
+
* consistent version. `epoch` is the current recovery version and is required
|
|
156
|
+
* by later mutations. Does not wait for a pending mutation to be repaired.
|
|
157
|
+
*
|
|
158
|
+
* @param session - Identifier authentication session used to authorize the
|
|
159
|
+
* read.
|
|
160
|
+
* @returns Recovered secret bytes and the selected epoch.
|
|
161
|
+
*/
|
|
162
|
+
async getRecoverySecret(session) {
|
|
163
|
+
return await this.#withLock(async () => {
|
|
164
|
+
const { token, requestId, pkE } = session;
|
|
165
|
+
this.#assertKnownIdentifierTypes([token.identifier]);
|
|
166
|
+
const requestHash = hash({
|
|
167
|
+
operation: 'getRecoverySecret',
|
|
168
|
+
requestId,
|
|
169
|
+
pkE,
|
|
170
|
+
});
|
|
171
|
+
if (token.requestHash !== requestHash) {
|
|
172
|
+
throw new MfaRecoveryError('Invalid identifier session', 'invalid_identifier_session');
|
|
173
|
+
}
|
|
174
|
+
const available = await this.#getAvailableEscrows();
|
|
175
|
+
if (available.length === 0) {
|
|
176
|
+
throw new MfaRecoveryError('No escrow is available', 'no_available_escrow');
|
|
177
|
+
}
|
|
178
|
+
const authorizedEscrows = await authorizeWithKeyBoundToken({
|
|
179
|
+
escrows: available,
|
|
180
|
+
token,
|
|
181
|
+
proofPrivateKey: session.proofPrivateKey,
|
|
182
|
+
});
|
|
183
|
+
const results = await Promise.allSettled(authorizedEscrows.map(async ({ escrow, authorization }) => {
|
|
184
|
+
const response = await escrow.getSecret(authorization, requestId, pkE);
|
|
185
|
+
if (response.wrapKeyId !== wrapKeyId(escrow.wrapPublicKey)) {
|
|
186
|
+
throw new MfaRecoveryError('Unexpected wrap key', 'wrap_key_mismatch');
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
escrowId: escrow.id,
|
|
190
|
+
recoverySecret: decryptFromPublic(session.ephemeralPrivateKey, escrow.wrapPublicKey, response.recoverySecret.ciphertext),
|
|
191
|
+
version: response.version,
|
|
192
|
+
lastMutationId: response.lastMutationId,
|
|
193
|
+
};
|
|
194
|
+
}));
|
|
195
|
+
const selected = selectHighestConsistentVersion(results);
|
|
196
|
+
return {
|
|
197
|
+
recoverySecret: selected.recoverySecret,
|
|
198
|
+
epoch: selected.version,
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Completes a persisted pending mutation, if any.
|
|
204
|
+
*/
|
|
205
|
+
async resume() {
|
|
206
|
+
await this.#withLock(async () => {
|
|
207
|
+
await this.#repairPendingMutation();
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Drops `authorizing` pending. `writing` must be finished with {@link resume}.
|
|
212
|
+
*/
|
|
213
|
+
async abort() {
|
|
214
|
+
await this.#withLock(async () => {
|
|
215
|
+
const pending = await this.#loadPending();
|
|
216
|
+
assertAbortAllowed(pending);
|
|
217
|
+
if (pending) {
|
|
218
|
+
await this.#clearPending();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* @returns Current recovery phase.
|
|
224
|
+
*/
|
|
225
|
+
async getPhase() {
|
|
226
|
+
return await this.#withLock(async () => getRecoveryPhase(await this.#loadPending()));
|
|
227
|
+
}
|
|
228
|
+
async #mutate(params) {
|
|
229
|
+
const { operation, payload, identifier, epoch } = params;
|
|
230
|
+
await this.#withLock(async () => {
|
|
231
|
+
if (await this.#loadPending()) {
|
|
232
|
+
throw new MfaRecoveryError('A pending mutation must be resumed or aborted first', 'pending_mutation');
|
|
233
|
+
}
|
|
234
|
+
const profileId = await this.#authProvider.getAuthenticatedProfileId();
|
|
235
|
+
const audiences = this.#escrows.map((escrow) => escrow.id);
|
|
236
|
+
const currentVersion = this.#resolveCurrentRecoveryVersion(epoch);
|
|
237
|
+
const payloadHash = hash(payload);
|
|
238
|
+
const mutationFields = {
|
|
239
|
+
id: randomId(),
|
|
240
|
+
profileId,
|
|
241
|
+
operation,
|
|
242
|
+
expectedVersion: currentVersion,
|
|
243
|
+
newVersion: currentVersion + 1,
|
|
244
|
+
payloadHash,
|
|
245
|
+
audiences,
|
|
246
|
+
};
|
|
247
|
+
const mutation = {
|
|
248
|
+
...mutationFields,
|
|
249
|
+
requestHash: hash(mutationFields),
|
|
250
|
+
};
|
|
251
|
+
const pending = {
|
|
252
|
+
phase: 'authorizing',
|
|
253
|
+
mutation,
|
|
254
|
+
payload,
|
|
255
|
+
identifier,
|
|
256
|
+
};
|
|
257
|
+
await this.#persistPending(pending);
|
|
258
|
+
const authControllerToken = await this.#authorizeMutation(mutation, payload);
|
|
259
|
+
await this.#replicateMutation({
|
|
260
|
+
...pending,
|
|
261
|
+
phase: 'writing',
|
|
262
|
+
authControllerToken,
|
|
263
|
+
receipts: [],
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
async #authorizeMutation(mutation, payload) {
|
|
268
|
+
return await this.#authProvider.authorizeRecoveryRequest({
|
|
269
|
+
requestHash: mutation.requestHash,
|
|
270
|
+
...(mutation.operation === 'register' ? {} : { requireTwoFactor: true }),
|
|
271
|
+
...(pendingPayloadHasIdentifiers(payload)
|
|
272
|
+
? { identifiers: payload.identifiers }
|
|
273
|
+
: {}),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
async #repairPendingMutation() {
|
|
277
|
+
const pending = await this.#loadPending();
|
|
278
|
+
if (!pending) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (pending.phase === 'authorizing') {
|
|
282
|
+
await this.#replicateMutation({
|
|
283
|
+
...pending,
|
|
284
|
+
phase: 'writing',
|
|
285
|
+
authControllerToken: await this.#authorizeMutation(pending.mutation, pending.payload),
|
|
286
|
+
receipts: [],
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// writing phase
|
|
291
|
+
await this.#replicateMutation(pending);
|
|
292
|
+
}
|
|
293
|
+
async #replicateMutation(pending) {
|
|
294
|
+
const { mutation, payload, identifier } = pending;
|
|
295
|
+
const remainingEscrows = this.#getRemainingEscrows(pending);
|
|
296
|
+
if (remainingEscrows.length === 0) {
|
|
297
|
+
await this.#clearPending();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const availableEscrows = await this.#getAvailableEscrows(remainingEscrows);
|
|
301
|
+
if (availableEscrows.length !== remainingEscrows.length) {
|
|
302
|
+
throw new MfaRecoveryError('All configured escrows are required for mutation', 'escrow_unavailable');
|
|
303
|
+
}
|
|
304
|
+
const authControllerToken = pending.authControllerToken.expiresAt <= this.#now()
|
|
305
|
+
? await this.#authorizeMutation(mutation, payload)
|
|
306
|
+
: pending.authControllerToken;
|
|
307
|
+
const writing = { ...pending, authControllerToken };
|
|
308
|
+
const authorizedEscrows = mutation.operation === 'register'
|
|
309
|
+
? null
|
|
310
|
+
: await this.#authorizeIdentifier({
|
|
311
|
+
escrows: remainingEscrows,
|
|
312
|
+
identifier: identifier,
|
|
313
|
+
requestHash: mutation.requestHash,
|
|
314
|
+
});
|
|
315
|
+
if (authorizedEscrows !== null &&
|
|
316
|
+
authorizedEscrows.length !== remainingEscrows.length) {
|
|
317
|
+
throw new MfaRecoveryError('Unable to authorize every escrow', 'identifier_auth_failed');
|
|
318
|
+
}
|
|
319
|
+
const payloads = remainingEscrows.map((escrow) => this.#payloadForEscrow(writing, escrow));
|
|
320
|
+
await this.#persistPending(writing);
|
|
321
|
+
const results = await Promise.allSettled(remainingEscrows.map((escrow, index) => escrow.applyMutation(mutation, writing.authControllerToken, authorizedEscrows?.[index]?.authorization ?? null, payloads[index])));
|
|
322
|
+
const { receipts, hasInvalidReceipt } = this.#collectMutationReceipts(results, remainingEscrows, mutation, writing.receipts);
|
|
323
|
+
await this.#persistPending({ ...writing, receipts });
|
|
324
|
+
if (hasInvalidReceipt) {
|
|
325
|
+
throw new MfaRecoveryError('Invalid mutation receipt', 'invalid_receipt');
|
|
326
|
+
}
|
|
327
|
+
if (new Set(receipts.map((receipt) => receipt.escrowId)).size !==
|
|
328
|
+
this.#escrows.length) {
|
|
329
|
+
throw new IncompleteMutationError(mutation.id);
|
|
330
|
+
}
|
|
331
|
+
await this.#clearPending();
|
|
332
|
+
}
|
|
333
|
+
#getRemainingEscrows(pending) {
|
|
334
|
+
const { mutation } = pending;
|
|
335
|
+
const remainingEscrows = [];
|
|
336
|
+
for (const escrow of this.#escrows) {
|
|
337
|
+
const receipt = pending.receipts.find((item) => item.escrowId === escrow.id);
|
|
338
|
+
if (receipt === undefined) {
|
|
339
|
+
remainingEscrows.push(escrow);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (!verifyMutationReceipt(receipt, mutation, escrow)) {
|
|
343
|
+
throw new MfaRecoveryError('Invalid mutation receipt', 'invalid_receipt');
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (remainingEscrows.length + pending.receipts.length !==
|
|
347
|
+
this.#escrows.length) {
|
|
348
|
+
throw new MfaRecoveryError('Receipt escrow is not configured', 'unknown_receipt_escrow');
|
|
349
|
+
}
|
|
350
|
+
return remainingEscrows;
|
|
351
|
+
}
|
|
352
|
+
#collectMutationReceipts(results, escrows, mutation, existingReceipts) {
|
|
353
|
+
const receipts = [...existingReceipts];
|
|
354
|
+
let hasInvalidReceipt = false;
|
|
355
|
+
results.forEach((result, index) => {
|
|
356
|
+
if (!isFulfilledResult(result)) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const escrow = escrows[index];
|
|
360
|
+
if (!isMutationReceipt(result.value) ||
|
|
361
|
+
!verifyMutationReceipt(result.value, mutation, escrow)) {
|
|
362
|
+
hasInvalidReceipt = true;
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
receipts.push(result.value);
|
|
366
|
+
});
|
|
367
|
+
return { receipts, hasInvalidReceipt };
|
|
368
|
+
}
|
|
369
|
+
async #authorizeIdentifier({ escrows, identifier, requestHash, }) {
|
|
370
|
+
getIdentifierAuthMode(identifier.type);
|
|
371
|
+
return await authorizeKeyBoundIdentifier({
|
|
372
|
+
escrows,
|
|
373
|
+
identifier,
|
|
374
|
+
requestHash,
|
|
375
|
+
identifierAuthProvider: this.#identifierAuthProvider,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
#payloadForEscrow(pending, escrow) {
|
|
379
|
+
const { payload } = pending;
|
|
380
|
+
if (!pendingPayloadHasRecoverySecret(payload)) {
|
|
381
|
+
return {
|
|
382
|
+
identifiers: payload.identifiers,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
const recoverySecret = this.#wrapRecoverySecret(decodeHex(payload.recoverySecret), escrow.wrapPublicKey);
|
|
386
|
+
if (pendingPayloadHasIdentifiers(payload)) {
|
|
387
|
+
return {
|
|
388
|
+
identifiers: payload.identifiers,
|
|
389
|
+
recoverySecret,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return { recoverySecret };
|
|
393
|
+
}
|
|
394
|
+
#resolveCurrentRecoveryVersion(epoch) {
|
|
395
|
+
if (!Number.isInteger(epoch) || epoch < 0) {
|
|
396
|
+
throw new MfaRecoveryError('Invalid epoch', 'invalid_epoch');
|
|
397
|
+
}
|
|
398
|
+
return epoch;
|
|
399
|
+
}
|
|
400
|
+
async #getAvailableEscrows(escrows = this.#escrows) {
|
|
401
|
+
const flags = await Promise.allSettled(escrows.map(async (escrow) => ({
|
|
402
|
+
escrow,
|
|
403
|
+
available: await escrow.isAvailable(),
|
|
404
|
+
})));
|
|
405
|
+
return flags.flatMap((result) => isFulfilledResult(result) && result.value.available
|
|
406
|
+
? [result.value.escrow]
|
|
407
|
+
: []);
|
|
408
|
+
}
|
|
409
|
+
#assertKnownIdentifierTypes(identifiers) {
|
|
410
|
+
for (const identifier of identifiers) {
|
|
411
|
+
getIdentifierAuthMode(identifier.type);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
#assertMinIdentifiers(identifiers) {
|
|
415
|
+
if (countDistinctIdentifiers(identifiers) < MIN_IDENTIFIERS) {
|
|
416
|
+
throw new MfaRecoveryError(`Requires at least ${MIN_IDENTIFIERS} identifiers`, 'too_few_identifiers');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
#wrapRecoverySecret(plaintext, wrapPublicKey) {
|
|
420
|
+
const ephemeral = generateSigningKey();
|
|
421
|
+
return {
|
|
422
|
+
pkE: JSON.parse(ephemeral.publicKey),
|
|
423
|
+
ciphertext: encryptToPublic(ephemeral.privateKey, wrapPublicKey, plaintext),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
async #loadPending() {
|
|
427
|
+
const { pendingOperation } = this.state;
|
|
428
|
+
if (pendingOperation === null) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const pending = (await this.#encryptor.decrypt(pendingOperation));
|
|
432
|
+
await this.#assertPending(pending);
|
|
433
|
+
return pending;
|
|
434
|
+
}
|
|
435
|
+
async #persistPending(operation) {
|
|
436
|
+
await this.#assertPending(operation);
|
|
437
|
+
const encrypted = await this.#encryptor.encrypt(operation);
|
|
438
|
+
this.update((state) => {
|
|
439
|
+
state.pendingOperation = encrypted;
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
async #assertPending(pending) {
|
|
443
|
+
await assertValidPendingOperation(pending, this.#escrows.map((escrow) => escrow.id));
|
|
444
|
+
}
|
|
445
|
+
async #clearPending() {
|
|
446
|
+
this.update((state) => {
|
|
447
|
+
state.pendingOperation = null;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
async #withLock(fn) {
|
|
451
|
+
const run = this.#lock.then(fn, fn);
|
|
452
|
+
this.#lock = run.then(() => undefined, () => undefined);
|
|
453
|
+
return await run;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
//# sourceMappingURL=MfaRecoveryController.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MfaRecoveryController.js","sourceRoot":"","sources":["../src/MfaRecoveryController.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangedEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\nimport { bytesToHex } from '@metamask/utils';\n\nimport {\n decodeHex,\n decryptFromPublic,\n encryptToPublic,\n generateSigningKey,\n hash,\n randomId,\n unixNow,\n wrapKeyId,\n} from './crypto.js';\nimport { IncompleteMutationError, MfaRecoveryError } from './errors.js';\nimport {\n isFulfilledResult,\n isMutationReceipt,\n selectHighestConsistentVersion,\n verifyMutationReceipt,\n} from './escrow-utils.js';\nimport {\n authorizeKeyBoundIdentifier,\n authorizeWithKeyBoundToken,\n countDistinctIdentifiers,\n getIdentifierAuthMode,\n MIN_IDENTIFIERS,\n requestKeyBoundIdentifierToken,\n} from './identifier-auth.js';\nimport type { AuthorizedEscrow } from './identifier-auth.js';\nimport type { MfaRecoveryControllerMethodActions } from './MfaRecoveryController-method-action-types.js';\nimport {\n assertValidPendingOperation,\n pendingPayloadHasIdentifiers,\n pendingPayloadHasRecoverySecret,\n} from './pending-operation-validation.js';\nimport { assertAbortAllowed, getRecoveryPhase } from './state-machine.js';\nimport type {\n AuthControllerToken,\n EcPublicJwk,\n Identifier,\n IdentifierSession,\n Mutation,\n MutationPayload,\n MutationReceipt,\n PendingMutationPayload,\n PendingOperation,\n PendingOperationEncryptor,\n PendingRegisterPayload,\n PendingUpdateIdentifiersPayload,\n PendingUpdateRecoverySecretPayload,\n RecoveryAuthProvider,\n RecoveryEscrowProvider,\n RecoveryIdentifierAuthProvider,\n RecoveryPhase,\n RecoveredSecret,\n WrappedSecret,\n WritingPendingOperation,\n} from './types.js';\n\nconst CONTROLLER_NAME = 'MfaRecoveryController';\n\nconst MESSENGER_EXPOSED_METHODS = [\n 'register',\n 'updateRecoverySecret',\n 'updateIdentifiers',\n 'authenticateIdentifier',\n 'getRecoverySecret',\n 'resume',\n 'abort',\n 'getPhase',\n] as const;\n\nexport type MfaRecoveryControllerState = {\n /**\n * Encrypted pending mutation, or `null` when idle.\n */\n pendingOperation: string | null;\n};\n\nconst mfaRecoveryControllerMetadata = {\n pendingOperation: {\n persist: true,\n includeInDebugSnapshot: false,\n includeInStateLogs: false,\n usedInUi: true,\n },\n} satisfies StateMetadata<MfaRecoveryControllerState>;\n\n/**\n * @returns The default {@link MfaRecoveryController} state.\n */\nexport function getDefaultMfaRecoveryControllerState(): MfaRecoveryControllerState {\n return { pendingOperation: null };\n}\n\nexport type MfaRecoveryControllerGetStateAction = ControllerGetStateAction<\n typeof CONTROLLER_NAME,\n MfaRecoveryControllerState\n>;\n\nexport type MfaRecoveryControllerActions =\n | MfaRecoveryControllerGetStateAction\n | MfaRecoveryControllerMethodActions;\n\ntype AllowedActions = never;\n\nexport type MfaRecoveryControllerStateChangedEvent =\n ControllerStateChangedEvent<\n typeof CONTROLLER_NAME,\n MfaRecoveryControllerState\n >;\n\nexport type MfaRecoveryControllerEvents =\n MfaRecoveryControllerStateChangedEvent;\n\ntype AllowedEvents = never;\n\nexport type MfaRecoveryControllerMessenger = Messenger<\n typeof CONTROLLER_NAME,\n MfaRecoveryControllerActions | AllowedActions,\n MfaRecoveryControllerEvents | AllowedEvents\n>;\n\nexport type MfaRecoveryControllerOptions = {\n /**\n * The messenger suited for this controller.\n */\n messenger: MfaRecoveryControllerMessenger;\n\n /**\n * The desired state with which to initialize this controller. Missing\n * properties will be filled in with defaults.\n */\n state?: Partial<MfaRecoveryControllerState>;\n\n /**\n * Authenticates the MetaMask profile and issues request-bound attestations.\n */\n authProvider: RecoveryAuthProvider;\n\n /**\n * Obtains key-bound identifier tokens from an IdP.\n */\n identifierAuthProvider: RecoveryIdentifierAuthProvider;\n\n /**\n * Ordered, non-empty set of recovery escrow replicas. This is build-time\n * configuration; public methods cannot override it.\n */\n escrows: RecoveryEscrowProvider[];\n\n /**\n * Encrypts pending mutation state using wallet secure storage.\n */\n pendingOperationEncryptor: PendingOperationEncryptor;\n\n /**\n * Clock used for AuthController token expiry checks. Unix seconds.\n * Defaults to {@link unixNow}.\n */\n now?: () => number;\n};\n\ntype MutateParams = {\n epoch: number;\n} & (\n | {\n operation: 'register';\n payload: PendingRegisterPayload;\n identifier: null;\n }\n | {\n operation: 'updateRecoverySecret';\n payload: PendingUpdateRecoverySecretPayload;\n identifier: Identifier;\n }\n | {\n operation: 'updateIdentifiers';\n payload: PendingUpdateIdentifiersPayload;\n identifier: Identifier;\n }\n);\n\n/**\n * Coordinates MFA recovery secret replication across independent escrows.\n */\nexport class MfaRecoveryController extends BaseController<\n typeof CONTROLLER_NAME,\n MfaRecoveryControllerState,\n MfaRecoveryControllerMessenger\n> {\n readonly #authProvider: RecoveryAuthProvider;\n\n readonly #identifierAuthProvider: RecoveryIdentifierAuthProvider;\n\n readonly #escrows: RecoveryEscrowProvider[];\n\n readonly #encryptor: PendingOperationEncryptor;\n\n readonly #now: () => number;\n\n #lock: Promise<void> = Promise.resolve();\n\n constructor({\n messenger,\n state,\n authProvider,\n identifierAuthProvider,\n escrows,\n pendingOperationEncryptor,\n now = unixNow,\n }: MfaRecoveryControllerOptions) {\n if (escrows.length === 0) {\n throw new MfaRecoveryError(\n 'At least one escrow is required',\n 'empty_escrow_set',\n );\n }\n const escrowIds = escrows.map((escrow) => escrow.id);\n if (new Set(escrowIds).size !== escrowIds.length) {\n throw new MfaRecoveryError('Duplicate escrow id', 'duplicate_escrow_id');\n }\n\n super({\n messenger,\n metadata: mfaRecoveryControllerMetadata,\n name: CONTROLLER_NAME,\n state: {\n ...getDefaultMfaRecoveryControllerState(),\n ...state,\n },\n });\n\n this.#authProvider = authProvider;\n this.#identifierAuthProvider = identifierAuthProvider;\n this.#escrows = [...escrows];\n this.#encryptor = pendingOperationEncryptor;\n this.#now = now;\n\n this.messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n }\n\n /**\n * Creates version 1 of a recovery record.\n *\n * @param recoverySecret - Secret replicated in full to every escrow.\n * @param identifiers - Ownership-approved identifier set. Must contain at\n * least two identifiers.\n */\n async register(\n recoverySecret: Uint8Array,\n identifiers: Identifier[],\n ): Promise<void> {\n this.#assertKnownIdentifierTypes(identifiers);\n this.#assertMinIdentifiers(identifiers);\n await this.#mutate({\n operation: 'register',\n payload: {\n identifiers,\n recoverySecret: bytesToHex(recoverySecret),\n },\n identifier: null,\n epoch: 0,\n });\n }\n\n /**\n * Replaces the recovery secret.\n *\n * @param identifier - Currently registered identifier used to authorize.\n * @param recoverySecret - New secret.\n * @param epoch - Current recovery version, used as `expectedVersion`.\n */\n async updateRecoverySecret(\n identifier: Identifier,\n recoverySecret: Uint8Array,\n epoch: number,\n ): Promise<void> {\n this.#assertKnownIdentifierTypes([identifier]);\n await this.#mutate({\n operation: 'updateRecoverySecret',\n payload: {\n recoverySecret: bytesToHex(recoverySecret),\n },\n identifier,\n epoch,\n });\n }\n\n /**\n * Replaces the complete identifier set.\n *\n * @param identifier - Currently registered identifier used to authorize.\n * @param identifiers - New identifier set. Must contain at least two\n * identifiers.\n * @param epoch - Current recovery version, used as `expectedVersion`.\n */\n async updateIdentifiers(\n identifier: Identifier,\n identifiers: Identifier[],\n epoch: number,\n ): Promise<void> {\n this.#assertKnownIdentifierTypes([identifier, ...identifiers]);\n this.#assertMinIdentifiers(identifiers);\n await this.#mutate({\n operation: 'updateIdentifiers',\n payload: { identifiers },\n identifier,\n epoch,\n });\n }\n\n /**\n * Authenticates an identifier for a subsequent recovery-secret read.\n *\n * @param identifier - Identifier to authenticate.\n * @returns Ephemeral session data required by {@link getRecoverySecret}.\n */\n async authenticateIdentifier(\n identifier: Identifier,\n ): Promise<IdentifierSession> {\n this.#assertKnownIdentifierTypes([identifier]);\n const requestId = randomId();\n const ephemeral = generateSigningKey();\n const pkE = JSON.parse(ephemeral.publicKey) as EcPublicJwk;\n const requestHash = hash({\n operation: 'getRecoverySecret',\n requestId,\n pkE,\n });\n const { token, proofPrivateKey } =\n await requestKeyBoundIdentifierToken({\n identifier,\n requestHash,\n identifierAuthProvider: this.#identifierAuthProvider,\n });\n return {\n token,\n proofPrivateKey,\n requestId,\n ephemeralPrivateKey: ephemeral.privateKey,\n pkE,\n };\n }\n\n /**\n * Reads the recovery secret from available escrows and returns the highest\n * consistent version. `epoch` is the current recovery version and is required\n * by later mutations. Does not wait for a pending mutation to be repaired.\n *\n * @param session - Identifier authentication session used to authorize the\n * read.\n * @returns Recovered secret bytes and the selected epoch.\n */\n async getRecoverySecret(\n session: IdentifierSession,\n ): Promise<RecoveredSecret> {\n return await this.#withLock(async () => {\n const { token, requestId, pkE } = session;\n this.#assertKnownIdentifierTypes([token.identifier]);\n const requestHash = hash({\n operation: 'getRecoverySecret',\n requestId,\n pkE,\n });\n if (token.requestHash !== requestHash) {\n throw new MfaRecoveryError(\n 'Invalid identifier session',\n 'invalid_identifier_session',\n );\n }\n const available = await this.#getAvailableEscrows();\n if (available.length === 0) {\n throw new MfaRecoveryError(\n 'No escrow is available',\n 'no_available_escrow',\n );\n }\n const authorizedEscrows = await authorizeWithKeyBoundToken({\n escrows: available,\n token,\n proofPrivateKey: session.proofPrivateKey,\n });\n const results = await Promise.allSettled(\n authorizedEscrows.map(async ({ escrow, authorization }) => {\n const response = await escrow.getSecret(\n authorization,\n requestId,\n pkE,\n );\n if (response.wrapKeyId !== wrapKeyId(escrow.wrapPublicKey)) {\n throw new MfaRecoveryError(\n 'Unexpected wrap key',\n 'wrap_key_mismatch',\n );\n }\n return {\n escrowId: escrow.id,\n recoverySecret: decryptFromPublic(\n session.ephemeralPrivateKey,\n escrow.wrapPublicKey,\n response.recoverySecret.ciphertext,\n ),\n version: response.version,\n lastMutationId: response.lastMutationId,\n };\n }),\n );\n const selected = selectHighestConsistentVersion(results);\n return {\n recoverySecret: selected.recoverySecret,\n epoch: selected.version,\n };\n });\n }\n\n /**\n * Completes a persisted pending mutation, if any.\n */\n async resume(): Promise<void> {\n await this.#withLock(async () => {\n await this.#repairPendingMutation();\n });\n }\n\n /**\n * Drops `authorizing` pending. `writing` must be finished with {@link resume}.\n */\n async abort(): Promise<void> {\n await this.#withLock(async () => {\n const pending = await this.#loadPending();\n assertAbortAllowed(pending);\n if (pending) {\n await this.#clearPending();\n }\n });\n }\n\n /**\n * @returns Current recovery phase.\n */\n async getPhase(): Promise<RecoveryPhase> {\n return await this.#withLock(async () =>\n getRecoveryPhase(await this.#loadPending()),\n );\n }\n\n async #mutate(params: MutateParams): Promise<void> {\n const { operation, payload, identifier, epoch } = params;\n await this.#withLock(async () => {\n if (await this.#loadPending()) {\n throw new MfaRecoveryError(\n 'A pending mutation must be resumed or aborted first',\n 'pending_mutation',\n );\n }\n\n const profileId = await this.#authProvider.getAuthenticatedProfileId();\n const audiences = this.#escrows.map((escrow) => escrow.id);\n const currentVersion = this.#resolveCurrentRecoveryVersion(epoch);\n const payloadHash = hash(payload);\n\n const mutationFields = {\n id: randomId(),\n profileId,\n operation,\n expectedVersion: currentVersion,\n newVersion: currentVersion + 1,\n payloadHash,\n audiences,\n };\n const mutation: Mutation = {\n ...mutationFields,\n requestHash: hash(mutationFields),\n };\n\n const pending: PendingOperation = {\n phase: 'authorizing',\n mutation,\n payload,\n identifier,\n };\n await this.#persistPending(pending);\n\n const authControllerToken = await this.#authorizeMutation(\n mutation,\n payload,\n );\n await this.#replicateMutation({\n ...pending,\n phase: 'writing',\n authControllerToken,\n receipts: [],\n });\n });\n }\n\n async #authorizeMutation(\n mutation: Mutation,\n payload: PendingMutationPayload,\n ): Promise<AuthControllerToken> {\n return await this.#authProvider.authorizeRecoveryRequest({\n requestHash: mutation.requestHash,\n ...(mutation.operation === 'register' ? {} : { requireTwoFactor: true }),\n ...(pendingPayloadHasIdentifiers(payload)\n ? { identifiers: payload.identifiers }\n : {}),\n });\n }\n\n async #repairPendingMutation(): Promise<void> {\n const pending = await this.#loadPending();\n if (!pending) {\n return;\n }\n\n if (pending.phase === 'authorizing') {\n await this.#replicateMutation({\n ...pending,\n phase: 'writing',\n authControllerToken: await this.#authorizeMutation(\n pending.mutation,\n pending.payload,\n ),\n receipts: [],\n });\n return;\n }\n\n // writing phase\n await this.#replicateMutation(pending);\n }\n\n async #replicateMutation(pending: WritingPendingOperation): Promise<void> {\n const { mutation, payload, identifier } = pending;\n const remainingEscrows = this.#getRemainingEscrows(pending);\n if (remainingEscrows.length === 0) {\n await this.#clearPending();\n return;\n }\n\n const availableEscrows = await this.#getAvailableEscrows(remainingEscrows);\n if (availableEscrows.length !== remainingEscrows.length) {\n throw new MfaRecoveryError(\n 'All configured escrows are required for mutation',\n 'escrow_unavailable',\n );\n }\n\n const authControllerToken =\n pending.authControllerToken.expiresAt <= this.#now()\n ? await this.#authorizeMutation(mutation, payload)\n : pending.authControllerToken;\n const writing = { ...pending, authControllerToken };\n const authorizedEscrows =\n mutation.operation === 'register'\n ? null\n : await this.#authorizeIdentifier({\n escrows: remainingEscrows,\n identifier: identifier as Identifier,\n requestHash: mutation.requestHash,\n });\n if (\n authorizedEscrows !== null &&\n authorizedEscrows.length !== remainingEscrows.length\n ) {\n throw new MfaRecoveryError(\n 'Unable to authorize every escrow',\n 'identifier_auth_failed',\n );\n }\n const payloads = remainingEscrows.map((escrow) =>\n this.#payloadForEscrow(writing, escrow),\n );\n\n await this.#persistPending(writing);\n const results = await Promise.allSettled(\n remainingEscrows.map((escrow, index) =>\n escrow.applyMutation(\n mutation,\n writing.authControllerToken,\n authorizedEscrows?.[index]?.authorization ?? null,\n payloads[index],\n ),\n ),\n );\n\n const { receipts, hasInvalidReceipt } = this.#collectMutationReceipts(\n results,\n remainingEscrows,\n mutation,\n writing.receipts,\n );\n await this.#persistPending({ ...writing, receipts });\n\n if (hasInvalidReceipt) {\n throw new MfaRecoveryError('Invalid mutation receipt', 'invalid_receipt');\n }\n\n if (\n new Set(receipts.map((receipt) => receipt.escrowId)).size !==\n this.#escrows.length\n ) {\n throw new IncompleteMutationError(mutation.id);\n }\n await this.#clearPending();\n }\n\n #getRemainingEscrows(\n pending: WritingPendingOperation,\n ): RecoveryEscrowProvider[] {\n const { mutation } = pending;\n const remainingEscrows: RecoveryEscrowProvider[] = [];\n for (const escrow of this.#escrows) {\n const receipt = pending.receipts.find(\n (item) => item.escrowId === escrow.id,\n );\n if (receipt === undefined) {\n remainingEscrows.push(escrow);\n continue;\n }\n if (!verifyMutationReceipt(receipt, mutation, escrow)) {\n throw new MfaRecoveryError(\n 'Invalid mutation receipt',\n 'invalid_receipt',\n );\n }\n }\n if (\n remainingEscrows.length + pending.receipts.length !==\n this.#escrows.length\n ) {\n throw new MfaRecoveryError(\n 'Receipt escrow is not configured',\n 'unknown_receipt_escrow',\n );\n }\n return remainingEscrows;\n }\n\n #collectMutationReceipts(\n results: PromiseSettledResult<MutationReceipt>[],\n escrows: RecoveryEscrowProvider[],\n mutation: Mutation,\n existingReceipts: MutationReceipt[],\n ): { receipts: MutationReceipt[]; hasInvalidReceipt: boolean } {\n const receipts = [...existingReceipts];\n let hasInvalidReceipt = false;\n results.forEach((result, index) => {\n if (!isFulfilledResult(result)) {\n return;\n }\n const escrow = escrows[index];\n if (\n !isMutationReceipt(result.value) ||\n !verifyMutationReceipt(result.value, mutation, escrow)\n ) {\n hasInvalidReceipt = true;\n return;\n }\n receipts.push(result.value);\n });\n return { receipts, hasInvalidReceipt };\n }\n\n async #authorizeIdentifier({\n escrows,\n identifier,\n requestHash,\n }: {\n escrows: RecoveryEscrowProvider[];\n identifier: Identifier;\n requestHash: string;\n }): Promise<AuthorizedEscrow[]> {\n getIdentifierAuthMode(identifier.type);\n return await authorizeKeyBoundIdentifier({\n escrows,\n identifier,\n requestHash,\n identifierAuthProvider: this.#identifierAuthProvider,\n });\n }\n\n #payloadForEscrow(\n pending: WritingPendingOperation,\n escrow: RecoveryEscrowProvider,\n ): MutationPayload {\n const { payload } = pending;\n if (!pendingPayloadHasRecoverySecret(payload)) {\n return {\n identifiers: payload.identifiers,\n };\n }\n const recoverySecret = this.#wrapRecoverySecret(\n decodeHex(payload.recoverySecret),\n escrow.wrapPublicKey,\n );\n if (pendingPayloadHasIdentifiers(payload)) {\n return {\n identifiers: payload.identifiers,\n recoverySecret,\n };\n }\n return { recoverySecret };\n }\n\n #resolveCurrentRecoveryVersion(epoch: number): number {\n if (!Number.isInteger(epoch) || epoch < 0) {\n throw new MfaRecoveryError('Invalid epoch', 'invalid_epoch');\n }\n return epoch;\n }\n\n async #getAvailableEscrows(\n escrows: RecoveryEscrowProvider[] = this.#escrows,\n ): Promise<RecoveryEscrowProvider[]> {\n const flags = await Promise.allSettled(\n escrows.map(async (escrow) => ({\n escrow,\n available: await escrow.isAvailable(),\n })),\n );\n return flags.flatMap((result) =>\n isFulfilledResult(result) && result.value.available\n ? [result.value.escrow]\n : [],\n );\n }\n\n #assertKnownIdentifierTypes(identifiers: Identifier[]): void {\n for (const identifier of identifiers) {\n getIdentifierAuthMode(identifier.type);\n }\n }\n\n #assertMinIdentifiers(identifiers: Identifier[]): void {\n if (countDistinctIdentifiers(identifiers) < MIN_IDENTIFIERS) {\n throw new MfaRecoveryError(\n `Requires at least ${MIN_IDENTIFIERS} identifiers`,\n 'too_few_identifiers',\n );\n }\n }\n\n #wrapRecoverySecret(\n plaintext: Uint8Array,\n wrapPublicKey: string,\n ): WrappedSecret {\n const ephemeral = generateSigningKey();\n return {\n pkE: JSON.parse(ephemeral.publicKey) as EcPublicJwk,\n ciphertext: encryptToPublic(\n ephemeral.privateKey,\n wrapPublicKey,\n plaintext,\n ),\n };\n }\n\n async #loadPending(): Promise<PendingOperation | null> {\n const { pendingOperation } = this.state;\n if (pendingOperation === null) {\n return null;\n }\n const pending = (await this.#encryptor.decrypt(\n pendingOperation,\n )) as unknown;\n await this.#assertPending(pending);\n return pending as PendingOperation;\n }\n\n async #persistPending(operation: PendingOperation): Promise<void> {\n await this.#assertPending(operation);\n const encrypted = await this.#encryptor.encrypt(operation);\n this.update((state) => {\n state.pendingOperation = encrypted;\n });\n }\n\n async #assertPending(pending: unknown): Promise<void> {\n await assertValidPendingOperation(\n pending,\n this.#escrows.map((escrow) => escrow.id),\n );\n }\n\n async #clearPending(): Promise<void> {\n this.update((state) => {\n state.pendingOperation = null;\n });\n }\n\n async #withLock<ReturnValue>(\n fn: () => Promise<ReturnValue>,\n ): Promise<ReturnValue> {\n const run = this.#lock.then(fn, fn);\n this.#lock = run.then(\n () => undefined,\n () => undefined,\n );\n return await run;\n }\n}\n"]}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Hex } from '@metamask/utils';
|
|
2
|
+
import type { Identifier } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Recursively sorts object keys so hashes are independent of property order.
|
|
5
|
+
*
|
|
6
|
+
* @param value - JSON-compatible value.
|
|
7
|
+
* @returns A canonical JSON string.
|
|
8
|
+
*/
|
|
9
|
+
export declare function canonicalize(value: unknown): string;
|
|
10
|
+
/**
|
|
11
|
+
* SHA-256 of the canonical JSON form of `value`, as a 0x-prefixed hex string.
|
|
12
|
+
*
|
|
13
|
+
* @param value - Value to hash.
|
|
14
|
+
* @returns Hex digest.
|
|
15
|
+
*/
|
|
16
|
+
export declare function hash(value: unknown): Hex;
|
|
17
|
+
/**
|
|
18
|
+
* Canonical identifier list bound into AuthController `identifiersHash`.
|
|
19
|
+
*
|
|
20
|
+
* @param identifiers - Identifier set.
|
|
21
|
+
* @returns Sorted identifier records including verifier material.
|
|
22
|
+
*/
|
|
23
|
+
export declare function canonicalizeIdentifiers(identifiers: Identifier[]): unknown;
|
|
24
|
+
/**
|
|
25
|
+
* @returns A random 0x-prefixed id.
|
|
26
|
+
*/
|
|
27
|
+
export declare function randomId(): Hex;
|
|
28
|
+
/**
|
|
29
|
+
* Unix time in seconds. AuthController tokens and PoP challenges use this unit.
|
|
30
|
+
*
|
|
31
|
+
* @returns Seconds since the Unix epoch.
|
|
32
|
+
*/
|
|
33
|
+
export declare function unixNow(): number;
|
|
34
|
+
/**
|
|
35
|
+
* Generates an ephemeral ECDSA P-256 proof key pair for key-bound identifier PoP.
|
|
36
|
+
*
|
|
37
|
+
* @returns JWK-encoded public and private keys.
|
|
38
|
+
*/
|
|
39
|
+
export declare function generateSigningKey(): {
|
|
40
|
+
publicKey: string;
|
|
41
|
+
privateKey: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Signs a message with the ephemeral proof private key.
|
|
45
|
+
*
|
|
46
|
+
* @param privateKey - JWK JSON private key.
|
|
47
|
+
* @param message - Message string; hashed with SHA-256 before ECDSA.
|
|
48
|
+
* @returns Compact IEEE P1363 hex signature (`r‖s`).
|
|
49
|
+
*/
|
|
50
|
+
export declare function sign(privateKey: string, message: string): Hex;
|
|
51
|
+
/**
|
|
52
|
+
* Verifies a P-256 signature against a public key.
|
|
53
|
+
*
|
|
54
|
+
* @param publicKey - JWK JSON public key.
|
|
55
|
+
* @param signature - Compact IEEE P1363 hex signature.
|
|
56
|
+
* @param message - Message string; hashed with SHA-256 before ECDSA.
|
|
57
|
+
* @returns Whether the signature is valid.
|
|
58
|
+
*/
|
|
59
|
+
export declare function verifySignature(publicKey: string, signature: string, message: string): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Unsigned mutation-receipt digest. SHA-256 of canonical JSON of the receipt
|
|
62
|
+
* fields excluding `signature`.
|
|
63
|
+
*
|
|
64
|
+
* @param receipt - Receipt fields covered by the signature.
|
|
65
|
+
* @param receipt.escrowId - Escrow that issued the receipt.
|
|
66
|
+
* @param receipt.mutationId - Mutation id.
|
|
67
|
+
* @param receipt.receiptKeyId - SHA-256 of the uncompressed receipt public key.
|
|
68
|
+
* @param receipt.requestHash - Mutation request hash.
|
|
69
|
+
* @param receipt.version - Applied version.
|
|
70
|
+
* @returns Hex digest.
|
|
71
|
+
*/
|
|
72
|
+
export declare function hashMutationReceipt(receipt: {
|
|
73
|
+
escrowId: string;
|
|
74
|
+
mutationId: string;
|
|
75
|
+
receiptKeyId: string;
|
|
76
|
+
requestHash: string;
|
|
77
|
+
version: number;
|
|
78
|
+
}): Hex;
|
|
79
|
+
/**
|
|
80
|
+
* Decodes hex that may or may not have a `0x` prefix.
|
|
81
|
+
*
|
|
82
|
+
* @param hexString - Hex string.
|
|
83
|
+
* @returns Raw bytes.
|
|
84
|
+
*/
|
|
85
|
+
export declare function decodeHex(hexString: string): Uint8Array;
|
|
86
|
+
/**
|
|
87
|
+
* SHA-256 of the uncompressed P-256 wrap public key, as `0x` hex.
|
|
88
|
+
*
|
|
89
|
+
* @param wrapPublicKey - Wrap public JWK JSON.
|
|
90
|
+
* @returns Key id matching cubist `wrapKeyId`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function wrapKeyId(wrapPublicKey: string): Hex;
|
|
93
|
+
/**
|
|
94
|
+
* Encrypts plaintext to a P-256 public key using escrow-wrap-v1.
|
|
95
|
+
*
|
|
96
|
+
* @param privateKey - Sender P-256 private JWK JSON.
|
|
97
|
+
* @param publicKey - Recipient P-256 public JWK JSON.
|
|
98
|
+
* @param plaintext - Bytes to encrypt.
|
|
99
|
+
* @returns Unprefixed hex of `nonce || ciphertext+tag`.
|
|
100
|
+
*/
|
|
101
|
+
export declare function encryptToPublic(privateKey: string, publicKey: string, plaintext: Uint8Array): string;
|
|
102
|
+
/**
|
|
103
|
+
* Decrypts escrow-wrap-v1 ciphertext produced by {@link encryptToPublic}.
|
|
104
|
+
*
|
|
105
|
+
* @param privateKey - Recipient P-256 private JWK JSON.
|
|
106
|
+
* @param publicKey - Sender P-256 public JWK JSON.
|
|
107
|
+
* @param ciphertextHex - Unprefixed or `0x` hex of `nonce || ciphertext+tag`.
|
|
108
|
+
* @returns Plaintext bytes.
|
|
109
|
+
*/
|
|
110
|
+
export declare function decryptFromPublic(privateKey: string, publicKey: string, ciphertextHex: string): Uint8Array;
|
|
111
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAM3C,OAAO,KAAK,EAAe,UAAU,EAAE,MAAM,YAAY,CAAC;AAM1D;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,GAAG,CAExC;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,UAAU,EAAE,GAAG,OAAO,CAW1E;AAED;;GAEG;AACH,wBAAgB,QAAQ,IAAI,GAAG,CAE9B;AAED;;;;GAIG;AACH,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB,CAgBA;AAED;;;;;;GAMG;AACH,wBAAgB,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,GAAG,CAU7D;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,OAAO,CAkBT;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,GAAG,CAQN;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,CAIvD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,aAAa,EAAE,MAAM,GAAG,GAAG,CAEpD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,UAAU,GACpB,MAAM,CAKR;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB,UAAU,CASZ"}
|