mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
[PM-5537] Biometric State Service (#7761)
* Create state for biometric client key halves * Move enc string util to central utils * Provide biometric state through service * Use biometric state to track client key half * Create migration for client key half * Ensure client key half is removed on logout * Remove account data for client key half * Remove unnecessary key definition likes * Remove moved state from account * Fix null-conditional operator failure * Simplify migration * Remove lame test * Fix test type * Add migrator * Prefer userKey when legacy not needed * Fix tests
This commit is contained in:
@@ -9,6 +9,7 @@ import { EverHadUserKeyMigrator } from "./migrations/10-move-ever-had-user-key-t
|
||||
import { OrganizationKeyMigrator } from "./migrations/11-move-org-keys-to-state-providers";
|
||||
import { MoveEnvironmentStateToProviders } from "./migrations/12-move-environment-state-to-providers";
|
||||
import { ProviderKeyMigrator } from "./migrations/13-move-provider-keys-to-state-providers";
|
||||
import { MoveBiometricClientKeyHalfToStateProviders } from "./migrations/14-move-biometric-client-key-half-state-to-providers";
|
||||
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";
|
||||
@@ -19,7 +20,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
||||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 2;
|
||||
export const CURRENT_VERSION = 13;
|
||||
export const CURRENT_VERSION = 14;
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export async function migrate(
|
||||
@@ -36,7 +37,6 @@ export async function migrate(
|
||||
await storageService.save("stateVersion", CURRENT_VERSION);
|
||||
return;
|
||||
}
|
||||
|
||||
await MigrationBuilder.create()
|
||||
.with(MinVersionMigrator)
|
||||
.with(FixPremiumMigrator, 2, 3)
|
||||
@@ -49,7 +49,8 @@ export async function migrate(
|
||||
.with(EverHadUserKeyMigrator, 9, 10)
|
||||
.with(OrganizationKeyMigrator, 10, 11)
|
||||
.with(MoveEnvironmentStateToProviders, 11, 12)
|
||||
.with(ProviderKeyMigrator, 12, CURRENT_VERSION)
|
||||
.with(ProviderKeyMigrator, 12, 13)
|
||||
.with(MoveBiometricClientKeyHalfToStateProviders, 13, CURRENT_VERSION)
|
||||
|
||||
.migrate(migrationHelper);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
MoveBiometricClientKeyHalfToStateProviders,
|
||||
CLIENT_KEY_HALF,
|
||||
} from "./14-move-biometric-client-key-half-state-to-providers";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
keys: {
|
||||
biometricEncryptionClientKeyHalf: "user1-key-half",
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
keys: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_biometricSettings_clientKeyHalf": "user1-key-half",
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
keys: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
keys: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("DesktopBiometricState migrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: MoveBiometricClientKeyHalfToStateProviders;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 13);
|
||||
sut = new MoveBiometricClientKeyHalfToStateProviders(13, 14);
|
||||
});
|
||||
|
||||
it("should remove biometricEncryptionClientKeyHalf from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
keys: {
|
||||
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", CLIENT_KEY_HALF, "user1-key-half");
|
||||
});
|
||||
|
||||
it("should not call extra setToUser", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 14);
|
||||
sut = new MoveBiometricClientKeyHalfToStateProviders(13, 14);
|
||||
});
|
||||
|
||||
it("should null out new values", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", CLIENT_KEY_HALF, 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", {
|
||||
keys: {
|
||||
biometricEncryptionClientKeyHalf: "user1-key-half",
|
||||
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());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
settings?: {
|
||||
disableAutoBiometricsPrompt?: boolean;
|
||||
biometricUnlock?: boolean;
|
||||
dismissedBiometricRequirePasswordOnStartCallout?: boolean;
|
||||
};
|
||||
keys?: { biometricEncryptionClientKeyHalf?: string };
|
||||
};
|
||||
|
||||
// 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 CLIENT_KEY_HALF: KeyDefinitionLike = {
|
||||
key: "clientKeyHalf",
|
||||
stateDefinition: { name: "biometricSettings" },
|
||||
};
|
||||
|
||||
export class MoveBiometricClientKeyHalfToStateProviders extends Migrator<13, 14> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyAccounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(
|
||||
legacyAccounts.map(async ({ userId, account }) => {
|
||||
// Move account data
|
||||
if (account?.keys?.biometricEncryptionClientKeyHalf != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
CLIENT_KEY_HALF,
|
||||
account.keys.biometricEncryptionClientKeyHalf,
|
||||
);
|
||||
|
||||
// Delete old account data
|
||||
delete account?.keys?.biometricEncryptionClientKeyHalf;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
async function rollbackUser(userId: string, account: ExpectedAccountType) {
|
||||
let updatedAccount = false;
|
||||
|
||||
const userKeyHalf = await helper.getFromUser<string>(userId, CLIENT_KEY_HALF);
|
||||
|
||||
if (userKeyHalf) {
|
||||
account ??= {};
|
||||
account.keys ??= {};
|
||||
|
||||
updatedAccount = true;
|
||||
account.keys.biometricEncryptionClientKeyHalf = userKeyHalf;
|
||||
await helper.setToUser(userId, CLIENT_KEY_HALF, null);
|
||||
}
|
||||
|
||||
if (updatedAccount) {
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user