1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 23:33:31 +00:00

[PM-5537] Remove Unecessary Biometric State (#7762)

* 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

* Remove state that is never read.

* Remove unnecessary biometric state

We don't need to determine platform in desktop background, it can be done in the UI at any time.

* Fix merge

* Use platform utils to identify OS desktop type
This commit is contained in:
Matt Gibson
2024-02-15 15:29:29 -05:00
committed by GitHub
parent fae12cd0e6
commit c8c1ed42ba
14 changed files with 36 additions and 109 deletions

View File

@@ -296,8 +296,6 @@ export class NativeMessagingBackground {
switch (message.command) { switch (message.command) {
case "biometricUnlock": { case "biometricUnlock": {
await this.stateService.setBiometricAwaitingAcceptance(null);
if (message.response === "not enabled") { if (message.response === "not enabled") {
this.messagingService.send("showDialog", { this.messagingService.send("showDialog", {
title: { key: "biometricsNotEnabledTitle" }, title: { key: "biometricsNotEnabledTitle" },

View File

@@ -369,14 +369,12 @@ export class SettingsComponent implements OnInit {
const awaitDesktopDialogRef = AwaitDesktopDialogComponent.open(this.dialogService); const awaitDesktopDialogRef = AwaitDesktopDialogComponent.open(this.dialogService);
const awaitDesktopDialogClosed = firstValueFrom(awaitDesktopDialogRef.closed); const awaitDesktopDialogClosed = firstValueFrom(awaitDesktopDialogRef.closed);
await this.stateService.setBiometricAwaitingAcceptance(true);
await this.cryptoService.refreshAdditionalKeys(); await this.cryptoService.refreshAdditionalKeys();
await Promise.race([ await Promise.race([
awaitDesktopDialogClosed.then(async (result) => { awaitDesktopDialogClosed.then(async (result) => {
if (result !== true) { if (result !== true) {
this.form.controls.biometric.setValue(false); this.form.controls.biometric.setValue(false);
await this.stateService.setBiometricAwaitingAcceptance(null);
} }
}), }),
this.platformUtilsService this.platformUtilsService

View File

@@ -24,6 +24,7 @@ import { DialogService } from "@bitwarden/components";
import { SetPinComponent } from "../../auth/components/set-pin.component"; import { SetPinComponent } from "../../auth/components/set-pin.component";
import { flagEnabled } from "../../platform/flags"; import { flagEnabled } from "../../platform/flags";
import { ElectronStateService } from "../../platform/services/electron-state.service.abstraction"; import { ElectronStateService } from "../../platform/services/electron-state.service.abstraction";
@Component({ @Component({
selector: "app-settings", selector: "app-settings",
templateUrl: "settings.component.html", templateUrl: "settings.component.html",
@@ -39,9 +40,7 @@ export class SettingsComponent implements OnInit {
themeOptions: any[]; themeOptions: any[];
clearClipboardOptions: any[]; clearClipboardOptions: any[];
supportsBiometric: boolean; supportsBiometric: boolean;
biometricText: string;
additionalBiometricSettingsText: string; additionalBiometricSettingsText: string;
autoPromptBiometricsText: string;
showAlwaysShowDock = false; showAlwaysShowDock = false;
requireEnableTray = false; requireEnableTray = false;
showDuckDuckGoIntegrationOption = false; showDuckDuckGoIntegrationOption = false;
@@ -275,12 +274,10 @@ export class SettingsComponent implements OnInit {
this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop; this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop;
this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
this.biometricText = await this.stateService.getBiometricText();
this.additionalBiometricSettingsText = this.additionalBiometricSettingsText =
this.biometricText === "unlockWithTouchId" this.biometricText === "unlockWithTouchId"
? "additionalTouchIdSettings" ? "additionalTouchIdSettings"
: "additionalWindowsHelloSettings"; : "additionalWindowsHelloSettings";
this.autoPromptBiometricsText = await this.stateService.getNoAutoPromptBiometricsText();
this.previousVaultTimeout = this.form.value.vaultTimeout; this.previousVaultTimeout = this.form.value.vaultTimeout;
this.refreshTimeoutSettings$ this.refreshTimeoutSettings$
@@ -667,4 +664,26 @@ export class SettingsComponent implements OnInit {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
} }
get biometricText() {
switch (this.platformUtilsService.getDevice()) {
case DeviceType.MacOsDesktop:
return "unlockWithTouchId";
case DeviceType.WindowsDesktop:
return "unlockWithWindowsHello";
default:
throw new Error("Unsupported platform");
}
}
get autoPromptBiometricsText() {
switch (this.platformUtilsService.getDevice()) {
case DeviceType.MacOsDesktop:
return "autoPromptTouchId";
case DeviceType.WindowsDesktop:
return "autoPromptWindowsHello";
default:
throw new Error("Unsupported platform");
}
}
} }

View File

@@ -185,4 +185,15 @@ export class LockComponent extends BaseLockComponent {
await this.stateService.setDismissedBiometricRequirePasswordOnStart(); await this.stateService.setDismissedBiometricRequirePasswordOnStart();
} }
} }
get biometricText() {
switch (this.platformUtilsService.getDevice()) {
case DeviceType.MacOsDesktop:
return "unlockWithTouchId";
case DeviceType.WindowsDesktop:
return "unlockWithWindowsHello";
default:
throw new Error("Unsupported platform");
}
}
} }

View File

@@ -225,9 +225,6 @@ export class Main {
} }
this.powerMonitorMain.init(); this.powerMonitorMain.init();
await this.updaterMain.init(); await this.updaterMain.init();
if (this.biometricsService != null) {
await this.biometricsService.init();
}
if ( if (
(await this.stateService.getEnableBrowserIntegration()) || (await this.stateService.getEnableBrowserIntegration()) ||

View File

@@ -1,21 +1,12 @@
import { systemPreferences } from "electron"; import { systemPreferences } from "electron";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { passwords } from "@bitwarden/desktop-native"; import { passwords } from "@bitwarden/desktop-native";
import { OsBiometricService } from "./biometrics.service.abstraction"; import { OsBiometricService } from "./biometrics.service.abstraction";
export default class BiometricDarwinMain implements OsBiometricService { export default class BiometricDarwinMain implements OsBiometricService {
constructor( constructor(private i18nservice: I18nService) {}
private i18nservice: I18nService,
private stateService: StateService,
) {}
async init() {
await this.stateService.setBiometricText("unlockWithTouchId");
await this.stateService.setNoAutoPromptBiometricsText("autoPromptTouchId");
}
async osSupportsBiometric(): Promise<boolean> { async osSupportsBiometric(): Promise<boolean> {
return systemPreferences.canPromptTouchID(); return systemPreferences.canPromptTouchID();

View File

@@ -5,7 +5,6 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
import { biometrics, passwords } from "@bitwarden/desktop-native"; import { biometrics, passwords } from "@bitwarden/desktop-native";
import { WindowMain } from "../../../main/window.main"; import { WindowMain } from "../../../main/window.main";
import { ElectronStateService } from "../../services/electron-state.service.abstraction";
import { OsBiometricService } from "./biometrics.service.abstraction"; import { OsBiometricService } from "./biometrics.service.abstraction";
@@ -21,15 +20,9 @@ export default class BiometricWindowsMain implements OsBiometricService {
constructor( constructor(
private i18nService: I18nService, private i18nService: I18nService,
private windowMain: WindowMain, private windowMain: WindowMain,
private stateService: ElectronStateService,
private logService: LogService, private logService: LogService,
) {} ) {}
async init() {
await this.stateService.setBiometricText("unlockWithWindowsHello");
await this.stateService.setNoAutoPromptBiometricsText("autoPromptWindowsHello");
}
async osSupportsBiometric(): Promise<boolean> { async osSupportsBiometric(): Promise<boolean> {
return await biometrics.available(); return await biometrics.available();
} }

View File

@@ -1,5 +1,4 @@
export abstract class BiometricsServiceAbstraction { export abstract class BiometricsServiceAbstraction {
init: () => Promise<void>;
osSupportsBiometric: () => Promise<boolean>; osSupportsBiometric: () => Promise<boolean>;
canAuthBiometric: ({ canAuthBiometric: ({
service, service,
@@ -26,7 +25,6 @@ export abstract class BiometricsServiceAbstraction {
} }
export interface OsBiometricService { export interface OsBiometricService {
init: () => Promise<void>;
osSupportsBiometric: () => Promise<boolean>; osSupportsBiometric: () => Promise<boolean>;
authenticateBiometric: () => Promise<boolean>; authenticateBiometric: () => Promise<boolean>;
getBiometricKey: ( getBiometricKey: (

View File

@@ -43,13 +43,7 @@ describe("biometrics tests", function () {
const mockService = mock<OsBiometricService>(); const mockService = mock<OsBiometricService>();
(sut as any).platformSpecificService = mockService; (sut as any).platformSpecificService = mockService;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. await sut.setEncryptionKeyHalf({ service: "test", key: "test", value: "test" });
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sut.init();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sut.setEncryptionKeyHalf({ service: "test", key: "test", value: "test" });
expect(mockService.init).toBeCalled();
await sut.canAuthBiometric({ service: "test", key: "test", userId }); await sut.canAuthBiometric({ service: "test", key: "test", userId });
expect(mockService.osSupportsBiometric).toBeCalled(); expect(mockService.osSupportsBiometric).toBeCalled();
@@ -111,9 +105,6 @@ describe("biometrics tests", function () {
innerService = mock(); innerService = mock();
(sut as any).platformSpecificService = innerService; (sut as any).platformSpecificService = innerService;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sut.init();
}); });
it("should return false if client key half is required and not provided", async () => { it("should return false if client key half is required and not provided", async () => {
@@ -128,7 +119,6 @@ describe("biometrics tests", function () {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
sut.setEncryptionKeyHalf({ service: "test", key: "test", value: "test" }); sut.setEncryptionKeyHalf({ service: "test", key: "test", value: "test" });
expect(innerService.init).toBeCalled();
await sut.canAuthBiometric({ service: "test", key: "test", userId }); await sut.canAuthBiometric({ service: "test", key: "test", userId });
expect(innerService.osSupportsBiometric).toBeCalled(); expect(innerService.osSupportsBiometric).toBeCalled();

View File

@@ -58,10 +58,6 @@ export class BiometricsService implements BiometricsServiceAbstraction {
this.platformSpecificService = new NoopBiometricsService(); this.platformSpecificService = new NoopBiometricsService();
} }
async init() {
return await this.platformSpecificService.init();
}
async osSupportsBiometric() { async osSupportsBiometric() {
return await this.platformSpecificService.osSupportsBiometric(); return await this.platformSpecificService.osSupportsBiometric();
} }

View File

@@ -41,7 +41,6 @@ export class LockComponent implements OnInit, OnDestroy {
formPromise: Promise<MasterPasswordPolicyResponse>; formPromise: Promise<MasterPasswordPolicyResponse>;
supportsBiometric: boolean; supportsBiometric: boolean;
biometricLock: boolean; biometricLock: boolean;
biometricText: string;
protected successRoute = "vault"; protected successRoute = "vault";
protected forcePasswordResetRoute = "update-temp-password"; protected forcePasswordResetRoute = "update-temp-password";
@@ -343,7 +342,6 @@ export class LockComponent implements OnInit, OnDestroy {
(await this.vaultTimeoutSettingsService.isBiometricLockSet()) && (await this.vaultTimeoutSettingsService.isBiometricLockSet()) &&
((await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric)) || ((await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric)) ||
!this.platformUtilsService.supportsSecureStorage()); !this.platformUtilsService.supportsSecureStorage());
this.biometricText = await this.stateService.getBiometricText();
this.email = await this.stateService.getEmail(); this.email = await this.stateService.getEmail();
this.webVaultHostname = await this.environmentService.getHost(); this.webVaultHostname = await this.environmentService.getHost();

View File

@@ -69,12 +69,8 @@ export abstract class StateService<T extends Account = Account> {
setApiKeyClientSecret: (value: string, options?: StorageOptions) => Promise<void>; setApiKeyClientSecret: (value: string, options?: StorageOptions) => Promise<void>;
getAutoConfirmFingerPrints: (options?: StorageOptions) => Promise<boolean>; getAutoConfirmFingerPrints: (options?: StorageOptions) => Promise<boolean>;
setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise<void>; setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise<void>;
getBiometricAwaitingAcceptance: (options?: StorageOptions) => Promise<boolean>;
setBiometricAwaitingAcceptance: (value: boolean, options?: StorageOptions) => Promise<void>;
getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>; getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>;
setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>; setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>;
getBiometricText: (options?: StorageOptions) => Promise<string>;
setBiometricText: (value: string, options?: StorageOptions) => Promise<void>;
getBiometricUnlock: (options?: StorageOptions) => Promise<boolean>; getBiometricUnlock: (options?: StorageOptions) => Promise<boolean>;
setBiometricUnlock: (value: boolean, options?: StorageOptions) => Promise<void>; setBiometricUnlock: (value: boolean, options?: StorageOptions) => Promise<void>;
getCanAccessPremium: (options?: StorageOptions) => Promise<boolean>; getCanAccessPremium: (options?: StorageOptions) => Promise<boolean>;
@@ -378,8 +374,6 @@ export abstract class StateService<T extends Account = Account> {
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>; setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: unknown }>; getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: unknown }>;
setNeverDomains: (value: { [id: string]: unknown }, options?: StorageOptions) => Promise<void>; setNeverDomains: (value: { [id: string]: unknown }, options?: StorageOptions) => Promise<void>;
getNoAutoPromptBiometricsText: (options?: StorageOptions) => Promise<string>;
setNoAutoPromptBiometricsText: (value: string, options?: StorageOptions) => Promise<void>;
getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>; getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>;
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>; setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>; getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;

View File

@@ -11,15 +11,11 @@ export class GlobalState {
window?: WindowState = new WindowState(); window?: WindowState = new WindowState();
twoFactorToken?: string; twoFactorToken?: string;
disableFavicon?: boolean; disableFavicon?: boolean;
biometricAwaitingAcceptance?: boolean;
biometricFingerprintValidated?: boolean; biometricFingerprintValidated?: boolean;
vaultTimeout?: number; vaultTimeout?: number;
vaultTimeoutAction?: string; vaultTimeoutAction?: string;
loginRedirect?: any; loginRedirect?: any;
mainWindowSize?: number; mainWindowSize?: number;
enableBiometrics?: boolean;
biometricText?: string;
noAutoPromptBiometricsText?: string;
enableTray?: boolean; enableTray?: boolean;
enableMinimizeToTray?: boolean; enableMinimizeToTray?: boolean;
enableCloseToTray?: boolean; enableCloseToTray?: boolean;

View File

@@ -372,24 +372,6 @@ export class StateService<
); );
} }
async getBiometricAwaitingAcceptance(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.biometricAwaitingAcceptance ?? false
);
}
async setBiometricAwaitingAcceptance(value: boolean, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.biometricAwaitingAcceptance = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getBiometricFingerprintValidated(options?: StorageOptions): Promise<boolean> { async getBiometricFingerprintValidated(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
@@ -408,23 +390,6 @@ export class StateService<
); );
} }
async getBiometricText(options?: StorageOptions): Promise<string> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.biometricText;
}
async setBiometricText(value: string, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.biometricText = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getBiometricUnlock(options?: StorageOptions): Promise<boolean> { async getBiometricUnlock(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
@@ -1989,23 +1954,6 @@ export class StateService<
); );
} }
async getNoAutoPromptBiometricsText(options?: StorageOptions): Promise<string> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.noAutoPromptBiometricsText;
}
async setNoAutoPromptBiometricsText(value: string, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.noAutoPromptBiometricsText = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getOpenAtLogin(options?: StorageOptions): Promise<boolean> { async getOpenAtLogin(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))