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:
@@ -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(
|
||||||
|
|||||||
@@ -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