diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 4aa9fc2b52..cd2cc91293 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2083,6 +2083,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 2819d6a21f..a6da98fe99 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -24,6 +24,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; @@ -67,6 +68,7 @@ export class LockComponent extends BaseLockComponent implements OnInit { pinService: PinServiceAbstraction, private routerService: BrowserRouterService, biometricStateService: BiometricStateService, + biometricsService: BiometricsService, accountService: AccountService, kdfConfigService: KdfConfigService, syncService: SyncService, @@ -93,6 +95,7 @@ export class LockComponent extends BaseLockComponent implements OnInit { userVerificationService, pinService, biometricStateService, + biometricsService, accountService, authService, kdfConfigService, @@ -129,22 +132,35 @@ export class LockComponent extends BaseLockComponent implements OnInit { this.isInitialLockScreen && (await this.authService.getAuthStatus()) === AuthenticationStatus.Locked ) { - await this.unlockBiometric(); + await this.unlockBiometric(true); } }, 100); } - override async unlockBiometric(): Promise { + override async unlockBiometric(automaticPrompt: boolean = false): Promise { if (!this.biometricLock) { return; } - this.pendingBiometric = true; this.biometricError = null; let success; try { - success = await super.unlockBiometric(); + const available = await super.isBiometricUnlockAvailable(); + if (!available) { + if (!automaticPrompt) { + await this.dialogService.openSimpleDialog({ + type: "warning", + title: { key: "biometricsNotAvailableTitle" }, + content: { key: "biometricsNotAvailableDesc" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + }); + } + } else { + this.pendingBiometric = true; + success = await super.unlockBiometric(); + } } catch (e) { const error = BiometricErrors[e?.message as BiometricErrorTypes]; diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 076c03801a..7bced79a0a 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -33,6 +33,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { VaultTimeout, VaultTimeoutOption, @@ -94,6 +95,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, ) { this.accountSwitcherEnabled = enableAccountSwitching(); } @@ -165,7 +167,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { }; this.form.patchValue(initialValues, { emitEvent: false }); - this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); + this.supportsBiometric = await this.biometricsService.supportsBiometric(); this.showChangeMasterPass = await this.userVerificationService.hasMasterPassword(); this.form.controls.vaultTimeout.valueChanges @@ -405,7 +407,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { const biometricsPromise = async () => { try { - const result = await this.platformUtilsService.authenticateBiometric(); + const result = await this.biometricsService.authenticateBiometric(); // prevent duplicate dialog biometricsResponseReceived = true; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 40e9c8551b..3944f2d8af 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -97,6 +97,7 @@ import { BiometricStateService, DefaultBiometricStateService, } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency creation @@ -228,6 +229,7 @@ import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender import { OffscreenDocumentService } from "../platform/offscreen-document/abstractions/offscreen-document"; import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service"; import { BrowserTaskSchedulerService } from "../platform/services/abstractions/browser-task-scheduler.service"; +import { BackgroundBrowserBiometricsService } from "../platform/services/background-browser-biometrics.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; @@ -343,6 +345,7 @@ export default class MainBackground { organizationVaultExportService: OrganizationVaultExportServiceAbstraction; vaultSettingsService: VaultSettingsServiceAbstraction; biometricStateService: BiometricStateService; + biometricsService: BiometricsService; stateEventRunnerService: StateEventRunnerService; ssoLoginService: SsoLoginServiceAbstraction; billingAccountProfileStateService: BillingAccountProfileStateService; @@ -429,7 +432,6 @@ export default class MainBackground { this.platformUtilsService = new BackgroundPlatformUtilsService( this.messagingService, (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), - async () => this.biometricUnlock(), self, this.offscreenDocumentService, ); @@ -611,6 +613,8 @@ export default class MainBackground { this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); + this.biometricsService = new BackgroundBrowserBiometricsService(this.nativeMessagingBackground); + this.kdfConfigService = new KdfConfigService(this.stateProvider); this.pinService = new PinService( @@ -637,6 +641,7 @@ export default class MainBackground { this.accountService, this.stateProvider, this.biometricStateService, + this.biometricsService, this.kdfConfigService, ); @@ -1508,17 +1513,6 @@ export default class MainBackground { } } - async biometricUnlock(): Promise { - if (this.nativeMessagingBackground == null) { - return false; - } - - const responsePromise = this.nativeMessagingBackground.getResponse(); - await this.nativeMessagingBackground.send({ command: "biometricUnlock" }); - const response = await responsePromise; - return response.response === "unlocked"; - } - private async fullSync(override = false) { const syncInternal = 6 * 60 * 60 * 1000; // 6 hours const lastSync = await this.syncService.getLastSync(); diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 777af9538b..613fe777ef 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -285,7 +285,9 @@ export class NativeMessagingBackground { switch (message.command) { case "biometricUnlock": { if ( - ["not enabled", "not supported", "not unlocked", "canceled"].includes(message.response) + ["not available", "not enabled", "not supported", "not unlocked", "canceled"].includes( + message.response, + ) ) { this.rejecter(message.response); return; @@ -352,6 +354,10 @@ export class NativeMessagingBackground { } break; } + case "biometricUnlockAvailable": { + this.resolver(message); + break; + } default: this.logService.error("NativeMessage, got unknown command: " + message.command); break; diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index b667936581..44e395659b 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -68,6 +68,7 @@ export default class RuntimeBackground { ) => { const messagesWithResponse = [ "biometricUnlock", + "biometricUnlockAvailable", "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag", "getInlineMenuFieldQualificationFeatureFlag", ]; @@ -179,7 +180,11 @@ export default class RuntimeBackground { } break; case "biometricUnlock": { - const result = await this.main.biometricUnlock(); + const result = await this.main.biometricsService.authenticateBiometric(); + return result; + } + case "biometricUnlockAvailable": { + const result = await this.main.biometricsService.isBiometricUnlockAvailable(); return result; } case "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag": { diff --git a/apps/browser/src/models/biometricErrors.ts b/apps/browser/src/models/biometricErrors.ts index 570c776f56..42d9c679d3 100644 --- a/apps/browser/src/models/biometricErrors.ts +++ b/apps/browser/src/models/biometricErrors.ts @@ -11,7 +11,8 @@ export type BiometricErrorTypes = | "not unlocked" | "invalidateEncryption" | "userkey wrong" - | "wrongUserId"; + | "wrongUserId" + | "not available"; export const BiometricErrors: Record = { startDesktop: { @@ -46,4 +47,8 @@ export const BiometricErrors: Record = { title: "biometricsWrongUserTitle", description: "biometricsWrongUserDesc", }, + "not available": { + title: "biometricsNotAvailableTitle", + description: "biometricsNotAvailableDesc", + }, }; diff --git a/apps/browser/src/platform/services/background-browser-biometrics.service.ts b/apps/browser/src/platform/services/background-browser-biometrics.service.ts new file mode 100644 index 0000000000..41ae15972c --- /dev/null +++ b/apps/browser/src/platform/services/background-browser-biometrics.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from "@angular/core"; + +import { NativeMessagingBackground } from "../../background/nativeMessaging.background"; + +import { BrowserBiometricsService } from "./browser-biometrics.service"; + +@Injectable() +export class BackgroundBrowserBiometricsService extends BrowserBiometricsService { + constructor(private nativeMessagingBackground: NativeMessagingBackground) { + super(); + } + + async authenticateBiometric(): Promise { + const responsePromise = this.nativeMessagingBackground.getResponse(); + await this.nativeMessagingBackground.send({ command: "biometricUnlock" }); + const response = await responsePromise; + return response.response === "unlocked"; + } + + async isBiometricUnlockAvailable(): Promise { + const responsePromise = this.nativeMessagingBackground.getResponse(); + await this.nativeMessagingBackground.send({ command: "biometricUnlockAvailable" }); + const response = await responsePromise; + return response.response === "available"; + } + + async biometricsNeedsSetup(): Promise { + return false; + } + + async biometricsSupportsAutoSetup(): Promise { + return false; + } + + async biometricsSetup(): Promise {} +} diff --git a/apps/browser/src/platform/services/browser-biometrics.service.ts b/apps/browser/src/platform/services/browser-biometrics.service.ts new file mode 100644 index 0000000000..84734fb492 --- /dev/null +++ b/apps/browser/src/platform/services/browser-biometrics.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from "@angular/core"; + +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; + +import { BrowserApi } from "../browser/browser-api"; + +@Injectable() +export abstract class BrowserBiometricsService extends BiometricsService { + async supportsBiometric() { + const platformInfo = await BrowserApi.getPlatformInfo(); + if (platformInfo.os === "mac" || platformInfo.os === "win" || platformInfo.os === "linux") { + return true; + } + return false; + } + + abstract authenticateBiometric(): Promise; + abstract isBiometricUnlockAvailable(): Promise; +} diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index 1242d52021..1d61fb4c8e 100644 --- a/apps/browser/src/platform/services/browser-crypto.service.ts +++ b/apps/browser/src/platform/services/browser-crypto.service.ts @@ -11,6 +11,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state"; @@ -31,6 +32,7 @@ export class BrowserCryptoService extends CryptoService { accountService: AccountService, stateProvider: StateProvider, private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, kdfConfigService: KdfConfigService, ) { super( @@ -68,7 +70,7 @@ export class BrowserCryptoService extends CryptoService { userId?: UserId, ): Promise { if (keySuffix === KeySuffixOptions.Biometric) { - const biometricsResult = await this.platformUtilService.authenticateBiometric(); + const biometricsResult = await this.biometricsService.authenticateBiometric(); if (!biometricsResult) { return null; diff --git a/apps/browser/src/platform/services/foreground-browser-biometrics.ts b/apps/browser/src/platform/services/foreground-browser-biometrics.ts new file mode 100644 index 0000000000..ee55de2010 --- /dev/null +++ b/apps/browser/src/platform/services/foreground-browser-biometrics.ts @@ -0,0 +1,34 @@ +import { BrowserApi } from "../browser/browser-api"; + +import { BrowserBiometricsService } from "./browser-biometrics.service"; + +export class ForegroundBrowserBiometricsService extends BrowserBiometricsService { + async authenticateBiometric(): Promise { + const response = await BrowserApi.sendMessageWithResponse<{ + result: boolean; + error: string; + }>("biometricUnlock"); + if (!response.result) { + throw response.error; + } + return response.result; + } + + async isBiometricUnlockAvailable(): Promise { + const response = await BrowserApi.sendMessageWithResponse<{ + result: boolean; + error: string; + }>("biometricUnlockAvailable"); + return response.result && response.result === true; + } + + async biometricsNeedsSetup(): Promise { + return false; + } + + async biometricsSupportsAutoSetup(): Promise { + return false; + } + + async biometricsSetup(): Promise {} +} diff --git a/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts index ec26d6aa29..da6a8faf3e 100644 --- a/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts @@ -8,11 +8,10 @@ export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService constructor( private messagingService: MessagingService, clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, - biometricCallback: () => Promise, win: Window & typeof globalThis, offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService); + super(clipboardWriteCallback, win, offscreenDocumentService); } override showToast( diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts index c86c915801..762380071b 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts @@ -16,7 +16,7 @@ class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService { win: Window & typeof globalThis, offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardSpy, null, win, offscreenDocumentService); + super(clipboardSpy, win, offscreenDocumentService); } showToast( diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts index 8428a74d43..b47488bdd7 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts @@ -15,7 +15,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic constructor( private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, - private biometricCallback: () => Promise, private globalContext: Window | ServiceWorkerGlobalScope, private offscreenDocumentService: OffscreenDocumentService, ) {} @@ -276,30 +275,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic return await BrowserClipboardService.read(windowContext); } - async supportsBiometric() { - const platformInfo = await BrowserApi.getPlatformInfo(); - if (platformInfo.os === "mac" || platformInfo.os === "win" || platformInfo.os === "linux") { - return true; - } - return false; - } - - async biometricsNeedsSetup(): Promise { - return false; - } - - async biometricsSupportsAutoSetup(): Promise { - return false; - } - - async biometricsSetup(): Promise { - return; - } - - authenticateBiometric() { - return this.biometricCallback(); - } - supportsSecureStorage(): boolean { return false; } diff --git a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts index f775f049e7..5b4b7288d1 100644 --- a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts @@ -8,11 +8,10 @@ export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService constructor( private toastService: ToastService, clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, - biometricCallback: () => Promise, win: Window & typeof globalThis, offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService); + super(clipboardWriteCallback, win, offscreenDocumentService); } override showToast( diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 6830809374..0349d1a694 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -63,6 +63,7 @@ import { ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; @@ -109,6 +110,7 @@ import { BrowserCryptoService } from "../../platform/services/browser-crypto.ser import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; +import { ForegroundBrowserBiometricsService } from "../../platform/services/foreground-browser-biometrics"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service"; @@ -217,6 +219,7 @@ const safeProviders: SafeProvider[] = [ accountService: AccountServiceAbstraction, stateProvider: StateProvider, biometricStateService: BiometricStateService, + biometricsService: BiometricsService, kdfConfigService: KdfConfigService, ) => { const cryptoService = new BrowserCryptoService( @@ -231,6 +234,7 @@ const safeProviders: SafeProvider[] = [ accountService, stateProvider, biometricStateService, + biometricsService, kdfConfigService, ); new ContainerService(cryptoService, encryptService).attachToGlobal(self); @@ -248,6 +252,7 @@ const safeProviders: SafeProvider[] = [ AccountServiceAbstraction, StateProvider, BiometricStateService, + BiometricsService, KdfConfigService, ], }), @@ -272,22 +277,19 @@ const safeProviders: SafeProvider[] = [ (clipboardValue: string, clearMs: number) => { void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); }, - async () => { - const response = await BrowserApi.sendMessageWithResponse<{ - result: boolean; - error: string; - }>("biometricUnlock"); - if (!response.result) { - throw response.error; - } - return response.result; - }, window, offscreenDocumentService, ); }, deps: [ToastService, OffscreenDocumentService], }), + safeProvider({ + provide: BiometricsService, + useFactory: () => { + return new ForegroundBrowserBiometricsService(); + }, + deps: [], + }), safeProvider({ provide: SyncService, useFactory: getBgService("syncService"), diff --git a/apps/cli/src/platform/services/cli-platform-utils.service.ts b/apps/cli/src/platform/services/cli-platform-utils.service.ts index 2a39510fda..24bceec389 100644 --- a/apps/cli/src/platform/services/cli-platform-utils.service.ts +++ b/apps/cli/src/platform/services/cli-platform-utils.service.ts @@ -131,26 +131,6 @@ export class CliPlatformUtilsService implements PlatformUtilsService { throw new Error("Not implemented."); } - supportsBiometric(): Promise { - return Promise.resolve(false); - } - - authenticateBiometric(): Promise { - return Promise.resolve(false); - } - - biometricsNeedsSetup(): Promise { - return Promise.resolve(false); - } - - biometricsSupportsAutoSetup(): Promise { - return Promise.resolve(false); - } - - biometricsSetup(): Promise { - return Promise.resolve(); - } - supportsSecureStorage(): boolean { return false; } diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index d85447398a..ebcae98a88 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -20,6 +20,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { KeySuffixOptions, ThemeType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; @@ -133,6 +134,7 @@ export class SettingsComponent implements OnInit, OnDestroy { private userVerificationService: UserVerificationServiceAbstraction, private desktopSettingsService: DesktopSettingsService, private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, private desktopAutofillSettingsService: DesktopAutofillSettingsService, private pinService: PinServiceAbstraction, private logService: LogService, @@ -287,7 +289,7 @@ export class SettingsComponent implements OnInit, OnDestroy { // Non-form values this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop; this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; - this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); + this.supportsBiometric = await this.biometricsService.supportsBiometric(); this.previousVaultTimeout = this.form.value.vaultTimeout; this.refreshTimeoutSettings$ @@ -466,13 +468,12 @@ export class SettingsComponent implements OnInit, OnDestroy { return; } - const needsSetup = await this.platformUtilsService.biometricsNeedsSetup(); - const supportsBiometricAutoSetup = - await this.platformUtilsService.biometricsSupportsAutoSetup(); + const needsSetup = await this.biometricsService.biometricsNeedsSetup(); + const supportsBiometricAutoSetup = await this.biometricsService.biometricsSupportsAutoSetup(); if (needsSetup) { if (supportsBiometricAutoSetup) { - await this.platformUtilsService.biometricsSetup(); + await this.biometricsService.biometricsSetup(); } else { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "biometricsManualSetupTitle" }, diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 85bfbc09f6..be110be138 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -56,6 +56,7 @@ import { StateService as StateServiceAbstraction } from "@bitwarden/common/platf import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; @@ -72,6 +73,7 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; +import { ElectronBiometricsService } from "../../platform/services/electron-biometrics.service"; import { ElectronCryptoService } from "../../platform/services/electron-crypto.service"; import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service"; import { @@ -104,6 +106,11 @@ const RELOAD_CALLBACK = new SafeInjectionToken<() => any>("RELOAD_CALLBACK"); */ const safeProviders: SafeProvider[] = [ safeProvider(InitService), + safeProvider({ + provide: BiometricsService, + useClass: ElectronBiometricsService, + deps: [], + }), safeProvider(NativeMessagingService), safeProvider(SearchBarService), safeProvider(DialogService), diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index c46b791b1b..c5b5b7acf0 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -28,6 +28,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService as AbstractBiometricService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -35,6 +36,8 @@ import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; +import { BiometricsService } from "src/platform/main/biometric"; + import { LockComponent } from "./lock.component"; // ipc mock global @@ -53,6 +56,7 @@ describe("LockComponent", () => { let fixture: ComponentFixture; let stateServiceMock: MockProxy; let biometricStateService: MockProxy; + let biometricsService: MockProxy; let messagingServiceMock: MockProxy; let broadcasterServiceMock: MockProxy; let platformUtilsServiceMock: MockProxy; @@ -163,6 +167,10 @@ describe("LockComponent", () => { provide: BiometricStateService, useValue: biometricStateService, }, + { + provide: AbstractBiometricService, + useValue: biometricsService, + }, { provide: AccountService, useValue: accountService, diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index e6c2f7d11d..55cc79e0a6 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -25,6 +25,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; @@ -66,6 +67,7 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro userVerificationService: UserVerificationService, pinService: PinServiceAbstraction, biometricStateService: BiometricStateService, + biometricsService: BiometricsService, accountService: AccountService, authService: AuthService, kdfConfigService: KdfConfigService, @@ -93,6 +95,7 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro userVerificationService, pinService, biometricStateService, + biometricsService, accountService, authService, kdfConfigService, @@ -139,7 +142,7 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro // start background listener until destroyed on interval this.timerId = setInterval(async () => { - this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); + this.supportsBiometric = await this.biometricsService.supportsBiometric(); this.biometricReady = await this.canUseBiometric(); }, 1000); } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index b77cc72269..86d07440a7 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -32,7 +32,7 @@ import { PowerMonitorMain } from "./main/power-monitor.main"; import { TrayMain } from "./main/tray.main"; import { UpdaterMain } from "./main/updater.main"; import { WindowMain } from "./main/window.main"; -import { BiometricsService, BiometricsServiceAbstraction } from "./platform/main/biometric/index"; +import { BiometricsService, DesktopBiometricsService } from "./platform/main/biometric/index"; import { ClipboardMain } from "./platform/main/clipboard.main"; import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener"; import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service"; @@ -64,7 +64,7 @@ export class Main { menuMain: MenuMain; powerMonitorMain: PowerMonitorMain; trayMain: TrayMain; - biometricsService: BiometricsServiceAbstraction; + biometricsService: DesktopBiometricsService; nativeMessagingMain: NativeMessagingMain; clipboardMain: ClipboardMain; desktopAutofillSettingsService: DesktopAutofillSettingsService; diff --git a/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts b/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts index 838968f190..0f26cc78fb 100644 --- a/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts +++ b/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts @@ -3,7 +3,7 @@ import { systemPreferences } from "electron"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { passwords } from "@bitwarden/desktop-napi"; -import { OsBiometricService } from "./biometrics.service.abstraction"; +import { OsBiometricService } from "./desktop.biometrics.service"; export default class BiometricDarwinMain implements OsBiometricService { constructor(private i18nservice: I18nService) {} diff --git a/apps/desktop/src/platform/main/biometric/biometric.noop.main.ts b/apps/desktop/src/platform/main/biometric/biometric.noop.main.ts index 3dfba76432..57a86942e8 100644 --- a/apps/desktop/src/platform/main/biometric/biometric.noop.main.ts +++ b/apps/desktop/src/platform/main/biometric/biometric.noop.main.ts @@ -1,4 +1,4 @@ -import { OsBiometricService } from "./biometrics.service.abstraction"; +import { OsBiometricService } from "./desktop.biometrics.service"; export default class NoopBiometricsService implements OsBiometricService { constructor() {} diff --git a/apps/desktop/src/platform/main/biometric/biometric.unix.main.ts b/apps/desktop/src/platform/main/biometric/biometric.unix.main.ts index e2428d9d12..c748276a6e 100644 --- a/apps/desktop/src/platform/main/biometric/biometric.unix.main.ts +++ b/apps/desktop/src/platform/main/biometric/biometric.unix.main.ts @@ -7,7 +7,7 @@ import { biometrics, passwords } from "@bitwarden/desktop-napi"; import { WindowMain } from "../../../main/window.main"; import { isFlatpak, isLinux, isSnapStore } from "../../../utils"; -import { OsBiometricService } from "./biometrics.service.abstraction"; +import { OsBiometricService } from "./desktop.biometrics.service"; const polkitPolicy = ` { return { diff --git a/apps/desktop/src/platform/main/biometric/biometrics.service.ts b/apps/desktop/src/platform/main/biometric/biometrics.service.ts index 686007c7b5..e432939c87 100644 --- a/apps/desktop/src/platform/main/biometric/biometrics.service.ts +++ b/apps/desktop/src/platform/main/biometric/biometrics.service.ts @@ -6,9 +6,9 @@ import { UserId } from "@bitwarden/common/types/guid"; import { WindowMain } from "../../../main/window.main"; -import { BiometricsServiceAbstraction, OsBiometricService } from "./biometrics.service.abstraction"; +import { DesktopBiometricsService, OsBiometricService } from "./desktop.biometrics.service"; -export class BiometricsService implements BiometricsServiceAbstraction { +export class BiometricsService extends DesktopBiometricsService { private platformSpecificService: OsBiometricService; private clientKeyHalves = new Map(); @@ -20,6 +20,7 @@ export class BiometricsService implements BiometricsServiceAbstraction { private platform: NodeJS.Platform, private biometricStateService: BiometricStateService, ) { + super(); this.loadPlatformSpecificService(this.platform); } @@ -63,19 +64,19 @@ export class BiometricsService implements BiometricsServiceAbstraction { this.platformSpecificService = new NoopBiometricsService(); } - async osSupportsBiometric() { + async supportsBiometric() { return await this.platformSpecificService.osSupportsBiometric(); } - async osBiometricsNeedsSetup() { + async biometricsNeedsSetup() { return await this.platformSpecificService.osBiometricsNeedsSetup(); } - async osBiometricsCanAutoSetup() { + async biometricsSupportsAutoSetup() { return await this.platformSpecificService.osBiometricsCanAutoSetup(); } - async osBiometricsSetup() { + async biometricsSetup() { await this.platformSpecificService.osBiometricsSetup(); } @@ -91,7 +92,7 @@ export class BiometricsService implements BiometricsServiceAbstraction { const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); const clientKeyHalfB64 = this.getClientKeyHalf(service, key); const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64; - return clientKeyHalfSatisfied && (await this.osSupportsBiometric()); + return clientKeyHalfSatisfied && (await this.supportsBiometric()); } async authenticateBiometric(): Promise { @@ -110,6 +111,10 @@ export class BiometricsService implements BiometricsServiceAbstraction { return result; } + async isBiometricUnlockAvailable(): Promise { + return await this.platformSpecificService.osSupportsBiometric(); + } + async getBiometricKey(service: string, storageKey: string): Promise { return await this.interruptProcessReload(async () => { await this.enforceClientKeyHalf(service, storageKey); diff --git a/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts b/apps/desktop/src/platform/main/biometric/desktop.biometrics.service.ts similarity index 84% rename from apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts rename to apps/desktop/src/platform/main/biometric/desktop.biometrics.service.ts index 22766b7a31..c8e3a59612 100644 --- a/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts +++ b/apps/desktop/src/platform/main/biometric/desktop.biometrics.service.ts @@ -1,8 +1,10 @@ -export abstract class BiometricsServiceAbstraction { - abstract osSupportsBiometric(): Promise; - abstract osBiometricsNeedsSetup: () => Promise; - abstract osBiometricsCanAutoSetup: () => Promise; - abstract osBiometricsSetup: () => Promise; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; + +/** + * This service extends the base biometrics service to provide desktop specific functions, + * specifically for the main process. + */ +export abstract class DesktopBiometricsService extends BiometricsService { abstract canAuthBiometric({ service, key, @@ -12,7 +14,6 @@ export abstract class BiometricsServiceAbstraction { key: string; userId: string; }): Promise; - abstract authenticateBiometric(): Promise; abstract getBiometricKey(service: string, key: string): Promise; abstract setBiometricKey(service: string, key: string, value: string): Promise; abstract setEncryptionKeyHalf({ diff --git a/apps/desktop/src/platform/main/biometric/index.ts b/apps/desktop/src/platform/main/biometric/index.ts index f5a594d966..ad7725d718 100644 --- a/apps/desktop/src/platform/main/biometric/index.ts +++ b/apps/desktop/src/platform/main/biometric/index.ts @@ -1,2 +1,2 @@ -export * from "./biometrics.service.abstraction"; +export * from "./desktop.biometrics.service"; export * from "./biometrics.service"; diff --git a/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts b/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts index 2f423e75fc..5f278b23a0 100644 --- a/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts +++ b/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts @@ -6,14 +6,14 @@ import { passwords } from "@bitwarden/desktop-napi"; import { BiometricMessage, BiometricAction } from "../../types/biometric-message"; -import { BiometricsServiceAbstraction } from "./biometric/index"; +import { DesktopBiometricsService } from "./biometric/index"; const AuthRequiredSuffix = "_biometric"; export class DesktopCredentialStorageListener { constructor( private serviceName: string, - private biometricService: BiometricsServiceAbstraction, + private biometricService: DesktopBiometricsService, private logService: ConsoleLogService, ) {} @@ -77,16 +77,16 @@ export class DesktopCredentialStorageListener { }); break; case BiometricAction.OsSupported: - val = await this.biometricService.osSupportsBiometric(); + val = await this.biometricService.supportsBiometric(); break; case BiometricAction.NeedsSetup: - val = await this.biometricService.osBiometricsNeedsSetup(); + val = await this.biometricService.biometricsNeedsSetup(); break; case BiometricAction.Setup: - await this.biometricService.osBiometricsSetup(); + await this.biometricService.biometricsSetup(); break; case BiometricAction.CanAutoSetup: - val = await this.biometricService.osBiometricsCanAutoSetup(); + val = await this.biometricService.biometricsSupportsAutoSetup(); break; default: } diff --git a/apps/desktop/src/platform/services/electron-biometrics.service.ts b/apps/desktop/src/platform/services/electron-biometrics.service.ts new file mode 100644 index 0000000000..8e1b1f8a5d --- /dev/null +++ b/apps/desktop/src/platform/services/electron-biometrics.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from "@angular/core"; + +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; + +/** + * This service implement the base biometrics service to provide desktop specific functions, + * specifically for the renderer process by passing messages to the main process. + */ +@Injectable() +export class ElectronBiometricsService extends BiometricsService { + async supportsBiometric(): Promise { + return await ipc.platform.biometric.osSupported(); + } + + async isBiometricUnlockAvailable(): Promise { + return await ipc.platform.biometric.osSupported(); + } + + /** This method is used to authenticate the user presence _only_. + * It should not be used in the process to retrieve + * biometric keys, which has a separate authentication mechanism. + * For biometric keys, invoke "keytar" with a biometric key suffix */ + async authenticateBiometric(): Promise { + return await ipc.platform.biometric.authenticate(); + } + + async biometricsNeedsSetup(): Promise { + return await ipc.platform.biometric.biometricsNeedsSetup(); + } + + async biometricsSupportsAutoSetup(): Promise { + return await ipc.platform.biometric.biometricsCanAutoSetup(); + } + + async biometricsSetup(): Promise { + return await ipc.platform.biometric.biometricsSetup(); + } +} diff --git a/apps/desktop/src/platform/services/electron-platform-utils.service.ts b/apps/desktop/src/platform/services/electron-platform-utils.service.ts index 30753f09f1..2808b74f09 100644 --- a/apps/desktop/src/platform/services/electron-platform-utils.service.ts +++ b/apps/desktop/src/platform/services/electron-platform-utils.service.ts @@ -131,30 +131,6 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return ipc.platform.clipboard.read(); } - async supportsBiometric(): Promise { - return await ipc.platform.biometric.osSupported(); - } - - async biometricsNeedsSetup(): Promise { - return await ipc.platform.biometric.biometricsNeedsSetup(); - } - - async biometricsSupportsAutoSetup(): Promise { - return await ipc.platform.biometric.biometricsCanAutoSetup(); - } - - async biometricsSetup(): Promise { - return await ipc.platform.biometric.biometricsSetup(); - } - - /** This method is used to authenticate the user presence _only_. - * It should not be used in the process to retrieve - * biometric keys, which has a separate authentication mechanism. - * For biometric keys, invoke "keytar" with a biometric key suffix */ - async authenticateBiometric(): Promise { - return await ipc.platform.biometric.authenticate(); - } - supportsSecureStorage(): boolean { return ELECTRON_SUPPORTS_SECURE_STORAGE; } diff --git a/apps/desktop/src/services/native-messaging.service.ts b/apps/desktop/src/services/native-messaging.service.ts index 4137c4e680..f106d137b7 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -8,8 +8,8 @@ import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/c import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -33,11 +33,11 @@ export class NativeMessagingService { constructor( private cryptoFunctionService: CryptoFunctionService, private cryptoService: CryptoService, - private platformUtilService: PlatformUtilsService, private logService: LogService, private messagingService: MessagingService, private desktopSettingService: DesktopSettingsService, private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, private nativeMessageHandler: NativeMessageHandlerService, private dialogService: DialogService, private accountService: AccountService, @@ -133,7 +133,14 @@ export class NativeMessagingService { switch (message.command) { case "biometricUnlock": { - if (!(await this.platformUtilService.supportsBiometric())) { + const isTemporarilyDisabled = + (await this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId)) && + !(await this.biometricsService.supportsBiometric()); + if (isTemporarilyDisabled) { + return this.send({ command: "biometricUnlock", response: "not available" }, appId); + } + + if (!(await this.biometricsService.supportsBiometric())) { return this.send({ command: "biometricUnlock", response: "not supported" }, appId); } @@ -198,8 +205,18 @@ export class NativeMessagingService { break; } + case "biometricUnlockAvailable": { + const isAvailable = await this.biometricsService.supportsBiometric(); + return this.send( + { + command: "biometricUnlockAvailable", + response: isAvailable ? "available" : "not available", + }, + appId, + ); + } default: - this.logService.error("NativeMessage, got unknown command."); + this.logService.error("NativeMessage, got unknown command: " + message.command); break; } } diff --git a/apps/web/src/app/core/web-platform-utils.service.ts b/apps/web/src/app/core/web-platform-utils.service.ts index dceaaf51d1..dbd0ef593d 100644 --- a/apps/web/src/app/core/web-platform-utils.service.ts +++ b/apps/web/src/app/core/web-platform-utils.service.ts @@ -186,20 +186,6 @@ export class WebPlatformUtilsService implements PlatformUtilsService { throw new Error("Cannot read from clipboard on web."); } - supportsBiometric() { - return Promise.resolve(false); - } - - authenticateBiometric() { - return Promise.resolve(false); - } - - biometricsNeedsSetup: () => Promise; - biometricsSupportsAutoSetup(): Promise { - throw new Error("Method not implemented."); - } - biometricsSetup: () => Promise; - supportsSecureStorage() { return false; } diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 50eded416b..400dcfd889 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -30,6 +30,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { UserId } from "@bitwarden/common/types/guid"; @@ -84,6 +85,7 @@ export class LockComponent implements OnInit, OnDestroy { protected userVerificationService: UserVerificationService, protected pinService: PinServiceAbstraction, protected biometricStateService: BiometricStateService, + protected biometricsService: BiometricsService, protected accountService: AccountService, protected authService: AuthService, protected kdfConfigService: KdfConfigService, @@ -146,6 +148,13 @@ export class LockComponent implements OnInit, OnDestroy { return !!userKey; } + async isBiometricUnlockAvailable(): Promise { + if (!(await this.biometricsService.supportsBiometric())) { + return false; + } + return this.biometricsService.isBiometricUnlockAvailable(); + } + togglePassword() { this.showPassword = !this.showPassword; const input = document.getElementById(this.pinEnabled ? "pin" : "masterPassword"); @@ -327,7 +336,7 @@ export class LockComponent implements OnInit, OnDestroy { this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword(); - this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); + this.supportsBiometric = await this.biometricsService.supportsBiometric(); this.biometricLock = (await this.vaultTimeoutSettingsService.isBiometricLockSet()) && ((await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric)) || diff --git a/libs/common/src/platform/abstractions/platform-utils.service.ts b/libs/common/src/platform/abstractions/platform-utils.service.ts index 9725435afa..fa0fc8f250 100644 --- a/libs/common/src/platform/abstractions/platform-utils.service.ts +++ b/libs/common/src/platform/abstractions/platform-utils.service.ts @@ -43,26 +43,6 @@ export abstract class PlatformUtilsService { abstract isSelfHost(): boolean; abstract copyToClipboard(text: string, options?: ClipboardOptions): void | boolean; abstract readFromClipboard(): Promise; - abstract supportsBiometric(): Promise; - /** - * Determine whether biometrics support requires going through a setup process. - * This is currently only needed on Linux. - * - * @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place) - */ - abstract biometricsNeedsSetup: () => Promise; - /** - * Determine whether biometrics support can be automatically setup, or requires user interaction. - * Auto-setup is prevented by sandboxed environments, such as Snap and Flatpak. - * - * @returns true if biometrics support can be automatically setup, false if it requires user interaction. - */ - abstract biometricsSupportsAutoSetup(): Promise; - /** - * Start automatic biometric setup, which places the required configuration files / changes the required settings. - */ - abstract biometricsSetup: () => Promise; - abstract authenticateBiometric(): Promise; abstract supportsSecureStorage(): boolean; abstract getAutofillKeyboardShortcut(): Promise; } diff --git a/libs/common/src/platform/biometrics/biometric.service.ts b/libs/common/src/platform/biometrics/biometric.service.ts new file mode 100644 index 0000000000..ae65dcd176 --- /dev/null +++ b/libs/common/src/platform/biometrics/biometric.service.ts @@ -0,0 +1,37 @@ +/** + * The biometrics service is used to provide access to the status of and access to biometric functionality on the platforms. + */ +export abstract class BiometricsService { + /** + * Check if the platform supports biometric authentication. + */ + abstract supportsBiometric(): Promise; + + /** + * Checks whether biometric unlock is currently available at the moment (e.g. if the laptop lid is shut, biometric unlock may not be available) + */ + abstract isBiometricUnlockAvailable(): Promise; + + /** + * Performs biometric authentication + */ + abstract authenticateBiometric(): Promise; + /** + * Determine whether biometrics support requires going through a setup process. + * This is currently only needed on Linux. + * + * @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place) + */ + abstract biometricsNeedsSetup(): Promise; + /** + * Determine whether biometrics support can be automatically setup, or requires user interaction. + * Auto-setup is prevented by sandboxed environments, such as Snap and Flatpak. + * + * @returns true if biometrics support can be automatically setup, false if it requires user interaction. + */ + abstract biometricsSupportsAutoSetup(): Promise; + /** + * Start automatic biometric setup, which places the required configuration files / changes the required settings. + */ + abstract biometricsSetup(): Promise; +}