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

[PM-6296] Fix biometrics error prompt when biometrics are temporarily unavailable in browser extension (v2) (#10374)

* Create unavailable message for biometrics when in clamshell mode

* Move browser biometrics

* Inject nativemessagingbackground instead of using constructor

* Fix linting

* Fix build on browser
This commit is contained in:
Bernd Schoolmann
2024-08-27 08:25:20 +02:00
committed by GitHub
parent 9459cda304
commit 3c9b3ea2cc
38 changed files with 326 additions and 178 deletions

View File

@@ -2083,6 +2083,12 @@
"biometricsNotUnlockedDesc": { "biometricsNotUnlockedDesc": {
"message": "Please unlock this user in the desktop application and try again." "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": { "biometricsFailedTitle": {
"message": "Biometrics failed" "message": "Biometrics failed"
}, },

View File

@@ -24,6 +24,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-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 { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
@@ -67,6 +68,7 @@ export class LockComponent extends BaseLockComponent implements OnInit {
pinService: PinServiceAbstraction, pinService: PinServiceAbstraction,
private routerService: BrowserRouterService, private routerService: BrowserRouterService,
biometricStateService: BiometricStateService, biometricStateService: BiometricStateService,
biometricsService: BiometricsService,
accountService: AccountService, accountService: AccountService,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
syncService: SyncService, syncService: SyncService,
@@ -93,6 +95,7 @@ export class LockComponent extends BaseLockComponent implements OnInit {
userVerificationService, userVerificationService,
pinService, pinService,
biometricStateService, biometricStateService,
biometricsService,
accountService, accountService,
authService, authService,
kdfConfigService, kdfConfigService,
@@ -129,22 +132,35 @@ export class LockComponent extends BaseLockComponent implements OnInit {
this.isInitialLockScreen && this.isInitialLockScreen &&
(await this.authService.getAuthStatus()) === AuthenticationStatus.Locked (await this.authService.getAuthStatus()) === AuthenticationStatus.Locked
) { ) {
await this.unlockBiometric(); await this.unlockBiometric(true);
} }
}, 100); }, 100);
} }
override async unlockBiometric(): Promise<boolean> { override async unlockBiometric(automaticPrompt: boolean = false): Promise<boolean> {
if (!this.biometricLock) { if (!this.biometricLock) {
return; return;
} }
this.pendingBiometric = true;
this.biometricError = null; this.biometricError = null;
let success; let success;
try { 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) { } catch (e) {
const error = BiometricErrors[e?.message as BiometricErrorTypes]; const error = BiometricErrors[e?.message as BiometricErrorTypes];

View File

@@ -33,6 +33,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service";
import { import {
VaultTimeout, VaultTimeout,
VaultTimeoutOption, VaultTimeoutOption,
@@ -94,6 +95,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
private dialogService: DialogService, private dialogService: DialogService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
private biometricsService: BiometricsService,
) { ) {
this.accountSwitcherEnabled = enableAccountSwitching(); this.accountSwitcherEnabled = enableAccountSwitching();
} }
@@ -165,7 +167,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
}; };
this.form.patchValue(initialValues, { emitEvent: false }); 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.showChangeMasterPass = await this.userVerificationService.hasMasterPassword();
this.form.controls.vaultTimeout.valueChanges this.form.controls.vaultTimeout.valueChanges
@@ -405,7 +407,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
const biometricsPromise = async () => { const biometricsPromise = async () => {
try { try {
const result = await this.platformUtilsService.authenticateBiometric(); const result = await this.biometricsService.authenticateBiometric();
// prevent duplicate dialog // prevent duplicate dialog
biometricsResponseReceived = true; biometricsResponseReceived = true;

View File

@@ -97,6 +97,7 @@ import {
BiometricStateService, BiometricStateService,
DefaultBiometricStateService, DefaultBiometricStateService,
} from "@bitwarden/common/platform/biometrics/biometric-state.service"; } 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 { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
// eslint-disable-next-line no-restricted-imports -- Used for dependency creation // 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 { OffscreenDocumentService } from "../platform/offscreen-document/abstractions/offscreen-document";
import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service"; import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service";
import { BrowserTaskSchedulerService } from "../platform/services/abstractions/browser-task-scheduler.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 { BrowserCryptoService } from "../platform/services/browser-crypto.service";
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
@@ -343,6 +345,7 @@ export default class MainBackground {
organizationVaultExportService: OrganizationVaultExportServiceAbstraction; organizationVaultExportService: OrganizationVaultExportServiceAbstraction;
vaultSettingsService: VaultSettingsServiceAbstraction; vaultSettingsService: VaultSettingsServiceAbstraction;
biometricStateService: BiometricStateService; biometricStateService: BiometricStateService;
biometricsService: BiometricsService;
stateEventRunnerService: StateEventRunnerService; stateEventRunnerService: StateEventRunnerService;
ssoLoginService: SsoLoginServiceAbstraction; ssoLoginService: SsoLoginServiceAbstraction;
billingAccountProfileStateService: BillingAccountProfileStateService; billingAccountProfileStateService: BillingAccountProfileStateService;
@@ -429,7 +432,6 @@ export default class MainBackground {
this.platformUtilsService = new BackgroundPlatformUtilsService( this.platformUtilsService = new BackgroundPlatformUtilsService(
this.messagingService, this.messagingService,
(clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs),
async () => this.biometricUnlock(),
self, self,
this.offscreenDocumentService, this.offscreenDocumentService,
); );
@@ -611,6 +613,8 @@ export default class MainBackground {
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
this.biometricsService = new BackgroundBrowserBiometricsService(this.nativeMessagingBackground);
this.kdfConfigService = new KdfConfigService(this.stateProvider); this.kdfConfigService = new KdfConfigService(this.stateProvider);
this.pinService = new PinService( this.pinService = new PinService(
@@ -637,6 +641,7 @@ export default class MainBackground {
this.accountService, this.accountService,
this.stateProvider, this.stateProvider,
this.biometricStateService, this.biometricStateService,
this.biometricsService,
this.kdfConfigService, this.kdfConfigService,
); );
@@ -1508,17 +1513,6 @@ export default class MainBackground {
} }
} }
async biometricUnlock(): Promise<boolean> {
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) { private async fullSync(override = false) {
const syncInternal = 6 * 60 * 60 * 1000; // 6 hours const syncInternal = 6 * 60 * 60 * 1000; // 6 hours
const lastSync = await this.syncService.getLastSync(); const lastSync = await this.syncService.getLastSync();

View File

@@ -285,7 +285,9 @@ export class NativeMessagingBackground {
switch (message.command) { switch (message.command) {
case "biometricUnlock": { case "biometricUnlock": {
if ( 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); this.rejecter(message.response);
return; return;
@@ -352,6 +354,10 @@ export class NativeMessagingBackground {
} }
break; break;
} }
case "biometricUnlockAvailable": {
this.resolver(message);
break;
}
default: default:
this.logService.error("NativeMessage, got unknown command: " + message.command); this.logService.error("NativeMessage, got unknown command: " + message.command);
break; break;

View File

@@ -68,6 +68,7 @@ export default class RuntimeBackground {
) => { ) => {
const messagesWithResponse = [ const messagesWithResponse = [
"biometricUnlock", "biometricUnlock",
"biometricUnlockAvailable",
"getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag", "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag",
"getInlineMenuFieldQualificationFeatureFlag", "getInlineMenuFieldQualificationFeatureFlag",
]; ];
@@ -179,7 +180,11 @@ export default class RuntimeBackground {
} }
break; break;
case "biometricUnlock": { 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; return result;
} }
case "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag": { case "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag": {

View File

@@ -11,7 +11,8 @@ export type BiometricErrorTypes =
| "not unlocked" | "not unlocked"
| "invalidateEncryption" | "invalidateEncryption"
| "userkey wrong" | "userkey wrong"
| "wrongUserId"; | "wrongUserId"
| "not available";
export const BiometricErrors: Record<BiometricErrorTypes, BiometricError> = { export const BiometricErrors: Record<BiometricErrorTypes, BiometricError> = {
startDesktop: { startDesktop: {
@@ -46,4 +47,8 @@ export const BiometricErrors: Record<BiometricErrorTypes, BiometricError> = {
title: "biometricsWrongUserTitle", title: "biometricsWrongUserTitle",
description: "biometricsWrongUserDesc", description: "biometricsWrongUserDesc",
}, },
"not available": {
title: "biometricsNotAvailableTitle",
description: "biometricsNotAvailableDesc",
},
}; };

View File

@@ -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<boolean> {
const responsePromise = this.nativeMessagingBackground.getResponse();
await this.nativeMessagingBackground.send({ command: "biometricUnlock" });
const response = await responsePromise;
return response.response === "unlocked";
}
async isBiometricUnlockAvailable(): Promise<boolean> {
const responsePromise = this.nativeMessagingBackground.getResponse();
await this.nativeMessagingBackground.send({ command: "biometricUnlockAvailable" });
const response = await responsePromise;
return response.response === "available";
}
async biometricsNeedsSetup(): Promise<boolean> {
return false;
}
async biometricsSupportsAutoSetup(): Promise<boolean> {
return false;
}
async biometricsSetup(): Promise<void> {}
}

View File

@@ -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<boolean>;
abstract isBiometricUnlockAvailable(): Promise<boolean>;
}

View File

@@ -11,6 +11,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-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 { KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
import { USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state"; import { USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state";
@@ -31,6 +32,7 @@ export class BrowserCryptoService extends CryptoService {
accountService: AccountService, accountService: AccountService,
stateProvider: StateProvider, stateProvider: StateProvider,
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
private biometricsService: BiometricsService,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
) { ) {
super( super(
@@ -68,7 +70,7 @@ export class BrowserCryptoService extends CryptoService {
userId?: UserId, userId?: UserId,
): Promise<UserKey> { ): Promise<UserKey> {
if (keySuffix === KeySuffixOptions.Biometric) { if (keySuffix === KeySuffixOptions.Biometric) {
const biometricsResult = await this.platformUtilService.authenticateBiometric(); const biometricsResult = await this.biometricsService.authenticateBiometric();
if (!biometricsResult) { if (!biometricsResult) {
return null; return null;

View File

@@ -0,0 +1,34 @@
import { BrowserApi } from "../browser/browser-api";
import { BrowserBiometricsService } from "./browser-biometrics.service";
export class ForegroundBrowserBiometricsService extends BrowserBiometricsService {
async authenticateBiometric(): Promise<boolean> {
const response = await BrowserApi.sendMessageWithResponse<{
result: boolean;
error: string;
}>("biometricUnlock");
if (!response.result) {
throw response.error;
}
return response.result;
}
async isBiometricUnlockAvailable(): Promise<boolean> {
const response = await BrowserApi.sendMessageWithResponse<{
result: boolean;
error: string;
}>("biometricUnlockAvailable");
return response.result && response.result === true;
}
async biometricsNeedsSetup(): Promise<boolean> {
return false;
}
async biometricsSupportsAutoSetup(): Promise<boolean> {
return false;
}
async biometricsSetup(): Promise<void> {}
}

View File

@@ -8,11 +8,10 @@ export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService
constructor( constructor(
private messagingService: MessagingService, private messagingService: MessagingService,
clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
biometricCallback: () => Promise<boolean>,
win: Window & typeof globalThis, win: Window & typeof globalThis,
offscreenDocumentService: OffscreenDocumentService, offscreenDocumentService: OffscreenDocumentService,
) { ) {
super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService); super(clipboardWriteCallback, win, offscreenDocumentService);
} }
override showToast( override showToast(

View File

@@ -16,7 +16,7 @@ class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService {
win: Window & typeof globalThis, win: Window & typeof globalThis,
offscreenDocumentService: OffscreenDocumentService, offscreenDocumentService: OffscreenDocumentService,
) { ) {
super(clipboardSpy, null, win, offscreenDocumentService); super(clipboardSpy, win, offscreenDocumentService);
} }
showToast( showToast(

View File

@@ -15,7 +15,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
constructor( constructor(
private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
private biometricCallback: () => Promise<boolean>,
private globalContext: Window | ServiceWorkerGlobalScope, private globalContext: Window | ServiceWorkerGlobalScope,
private offscreenDocumentService: OffscreenDocumentService, private offscreenDocumentService: OffscreenDocumentService,
) {} ) {}
@@ -276,30 +275,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
return await BrowserClipboardService.read(windowContext); 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<boolean> {
return false;
}
async biometricsSupportsAutoSetup(): Promise<boolean> {
return false;
}
async biometricsSetup(): Promise<void> {
return;
}
authenticateBiometric() {
return this.biometricCallback();
}
supportsSecureStorage(): boolean { supportsSecureStorage(): boolean {
return false; return false;
} }

View File

@@ -8,11 +8,10 @@ export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService
constructor( constructor(
private toastService: ToastService, private toastService: ToastService,
clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
biometricCallback: () => Promise<boolean>,
win: Window & typeof globalThis, win: Window & typeof globalThis,
offscreenDocumentService: OffscreenDocumentService, offscreenDocumentService: OffscreenDocumentService,
) { ) {
super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService); super(clipboardWriteCallback, win, offscreenDocumentService);
} }
override showToast( override showToast(

View File

@@ -63,6 +63,7 @@ import {
ObservableStorageService, ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service"; } from "@bitwarden/common/platform/abstractions/storage.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.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"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection // eslint-disable-next-line no-restricted-imports -- Used for dependency injection
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; 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 { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.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 I18nService from "../../platform/services/i18n.service";
import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service";
import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service"; import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service";
@@ -217,6 +219,7 @@ const safeProviders: SafeProvider[] = [
accountService: AccountServiceAbstraction, accountService: AccountServiceAbstraction,
stateProvider: StateProvider, stateProvider: StateProvider,
biometricStateService: BiometricStateService, biometricStateService: BiometricStateService,
biometricsService: BiometricsService,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
) => { ) => {
const cryptoService = new BrowserCryptoService( const cryptoService = new BrowserCryptoService(
@@ -231,6 +234,7 @@ const safeProviders: SafeProvider[] = [
accountService, accountService,
stateProvider, stateProvider,
biometricStateService, biometricStateService,
biometricsService,
kdfConfigService, kdfConfigService,
); );
new ContainerService(cryptoService, encryptService).attachToGlobal(self); new ContainerService(cryptoService, encryptService).attachToGlobal(self);
@@ -248,6 +252,7 @@ const safeProviders: SafeProvider[] = [
AccountServiceAbstraction, AccountServiceAbstraction,
StateProvider, StateProvider,
BiometricStateService, BiometricStateService,
BiometricsService,
KdfConfigService, KdfConfigService,
], ],
}), }),
@@ -272,22 +277,19 @@ const safeProviders: SafeProvider[] = [
(clipboardValue: string, clearMs: number) => { (clipboardValue: string, clearMs: number) => {
void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); 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, window,
offscreenDocumentService, offscreenDocumentService,
); );
}, },
deps: [ToastService, OffscreenDocumentService], deps: [ToastService, OffscreenDocumentService],
}), }),
safeProvider({
provide: BiometricsService,
useFactory: () => {
return new ForegroundBrowserBiometricsService();
},
deps: [],
}),
safeProvider({ safeProvider({
provide: SyncService, provide: SyncService,
useFactory: getBgService<SyncService>("syncService"), useFactory: getBgService<SyncService>("syncService"),

View File

@@ -131,26 +131,6 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
throw new Error("Not implemented."); throw new Error("Not implemented.");
} }
supportsBiometric(): Promise<boolean> {
return Promise.resolve(false);
}
authenticateBiometric(): Promise<boolean> {
return Promise.resolve(false);
}
biometricsNeedsSetup(): Promise<boolean> {
return Promise.resolve(false);
}
biometricsSupportsAutoSetup(): Promise<boolean> {
return Promise.resolve(false);
}
biometricsSetup(): Promise<void> {
return Promise.resolve();
}
supportsSecureStorage(): boolean { supportsSecureStorage(): boolean {
return false; return false;
} }

View File

@@ -20,6 +20,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-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 { KeySuffixOptions, ThemeType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
@@ -133,6 +134,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
private userVerificationService: UserVerificationServiceAbstraction, private userVerificationService: UserVerificationServiceAbstraction,
private desktopSettingsService: DesktopSettingsService, private desktopSettingsService: DesktopSettingsService,
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
private biometricsService: BiometricsService,
private desktopAutofillSettingsService: DesktopAutofillSettingsService, private desktopAutofillSettingsService: DesktopAutofillSettingsService,
private pinService: PinServiceAbstraction, private pinService: PinServiceAbstraction,
private logService: LogService, private logService: LogService,
@@ -287,7 +289,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
// Non-form values // Non-form values
this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop; this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop;
this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); this.supportsBiometric = await this.biometricsService.supportsBiometric();
this.previousVaultTimeout = this.form.value.vaultTimeout; this.previousVaultTimeout = this.form.value.vaultTimeout;
this.refreshTimeoutSettings$ this.refreshTimeoutSettings$
@@ -466,13 +468,12 @@ export class SettingsComponent implements OnInit, OnDestroy {
return; return;
} }
const needsSetup = await this.platformUtilsService.biometricsNeedsSetup(); const needsSetup = await this.biometricsService.biometricsNeedsSetup();
const supportsBiometricAutoSetup = const supportsBiometricAutoSetup = await this.biometricsService.biometricsSupportsAutoSetup();
await this.platformUtilsService.biometricsSupportsAutoSetup();
if (needsSetup) { if (needsSetup) {
if (supportsBiometricAutoSetup) { if (supportsBiometricAutoSetup) {
await this.platformUtilsService.biometricsSetup(); await this.biometricsService.biometricsSetup();
} else { } else {
const confirmed = await this.dialogService.openSimpleDialog({ const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "biometricsManualSetupTitle" }, title: { key: "biometricsManualSetupTitle" },

View File

@@ -56,6 +56,7 @@ import { StateService as StateServiceAbstraction } from "@bitwarden/common/platf
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.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"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection // eslint-disable-next-line no-restricted-imports -- Used for dependency injection
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; 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 { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
import { DesktopSettingsService } from "../../platform/services/desktop-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 { ElectronCryptoService } from "../../platform/services/electron-crypto.service";
import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service"; import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service";
import { import {
@@ -104,6 +106,11 @@ const RELOAD_CALLBACK = new SafeInjectionToken<() => any>("RELOAD_CALLBACK");
*/ */
const safeProviders: SafeProvider[] = [ const safeProviders: SafeProvider[] = [
safeProvider(InitService), safeProvider(InitService),
safeProvider({
provide: BiometricsService,
useClass: ElectronBiometricsService,
deps: [],
}),
safeProvider(NativeMessagingService), safeProvider(NativeMessagingService),
safeProvider(SearchBarService), safeProvider(SearchBarService),
safeProvider(DialogService), safeProvider(DialogService),

View File

@@ -28,6 +28,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-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 { Utils } from "@bitwarden/common/platform/misc/utils";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; 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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { BiometricsService } from "src/platform/main/biometric";
import { LockComponent } from "./lock.component"; import { LockComponent } from "./lock.component";
// ipc mock global // ipc mock global
@@ -53,6 +56,7 @@ describe("LockComponent", () => {
let fixture: ComponentFixture<LockComponent>; let fixture: ComponentFixture<LockComponent>;
let stateServiceMock: MockProxy<StateService>; let stateServiceMock: MockProxy<StateService>;
let biometricStateService: MockProxy<BiometricStateService>; let biometricStateService: MockProxy<BiometricStateService>;
let biometricsService: MockProxy<BiometricsService>;
let messagingServiceMock: MockProxy<MessagingService>; let messagingServiceMock: MockProxy<MessagingService>;
let broadcasterServiceMock: MockProxy<BroadcasterService>; let broadcasterServiceMock: MockProxy<BroadcasterService>;
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>; let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
@@ -163,6 +167,10 @@ describe("LockComponent", () => {
provide: BiometricStateService, provide: BiometricStateService,
useValue: biometricStateService, useValue: biometricStateService,
}, },
{
provide: AbstractBiometricService,
useValue: biometricsService,
},
{ {
provide: AccountService, provide: AccountService,
useValue: accountService, useValue: accountService,

View File

@@ -25,6 +25,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-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 { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
@@ -66,6 +67,7 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro
userVerificationService: UserVerificationService, userVerificationService: UserVerificationService,
pinService: PinServiceAbstraction, pinService: PinServiceAbstraction,
biometricStateService: BiometricStateService, biometricStateService: BiometricStateService,
biometricsService: BiometricsService,
accountService: AccountService, accountService: AccountService,
authService: AuthService, authService: AuthService,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
@@ -93,6 +95,7 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro
userVerificationService, userVerificationService,
pinService, pinService,
biometricStateService, biometricStateService,
biometricsService,
accountService, accountService,
authService, authService,
kdfConfigService, kdfConfigService,
@@ -139,7 +142,7 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro
// start background listener until destroyed on interval // start background listener until destroyed on interval
this.timerId = setInterval(async () => { this.timerId = setInterval(async () => {
this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); this.supportsBiometric = await this.biometricsService.supportsBiometric();
this.biometricReady = await this.canUseBiometric(); this.biometricReady = await this.canUseBiometric();
}, 1000); }, 1000);
} }

View File

@@ -32,7 +32,7 @@ import { PowerMonitorMain } from "./main/power-monitor.main";
import { TrayMain } from "./main/tray.main"; import { TrayMain } from "./main/tray.main";
import { UpdaterMain } from "./main/updater.main"; import { UpdaterMain } from "./main/updater.main";
import { WindowMain } from "./main/window.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 { ClipboardMain } from "./platform/main/clipboard.main";
import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener"; import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener";
import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service"; import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service";
@@ -64,7 +64,7 @@ export class Main {
menuMain: MenuMain; menuMain: MenuMain;
powerMonitorMain: PowerMonitorMain; powerMonitorMain: PowerMonitorMain;
trayMain: TrayMain; trayMain: TrayMain;
biometricsService: BiometricsServiceAbstraction; biometricsService: DesktopBiometricsService;
nativeMessagingMain: NativeMessagingMain; nativeMessagingMain: NativeMessagingMain;
clipboardMain: ClipboardMain; clipboardMain: ClipboardMain;
desktopAutofillSettingsService: DesktopAutofillSettingsService; desktopAutofillSettingsService: DesktopAutofillSettingsService;

View File

@@ -3,7 +3,7 @@ import { systemPreferences } from "electron";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { passwords } from "@bitwarden/desktop-napi"; import { passwords } from "@bitwarden/desktop-napi";
import { OsBiometricService } from "./biometrics.service.abstraction"; import { OsBiometricService } from "./desktop.biometrics.service";
export default class BiometricDarwinMain implements OsBiometricService { export default class BiometricDarwinMain implements OsBiometricService {
constructor(private i18nservice: I18nService) {} constructor(private i18nservice: I18nService) {}

View File

@@ -1,4 +1,4 @@
import { OsBiometricService } from "./biometrics.service.abstraction"; import { OsBiometricService } from "./desktop.biometrics.service";
export default class NoopBiometricsService implements OsBiometricService { export default class NoopBiometricsService implements OsBiometricService {
constructor() {} constructor() {}

View File

@@ -7,7 +7,7 @@ import { biometrics, passwords } from "@bitwarden/desktop-napi";
import { WindowMain } from "../../../main/window.main"; import { WindowMain } from "../../../main/window.main";
import { isFlatpak, isLinux, isSnapStore } from "../../../utils"; import { isFlatpak, isLinux, isSnapStore } from "../../../utils";
import { OsBiometricService } from "./biometrics.service.abstraction"; import { OsBiometricService } from "./desktop.biometrics.service";
const polkitPolicy = `<?xml version="1.0" encoding="UTF-8"?> const polkitPolicy = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC <!DOCTYPE policyconfig PUBLIC

View File

@@ -6,7 +6,7 @@ import { biometrics, passwords } from "@bitwarden/desktop-napi";
import { WindowMain } from "../../../main/window.main"; import { WindowMain } from "../../../main/window.main";
import { OsBiometricService } from "./biometrics.service.abstraction"; import { OsBiometricService } from "./desktop.biometrics.service";
const KEY_WITNESS_SUFFIX = "_witness"; const KEY_WITNESS_SUFFIX = "_witness";
const WITNESS_VALUE = "known key"; const WITNESS_VALUE = "known key";

View File

@@ -11,7 +11,7 @@ import { WindowMain } from "../../../main/window.main";
import BiometricDarwinMain from "./biometric.darwin.main"; import BiometricDarwinMain from "./biometric.darwin.main";
import BiometricWindowsMain from "./biometric.windows.main"; import BiometricWindowsMain from "./biometric.windows.main";
import { BiometricsService } from "./biometrics.service"; import { BiometricsService } from "./biometrics.service";
import { OsBiometricService } from "./biometrics.service.abstraction"; import { OsBiometricService } from "./desktop.biometrics.service";
jest.mock("@bitwarden/desktop-napi", () => { jest.mock("@bitwarden/desktop-napi", () => {
return { return {

View File

@@ -6,9 +6,9 @@ import { UserId } from "@bitwarden/common/types/guid";
import { WindowMain } from "../../../main/window.main"; 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 platformSpecificService: OsBiometricService;
private clientKeyHalves = new Map<string, string>(); private clientKeyHalves = new Map<string, string>();
@@ -20,6 +20,7 @@ export class BiometricsService implements BiometricsServiceAbstraction {
private platform: NodeJS.Platform, private platform: NodeJS.Platform,
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
) { ) {
super();
this.loadPlatformSpecificService(this.platform); this.loadPlatformSpecificService(this.platform);
} }
@@ -63,19 +64,19 @@ export class BiometricsService implements BiometricsServiceAbstraction {
this.platformSpecificService = new NoopBiometricsService(); this.platformSpecificService = new NoopBiometricsService();
} }
async osSupportsBiometric() { async supportsBiometric() {
return await this.platformSpecificService.osSupportsBiometric(); return await this.platformSpecificService.osSupportsBiometric();
} }
async osBiometricsNeedsSetup() { async biometricsNeedsSetup() {
return await this.platformSpecificService.osBiometricsNeedsSetup(); return await this.platformSpecificService.osBiometricsNeedsSetup();
} }
async osBiometricsCanAutoSetup() { async biometricsSupportsAutoSetup() {
return await this.platformSpecificService.osBiometricsCanAutoSetup(); return await this.platformSpecificService.osBiometricsCanAutoSetup();
} }
async osBiometricsSetup() { async biometricsSetup() {
await this.platformSpecificService.osBiometricsSetup(); await this.platformSpecificService.osBiometricsSetup();
} }
@@ -91,7 +92,7 @@ export class BiometricsService implements BiometricsServiceAbstraction {
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
const clientKeyHalfB64 = this.getClientKeyHalf(service, key); const clientKeyHalfB64 = this.getClientKeyHalf(service, key);
const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64; const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
return clientKeyHalfSatisfied && (await this.osSupportsBiometric()); return clientKeyHalfSatisfied && (await this.supportsBiometric());
} }
async authenticateBiometric(): Promise<boolean> { async authenticateBiometric(): Promise<boolean> {
@@ -110,6 +111,10 @@ export class BiometricsService implements BiometricsServiceAbstraction {
return result; return result;
} }
async isBiometricUnlockAvailable(): Promise<boolean> {
return await this.platformSpecificService.osSupportsBiometric();
}
async getBiometricKey(service: string, storageKey: string): Promise<string | null> { async getBiometricKey(service: string, storageKey: string): Promise<string | null> {
return await this.interruptProcessReload(async () => { return await this.interruptProcessReload(async () => {
await this.enforceClientKeyHalf(service, storageKey); await this.enforceClientKeyHalf(service, storageKey);

View File

@@ -1,8 +1,10 @@
export abstract class BiometricsServiceAbstraction { import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service";
abstract osSupportsBiometric(): Promise<boolean>;
abstract osBiometricsNeedsSetup: () => Promise<boolean>; /**
abstract osBiometricsCanAutoSetup: () => Promise<boolean>; * This service extends the base biometrics service to provide desktop specific functions,
abstract osBiometricsSetup: () => Promise<void>; * specifically for the main process.
*/
export abstract class DesktopBiometricsService extends BiometricsService {
abstract canAuthBiometric({ abstract canAuthBiometric({
service, service,
key, key,
@@ -12,7 +14,6 @@ export abstract class BiometricsServiceAbstraction {
key: string; key: string;
userId: string; userId: string;
}): Promise<boolean>; }): Promise<boolean>;
abstract authenticateBiometric(): Promise<boolean>;
abstract getBiometricKey(service: string, key: string): Promise<string | null>; abstract getBiometricKey(service: string, key: string): Promise<string | null>;
abstract setBiometricKey(service: string, key: string, value: string): Promise<void>; abstract setBiometricKey(service: string, key: string, value: string): Promise<void>;
abstract setEncryptionKeyHalf({ abstract setEncryptionKeyHalf({

View File

@@ -1,2 +1,2 @@
export * from "./biometrics.service.abstraction"; export * from "./desktop.biometrics.service";
export * from "./biometrics.service"; export * from "./biometrics.service";

View File

@@ -6,14 +6,14 @@ import { passwords } from "@bitwarden/desktop-napi";
import { BiometricMessage, BiometricAction } from "../../types/biometric-message"; import { BiometricMessage, BiometricAction } from "../../types/biometric-message";
import { BiometricsServiceAbstraction } from "./biometric/index"; import { DesktopBiometricsService } from "./biometric/index";
const AuthRequiredSuffix = "_biometric"; const AuthRequiredSuffix = "_biometric";
export class DesktopCredentialStorageListener { export class DesktopCredentialStorageListener {
constructor( constructor(
private serviceName: string, private serviceName: string,
private biometricService: BiometricsServiceAbstraction, private biometricService: DesktopBiometricsService,
private logService: ConsoleLogService, private logService: ConsoleLogService,
) {} ) {}
@@ -77,16 +77,16 @@ export class DesktopCredentialStorageListener {
}); });
break; break;
case BiometricAction.OsSupported: case BiometricAction.OsSupported:
val = await this.biometricService.osSupportsBiometric(); val = await this.biometricService.supportsBiometric();
break; break;
case BiometricAction.NeedsSetup: case BiometricAction.NeedsSetup:
val = await this.biometricService.osBiometricsNeedsSetup(); val = await this.biometricService.biometricsNeedsSetup();
break; break;
case BiometricAction.Setup: case BiometricAction.Setup:
await this.biometricService.osBiometricsSetup(); await this.biometricService.biometricsSetup();
break; break;
case BiometricAction.CanAutoSetup: case BiometricAction.CanAutoSetup:
val = await this.biometricService.osBiometricsCanAutoSetup(); val = await this.biometricService.biometricsSupportsAutoSetup();
break; break;
default: default:
} }

View File

@@ -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<boolean> {
return await ipc.platform.biometric.osSupported();
}
async isBiometricUnlockAvailable(): Promise<boolean> {
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<boolean> {
return await ipc.platform.biometric.authenticate();
}
async biometricsNeedsSetup(): Promise<boolean> {
return await ipc.platform.biometric.biometricsNeedsSetup();
}
async biometricsSupportsAutoSetup(): Promise<boolean> {
return await ipc.platform.biometric.biometricsCanAutoSetup();
}
async biometricsSetup(): Promise<void> {
return await ipc.platform.biometric.biometricsSetup();
}
}

View File

@@ -131,30 +131,6 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
return ipc.platform.clipboard.read(); return ipc.platform.clipboard.read();
} }
async supportsBiometric(): Promise<boolean> {
return await ipc.platform.biometric.osSupported();
}
async biometricsNeedsSetup(): Promise<boolean> {
return await ipc.platform.biometric.biometricsNeedsSetup();
}
async biometricsSupportsAutoSetup(): Promise<boolean> {
return await ipc.platform.biometric.biometricsCanAutoSetup();
}
async biometricsSetup(): Promise<void> {
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<boolean> {
return await ipc.platform.biometric.authenticate();
}
supportsSecureStorage(): boolean { supportsSecureStorage(): boolean {
return ELECTRON_SUPPORTS_SECURE_STORAGE; return ELECTRON_SUPPORTS_SECURE_STORAGE;
} }

View File

@@ -8,8 +8,8 @@ import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/c
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.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 { 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 { KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
@@ -33,11 +33,11 @@ export class NativeMessagingService {
constructor( constructor(
private cryptoFunctionService: CryptoFunctionService, private cryptoFunctionService: CryptoFunctionService,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private platformUtilService: PlatformUtilsService,
private logService: LogService, private logService: LogService,
private messagingService: MessagingService, private messagingService: MessagingService,
private desktopSettingService: DesktopSettingsService, private desktopSettingService: DesktopSettingsService,
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
private biometricsService: BiometricsService,
private nativeMessageHandler: NativeMessageHandlerService, private nativeMessageHandler: NativeMessageHandlerService,
private dialogService: DialogService, private dialogService: DialogService,
private accountService: AccountService, private accountService: AccountService,
@@ -133,7 +133,14 @@ export class NativeMessagingService {
switch (message.command) { switch (message.command) {
case "biometricUnlock": { 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); return this.send({ command: "biometricUnlock", response: "not supported" }, appId);
} }
@@ -198,8 +205,18 @@ export class NativeMessagingService {
break; break;
} }
case "biometricUnlockAvailable": {
const isAvailable = await this.biometricsService.supportsBiometric();
return this.send(
{
command: "biometricUnlockAvailable",
response: isAvailable ? "available" : "not available",
},
appId,
);
}
default: default:
this.logService.error("NativeMessage, got unknown command."); this.logService.error("NativeMessage, got unknown command: " + message.command);
break; break;
} }
} }

View File

@@ -186,20 +186,6 @@ export class WebPlatformUtilsService implements PlatformUtilsService {
throw new Error("Cannot read from clipboard on web."); throw new Error("Cannot read from clipboard on web.");
} }
supportsBiometric() {
return Promise.resolve(false);
}
authenticateBiometric() {
return Promise.resolve(false);
}
biometricsNeedsSetup: () => Promise<boolean>;
biometricsSupportsAutoSetup(): Promise<boolean> {
throw new Error("Method not implemented.");
}
biometricsSetup: () => Promise<void>;
supportsSecureStorage() { supportsSecureStorage() {
return false; return false;
} }

View File

@@ -30,6 +30,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-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 { KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
@@ -84,6 +85,7 @@ export class LockComponent implements OnInit, OnDestroy {
protected userVerificationService: UserVerificationService, protected userVerificationService: UserVerificationService,
protected pinService: PinServiceAbstraction, protected pinService: PinServiceAbstraction,
protected biometricStateService: BiometricStateService, protected biometricStateService: BiometricStateService,
protected biometricsService: BiometricsService,
protected accountService: AccountService, protected accountService: AccountService,
protected authService: AuthService, protected authService: AuthService,
protected kdfConfigService: KdfConfigService, protected kdfConfigService: KdfConfigService,
@@ -146,6 +148,13 @@ export class LockComponent implements OnInit, OnDestroy {
return !!userKey; return !!userKey;
} }
async isBiometricUnlockAvailable(): Promise<boolean> {
if (!(await this.biometricsService.supportsBiometric())) {
return false;
}
return this.biometricsService.isBiometricUnlockAvailable();
}
togglePassword() { togglePassword() {
this.showPassword = !this.showPassword; this.showPassword = !this.showPassword;
const input = document.getElementById(this.pinEnabled ? "pin" : "masterPassword"); 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.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword();
this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); this.supportsBiometric = await this.biometricsService.supportsBiometric();
this.biometricLock = this.biometricLock =
(await this.vaultTimeoutSettingsService.isBiometricLockSet()) && (await this.vaultTimeoutSettingsService.isBiometricLockSet()) &&
((await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric)) || ((await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric)) ||

View File

@@ -43,26 +43,6 @@ export abstract class PlatformUtilsService {
abstract isSelfHost(): boolean; abstract isSelfHost(): boolean;
abstract copyToClipboard(text: string, options?: ClipboardOptions): void | boolean; abstract copyToClipboard(text: string, options?: ClipboardOptions): void | boolean;
abstract readFromClipboard(): Promise<string>; abstract readFromClipboard(): Promise<string>;
abstract supportsBiometric(): Promise<boolean>;
/**
* 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<boolean>;
/**
* 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<boolean>;
/**
* Start automatic biometric setup, which places the required configuration files / changes the required settings.
*/
abstract biometricsSetup: () => Promise<void>;
abstract authenticateBiometric(): Promise<boolean>;
abstract supportsSecureStorage(): boolean; abstract supportsSecureStorage(): boolean;
abstract getAutofillKeyboardShortcut(): Promise<string>; abstract getAutofillKeyboardShortcut(): Promise<string>;
} }

View File

@@ -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<boolean>;
/**
* 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<boolean>;
/**
* Performs biometric authentication
*/
abstract authenticateBiometric(): Promise<boolean>;
/**
* 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<boolean>;
/**
* 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<boolean>;
/**
* Start automatic biometric setup, which places the required configuration files / changes the required settings.
*/
abstract biometricsSetup(): Promise<void>;
}