mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
[PM-25287] Add AddMasterPasswordUnlockData state migration (#16202)
* Add AddMasterPasswordUnlockData state migration
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user