@@ -12,6 +12,9 @@ import {
12
12
TOPRFError ,
13
13
} from '@metamask/toprf-secure-backup' ;
14
14
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' ;
15
18
import { secp256k1 } from '@noble/curves/secp256k1' ;
16
19
import { Mutex } from 'async-mutex' ;
17
20
@@ -119,6 +122,10 @@ const seedlessOnboardingMetadata: StateMetadata<SeedlessOnboardingControllerStat
119
122
persist : false ,
120
123
anonymous : true ,
121
124
} ,
125
+ encryptedKeyringEncryptionKey : {
126
+ persist : true ,
127
+ anonymous : true ,
128
+ } ,
122
129
} ;
123
130
124
131
export class SeedlessOnboardingController < EncryptionKey > extends BaseController <
@@ -276,12 +283,14 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
276
283
* @param password - The password used to create new wallet and seedphrase
277
284
* @param seedPhrase - The initial seed phrase (Mnemonic) created together with the wallet.
278
285
* @param keyringId - The keyring id of the backup seed phrase
286
+ * @param keyringEncryptionKey - The encryption key to be used for encrypting the keyring vault.
279
287
* @returns A promise that resolves to the encrypted seed phrase and the encryption key.
280
288
*/
281
289
async createToprfKeyAndBackupSeedPhrase (
282
290
password : string ,
283
291
seedPhrase : Uint8Array ,
284
292
keyringId : string ,
293
+ keyringEncryptionKey : string ,
285
294
) : Promise < void > {
286
295
return await this . #withControllerLock( async ( ) => {
287
296
// to make sure that fail fast,
@@ -319,6 +328,8 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
319
328
this . #persistAuthPubKey( {
320
329
authPubKey : authKeyPair . pk ,
321
330
} ) ;
331
+ // encrypt and store the keyring encryption key
332
+ await this . #storeKeyringEncryptionKey( encKey , keyringEncryptionKey ) ;
322
333
} ;
323
334
324
335
await this . #executeWithTokenRefresh(
@@ -455,11 +466,21 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
455
466
*
456
467
* Changing password will also update the encryption key, metadata store and the vault with new encrypted values.
457
468
*
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.
460
473
* @returns A promise that resolves to the success of the operation.
461
474
*/
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
+ } ) {
463
484
return await this . #withControllerLock( async ( ) => {
464
485
this . #assertIsUnlocked( ) ;
465
486
// verify the old password of the encrypted vault
@@ -474,7 +495,11 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
474
495
const attemptChangePassword = async ( ) : Promise < void > => {
475
496
// update the encryption key with new password and update the Metadata Store
476
497
const { encKey : newEncKey , authKeyPair : newAuthKeyPair } =
477
- await this . #changeEncryptionKey( newPassword , oldPassword ) ;
498
+ await this . #changeEncryptionKey( {
499
+ oldPassword,
500
+ newPassword,
501
+ newKeyringEncryptionKey,
502
+ } ) ;
478
503
479
504
// update and encrypt the vault with new password
480
505
await this . #createNewVaultWithAuthData( {
@@ -653,52 +678,58 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
653
678
* @param params.globalPassword - The latest global password.
654
679
* @returns A promise that resolves to the password corresponding to the current authPubKey in state.
655
680
*/
656
- async recoverCurrentDevicePassword ( {
681
+ async recoverKeyringEncryptionKey ( {
657
682
globalPassword,
658
683
} : {
659
684
globalPassword : string ;
660
- } ) : Promise < { password : string } > {
685
+ } ) : Promise < { keyringEncryptionKey : string } > {
661
686
return await this . #withControllerLock( async ( ) => {
662
687
return await this . #executeWithTokenRefresh( async ( ) => {
663
688
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 ,
667
692
globalPassword,
668
- } ,
669
- ) ;
693
+ } ) ;
670
694
return {
671
- password : currentDevicePassword ,
695
+ keyringEncryptionKey ,
672
696
} ;
673
- } , 'recoverCurrentDevicePassword ' ) ;
697
+ } , 'recoverKeyringEncryptionKey ' ) ;
674
698
} ) ;
675
699
}
676
700
677
701
/**
678
- * @description Fetch the password corresponding to the targetPwPubKey .
702
+ * @description Fetch the keyring encryption key corresponding to the targetAuthPubKey .
679
703
*
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.
682
706
* @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.
684
708
*/
685
- async #recoverPassword ( {
686
- targetPwPubKey ,
709
+ async #recoverKeyringEncryptionKey ( {
710
+ targetAuthPubKey ,
687
711
globalPassword,
688
712
} : {
689
- targetPwPubKey : SEC1EncodedPublicKey ;
713
+ targetAuthPubKey : SEC1EncodedPublicKey ;
690
714
globalPassword : string ;
691
- } ) : Promise < { password : string } > {
715
+ } ) : Promise < { keyringEncryptionKey : string } > {
692
716
const { encKey : latestPwEncKey , authKeyPair : latestPwAuthKeyPair } =
693
717
await this . #recoverEncKey( globalPassword ) ;
694
718
695
719
try {
696
720
const res = await this . toprfClient . recoverPassword ( {
697
- targetPwPubKey,
721
+ targetPwPubKey : targetAuthPubKey ,
698
722
curEncKey : latestPwEncKey ,
699
723
curAuthKeyPair : latestPwAuthKeyPair ,
700
724
} ) ;
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 } ;
702
733
} catch ( error ) {
703
734
if ( this . #isTokenExpiredError( error ) ) {
704
735
throw error ;
@@ -832,6 +863,42 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
832
863
} ) ;
833
864
}
834
865
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
+
835
902
/**
836
903
* Recover the authentication public key from the state.
837
904
* convert to pubkey format before recovering.
@@ -881,11 +948,21 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
881
948
/**
882
949
* Update the encryption key with new password and update the Metadata Store with new encryption key.
883
950
*
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.
886
955
* @returns A promise that resolves to new encryption key and authentication key pair.
887
956
*/
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
+ } ) {
889
966
this . #assertIsAuthenticatedUser( this . state ) ;
890
967
const { authConnectionId, groupedAuthConnectionId, userId } = this . state ;
891
968
@@ -895,17 +972,22 @@ export class SeedlessOnboardingController<EncryptionKey> extends BaseController<
895
972
keyShareIndex : newKeyShareIndex ,
896
973
} = await this . #recoverEncKey( oldPassword ) ;
897
974
898
- return await this . toprfClient . changeEncKey ( {
975
+ const result = await this . toprfClient . changeEncKey ( {
899
976
nodeAuthTokens : this . state . nodeAuthTokens ,
900
977
authConnectionId,
901
978
groupedAuthConnectionId,
902
979
userId,
903
980
oldEncKey : encKey ,
904
981
oldAuthKeyPair : authKeyPair ,
905
982
newKeyShareIndex,
906
- oldPassword,
983
+ oldPassword : bytesToBase64 ( encKey ) ,
907
984
newPassword,
908
985
} ) ;
986
+ await this . #storeKeyringEncryptionKey(
987
+ result . encKey ,
988
+ newKeyringEncryptionKey ,
989
+ ) ;
990
+ return result ;
909
991
}
910
992
911
993
/**
@@ -1673,3 +1755,19 @@ async function withLock<Result>(
1673
1755
releaseLock ( ) ;
1674
1756
}
1675
1757
}
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
+ }
0 commit comments