diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 85937b63304..55c9ae8616b 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -4656,6 +4656,33 @@
"noEditPermissions": {
"message": "You don't have permission to edit this item"
},
+ "biometricsStatusHelptextUnlockNeeded": {
+ "message": "Biometric unlock is unavailable because PIN or password unlock is required first."
+ },
+ "biometricsStatusHelptextHardwareUnavailable": {
+ "message": "Biometric unlock is currently unavailable."
+ },
+ "biometricsStatusHelptextAutoSetupNeeded": {
+ "message": "Biometric unlock is unavailable due to misconfigured system files."
+ },
+ "biometricsStatusHelptextManualSetupNeeded": {
+ "message": "Biometric unlock is unavailable due to misconfigured system files."
+ },
+ "biometricsStatusHelptextDesktopDisconnected": {
+ "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed."
+ },
+ "biometricsStatusHelptextNotEnabledInDesktop": {
+ "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.",
+ "placeholders": {
+ "email": {
+ "content": "$1",
+ "example": "mail@example.com"
+ }
+ }
+ },
+ "biometricsStatusHelptextUnavailableReasonUnknown": {
+ "message": "Biometric unlock is currently unavailable for an unknown reason."
+ },
"authenticating": {
"message": "Authenticating"
},
diff --git a/apps/browser/src/auth/popup/account-switching/account.component.ts b/apps/browser/src/auth/popup/account-switching/account.component.ts
index 104241e9c7b..dad74977d34 100644
--- a/apps/browser/src/auth/popup/account-switching/account.component.ts
+++ b/apps/browser/src/auth/popup/account-switching/account.component.ts
@@ -8,6 +8,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { AvatarModule, ItemModule } from "@bitwarden/components";
+import { BiometricsService } from "@bitwarden/key-management";
import { AccountSwitcherService, AvailableAccount } from "./services/account-switcher.service";
@@ -26,6 +27,7 @@ export class AccountComponent {
private location: Location,
private i18nService: I18nService,
private logService: LogService,
+ private biometricsService: BiometricsService,
) {}
get specialAccountAddId() {
@@ -45,6 +47,9 @@ export class AccountComponent {
// locked or logged out account statuses are handled by background and app.component
if (result?.status === AuthenticationStatus.Unlocked) {
this.location.back();
+ await this.biometricsService.setShouldAutopromptNow(false);
+ } else {
+ await this.biometricsService.setShouldAutopromptNow(true);
}
this.loading.emit(false);
}
diff --git a/apps/browser/src/auth/popup/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html
index e0dfde7be77..3f874fc1a76 100644
--- a/apps/browser/src/auth/popup/settings/account-security.component.html
+++ b/apps/browser/src/auth/popup/settings/account-security.component.html
@@ -11,13 +11,16 @@
{{ "unlockMethods" | i18n }}
-
+
{{
"unlockWithBiometrics" | i18n
}}
+
+ {{ biometricUnavailabilityReason }}
+
-
+
{
+ const status = await this.biometricsService.getBiometricsStatusForUser(activeAccount.id);
+ const biometricSettingAvailable =
+ (status !== BiometricsStatus.DesktopDisconnected &&
+ status !== BiometricsStatus.NotEnabledInConnectedDesktopApp) ||
+ (await this.vaultTimeoutSettingsService.isBiometricLockSet());
+ if (!biometricSettingAvailable) {
+ this.form.controls.biometric.disable({ emitEvent: false });
+ } else {
+ this.form.controls.biometric.enable({ emitEvent: false });
+ }
+
+ if (status === BiometricsStatus.DesktopDisconnected && !biometricSettingAvailable) {
+ this.biometricUnavailabilityReason = this.i18nService.t(
+ "biometricsStatusHelptextDesktopDisconnected",
+ );
+ } else if (
+ status === BiometricsStatus.NotEnabledInConnectedDesktopApp &&
+ !biometricSettingAvailable
+ ) {
+ this.biometricUnavailabilityReason = this.i18nService.t(
+ "biometricsStatusHelptextNotEnabledInDesktop",
+ activeAccount.email,
+ );
+ } else {
+ this.biometricUnavailabilityReason = "";
+ }
+ }),
+ takeUntil(this.destroy$),
+ )
+ .subscribe();
+
this.showChangeMasterPass = await this.userVerificationService.hasMasterPassword();
this.form.controls.vaultTimeout.valueChanges
@@ -399,7 +438,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
}
async updateBiometric(enabled: boolean) {
- if (enabled && this.supportsBiometric) {
+ if (enabled) {
let granted;
try {
granted = await BrowserApi.requestPermission({ permissions: ["nativeMessaging"] });
@@ -471,7 +510,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
const biometricsPromise = async () => {
try {
- const result = await this.biometricsService.authenticateBiometric();
+ const result = await this.biometricsService.authenticateWithBiometrics();
// prevent duplicate dialog
biometricsResponseReceived = true;
diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts
index bcfa797e0ff..4bec3d6cc0a 100644
--- a/apps/browser/src/background/main.background.ts
+++ b/apps/browser/src/background/main.background.ts
@@ -204,6 +204,7 @@ import {
BiometricStateService,
BiometricsService,
DefaultBiometricStateService,
+ DefaultKeyService,
DefaultKdfConfigService,
KdfConfigService,
KeyService as KeyServiceAbstraction,
@@ -241,7 +242,6 @@ import AutofillService from "../autofill/services/autofill.service";
import { InlineMenuFieldQualificationService } from "../autofill/services/inline-menu-field-qualification.service";
import { SafariApp } from "../browser/safariApp";
import { BackgroundBrowserBiometricsService } from "../key-management/biometrics/background-browser-biometrics.service";
-import { BrowserKeyService } from "../key-management/browser-key.service";
import { BrowserApi } from "../platform/browser/browser-api";
import { flagEnabled } from "../platform/flags";
import { UpdateBadge } from "../platform/listeners/update-badge";
@@ -416,6 +416,7 @@ export default class MainBackground {
await this.refreshMenu(true);
if (this.systemService != null) {
await this.systemService.clearPendingClipboard();
+ await this.biometricsService.setShouldAutopromptNow(false);
await this.processReloadService.startProcessReload(this.authService);
}
};
@@ -633,6 +634,7 @@ export default class MainBackground {
this.biometricsService = new BackgroundBrowserBiometricsService(
runtimeNativeMessagingBackground,
+ this.logService,
);
this.kdfConfigService = new DefaultKdfConfigService(this.stateProvider);
@@ -649,7 +651,7 @@ export default class MainBackground {
this.stateService,
);
- this.keyService = new BrowserKeyService(
+ this.keyService = new DefaultKeyService(
this.pinService,
this.masterPasswordService,
this.keyGenerationService,
@@ -660,8 +662,6 @@ export default class MainBackground {
this.stateService,
this.accountService,
this.stateProvider,
- this.biometricStateService,
- this.biometricsService,
this.kdfConfigService,
);
@@ -857,10 +857,8 @@ export default class MainBackground {
this.userVerificationApiService,
this.userDecryptionOptionsService,
this.pinService,
- this.logService,
- this.vaultTimeoutSettingsService,
- this.platformUtilsService,
this.kdfConfigService,
+ this.biometricsService,
);
this.vaultFilterService = new VaultFilterService(
@@ -890,6 +888,7 @@ export default class MainBackground {
this.stateEventRunnerService,
this.taskSchedulerService,
this.logService,
+ this.biometricsService,
lockedCallback,
logoutCallback,
);
@@ -1081,6 +1080,7 @@ export default class MainBackground {
this.vaultTimeoutSettingsService,
this.biometricStateService,
this.accountService,
+ this.logService,
);
// Other fields
diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts
index 2ded1760235..116d048d2e8 100644
--- a/apps/browser/src/background/nativeMessaging.background.ts
+++ b/apps/browser/src/background/nativeMessaging.background.ts
@@ -1,10 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { firstValueFrom, map } from "rxjs";
+import { delay, filter, firstValueFrom, from, map, race, timer } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
-import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@@ -14,18 +13,19 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
-import { UserKey } from "@bitwarden/common/types/key";
-import { KeyService, BiometricStateService } from "@bitwarden/key-management";
+import { KeyService, BiometricStateService, BiometricsCommands } from "@bitwarden/key-management";
import { BrowserApi } from "../platform/browser/browser-api";
import RuntimeBackground from "./runtime.background";
const MessageValidTimeout = 10 * 1000;
+const MessageNoResponseTimeout = 60 * 1000;
const HashAlgorithmForEncryption = "sha1";
type Message = {
command: string;
+ messageId?: number;
// Filled in by this service
userId?: string;
@@ -43,6 +43,7 @@ type OuterMessage = {
type ReceiveMessage = {
timestamp: number;
command: string;
+ messageId: number;
response?: any;
// Unlock key
@@ -53,19 +54,23 @@ type ReceiveMessage = {
type ReceiveMessageOuter = {
command: string;
appId: string;
+ messageId?: number;
// Should only have one of these.
message?: EncString;
sharedSecret?: string;
};
+type Callback = {
+ resolver: any;
+ rejecter: any;
+};
+
export class NativeMessagingBackground {
- private connected = false;
+ connected = false;
private connecting: boolean;
private port: browser.runtime.Port | chrome.runtime.Port;
- private resolver: any = null;
- private rejecter: any = null;
private privateKey: Uint8Array = null;
private publicKey: Uint8Array = null;
private secureSetupResolve: any = null;
@@ -73,6 +78,11 @@ export class NativeMessagingBackground {
private appId: string;
private validatingFingerprint: boolean;
+ private messageId = 0;
+ private callbacks = new Map();
+
+ isConnectedToOutdatedDesktopClient = true;
+
constructor(
private keyService: KeyService,
private encryptService: EncryptService,
@@ -97,6 +107,7 @@ export class NativeMessagingBackground {
}
async connect() {
+ this.logService.info("[Native Messaging IPC] Connecting to Bitwarden Desktop app...");
this.appId = await this.appIdService.getAppId();
await this.biometricStateService.setFingerprintValidated(false);
@@ -106,6 +117,9 @@ export class NativeMessagingBackground {
this.connecting = true;
const connectedCallback = () => {
+ this.logService.info(
+ "[Native Messaging IPC] Connection to Bitwarden Desktop app established!",
+ );
this.connected = true;
this.connecting = false;
resolve();
@@ -123,11 +137,17 @@ export class NativeMessagingBackground {
connectedCallback();
break;
case "disconnected":
+ this.logService.info("[Native Messaging IPC] Disconnected from Bitwarden Desktop app.");
if (this.connecting) {
reject(new Error("startDesktop"));
}
this.connected = false;
this.port.disconnect();
+ // reject all
+ for (const callback of this.callbacks.values()) {
+ callback.rejecter("disconnected");
+ }
+ this.callbacks.clear();
break;
case "setupEncryption": {
// Ignore since it belongs to another device
@@ -147,6 +167,16 @@ export class NativeMessagingBackground {
await this.biometricStateService.setFingerprintValidated(true);
}
this.sharedSecret = new SymmetricCryptoKey(decrypted);
+ this.logService.info("[Native Messaging IPC] Secure channel established");
+
+ if ("messageId" in message) {
+ this.logService.info("[Native Messaging IPC] Non-legacy desktop client");
+ this.isConnectedToOutdatedDesktopClient = false;
+ } else {
+ this.logService.info("[Native Messaging IPC] Legacy desktop client");
+ this.isConnectedToOutdatedDesktopClient = true;
+ }
+
this.secureSetupResolve();
break;
}
@@ -155,17 +185,25 @@ export class NativeMessagingBackground {
if (message.appId !== this.appId) {
return;
}
+ this.logService.warning(
+ "[Native Messaging IPC] Secure channel encountered an error; disconnecting and wiping keys...",
+ );
this.sharedSecret = null;
this.privateKey = null;
this.connected = false;
- this.rejecter({
- message: "invalidateEncryption",
- });
+ if (this.callbacks.has(message.messageId)) {
+ this.callbacks.get(message.messageId).rejecter({
+ message: "invalidateEncryption",
+ });
+ }
return;
case "verifyFingerprint": {
if (this.sharedSecret == null) {
+ this.logService.info(
+ "[Native Messaging IPC] Desktop app requested trust verification by fingerprint.",
+ );
this.validatingFingerprint = true;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -174,9 +212,11 @@ export class NativeMessagingBackground {
break;
}
case "wrongUserId":
- this.rejecter({
- message: "wrongUserId",
- });
+ if (this.callbacks.has(message.messageId)) {
+ this.callbacks.get(message.messageId).rejecter({
+ message: "wrongUserId",
+ });
+ }
return;
default:
// Ignore since it belongs to another device
@@ -210,6 +250,60 @@ export class NativeMessagingBackground {
});
}
+ async callCommand(message: Message): Promise {
+ const messageId = this.messageId++;
+
+ if (
+ message.command == BiometricsCommands.Unlock ||
+ message.command == BiometricsCommands.IsAvailable
+ ) {
+ // TODO remove after 2025.01
+ // wait until there is no other callbacks, or timeout
+ const call = await firstValueFrom(
+ race(
+ from([false]).pipe(delay(5000)),
+ timer(0, 100).pipe(
+ filter(() => this.callbacks.size === 0),
+ map(() => true),
+ ),
+ ),
+ );
+ if (!call) {
+ this.logService.info(
+ `[Native Messaging IPC] Message of type ${message.command} did not get a response before timing out`,
+ );
+ return;
+ }
+ }
+
+ const callback = new Promise((resolver, rejecter) => {
+ this.callbacks.set(messageId, { resolver, rejecter });
+ });
+ message.messageId = messageId;
+ try {
+ await this.send(message);
+ } catch (e) {
+ this.logService.info(
+ `[Native Messaging IPC] Error sending message of type ${message.command} to Bitwarden Desktop app. Error: ${e}`,
+ );
+ const callback = this.callbacks.get(messageId);
+ this.callbacks.delete(messageId);
+ callback.rejecter("errorConnecting");
+ }
+
+ setTimeout(() => {
+ if (this.callbacks.has(messageId)) {
+ this.logService.info("[Native Messaging IPC] Message timed out and received no response");
+ this.callbacks.get(messageId).rejecter({
+ message: "timeout",
+ });
+ this.callbacks.delete(messageId);
+ }
+ }, MessageNoResponseTimeout);
+
+ return callback;
+ }
+
async send(message: Message) {
if (!this.connected) {
await this.connect();
@@ -233,20 +327,7 @@ export class NativeMessagingBackground {
return await this.encryptService.encrypt(JSON.stringify(message), this.sharedSecret);
}
- getResponse(): Promise {
- return new Promise((resolve, reject) => {
- this.resolver = function (response: any) {
- resolve(response);
- };
- this.rejecter = function (resp: any) {
- reject({
- message: resp,
- });
- };
- });
- }
-
- private postMessage(message: OuterMessage) {
+ private postMessage(message: OuterMessage, messageId?: number) {
// Wrap in try-catch to when the port disconnected without triggering `onDisconnect`.
try {
const msg: any = message;
@@ -262,13 +343,17 @@ export class NativeMessagingBackground {
}
this.port.postMessage(msg);
} catch (e) {
- this.logService.error("NativeMessaging port disconnected, disconnecting.");
+ this.logService.info(
+ "[Native Messaging IPC] Disconnected from Bitwarden Desktop app because of the native port disconnecting.",
+ );
this.sharedSecret = null;
this.privateKey = null;
this.connected = false;
- this.rejecter("invalidateEncryption");
+ if (this.callbacks.has(messageId)) {
+ this.callbacks.get(messageId).rejecter("invalidateEncryption");
+ }
}
}
@@ -285,90 +370,30 @@ export class NativeMessagingBackground {
}
if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) {
- this.logService.error("NativeMessage is to old, ignoring.");
+ this.logService.info("[Native Messaging IPC] Received an old native message, ignoring...");
return;
}
- switch (message.command) {
- case "biometricUnlock": {
- if (
- ["not available", "not enabled", "not supported", "not unlocked", "canceled"].includes(
- message.response,
- )
- ) {
- this.rejecter(message.response);
- return;
- }
+ const messageId = message.messageId;
- // Check for initial setup of biometric unlock
- const enabled = await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$);
- if (enabled === null || enabled === false) {
- if (message.response === "unlocked") {
- await this.biometricStateService.setBiometricUnlockEnabled(true);
- }
- break;
- }
-
- // Ignore unlock if already unlocked
- if ((await this.authService.getAuthStatus()) === AuthenticationStatus.Unlocked) {
- break;
- }
-
- if (message.response === "unlocked") {
- try {
- if (message.userKeyB64) {
- const userKey = new SymmetricCryptoKey(
- Utils.fromB64ToArray(message.userKeyB64),
- ) as UserKey;
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
- const isUserKeyValid = await this.keyService.validateUserKey(userKey, activeUserId);
- if (isUserKeyValid) {
- await this.keyService.setUserKey(userKey, activeUserId);
- } else {
- this.logService.error("Unable to verify biometric unlocked userkey");
- await this.keyService.clearKeys(activeUserId);
- this.rejecter("userkey wrong");
- return;
- }
- } else {
- throw new Error("No key received");
- }
- } catch (e) {
- this.logService.error("Unable to set key: " + e);
- this.rejecter("userkey wrong");
- return;
- }
-
- // Verify key is correct by attempting to decrypt a secret
- try {
- const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
- await this.keyService.getFingerprint(userId);
- } catch (e) {
- this.logService.error("Unable to verify key: " + e);
- await this.keyService.clearKeys();
- this.rejecter("userkey wrong");
- return;
- }
-
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.runtimeBackground.processMessage({ command: "unlocked" });
- }
- break;
- }
- case "biometricUnlockAvailable": {
- this.resolver(message);
- break;
- }
- default:
- this.logService.error("NativeMessage, got unknown command: " + message.command);
- break;
+ if (
+ message.command == BiometricsCommands.Unlock ||
+ message.command == BiometricsCommands.IsAvailable
+ ) {
+ this.logService.info(
+ `[Native Messaging IPC] Received legacy message of type ${message.command}`,
+ );
+ const messageId = this.callbacks.keys().next().value;
+ const resolver = this.callbacks.get(messageId);
+ this.callbacks.delete(messageId);
+ resolver.resolver(message);
+ return;
}
- if (this.resolver) {
- this.resolver(message);
+ if (this.callbacks.has(messageId)) {
+ this.callbacks.get(messageId).resolver(message);
+ } else {
+ this.logService.info("[Native Messaging IPC] Received message without a callback", message);
}
}
@@ -384,6 +409,7 @@ export class NativeMessagingBackground {
command: "setupEncryption",
publicKey: Utils.fromBufferToB64(publicKey),
userId: userId,
+ messageId: this.messageId++,
});
return new Promise((resolve, reject) => (this.secureSetupResolve = resolve));
diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts
index c31ec94be90..75340e3fbc3 100644
--- a/apps/browser/src/background/runtime.background.ts
+++ b/apps/browser/src/background/runtime.background.ts
@@ -16,6 +16,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherType } from "@bitwarden/common/vault/enums";
+import { BiometricsCommands } from "@bitwarden/key-management";
import { MessageListener, isExternalMessage } from "../../../../libs/common/src/platform/messaging";
import {
@@ -71,8 +72,10 @@ export default class RuntimeBackground {
sendResponse: (response: any) => void,
) => {
const messagesWithResponse = [
- "biometricUnlock",
- "biometricUnlockAvailable",
+ BiometricsCommands.AuthenticateWithBiometrics,
+ BiometricsCommands.GetBiometricsStatus,
+ BiometricsCommands.UnlockWithBiometricsForUser,
+ BiometricsCommands.GetBiometricsStatusForUser,
"getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag",
"getInlineMenuFieldQualificationFeatureFlag",
"getInlineMenuTotpFeatureFlag",
@@ -185,13 +188,17 @@ export default class RuntimeBackground {
break;
}
break;
- case "biometricUnlock": {
- const result = await this.main.biometricsService.authenticateBiometric();
- return result;
+ case BiometricsCommands.AuthenticateWithBiometrics: {
+ return await this.main.biometricsService.authenticateWithBiometrics();
}
- case "biometricUnlockAvailable": {
- const result = await this.main.biometricsService.isBiometricUnlockAvailable();
- return result;
+ case BiometricsCommands.GetBiometricsStatus: {
+ return await this.main.biometricsService.getBiometricsStatus();
+ }
+ case BiometricsCommands.UnlockWithBiometricsForUser: {
+ return await this.main.biometricsService.unlockWithBiometricsForUser(msg.userId);
+ }
+ case BiometricsCommands.GetBiometricsStatusForUser: {
+ return await this.main.biometricsService.getBiometricsStatusForUser(msg.userId);
}
case "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag": {
return await this.configService.getFeatureFlag(
diff --git a/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts
index 0cd48c45938..8e6fc562d14 100644
--- a/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts
+++ b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts
@@ -1,36 +1,136 @@
import { Injectable } from "@angular/core";
-import { NativeMessagingBackground } from "../../background/nativeMessaging.background";
+import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
+import { UserId } from "@bitwarden/common/types/guid";
+import { UserKey } from "@bitwarden/common/types/key";
+import { BiometricsService, BiometricsCommands, BiometricsStatus } from "@bitwarden/key-management";
-import { BrowserBiometricsService } from "./browser-biometrics.service";
+import { NativeMessagingBackground } from "../../background/nativeMessaging.background";
+import { BrowserApi } from "../../platform/browser/browser-api";
@Injectable()
-export class BackgroundBrowserBiometricsService extends BrowserBiometricsService {
- constructor(private nativeMessagingBackground: () => NativeMessagingBackground) {
+export class BackgroundBrowserBiometricsService extends BiometricsService {
+ constructor(
+ private nativeMessagingBackground: () => NativeMessagingBackground,
+ private logService: LogService,
+ ) {
super();
}
- async authenticateBiometric(): Promise {
- const responsePromise = this.nativeMessagingBackground().getResponse();
- await this.nativeMessagingBackground().send({ command: "biometricUnlock" });
- const response = await responsePromise;
- return response.response === "unlocked";
+ async authenticateWithBiometrics(): Promise {
+ try {
+ await this.ensureConnected();
+
+ if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
+ const response = await this.nativeMessagingBackground().callCommand({
+ command: BiometricsCommands.Unlock,
+ });
+ return response.response == "unlocked";
+ } else {
+ const response = await this.nativeMessagingBackground().callCommand({
+ command: BiometricsCommands.AuthenticateWithBiometrics,
+ });
+ return response.response;
+ }
+ } catch (e) {
+ this.logService.info("Biometric authentication failed", e);
+ return false;
+ }
}
- async isBiometricUnlockAvailable(): Promise {
- const responsePromise = this.nativeMessagingBackground().getResponse();
- await this.nativeMessagingBackground().send({ command: "biometricUnlockAvailable" });
- const response = await responsePromise;
- return response.response === "available";
+ async getBiometricsStatus(): Promise {
+ if (!(await BrowserApi.permissionsGranted(["nativeMessaging"]))) {
+ return BiometricsStatus.NativeMessagingPermissionMissing;
+ }
+
+ try {
+ await this.ensureConnected();
+
+ if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
+ const response = await this.nativeMessagingBackground().callCommand({
+ command: BiometricsCommands.IsAvailable,
+ });
+ const resp =
+ response.response == "available"
+ ? BiometricsStatus.Available
+ : BiometricsStatus.HardwareUnavailable;
+ return resp;
+ } else {
+ const response = await this.nativeMessagingBackground().callCommand({
+ command: BiometricsCommands.GetBiometricsStatus,
+ });
+
+ if (response.response) {
+ return response.response;
+ }
+ }
+ return BiometricsStatus.Available;
+ } catch (e) {
+ return BiometricsStatus.DesktopDisconnected;
+ }
}
- async biometricsNeedsSetup(): Promise {
+ async unlockWithBiometricsForUser(userId: UserId): Promise {
+ try {
+ await this.ensureConnected();
+
+ if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
+ const response = await this.nativeMessagingBackground().callCommand({
+ command: BiometricsCommands.Unlock,
+ });
+ if (response.response == "unlocked") {
+ return response.userKeyB64;
+ } else {
+ return null;
+ }
+ } else {
+ const response = await this.nativeMessagingBackground().callCommand({
+ command: BiometricsCommands.UnlockWithBiometricsForUser,
+ userId: userId,
+ });
+ if (response.response) {
+ return response.userKeyB64;
+ } else {
+ return null;
+ }
+ }
+ } catch (e) {
+ this.logService.info("Biometric unlock for user failed", e);
+ throw new Error("Biometric unlock failed");
+ }
+ }
+
+ async getBiometricsStatusForUser(id: UserId): Promise {
+ try {
+ await this.ensureConnected();
+
+ if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
+ return await this.getBiometricsStatus();
+ }
+
+ return (
+ await this.nativeMessagingBackground().callCommand({
+ command: BiometricsCommands.GetBiometricsStatusForUser,
+ userId: id,
+ })
+ ).response;
+ } catch (e) {
+ return BiometricsStatus.DesktopDisconnected;
+ }
+ }
+
+ // the first time we call, this might use an outdated version of the protocol, so we drop the response
+ private async ensureConnected() {
+ if (!this.nativeMessagingBackground().connected) {
+ await this.nativeMessagingBackground().callCommand({
+ command: BiometricsCommands.IsAvailable,
+ });
+ }
+ }
+
+ async getShouldAutopromptNow(): Promise {
return false;
}
- async biometricsSupportsAutoSetup(): Promise {
- return false;
- }
-
- async biometricsSetup(): Promise {}
+ async setShouldAutopromptNow(value: boolean): Promise {}
}
diff --git a/apps/browser/src/key-management/biometrics/browser-biometrics.service.ts b/apps/browser/src/key-management/biometrics/browser-biometrics.service.ts
deleted file mode 100644
index 7ffbed45415..00000000000
--- a/apps/browser/src/key-management/biometrics/browser-biometrics.service.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Injectable } from "@angular/core";
-
-import { BiometricsService } from "@bitwarden/key-management";
-
-import { BrowserApi } from "../../platform/browser/browser-api";
-
-@Injectable()
-export abstract class BrowserBiometricsService extends BiometricsService {
- async supportsBiometric() {
- const platformInfo = await BrowserApi.getPlatformInfo();
- if (platformInfo.os === "mac" || platformInfo.os === "win" || platformInfo.os === "linux") {
- return true;
- }
- return false;
- }
-
- abstract authenticateBiometric(): Promise;
- abstract isBiometricUnlockAvailable(): Promise;
-}
diff --git a/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts
index f50468c8b7a..0235ad5bd9c 100644
--- a/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts
+++ b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts
@@ -1,34 +1,55 @@
+import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
+import { UserId } from "@bitwarden/common/types/guid";
+import { UserKey } from "@bitwarden/common/types/key";
+import { BiometricsCommands, BiometricsService, BiometricsStatus } from "@bitwarden/key-management";
+
import { BrowserApi } from "../../platform/browser/browser-api";
-import { BrowserBiometricsService } from "./browser-biometrics.service";
+export class ForegroundBrowserBiometricsService extends BiometricsService {
+ shouldAutopromptNow = true;
-export class ForegroundBrowserBiometricsService extends BrowserBiometricsService {
- async authenticateBiometric(): Promise {
+ async authenticateWithBiometrics(): Promise {
const response = await BrowserApi.sendMessageWithResponse<{
result: boolean;
error: string;
- }>("biometricUnlock");
+ }>(BiometricsCommands.AuthenticateWithBiometrics);
if (!response.result) {
throw response.error;
}
return response.result;
}
- async isBiometricUnlockAvailable(): Promise {
+ async getBiometricsStatus(): Promise {
const response = await BrowserApi.sendMessageWithResponse<{
- result: boolean;
+ result: BiometricsStatus;
error: string;
- }>("biometricUnlockAvailable");
- return response.result && response.result === true;
+ }>(BiometricsCommands.GetBiometricsStatus);
+ return response.result;
}
- async biometricsNeedsSetup(): Promise {
- return false;
+ async unlockWithBiometricsForUser(userId: UserId): Promise {
+ const response = await BrowserApi.sendMessageWithResponse<{
+ result: string;
+ error: string;
+ }>(BiometricsCommands.UnlockWithBiometricsForUser, { userId });
+ if (!response.result) {
+ return null;
+ }
+ return SymmetricCryptoKey.fromString(response.result) as UserKey;
}
- async biometricsSupportsAutoSetup(): Promise {
- return false;
+ async getBiometricsStatusForUser(id: UserId): Promise {
+ const response = await BrowserApi.sendMessageWithResponse<{
+ result: BiometricsStatus;
+ error: string;
+ }>(BiometricsCommands.GetBiometricsStatusForUser, { userId: id });
+ return response.result;
}
- async biometricsSetup(): Promise {}
+ async getShouldAutopromptNow(): Promise {
+ return this.shouldAutopromptNow;
+ }
+ async setShouldAutopromptNow(value: boolean): Promise {
+ this.shouldAutopromptNow = value;
+ }
}
diff --git a/apps/browser/src/key-management/browser-key.service.ts b/apps/browser/src/key-management/browser-key.service.ts
deleted file mode 100644
index 0cc5f13a27e..00000000000
--- a/apps/browser/src/key-management/browser-key.service.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { firstValueFrom } from "rxjs";
-
-import { PinServiceAbstraction } from "@bitwarden/auth/common";
-import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
-import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
-import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
-import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
-import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
-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 { KeySuffixOptions } from "@bitwarden/common/platform/enums";
-import { USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state";
-import { StateProvider } from "@bitwarden/common/platform/state";
-import { UserId } from "@bitwarden/common/types/guid";
-import { UserKey } from "@bitwarden/common/types/key";
-import {
- KdfConfigService,
- DefaultKeyService,
- BiometricsService,
- BiometricStateService,
-} from "@bitwarden/key-management";
-
-export class BrowserKeyService extends DefaultKeyService {
- constructor(
- pinService: PinServiceAbstraction,
- masterPasswordService: InternalMasterPasswordServiceAbstraction,
- keyGenerationService: KeyGenerationService,
- cryptoFunctionService: CryptoFunctionService,
- encryptService: EncryptService,
- platformUtilService: PlatformUtilsService,
- logService: LogService,
- stateService: StateService,
- accountService: AccountService,
- stateProvider: StateProvider,
- private biometricStateService: BiometricStateService,
- private biometricsService: BiometricsService,
- kdfConfigService: KdfConfigService,
- ) {
- super(
- pinService,
- masterPasswordService,
- keyGenerationService,
- cryptoFunctionService,
- encryptService,
- platformUtilService,
- logService,
- stateService,
- accountService,
- stateProvider,
- kdfConfigService,
- );
- }
- override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise {
- if (keySuffix === KeySuffixOptions.Biometric) {
- const biometricUnlockPromise =
- userId == null
- ? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
- : this.biometricStateService.getBiometricUnlockEnabled(userId);
- return await biometricUnlockPromise;
- }
- return super.hasUserKeyStored(keySuffix, userId);
- }
-
- /**
- * Browser doesn't store biometric keys, so we retrieve them from the desktop and return
- * if we successfully saved it into memory as the User Key
- * @returns the `UserKey` if the user passes a biometrics prompt, otherwise return `null`.
- */
- protected override async getKeyFromStorage(
- keySuffix: KeySuffixOptions,
- userId?: UserId,
- ): Promise {
- if (keySuffix === KeySuffixOptions.Biometric) {
- const biometricsResult = await this.biometricsService.authenticateBiometric();
-
- if (!biometricsResult) {
- return null;
- }
-
- const userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId));
- if (userKey) {
- return userKey;
- }
- }
-
- return await super.getKeyFromStorage(keySuffix, userId);
- }
-}
diff --git a/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts
index 272201c6ede..4b0323d5ebe 100644
--- a/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts
+++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts
@@ -9,8 +9,8 @@ import {
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
-import { KeyService, BiometricsService } from "@bitwarden/key-management";
-import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/key-management/angular";
+import { KeyService, BiometricsService, BiometricsStatus } from "@bitwarden/key-management";
+import { UnlockOptions } from "@bitwarden/key-management/angular";
import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service";
@@ -121,8 +121,7 @@ describe("ExtensionLockComponentService", () => {
describe("getAvailableUnlockOptions$", () => {
interface MockInputs {
hasMasterPassword: boolean;
- osSupportsBiometric: boolean;
- biometricLockSet: boolean;
+ biometricsStatusForUser: BiometricsStatus;
hasBiometricEncryptedUserKeyStored: boolean;
platformSupportsSecureStorage: boolean;
pinDecryptionAvailable: boolean;
@@ -133,8 +132,7 @@ describe("ExtensionLockComponentService", () => {
// MP + PIN + Biometrics available
{
hasMasterPassword: true,
- osSupportsBiometric: true,
- biometricLockSet: true,
+ biometricsStatusForUser: BiometricsStatus.Available,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: true,
@@ -148,7 +146,7 @@ describe("ExtensionLockComponentService", () => {
},
biometrics: {
enabled: true,
- disableReason: null,
+ biometricsStatus: BiometricsStatus.Available,
},
},
],
@@ -156,8 +154,7 @@ describe("ExtensionLockComponentService", () => {
// PIN + Biometrics available
{
hasMasterPassword: false,
- osSupportsBiometric: true,
- biometricLockSet: true,
+ biometricsStatusForUser: BiometricsStatus.Available,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: true,
@@ -171,7 +168,7 @@ describe("ExtensionLockComponentService", () => {
},
biometrics: {
enabled: true,
- disableReason: null,
+ biometricsStatus: BiometricsStatus.Available,
},
},
],
@@ -179,8 +176,7 @@ describe("ExtensionLockComponentService", () => {
// Biometrics available: user key stored with no secure storage
{
hasMasterPassword: false,
- osSupportsBiometric: true,
- biometricLockSet: true,
+ biometricsStatusForUser: BiometricsStatus.Available,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: false,
pinDecryptionAvailable: false,
@@ -194,7 +190,7 @@ describe("ExtensionLockComponentService", () => {
},
biometrics: {
enabled: true,
- disableReason: null,
+ biometricsStatus: BiometricsStatus.Available,
},
},
],
@@ -202,8 +198,7 @@ describe("ExtensionLockComponentService", () => {
// Biometrics available: no user key stored with no secure storage
{
hasMasterPassword: false,
- osSupportsBiometric: true,
- biometricLockSet: true,
+ biometricsStatusForUser: BiometricsStatus.Available,
hasBiometricEncryptedUserKeyStored: false,
platformSupportsSecureStorage: false,
pinDecryptionAvailable: false,
@@ -217,7 +212,7 @@ describe("ExtensionLockComponentService", () => {
},
biometrics: {
enabled: true,
- disableReason: null,
+ biometricsStatus: BiometricsStatus.Available,
},
},
],
@@ -225,8 +220,7 @@ describe("ExtensionLockComponentService", () => {
// Biometrics not available: biometric lock not set
{
hasMasterPassword: false,
- osSupportsBiometric: true,
- biometricLockSet: false,
+ biometricsStatusForUser: BiometricsStatus.UnlockNeeded,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: false,
@@ -240,7 +234,7 @@ describe("ExtensionLockComponentService", () => {
},
biometrics: {
enabled: false,
- disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
+ biometricsStatus: BiometricsStatus.UnlockNeeded,
},
},
],
@@ -248,8 +242,7 @@ describe("ExtensionLockComponentService", () => {
// Biometrics not available: user key not stored
{
hasMasterPassword: false,
- osSupportsBiometric: true,
- biometricLockSet: true,
+ biometricsStatusForUser: BiometricsStatus.NotEnabledInConnectedDesktopApp,
hasBiometricEncryptedUserKeyStored: false,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: false,
@@ -263,7 +256,7 @@ describe("ExtensionLockComponentService", () => {
},
biometrics: {
enabled: false,
- disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
+ biometricsStatus: BiometricsStatus.NotEnabledInConnectedDesktopApp,
},
},
],
@@ -271,8 +264,7 @@ describe("ExtensionLockComponentService", () => {
// Biometrics not available: OS doesn't support
{
hasMasterPassword: false,
- osSupportsBiometric: false,
- biometricLockSet: true,
+ biometricsStatusForUser: BiometricsStatus.HardwareUnavailable,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: false,
@@ -286,7 +278,7 @@ describe("ExtensionLockComponentService", () => {
},
biometrics: {
enabled: false,
- disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem,
+ biometricsStatus: BiometricsStatus.HardwareUnavailable,
},
},
],
@@ -304,8 +296,12 @@ describe("ExtensionLockComponentService", () => {
);
// Biometrics
- biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric);
- vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet);
+ biometricsService.getBiometricsStatusForUser.mockResolvedValue(
+ mockInputs.biometricsStatusForUser,
+ );
+ vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(
+ mockInputs.hasBiometricEncryptedUserKeyStored,
+ );
keyService.hasUserKeyStored.mockResolvedValue(mockInputs.hasBiometricEncryptedUserKeyStored);
platformUtilsService.supportsSecureStorage.mockReturnValue(
mockInputs.platformSupportsSecureStorage,
diff --git a/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts
index 07fb2ec6b87..f21beb91cff 100644
--- a/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts
+++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts
@@ -7,27 +7,17 @@ import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
-import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
-import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
-import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { UserId } from "@bitwarden/common/types/guid";
-import { KeyService, BiometricsService } from "@bitwarden/key-management";
-import {
- LockComponentService,
- BiometricsDisableReason,
- UnlockOptions,
-} from "@bitwarden/key-management/angular";
+import { BiometricsService, BiometricsStatus } from "@bitwarden/key-management";
+import { LockComponentService, UnlockOptions } from "@bitwarden/key-management/angular";
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service";
export class ExtensionLockComponentService implements LockComponentService {
private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction);
- private readonly platformUtilsService = inject(PlatformUtilsService);
private readonly biometricsService = inject(BiometricsService);
private readonly pinService = inject(PinServiceAbstraction);
- private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
- private readonly keyService = inject(KeyService);
private readonly routerService = inject(BrowserRouterService);
getPreviousUrl(): string | null {
@@ -52,67 +42,28 @@ export class ExtensionLockComponentService implements LockComponentService {
return "unlockWithBiometrics";
}
- private async isBiometricLockSet(userId: UserId): Promise {
- const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId);
- const hasBiometricEncryptedUserKeyStored = await this.keyService.hasUserKeyStored(
- KeySuffixOptions.Biometric,
- userId,
- );
- const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage();
-
- return (
- biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage)
- );
- }
-
- private getBiometricsDisabledReason(
- osSupportsBiometric: boolean,
- biometricLockSet: boolean,
- ): BiometricsDisableReason | null {
- if (!osSupportsBiometric) {
- return BiometricsDisableReason.NotSupportedOnOperatingSystem;
- } else if (!biometricLockSet) {
- return BiometricsDisableReason.EncryptedKeysUnavailable;
- }
-
- return null;
- }
-
getAvailableUnlockOptions$(userId: UserId): Observable {
return combineLatest([
// Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to
- defer(() => this.biometricsService.supportsBiometric()),
- defer(() => this.isBiometricLockSet(userId)),
+ defer(async () => await this.biometricsService.getBiometricsStatusForUser(userId)),
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
defer(() => this.pinService.isPinDecryptionAvailable(userId)),
]).pipe(
- map(
- ([
- supportsBiometric,
- isBiometricsLockSet,
- userDecryptionOptions,
- pinDecryptionAvailable,
- ]) => {
- const disableReason = this.getBiometricsDisabledReason(
- supportsBiometric,
- isBiometricsLockSet,
- );
-
- const unlockOpts: UnlockOptions = {
- masterPassword: {
- enabled: userDecryptionOptions.hasMasterPassword,
- },
- pin: {
- enabled: pinDecryptionAvailable,
- },
- biometrics: {
- enabled: supportsBiometric && isBiometricsLockSet,
- disableReason: disableReason,
- },
- };
- return unlockOpts;
- },
- ),
+ map(([biometricsStatus, userDecryptionOptions, pinDecryptionAvailable]) => {
+ const unlockOpts: UnlockOptions = {
+ masterPassword: {
+ enabled: userDecryptionOptions.hasMasterPassword,
+ },
+ pin: {
+ enabled: pinDecryptionAvailable,
+ },
+ biometrics: {
+ enabled: biometricsStatus === BiometricsStatus.Available,
+ biometricsStatus: biometricsStatus,
+ },
+ };
+ return unlockOpts;
+ }),
);
}
}
diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts
index 6542eb9c814..0fb21732fdd 100644
--- a/apps/browser/src/popup/services/services.module.ts
+++ b/apps/browser/src/popup/services/services.module.ts
@@ -111,8 +111,8 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac
import {
KdfConfigService,
KeyService,
- BiometricStateService,
BiometricsService,
+ DefaultKeyService,
} from "@bitwarden/key-management";
import { LockComponentService } from "@bitwarden/key-management/angular";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -126,7 +126,6 @@ import { AutofillService as AutofillServiceAbstraction } from "../../autofill/se
import AutofillService from "../../autofill/services/autofill.service";
import { InlineMenuFieldQualificationService } from "../../autofill/services/inline-menu-field-qualification.service";
import { ForegroundBrowserBiometricsService } from "../../key-management/biometrics/foreground-browser-biometrics";
-import { BrowserKeyService } from "../../key-management/browser-key.service";
import { ExtensionLockComponentService } from "../../key-management/lock/services/extension-lock-component.service";
import { BrowserApi } from "../../platform/browser/browser-api";
import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator";
@@ -232,11 +231,9 @@ const safeProviders: SafeProvider[] = [
stateService: StateService,
accountService: AccountServiceAbstraction,
stateProvider: StateProvider,
- biometricStateService: BiometricStateService,
- biometricsService: BiometricsService,
kdfConfigService: KdfConfigService,
) => {
- const keyService = new BrowserKeyService(
+ const keyService = new DefaultKeyService(
pinService,
masterPasswordService,
keyGenerationService,
@@ -247,8 +244,6 @@ const safeProviders: SafeProvider[] = [
stateService,
accountService,
stateProvider,
- biometricStateService,
- biometricsService,
kdfConfigService,
);
new ContainerService(keyService, encryptService).attachToGlobal(self);
@@ -265,8 +260,6 @@ const safeProviders: SafeProvider[] = [
StateService,
AccountServiceAbstraction,
StateProvider,
- BiometricStateService,
- BiometricsService,
KdfConfigService,
],
}),
diff --git a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift
index 1768ce6b15f..58d95f959be 100644
--- a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift
+++ b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift
@@ -86,8 +86,203 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
context.completeRequest(returningItems: [response], completionHandler: nil)
}
return
- case "biometricUnlock":
+ case "authenticateWithBiometrics":
+ let messageId = message?["messageId"] as? Int
+ let laContext = LAContext()
+ guard let accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.privateKeyUsage, .userPresence], nil) else {
+ response.userInfo = [
+ SFExtensionMessageKey: [
+ "message": [
+ "command": "authenticateWithBiometrics",
+ "response": false,
+ "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
+ "messageId": messageId,
+ ],
+ ],
+ ]
+ break
+ }
+ laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "authenticate") { (success, error) in
+ if success {
+ response.userInfo = [ SFExtensionMessageKey: [
+ "message": [
+ "command": "authenticateWithBiometrics",
+ "response": true,
+ "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
+ "messageId": messageId,
+ ],
+ ]]
+ } else {
+ response.userInfo = [ SFExtensionMessageKey: [
+ "message": [
+ "command": "authenticateWithBiometrics",
+ "response": false,
+ "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
+ "messageId": messageId,
+ ],
+ ]]
+ }
+ context.completeRequest(returningItems: [response], completionHandler: nil)
+ }
+ return
+ case "getBiometricsStatus":
+ let messageId = message?["messageId"] as? Int
+ response.userInfo = [
+ SFExtensionMessageKey: [
+ "message": [
+ "command": "getBiometricsStatus",
+ "response": BiometricsStatus.Available.rawValue,
+ "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
+ "messageId": messageId,
+ ],
+ ],
+ ]
+
+ context.completeRequest(returningItems: [response], completionHandler: nil);
+ break
+ case "unlockWithBiometricsForUser":
+ let messageId = message?["messageId"] as? Int
+ var error: NSError?
+ let laContext = LAContext()
+
+ laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
+
+ if let e = error, e.code != kLAErrorBiometryLockout {
+ response.userInfo = [
+ SFExtensionMessageKey: [
+ "message": [
+ "command": "biometricUnlock",
+ "response": false,
+ "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
+ "messageId": messageId,
+ ],
+ ],
+ ]
+
+ context.completeRequest(returningItems: [response], completionHandler: nil)
+ break
+ }
+
+ guard let accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.privateKeyUsage, .userPresence], nil) else {
+ let messageId = message?["messageId"] as? Int
+ response.userInfo = [
+ SFExtensionMessageKey: [
+ "message": [
+ "command": "biometricUnlock",
+ "response": false,
+ "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
+ "messageId": messageId,
+ ],
+ ],
+ ]
+
+ context.completeRequest(returningItems: [response], completionHandler: nil)
+ break
+ }
+ laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "unlock your vault") { (success, error) in
+ if success {
+ guard let userId = message?["userId"] as? String else {
+ return
+ }
+ let passwordName = userId + "_user_biometric"
+ var passwordLength: UInt32 = 0
+ var passwordPtr: UnsafeMutableRawPointer? = nil
+
+ var status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(passwordName.utf8.count), passwordName, &passwordLength, &passwordPtr, nil)
+ if status != errSecSuccess {
+ let fallbackName = "key"
+ status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(fallbackName.utf8.count), fallbackName, &passwordLength, &passwordPtr, nil)
+ }
+
+ if status == errSecSuccess {
+ let result = NSString(bytes: passwordPtr!, length: Int(passwordLength), encoding: String.Encoding.utf8.rawValue) as String?
+ SecKeychainItemFreeContent(nil, passwordPtr)
+
+ response.userInfo = [ SFExtensionMessageKey: [
+ "message": [
+ "command": "biometricUnlock",
+ "response": true,
+ "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
+ "userKeyB64": result!.replacingOccurrences(of: "\"", with: ""),
+ "messageId": messageId,
+ ],
+ ]]
+ } else {
+ response.userInfo = [
+ SFExtensionMessageKey: [
+ "message": [
+ "command": "biometricUnlock",
+ "response": true,
+ "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
+ "messageId": messageId,
+ ],
+ ],
+ ]
+ }
+ }
+
+ context.completeRequest(returningItems: [response], completionHandler: nil)
+ }
+ return
+ case "getBiometricsStatusForUser":
+ let messageId = message?["messageId"] as? Int
+ let laContext = LAContext()
+ if !laContext.isBiometricsAvailable() {
+ response.userInfo = [
+ SFExtensionMessageKey: [
+ "message": [
+ "command": "getBiometricsStatusForUser",
+ "response": BiometricsStatus.HardwareUnavailable.rawValue,
+ "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
+ "messageId": messageId,
+ ],
+ ],
+ ]
+
+ context.completeRequest(returningItems: [response], completionHandler: nil)
+ break
+ }
+
+ guard let userId = message?["userId"] as? String else {
+ return
+ }
+ let passwordName = userId + "_user_biometric"
+ var passwordLength: UInt32 = 0
+ var passwordPtr: UnsafeMutableRawPointer? = nil
+
+ var status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(passwordName.utf8.count), passwordName, &passwordLength, &passwordPtr, nil)
+ if status != errSecSuccess {
+ let fallbackName = "key"
+ status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(fallbackName.utf8.count), fallbackName, &passwordLength, &passwordPtr, nil)
+ }
+
+ if status == errSecSuccess {
+ response.userInfo = [
+ SFExtensionMessageKey: [
+ "message": [
+ "command": "getBiometricsStatusForUser",
+ "response": BiometricsStatus.Available.rawValue,
+ "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
+ "messageId": messageId,
+ ],
+ ],
+ ]
+ } else {
+ response.userInfo = [
+ SFExtensionMessageKey: [
+ "message": [
+ "command": "getBiometricsStatusForUser",
+ "response": BiometricsStatus.NotEnabledInConnectedDesktopApp.rawValue,
+ "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
+ "messageId": messageId,
+ ],
+ ],
+ ]
+ }
+ break
+ case "biometricUnlock":
+ var error: NSError?
let laContext = LAContext()
if(!laContext.isBiometricsAvailable()){
@@ -115,7 +310,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
]
break
}
- laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "Bitwarden Safari Extension") { (success, error) in
+ laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "Biometric Unlock") { (success, error) in
if success {
guard let userId = message?["userId"] as? String else {
return
@@ -157,7 +352,6 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
context.completeRequest(returningItems: [response], completionHandler: nil)
}
-
return
case "biometricUnlockAvailable":
let laContext = LAContext()
@@ -228,3 +422,15 @@ class DownloadFileMessage: Decodable, Encodable {
class DownloadFileMessageBlobOptions: Decodable, Encodable {
var type: String?
}
+
+enum BiometricsStatus : Int {
+ case Available = 0
+ case UnlockNeeded = 1
+ case HardwareUnavailable = 2
+ case AutoSetupNeeded = 3
+ case ManualSetupNeeded = 4
+ case PlatformUnsupported = 5
+ case DesktopDisconnected = 6
+ case NotEnabledLocally = 7
+ case NotEnabledInConnectedDesktopApp = 8
+}
diff --git a/apps/cli/src/key-management/cli-biometrics-service.ts b/apps/cli/src/key-management/cli-biometrics-service.ts
new file mode 100644
index 00000000000..bda8fe82895
--- /dev/null
+++ b/apps/cli/src/key-management/cli-biometrics-service.ts
@@ -0,0 +1,27 @@
+import { UserId } from "@bitwarden/common/types/guid";
+import { UserKey } from "@bitwarden/common/types/key";
+import { BiometricsService, BiometricsStatus } from "@bitwarden/key-management";
+
+export class CliBiometricsService extends BiometricsService {
+ async authenticateWithBiometrics(): Promise {
+ return false;
+ }
+
+ async getBiometricsStatus(): Promise {
+ return BiometricsStatus.PlatformUnsupported;
+ }
+
+ async unlockWithBiometricsForUser(userId: UserId): Promise {
+ return null;
+ }
+
+ async getBiometricsStatusForUser(userId: UserId): Promise {
+ return BiometricsStatus.PlatformUnsupported;
+ }
+
+ async getShouldAutopromptNow(): Promise {
+ return false;
+ }
+
+ async setShouldAutopromptNow(value: boolean): Promise {}
+}
diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts
index bef4d52fad5..f57db9909d6 100644
--- a/apps/cli/src/service-container/service-container.ts
+++ b/apps/cli/src/service-container/service-container.ts
@@ -165,6 +165,7 @@ import {
VaultExportServiceAbstraction,
} from "@bitwarden/vault-export-core";
+import { CliBiometricsService } from "../key-management/cli-biometrics-service";
import { flagEnabled } from "../platform/flags";
import { CliPlatformUtilsService } from "../platform/services/cli-platform-utils.service";
import { ConsoleLogService } from "../platform/services/console-log.service";
@@ -693,12 +694,12 @@ export class ServiceContainer {
this.userVerificationApiService,
this.userDecryptionOptionsService,
this.pinService,
- this.logService,
- this.vaultTimeoutSettingsService,
- this.platformUtilsService,
this.kdfConfigService,
+ new CliBiometricsService(),
);
+ const biometricService = new CliBiometricsService();
+
this.vaultTimeoutService = new VaultTimeoutService(
this.accountService,
this.masterPasswordService,
@@ -714,6 +715,7 @@ export class ServiceContainer {
this.stateEventRunnerService,
this.taskSchedulerService,
this.logService,
+ biometricService,
lockedCallback,
undefined,
);
diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts
index c27ca240d3f..19748e797bb 100644
--- a/apps/desktop/src/app/accounts/settings.component.ts
+++ b/apps/desktop/src/app/accounts/settings.component.ts
@@ -22,7 +22,7 @@ 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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
-import { KeySuffixOptions, ThemeType } from "@bitwarden/common/platform/enums";
+import { ThemeType } from "@bitwarden/common/platform/enums/theme-type.enum";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { UserId } from "@bitwarden/common/types/guid";
@@ -32,10 +32,11 @@ import {
VaultTimeoutStringType,
} from "@bitwarden/common/types/vault-timeout.type";
import { DialogService } from "@bitwarden/components";
-import { KeyService, BiometricsService, BiometricStateService } from "@bitwarden/key-management";
+import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management";
import { SetPinComponent } from "../../auth/components/set-pin.component";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
+import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
import { NativeMessagingManifestService } from "../services/native-messaging-manifest.service";
@@ -54,6 +55,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
themeOptions: any[];
clearClipboardOptions: any[];
supportsBiometric: boolean;
+ private timerId: any;
showAlwaysShowDock = false;
requireEnableTray = false;
showDuckDuckGoIntegrationOption = false;
@@ -139,7 +141,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
private userVerificationService: UserVerificationServiceAbstraction,
private desktopSettingsService: DesktopSettingsService,
private biometricStateService: BiometricStateService,
- private biometricsService: BiometricsService,
+ private biometricsService: DesktopBiometricsService,
private desktopAutofillSettingsService: DesktopAutofillSettingsService,
private pinService: PinServiceAbstraction,
private logService: LogService,
@@ -297,7 +299,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
// Non-form values
this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop;
this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
- this.supportsBiometric = await this.biometricsService.supportsBiometric();
this.previousVaultTimeout = this.form.value.vaultTimeout;
this.refreshTimeoutSettings$
@@ -360,6 +361,13 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.form.controls.enableBrowserIntegrationFingerprint.disable();
}
});
+
+ this.supportsBiometric =
+ (await this.biometricsService.getBiometricsStatus()) === BiometricsStatus.Available;
+ this.timerId = setInterval(async () => {
+ this.supportsBiometric =
+ (await this.biometricsService.getBiometricsStatus()) === BiometricsStatus.Available;
+ }, 1000);
}
async saveVaultTimeout(newValue: VaultTimeout) {
@@ -476,23 +484,20 @@ export class SettingsComponent implements OnInit, OnDestroy {
return;
}
- const needsSetup = await this.biometricsService.biometricsNeedsSetup();
- const supportsBiometricAutoSetup = await this.biometricsService.biometricsSupportsAutoSetup();
+ const status = await this.biometricsService.getBiometricsStatus();
- if (needsSetup) {
- if (supportsBiometricAutoSetup) {
- await this.biometricsService.biometricsSetup();
- } else {
- const confirmed = await this.dialogService.openSimpleDialog({
- title: { key: "biometricsManualSetupTitle" },
- content: { key: "biometricsManualSetupDesc" },
- type: "warning",
- });
- if (confirmed) {
- this.platformUtilsService.launchUri("https://bitwarden.com/help/biometrics/");
- }
- return;
+ if (status === BiometricsStatus.AutoSetupNeeded) {
+ await this.biometricsService.setupBiometrics();
+ } else if (status === BiometricsStatus.ManualSetupNeeded) {
+ const confirmed = await this.dialogService.openSimpleDialog({
+ title: { key: "biometricsManualSetupTitle" },
+ content: { key: "biometricsManualSetupDesc" },
+ type: "warning",
+ });
+ if (confirmed) {
+ this.platformUtilsService.launchUri("https://bitwarden.com/help/biometrics/");
}
+ return;
}
await this.biometricStateService.setBiometricUnlockEnabled(true);
@@ -513,8 +518,13 @@ export class SettingsComponent implements OnInit, OnDestroy {
}
await this.keyService.refreshAdditionalKeys();
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ );
// Validate the key is stored in case biometrics fail.
- const biometricSet = await this.keyService.hasUserKeyStored(KeySuffixOptions.Biometric);
+ const biometricSet =
+ (await this.biometricsService.getBiometricsStatusForUser(activeUserId)) ===
+ BiometricsStatus.Available;
this.form.controls.biometric.setValue(biometricSet, { emitEvent: false });
if (!biometricSet) {
await this.biometricStateService.setBiometricUnlockEnabled(false);
@@ -779,6 +789,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
+ clearInterval(this.timerId);
}
get biometricText() {
diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts
index cbd0dcf78aa..db8c2a85bde 100644
--- a/apps/desktop/src/app/layout/account-switcher.component.ts
+++ b/apps/desktop/src/app/layout/account-switcher.component.ts
@@ -17,6 +17,8 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging";
import { UserId } from "@bitwarden/common/types/guid";
+import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
+
type ActiveAccount = {
id: string;
name: string;
@@ -90,6 +92,7 @@ export class AccountSwitcherComponent implements OnInit {
private environmentService: EnvironmentService,
private loginEmailService: LoginEmailServiceAbstraction,
private accountService: AccountService,
+ private biometricsService: DesktopBiometricsService,
) {
this.activeAccount$ = this.accountService.activeAccount$.pipe(
switchMap(async (active) => {
@@ -181,6 +184,7 @@ export class AccountSwitcherComponent implements OnInit {
async switch(userId: string) {
this.close();
+ await this.biometricsService.setShouldAutopromptNow(true);
this.disabled = true;
const accountSwitchFinishedPromise = firstValueFrom(
diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts
index 87c2a833073..8b890032443 100644
--- a/apps/desktop/src/app/services/services.module.ts
+++ b/apps/desktop/src/app/services/services.module.ts
@@ -102,7 +102,8 @@ import { DesktopLoginComponentService } from "../../auth/login/desktop-login-com
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service";
import { DesktopFido2UserInterfaceService } from "../../autofill/services/desktop-fido2-user-interface.service";
-import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service";
+import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
+import { RendererBiometricsService } from "../../key-management/biometrics/renderer-biometrics.service";
import { DesktopLockComponentService } from "../../key-management/lock/services/desktop-lock-component.service";
import { flagEnabled } from "../../platform/flags";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
@@ -142,7 +143,12 @@ const safeProviders: SafeProvider[] = [
safeProvider(InitService),
safeProvider({
provide: BiometricsService,
- useClass: ElectronBiometricsService,
+ useClass: RendererBiometricsService,
+ deps: [],
+ }),
+ safeProvider({
+ provide: DesktopBiometricsService,
+ useClass: RendererBiometricsService,
deps: [],
}),
safeProvider(NativeMessagingService),
@@ -241,6 +247,7 @@ const safeProviders: SafeProvider[] = [
VaultTimeoutSettingsService,
BiometricStateService,
AccountServiceAbstraction,
+ LogService,
],
}),
safeProvider({
@@ -302,6 +309,7 @@ const safeProviders: SafeProvider[] = [
StateProvider,
BiometricStateService,
KdfConfigService,
+ DesktopBiometricsService,
],
}),
safeProvider({
diff --git a/apps/desktop/src/key-management/biometrics/biometric.noop.main.ts b/apps/desktop/src/key-management/biometrics/biometric.noop.main.ts
deleted file mode 100644
index 57a86942e8c..00000000000
--- a/apps/desktop/src/key-management/biometrics/biometric.noop.main.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { OsBiometricService } from "./desktop.biometrics.service";
-
-export default class NoopBiometricsService implements OsBiometricService {
- constructor() {}
-
- async init() {}
-
- async osSupportsBiometric(): Promise {
- return false;
- }
-
- async osBiometricsNeedsSetup(): Promise {
- return false;
- }
-
- async osBiometricsCanAutoSetup(): Promise {
- return false;
- }
-
- async osBiometricsSetup(): Promise {}
-
- async getBiometricKey(
- service: string,
- storageKey: string,
- clientKeyHalfB64: string,
- ): Promise {
- return null;
- }
-
- async setBiometricKey(
- service: string,
- storageKey: string,
- value: string,
- clientKeyPartB64: string | undefined,
- ): Promise {
- return;
- }
-
- async deleteBiometricKey(service: string, key: string): Promise {}
-
- async authenticateBiometric(): Promise {
- throw new Error("Not supported on this platform");
- }
-}
diff --git a/apps/desktop/src/key-management/biometrics/biometric.renderer-ipc.listener.ts b/apps/desktop/src/key-management/biometrics/biometric.renderer-ipc.listener.ts
deleted file mode 100644
index a057deca54f..00000000000
--- a/apps/desktop/src/key-management/biometrics/biometric.renderer-ipc.listener.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { ipcMain } from "electron";
-
-import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
-
-import { BiometricMessage, BiometricAction } from "../../types/biometric-message";
-
-import { DesktopBiometricsService } from "./desktop.biometrics.service";
-
-export class BiometricsRendererIPCListener {
- constructor(
- private serviceName: string,
- private biometricService: DesktopBiometricsService,
- private logService: ConsoleLogService,
- ) {}
-
- init() {
- ipcMain.handle("biometric", async (event: any, message: BiometricMessage) => {
- try {
- let serviceName = this.serviceName;
- message.keySuffix = "_" + (message.keySuffix ?? "");
- if (message.keySuffix !== "_") {
- serviceName += message.keySuffix;
- }
-
- let val: string | boolean = null;
-
- if (!message.action) {
- return val;
- }
-
- switch (message.action) {
- case BiometricAction.EnabledForUser:
- if (!message.key || !message.userId) {
- break;
- }
- val = await this.biometricService.canAuthBiometric({
- service: serviceName,
- key: message.key,
- userId: message.userId,
- });
- break;
- case BiometricAction.OsSupported:
- val = await this.biometricService.supportsBiometric();
- break;
- case BiometricAction.NeedsSetup:
- val = await this.biometricService.biometricsNeedsSetup();
- break;
- case BiometricAction.Setup:
- await this.biometricService.biometricsSetup();
- break;
- case BiometricAction.CanAutoSetup:
- val = await this.biometricService.biometricsSupportsAutoSetup();
- break;
- default:
- }
-
- return val;
- } catch (e) {
- this.logService.info(e);
- }
- });
- }
-}
diff --git a/apps/desktop/src/key-management/biometrics/biometrics.service.spec.ts b/apps/desktop/src/key-management/biometrics/biometrics.service.spec.ts
index d2ed648ba65..e69ebca3630 100644
--- a/apps/desktop/src/key-management/biometrics/biometrics.service.spec.ts
+++ b/apps/desktop/src/key-management/biometrics/biometrics.service.spec.ts
@@ -4,14 +4,19 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { UserId } from "@bitwarden/common/types/guid";
-import { BiometricStateService } from "@bitwarden/key-management";
+import {
+ BiometricsService,
+ BiometricsStatus,
+ BiometricStateService,
+} from "@bitwarden/key-management";
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 "./desktop.biometrics.service";
+import { MainBiometricsService } from "./main-biometrics.service";
+import OsBiometricsServiceLinux from "./os-biometrics-linux.service";
+import OsBiometricsServiceMac from "./os-biometrics-mac.service";
+import OsBiometricsServiceWindows from "./os-biometrics-windows.service";
+import { OsBiometricService } from "./os-biometrics.service";
jest.mock("@bitwarden/desktop-napi", () => {
return {
@@ -28,8 +33,7 @@ describe("biometrics tests", function () {
const biometricStateService = mock();
it("Should call the platformspecific methods", async () => {
- const userId = "userId-1" as UserId;
- const sut = new BiometricsService(
+ const sut = new MainBiometricsService(
i18nService,
windowMain,
logService,
@@ -39,21 +43,15 @@ describe("biometrics tests", function () {
);
const mockService = mock();
- (sut as any).platformSpecificService = mockService;
- await sut.setEncryptionKeyHalf({ service: "test", key: "test", value: "test" });
+ (sut as any).osBiometricsService = mockService;
- await sut.canAuthBiometric({ service: "test", key: "test", userId });
- expect(mockService.osSupportsBiometric).toBeCalled();
-
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- sut.authenticateBiometric();
+ await sut.authenticateBiometric();
expect(mockService.authenticateBiometric).toBeCalled();
});
describe("Should create a platform specific service", function () {
it("Should create a biometrics service specific for Windows", () => {
- const sut = new BiometricsService(
+ const sut = new MainBiometricsService(
i18nService,
windowMain,
logService,
@@ -62,13 +60,13 @@ describe("biometrics tests", function () {
biometricStateService,
);
- const internalService = (sut as any).platformSpecificService;
+ const internalService = (sut as any).osBiometricsService;
expect(internalService).not.toBeNull();
- expect(internalService).toBeInstanceOf(BiometricWindowsMain);
+ expect(internalService).toBeInstanceOf(OsBiometricsServiceWindows);
});
it("Should create a biometrics service specific for MacOs", () => {
- const sut = new BiometricsService(
+ const sut = new MainBiometricsService(
i18nService,
windowMain,
logService,
@@ -76,19 +74,33 @@ describe("biometrics tests", function () {
"darwin",
biometricStateService,
);
- const internalService = (sut as any).platformSpecificService;
+ const internalService = (sut as any).osBiometricsService;
expect(internalService).not.toBeNull();
- expect(internalService).toBeInstanceOf(BiometricDarwinMain);
+ expect(internalService).toBeInstanceOf(OsBiometricsServiceMac);
+ });
+
+ it("Should create a biometrics service specific for Linux", () => {
+ const sut = new MainBiometricsService(
+ i18nService,
+ windowMain,
+ logService,
+ messagingService,
+ "linux",
+ biometricStateService,
+ );
+
+ const internalService = (sut as any).osBiometricsService;
+ expect(internalService).not.toBeNull();
+ expect(internalService).toBeInstanceOf(OsBiometricsServiceLinux);
});
});
describe("can auth biometric", () => {
let sut: BiometricsService;
let innerService: MockProxy;
- const userId = "userId-1" as UserId;
beforeEach(() => {
- sut = new BiometricsService(
+ sut = new MainBiometricsService(
i18nService,
windowMain,
logService,
@@ -98,34 +110,78 @@ describe("biometrics tests", function () {
);
innerService = mock();
- (sut as any).platformSpecificService = innerService;
+ (sut as any).osBiometricsService = innerService;
});
- it("should return false if client key half is required and not provided", async () => {
- biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true);
+ it("should return the correct biometric status for system status", async () => {
+ const testCases = [
+ // happy path
+ [true, false, false, BiometricsStatus.Available],
+ [false, true, true, BiometricsStatus.AutoSetupNeeded],
+ [false, true, false, BiometricsStatus.ManualSetupNeeded],
+ [false, false, false, BiometricsStatus.HardwareUnavailable],
- const result = await sut.canAuthBiometric({ service: "test", key: "test", userId });
+ // should not happen
+ [false, false, true, BiometricsStatus.HardwareUnavailable],
+ [true, true, true, BiometricsStatus.Available],
+ [true, true, false, BiometricsStatus.Available],
+ [true, false, true, BiometricsStatus.Available],
+ ];
- expect(result).toBe(false);
+ for (const [supportsBiometric, needsSetup, canAutoSetup, expected] of testCases) {
+ innerService.osSupportsBiometric.mockResolvedValue(supportsBiometric as boolean);
+ innerService.osBiometricsNeedsSetup.mockResolvedValue(needsSetup as boolean);
+ innerService.osBiometricsCanAutoSetup.mockResolvedValue(canAutoSetup as boolean);
+
+ const actual = await sut.getBiometricsStatus();
+ expect(actual).toBe(expected);
+ }
});
- it("should call osSupportsBiometric if client key half is provided", async () => {
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- sut.setEncryptionKeyHalf({ service: "test", key: "test", value: "test" });
+ it("should return the correct biometric status for user status", async () => {
+ const testCases = [
+ // system status, biometric unlock enabled, require password on start, has key half, result
+ [BiometricsStatus.Available, false, false, false, BiometricsStatus.NotEnabledLocally],
+ [BiometricsStatus.Available, false, true, false, BiometricsStatus.NotEnabledLocally],
+ [BiometricsStatus.Available, false, false, true, BiometricsStatus.NotEnabledLocally],
+ [BiometricsStatus.Available, false, true, true, BiometricsStatus.NotEnabledLocally],
- await sut.canAuthBiometric({ service: "test", key: "test", userId });
- expect(innerService.osSupportsBiometric).toBeCalled();
- });
+ [
+ BiometricsStatus.PlatformUnsupported,
+ true,
+ true,
+ true,
+ BiometricsStatus.PlatformUnsupported,
+ ],
+ [BiometricsStatus.ManualSetupNeeded, true, true, true, BiometricsStatus.ManualSetupNeeded],
+ [BiometricsStatus.AutoSetupNeeded, true, true, true, BiometricsStatus.AutoSetupNeeded],
- it("should call osSupportBiometric if client key half is not required", async () => {
- biometricStateService.getRequirePasswordOnStart.mockResolvedValue(false);
- innerService.osSupportsBiometric.mockResolvedValue(true);
+ [BiometricsStatus.Available, true, false, true, BiometricsStatus.Available],
+ [BiometricsStatus.Available, true, true, false, BiometricsStatus.UnlockNeeded],
+ [BiometricsStatus.Available, true, false, true, BiometricsStatus.Available],
+ ];
- const result = await sut.canAuthBiometric({ service: "test", key: "test", userId });
+ for (const [
+ systemStatus,
+ unlockEnabled,
+ requirePasswordOnStart,
+ hasKeyHalf,
+ expected,
+ ] of testCases) {
+ sut.getBiometricsStatus = jest.fn().mockResolvedValue(systemStatus as BiometricsStatus);
+ biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(unlockEnabled as boolean);
+ biometricStateService.getRequirePasswordOnStart.mockResolvedValue(
+ requirePasswordOnStart as boolean,
+ );
+ (sut as any).clientKeyHalves = new Map();
+ const userId = "test" as UserId;
+ if (hasKeyHalf) {
+ (sut as any).clientKeyHalves.set(userId, "test");
+ }
- expect(result).toBe(true);
- expect(innerService.osSupportsBiometric).toHaveBeenCalled();
+ const actual = await sut.getBiometricsStatusForUser(userId);
+ expect(actual).toBe(expected);
+ }
});
});
});
diff --git a/apps/desktop/src/key-management/biometrics/biometrics.service.ts b/apps/desktop/src/key-management/biometrics/biometrics.service.ts
deleted file mode 100644
index 3867412d884..00000000000
--- a/apps/desktop/src/key-management/biometrics/biometrics.service.ts
+++ /dev/null
@@ -1,212 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
-import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
-import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
-import { UserId } from "@bitwarden/common/types/guid";
-import { BiometricStateService } from "@bitwarden/key-management";
-
-import { WindowMain } from "../../main/window.main";
-
-import { DesktopBiometricsService, OsBiometricService } from "./desktop.biometrics.service";
-
-export class BiometricsService extends DesktopBiometricsService {
- private platformSpecificService: OsBiometricService;
- private clientKeyHalves = new Map();
-
- constructor(
- private i18nService: I18nService,
- private windowMain: WindowMain,
- private logService: LogService,
- private messagingService: MessagingService,
- private platform: NodeJS.Platform,
- private biometricStateService: BiometricStateService,
- ) {
- super();
- this.loadPlatformSpecificService(this.platform);
- }
-
- private loadPlatformSpecificService(platform: NodeJS.Platform) {
- if (platform === "win32") {
- this.loadWindowsHelloService();
- } else if (platform === "darwin") {
- this.loadMacOSService();
- } else if (platform === "linux") {
- this.loadUnixService();
- } else {
- this.loadNoopBiometricsService();
- }
- }
-
- private loadWindowsHelloService() {
- // eslint-disable-next-line
- const BiometricWindowsMain = require("./biometric.windows.main").default;
- this.platformSpecificService = new BiometricWindowsMain(
- this.i18nService,
- this.windowMain,
- this.logService,
- );
- }
-
- private loadMacOSService() {
- // eslint-disable-next-line
- const BiometricDarwinMain = require("./biometric.darwin.main").default;
- this.platformSpecificService = new BiometricDarwinMain(this.i18nService);
- }
-
- private loadUnixService() {
- // eslint-disable-next-line
- const BiometricUnixMain = require("./biometric.unix.main").default;
- this.platformSpecificService = new BiometricUnixMain(this.i18nService, this.windowMain);
- }
-
- private loadNoopBiometricsService() {
- // eslint-disable-next-line
- const NoopBiometricsService = require("./biometric.noop.main").default;
- this.platformSpecificService = new NoopBiometricsService();
- }
-
- async supportsBiometric() {
- return await this.platformSpecificService.osSupportsBiometric();
- }
-
- async biometricsNeedsSetup() {
- return await this.platformSpecificService.osBiometricsNeedsSetup();
- }
-
- async biometricsSupportsAutoSetup() {
- return await this.platformSpecificService.osBiometricsCanAutoSetup();
- }
-
- async biometricsSetup() {
- await this.platformSpecificService.osBiometricsSetup();
- }
-
- async canAuthBiometric({
- service,
- key,
- userId,
- }: {
- service: string;
- key: string;
- userId: UserId;
- }): Promise {
- const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
- const clientKeyHalfB64 = this.getClientKeyHalf(service, key);
- const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
- return clientKeyHalfSatisfied && (await this.supportsBiometric());
- }
-
- async authenticateBiometric(): Promise {
- let result = false;
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.interruptProcessReload(
- () => {
- return this.platformSpecificService.authenticateBiometric();
- },
- (response) => {
- result = response;
- return !response;
- },
- );
- 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);
-
- return await this.platformSpecificService.getBiometricKey(
- service,
- storageKey,
- this.getClientKeyHalf(service, storageKey),
- );
- });
- }
-
- async setBiometricKey(service: string, storageKey: string, value: string): Promise {
- await this.enforceClientKeyHalf(service, storageKey);
-
- return await this.platformSpecificService.setBiometricKey(
- service,
- storageKey,
- value,
- this.getClientKeyHalf(service, storageKey),
- );
- }
-
- /** Registers the client-side encryption key half for the OS stored Biometric key. The other half is protected by the OS.*/
- async setEncryptionKeyHalf({
- service,
- key,
- value,
- }: {
- service: string;
- key: string;
- value: string;
- }): Promise {
- if (value == null) {
- this.clientKeyHalves.delete(this.clientKeyHalfKey(service, key));
- } else {
- this.clientKeyHalves.set(this.clientKeyHalfKey(service, key), value);
- }
- }
-
- async deleteBiometricKey(service: string, storageKey: string): Promise {
- this.clientKeyHalves.delete(this.clientKeyHalfKey(service, storageKey));
- return await this.platformSpecificService.deleteBiometricKey(service, storageKey);
- }
-
- private async interruptProcessReload(
- callback: () => Promise,
- restartReloadCallback: (arg: T) => boolean = () => false,
- ): Promise {
- this.messagingService.send("cancelProcessReload");
- let restartReload = false;
- let response: T;
- try {
- response = await callback();
- restartReload ||= restartReloadCallback(response);
- } catch (error) {
- if (error.message === "Biometric authentication failed") {
- restartReload = false;
- } else {
- restartReload = true;
- }
- }
-
- if (restartReload) {
- this.messagingService.send("startProcessReload");
- }
-
- return response;
- }
-
- private clientKeyHalfKey(service: string, key: string): string {
- return `${service}:${key}`;
- }
-
- private getClientKeyHalf(service: string, key: string): string | undefined {
- return this.clientKeyHalves.get(this.clientKeyHalfKey(service, key)) ?? undefined;
- }
-
- private async enforceClientKeyHalf(service: string, storageKey: string): Promise {
- // The first half of the storageKey is the userId, separated by `_`
- // We need to extract from the service because the active user isn't properly synced to the main process,
- // So we can't use the observables on `biometricStateService`
- const [userId] = storageKey.split("_");
- const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(
- userId as UserId,
- );
- const clientKeyHalfB64 = this.getClientKeyHalf(service, storageKey);
-
- if (requireClientKeyHalf && !clientKeyHalfB64) {
- throw new Error("Biometric key requirements not met. No client key half provided.");
- }
- }
-}
diff --git a/apps/desktop/src/key-management/biometrics/desktop.biometrics.service.ts b/apps/desktop/src/key-management/biometrics/desktop.biometrics.service.ts
index eee3e5fc7f3..0c0efea78f9 100644
--- a/apps/desktop/src/key-management/biometrics/desktop.biometrics.service.ts
+++ b/apps/desktop/src/key-management/biometrics/desktop.biometrics.service.ts
@@ -1,3 +1,4 @@
+import { UserId } from "@bitwarden/common/types/guid";
import { BiometricsService } from "@bitwarden/key-management";
/**
@@ -5,58 +6,10 @@ import { BiometricsService } from "@bitwarden/key-management";
* specifically for the main process.
*/
export abstract class DesktopBiometricsService extends BiometricsService {
- abstract canAuthBiometric({
- service,
- key,
- userId,
- }: {
- service: string;
- key: string;
- userId: string;
- }): Promise;
- abstract getBiometricKey(service: string, key: string): Promise;
- abstract setBiometricKey(service: string, key: string, value: string): Promise;
- abstract setEncryptionKeyHalf({
- service,
- key,
- value,
- }: {
- service: string;
- key: string;
- value: string;
- }): void;
- abstract deleteBiometricKey(service: string, key: string): Promise;
-}
+ abstract setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise;
+ abstract deleteBiometricUnlockKeyForUser(userId: UserId): Promise;
-export interface OsBiometricService {
- osSupportsBiometric(): Promise;
- /**
- * Check whether support for biometric unlock requires setup. This can be automatic or manual.
- *
- * @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place)
- */
- osBiometricsNeedsSetup: () => Promise;
- /**
- * Check whether biometrics can be automatically setup, or requires user interaction.
- *
- * @returns true if biometrics support can be automatically setup, false if it requires user interaction.
- */
- osBiometricsCanAutoSetup: () => Promise;
- /**
- * Starts automatic biometric setup, which places the required configuration files / changes the required settings.
- */
- osBiometricsSetup: () => Promise;
- authenticateBiometric(): Promise;
- getBiometricKey(
- service: string,
- key: string,
- clientKeyHalfB64: string | undefined,
- ): Promise;
- setBiometricKey(
- service: string,
- key: string,
- value: string,
- clientKeyHalfB64: string | undefined,
- ): Promise;
- deleteBiometricKey(service: string, key: string): Promise;
+ abstract setupBiometrics(): Promise;
+
+ abstract setClientKeyHalfForUser(userId: UserId, value: string): Promise;
}
diff --git a/apps/desktop/src/key-management/biometrics/electron-biometrics.service.ts b/apps/desktop/src/key-management/biometrics/electron-biometrics.service.ts
deleted file mode 100644
index 226c914e6ff..00000000000
--- a/apps/desktop/src/key-management/biometrics/electron-biometrics.service.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Injectable } from "@angular/core";
-
-import { BiometricsService } from "@bitwarden/key-management";
-
-/**
- * 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.keyManagement.biometric.osSupported();
- }
-
- async isBiometricUnlockAvailable(): Promise {
- return await ipc.keyManagement.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.keyManagement.biometric.authenticate();
- }
-
- async biometricsNeedsSetup(): Promise {
- return await ipc.keyManagement.biometric.biometricsNeedsSetup();
- }
-
- async biometricsSupportsAutoSetup(): Promise {
- return await ipc.keyManagement.biometric.biometricsCanAutoSetup();
- }
-
- async biometricsSetup(): Promise {
- return await ipc.keyManagement.biometric.biometricsSetup();
- }
-}
diff --git a/apps/desktop/src/key-management/biometrics/index.ts b/apps/desktop/src/key-management/biometrics/index.ts
deleted file mode 100644
index ad7725d718a..00000000000
--- a/apps/desktop/src/key-management/biometrics/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from "./desktop.biometrics.service";
-export * from "./biometrics.service";
diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts b/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts
new file mode 100644
index 00000000000..eebafd8d48b
--- /dev/null
+++ b/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts
@@ -0,0 +1,63 @@
+import { ipcMain } from "electron";
+
+import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
+import { UserId } from "@bitwarden/common/types/guid";
+
+import { BiometricMessage, BiometricAction } from "../../types/biometric-message";
+
+import { DesktopBiometricsService } from "./desktop.biometrics.service";
+
+export class MainBiometricsIPCListener {
+ constructor(
+ private biometricService: DesktopBiometricsService,
+ private logService: ConsoleLogService,
+ ) {}
+
+ init() {
+ ipcMain.handle("biometric", async (event: any, message: BiometricMessage) => {
+ try {
+ if (!message.action) {
+ return;
+ }
+
+ switch (message.action) {
+ case BiometricAction.Authenticate:
+ return await this.biometricService.authenticateWithBiometrics();
+ case BiometricAction.GetStatus:
+ return await this.biometricService.getBiometricsStatus();
+ case BiometricAction.UnlockForUser:
+ return await this.biometricService.unlockWithBiometricsForUser(
+ message.userId as UserId,
+ );
+ case BiometricAction.GetStatusForUser:
+ return await this.biometricService.getBiometricsStatusForUser(message.userId as UserId);
+ case BiometricAction.SetKeyForUser:
+ return await this.biometricService.setBiometricProtectedUnlockKeyForUser(
+ message.userId as UserId,
+ message.key,
+ );
+ case BiometricAction.RemoveKeyForUser:
+ return await this.biometricService.deleteBiometricUnlockKeyForUser(
+ message.userId as UserId,
+ );
+ case BiometricAction.SetClientKeyHalf:
+ return await this.biometricService.setClientKeyHalfForUser(
+ message.userId as UserId,
+ message.key,
+ );
+ case BiometricAction.Setup:
+ return await this.biometricService.setupBiometrics();
+
+ case BiometricAction.SetShouldAutoprompt:
+ return await this.biometricService.setShouldAutopromptNow(message.data as boolean);
+ case BiometricAction.GetShouldAutoprompt:
+ return await this.biometricService.getShouldAutopromptNow();
+ default:
+ return;
+ }
+ } catch (e) {
+ this.logService.info(e);
+ }
+ });
+ }
+}
diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts
new file mode 100644
index 00000000000..06956503a05
--- /dev/null
+++ b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts
@@ -0,0 +1,167 @@
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
+import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
+import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
+import { UserId } from "@bitwarden/common/types/guid";
+import { UserKey } from "@bitwarden/common/types/key";
+import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management";
+
+import { WindowMain } from "../../main/window.main";
+
+import { DesktopBiometricsService } from "./desktop.biometrics.service";
+import { OsBiometricService } from "./os-biometrics.service";
+
+export class MainBiometricsService extends DesktopBiometricsService {
+ private osBiometricsService: OsBiometricService;
+ private clientKeyHalves = new Map();
+ private shouldAutoPrompt = true;
+
+ constructor(
+ private i18nService: I18nService,
+ private windowMain: WindowMain,
+ private logService: LogService,
+ private messagingService: MessagingService,
+ private platform: NodeJS.Platform,
+ private biometricStateService: BiometricStateService,
+ ) {
+ super();
+ this.loadOsBiometricService(this.platform);
+ }
+
+ private loadOsBiometricService(platform: NodeJS.Platform) {
+ if (platform === "win32") {
+ // eslint-disable-next-line
+ const OsBiometricsServiceWindows = require("./os-biometrics-windows.service").default;
+ this.osBiometricsService = new OsBiometricsServiceWindows(
+ this.i18nService,
+ this.windowMain,
+ this.logService,
+ );
+ } else if (platform === "darwin") {
+ // eslint-disable-next-line
+ const OsBiometricsServiceMac = require("./os-biometrics-mac.service").default;
+ this.osBiometricsService = new OsBiometricsServiceMac(this.i18nService);
+ } else if (platform === "linux") {
+ // eslint-disable-next-line
+ const OsBiometricsServiceLinux = require("./os-biometrics-linux.service").default;
+ this.osBiometricsService = new OsBiometricsServiceLinux(this.i18nService, this.windowMain);
+ } else {
+ throw new Error("Unsupported platform");
+ }
+ }
+
+ /**
+ * Get the status of biometrics for the platform. Biometrics status for the platform can be one of:
+ * - Available: Biometrics are available and can be used (On windows hello, (touch id (for now)) and polkit, this MAY fall back to password)
+ * - HardwareUnavailable: Biometrics are not available on the platform
+ * - ManualSetupNeeded: In order to use biometrics, the user must perform manual steps (linux only)
+ * - AutoSetupNeeded: In order to use biometrics, the user must perform automatic steps (linux only)
+ * @returns the status of the biometrics of the platform
+ */
+ async getBiometricsStatus(): Promise {
+ if (!(await this.osBiometricsService.osSupportsBiometric())) {
+ if (await this.osBiometricsService.osBiometricsNeedsSetup()) {
+ if (await this.osBiometricsService.osBiometricsCanAutoSetup()) {
+ return BiometricsStatus.AutoSetupNeeded;
+ } else {
+ return BiometricsStatus.ManualSetupNeeded;
+ }
+ }
+
+ return BiometricsStatus.HardwareUnavailable;
+ }
+ return BiometricsStatus.Available;
+ }
+
+ /**
+ * Get the status of biometric unlock for a specific user. For this, biometric unlock needs to be set up for the user in the settings.
+ * Next, biometrics unlock needs to be available on the platform level. If "masterpassword reprompt" is enabled, a client key half (set on first unlock) for this user
+ * needs to be held in memory.
+ * @param userId the user to check the biometric unlock status for
+ * @returns the status of the biometric unlock for the user
+ */
+ async getBiometricsStatusForUser(userId: UserId): Promise {
+ if (!(await this.biometricStateService.getBiometricUnlockEnabled(userId))) {
+ return BiometricsStatus.NotEnabledLocally;
+ }
+
+ const platformStatus = await this.getBiometricsStatus();
+ if (!(platformStatus === BiometricsStatus.Available)) {
+ return platformStatus;
+ }
+
+ const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
+ const clientKeyHalfB64 = this.clientKeyHalves.get(userId);
+ const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
+ if (!clientKeyHalfSatisfied) {
+ return BiometricsStatus.UnlockNeeded;
+ }
+
+ return BiometricsStatus.Available;
+ }
+
+ async authenticateBiometric(): Promise {
+ return await this.osBiometricsService.authenticateBiometric();
+ }
+
+ async setupBiometrics(): Promise {
+ return await this.osBiometricsService.osBiometricsSetup();
+ }
+
+ async setClientKeyHalfForUser(userId: UserId, value: string): Promise {
+ this.clientKeyHalves.set(userId, value);
+ }
+
+ async authenticateWithBiometrics(): Promise {
+ return await this.osBiometricsService.authenticateBiometric();
+ }
+
+ async unlockWithBiometricsForUser(userId: UserId): Promise {
+ return SymmetricCryptoKey.fromString(
+ await this.osBiometricsService.getBiometricKey(
+ "Bitwarden_biometric",
+ `${userId}_user_biometric`,
+ this.clientKeyHalves.get(userId),
+ ),
+ ) as UserKey;
+ }
+
+ async setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise {
+ const service = "Bitwarden_biometric";
+ const storageKey = `${userId}_user_biometric`;
+ if (!this.clientKeyHalves.has(userId)) {
+ throw new Error("No client key half provided for user");
+ }
+
+ return await this.osBiometricsService.setBiometricKey(
+ service,
+ storageKey,
+ value,
+ this.clientKeyHalves.get(userId),
+ );
+ }
+
+ async deleteBiometricUnlockKeyForUser(userId: UserId): Promise {
+ return await this.osBiometricsService.deleteBiometricKey(
+ "Bitwarden_biometric",
+ `${userId}_user_biometric`,
+ );
+ }
+
+ /**
+ * Set whether to auto-prompt the user for biometric unlock; this can be used to prevent auto-prompting being initiated by a process reload.
+ * Reasons for enabling auto prompt include: Starting the app, un-minimizing the app, manually account switching
+ * @param value Whether to auto-prompt the user for biometric unlock
+ */
+ async setShouldAutopromptNow(value: boolean): Promise {
+ this.shouldAutoPrompt = value;
+ }
+
+ /**
+ * Get whether to auto-prompt the user for biometric unlock; If the user is auto-prompted, setShouldAutopromptNow should be immediately called with false in order to prevent another auto-prompt.
+ * @returns Whether to auto-prompt the user for biometric unlock
+ */
+ async getShouldAutopromptNow(): Promise {
+ return this.shouldAutoPrompt;
+ }
+}
diff --git a/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts
similarity index 97%
rename from apps/desktop/src/key-management/biometrics/biometric.unix.main.ts
rename to apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts
index f2bcf62e03e..791b4d6f885 100644
--- a/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts
+++ b/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts
@@ -9,7 +9,7 @@ import { biometrics, passwords } from "@bitwarden/desktop-napi";
import { WindowMain } from "../../main/window.main";
import { isFlatpak, isLinux, isSnapStore } from "../../utils";
-import { OsBiometricService } from "./desktop.biometrics.service";
+import { OsBiometricService } from "./os-biometrics.service";
const polkitPolicy = `
const policyFileName = "com.bitwarden.Bitwarden.policy";
const policyPath = "/usr/share/polkit-1/actions/";
-export default class BiometricUnixMain implements OsBiometricService {
+export default class OsBiometricsServiceLinux implements OsBiometricService {
constructor(
private i18nservice: I18nService,
private windowMain: WindowMain,
diff --git a/apps/desktop/src/key-management/biometrics/biometric.darwin.main.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-mac.service.ts
similarity index 92%
rename from apps/desktop/src/key-management/biometrics/biometric.darwin.main.ts
rename to apps/desktop/src/key-management/biometrics/os-biometrics-mac.service.ts
index 0f26cc78fbf..e361084726a 100644
--- a/apps/desktop/src/key-management/biometrics/biometric.darwin.main.ts
+++ b/apps/desktop/src/key-management/biometrics/os-biometrics-mac.service.ts
@@ -3,9 +3,9 @@ import { systemPreferences } from "electron";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { passwords } from "@bitwarden/desktop-napi";
-import { OsBiometricService } from "./desktop.biometrics.service";
+import { OsBiometricService } from "./os-biometrics.service";
-export default class BiometricDarwinMain implements OsBiometricService {
+export default class OsBiometricsServiceMac implements OsBiometricService {
constructor(private i18nservice: I18nService) {}
async osSupportsBiometric(): Promise {
diff --git a/apps/desktop/src/key-management/biometrics/biometric.windows.main.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts
similarity index 93%
rename from apps/desktop/src/key-management/biometrics/biometric.windows.main.ts
rename to apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts
index 0b0ad8c4500..9643c2b6f15 100644
--- a/apps/desktop/src/key-management/biometrics/biometric.windows.main.ts
+++ b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts
@@ -8,12 +8,12 @@ import { biometrics, passwords } from "@bitwarden/desktop-napi";
import { WindowMain } from "../../main/window.main";
-import { OsBiometricService } from "./desktop.biometrics.service";
+import { OsBiometricService } from "./os-biometrics.service";
const KEY_WITNESS_SUFFIX = "_witness";
const WITNESS_VALUE = "known key";
-export default class BiometricWindowsMain implements OsBiometricService {
+export default class OsBiometricsServiceWindows implements OsBiometricService {
// Use set helper method instead of direct access
private _iv: string | null = null;
// Use getKeyMaterial helper instead of direct access
@@ -113,13 +113,19 @@ export default class BiometricWindowsMain implements OsBiometricService {
this._iv = keyMaterial.ivB64;
}
- return {
+ const result = {
key_material: {
osKeyPartB64: this._osKeyHalf,
clientKeyPartB64: clientKeyHalfB64,
},
ivB64: this._iv,
};
+
+ // napi-rs fails to convert null values
+ if (result.key_material.clientKeyPartB64 == null) {
+ delete result.key_material.clientKeyPartB64;
+ }
+ return result;
}
// Nulls out key material in order to force a re-derive. This should only be used in getBiometricKey
@@ -211,10 +217,17 @@ export default class BiometricWindowsMain implements OsBiometricService {
clientKeyPartB64: string,
): biometrics.KeyMaterial {
const key = symmetricKey?.macKeyB64 ?? symmetricKey?.keyB64;
- return {
+
+ const result = {
osKeyPartB64: key,
clientKeyPartB64,
};
+
+ // napi-rs fails to convert null values
+ if (result.clientKeyPartB64 == null) {
+ delete result.clientKeyPartB64;
+ }
+ return result;
}
async osBiometricsNeedsSetup() {
diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics.service.ts
new file mode 100644
index 00000000000..f5132200149
--- /dev/null
+++ b/apps/desktop/src/key-management/biometrics/os-biometrics.service.ts
@@ -0,0 +1,32 @@
+export interface OsBiometricService {
+ osSupportsBiometric(): Promise;
+ /**
+ * Check whether support for biometric unlock requires setup. This can be automatic or manual.
+ *
+ * @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place)
+ */
+ osBiometricsNeedsSetup: () => Promise;
+ /**
+ * Check whether biometrics can be automatically setup, or requires user interaction.
+ *
+ * @returns true if biometrics support can be automatically setup, false if it requires user interaction.
+ */
+ osBiometricsCanAutoSetup: () => Promise;
+ /**
+ * Starts automatic biometric setup, which places the required configuration files / changes the required settings.
+ */
+ osBiometricsSetup: () => Promise;
+ authenticateBiometric(): Promise;
+ getBiometricKey(
+ service: string,
+ key: string,
+ clientKeyHalfB64: string | undefined,
+ ): Promise;
+ setBiometricKey(
+ service: string,
+ key: string,
+ value: string,
+ clientKeyHalfB64: string | undefined,
+ ): Promise;
+ deleteBiometricKey(service: string, key: string): Promise;
+}
diff --git a/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts b/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts
new file mode 100644
index 00000000000..a08e68b53f2
--- /dev/null
+++ b/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts
@@ -0,0 +1,54 @@
+import { Injectable } from "@angular/core";
+
+import { UserId } from "@bitwarden/common/types/guid";
+import { UserKey } from "@bitwarden/common/types/key";
+import { BiometricsStatus } from "@bitwarden/key-management";
+
+import { DesktopBiometricsService } from "./desktop.biometrics.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 RendererBiometricsService extends DesktopBiometricsService {
+ async authenticateWithBiometrics(): Promise {
+ return await ipc.keyManagement.biometric.authenticateWithBiometrics();
+ }
+
+ async getBiometricsStatus(): Promise {
+ return await ipc.keyManagement.biometric.getBiometricsStatus();
+ }
+
+ async unlockWithBiometricsForUser(userId: UserId): Promise {
+ return await ipc.keyManagement.biometric.unlockWithBiometricsForUser(userId);
+ }
+
+ async getBiometricsStatusForUser(id: UserId): Promise {
+ return await ipc.keyManagement.biometric.getBiometricsStatusForUser(id);
+ }
+
+ async setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise {
+ return await ipc.keyManagement.biometric.setBiometricProtectedUnlockKeyForUser(userId, value);
+ }
+
+ async deleteBiometricUnlockKeyForUser(userId: UserId): Promise {
+ return await ipc.keyManagement.biometric.deleteBiometricUnlockKeyForUser(userId);
+ }
+
+ async setupBiometrics(): Promise {
+ return await ipc.keyManagement.biometric.setupBiometrics();
+ }
+
+ async setClientKeyHalfForUser(userId: UserId, value: string): Promise {
+ return await ipc.keyManagement.biometric.setClientKeyHalf(userId, value);
+ }
+
+ async getShouldAutopromptNow(): Promise {
+ return await ipc.keyManagement.biometric.getShouldAutoprompt();
+ }
+
+ async setShouldAutopromptNow(value: boolean): Promise {
+ return await ipc.keyManagement.biometric.setShouldAutoprompt(value);
+ }
+}
diff --git a/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.spec.ts b/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.spec.ts
index 2d60cdeb663..2cc8d770f58 100644
--- a/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.spec.ts
+++ b/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.spec.ts
@@ -10,8 +10,8 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
import { DeviceType } from "@bitwarden/common/enums";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
-import { KeyService, BiometricsService } from "@bitwarden/key-management";
-import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/key-management/angular";
+import { KeyService, BiometricsService, BiometricsStatus } from "@bitwarden/key-management";
+import { UnlockOptions } from "@bitwarden/key-management/angular";
import { DesktopLockComponentService } from "./desktop-lock-component.service";
@@ -140,11 +140,7 @@ describe("DesktopLockComponentService", () => {
describe("getAvailableUnlockOptions$", () => {
interface MockInputs {
hasMasterPassword: boolean;
- osSupportsBiometric: boolean;
- biometricLockSet: boolean;
- biometricReady: boolean;
- hasBiometricEncryptedUserKeyStored: boolean;
- platformSupportsSecureStorage: boolean;
+ biometricsStatus: BiometricsStatus;
pinDecryptionAvailable: boolean;
}
@@ -153,11 +149,7 @@ describe("DesktopLockComponentService", () => {
// MP + PIN + Biometrics available
{
hasMasterPassword: true,
- osSupportsBiometric: true,
- biometricLockSet: true,
- hasBiometricEncryptedUserKeyStored: true,
- biometricReady: true,
- platformSupportsSecureStorage: true,
+ biometricsStatus: BiometricsStatus.Available,
pinDecryptionAvailable: true,
},
{
@@ -169,7 +161,7 @@ describe("DesktopLockComponentService", () => {
},
biometrics: {
enabled: true,
- disableReason: null,
+ biometricsStatus: BiometricsStatus.Available,
},
},
],
@@ -177,11 +169,7 @@ describe("DesktopLockComponentService", () => {
// PIN + Biometrics available
{
hasMasterPassword: false,
- osSupportsBiometric: true,
- biometricLockSet: true,
- hasBiometricEncryptedUserKeyStored: true,
- biometricReady: true,
- platformSupportsSecureStorage: true,
+ biometricsStatus: BiometricsStatus.Available,
pinDecryptionAvailable: true,
},
{
@@ -193,43 +181,16 @@ describe("DesktopLockComponentService", () => {
},
biometrics: {
enabled: true,
- disableReason: null,
- },
- },
- ],
- [
- // Biometrics available: user key stored with no secure storage
- {
- hasMasterPassword: false,
- osSupportsBiometric: true,
- biometricLockSet: true,
- hasBiometricEncryptedUserKeyStored: true,
- biometricReady: true,
- platformSupportsSecureStorage: false,
- pinDecryptionAvailable: false,
- },
- {
- masterPassword: {
- enabled: false,
- },
- pin: {
- enabled: false,
- },
- biometrics: {
- enabled: true,
- disableReason: null,
+ biometricsStatus: BiometricsStatus.Available,
},
},
],
[
// Biometrics available: no user key stored with no secure storage
+ // Biometric auth is available, but not unlock since there is no way to access the userkey
{
hasMasterPassword: false,
- osSupportsBiometric: true,
- biometricLockSet: true,
- hasBiometricEncryptedUserKeyStored: false,
- biometricReady: true,
- platformSupportsSecureStorage: false,
+ biometricsStatus: BiometricsStatus.NotEnabledLocally,
pinDecryptionAvailable: false,
},
{
@@ -240,8 +201,8 @@ describe("DesktopLockComponentService", () => {
enabled: false,
},
biometrics: {
- enabled: true,
- disableReason: null,
+ enabled: false,
+ biometricsStatus: BiometricsStatus.NotEnabledLocally,
},
},
],
@@ -249,11 +210,7 @@ describe("DesktopLockComponentService", () => {
// Biometrics not available: biometric not ready
{
hasMasterPassword: false,
- osSupportsBiometric: true,
- biometricLockSet: true,
- hasBiometricEncryptedUserKeyStored: true,
- biometricReady: false,
- platformSupportsSecureStorage: true,
+ biometricsStatus: BiometricsStatus.HardwareUnavailable,
pinDecryptionAvailable: false,
},
{
@@ -265,55 +222,7 @@ describe("DesktopLockComponentService", () => {
},
biometrics: {
enabled: false,
- disableReason: BiometricsDisableReason.SystemBiometricsUnavailable,
- },
- },
- ],
- [
- // Biometrics not available: biometric lock not set
- {
- hasMasterPassword: false,
- osSupportsBiometric: true,
- biometricLockSet: false,
- hasBiometricEncryptedUserKeyStored: true,
- biometricReady: true,
- platformSupportsSecureStorage: true,
- pinDecryptionAvailable: false,
- },
- {
- masterPassword: {
- enabled: false,
- },
- pin: {
- enabled: false,
- },
- biometrics: {
- enabled: false,
- disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
- },
- },
- ],
- [
- // Biometrics not available: user key not stored
- {
- hasMasterPassword: false,
- osSupportsBiometric: true,
- biometricLockSet: true,
- hasBiometricEncryptedUserKeyStored: false,
- biometricReady: true,
- platformSupportsSecureStorage: true,
- pinDecryptionAvailable: false,
- },
- {
- masterPassword: {
- enabled: false,
- },
- pin: {
- enabled: false,
- },
- biometrics: {
- enabled: false,
- disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
+ biometricsStatus: BiometricsStatus.HardwareUnavailable,
},
},
],
@@ -321,11 +230,7 @@ describe("DesktopLockComponentService", () => {
// Biometrics not available: OS doesn't support
{
hasMasterPassword: false,
- osSupportsBiometric: false,
- biometricLockSet: true,
- hasBiometricEncryptedUserKeyStored: true,
- biometricReady: true,
- platformSupportsSecureStorage: true,
+ biometricsStatus: BiometricsStatus.PlatformUnsupported,
pinDecryptionAvailable: false,
},
{
@@ -337,7 +242,7 @@ describe("DesktopLockComponentService", () => {
},
biometrics: {
enabled: false,
- disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem,
+ biometricsStatus: BiometricsStatus.PlatformUnsupported,
},
},
],
@@ -355,13 +260,8 @@ describe("DesktopLockComponentService", () => {
);
// Biometrics
- biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric);
- vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet);
- keyService.hasUserKeyStored.mockResolvedValue(mockInputs.hasBiometricEncryptedUserKeyStored);
- platformUtilsService.supportsSecureStorage.mockReturnValue(
- mockInputs.platformSupportsSecureStorage,
- );
- biometricEnabledMock.mockResolvedValue(mockInputs.biometricReady);
+ // TODO: FIXME
+ biometricsService.getBiometricsStatusForUser.mockResolvedValue(mockInputs.biometricsStatus);
// PIN
pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable);
diff --git a/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.ts b/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.ts
index 76232fd3196..1d2d68c1d97 100644
--- a/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.ts
+++ b/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.ts
@@ -5,25 +5,17 @@ import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
-import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { DeviceType } from "@bitwarden/common/enums";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
-import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { UserId } from "@bitwarden/common/types/guid";
-import { KeyService, BiometricsService } from "@bitwarden/key-management";
-import {
- BiometricsDisableReason,
- LockComponentService,
- UnlockOptions,
-} from "@bitwarden/key-management/angular";
+import { BiometricsService, BiometricsStatus } from "@bitwarden/key-management";
+import { LockComponentService, UnlockOptions } from "@bitwarden/key-management/angular";
export class DesktopLockComponentService implements LockComponentService {
private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction);
private readonly platformUtilsService = inject(PlatformUtilsService);
private readonly biometricsService = inject(BiometricsService);
private readonly pinService = inject(PinServiceAbstraction);
- private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
- private readonly keyService = inject(KeyService);
constructor() {}
@@ -52,77 +44,29 @@ export class DesktopLockComponentService implements LockComponentService {
}
}
- private async isBiometricLockSet(userId: UserId): Promise {
- const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId);
- const hasBiometricEncryptedUserKeyStored = await this.keyService.hasUserKeyStored(
- KeySuffixOptions.Biometric,
- userId,
- );
- const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage();
-
- return (
- biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage)
- );
- }
-
- private async isBiometricsSupportedAndReady(
- userId: UserId,
- ): Promise<{ supportsBiometric: boolean; biometricReady: boolean }> {
- const supportsBiometric = await this.biometricsService.supportsBiometric();
- const biometricReady = await ipc.keyManagement.biometric.enabled(userId);
- return { supportsBiometric, biometricReady };
- }
-
getAvailableUnlockOptions$(userId: UserId): Observable {
return combineLatest([
// Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to
- defer(() => this.isBiometricsSupportedAndReady(userId)),
- defer(() => this.isBiometricLockSet(userId)),
+ defer(() => this.biometricsService.getBiometricsStatusForUser(userId)),
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
defer(() => this.pinService.isPinDecryptionAvailable(userId)),
]).pipe(
- map(
- ([biometricsData, isBiometricsLockSet, userDecryptionOptions, pinDecryptionAvailable]) => {
- const disableReason = this.getBiometricsDisabledReason(
- biometricsData.supportsBiometric,
- isBiometricsLockSet,
- biometricsData.biometricReady,
- );
+ map(([biometricsStatus, userDecryptionOptions, pinDecryptionAvailable]) => {
+ const unlockOpts: UnlockOptions = {
+ masterPassword: {
+ enabled: userDecryptionOptions.hasMasterPassword,
+ },
+ pin: {
+ enabled: pinDecryptionAvailable,
+ },
+ biometrics: {
+ enabled: biometricsStatus == BiometricsStatus.Available,
+ biometricsStatus: biometricsStatus,
+ },
+ };
- const unlockOpts: UnlockOptions = {
- masterPassword: {
- enabled: userDecryptionOptions.hasMasterPassword,
- },
- pin: {
- enabled: pinDecryptionAvailable,
- },
- biometrics: {
- enabled:
- biometricsData.supportsBiometric &&
- isBiometricsLockSet &&
- biometricsData.biometricReady,
- disableReason: disableReason,
- },
- };
-
- return unlockOpts;
- },
- ),
+ return unlockOpts;
+ }),
);
}
-
- private getBiometricsDisabledReason(
- osSupportsBiometric: boolean,
- biometricLockSet: boolean,
- biometricReady: boolean,
- ): BiometricsDisableReason | null {
- if (!osSupportsBiometric) {
- return BiometricsDisableReason.NotSupportedOnOperatingSystem;
- } else if (!biometricLockSet) {
- return BiometricsDisableReason.EncryptedKeysUnavailable;
- } else if (!biometricReady) {
- return BiometricsDisableReason.SystemBiometricsUnavailable;
- }
- return null;
- }
}
diff --git a/apps/desktop/src/key-management/preload.ts b/apps/desktop/src/key-management/preload.ts
index ffb6159a46f..b73542ca725 100644
--- a/apps/desktop/src/key-management/preload.ts
+++ b/apps/desktop/src/key-management/preload.ts
@@ -1,36 +1,58 @@
import { ipcRenderer } from "electron";
-import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
+import { UserKey } from "@bitwarden/common/types/key";
+import { BiometricsStatus } from "@bitwarden/key-management";
import { BiometricMessage, BiometricAction } from "../types/biometric-message";
const biometric = {
- enabled: (userId: string): Promise =>
+ authenticateWithBiometrics: (): Promise =>
ipcRenderer.invoke("biometric", {
- action: BiometricAction.EnabledForUser,
- key: `${userId}_user_biometric`,
- keySuffix: KeySuffixOptions.Biometric,
+ action: BiometricAction.Authenticate,
+ } satisfies BiometricMessage),
+ getBiometricsStatus: (): Promise =>
+ ipcRenderer.invoke("biometric", {
+ action: BiometricAction.GetStatus,
+ } satisfies BiometricMessage),
+ unlockWithBiometricsForUser: (userId: string): Promise =>
+ ipcRenderer.invoke("biometric", {
+ action: BiometricAction.UnlockForUser,
userId: userId,
} satisfies BiometricMessage),
- osSupported: (): Promise =>
+ getBiometricsStatusForUser: (userId: string): Promise =>
ipcRenderer.invoke("biometric", {
- action: BiometricAction.OsSupported,
+ action: BiometricAction.GetStatusForUser,
+ userId: userId,
} satisfies BiometricMessage),
- biometricsNeedsSetup: (): Promise =>
+ setBiometricProtectedUnlockKeyForUser: (userId: string, value: string): Promise =>
ipcRenderer.invoke("biometric", {
- action: BiometricAction.NeedsSetup,
+ action: BiometricAction.SetKeyForUser,
+ userId: userId,
+ key: value,
} satisfies BiometricMessage),
- biometricsSetup: (): Promise =>
+ deleteBiometricUnlockKeyForUser: (userId: string): Promise =>
+ ipcRenderer.invoke("biometric", {
+ action: BiometricAction.RemoveKeyForUser,
+ userId: userId,
+ } satisfies BiometricMessage),
+ setupBiometrics: (): Promise =>
ipcRenderer.invoke("biometric", {
action: BiometricAction.Setup,
} satisfies BiometricMessage),
- biometricsCanAutoSetup: (): Promise =>
+ setClientKeyHalf: (userId: string, value: string): Promise =>
ipcRenderer.invoke("biometric", {
- action: BiometricAction.CanAutoSetup,
+ action: BiometricAction.SetClientKeyHalf,
+ userId: userId,
+ key: value,
} satisfies BiometricMessage),
- authenticate: (): Promise =>
+ getShouldAutoprompt: (): Promise =>
ipcRenderer.invoke("biometric", {
- action: BiometricAction.Authenticate,
+ action: BiometricAction.GetShouldAutoprompt,
+ } satisfies BiometricMessage),
+ setShouldAutoprompt: (should: boolean): Promise =>
+ ipcRenderer.invoke("biometric", {
+ action: BiometricAction.SetShouldAutoprompt,
+ data: should,
} satisfies BiometricMessage),
};
diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json
index 323d0cd3f7b..9ab15230604 100644
--- a/apps/desktop/src/locales/en/messages.json
+++ b/apps/desktop/src/locales/en/messages.json
@@ -3362,6 +3362,30 @@
"ssoError": {
"message": "No free ports could be found for the sso login."
},
+ "biometricsStatusHelptextUnlockNeeded": {
+ "message": "Biometric unlock is unavailable because PIN or password unlock is required first."
+ },
+ "biometricsStatusHelptextHardwareUnavailable": {
+ "message": "Biometric unlock is currently unavailable."
+ },
+ "biometricsStatusHelptextAutoSetupNeeded": {
+ "message": "Biometric unlock is unavailable due to misconfigured system files."
+ },
+ "biometricsStatusHelptextManualSetupNeeded": {
+ "message": "Biometric unlock is unavailable due to misconfigured system files."
+ },
+ "biometricsStatusHelptextNotEnabledLocally": {
+ "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.",
+ "placeholders": {
+ "email": {
+ "content": "$1",
+ "example": "mail@example.com"
+ }
+ }
+ },
+ "biometricsStatusHelptextUnavailableReasonUnknown": {
+ "message": "Biometric unlock is currently unavailable for an unknown reason."
+ },
"authorize": {
"message": "Authorize"
},
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
index a4842249c93..3232eef2b9b 100644
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -28,8 +28,9 @@ import { DefaultBiometricStateService } from "@bitwarden/key-management";
/* eslint-enable import/no-restricted-paths */
import { DesktopAutofillSettingsService } from "./autofill/services/desktop-autofill-settings.service";
-import { BiometricsRendererIPCListener } from "./key-management/biometrics/biometric.renderer-ipc.listener";
-import { BiometricsService, DesktopBiometricsService } from "./key-management/biometrics/index";
+import { DesktopBiometricsService } from "./key-management/biometrics/desktop.biometrics.service";
+import { MainBiometricsIPCListener } from "./key-management/biometrics/main-biometrics-ipc.listener";
+import { MainBiometricsService } from "./key-management/biometrics/main-biometrics.service";
import { MenuMain } from "./main/menu/menu.main";
import { MessagingMain } from "./main/messaging.main";
import { NativeMessagingMain } from "./main/native-messaging.main";
@@ -61,7 +62,7 @@ export class Main {
messagingService: MessageSender;
environmentService: DefaultEnvironmentService;
desktopCredentialStorageListener: DesktopCredentialStorageListener;
- biometricsRendererIPCListener: BiometricsRendererIPCListener;
+ mainBiometricsIpcListener: MainBiometricsIPCListener;
desktopSettingsService: DesktopSettingsService;
mainCryptoFunctionService: MainCryptoFunctionService;
migrationRunner: MigrationRunner;
@@ -177,6 +178,15 @@ export class Main {
this.desktopSettingsService = new DesktopSettingsService(stateProvider);
const biometricStateService = new DefaultBiometricStateService(stateProvider);
+ this.biometricsService = new MainBiometricsService(
+ this.i18nService,
+ this.windowMain,
+ this.logService,
+ this.messagingService,
+ process.platform,
+ biometricStateService,
+ );
+
this.windowMain = new WindowMain(
biometricStateService,
this.logService,
@@ -187,7 +197,6 @@ export class Main {
);
this.messagingMain = new MessagingMain(this, this.desktopSettingsService);
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain);
- this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService);
const messageSubject = new Subject>>();
this.messagingService = MessageSender.combine(
@@ -218,22 +227,19 @@ export class Main {
this.versionMain,
);
- this.biometricsService = new BiometricsService(
- this.i18nService,
+ this.trayMain = new TrayMain(
this.windowMain,
- this.logService,
- this.messagingService,
- process.platform,
+ this.i18nService,
+ this.desktopSettingsService,
biometricStateService,
+ this.biometricsService,
);
this.desktopCredentialStorageListener = new DesktopCredentialStorageListener(
"Bitwarden",
- this.biometricsService,
this.logService,
);
- this.biometricsRendererIPCListener = new BiometricsRendererIPCListener(
- "Bitwarden",
+ this.mainBiometricsIpcListener = new MainBiometricsIPCListener(
this.biometricsService,
this.logService,
);
@@ -267,7 +273,7 @@ export class Main {
bootstrap() {
this.desktopCredentialStorageListener.init();
- this.biometricsRendererIPCListener.init();
+ this.mainBiometricsIpcListener.init();
// Run migrations first, then other things
this.migrationRunner.run().then(
async () => {
diff --git a/apps/desktop/src/main/tray.main.ts b/apps/desktop/src/main/tray.main.ts
index 52a8615a1da..9fa7fe6143f 100644
--- a/apps/desktop/src/main/tray.main.ts
+++ b/apps/desktop/src/main/tray.main.ts
@@ -6,6 +6,7 @@ import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray
import { firstValueFrom } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { BiometricStateService, BiometricsService } from "@bitwarden/key-management";
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
@@ -23,6 +24,8 @@ export class TrayMain {
private windowMain: WindowMain,
private i18nService: I18nService,
private desktopSettingsService: DesktopSettingsService,
+ private biometricsStateService: BiometricStateService,
+ private biometricService: BiometricsService,
) {
if (process.platform === "win32") {
this.icon = path.join(__dirname, "/images/icon.ico");
@@ -72,6 +75,10 @@ export class TrayMain {
}
});
+ win.on("restore", async () => {
+ await this.biometricService.setShouldAutopromptNow(true);
+ });
+
win.on("close", async (e: Event) => {
if (await firstValueFrom(this.desktopSettingsService.closeToTray$)) {
if (!this.windowMain.isQuitting) {
diff --git a/apps/desktop/src/models/native-messaging/legacy-message.ts b/apps/desktop/src/models/native-messaging/legacy-message.ts
index a2bcf2aa7e5..99047cdcd34 100644
--- a/apps/desktop/src/models/native-messaging/legacy-message.ts
+++ b/apps/desktop/src/models/native-messaging/legacy-message.ts
@@ -1,5 +1,6 @@
export type LegacyMessage = {
command: string;
+ messageId: number;
userId?: string;
timestamp?: number;
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 294f9a3cbe9..ca4d9a2d3ca 100644
--- a/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts
+++ b/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts
@@ -2,18 +2,12 @@
// @ts-strict-ignore
import { ipcMain } from "electron";
-import { BiometricKey } from "@bitwarden/common/auth/types/biometric-key";
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { passwords } from "@bitwarden/desktop-napi";
-import { DesktopBiometricsService } from "../../key-management/biometrics/index";
-
-const AuthRequiredSuffix = "_biometric";
-
export class DesktopCredentialStorageListener {
constructor(
private serviceName: string,
- private biometricService: DesktopBiometricsService,
private logService: ConsoleLogService,
) {}
@@ -54,13 +48,7 @@ export class DesktopCredentialStorageListener {
// Gracefully handle old keytar values, and if detected updated the entry to the proper format
private async getPassword(serviceName: string, key: string, keySuffix: string) {
- let val: string;
- // todo: remove this when biometrics has been migrated to desktop_native
- if (keySuffix === AuthRequiredSuffix) {
- val = (await this.biometricService.getBiometricKey(serviceName, key)) ?? null;
- } else {
- val = await passwords.getPassword(serviceName, key);
- }
+ const val = await passwords.getPassword(serviceName, key);
try {
JSON.parse(val);
@@ -72,25 +60,10 @@ export class DesktopCredentialStorageListener {
}
private async setPassword(serviceName: string, key: string, value: string, keySuffix: string) {
- if (keySuffix === AuthRequiredSuffix) {
- const valueObj = JSON.parse(value) as BiometricKey;
- await this.biometricService.setEncryptionKeyHalf({
- service: serviceName,
- key,
- value: valueObj?.clientEncKeyHalf,
- });
- // Value is usually a JSON string, but we need to pass the key half as well, so we re-stringify key here.
- await this.biometricService.setBiometricKey(serviceName, key, JSON.stringify(valueObj?.key));
- } else {
- await passwords.setPassword(serviceName, key, value);
- }
+ await passwords.setPassword(serviceName, key, value);
}
private async deletePassword(serviceName: string, key: string, keySuffix: string) {
- if (keySuffix === AuthRequiredSuffix) {
- await this.biometricService.deleteBiometricKey(serviceName, key);
- } else {
- await passwords.deletePassword(serviceName, key);
- }
+ await passwords.deletePassword(serviceName, key);
}
}
diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts
index 0b61d894776..9c1986fb61d 100644
--- a/apps/desktop/src/platform/preload.ts
+++ b/apps/desktop/src/platform/preload.ts
@@ -87,6 +87,7 @@ const nativeMessaging = {
},
sendMessage: (message: {
appId: string;
+ messageId?: number;
command?: string;
sharedSecret?: string;
message?: EncString;
diff --git a/apps/desktop/src/platform/services/electron-key.service.spec.ts b/apps/desktop/src/platform/services/electron-key.service.spec.ts
deleted file mode 100644
index fc87ae4ceaf..00000000000
--- a/apps/desktop/src/platform/services/electron-key.service.spec.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
-import { mock } from "jest-mock-extended";
-
-import { PinServiceAbstraction } from "@bitwarden/auth/common";
-import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
-import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
-import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
-import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
-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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
-import { makeEncString } from "@bitwarden/common/spec";
-import { CsprngArray } from "@bitwarden/common/types/csprng";
-import { UserId } from "@bitwarden/common/types/guid";
-import { UserKey } from "@bitwarden/common/types/key";
-import { KdfConfigService, BiometricStateService } from "@bitwarden/key-management";
-
-import {
- FakeAccountService,
- mockAccountServiceWith,
-} from "../../../../../libs/common/spec/fake-account-service";
-
-import { ElectronKeyService } from "./electron-key.service";
-
-describe("electronKeyService", () => {
- let sut: ElectronKeyService;
-
- const pinService = mock();
- const keyGenerationService = mock();
- const cryptoFunctionService = mock();
- const encryptService = mock();
- const platformUtilService = mock();
- const logService = mock();
- const stateService = mock();
- let masterPasswordService: FakeMasterPasswordService;
- let accountService: FakeAccountService;
- let stateProvider: FakeStateProvider;
- const biometricStateService = mock();
- const kdfConfigService = mock();
-
- const mockUserId = "mock user id" as UserId;
-
- beforeEach(() => {
- accountService = mockAccountServiceWith("userId" as UserId);
- masterPasswordService = new FakeMasterPasswordService();
- stateProvider = new FakeStateProvider(accountService);
-
- sut = new ElectronKeyService(
- pinService,
- masterPasswordService,
- keyGenerationService,
- cryptoFunctionService,
- encryptService,
- platformUtilService,
- logService,
- stateService,
- accountService,
- stateProvider,
- biometricStateService,
- kdfConfigService,
- );
- });
-
- afterEach(() => {
- jest.resetAllMocks();
- });
-
- describe("setUserKey", () => {
- let mockUserKey: UserKey;
-
- beforeEach(() => {
- const mockRandomBytes = new Uint8Array(64) as CsprngArray;
- mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
- });
-
- describe("Biometric Key refresh", () => {
- const encClientKeyHalf = makeEncString();
- const decClientKeyHalf = "decrypted client key half";
-
- beforeEach(() => {
- encClientKeyHalf.decrypt = jest.fn().mockResolvedValue(decClientKeyHalf);
- });
-
- it("sets a Biometric key if getBiometricUnlock is true and the platform supports secure storage", async () => {
- biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(true);
- platformUtilService.supportsSecureStorage.mockReturnValue(true);
- biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true);
- biometricStateService.getEncryptedClientKeyHalf.mockResolvedValue(encClientKeyHalf);
-
- await sut.setUserKey(mockUserKey, mockUserId);
-
- expect(stateService.setUserKeyBiometric).toHaveBeenCalledWith(
- expect.objectContaining({ key: expect.any(String), clientEncKeyHalf: decClientKeyHalf }),
- {
- userId: mockUserId,
- },
- );
- });
-
- it("clears the Biometric key if getBiometricUnlock is false or the platform does not support secure storage", async () => {
- biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(true);
- platformUtilService.supportsSecureStorage.mockReturnValue(false);
-
- await sut.setUserKey(mockUserKey, mockUserId);
-
- expect(stateService.setUserKeyBiometric).toHaveBeenCalledWith(null, {
- userId: mockUserId,
- });
- });
- });
- });
-});
diff --git a/apps/desktop/src/platform/services/electron-key.service.ts b/apps/desktop/src/platform/services/electron-key.service.ts
index a4719873375..9a18753e4b5 100644
--- a/apps/desktop/src/platform/services/electron-key.service.ts
+++ b/apps/desktop/src/platform/services/electron-key.service.ts
@@ -1,7 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { firstValueFrom } from "rxjs";
-
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
@@ -13,7 +11,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
-import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { StateProvider } from "@bitwarden/common/platform/state";
import { CsprngString } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
@@ -24,6 +21,8 @@ import {
BiometricStateService,
} from "@bitwarden/key-management";
+import { DesktopBiometricsService } from "src/key-management/biometrics/desktop.biometrics.service";
+
export class ElectronKeyService extends DefaultKeyService {
constructor(
pinService: PinServiceAbstraction,
@@ -38,6 +37,7 @@ export class ElectronKeyService extends DefaultKeyService {
stateProvider: StateProvider,
private biometricStateService: BiometricStateService,
kdfConfigService: KdfConfigService,
+ private biometricService: DesktopBiometricsService,
) {
super(
pinService,
@@ -55,19 +55,10 @@ export class ElectronKeyService extends DefaultKeyService {
}
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise {
- if (keySuffix === KeySuffixOptions.Biometric) {
- return await this.stateService.hasUserKeyBiometric({ userId: userId });
- }
return super.hasUserKeyStored(keySuffix, userId);
}
override async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise {
- if (keySuffix === KeySuffixOptions.Biometric) {
- await this.stateService.setUserKeyBiometric(null, { userId: userId });
- await this.biometricStateService.removeEncryptedClientKeyHalf(userId);
- await this.clearDeprecatedKeys(KeySuffixOptions.Biometric, userId);
- return;
- }
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
await super.clearStoredUserKey(keySuffix, userId);
@@ -76,52 +67,35 @@ export class ElectronKeyService extends DefaultKeyService {
protected override async storeAdditionalKeys(key: UserKey, userId: UserId) {
await super.storeAdditionalKeys(key, userId);
- const storeBiometricKey = await this.shouldStoreKey(KeySuffixOptions.Biometric, userId);
-
- if (storeBiometricKey) {
- await this.storeBiometricKey(key, userId);
- } else {
- await this.stateService.setUserKeyBiometric(null, { userId: userId });
+ if (await this.biometricStateService.getBiometricUnlockEnabled(userId)) {
+ await this.storeBiometricsProtectedUserKey(key, userId);
}
- await this.clearDeprecatedKeys(KeySuffixOptions.Biometric, userId);
}
protected override async getKeyFromStorage(
keySuffix: KeySuffixOptions,
userId?: UserId,
): Promise {
- if (keySuffix === KeySuffixOptions.Biometric) {
- const userKey = await this.stateService.getUserKeyBiometric({ userId: userId });
- return userKey == null
- ? null
- : (new SymmetricCryptoKey(Utils.fromB64ToArray(userKey)) as UserKey);
- }
return await super.getKeyFromStorage(keySuffix, userId);
}
- protected async storeBiometricKey(key: UserKey, userId?: UserId): Promise {
+ protected async storeBiometricsProtectedUserKey(
+ userKey: UserKey,
+ userId?: UserId,
+ ): Promise {
// May resolve to null, in which case no client key have is required
- const clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(key, userId);
- await this.stateService.setUserKeyBiometric(
- { key: key.keyB64, clientEncKeyHalf },
- { userId: userId },
- );
+ // TODO: Move to windows implementation
+ const clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userKey, userId);
+ await this.biometricService.setClientKeyHalfForUser(userId, clientEncKeyHalf);
+ await this.biometricService.setBiometricProtectedUnlockKeyForUser(userId, userKey.keyB64);
}
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise {
- if (keySuffix === KeySuffixOptions.Biometric) {
- const biometricUnlockPromise =
- userId == null
- ? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
- : this.biometricStateService.getBiometricUnlockEnabled(userId);
- const biometricUnlock = await biometricUnlockPromise;
- return biometricUnlock && this.platformUtilService.supportsSecureStorage();
- }
return await super.shouldStoreKey(keySuffix, userId);
}
protected override async clearAllStoredUserKeys(userId?: UserId): Promise {
- await this.clearStoredUserKey(KeySuffixOptions.Biometric, userId);
+ await this.biometricService.deleteBiometricUnlockKeyForUser(userId);
await super.clearAllStoredUserKeys(userId);
}
@@ -135,18 +109,18 @@ export class ElectronKeyService extends DefaultKeyService {
}
// Retrieve existing key half if it exists
- let biometricKey = await this.biometricStateService
+ let clientKeyHalf = await this.biometricStateService
.getEncryptedClientKeyHalf(userId)
.then((result) => result?.decrypt(null /* user encrypted */, userKey))
.then((result) => result as CsprngString);
- if (biometricKey == null && userKey != null) {
+ if (clientKeyHalf == null && userKey != null) {
// Set a key half if it doesn't exist
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
- biometricKey = Utils.fromBufferToUtf8(keyBytes) as CsprngString;
- const encKey = await this.encryptService.encrypt(biometricKey, userKey);
+ clientKeyHalf = Utils.fromBufferToUtf8(keyBytes) as CsprngString;
+ const encKey = await this.encryptService.encrypt(clientKeyHalf, userKey);
await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
}
- return biometricKey;
+ return clientKeyHalf;
}
}
diff --git a/apps/desktop/src/services/biometric-message-handler.service.spec.ts b/apps/desktop/src/services/biometric-message-handler.service.spec.ts
new file mode 100644
index 00000000000..13b668f6b83
--- /dev/null
+++ b/apps/desktop/src/services/biometric-message-handler.service.spec.ts
@@ -0,0 +1,123 @@
+import { NgZone } from "@angular/core";
+import { mock, MockProxy } from "jest-mock-extended";
+import { of } from "rxjs";
+
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
+import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
+import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
+import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
+import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
+import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
+import { FakeAccountService } from "@bitwarden/common/spec";
+import { UserId } from "@bitwarden/common/types/guid";
+import { DialogService } from "@bitwarden/components";
+import { KeyService, BiometricsService, BiometricStateService } from "@bitwarden/key-management";
+
+import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
+
+import { BiometricMessageHandlerService } from "./biometric-message-handler.service";
+
+(global as any).ipc = {
+ platform: {
+ reloadProcess: jest.fn(),
+ },
+};
+
+const SomeUser = "SomeUser" as UserId;
+const AnotherUser = "SomeOtherUser" as UserId;
+const accounts = {
+ [SomeUser]: {
+ name: "some user",
+ email: "some.user@example.com",
+ emailVerified: true,
+ },
+ [AnotherUser]: {
+ name: "some other user",
+ email: "some.other.user@example.com",
+ emailVerified: true,
+ },
+};
+
+describe("BiometricMessageHandlerService", () => {
+ let service: BiometricMessageHandlerService;
+
+ let cryptoFunctionService: MockProxy;
+ let keyService: MockProxy;
+ let encryptService: MockProxy;
+ let logService: MockProxy;
+ let messagingService: MockProxy;
+ let desktopSettingsService: DesktopSettingsService;
+ let biometricStateService: BiometricStateService;
+ let biometricsService: MockProxy;
+ let dialogService: MockProxy;
+ let accountService: AccountService;
+ let authService: MockProxy;
+ let ngZone: MockProxy;
+
+ beforeEach(() => {
+ cryptoFunctionService = mock();
+ keyService = mock();
+ encryptService = mock();
+ logService = mock();
+ messagingService = mock();
+ desktopSettingsService = mock();
+ biometricStateService = mock();
+ biometricsService = mock();
+ dialogService = mock();
+
+ accountService = new FakeAccountService(accounts);
+ authService = mock();
+ ngZone = mock();
+
+ service = new BiometricMessageHandlerService(
+ cryptoFunctionService,
+ keyService,
+ encryptService,
+ logService,
+ messagingService,
+ desktopSettingsService,
+ biometricStateService,
+ biometricsService,
+ dialogService,
+ accountService,
+ authService,
+ ngZone,
+ );
+ });
+
+ describe("process reload", () => {
+ const testCases = [
+ // don't reload when the active user is the requested one and unlocked
+ [SomeUser, AuthenticationStatus.Unlocked, SomeUser, false, false],
+ // do reload when the active user is the requested one but locked
+ [SomeUser, AuthenticationStatus.Locked, SomeUser, false, true],
+ // always reload when another user is active than the requested one
+ [SomeUser, AuthenticationStatus.Unlocked, AnotherUser, false, true],
+ [SomeUser, AuthenticationStatus.Locked, AnotherUser, false, true],
+
+ // don't reload in dev mode
+ [SomeUser, AuthenticationStatus.Unlocked, SomeUser, true, false],
+ [SomeUser, AuthenticationStatus.Locked, SomeUser, true, false],
+ [SomeUser, AuthenticationStatus.Unlocked, AnotherUser, true, false],
+ [SomeUser, AuthenticationStatus.Locked, AnotherUser, true, false],
+ ];
+
+ it.each(testCases)(
+ "process reload for active user %s with auth status %s and other user %s and isdev: %s should process reload: %s",
+ async (activeUser, authStatus, messageUser, isDev, shouldReload) => {
+ await accountService.switchAccount(activeUser as UserId);
+ authService.authStatusFor$.mockReturnValue(of(authStatus as AuthenticationStatus));
+ (global as any).ipc.platform.isDev = isDev;
+ (global as any).ipc.platform.reloadProcess.mockClear();
+ await service.processReloadWhenRequired(messageUser as UserId);
+
+ if (shouldReload) {
+ expect((global as any).ipc.platform.reloadProcess).toHaveBeenCalled();
+ } else {
+ expect((global as any).ipc.platform.reloadProcess).not.toHaveBeenCalled();
+ }
+ },
+ );
+ });
+});
diff --git a/apps/desktop/src/services/biometric-message-handler.service.ts b/apps/desktop/src/services/biometric-message-handler.service.ts
index 68b2e8f505c..ea1e7e76c56 100644
--- a/apps/desktop/src/services/biometric-message-handler.service.ts
+++ b/apps/desktop/src/services/biometric-message-handler.service.ts
@@ -10,13 +10,18 @@ import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/c
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.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";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
-import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management";
+import {
+ BiometricStateService,
+ BiometricsCommands,
+ BiometricsService,
+ BiometricsStatus,
+ KeyService,
+} from "@bitwarden/key-management";
import { BrowserSyncVerificationDialogComponent } from "../app/components/browser-sync-verification-dialog.component";
import { LegacyMessage } from "../models/native-messaging/legacy-message";
@@ -54,6 +59,9 @@ export class BiometricMessageHandlerService {
const accounts = await firstValueFrom(this.accountService.accounts$);
const userIds = Object.keys(accounts);
if (!userIds.includes(rawMessage.userId)) {
+ this.logService.info(
+ "[Native Messaging IPC] Received message for user that is not logged into the desktop app.",
+ );
ipc.platform.nativeMessaging.sendMessage({
command: "wrongUserId",
appId: appId,
@@ -62,6 +70,7 @@ export class BiometricMessageHandlerService {
}
if (await firstValueFrom(this.desktopSettingService.browserIntegrationFingerprintEnabled$)) {
+ this.logService.info("[Native Messaging IPC] Requesting fingerprint verification.");
ipc.platform.nativeMessaging.sendMessage({
command: "verifyFingerprint",
appId: appId,
@@ -81,6 +90,7 @@ export class BiometricMessageHandlerService {
const browserSyncVerified = await firstValueFrom(dialogRef.closed);
if (browserSyncVerified !== true) {
+ this.logService.info("[Native Messaging IPC] Fingerprint verification failed.");
return;
}
}
@@ -90,6 +100,9 @@ export class BiometricMessageHandlerService {
}
if ((await ipc.platform.ephemeralStore.getEphemeralValue(appId)) == null) {
+ this.logService.info(
+ "[Native Messaging IPC] Epheremal secret for secure channel is missing. Invalidating encryption...",
+ );
ipc.platform.nativeMessaging.sendMessage({
command: "invalidateEncryption",
appId: appId,
@@ -106,6 +119,9 @@ export class BiometricMessageHandlerService {
// Shared secret is invalidated, force re-authentication
if (message == null) {
+ this.logService.info(
+ "[Native Messaging IPC] Secure channel failed to decrypt message. Invalidating encryption...",
+ );
ipc.platform.nativeMessaging.sendMessage({
command: "invalidateEncryption",
appId: appId,
@@ -114,20 +130,86 @@ export class BiometricMessageHandlerService {
}
if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) {
- this.logService.error("NativeMessage is to old, ignoring.");
+ this.logService.info("[Native Messaging IPC] Received a too old message. Ignoring.");
return;
}
+ const messageId = message.messageId;
+
switch (message.command) {
- case "biometricUnlock": {
+ case BiometricsCommands.UnlockWithBiometricsForUser: {
+ await this.handleUnlockWithBiometricsForUser(message, messageId, appId);
+ break;
+ }
+ case BiometricsCommands.AuthenticateWithBiometrics: {
+ try {
+ const unlocked = await this.biometricsService.authenticateWithBiometrics();
+ await this.send(
+ {
+ command: BiometricsCommands.AuthenticateWithBiometrics,
+ messageId,
+ response: unlocked,
+ },
+ appId,
+ );
+ } catch (e) {
+ this.logService.error("[Native Messaging IPC] Biometric authentication failed", e);
+ await this.send(
+ { command: BiometricsCommands.AuthenticateWithBiometrics, messageId, response: false },
+ appId,
+ );
+ }
+ break;
+ }
+ case BiometricsCommands.GetBiometricsStatus: {
+ const status = await this.biometricsService.getBiometricsStatus();
+ return this.send(
+ {
+ command: BiometricsCommands.GetBiometricsStatus,
+ messageId,
+ response: status,
+ },
+ appId,
+ );
+ }
+ case BiometricsCommands.GetBiometricsStatusForUser: {
+ let status = await this.biometricsService.getBiometricsStatusForUser(
+ message.userId as UserId,
+ );
+ if (status == BiometricsStatus.NotEnabledLocally) {
+ status = BiometricsStatus.NotEnabledInConnectedDesktopApp;
+ }
+ return this.send(
+ {
+ command: BiometricsCommands.GetBiometricsStatusForUser,
+ messageId,
+ response: status,
+ },
+ appId,
+ );
+ }
+ // TODO: legacy, remove after 2025.01
+ case BiometricsCommands.IsAvailable: {
+ const available =
+ (await this.biometricsService.getBiometricsStatus()) == BiometricsStatus.Available;
+ return this.send(
+ {
+ command: BiometricsCommands.IsAvailable,
+ response: available ? "available" : "not available",
+ },
+ appId,
+ );
+ }
+ // TODO: legacy, remove after 2025.01
+ case BiometricsCommands.Unlock: {
const isTemporarilyDisabled =
(await this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId)) &&
- !(await this.biometricsService.supportsBiometric());
+ !((await this.biometricsService.getBiometricsStatus()) == BiometricsStatus.Available);
if (isTemporarilyDisabled) {
return this.send({ command: "biometricUnlock", response: "not available" }, appId);
}
- if (!(await this.biometricsService.supportsBiometric())) {
+ if (!((await this.biometricsService.getBiometricsStatus()) == BiometricsStatus.Available)) {
return this.send({ command: "biometricUnlock", response: "not supported" }, appId);
}
@@ -158,10 +240,7 @@ export class BiometricMessageHandlerService {
}
try {
- const userKey = await this.keyService.getUserKeyFromStorage(
- KeySuffixOptions.Biometric,
- message.userId,
- );
+ const userKey = await this.biometricsService.unlockWithBiometricsForUser(userId);
if (userKey != null) {
await this.send(
@@ -189,19 +268,8 @@ export class BiometricMessageHandlerService {
} catch (e) {
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
}
-
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: " + message.command);
break;
@@ -216,7 +284,11 @@ export class BiometricMessageHandlerService {
SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)),
);
- ipc.platform.nativeMessaging.sendMessage({ appId: appId, message: encrypted });
+ ipc.platform.nativeMessaging.sendMessage({
+ appId: appId,
+ messageId: message.messageId,
+ message: encrypted,
+ });
}
private async secureCommunication(remotePublicKey: Uint8Array, appId: string) {
@@ -226,6 +298,7 @@ export class BiometricMessageHandlerService {
new SymmetricCryptoKey(secret).keyB64,
);
+ this.logService.info("[Native Messaging IPC] Setting up secure channel");
const encryptedSecret = await this.cryptoFunctionService.rsaEncrypt(
secret,
remotePublicKey,
@@ -234,7 +307,62 @@ export class BiometricMessageHandlerService {
ipc.platform.nativeMessaging.sendMessage({
appId: appId,
command: "setupEncryption",
+ messageId: -1, // to indicate to the other side that this is a new desktop client. refactor later to use proper versioning
sharedSecret: Utils.fromBufferToB64(encryptedSecret),
});
}
+
+ private async handleUnlockWithBiometricsForUser(
+ message: LegacyMessage,
+ messageId: number,
+ appId: string,
+ ) {
+ const messageUserId = message.userId as UserId;
+ try {
+ const userKey = await this.biometricsService.unlockWithBiometricsForUser(messageUserId);
+ if (userKey != null) {
+ this.logService.info("[Native Messaging IPC] Biometric unlock for user: " + messageUserId);
+ await this.send(
+ {
+ command: BiometricsCommands.UnlockWithBiometricsForUser,
+ response: true,
+ messageId,
+ userKeyB64: userKey.keyB64,
+ },
+ appId,
+ );
+ await this.processReloadWhenRequired(messageUserId);
+ } else {
+ await this.send(
+ {
+ command: BiometricsCommands.UnlockWithBiometricsForUser,
+ messageId,
+ response: false,
+ },
+ appId,
+ );
+ }
+ } catch (e) {
+ await this.send(
+ { command: BiometricsCommands.UnlockWithBiometricsForUser, messageId, response: false },
+ appId,
+ );
+ }
+ }
+
+ /** A process reload after a biometric unlock should happen if the userkey that was used for biometric unlock is for a different user than the
+ * currently active account. The userkey for the active account was in memory anyways. Further, if the desktop app is locked, a reload should occur (since the userkey was not already in memory).
+ */
+ async processReloadWhenRequired(messageUserId: UserId) {
+ const currentlyActiveAccountId = (await firstValueFrom(this.accountService.activeAccount$)).id;
+ const isCurrentlyActiveAccountUnlocked =
+ (await firstValueFrom(this.authService.authStatusFor$(currentlyActiveAccountId))) ==
+ AuthenticationStatus.Unlocked;
+
+ if (currentlyActiveAccountId !== messageUserId || !isCurrentlyActiveAccountUnlocked) {
+ if (!ipc.platform.isDev) {
+ ipc.platform.reloadProcess();
+ }
+ }
+ }
}
diff --git a/apps/desktop/src/types/biometric-message.ts b/apps/desktop/src/types/biometric-message.ts
index 0db7b60a2df..7946280e9a6 100644
--- a/apps/desktop/src/types/biometric-message.ts
+++ b/apps/desktop/src/types/biometric-message.ts
@@ -1,15 +1,23 @@
export enum BiometricAction {
- EnabledForUser = "enabled",
- OsSupported = "osSupported",
Authenticate = "authenticate",
- NeedsSetup = "needsSetup",
+ GetStatus = "status",
+
+ UnlockForUser = "unlockForUser",
+ GetStatusForUser = "statusForUser",
+ SetKeyForUser = "setKeyForUser",
+ RemoveKeyForUser = "removeKeyForUser",
+
+ SetClientKeyHalf = "setClientKeyHalf",
+
Setup = "setup",
- CanAutoSetup = "canAutoSetup",
+
+ GetShouldAutoprompt = "getShouldAutoprompt",
+ SetShouldAutoprompt = "setShouldAutoprompt",
}
export type BiometricMessage = {
action: BiometricAction;
- keySuffix?: string;
key?: string;
userId?: string;
+ data?: any;
};
diff --git a/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts b/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts
index 5eb26a8c76c..3c941fe24c7 100644
--- a/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts
+++ b/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts
@@ -4,6 +4,7 @@ import { firstValueFrom, of } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { UserId } from "@bitwarden/common/types/guid";
+import { BiometricsStatus } from "@bitwarden/key-management";
import { WebLockComponentService } from "./web-lock-component.service";
@@ -86,7 +87,7 @@ describe("WebLockComponentService", () => {
},
biometrics: {
enabled: false,
- disableReason: null,
+ biometricsStatus: BiometricsStatus.PlatformUnsupported,
},
});
});
diff --git a/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts b/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts
index dc124983c9a..02910966d6e 100644
--- a/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts
+++ b/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts
@@ -6,6 +6,7 @@ import {
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { UserId } from "@bitwarden/common/types/guid";
+import { BiometricsStatus } from "@bitwarden/key-management";
import { LockComponentService, UnlockOptions } from "@bitwarden/key-management/angular";
export class WebLockComponentService implements LockComponentService {
@@ -45,7 +46,7 @@ export class WebLockComponentService implements LockComponentService {
},
biometrics: {
enabled: false,
- disableReason: null,
+ biometricsStatus: BiometricsStatus.PlatformUnsupported,
},
};
return unlockOpts;
diff --git a/apps/web/src/app/key-management/web-biometric.service.ts b/apps/web/src/app/key-management/web-biometric.service.ts
index 4681eb6fa49..0c58c0da759 100644
--- a/apps/web/src/app/key-management/web-biometric.service.ts
+++ b/apps/web/src/app/key-management/web-biometric.service.ts
@@ -1,27 +1,27 @@
-import { BiometricsService } from "@bitwarden/key-management";
+import { UserId } from "@bitwarden/common/types/guid";
+import { UserKey } from "@bitwarden/common/types/key";
+import { BiometricsService, BiometricsStatus } from "@bitwarden/key-management";
export class WebBiometricsService extends BiometricsService {
- async supportsBiometric(): Promise {
+ async authenticateWithBiometrics(): Promise {
return false;
}
- async isBiometricUnlockAvailable(): Promise {
+ async getBiometricsStatus(): Promise {
+ return BiometricsStatus.PlatformUnsupported;
+ }
+
+ async unlockWithBiometricsForUser(userId: UserId): Promise {
+ return null;
+ }
+
+ async getBiometricsStatusForUser(userId: UserId): Promise {
+ return BiometricsStatus.PlatformUnsupported;
+ }
+
+ async getShouldAutopromptNow(): Promise {
return false;
}
- async authenticateBiometric(): Promise {
- throw new Error("Method not implemented.");
- }
-
- async biometricsNeedsSetup(): Promise {
- throw new Error("Method not implemented.");
- }
-
- async biometricsSupportsAutoSetup(): Promise {
- throw new Error("Method not implemented.");
- }
-
- async biometricsSetup(): Promise {
- throw new Error("Method not implemented.");
- }
+ async setShouldAutopromptNow(value: boolean): Promise {}
}
diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts
index d990a7315f2..f5940b8e144 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -279,12 +279,13 @@ import {
ImportServiceAbstraction,
} from "@bitwarden/importer/core";
import {
- KeyService as KeyServiceAbstraction,
- DefaultKeyService as KeyService,
+ KeyService,
+ DefaultKeyService,
BiometricStateService,
DefaultBiometricStateService,
- KdfConfigService,
+ BiometricsService,
DefaultKdfConfigService,
+ KdfConfigService,
UserAsymmetricKeysRegenerationService,
DefaultUserAsymmetricKeysRegenerationService,
UserAsymmetricKeysRegenerationApiService,
@@ -416,7 +417,7 @@ const safeProviders: SafeProvider[] = [
deps: [
AccountServiceAbstraction,
MessagingServiceAbstraction,
- KeyServiceAbstraction,
+ KeyService,
ApiServiceAbstraction,
StateServiceAbstraction,
TokenServiceAbstraction,
@@ -428,7 +429,7 @@ const safeProviders: SafeProvider[] = [
deps: [
AccountServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
- KeyServiceAbstraction,
+ KeyService,
ApiServiceAbstraction,
TokenServiceAbstraction,
AppIdServiceAbstraction,
@@ -471,7 +472,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: CipherServiceAbstraction,
useFactory: (
- keyService: KeyServiceAbstraction,
+ keyService: KeyService,
domainSettingsService: DomainSettingsService,
apiService: ApiServiceAbstraction,
i18nService: I18nServiceAbstraction,
@@ -501,7 +502,7 @@ const safeProviders: SafeProvider[] = [
accountService,
),
deps: [
- KeyServiceAbstraction,
+ KeyService,
DomainSettingsService,
ApiServiceAbstraction,
I18nServiceAbstraction,
@@ -520,7 +521,7 @@ const safeProviders: SafeProvider[] = [
provide: InternalFolderService,
useClass: FolderService,
deps: [
- KeyServiceAbstraction,
+ KeyService,
EncryptService,
I18nServiceAbstraction,
CipherServiceAbstraction,
@@ -565,7 +566,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: CollectionService,
useClass: DefaultCollectionService,
- deps: [KeyServiceAbstraction, EncryptService, I18nServiceAbstraction, StateProvider],
+ deps: [KeyService, EncryptService, I18nServiceAbstraction, StateProvider],
}),
safeProvider({
provide: ENV_ADDITIONAL_REGIONS,
@@ -610,8 +611,8 @@ const safeProviders: SafeProvider[] = [
deps: [CryptoFunctionServiceAbstraction],
}),
safeProvider({
- provide: KeyServiceAbstraction,
- useClass: KeyService,
+ provide: KeyService,
+ useClass: DefaultKeyService,
deps: [
PinServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
@@ -636,7 +637,7 @@ const safeProviders: SafeProvider[] = [
useFactory: legacyPasswordGenerationServiceFactory,
deps: [
EncryptService,
- KeyServiceAbstraction,
+ KeyService,
PolicyServiceAbstraction,
AccountServiceAbstraction,
StateProvider,
@@ -645,7 +646,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: GeneratorHistoryService,
useClass: LocalGeneratorHistoryService,
- deps: [EncryptService, KeyServiceAbstraction, StateProvider],
+ deps: [EncryptService, KeyService, StateProvider],
}),
safeProvider({
provide: UsernameGenerationServiceAbstraction,
@@ -653,7 +654,7 @@ const safeProviders: SafeProvider[] = [
deps: [
ApiServiceAbstraction,
I18nServiceAbstraction,
- KeyServiceAbstraction,
+ KeyService,
EncryptService,
PolicyServiceAbstraction,
AccountServiceAbstraction,
@@ -693,7 +694,7 @@ const safeProviders: SafeProvider[] = [
provide: InternalSendService,
useClass: SendService,
deps: [
- KeyServiceAbstraction,
+ KeyService,
I18nServiceAbstraction,
KeyGenerationServiceAbstraction,
SendStateProviderAbstraction,
@@ -720,7 +721,7 @@ const safeProviders: SafeProvider[] = [
DomainSettingsService,
InternalFolderService,
CipherServiceAbstraction,
- KeyServiceAbstraction,
+ KeyService,
CollectionService,
MessagingServiceAbstraction,
InternalPolicyService,
@@ -753,7 +754,7 @@ const safeProviders: SafeProvider[] = [
AccountServiceAbstraction,
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
- KeyServiceAbstraction,
+ KeyService,
TokenServiceAbstraction,
PolicyServiceAbstraction,
BiometricStateService,
@@ -780,6 +781,7 @@ const safeProviders: SafeProvider[] = [
StateEventRunnerService,
TaskSchedulerService,
LogService,
+ BiometricsService,
LOCKED_CALLBACK,
LOGOUT_CALLBACK,
],
@@ -826,7 +828,7 @@ const safeProviders: SafeProvider[] = [
ImportApiServiceAbstraction,
I18nServiceAbstraction,
CollectionService,
- KeyServiceAbstraction,
+ KeyService,
EncryptService,
PinServiceAbstraction,
AccountServiceAbstraction,
@@ -839,7 +841,7 @@ const safeProviders: SafeProvider[] = [
FolderServiceAbstraction,
CipherServiceAbstraction,
PinServiceAbstraction,
- KeyServiceAbstraction,
+ KeyService,
EncryptService,
CryptoFunctionServiceAbstraction,
KdfConfigService,
@@ -853,7 +855,7 @@ const safeProviders: SafeProvider[] = [
CipherServiceAbstraction,
ApiServiceAbstraction,
PinServiceAbstraction,
- KeyServiceAbstraction,
+ KeyService,
EncryptService,
CryptoFunctionServiceAbstraction,
CollectionService,
@@ -960,7 +962,7 @@ const safeProviders: SafeProvider[] = [
deps: [
AccountServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
- KeyServiceAbstraction,
+ KeyService,
ApiServiceAbstraction,
TokenServiceAbstraction,
LogService,
@@ -974,17 +976,15 @@ const safeProviders: SafeProvider[] = [
provide: UserVerificationServiceAbstraction,
useClass: UserVerificationService,
deps: [
- KeyServiceAbstraction,
+ KeyService,
AccountServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
I18nServiceAbstraction,
UserVerificationApiServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
PinServiceAbstraction,
- LogService,
- VaultTimeoutSettingsServiceAbstraction,
- PlatformUtilsServiceAbstraction,
KdfConfigService,
+ BiometricsService,
],
}),
safeProvider({
@@ -1007,7 +1007,7 @@ const safeProviders: SafeProvider[] = [
deps: [
OrganizationApiServiceAbstraction,
AccountServiceAbstraction,
- KeyServiceAbstraction,
+ KeyService,
EncryptService,
OrganizationUserApiService,
I18nServiceAbstraction,
@@ -1117,7 +1117,7 @@ const safeProviders: SafeProvider[] = [
deps: [
KeyGenerationServiceAbstraction,
CryptoFunctionServiceAbstraction,
- KeyServiceAbstraction,
+ KeyService,
EncryptService,
AppIdServiceAbstraction,
DevicesApiServiceAbstraction,
@@ -1137,7 +1137,7 @@ const safeProviders: SafeProvider[] = [
AppIdServiceAbstraction,
AccountServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
- KeyServiceAbstraction,
+ KeyService,
EncryptService,
ApiServiceAbstraction,
StateProvider,
@@ -1231,7 +1231,7 @@ const safeProviders: SafeProvider[] = [
ApiServiceAbstraction,
BillingApiServiceAbstraction,
ConfigService,
- KeyServiceAbstraction,
+ KeyService,
EncryptService,
I18nServiceAbstraction,
OrganizationApiServiceAbstraction,
@@ -1291,7 +1291,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: UserAutoUnlockKeyService,
useClass: UserAutoUnlockKeyService,
- deps: [KeyServiceAbstraction],
+ deps: [KeyService],
}),
safeProvider({
provide: ErrorHandler,
@@ -1335,7 +1335,7 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultSetPasswordJitService,
deps: [
ApiServiceAbstraction,
- KeyServiceAbstraction,
+ KeyService,
EncryptService,
I18nServiceAbstraction,
KdfConfigService,
@@ -1363,7 +1363,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: RegistrationFinishServiceAbstraction,
useClass: DefaultRegistrationFinishService,
- deps: [KeyServiceAbstraction, AccountApiServiceAbstraction],
+ deps: [KeyService, AccountApiServiceAbstraction],
}),
safeProvider({
provide: ViewCacheService,
@@ -1390,7 +1390,7 @@ const safeProviders: SafeProvider[] = [
PlatformUtilsServiceAbstraction,
AccountServiceAbstraction,
KdfConfigService,
- KeyServiceAbstraction,
+ KeyService,
],
}),
safeProvider({
@@ -1418,7 +1418,7 @@ const safeProviders: SafeProvider[] = [
provide: UserAsymmetricKeysRegenerationService,
useClass: DefaultUserAsymmetricKeysRegenerationService,
deps: [
- KeyServiceAbstraction,
+ KeyService,
CipherServiceAbstraction,
UserAsymmetricKeysRegenerationApiService,
LogService,
diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts
index 4aa3a632855..081dafb1706 100644
--- a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts
+++ b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts
@@ -7,14 +7,17 @@ import {
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
-import { KdfConfig, KeyService } from "@bitwarden/key-management";
+import {
+ BiometricsService,
+ BiometricsStatus,
+ KdfConfig,
+ KeyService,
+} from "@bitwarden/key-management";
import { KdfConfigService } from "../../../../../key-management/src/abstractions/kdf-config.service";
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
import { VaultTimeoutSettingsService } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
-import { LogService } from "../../../platform/abstractions/log.service";
-import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { HashPurpose } from "../../../platform/enums";
import { Utils } from "../../../platform/misc/utils";
import { UserId } from "../../../types/guid";
@@ -36,10 +39,9 @@ describe("UserVerificationService", () => {
const userVerificationApiService = mock();
const userDecryptionOptionsService = mock();
const pinService = mock();
- const logService = mock();
const vaultTimeoutSettingsService = mock();
- const platformUtilsService = mock();
const kdfConfigService = mock();
+ const biometricsService = mock();
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
@@ -56,10 +58,8 @@ describe("UserVerificationService", () => {
userVerificationApiService,
userDecryptionOptionsService,
pinService,
- logService,
- vaultTimeoutSettingsService,
- platformUtilsService,
kdfConfigService,
+ biometricsService,
);
});
@@ -113,26 +113,15 @@ describe("UserVerificationService", () => {
);
test.each([
- [true, true, true, true],
- [true, true, true, false],
- [true, true, false, false],
- [false, true, false, true],
- [false, false, false, false],
- [false, false, true, false],
- [false, false, false, true],
+ [true, BiometricsStatus.Available],
+ [false, BiometricsStatus.DesktopDisconnected],
+ [false, BiometricsStatus.HardwareUnavailable],
])(
"returns %s for biometrics availability when isBiometricLockSet is %s, hasUserKeyStored is %s, and supportsSecureStorage is %s",
- async (
- expectedReturn: boolean,
- isBiometricsLockSet: boolean,
- isBiometricsUserKeyStored: boolean,
- platformSupportSecureStorage: boolean,
- ) => {
+ async (expectedReturn: boolean, biometricsStatus: BiometricsStatus) => {
setMasterPasswordAvailability(false);
setPinAvailability("DISABLED");
- vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(isBiometricsLockSet);
- keyService.hasUserKeyStored.mockResolvedValue(isBiometricsUserKeyStored);
- platformUtilsService.supportsSecureStorage.mockReturnValue(platformSupportSecureStorage);
+ biometricsService.getBiometricsStatus.mockResolvedValue(biometricsStatus);
const result = await sut.getAvailableVerificationOptions("client");
diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts
index 822ee70ec5b..2935c1958a4 100644
--- a/libs/common/src/auth/services/user-verification/user-verification.service.ts
+++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts
@@ -3,17 +3,17 @@
import { firstValueFrom, map } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
-import { KdfConfigService, KeyService } from "@bitwarden/key-management";
+import {
+ BiometricsService,
+ BiometricsStatus,
+ KdfConfigService,
+ KeyService,
+} from "@bitwarden/key-management";
import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction";
-import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
-import { LogService } from "../../../platform/abstractions/log.service";
-import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { HashPurpose } from "../../../platform/enums";
-import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enum";
import { UserId } from "../../../types/guid";
-import { UserKey } from "../../../types/key";
import { AccountService } from "../../abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
@@ -47,10 +47,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
private userVerificationApiService: UserVerificationApiServiceAbstraction,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private pinService: PinServiceAbstraction,
- private logService: LogService,
- private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction,
- private platformUtilsService: PlatformUtilsService,
private kdfConfigService: KdfConfigService,
+ private biometricsService: BiometricsService,
) {}
async getAvailableVerificationOptions(
@@ -58,17 +56,13 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
): Promise {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (verificationType === "client") {
- const [
- userHasMasterPassword,
- isPinDecryptionAvailable,
- biometricsLockSet,
- biometricsUserKeyStored,
- ] = await Promise.all([
- this.hasMasterPasswordAndMasterKeyHash(userId),
- this.pinService.isPinDecryptionAvailable(userId),
- this.vaultTimeoutSettingsService.isBiometricLockSet(userId),
- this.keyService.hasUserKeyStored(KeySuffixOptions.Biometric, userId),
- ]);
+ const [userHasMasterPassword, isPinDecryptionAvailable, biometricsStatus] = await Promise.all(
+ [
+ this.hasMasterPasswordAndMasterKeyHash(userId),
+ this.pinService.isPinDecryptionAvailable(userId),
+ this.biometricsService.getBiometricsStatus(),
+ ],
+ );
// note: we do not need to check this.platformUtilsService.supportsBiometric() because
// we can just use the logic below which works for both desktop & the browser extension.
@@ -77,9 +71,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
client: {
masterPassword: userHasMasterPassword,
pin: isPinDecryptionAvailable,
- biometrics:
- biometricsLockSet &&
- (biometricsUserKeyStored || !this.platformUtilsService.supportsSecureStorage()),
+ biometrics: biometricsStatus === BiometricsStatus.Available,
},
server: {
masterPassword: false,
@@ -253,17 +245,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
}
private async verifyUserByBiometrics(): Promise {
- let userKey: UserKey;
- // Biometrics crashes and doesn't return a value if the user cancels the prompt
- try {
- userKey = await this.keyService.getUserKeyFromStorage(KeySuffixOptions.Biometric);
- } catch (e) {
- this.logService.error(`Biometrics User Verification failed: ${e.message}`);
- // So, any failures should be treated as a failed verification
- return false;
- }
-
- return userKey != null;
+ return this.biometricsService.authenticateWithBiometrics();
}
async requestOTP() {
diff --git a/libs/common/src/key-management/services/default-process-reload.service.ts b/libs/common/src/key-management/services/default-process-reload.service.ts
index 961d199b06e..8c1d1117c89 100644
--- a/libs/common/src/key-management/services/default-process-reload.service.ts
+++ b/libs/common/src/key-management/services/default-process-reload.service.ts
@@ -2,6 +2,7 @@
// @ts-strict-ignore
import { firstValueFrom, map, timeout } from "rxjs";
+import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { BiometricStateService } from "@bitwarden/key-management";
@@ -24,6 +25,7 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private biometricStateService: BiometricStateService,
private accountService: AccountService,
+ private logService: LogService,
) {}
async startProcessReload(authService: AuthService): Promise {
diff --git a/libs/common/src/platform/enums/key-suffix-options.enum.ts b/libs/common/src/platform/enums/key-suffix-options.enum.ts
index b268c4b777f..98fa215be6a 100644
--- a/libs/common/src/platform/enums/key-suffix-options.enum.ts
+++ b/libs/common/src/platform/enums/key-suffix-options.enum.ts
@@ -1,5 +1,4 @@
export enum KeySuffixOptions {
Auto = "auto",
- Biometric = "biometric",
Pin = "pin",
}
diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts
index 1350010f849..8a166e63a1f 100644
--- a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts
+++ b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts
@@ -5,6 +5,7 @@ import { CollectionService } from "@bitwarden/admin-console/common";
import { LogoutReason } from "@bitwarden/auth/common";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling";
+import { BiometricsService } from "@bitwarden/key-management";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { SearchService } from "../../abstractions/search.service";
@@ -41,6 +42,7 @@ describe("VaultTimeoutService", () => {
let stateEventRunnerService: MockProxy;
let taskSchedulerService: MockProxy;
let logService: MockProxy;
+ let biometricsService: MockProxy;
let lockedCallback: jest.Mock, [userId: string]>;
let loggedOutCallback: jest.Mock, [logoutReason: LogoutReason, userId?: string]>;
@@ -66,6 +68,7 @@ describe("VaultTimeoutService", () => {
stateEventRunnerService = mock();
taskSchedulerService = mock();
logService = mock();
+ biometricsService = mock();
lockedCallback = jest.fn();
loggedOutCallback = jest.fn();
@@ -93,6 +96,7 @@ describe("VaultTimeoutService", () => {
stateEventRunnerService,
taskSchedulerService,
logService,
+ biometricsService,
lockedCallback,
loggedOutCallback,
);
diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts
index 55d5bffa99a..8ab10b44b24 100644
--- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts
+++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts
@@ -6,6 +6,7 @@ import { CollectionService } from "@bitwarden/admin-console/common";
import { LogoutReason } from "@bitwarden/auth/common";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling";
+import { BiometricsService } from "@bitwarden/key-management";
import { SearchService } from "../../abstractions/search.service";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
@@ -41,6 +42,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
private stateEventRunnerService: StateEventRunnerService,
private taskSchedulerService: TaskSchedulerService,
protected logService: LogService,
+ private biometricService: BiometricsService,
private lockedCallback: (userId?: string) => Promise = null,
private loggedOutCallback: (
logoutReason: LogoutReason,
@@ -98,6 +100,8 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
}
async lock(userId?: UserId): Promise {
+ await this.biometricService.setShouldAutopromptNow(false);
+
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
if (!authed) {
return;
diff --git a/libs/key-management/src/angular/index.ts b/libs/key-management/src/angular/index.ts
index d7fadc52ce6..1eb9b88b072 100644
--- a/libs/key-management/src/angular/index.ts
+++ b/libs/key-management/src/angular/index.ts
@@ -3,8 +3,4 @@
*/
export { LockComponent } from "./lock/components/lock.component";
-export {
- LockComponentService,
- BiometricsDisableReason,
- UnlockOptions,
-} from "./lock/services/lock-component.service";
+export { LockComponentService, UnlockOptions } from "./lock/services/lock-component.service";
diff --git a/libs/key-management/src/angular/lock/components/lock.component.html b/libs/key-management/src/angular/lock/components/lock.component.html
index 5f5991c681e..7d9ed6124f6 100644
--- a/libs/key-management/src/angular/lock/components/lock.component.html
+++ b/libs/key-management/src/angular/lock/components/lock.component.html
@@ -86,12 +86,13 @@
{{ "or" | i18n }}
-
+