@@ -26,6 +26,9 @@ import {
26
26
stringToBytes ,
27
27
bigIntToHex ,
28
28
} from '@metamask/utils' ;
29
+ import { gcm } from '@noble/ciphers/aes' ;
30
+ import { utf8ToBytes } from '@noble/ciphers/utils' ;
31
+ import { managedNonce } from '@noble/ciphers/webcrypto' ;
29
32
import type { webcrypto } from 'node:crypto' ;
30
33
31
34
import {
@@ -91,6 +94,7 @@ const MOCK_NODE_AUTH_TOKENS = [
91
94
] ;
92
95
93
96
const MOCK_KEYRING_ID = 'mock-keyring-id' ;
97
+ const MOCK_KEYRING_ENCRYPTION_KEY = 'mock-keyring-encryption-key' ;
94
98
const MOCK_SEED_PHRASE = stringToBytes (
95
99
'horror pink muffin canal young photo magnet runway start elder patch until' ,
96
100
) ;
@@ -396,11 +400,17 @@ async function createMockVault(
396
400
const { vault : encryptedMockVault , exportedKeyString } =
397
401
await encryptor . encryptWithDetail ( MOCK_PASSWORD , serializedKeyData ) ;
398
402
403
+ const aes = managedNonce ( gcm ) ( encKey ) ;
404
+ const encryptedKeyringEncryptionKey = aes . encrypt (
405
+ utf8ToBytes ( MOCK_KEYRING_ENCRYPTION_KEY ) ,
406
+ ) ;
407
+
399
408
return {
400
409
encryptedMockVault,
401
410
vaultEncryptionKey : exportedKeyString ,
402
411
vaultEncryptionSalt : JSON . parse ( encryptedMockVault ) . salt ,
403
412
revokeToken : mockRevokeToken ,
413
+ encryptedKeyringEncryptionKey,
404
414
} ;
405
415
}
406
416
@@ -445,6 +455,7 @@ async function decryptVault(vault: string, password: string) {
445
455
* @param options.vault - The mock vault data.
446
456
* @param options.vaultEncryptionKey - The mock vault encryption key.
447
457
* @param options.vaultEncryptionSalt - The mock vault encryption salt.
458
+ * @param options.encryptedKeyringEncryptionKey - The mock encrypted keyring encryption key.
448
459
* @returns The initial controller state with the mock authenticated user.
449
460
*/
450
461
function getMockInitialControllerState ( options ?: {
@@ -455,6 +466,7 @@ function getMockInitialControllerState(options?: {
455
466
vault ?: string ;
456
467
vaultEncryptionKey ?: string ;
457
468
vaultEncryptionSalt ?: string ;
469
+ encryptedKeyringEncryptionKey ?: string ;
458
470
} ) : Partial < SeedlessOnboardingControllerState > {
459
471
const state = getDefaultSeedlessOnboardingControllerState ( ) ;
460
472
@@ -486,6 +498,10 @@ function getMockInitialControllerState(options?: {
486
498
state . authPubKey = options . authPubKey ?? MOCK_AUTH_PUB_KEY ;
487
499
}
488
500
501
+ if ( options ?. encryptedKeyringEncryptionKey ) {
502
+ state . encryptedKeyringEncryptionKey = options . encryptedKeyringEncryptionKey ;
503
+ }
504
+
489
505
return state ;
490
506
}
491
507
@@ -2942,11 +2958,11 @@ describe('SeedlessOnboardingController', () => {
2942
2958
} ) ;
2943
2959
} ) ;
2944
2960
2945
- describe ( 'recoverCurrentDevicePassword ' , ( ) => {
2961
+ describe ( 'recoverKeyringEncryptionKey ' , ( ) => {
2946
2962
const GLOBAL_PASSWORD = 'global-password' ;
2947
2963
const RECOVERED_PASSWORD = 'recovered-password' ;
2948
2964
2949
- it ( 'should recover the password for the current device ' , async ( ) => {
2965
+ it ( 'should store and recover keyring encryption key ' , async ( ) => {
2950
2966
await withController (
2951
2967
{
2952
2968
state : getMockInitialControllerState ( {
@@ -2955,6 +2971,19 @@ describe('SeedlessOnboardingController', () => {
2955
2971
} ) ,
2956
2972
} ,
2957
2973
async ( { controller, toprfClient } ) => {
2974
+ // Setup and store keyring encryption key.
2975
+ await mockCreateToprfKeyAndBackupSeedPhrase (
2976
+ toprfClient ,
2977
+ controller ,
2978
+ RECOVERED_PASSWORD ,
2979
+ MOCK_SEED_PHRASE ,
2980
+ MOCK_KEYRING_ID ,
2981
+ ) ;
2982
+
2983
+ await controller . storeKeyringEncryptionKey (
2984
+ MOCK_KEYRING_ENCRYPTION_KEY ,
2985
+ ) ;
2986
+
2958
2987
// Mock recoverEncKey for the global password
2959
2988
const mockToprfEncryptor = createMockToprfEncryptor ( ) ;
2960
2989
const encKey = mockToprfEncryptor . deriveEncKey ( GLOBAL_PASSWORD ) ;
@@ -2968,29 +2997,72 @@ describe('SeedlessOnboardingController', () => {
2968
2997
} ) ;
2969
2998
2970
2999
// Mock toprfClient.recoverPassword
3000
+ const recoveredEncKey =
3001
+ mockToprfEncryptor . deriveEncKey ( RECOVERED_PASSWORD ) ;
2971
3002
jest . spyOn ( toprfClient , 'recoverPassword' ) . mockResolvedValueOnce ( {
2972
- password : RECOVERED_PASSWORD ,
3003
+ password : bytesToBase64 ( recoveredEncKey ) ,
2973
3004
} ) ;
2974
3005
2975
- const result = await controller . recoverCurrentDevicePassword ( {
3006
+ const result = await controller . recoverKeyringEncryptionKey ( {
2976
3007
globalPassword : GLOBAL_PASSWORD ,
2977
3008
} ) ;
2978
3009
2979
- expect ( result ) . toStrictEqual ( { password : RECOVERED_PASSWORD } ) ;
3010
+ expect ( result ) . toStrictEqual ( {
3011
+ keyringEncryptionKey : MOCK_KEYRING_ENCRYPTION_KEY ,
3012
+ } ) ;
2980
3013
expect ( toprfClient . recoverEncKey ) . toHaveBeenCalled ( ) ;
2981
3014
expect ( toprfClient . recoverPassword ) . toHaveBeenCalled ( ) ;
2982
3015
} ,
2983
3016
) ;
2984
3017
} ) ;
2985
3018
3019
+ it ( 'should throw if encryptedKeyringEncryptionKey not set' , async ( ) => {
3020
+ await withController (
3021
+ {
3022
+ state : getMockInitialControllerState ( {
3023
+ withMockAuthenticatedUser : true ,
3024
+ withMockAuthPubKey : true ,
3025
+ } ) ,
3026
+ } ,
3027
+ async ( { controller, toprfClient } ) => {
3028
+ // Mock recoverEncKey for the global password
3029
+ const mockToprfEncryptor = createMockToprfEncryptor ( ) ;
3030
+ const encKey = mockToprfEncryptor . deriveEncKey ( GLOBAL_PASSWORD ) ;
3031
+ const authKeyPair =
3032
+ mockToprfEncryptor . deriveAuthKeyPair ( GLOBAL_PASSWORD ) ;
3033
+ jest . spyOn ( toprfClient , 'recoverEncKey' ) . mockResolvedValueOnce ( {
3034
+ encKey,
3035
+ authKeyPair,
3036
+ rateLimitResetResult : Promise . resolve ( ) ,
3037
+ keyShareIndex : 1 ,
3038
+ } ) ;
3039
+
3040
+ // Mock toprfClient.recoverPassword
3041
+ const recoveredEncKey =
3042
+ mockToprfEncryptor . deriveEncKey ( RECOVERED_PASSWORD ) ;
3043
+ jest . spyOn ( toprfClient , 'recoverPassword' ) . mockResolvedValueOnce ( {
3044
+ password : bytesToBase64 ( recoveredEncKey ) ,
3045
+ } ) ;
3046
+
3047
+ await expect (
3048
+ controller . recoverKeyringEncryptionKey ( {
3049
+ globalPassword : GLOBAL_PASSWORD ,
3050
+ } ) ,
3051
+ ) . rejects . toThrow (
3052
+ SeedlessOnboardingControllerErrorMessage . CouldNotRecoverPassword ,
3053
+ ) ;
3054
+ } ,
3055
+ ) ;
3056
+ } ) ;
3057
+
2986
3058
it ( 'should throw SRPNotBackedUpError if no authPubKey in state' , async ( ) => {
2987
3059
await withController (
2988
3060
{
2989
3061
state : getMockInitialControllerState ( { } ) ,
2990
3062
} ,
2991
3063
async ( { controller } ) => {
2992
3064
await expect (
2993
- controller . recoverCurrentDevicePassword ( {
3065
+ controller . recoverKeyringEncryptionKey ( {
2994
3066
globalPassword : GLOBAL_PASSWORD ,
2995
3067
} ) ,
2996
3068
) . rejects . toThrow (
@@ -3019,7 +3091,7 @@ describe('SeedlessOnboardingController', () => {
3019
3091
) ;
3020
3092
3021
3093
await expect (
3022
- controller . recoverCurrentDevicePassword ( {
3094
+ controller . recoverKeyringEncryptionKey ( {
3023
3095
globalPassword : GLOBAL_PASSWORD ,
3024
3096
} ) ,
3025
3097
) . rejects . toStrictEqual (
@@ -3061,7 +3133,7 @@ describe('SeedlessOnboardingController', () => {
3061
3133
) ;
3062
3134
3063
3135
await expect (
3064
- controller . recoverCurrentDevicePassword ( {
3136
+ controller . recoverKeyringEncryptionKey ( {
3065
3137
globalPassword : GLOBAL_PASSWORD ,
3066
3138
} ) ,
3067
3139
) . rejects . toStrictEqual (
@@ -3098,7 +3170,7 @@ describe('SeedlessOnboardingController', () => {
3098
3170
. mockRejectedValueOnce ( new Error ( 'Unknown error' ) ) ;
3099
3171
3100
3172
await expect (
3101
- controller . recoverCurrentDevicePassword ( {
3173
+ controller . recoverKeyringEncryptionKey ( {
3102
3174
globalPassword : GLOBAL_PASSWORD ,
3103
3175
} ) ,
3104
3176
) . rejects . toStrictEqual (
@@ -3995,7 +4067,7 @@ describe('SeedlessOnboardingController', () => {
3995
4067
} ) ;
3996
4068
} ) ;
3997
4069
3998
- describe ( 'recoverCurrentDevicePassword with token refresh' , ( ) => {
4070
+ describe ( 'recoverKeyringEncryptionKey with token refresh' , ( ) => {
3999
4071
// const OLD_PASSWORD = 'old-mock-password';
4000
4072
// const GLOBAL_PASSWORD = 'new-global-password';
4001
4073
let MOCK_VAULT : string ;
@@ -4004,6 +4076,7 @@ describe('SeedlessOnboardingController', () => {
4004
4076
let INITIAL_AUTH_PUB_KEY : string ;
4005
4077
let initialAuthKeyPair : KeyPair ; // Store initial keypair for vault creation
4006
4078
let initialEncKey : Uint8Array ; // Store initial encKey for vault creation
4079
+ let initialEncryptedKeyringEncryptionKey : Uint8Array ; // Store initial encKey for vault creation
4007
4080
4008
4081
// Generate initial keys and vault state before tests run
4009
4082
beforeAll ( async ( ) => {
@@ -4023,9 +4096,11 @@ describe('SeedlessOnboardingController', () => {
4023
4096
MOCK_VAULT = mockResult . encryptedMockVault ;
4024
4097
MOCK_VAULT_ENCRYPTION_KEY = mockResult . vaultEncryptionKey ;
4025
4098
MOCK_VAULT_ENCRYPTION_SALT = mockResult . vaultEncryptionSalt ;
4099
+ initialEncryptedKeyringEncryptionKey =
4100
+ mockResult . encryptedKeyringEncryptionKey ;
4026
4101
} ) ;
4027
4102
4028
- it ( 'should retry recoverCurrentDevicePassword after refreshing expired tokens' , async ( ) => {
4103
+ it ( 'should retry recoverKeyringEncryptionKey after refreshing expired tokens' , async ( ) => {
4029
4104
await withController (
4030
4105
{
4031
4106
state : getMockInitialControllerState ( {
@@ -4034,6 +4109,9 @@ describe('SeedlessOnboardingController', () => {
4034
4109
vault : MOCK_VAULT ,
4035
4110
vaultEncryptionKey : MOCK_VAULT_ENCRYPTION_KEY ,
4036
4111
vaultEncryptionSalt : MOCK_VAULT_ENCRYPTION_SALT ,
4112
+ encryptedKeyringEncryptionKey : bytesToBase64 (
4113
+ initialEncryptedKeyringEncryptionKey ,
4114
+ ) ,
4037
4115
} ) ,
4038
4116
} ,
4039
4117
async ( { controller, toprfClient, mockRefreshJWTToken } ) => {
@@ -4055,7 +4133,7 @@ describe('SeedlessOnboardingController', () => {
4055
4133
) ;
4056
4134
} )
4057
4135
. mockResolvedValueOnce ( {
4058
- password : MOCK_PASSWORD ,
4136
+ password : bytesToBase64 ( initialEncKey ) ,
4059
4137
} ) ;
4060
4138
4061
4139
// Mock authenticate for token refresh
@@ -4064,7 +4142,7 @@ describe('SeedlessOnboardingController', () => {
4064
4142
isNewUser : false ,
4065
4143
} ) ;
4066
4144
4067
- await controller . recoverCurrentDevicePassword ( {
4145
+ await controller . recoverKeyringEncryptionKey ( {
4068
4146
globalPassword : MOCK_PASSWORD ,
4069
4147
} ) ;
4070
4148
0 commit comments