From 2d34a19b23dc1497736ee6f1404b55a5c0844912 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:41:38 -0500 Subject: [PATCH] [PM-25287] Add AddMasterPasswordUnlockData state migration (#16202) * Add AddMasterPasswordUnlockData state migration --- libs/state/src/state-migrations/migrate.ts | 6 +- ...73-add-master-password-unlock-data.spec.ts | 155 ++++++++++++++++++ .../73-add-master-password-unlock-data.ts | 72 ++++++++ 3 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.spec.ts create mode 100644 libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.ts diff --git a/libs/state/src/state-migrations/migrate.ts b/libs/state/src/state-migrations/migrate.ts index 620c2d3bb19..bf4cd17adba 100644 --- a/libs/state/src/state-migrations/migrate.ts +++ b/libs/state/src/state-migrations/migrate.ts @@ -69,12 +69,13 @@ import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric- import { RemoveAcBannersDismissed } from "./migrations/70-remove-ac-banner-dismissed"; import { RemoveNewCustomizationOptionsCalloutDismissed } from "./migrations/71-remove-new-customization-options-callout-dismissed"; import { RemoveAccountDeprovisioningBannerDismissed } from "./migrations/72-remove-account-deprovisioning-banner-dismissed"; +import { AddMasterPasswordUnlockData } from "./migrations/73-add-master-password-unlock-data"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 72; +export const CURRENT_VERSION = 73; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -148,7 +149,8 @@ export function createMigrationBuilder() { .with(MigrateIncorrectFolderKey, 68, 69) .with(RemoveAcBannersDismissed, 69, 70) .with(RemoveNewCustomizationOptionsCalloutDismissed, 70, 71) - .with(RemoveAccountDeprovisioningBannerDismissed, 71, CURRENT_VERSION); + .with(RemoveAccountDeprovisioningBannerDismissed, 71, 72) + .with(AddMasterPasswordUnlockData, 72, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.spec.ts b/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.spec.ts new file mode 100644 index 00000000000..28e65216653 --- /dev/null +++ b/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.spec.ts @@ -0,0 +1,155 @@ +import { runMigrator } from "../migration-helper.spec"; + +import { AddMasterPasswordUnlockData } from "./73-add-master-password-unlock-data"; + +describe("AddMasterPasswordUnlockData", () => { + const sut = new AddMasterPasswordUnlockData(72, 73); + + describe("migrate", () => { + it("updates users that don't have master password unlock data", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + email: "user1@email.Com", + name: "User 1", + }, + user2: { + email: "user2@email.com", + name: "User 2", + }, + }, + user_user1_masterPassword_masterKeyEncryptedUserKey: "user1MasterKeyEncryptedUser", + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + user_user2_masterPassword_masterKeyEncryptedUserKey: "user2MasterKeyEncryptedUser", + user_user2_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600001 }, + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.Com", + name: "User 1", + }, + user2: { + email: "user2@email.com", + name: "User 2", + }, + }, + user_user1_masterPassword_masterKeyEncryptedUserKey: "user1MasterKeyEncryptedUser", + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + user_user1_masterPasswordUnlock_masterPasswordUnlockKey: { + salt: "user1@email.com", + kdf: { kdfType: 0, iterations: 600000 }, + masterKeyWrappedUserKey: "user1MasterKeyEncryptedUser", + }, + user_user2_masterPassword_masterKeyEncryptedUserKey: "user2MasterKeyEncryptedUser", + user_user2_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600001 }, + user_user2_masterPasswordUnlock_masterPasswordUnlockKey: { + salt: "user2@email.com", + kdf: { kdfType: 0, iterations: 600001 }, + masterKeyWrappedUserKey: "user2MasterKeyEncryptedUser", + }, + }); + }); + + it("does not update users that already have master password unlock data", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + email: "user1@email.Com", + name: "User 1", + }, + }, + user_user1_masterPassword_masterKeyEncryptedUserKey: "user1MasterKeyEncryptedUser", + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + user_user1_masterPasswordUnlock_masterPasswordUnlockKey: { someData: "data" }, + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.Com", + name: "User 1", + }, + }, + user_user1_masterPassword_masterKeyEncryptedUserKey: "user1MasterKeyEncryptedUser", + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + user_user1_masterPasswordUnlock_masterPasswordUnlockKey: { someData: "data" }, + }); + }); + + it("does not update users that have missing data required to construct master password unlock data", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + name: "User 1", + }, + }, + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + name: "User 1", + }, + }, + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + }); + }); + }); + + describe("rollback", () => { + it("rolls back data", async () => { + const output = await runMigrator( + sut, + { + global_account_accounts: { + user1: { + email: "user1@email.Com", + name: "User 1", + }, + user2: { + email: "user2@email.com", + name: "User 2", + }, + user3: { + email: "user3@email.com", + name: "User 3", + }, + }, + user_user1_masterPassword_masterKeyEncryptedUserKey: "user1MasterKeyEncryptedUser", + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + user_user2_masterPassword_masterKeyEncryptedUserKey: "user2MasterKeyEncryptedUser", + user_user2_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600001 }, + user_user1_masterPasswordUnlock_masterPasswordUnlockKey: "fakeData", + user_user2_masterPasswordUnlock_masterPasswordUnlockKey: "fakeData", + user_user3_masterPasswordUnlock_masterPasswordUnlockKey: null, + }, + "rollback", + ); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.Com", + name: "User 1", + }, + user2: { + email: "user2@email.com", + name: "User 2", + }, + user3: { + email: "user3@email.com", + name: "User 3", + }, + }, + user_user1_masterPassword_masterKeyEncryptedUserKey: "user1MasterKeyEncryptedUser", + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + user_user2_masterPassword_masterKeyEncryptedUserKey: "user2MasterKeyEncryptedUser", + user_user2_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600001 }, + user_user3_masterPasswordUnlock_masterPasswordUnlockKey: null, + }); + }); + }); +}); diff --git a/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.ts b/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.ts new file mode 100644 index 00000000000..b9833f439a6 --- /dev/null +++ b/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.ts @@ -0,0 +1,72 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +export const ACCOUNT_ACCOUNTS: KeyDefinitionLike = { + stateDefinition: { + name: "account", + }, + key: "accounts", +}; + +export const MASTER_PASSWORD_UNLOCK_KEY: KeyDefinitionLike = { + key: "masterPasswordUnlockKey", + stateDefinition: { name: "masterPasswordUnlock" }, +}; + +export const MASTER_KEY_ENCRYPTED_USER_KEY: KeyDefinitionLike = { + key: "masterKeyEncryptedUserKey", + stateDefinition: { name: "masterPassword" }, +}; + +export const KDF_CONFIG_DISK: KeyDefinitionLike = { + key: "kdfConfig", + stateDefinition: { name: "kdfConfig" }, +}; + +type AccountsMap = Record; +type Account = { + email: string; + name: string; +}; + +export class AddMasterPasswordUnlockData extends Migrator<72, 73> { + async migrate(helper: MigrationHelper): Promise { + async function migrateAccount(userId: string, account: Account) { + const email = account.email; + const kdfConfig = await helper.getFromUser(userId, KDF_CONFIG_DISK); + const masterKeyEncryptedUserKey = await helper.getFromUser( + userId, + MASTER_KEY_ENCRYPTED_USER_KEY, + ); + if ( + (await helper.getFromUser(userId, MASTER_PASSWORD_UNLOCK_KEY)) == null && + email != null && + kdfConfig != null && + masterKeyEncryptedUserKey != null + ) { + await helper.setToUser(userId, MASTER_PASSWORD_UNLOCK_KEY, { + salt: email.trim().toLowerCase(), + kdf: kdfConfig, + masterKeyWrappedUserKey: masterKeyEncryptedUserKey, + }); + } + } + + const accountDictionary = await helper.getFromGlobal(ACCOUNT_ACCOUNTS); + const accounts = await helper.getAccounts(); + await Promise.all( + accounts.map(({ userId }) => migrateAccount(userId, accountDictionary[userId])), + ); + } + + async rollback(helper: MigrationHelper): Promise { + async function rollbackAccount(userId: string) { + if ((await helper.getFromUser(userId, MASTER_PASSWORD_UNLOCK_KEY)) != null) { + await helper.removeFromUser(userId, MASTER_PASSWORD_UNLOCK_KEY); + } + } + + const accounts = await helper.getAccounts(); + await Promise.all(accounts.map(({ userId }) => rollbackAccount(userId))); + } +}