From 425f477c4364c3788882cc849d5ff2f405350bd4 Mon Sep 17 00:00:00 2001 From: lwin Date: Thu, 24 Jul 2025 18:10:12 +0800 Subject: [PATCH 1/7] feat: optimize submitGlobalPassword --- .../src/SeedlessOnboardingController.test.ts | 466 ++---------------- .../src/SeedlessOnboardingController.ts | 78 +-- 2 files changed, 58 insertions(+), 486 deletions(-) diff --git a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts index 3435dc16bf2..b3a85392fb7 100644 --- a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts +++ b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts @@ -3529,230 +3529,6 @@ describe('SeedlessOnboardingController', () => { }); }); - describe('syncLatestGlobalPassword', () => { - const OLD_PASSWORD = 'old-mock-password'; - const GLOBAL_PASSWORD = 'new-global-password'; - let MOCK_VAULT: string; - let MOCK_VAULT_ENCRYPTION_KEY: string; - let MOCK_VAULT_ENCRYPTION_SALT: string; - let INITIAL_AUTH_PUB_KEY: string; - let initialAuthKeyPair: KeyPair; // Store initial keypair for vault creation - let initialEncKey: Uint8Array; // Store initial encKey for vault creation - let initialPwEncKey: Uint8Array; // Store initial pwEncKey for vault creation - - // Generate initial keys and vault state before tests run - beforeAll(async () => { - const mockToprfEncryptor = createMockToprfEncryptor(); - initialEncKey = mockToprfEncryptor.deriveEncKey(OLD_PASSWORD); - initialPwEncKey = mockToprfEncryptor.derivePwEncKey(OLD_PASSWORD); - initialAuthKeyPair = mockToprfEncryptor.deriveAuthKeyPair(OLD_PASSWORD); - INITIAL_AUTH_PUB_KEY = bytesToBase64(initialAuthKeyPair.pk); - - const mockResult = await createMockVault( - initialEncKey, - initialPwEncKey, - initialAuthKeyPair, - OLD_PASSWORD, - ); - - MOCK_VAULT = mockResult.encryptedMockVault; - MOCK_VAULT_ENCRYPTION_KEY = mockResult.vaultEncryptionKey; - MOCK_VAULT_ENCRYPTION_SALT = mockResult.vaultEncryptionSalt; - }); - - // Remove beforeEach as setup is done in beforeAll now - - it('should successfully sync the latest global password', async () => { - await withController( - { - // Pass the pre-generated state values - state: getMockInitialControllerState({ - withMockAuthenticatedUser: true, - authPubKey: INITIAL_AUTH_PUB_KEY, // Use the base64 encoded key - vault: MOCK_VAULT, - vaultEncryptionKey: MOCK_VAULT_ENCRYPTION_KEY, - vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, - }), - }, - async ({ controller, toprfClient, encryptor }) => { - // Unlock controller first - requires vaultEncryptionKey/Salt or password - // Since we provide key/salt in state, submitPassword isn't strictly needed here - // but we keep it to match the method's requirement of being unlocked - // We'll use the key/salt implicitly by not providing password to unlockVaultAndGetBackupEncKey - await controller.submitPassword(OLD_PASSWORD); // Unlock using the standard method - - const recoverEncKeySpy = jest.spyOn(toprfClient, 'recoverEncKey'); - const encryptorSpy = jest.spyOn(encryptor, 'encryptWithDetail'); - - // Mock recoverEncKey for the new global password - const mockToprfEncryptor = createMockToprfEncryptor(); - const newEncKey = mockToprfEncryptor.deriveEncKey(GLOBAL_PASSWORD); - const newPwEncKey = - mockToprfEncryptor.derivePwEncKey(GLOBAL_PASSWORD); - const newAuthKeyPair = - mockToprfEncryptor.deriveAuthKeyPair(GLOBAL_PASSWORD); - - recoverEncKeySpy.mockResolvedValueOnce({ - encKey: newEncKey, - pwEncKey: newPwEncKey, - authKeyPair: newAuthKeyPair, - rateLimitResetResult: Promise.resolve(), - keyShareIndex: 1, - }); - - // We still need verifyPassword to work conceptually, even if unlock is bypassed - // verifyPasswordSpy.mockResolvedValueOnce(); // Don't mock, let the real one run inside syncLatestGlobalPassword - - controller.setLocked(); - - // Mock recoverEncKey for the global password - const encKey = mockToprfEncryptor.deriveEncKey(GLOBAL_PASSWORD); - const pwEncKey = mockToprfEncryptor.derivePwEncKey(GLOBAL_PASSWORD); - const authKeyPair = - mockToprfEncryptor.deriveAuthKeyPair(GLOBAL_PASSWORD); - jest.spyOn(toprfClient, 'recoverEncKey').mockResolvedValueOnce({ - encKey, - pwEncKey, - authKeyPair, - rateLimitResetResult: Promise.resolve(), - keyShareIndex: 1, - }); - - // Mock toprfClient.recoverPwEncKey - const recoveredPwEncKey = - mockToprfEncryptor.derivePwEncKey(OLD_PASSWORD); - jest.spyOn(toprfClient, 'recoverPwEncKey').mockResolvedValueOnce({ - pwEncKey: recoveredPwEncKey, - }); - - await controller.submitGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }); - - await controller.syncLatestGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }); - - // Assertions - expect(recoverEncKeySpy).toHaveBeenCalledWith( - expect.objectContaining({ password: GLOBAL_PASSWORD }), - ); - - // Check if vault was re-encrypted with the new password and keys - const expectedSerializedVaultData = JSON.stringify({ - toprfEncryptionKey: bytesToBase64(newEncKey), - toprfPwEncryptionKey: bytesToBase64(newPwEncKey), - toprfAuthKeyPair: JSON.stringify({ - sk: bigIntToHex(newAuthKeyPair.sk), - pk: bytesToBase64(newAuthKeyPair.pk), - }), - revokeToken: controller.state.revokeToken, - accessToken: controller.state.accessToken, - }); - expect(encryptorSpy).toHaveBeenCalledWith( - GLOBAL_PASSWORD, - expectedSerializedVaultData, - ); - - // Check if authPubKey was updated in state - expect(controller.state.authPubKey).toBe( - bytesToBase64(newAuthKeyPair.pk), - ); - // Check if vault content actually changed - expect(controller.state.vault).not.toBe(MOCK_VAULT); - }, - ); - }); - - it('should throw an error if recovering the encryption key for the global password fails', async () => { - await withController( - { - state: getMockInitialControllerState({ - withMockAuthenticatedUser: true, - authPubKey: INITIAL_AUTH_PUB_KEY, - vault: MOCK_VAULT, - vaultEncryptionKey: MOCK_VAULT_ENCRYPTION_KEY, - vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, - }), - }, - async ({ controller, toprfClient }) => { - // Unlock controller first - await controller.submitPassword(OLD_PASSWORD); - - const recoverEncKeySpy = jest - .spyOn(toprfClient, 'recoverEncKey') - .mockRejectedValueOnce( - new RecoveryError( - SeedlessOnboardingControllerErrorMessage.LoginFailedError, - ), - ); - - await expect( - controller.syncLatestGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), - ).rejects.toThrow( - SeedlessOnboardingControllerErrorMessage.LoginFailedError, - ); - - expect(recoverEncKeySpy).toHaveBeenCalledWith( - expect.objectContaining({ password: GLOBAL_PASSWORD }), - ); - }, - ); - }); - - it('should throw an error if creating the new vault fails', async () => { - await withController( - { - state: getMockInitialControllerState({ - withMockAuthenticatedUser: true, - authPubKey: INITIAL_AUTH_PUB_KEY, - vault: MOCK_VAULT, - vaultEncryptionKey: MOCK_VAULT_ENCRYPTION_KEY, - vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, - }), - }, - async ({ controller, toprfClient, encryptor }) => { - // Unlock controller first - await controller.submitPassword(OLD_PASSWORD); - - const recoverEncKeySpy = jest.spyOn(toprfClient, 'recoverEncKey'); - const encryptorSpy = jest - .spyOn(encryptor, 'encryptWithDetail') - .mockRejectedValueOnce(new Error('Vault creation failed')); - - // Mock recoverEncKey for the new global password - const mockToprfEncryptor = createMockToprfEncryptor(); - const newEncKey = mockToprfEncryptor.deriveEncKey(GLOBAL_PASSWORD); - const newPwEncKey = - mockToprfEncryptor.derivePwEncKey(GLOBAL_PASSWORD); - const newAuthKeyPair = - mockToprfEncryptor.deriveAuthKeyPair(GLOBAL_PASSWORD); - - recoverEncKeySpy.mockResolvedValueOnce({ - encKey: newEncKey, - pwEncKey: newPwEncKey, - authKeyPair: newAuthKeyPair, - rateLimitResetResult: Promise.resolve(), - keyShareIndex: 1, - }); - - await expect( - controller.syncLatestGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), - ).rejects.toThrow('Vault creation failed'); - - expect(recoverEncKeySpy).toHaveBeenCalledWith( - expect.objectContaining({ password: GLOBAL_PASSWORD }), - ); - expect(encryptorSpy).toHaveBeenCalled(); - }, - ); - }); - }); - describe('token refresh functionality', () => { const MOCK_PASSWORD = 'mock-password'; const NEW_MOCK_PASSWORD = 'new-mock-password'; @@ -4022,212 +3798,6 @@ describe('SeedlessOnboardingController', () => { }); }); - describe('syncLatestGlobalPassword with token refresh', () => { - const OLD_PASSWORD = 'old-mock-password'; - const GLOBAL_PASSWORD = 'new-global-password'; - let MOCK_VAULT: string; - let MOCK_VAULT_ENCRYPTION_KEY: string; - let MOCK_VAULT_ENCRYPTION_SALT: string; - let INITIAL_AUTH_PUB_KEY: string; - let initialAuthKeyPair: KeyPair; // Store initial keypair for vault creation - let initialEncKey: Uint8Array; // Store initial encKey for vault creation - let initialPwEncKey: Uint8Array; // Store initial pwEncKey for vault creation - - // Generate initial keys and vault state before tests run - beforeAll(async () => { - const mockToprfEncryptor = createMockToprfEncryptor(); - initialEncKey = mockToprfEncryptor.deriveEncKey(OLD_PASSWORD); - initialPwEncKey = mockToprfEncryptor.derivePwEncKey(OLD_PASSWORD); - initialAuthKeyPair = mockToprfEncryptor.deriveAuthKeyPair(OLD_PASSWORD); - INITIAL_AUTH_PUB_KEY = bytesToBase64(initialAuthKeyPair.pk); - - const mockResult = await createMockVault( - initialEncKey, - initialPwEncKey, - initialAuthKeyPair, - OLD_PASSWORD, - ); - - MOCK_VAULT = mockResult.encryptedMockVault; - MOCK_VAULT_ENCRYPTION_KEY = mockResult.vaultEncryptionKey; - MOCK_VAULT_ENCRYPTION_SALT = mockResult.vaultEncryptionSalt; - }); - - it('should retry syncLatestGlobalPassword after refreshing expired tokens', async () => { - await withController( - { - state: getMockInitialControllerState({ - withMockAuthenticatedUser: true, - authPubKey: INITIAL_AUTH_PUB_KEY, - vault: MOCK_VAULT, - vaultEncryptionKey: MOCK_VAULT_ENCRYPTION_KEY, - vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, - }), - }, - async ({ - controller, - toprfClient, - encryptor, - mockRefreshJWTToken, - }) => { - // Unlock controller first - await controller.submitPassword(OLD_PASSWORD); - - const recoverEncKeySpy = jest.spyOn(toprfClient, 'recoverEncKey'); - const encryptorSpy = jest.spyOn(encryptor, 'encryptWithDetail'); - - // Mock recoverEncKey for the new global password - const mockToprfEncryptor = createMockToprfEncryptor(); - const newEncKey = mockToprfEncryptor.deriveEncKey(GLOBAL_PASSWORD); - const newPwEncKey = - mockToprfEncryptor.derivePwEncKey(GLOBAL_PASSWORD); - const newAuthKeyPair = - mockToprfEncryptor.deriveAuthKeyPair(GLOBAL_PASSWORD); - - // Mock recoverEncKey to fail first with token expired error, then succeed - recoverEncKeySpy - .mockImplementationOnce(() => { - throw new TOPRFError( - TOPRFErrorCode.AuthTokenExpired, - 'Auth token expired', - ); - }) - .mockResolvedValueOnce({ - encKey: newEncKey, - pwEncKey: newPwEncKey, - authKeyPair: newAuthKeyPair, - rateLimitResetResult: Promise.resolve(), - keyShareIndex: 1, - }); - - // Mock authenticate for token refresh - jest.spyOn(toprfClient, 'authenticate').mockResolvedValue({ - nodeAuthTokens: MOCK_NODE_AUTH_TOKENS, - isNewUser: false, - }); - - await controller.syncLatestGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }); - - // Verify that getNewRefreshToken was called - expect(mockRefreshJWTToken).toHaveBeenCalledWith({ - connection: controller.state.authConnection, - refreshToken: 'newRefreshToken', - }); - - // Verify that recoverEncKey was called twice (once failed, once succeeded) - expect(recoverEncKeySpy).toHaveBeenCalledTimes(2); - - // Verify that authenticate was called during token refresh - expect(toprfClient.authenticate).toHaveBeenCalled(); - - // Check if vault was re-encrypted with the new password and keys - const expectedSerializedVaultData = JSON.stringify({ - toprfEncryptionKey: bytesToBase64(newEncKey), - toprfPwEncryptionKey: bytesToBase64(newPwEncKey), - toprfAuthKeyPair: JSON.stringify({ - sk: bigIntToHex(newAuthKeyPair.sk), - pk: bytesToBase64(newAuthKeyPair.pk), - }), - revokeToken: controller.state.revokeToken, - accessToken: controller.state.accessToken, - }); - expect(encryptorSpy).toHaveBeenCalledWith( - GLOBAL_PASSWORD, - expectedSerializedVaultData, - ); - - // Check if authPubKey was updated in state - expect(controller.state.authPubKey).toBe( - bytesToBase64(newAuthKeyPair.pk), - ); - }, - ); - }); - - it('should fail if token refresh fails during syncLatestGlobalPassword', async () => { - await withController( - { - state: getMockInitialControllerState({ - withMockAuthenticatedUser: true, - authPubKey: INITIAL_AUTH_PUB_KEY, - vault: MOCK_VAULT, - vaultEncryptionKey: MOCK_VAULT_ENCRYPTION_KEY, - vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, - }), - }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { - // Unlock controller first - await controller.submitPassword(OLD_PASSWORD); - - // Mock recoverEncKey to fail with token expired error - jest - .spyOn(toprfClient, 'recoverEncKey') - .mockImplementationOnce(() => { - throw new TOPRFError( - TOPRFErrorCode.AuthTokenExpired, - 'Auth token expired', - ); - }); - - // Mock getNewRefreshToken to fail - mockRefreshJWTToken.mockRejectedValueOnce( - new Error('Failed to get new refresh token'), - ); - - await expect( - controller.syncLatestGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), - ).rejects.toThrow( - SeedlessOnboardingControllerErrorMessage.AuthenticationError, - ); - - // Verify that getNewRefreshToken was called - expect(mockRefreshJWTToken).toHaveBeenCalled(); - }, - ); - }); - - it('should not retry on non-token-related errors during syncLatestGlobalPassword', async () => { - await withController( - { - state: getMockInitialControllerState({ - withMockAuthenticatedUser: true, - authPubKey: INITIAL_AUTH_PUB_KEY, - vault: MOCK_VAULT, - vaultEncryptionKey: MOCK_VAULT_ENCRYPTION_KEY, - vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, - }), - }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { - // Unlock controller first - await controller.submitPassword(OLD_PASSWORD); - - // Mock recoverEncKey to fail with a non-token error - jest - .spyOn(toprfClient, 'recoverEncKey') - .mockRejectedValue(new Error('Some other error')); - - await expect( - controller.syncLatestGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), - ).rejects.toThrow( - SeedlessOnboardingControllerErrorMessage.LoginFailedError, - ); - - // Verify that getNewRefreshToken was NOT called - expect(mockRefreshJWTToken).not.toHaveBeenCalled(); - - // Verify that recoverEncKey was only called once (no retry) - expect(toprfClient.recoverEncKey).toHaveBeenCalledTimes(1); - }, - ); - }); - }); - describe('addNewSecretData with token refresh', () => { const NEW_KEY_RING = { id: 'new-keyring-1', @@ -4682,6 +4252,42 @@ describe('SeedlessOnboardingController', () => { ); }); }); + + describe('submitGlobalPassword with token refresh', () => { + it('should refresh auth tokens if recoverEncKey fails with token expired error', async () => { + await withController( + { + state: getMockInitialControllerState({ + withMockAuthenticatedUser: true, + authPubKey: MOCK_AUTH_PUB_KEY, + }), + }, + async ({ controller, toprfClient }) => { + // Mock recoverEncKey to fail with token expired error + jest + .spyOn(toprfClient, 'recoverEncKey') + .mockRejectedValueOnce( + new TOPRFError( + TOPRFErrorCode.AuthTokenExpired, + 'Invalid auth token', + ), + ); + + const spiedRefreshAuthTokens = jest + .spyOn(controller, 'refreshAuthTokens') + .mockResolvedValue(); + + await expect( + controller.submitGlobalPassword({ + globalPassword: MOCK_PASSWORD, + }), + ).rejects.toThrow(Error); + + expect(spiedRefreshAuthTokens).toHaveBeenCalled(); + }, + ); + }); + }); }); describe('fetchMetadataAccessCreds', () => { diff --git a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts index 3ff84cf04bc..999321b9734 100644 --- a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts +++ b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts @@ -682,43 +682,6 @@ export class SeedlessOnboardingController extends BaseController< this.#isUnlocked = false; } - /** - * Sync the latest global password to the controller. - * reset vault with latest globalPassword, - * persist the latest global password authPubKey - * - * @param params - The parameters for syncing the latest global password. - * @param params.globalPassword - The latest global password. - * @returns A promise that resolves to the success of the operation. - */ - async syncLatestGlobalPassword({ - globalPassword, - }: { - globalPassword: string; - }) { - return await this.#withControllerLock(async () => { - this.#assertIsUnlocked(); - const doSyncPassword = async () => { - // update vault with latest globalPassword - const { encKey, pwEncKey, authKeyPair } = - await this.#recoverEncKey(globalPassword); - // update and encrypt the vault with new password - await this.#createNewVaultWithAuthData({ - password: globalPassword, - rawToprfEncryptionKey: encKey, - rawToprfPwEncryptionKey: pwEncKey, - rawToprfAuthKeyPair: authKeyPair, - }); - - this.#resetPasswordOutdatedCache(); - }; - return await this.#executeWithTokenRefresh( - doSyncPassword, - 'syncLatestGlobalPassword', - ); - }); - } - /** * @description Unlock the controller with the latest global password. * @@ -767,40 +730,43 @@ export class SeedlessOnboardingController extends BaseController< globalPassword: string; maxKeyChainLength: number; }): Promise { - const { pwEncKey: curPwEncKey, authKeyPair: curAuthKeyPair } = - await this.#recoverEncKey(globalPassword); + const { + pwEncKey: latestPwEncKey, + authKeyPair: latestAuthKeyPair, + encKey: latestEncKey, + } = await this.#recoverEncKey(globalPassword); try { // Recover vault encryption key. - const res = await this.toprfClient.recoverPwEncKey({ + const { pwEncKey } = await this.toprfClient.recoverPwEncKey({ targetAuthPubKey, - curPwEncKey, - curAuthKeyPair, + curPwEncKey: latestPwEncKey, + curAuthKeyPair: latestAuthKeyPair, maxPwChainLength: maxKeyChainLength, }); - const { pwEncKey } = res; const vaultKey = await this.#loadSeedlessEncryptionKey(pwEncKey); + const keyringEncryptionKey = + await this.#loadKeyringEncryptionKey(pwEncKey); // Unlock the controller - const { - revokeToken, - toprfEncryptionKey, - toprfPwEncryptionKey, - toprfAuthKeyPair, - } = await this.#unlockVaultAndGetVaultData(undefined, vaultKey); + const { revokeToken } = await this.#unlockVaultAndGetVaultData( + undefined, + vaultKey, + ); this.#setUnlocked(); if (revokeToken) { // revoke and recyle refresh token after unlock to keep refresh token fresh, avoid malicious use of leaked refresh token await this.#revokeRefreshTokenAndUpdateState(revokeToken); - // re-creating vault to persist the new revoke token - await this.#createNewVaultWithAuthData({ - password: globalPassword, - rawToprfEncryptionKey: toprfEncryptionKey, - rawToprfPwEncryptionKey: toprfPwEncryptionKey, - rawToprfAuthKeyPair: toprfAuthKeyPair, - }); } + // re-creating vault to persist the new revoke token + await this.#createNewVaultWithAuthData({ + password: globalPassword, + rawToprfEncryptionKey: latestEncKey, + rawToprfPwEncryptionKey: latestPwEncKey, + rawToprfAuthKeyPair: latestAuthKeyPair, + }); + await this.storeKeyringEncryptionKey(keyringEncryptionKey); } catch (error) { if (this.#isTokenExpiredError(error)) { throw error; From 3a4396168f8d204d57463f37495d058666bac41b Mon Sep 17 00:00:00 2001 From: lwin Date: Thu, 24 Jul 2025 18:11:16 +0800 Subject: [PATCH 2/7] fix: fixed lint --- .../src/SeedlessOnboardingController.test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts index bbfa0b053e9..0c8d8890398 100644 --- a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts +++ b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts @@ -19,12 +19,7 @@ import { type ToprfSecureBackup, TOPRFErrorCode, } from '@metamask/toprf-secure-backup'; -import { - base64ToBytes, - bytesToBase64, - stringToBytes, - bigIntToHex, -} from '@metamask/utils'; +import { base64ToBytes, bytesToBase64, stringToBytes } from '@metamask/utils'; import { gcm } from '@noble/ciphers/aes'; import { utf8ToBytes } from '@noble/ciphers/utils'; import { managedNonce } from '@noble/ciphers/webcrypto'; From 4aa55b888aad96da5e16f7121b9b7af012e25292 Mon Sep 17 00:00:00 2001 From: lwin Date: Thu, 24 Jul 2025 18:41:05 +0800 Subject: [PATCH 3/7] chore: updated ChangeLog --- packages/seedless-onboarding-controller/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/seedless-onboarding-controller/CHANGELOG.md b/packages/seedless-onboarding-controller/CHANGELOG.md index bd495f846e6..885d7ca2d1c 100644 --- a/packages/seedless-onboarding-controller/CHANGELOG.md +++ b/packages/seedless-onboarding-controller/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added an optional parameter, `passwordOutdatedCacheTTL` to the constructor params and exported `SecretMetadata` class from the controller.([#6169](https://github.com/MetaMask/core/pull/6169)) +### Removed + +- Removed `syncLatestGlobalPassword` from the Controller. ([#6180](https://github.com/MetaMask/core/pull/6180)) + - Moved the vault syncing to the `submitGlobalPassword` method + ## [2.4.0] ### Fixed From 80740171ec806006d58200b8f99593d91753461f Mon Sep 17 00:00:00 2001 From: lwin Date: Thu, 24 Jul 2025 19:40:15 +0800 Subject: [PATCH 4/7] chore: add some comments --- .../src/SeedlessOnboardingController.ts | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts index f936ac2d423..779ac6f5be9 100644 --- a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts +++ b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts @@ -739,18 +739,21 @@ export class SeedlessOnboardingController extends BaseController< } = await this.#recoverEncKey(globalPassword); try { - // Recover vault encryption key. - const { pwEncKey } = await this.toprfClient.recoverPwEncKey({ - targetAuthPubKey, - curPwEncKey: latestPwEncKey, - curAuthKeyPair: latestAuthKeyPair, - maxPwChainLength: maxKeyChainLength, - }); - const vaultKey = await this.#loadSeedlessEncryptionKey(pwEncKey); - const keyringEncryptionKey = - await this.#loadKeyringEncryptionKey(pwEncKey); + // Recover current device's vault encryption key with the latest global password + const { pwEncKey: currentDevicePwEncKey } = + await this.toprfClient.recoverPwEncKey({ + targetAuthPubKey, + curPwEncKey: latestPwEncKey, + curAuthKeyPair: latestAuthKeyPair, + maxPwChainLength: maxKeyChainLength, + }); + // recover the vault encryption key and keyring encryption key with the current device's pwEncKey + const [vaultKey, keyringEncryptionKey] = await Promise.all([ + this.#loadSeedlessEncryptionKey(currentDevicePwEncKey), + this.#loadKeyringEncryptionKey(currentDevicePwEncKey), + ]); - // Unlock the controller + // Unlock the controller and vault const { revokeToken } = await this.#unlockVaultAndGetVaultData( undefined, vaultKey, @@ -768,6 +771,8 @@ export class SeedlessOnboardingController extends BaseController< rawToprfPwEncryptionKey: latestPwEncKey, rawToprfAuthKeyPair: latestAuthKeyPair, }); + + // restore the current keyring encryption key with the new global password await this.storeKeyringEncryptionKey(keyringEncryptionKey); } catch (error) { if (this.#isTokenExpiredError(error)) { From 0a1c527a6df089071b6cd4c462fcd519f41a0d1d Mon Sep 17 00:00:00 2001 From: lwin Date: Thu, 24 Jul 2025 20:00:36 +0800 Subject: [PATCH 5/7] feat: reset password outdated cache after sucessful submitGlobalPassword --- .../src/SeedlessOnboardingController.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts index 779ac6f5be9..a5c3916743a 100644 --- a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts +++ b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts @@ -774,6 +774,9 @@ export class SeedlessOnboardingController extends BaseController< // restore the current keyring encryption key with the new global password await this.storeKeyringEncryptionKey(keyringEncryptionKey); + + // reset the password outdated cache after successful global password submission and state sync + this.#resetPasswordOutdatedCache(); } catch (error) { if (this.#isTokenExpiredError(error)) { throw error; From 415e7d8a5ad8ce192641d6a080e2be1e2c3888b5 Mon Sep 17 00:00:00 2001 From: lwin Date: Thu, 24 Jul 2025 20:19:27 +0800 Subject: [PATCH 6/7] chore: renamed `submitGlobalPassword` method to `submitGlobalPasswordAndSync` --- .../CHANGELOG.md | 4 ++++ .../src/SeedlessOnboardingController.test.ts | 20 +++++++++---------- .../src/SeedlessOnboardingController.ts | 8 ++++---- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/seedless-onboarding-controller/CHANGELOG.md b/packages/seedless-onboarding-controller/CHANGELOG.md index 885d7ca2d1c..18fb59c824e 100644 --- a/packages/seedless-onboarding-controller/CHANGELOG.md +++ b/packages/seedless-onboarding-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Renamed public method `submitGlobalPassword` to `submitGlobalPasswordAndSync`. ([#6180](https://github.com/MetaMask/core/pull/6180)) + ### Added - Added an optional parameter, `passwordOutdatedCacheTTL` to the constructor params and exported `SecretMetadata` class from the controller.([#6169](https://github.com/MetaMask/core/pull/6169)) diff --git a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts index 0c8d8890398..2108de18e1b 100644 --- a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts +++ b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts @@ -3316,7 +3316,7 @@ describe('SeedlessOnboardingController', () => { controller.setLocked(); - await controller.submitGlobalPassword({ + await controller.submitGlobalPasswordAndSync({ globalPassword: GLOBAL_PASSWORD, }); @@ -3484,7 +3484,7 @@ describe('SeedlessOnboardingController', () => { controller.setLocked(); - await controller.submitGlobalPassword({ + await controller.submitGlobalPasswordAndSync({ globalPassword: GLOBAL_PASSWORD, }); @@ -3529,7 +3529,7 @@ describe('SeedlessOnboardingController', () => { }); await expect( - controller.submitGlobalPassword({ + controller.submitGlobalPasswordAndSync({ globalPassword: GLOBAL_PASSWORD, }), ).rejects.toThrow( @@ -3546,7 +3546,7 @@ describe('SeedlessOnboardingController', () => { }, async ({ controller }) => { await expect( - controller.submitGlobalPassword({ + controller.submitGlobalPasswordAndSync({ globalPassword: GLOBAL_PASSWORD, }), ).rejects.toThrow( @@ -3575,7 +3575,7 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.submitGlobalPassword({ + controller.submitGlobalPasswordAndSync({ globalPassword: GLOBAL_PASSWORD, }), ).rejects.toStrictEqual( @@ -3619,7 +3619,7 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.submitGlobalPassword({ + controller.submitGlobalPasswordAndSync({ globalPassword: GLOBAL_PASSWORD, }), ).rejects.toStrictEqual( @@ -3658,7 +3658,7 @@ describe('SeedlessOnboardingController', () => { .mockRejectedValueOnce(new Error('Unknown error')); await expect( - controller.submitGlobalPassword({ + controller.submitGlobalPasswordAndSync({ globalPassword: GLOBAL_PASSWORD, }), ).rejects.toStrictEqual( @@ -3705,7 +3705,7 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.submitGlobalPassword({ + controller.submitGlobalPasswordAndSync({ globalPassword: GLOBAL_PASSWORD, }), ).rejects.toThrow( @@ -4296,7 +4296,7 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.submitGlobalPassword({ + await controller.submitGlobalPasswordAndSync({ globalPassword: MOCK_PASSWORD, }); @@ -4465,7 +4465,7 @@ describe('SeedlessOnboardingController', () => { .mockResolvedValue(); await expect( - controller.submitGlobalPassword({ + controller.submitGlobalPasswordAndSync({ globalPassword: MOCK_PASSWORD, }), ).rejects.toThrow(Error); diff --git a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts index a5c3916743a..794d0773a22 100644 --- a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts +++ b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts @@ -692,7 +692,7 @@ export class SeedlessOnboardingController extends BaseController< * @param params.globalPassword - The latest global password. * @returns A promise that resolves to the success of the operation. */ - async submitGlobalPassword({ + async submitGlobalPasswordAndSync({ globalPassword, maxKeyChainLength = 5, }: { @@ -702,12 +702,12 @@ export class SeedlessOnboardingController extends BaseController< return await this.#withControllerLock(async () => { return await this.#executeWithTokenRefresh(async () => { const currentDeviceAuthPubKey = this.#recoverAuthPubKey(); - await this.#submitGlobalPassword({ + await this.#submitGlobalPasswordAndSync({ targetAuthPubKey: currentDeviceAuthPubKey, globalPassword, maxKeyChainLength, }); - }, 'submitGlobalPassword'); + }, 'submitGlobalPasswordAndSync'); }); } @@ -723,7 +723,7 @@ export class SeedlessOnboardingController extends BaseController< * @returns A promise that resolves to the keyring encryption key * corresponding to the current authPubKey in state. */ - async #submitGlobalPassword({ + async #submitGlobalPasswordAndSync({ targetAuthPubKey, globalPassword, maxKeyChainLength, From a0e6b4825abcc24951a25e84e0b9dd6f05c16c78 Mon Sep 17 00:00:00 2001 From: lwin Date: Thu, 24 Jul 2025 20:27:06 +0800 Subject: [PATCH 7/7] fix: fixed ChangeLog --- packages/seedless-onboarding-controller/CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/seedless-onboarding-controller/CHANGELOG.md b/packages/seedless-onboarding-controller/CHANGELOG.md index 18fb59c824e..c28a50eb50a 100644 --- a/packages/seedless-onboarding-controller/CHANGELOG.md +++ b/packages/seedless-onboarding-controller/CHANGELOG.md @@ -7,14 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Changed - -- Renamed public method `submitGlobalPassword` to `submitGlobalPasswordAndSync`. ([#6180](https://github.com/MetaMask/core/pull/6180)) - ### Added - Added an optional parameter, `passwordOutdatedCacheTTL` to the constructor params and exported `SecretMetadata` class from the controller.([#6169](https://github.com/MetaMask/core/pull/6169)) +### Changed + +- Renamed public method `submitGlobalPassword` to `submitGlobalPasswordAndSync`. ([#6180](https://github.com/MetaMask/core/pull/6180)) + ### Removed - Removed `syncLatestGlobalPassword` from the Controller. ([#6180](https://github.com/MetaMask/core/pull/6180))