diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 4e3f7b7abc..7fc97a4004 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2018,6 +2018,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 5047889b8e..e9c29b8fbc 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 { pinService: PinServiceAbstraction, private routerService: BrowserRouterService, biometricStateService: BiometricStateService, + biometricsService: BiometricsService, accountService: AccountService, kdfConfigService: KdfConfigService, syncService: SyncService, @@ -93,6 +95,7 @@ export class LockComponent extends BaseLockComponent { userVerificationService, pinService, biometricStateService, + biometricsService, accountService, authService, kdfConfigService, @@ -129,22 +132,35 @@ export class LockComponent extends BaseLockComponent { 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 10c9b2fb98..c35f7c0242 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -32,6 +32,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, @@ -93,6 +94,7 @@ export class AccountSecurityComponent implements OnInit { private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, ) { this.accountSwitcherEnabled = enableAccountSwitching(); } @@ -164,7 +166,7 @@ export class AccountSecurityComponent implements OnInit { }; 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 @@ -393,7 +395,7 @@ export class AccountSecurityComponent implements OnInit { this.form.controls.biometric.setValue(false); } }), - this.platformUtilsService + this.biometricsService .authenticateBiometric() .then((result) => { this.form.controls.biometric.setValue(result); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 70978f4070..1bd70e4eb1 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 @@ -223,6 +224,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."; 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"; @@ -337,6 +339,7 @@ export default class MainBackground { organizationVaultExportService: OrganizationVaultExportServiceAbstraction; vaultSettingsService: VaultSettingsServiceAbstraction; biometricStateService: BiometricStateService; + biometricsService: BiometricsService; stateEventRunnerService: StateEventRunnerService; ssoLoginService: SsoLoginServiceAbstraction; billingAccountProfileStateService: BillingAccountProfileStateService; @@ -420,7 +423,6 @@ export default class MainBackground { this.platformUtilsService = new BackgroundPlatformUtilsService( this.messagingService, (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), - async () => this.biometricUnlock(), self, this.offscreenDocumentService, ); @@ -589,6 +591,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( @@ -615,6 +619,7 @@ export default class MainBackground { this.accountService, this.stateProvider, this.biometricStateService, + this.biometricsService, this.kdfConfigService, ); @@ -1463,17 +1468,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 3fb943f613..1f8fe82859 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -326,6 +326,15 @@ export class NativeMessagingBackground { type: "danger", }); break; + } else if (message.response === "not available") { + this.messagingService.send("showDialog", { + title: { key: "biometricsNotAvailableTitle" }, + content: { key: "biometricsNotAvailableDesc" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + type: "danger", + }); + break; } else if (message.response === "canceled") { break; } @@ -392,6 +401,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 8b216b9a67..dbdca8c1de 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/platform/services/background-browser-biometrics..ts b/apps/browser/src/platform/services/background-browser-biometrics..ts new file mode 100644 index 0000000000..cf79f4f555 --- /dev/null +++ b/apps/browser/src/platform/services/background-browser-biometrics..ts @@ -0,0 +1,26 @@ +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"; + } +} 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..a5b72b9293 --- /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") { + 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..060308ee24 --- /dev/null +++ b/apps/browser/src/platform/services/foreground-browser-biometrics.ts @@ -0,0 +1,24 @@ +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; + } +} 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 047687e09f..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,18 +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") { - return true; - } - return false; - } - - 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 2c7129db29..f34407e0d1 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -57,6 +57,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"; @@ -99,6 +100,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"; @@ -203,6 +205,7 @@ const safeProviders: SafeProvider[] = [ accountService: AccountServiceAbstraction, stateProvider: StateProvider, biometricStateService: BiometricStateService, + biometricsService: BiometricsService, kdfConfigService: KdfConfigService, ) => { const cryptoService = new BrowserCryptoService( @@ -217,6 +220,7 @@ const safeProviders: SafeProvider[] = [ accountService, stateProvider, biometricStateService, + biometricsService, kdfConfigService, ); new ContainerService(cryptoService, encryptService).attachToGlobal(self); @@ -234,6 +238,7 @@ const safeProviders: SafeProvider[] = [ AccountServiceAbstraction, StateProvider, BiometricStateService, + BiometricsService, KdfConfigService, ], }), @@ -258,22 +263,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 0950a7dfec..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,14 +131,6 @@ export class CliPlatformUtilsService implements PlatformUtilsService { throw new Error("Not implemented."); } - supportsBiometric(): Promise { - return Promise.resolve(false); - } - - authenticateBiometric(): Promise { - return Promise.resolve(false); - } - 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 4f006f2364..a9fafbaf1b 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"; @@ -132,6 +133,7 @@ export class SettingsComponent implements OnInit { private userVerificationService: UserVerificationServiceAbstraction, private desktopSettingsService: DesktopSettingsService, private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, private desktopAutofillSettingsService: DesktopAutofillSettingsService, private pinService: PinServiceAbstraction, private logService: LogService, @@ -285,7 +287,7 @@ export class SettingsComponent implements OnInit { // 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$ 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 2a71a3d693..518e2a8189 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 { userVerificationService: UserVerificationService, pinService: PinServiceAbstraction, biometricStateService: BiometricStateService, + biometricsService: BiometricsService, accountService: AccountService, authService: AuthService, kdfConfigService: KdfConfigService, @@ -93,6 +95,7 @@ export class LockComponent extends BaseLockComponent { userVerificationService, pinService, biometricStateService, + biometricsService, accountService, authService, kdfConfigService, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 816d317f41..ced82d1421 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"; @@ -63,7 +63,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 e1a5c3da9a..83a1af571d 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 152b5ce32f..fe26f15b40 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.windows.main.ts b/apps/desktop/src/platform/main/biometric/biometric.windows.main.ts index 75d5bce8f5..c7f703629c 100644 --- a/apps/desktop/src/platform/main/biometric/biometric.windows.main.ts +++ b/apps/desktop/src/platform/main/biometric/biometric.windows.main.ts @@ -6,7 +6,7 @@ import { biometrics, passwords } from "@bitwarden/desktop-napi"; import { WindowMain } from "../../../main/window.main"; -import { OsBiometricService } from "./biometrics.service.abstraction"; +import { OsBiometricService } from "./desktop.biometrics.service"; const KEY_WITNESS_SUFFIX = "_witness"; const WITNESS_VALUE = "known key"; diff --git a/apps/desktop/src/platform/main/biometric/biometrics.service.spec.ts b/apps/desktop/src/platform/main/biometric/biometrics.service.spec.ts index cb6bb4858c..10ba1c83b6 100644 --- a/apps/desktop/src/platform/main/biometric/biometrics.service.spec.ts +++ b/apps/desktop/src/platform/main/biometric/biometrics.service.spec.ts @@ -11,7 +11,7 @@ import { WindowMain } from "../../../main/window.main"; import BiometricDarwinMain from "./biometric.darwin.main"; import BiometricWindowsMain from "./biometric.windows.main"; import { BiometricsService } from "./biometrics.service"; -import { OsBiometricService } from "./biometrics.service.abstraction"; +import { OsBiometricService } from "./desktop.biometrics.service"; jest.mock("@bitwarden/desktop-napi", () => { return { diff --git a/apps/desktop/src/platform/main/biometric/biometrics.service.ts b/apps/desktop/src/platform/main/biometric/biometrics.service.ts index b0331cce3e..470e45a83a 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); } @@ -55,7 +56,7 @@ export class BiometricsService implements BiometricsServiceAbstraction { this.platformSpecificService = new NoopBiometricsService(); } - async osSupportsBiometric() { + async supportsBiometric() { return await this.platformSpecificService.osSupportsBiometric(); } @@ -71,7 +72,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 { @@ -90,6 +91,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 76% 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 fb7ce048b5..71dfb6b02e 100644 --- a/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts +++ b/apps/desktop/src/platform/main/biometric/desktop.biometrics.service.ts @@ -1,5 +1,10 @@ -export abstract class BiometricsServiceAbstraction { - abstract osSupportsBiometric(): 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, @@ -9,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 adc7935e05..9e4d39da1f 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,7 +77,7 @@ export class DesktopCredentialStorageListener { }); break; case BiometricAction.OsSupported: - val = await this.biometricService.osSupportsBiometric(); + val = await this.biometricService.supportsBiometric(); 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..d0934282be --- /dev/null +++ b/apps/desktop/src/platform/services/electron-biometrics.service.ts @@ -0,0 +1,26 @@ +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(); + } +} 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 2d50712dfb..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,18 +131,6 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return ipc.platform.clipboard.read(); } - async supportsBiometric(): 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(); - } - 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 5980fade83..2575c489c5 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -7,8 +7,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"; @@ -32,11 +32,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, @@ -132,7 +132,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); } @@ -187,8 +194,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 02c7c29e34..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,14 +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); - } - supportsSecureStorage() { return false; } diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 88b042c5b8..5e14971c7b 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -32,6 +32,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"; @@ -86,6 +87,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, @@ -145,6 +147,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"); @@ -355,7 +364,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 f2dff46c78..fa0fc8f250 100644 --- a/libs/common/src/platform/abstractions/platform-utils.service.ts +++ b/libs/common/src/platform/abstractions/platform-utils.service.ts @@ -43,8 +43,6 @@ export abstract class PlatformUtilsService { abstract isSelfHost(): boolean; abstract copyToClipboard(text: string, options?: ClipboardOptions): void | boolean; abstract readFromClipboard(): Promise; - abstract supportsBiometric(): 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..d2056c6bf9 --- /dev/null +++ b/libs/common/src/platform/biometrics/biometric.service.ts @@ -0,0 +1,19 @@ +/** + * 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; +}