From 7a6d7b3a68a61bd36685ad60abf6f15359c00b3e Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 14 Feb 2024 14:25:08 -0500 Subject: [PATCH] Include missing migration (#7840) This migration missing from #7825 does not suggest missing data since no client has been released in the interim. --- libs/common/src/state-migrations/migrate.ts | 6 +- ...-migrate-require-password-on-start.spec.ts | 123 ++++++++++++++++++ .../19-migrate-require-password-on-start.ts | 56 ++++++++ 3 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 libs/common/src/state-migrations/migrations/19-migrate-require-password-on-start.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/19-migrate-require-password-on-start.ts diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 376aec0026..f43b64ab6d 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -13,6 +13,7 @@ import { FolderMigrator } from "./migrations/15-move-folder-state-to-state-provi import { LastSyncMigrator } from "./migrations/16-move-last-sync-to-state-provider"; import { EnablePasskeysMigrator } from "./migrations/17-move-enable-passkeys-to-state-providers"; import { AutofillSettingsKeyMigrator } from "./migrations/18-move-autofill-settings-to-state-providers"; +import { RequirePasswordOnStartMigrator } from "./migrations/19-migrate-require-password-on-start"; import { FixPremiumMigrator } from "./migrations/3-fix-premium"; import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; @@ -23,7 +24,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 2; -export const CURRENT_VERSION = 18; +export const CURRENT_VERSION = 19; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -44,7 +45,8 @@ export function createMigrationBuilder() { .with(FolderMigrator, 14, 15) .with(LastSyncMigrator, 15, 16) .with(EnablePasskeysMigrator, 16, 17) - .with(AutofillSettingsKeyMigrator, 17, CURRENT_VERSION); + .with(AutofillSettingsKeyMigrator, 17, 18) + .with(RequirePasswordOnStartMigrator, 18, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/19-migrate-require-password-on-start.spec.ts b/libs/common/src/state-migrations/migrations/19-migrate-require-password-on-start.spec.ts new file mode 100644 index 0000000000..2688e8e9a6 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/19-migrate-require-password-on-start.spec.ts @@ -0,0 +1,123 @@ +import { MockProxy, any } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { + REQUIRE_PASSWORD_ON_START, + RequirePasswordOnStartMigrator, +} from "./19-migrate-require-password-on-start"; + +function exampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user-1", "user-2", "user-3"], + "user-1": { + settings: { + requirePasswordOnStart: true, + otherStuff: "overStuff2", + }, + otherStuff: "otherStuff3", + }, + "user-2": { + keys: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +function rollbackJSON() { + return { + "user_user-1_biometricSettings_requirePasswordOnStart": true, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user-1", "user-2", "user-3"], + "user-1": { + settings: { + otherStuff: "overStuff2", + }, + otherStuff: "otherStuff3", + }, + "user-2": { + keys: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +describe("DesktopBiometricState migrator", () => { + let helper: MockProxy; + let sut: RequirePasswordOnStartMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 18); + sut = new RequirePasswordOnStartMigrator(18, 19); + }); + + it("should remove biometricEncryptionClientKeyHalf from all accounts", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledTimes(1); + expect(helper.set).toHaveBeenCalledWith("user-1", { + settings: { + otherStuff: "overStuff2", + }, + otherStuff: "otherStuff3", + }); + }); + + it("should set biometricEncryptionClientKeyHalf value for account that have it", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user-1", REQUIRE_PASSWORD_ON_START, true); + }); + + it("should not call extra setToUser", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledTimes(1); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 19); + sut = new RequirePasswordOnStartMigrator(18, 19); + }); + + it("should null out new values", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user-1", REQUIRE_PASSWORD_ON_START, null); + }); + + it("should add explicit value back to accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledTimes(1); + expect(helper.set).toHaveBeenCalledWith("user-1", { + settings: { + requirePasswordOnStart: true, + otherStuff: "overStuff2", + }, + otherStuff: "otherStuff3", + }); + }); + + it.each(["user-2", "user-3"])( + "should not try to restore values to missing accounts", + async (userId) => { + await sut.rollback(helper); + + expect(helper.set).not.toHaveBeenCalledWith(userId, any()); + }, + ); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/19-migrate-require-password-on-start.ts b/libs/common/src/state-migrations/migrations/19-migrate-require-password-on-start.ts new file mode 100644 index 0000000000..2211714f9c --- /dev/null +++ b/libs/common/src/state-migrations/migrations/19-migrate-require-password-on-start.ts @@ -0,0 +1,56 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedAccountType = { + settings?: { + requirePasswordOnStart?: boolean; + }; +}; + +// Biometric text, no auto prompt text, fingerprint validated, and prompt cancelled are refreshed on every app start, so we don't need to migrate them +export const REQUIRE_PASSWORD_ON_START: KeyDefinitionLike = { + key: "requirePasswordOnStart", + stateDefinition: { name: "biometricSettings" }, +}; + +export class RequirePasswordOnStartMigrator extends Migrator<18, 19> { + async migrate(helper: MigrationHelper): Promise { + const legacyAccounts = await helper.getAccounts(); + + await Promise.all( + legacyAccounts.map(async ({ userId, account }) => { + // Move account data + if (account?.settings?.requirePasswordOnStart != null) { + await helper.setToUser( + userId, + REQUIRE_PASSWORD_ON_START, + account.settings.requirePasswordOnStart, + ); + + // Delete old account data + delete account.settings.requirePasswordOnStart; + await helper.set(userId, account); + } + }), + ); + } + + async rollback(helper: MigrationHelper): Promise { + async function rollbackUser(userId: string, account: ExpectedAccountType) { + const requirePassword = await helper.getFromUser(userId, REQUIRE_PASSWORD_ON_START); + + if (requirePassword) { + account ??= {}; + account.settings ??= {}; + + account.settings.requirePasswordOnStart = requirePassword; + await helper.setToUser(userId, REQUIRE_PASSWORD_ON_START, null); + await helper.set(userId, account); + } + } + + const accounts = await helper.getAccounts(); + + await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account))); + } +}