1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

[PM-25287] Add AddMasterPasswordUnlockData state migration (#16202)

* Add AddMasterPasswordUnlockData state migration
This commit is contained in:
Thomas Avery
2025-10-23 13:41:38 -05:00
committed by GitHub
parent 9b23b2d1b0
commit 2d34a19b23
3 changed files with 231 additions and 2 deletions

View File

@@ -69,12 +69,13 @@ import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-
import { RemoveAcBannersDismissed } from "./migrations/70-remove-ac-banner-dismissed"; import { RemoveAcBannersDismissed } from "./migrations/70-remove-ac-banner-dismissed";
import { RemoveNewCustomizationOptionsCalloutDismissed } from "./migrations/71-remove-new-customization-options-callout-dismissed"; import { RemoveNewCustomizationOptionsCalloutDismissed } from "./migrations/71-remove-new-customization-options-callout-dismissed";
import { RemoveAccountDeprovisioningBannerDismissed } from "./migrations/72-remove-account-deprovisioning-banner-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 { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
import { MinVersionMigrator } from "./migrations/min-version"; import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3; export const MIN_VERSION = 3;
export const CURRENT_VERSION = 72; export const CURRENT_VERSION = 73;
export type MinVersion = typeof MIN_VERSION; export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() { export function createMigrationBuilder() {
@@ -148,7 +149,8 @@ export function createMigrationBuilder() {
.with(MigrateIncorrectFolderKey, 68, 69) .with(MigrateIncorrectFolderKey, 68, 69)
.with(RemoveAcBannersDismissed, 69, 70) .with(RemoveAcBannersDismissed, 69, 70)
.with(RemoveNewCustomizationOptionsCalloutDismissed, 70, 71) .with(RemoveNewCustomizationOptionsCalloutDismissed, 70, 71)
.with(RemoveAccountDeprovisioningBannerDismissed, 71, CURRENT_VERSION); .with(RemoveAccountDeprovisioningBannerDismissed, 71, 72)
.with(AddMasterPasswordUnlockData, 72, CURRENT_VERSION);
} }
export async function currentVersion( export async function currentVersion(

View File

@@ -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,
});
});
});
});

View File

@@ -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<string, Account>;
type Account = {
email: string;
name: string;
};
export class AddMasterPasswordUnlockData extends Migrator<72, 73> {
async migrate(helper: MigrationHelper): Promise<void> {
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<AccountsMap>(ACCOUNT_ACCOUNTS);
const accounts = await helper.getAccounts();
await Promise.all(
accounts.map(({ userId }) => migrateAccount(userId, accountDictionary[userId])),
);
}
async rollback(helper: MigrationHelper): Promise<void> {
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)));
}
}