Skip to content

Commit a28148c

Browse files
committed
store and recover keyring encryption key
1 parent 97f3959 commit a28148c

File tree

4 files changed

+138
-34
lines changed

4 files changed

+138
-34
lines changed

packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2972,7 +2972,7 @@ describe('SeedlessOnboardingController', () => {
29722972
password: RECOVERED_PASSWORD,
29732973
});
29742974

2975-
const result = await controller.recoverCurrentDevicePassword({
2975+
const result = await controller.recoverKeyringEncryptionKey({
29762976
globalPassword: GLOBAL_PASSWORD,
29772977
});
29782978

@@ -2990,7 +2990,7 @@ describe('SeedlessOnboardingController', () => {
29902990
},
29912991
async ({ controller }) => {
29922992
await expect(
2993-
controller.recoverCurrentDevicePassword({
2993+
controller.recoverKeyringEncryptionKey({
29942994
globalPassword: GLOBAL_PASSWORD,
29952995
}),
29962996
).rejects.toThrow(
@@ -3019,7 +3019,7 @@ describe('SeedlessOnboardingController', () => {
30193019
);
30203020

30213021
await expect(
3022-
controller.recoverCurrentDevicePassword({
3022+
controller.recoverKeyringEncryptionKey({
30233023
globalPassword: GLOBAL_PASSWORD,
30243024
}),
30253025
).rejects.toStrictEqual(
@@ -3061,7 +3061,7 @@ describe('SeedlessOnboardingController', () => {
30613061
);
30623062

30633063
await expect(
3064-
controller.recoverCurrentDevicePassword({
3064+
controller.recoverKeyringEncryptionKey({
30653065
globalPassword: GLOBAL_PASSWORD,
30663066
}),
30673067
).rejects.toStrictEqual(
@@ -3098,7 +3098,7 @@ describe('SeedlessOnboardingController', () => {
30983098
.mockRejectedValueOnce(new Error('Unknown error'));
30993099

31003100
await expect(
3101-
controller.recoverCurrentDevicePassword({
3101+
controller.recoverKeyringEncryptionKey({
31023102
globalPassword: GLOBAL_PASSWORD,
31033103
}),
31043104
).rejects.toStrictEqual(
@@ -4131,7 +4131,7 @@ describe('SeedlessOnboardingController', () => {
41314131
isNewUser: false,
41324132
});
41334133

4134-
await controller.recoverCurrentDevicePassword({
4134+
await controller.recoverKeyringEncryptionKey({
41354135
globalPassword: MOCK_PASSWORD,
41364136
});
41374137

packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts

Lines changed: 126 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import {
1212
TOPRFError,
1313
} from '@metamask/toprf-secure-backup';
1414
import { base64ToBytes, bytesToBase64, bigIntToHex } from '@metamask/utils';
15+
import { gcm } from '@noble/ciphers/aes';
16+
import { bytesToUtf8, utf8ToBytes } from '@noble/ciphers/utils';
17+
import { managedNonce } from '@noble/ciphers/webcrypto';
1518
import { secp256k1 } from '@noble/curves/secp256k1';
1619
import { Mutex } from 'async-mutex';
1720

@@ -119,6 +122,10 @@ const seedlessOnboardingMetadata: StateMetadata<SeedlessOnboardingControllerStat
119122
persist: false,
120123
anonymous: true,
121124
},
125+
encryptedKeyringEncryptionKey: {
126+
persist: true,
127+
anonymous: true,
128+
},
122129
};
123130

124131
export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
@@ -276,12 +283,14 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
276283
* @param password - The password used to create new wallet and seedphrase
277284
* @param seedPhrase - The initial seed phrase (Mnemonic) created together with the wallet.
278285
* @param keyringId - The keyring id of the backup seed phrase
286+
* @param keyringEncryptionKey - The encryption key to be used for encrypting the keyring vault.
279287
* @returns A promise that resolves to the encrypted seed phrase and the encryption key.
280288
*/
281289
async createToprfKeyAndBackupSeedPhrase(
282290
password: string,
283291
seedPhrase: Uint8Array,
284292
keyringId: string,
293+
keyringEncryptionKey: string,
285294
): Promise<void> {
286295
return await this.#withControllerLock(async () => {
287296
// to make sure that fail fast,
@@ -319,6 +328,8 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
319328
this.#persistAuthPubKey({
320329
authPubKey: authKeyPair.pk,
321330
});
331+
// encrypt and store the keyring encryption key
332+
await this.#storeKeyringEncryptionKey(encKey, keyringEncryptionKey);
322333
};
323334

324335
await this.#executeWithTokenRefresh(
@@ -455,11 +466,21 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
455466
*
456467
* Changing password will also update the encryption key, metadata store and the vault with new encrypted values.
457468
*
458-
* @param newPassword - The new password to update.
459-
* @param oldPassword - The old password to verify.
469+
* @param params - The function parameters.
470+
* @param params.oldPassword - The old password to verify.
471+
* @param params.newPassword - The new password to update.
472+
* @param params.newKeyringEncryptionKey - The new keyring encryption key to store.
460473
* @returns A promise that resolves to the success of the operation.
461474
*/
462-
async changePassword(newPassword: string, oldPassword: string) {
475+
async changePassword({
476+
oldPassword,
477+
newPassword,
478+
newKeyringEncryptionKey,
479+
}: {
480+
oldPassword: string;
481+
newPassword: string;
482+
newKeyringEncryptionKey: string;
483+
}) {
463484
return await this.#withControllerLock(async () => {
464485
this.#assertIsUnlocked();
465486
// verify the old password of the encrypted vault
@@ -474,7 +495,11 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
474495
const attemptChangePassword = async (): Promise<void> => {
475496
// update the encryption key with new password and update the Metadata Store
476497
const { encKey: newEncKey, authKeyPair: newAuthKeyPair } =
477-
await this.#changeEncryptionKey(newPassword, oldPassword);
498+
await this.#changeEncryptionKey({
499+
oldPassword,
500+
newPassword,
501+
newKeyringEncryptionKey,
502+
});
478503

479504
// update and encrypt the vault with new password
480505
await this.#createNewVaultWithAuthData({
@@ -653,52 +678,58 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
653678
* @param params.globalPassword - The latest global password.
654679
* @returns A promise that resolves to the password corresponding to the current authPubKey in state.
655680
*/
656-
async recoverCurrentDevicePassword({
681+
async recoverKeyringEncryptionKey({
657682
globalPassword,
658683
}: {
659684
globalPassword: string;
660-
}): Promise<{ password: string }> {
685+
}): Promise<{ keyringEncryptionKey: string }> {
661686
return await this.#withControllerLock(async () => {
662687
return await this.#executeWithTokenRefresh(async () => {
663688
const currentDeviceAuthPubKey = this.#recoverAuthPubKey();
664-
const { password: currentDevicePassword } = await this.#recoverPassword(
665-
{
666-
targetPwPubKey: currentDeviceAuthPubKey,
689+
const { keyringEncryptionKey } =
690+
await this.#recoverKeyringEncryptionKey({
691+
targetAuthPubKey: currentDeviceAuthPubKey,
667692
globalPassword,
668-
},
669-
);
693+
});
670694
return {
671-
password: currentDevicePassword,
695+
keyringEncryptionKey,
672696
};
673-
}, 'recoverCurrentDevicePassword');
697+
}, 'recoverKeyringEncryptionKey');
674698
});
675699
}
676700

677701
/**
678-
* @description Fetch the password corresponding to the targetPwPubKey.
702+
* @description Fetch the keyring encryption key corresponding to the targetAuthPubKey.
679703
*
680-
* @param params - The parameters for fetching the password.
681-
* @param params.targetPwPubKey - The target public key of the password to recover.
704+
* @param params - The parameters for fetching the keyring encryption key.
705+
* @param params.targetAuthPubKey - The target public key of the keyring encryption key to recover.
682706
* @param params.globalPassword - The latest global password.
683-
* @returns A promise that resolves to the password corresponding to the current authPubKey in state.
707+
* @returns A promise that resolves to the keyring encryption key corresponding to the current authPubKey in state.
684708
*/
685-
async #recoverPassword({
686-
targetPwPubKey,
709+
async #recoverKeyringEncryptionKey({
710+
targetAuthPubKey,
687711
globalPassword,
688712
}: {
689-
targetPwPubKey: SEC1EncodedPublicKey;
713+
targetAuthPubKey: SEC1EncodedPublicKey;
690714
globalPassword: string;
691-
}): Promise<{ password: string }> {
715+
}): Promise<{ keyringEncryptionKey: string }> {
692716
const { encKey: latestPwEncKey, authKeyPair: latestPwAuthKeyPair } =
693717
await this.#recoverEncKey(globalPassword);
694718

695719
try {
696720
const res = await this.toprfClient.recoverPassword({
697-
targetPwPubKey,
721+
targetPwPubKey: targetAuthPubKey,
698722
curEncKey: latestPwEncKey,
699723
curAuthKeyPair: latestPwAuthKeyPair,
700724
});
701-
return res;
725+
const { password: recoveredEncryptionKeyBase64 } = res;
726+
const recoveredEncryptionKey = base64ToBytes(
727+
recoveredEncryptionKeyBase64,
728+
);
729+
const keyringEncryptionKey = await this.#loadKeyringEncryptionKey(
730+
recoveredEncryptionKey,
731+
);
732+
return { keyringEncryptionKey };
702733
} catch (error) {
703734
if (this.#isTokenExpiredError(error)) {
704735
throw error;
@@ -832,6 +863,42 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
832863
});
833864
}
834865

866+
/**
867+
* Encrypt the keyring encryption key and store it in state.
868+
*
869+
* @param encKey - The encryption key.
870+
* @param keyringEncryptionKey - The keyring encryption key.
871+
*/
872+
async #storeKeyringEncryptionKey(
873+
encKey: Uint8Array,
874+
keyringEncryptionKey: string,
875+
) {
876+
const aes = managedNonce(gcm)(encKey);
877+
const encryptedKeyringEncryptionKey = aes.encrypt(
878+
utf8ToBytes(keyringEncryptionKey),
879+
);
880+
this.update((state) => {
881+
state.encryptedKeyringEncryptionKey = bytesToBase64(
882+
encryptedKeyringEncryptionKey,
883+
);
884+
});
885+
}
886+
887+
/**
888+
* Decrypt the keyring encryption key from state.
889+
*
890+
* @param encKey - The encryption key.
891+
* @returns The keyring encryption key.
892+
*/
893+
async #loadKeyringEncryptionKey(encKey: Uint8Array) {
894+
const { encryptedKeyringEncryptionKey: encryptedKey } = this.state;
895+
assertIsEncryptedKeyringEncryptionKeySet(encryptedKey);
896+
const encryptedPasswordBytes = base64ToBytes(encryptedKey);
897+
const aes = managedNonce(gcm)(encKey);
898+
const password = aes.decrypt(encryptedPasswordBytes);
899+
return bytesToUtf8(password);
900+
}
901+
835902
/**
836903
* Recover the authentication public key from the state.
837904
* convert to pubkey format before recovering.
@@ -881,11 +948,21 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
881948
/**
882949
* Update the encryption key with new password and update the Metadata Store with new encryption key.
883950
*
884-
* @param newPassword - The new password to update.
885-
* @param oldPassword - The old password to verify.
951+
* @param params - The function parameters.
952+
* @param params.oldPassword - The old password to verify.
953+
* @param params.newPassword - The new password to update.
954+
* @param params.newKeyringEncryptionKey - The new keyring encryption key to store.
886955
* @returns A promise that resolves to new encryption key and authentication key pair.
887956
*/
888-
async #changeEncryptionKey(newPassword: string, oldPassword: string) {
957+
async #changeEncryptionKey({
958+
oldPassword,
959+
newPassword,
960+
newKeyringEncryptionKey,
961+
}: {
962+
newPassword: string;
963+
oldPassword: string;
964+
newKeyringEncryptionKey: string;
965+
}) {
889966
this.#assertIsAuthenticatedUser(this.state);
890967
const { authConnectionId, groupedAuthConnectionId, userId } = this.state;
891968

@@ -895,17 +972,22 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
895972
keyShareIndex: newKeyShareIndex,
896973
} = await this.#recoverEncKey(oldPassword);
897974

898-
return await this.toprfClient.changeEncKey({
975+
const result = await this.toprfClient.changeEncKey({
899976
nodeAuthTokens: this.state.nodeAuthTokens,
900977
authConnectionId,
901978
groupedAuthConnectionId,
902979
userId,
903980
oldEncKey: encKey,
904981
oldAuthKeyPair: authKeyPair,
905982
newKeyShareIndex,
906-
oldPassword,
983+
oldPassword: bytesToBase64(encKey),
907984
newPassword,
908985
});
986+
await this.#storeKeyringEncryptionKey(
987+
result.encKey,
988+
newKeyringEncryptionKey,
989+
);
990+
return result;
909991
}
910992

911993
/**
@@ -1673,3 +1755,19 @@ async function withLock<Result>(
16731755
releaseLock();
16741756
}
16751757
}
1758+
1759+
/**
1760+
* Assert that the provided encrypted keyring encryption key is a valid non-empty string.
1761+
*
1762+
* @param encryptedKeyringEncryptionKey - The encrypted keyring encryption key to check.
1763+
* @throws If the encrypted keyring encryption key is not a valid string.
1764+
*/
1765+
function assertIsEncryptedKeyringEncryptionKeySet(
1766+
encryptedKeyringEncryptionKey: string | undefined,
1767+
): asserts encryptedKeyringEncryptionKey is string {
1768+
if (!encryptedKeyringEncryptionKey) {
1769+
throw new Error(
1770+
SeedlessOnboardingControllerErrorMessage.EncryptedKeyringEncryptionKeyNotSet,
1771+
);
1772+
}
1773+
}

packages/seedless-onboarding-controller/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,5 @@ export enum SeedlessOnboardingControllerErrorMessage {
5050
OutdatedPassword = `${controllerName} - Outdated password`,
5151
CouldNotRecoverPassword = `${controllerName} - Could not recover password`,
5252
SRPNotBackedUpError = `${controllerName} - SRP not backed up`,
53+
EncryptedKeyringEncryptionKeyNotSet = `${controllerName} - Encrypted keyring encryption key is not set`,
5354
}

packages/seedless-onboarding-controller/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ export type SeedlessOnboardingControllerState =
148148
* This is temporarily stored in state during authentication and then persisted in the vault.
149149
*/
150150
revokeToken?: string;
151+
152+
/**
153+
* The encrypted keyring encryption key used to encrypt the keyring vault.
154+
*/
155+
encryptedKeyringEncryptionKey?: string;
151156
};
152157

153158
// Actions

0 commit comments

Comments
 (0)