diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts
index 246561889ad..c785a1e7c39 100644
--- a/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts
+++ b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts
@@ -32,10 +32,6 @@ export class MainBiometricsService extends DesktopBiometricsService {
this.osBiometricsService = new OsBiometricsServiceWindows(
this.i18nService,
this.windowMain,
- this.logService,
- this.biometricStateService,
- this.encryptService,
- this.cryptoFunctionService,
);
} else if (platform === "darwin") {
// eslint-disable-next-line
@@ -43,9 +39,8 @@ export class MainBiometricsService extends DesktopBiometricsService {
this.osBiometricsService = new OsBiometricsServiceMac(this.i18nService, this.logService);
} else if (platform === "linux") {
// eslint-disable-next-line
- const OsBiometricsServiceLinux = require("./os-biometrics-linux-v2.service").default;
- this.osBiometricsService = new OsBiometricsServiceLinux(
- );
+ const OsBiometricsServiceLinux = require("./os-biometrics-linux.service").default;
+ this.osBiometricsService = new OsBiometricsServiceLinux();
} else {
throw new Error("Unsupported platform");
}
diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-linux-v2.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-linux-v2.service.ts
deleted file mode 100644
index 71bbcb3e976..00000000000
--- a/apps/desktop/src/key-management/biometrics/os-biometrics-linux-v2.service.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { spawn } from "child_process";
-
-import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
-import { UserId } from "@bitwarden/common/types/guid";
-import { biometrics, biometrics_v2, passwords } from "@bitwarden/desktop-napi";
-import { BiometricsStatus } from "@bitwarden/key-management";
-
-import { isFlatpak, isLinux, isSnapStore } from "../../utils";
-
-import { OsBiometricService } from "./os-biometrics.service";
-
-const polkitPolicy = `
-
-
-
-
- Unlock Bitwarden
- Authenticate to unlock Bitwarden
-
- no
- no
- auth_self
-
-
-`;
-const policyFileName = "com.bitwarden.Bitwarden.policy";
-const policyPath = "/usr/share/polkit-1/actions/";
-
-export default class OsBiometricsServiceLinux implements OsBiometricService {
- private biometricsSystem = biometrics_v2.initBiometricSystem();
-
- constructor() {}
-
- async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise {
- await biometrics_v2.provideKey(
- this.biometricsSystem,
- userId,
- Buffer.from(key.toEncoded().buffer),
- );
- }
-
- async deleteBiometricKey(userId: UserId): Promise {}
-
- async getBiometricKey(userId: UserId): Promise {
- const result = await biometrics_v2.unlock(this.biometricsSystem, userId, Buffer.from(""));
- return result ? new SymmetricCryptoKey(Uint8Array.from(result)) : null;
- }
-
- async authenticateBiometric(): Promise {
- return await biometrics_v2.authenticate(
- this.biometricsSystem,
- Buffer.from(""),
- "Authenticate to unlock",
- );
- }
-
- async supportsBiometrics(): Promise {
- // We assume all linux distros have some polkit implementation
- // that either has bitwarden set up or not, which is reflected in osBiomtricsNeedsSetup.
- // Snap does not have access at the moment to polkit
- // This could be dynamically detected on dbus in the future.
- // We should check if a libsecret implementation is available on the system
- // because otherwise we cannot offlod the protected userkey to secure storage.
- return await passwords.isAvailable();
- }
-
- async needsSetup(): Promise {
- if (isSnapStore()) {
- return false;
- }
-
- // check whether the polkit policy is loaded via dbus call to polkit
- return !(await biometrics.available());
- }
-
- async canAutoSetup(): Promise {
- // We cannot auto setup on snap or flatpak since the filesystem is sandboxed.
- // The user needs to manually set up the polkit policy outside of the sandbox
- // since we allow access to polkit via dbus for the sandboxed clients, the authentication works from
- // the sandbox, once the policy is set up outside of the sandbox.
- return isLinux() && !isSnapStore() && !isFlatpak();
- }
-
- async runSetup(): Promise {
- const process = spawn("pkexec", [
- "bash",
- "-c",
- `echo '${polkitPolicy}' > ${policyPath + policyFileName} && chown root:root ${policyPath + policyFileName} && chcon system_u:object_r:usr_t:s0 ${policyPath + policyFileName}`,
- ]);
-
- await new Promise((resolve, reject) => {
- process.on("close", (code) => {
- if (code !== 0) {
- reject("Failed to set up polkit policy");
- } else {
- resolve(null);
- }
- });
- });
- }
-
- async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise {
- return (await biometrics_v2.unlockAvailable(this.biometricsSystem, userId))
- ? BiometricsStatus.Available
- : BiometricsStatus.UnlockNeeded;
- }
-}
diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.spec.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.spec.ts
deleted file mode 100644
index 64af0cc625e..00000000000
--- a/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.spec.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { mock } from "jest-mock-extended";
-
-import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
-import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
-import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
-import { UserId } from "@bitwarden/common/types/guid";
-import { passwords } from "@bitwarden/desktop-napi";
-import { BiometricStateService } from "@bitwarden/key-management";
-
-import OsBiometricsServiceLinux from "./os-biometrics-linux.service";
-
-jest.mock("@bitwarden/desktop-napi", () => ({
- biometrics: {
- setBiometricSecret: jest.fn(),
- getBiometricSecret: jest.fn(),
- deleteBiometricSecret: jest.fn(),
- prompt: jest.fn(),
- available: jest.fn(),
- deriveKeyMaterial: jest.fn(),
- },
- passwords: {
- deletePassword: jest.fn(),
- getPassword: jest.fn(),
- isAvailable: jest.fn(),
- PASSWORD_NOT_FOUND: "Password not found",
- },
-}));
-
-describe("OsBiometricsServiceLinux", () => {
- let service: OsBiometricsServiceLinux;
- let logService: LogService;
-
- const mockUserId = "test-user-id" as UserId;
-
- beforeEach(() => {
- const biometricStateService = mock();
- const encryptService = mock();
- const cryptoFunctionService = mock();
- logService = mock();
- service = new OsBiometricsServiceLinux(
- biometricStateService,
- encryptService,
- cryptoFunctionService,
- logService,
- );
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- describe("deleteBiometricKey", () => {
- const serviceName = "Bitwarden_biometric";
- const keyName = "test-user-id_user_biometric";
-
- it("should delete biometric key successfully", async () => {
- await service.deleteBiometricKey(mockUserId);
-
- expect(passwords.deletePassword).toHaveBeenCalledWith(serviceName, keyName);
- });
-
- it("should not throw error if key not found", async () => {
- passwords.deletePassword = jest
- .fn()
- .mockRejectedValueOnce(new Error(passwords.PASSWORD_NOT_FOUND));
-
- await service.deleteBiometricKey(mockUserId);
-
- expect(passwords.deletePassword).toHaveBeenCalledWith(serviceName, keyName);
- expect(logService.debug).toHaveBeenCalledWith(
- "[OsBiometricService] Biometric key %s not found for service %s.",
- keyName,
- serviceName,
- );
- });
-
- it("should throw error for unexpected errors", async () => {
- const error = new Error("Unexpected error");
- passwords.deletePassword = jest.fn().mockRejectedValueOnce(error);
-
- await expect(service.deleteBiometricKey(mockUserId)).rejects.toThrow(error);
-
- expect(passwords.deletePassword).toHaveBeenCalledWith(serviceName, keyName);
- });
- });
-});
diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts
index 0ef3033b4c5..71bbcb3e976 100644
--- a/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts
+++ b/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts
@@ -1,14 +1,9 @@
import { spawn } from "child_process";
-import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
-import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
-import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
-import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
-import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid";
-import { biometrics, passwords } from "@bitwarden/desktop-napi";
-import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management";
+import { biometrics, biometrics_v2, passwords } from "@bitwarden/desktop-napi";
+import { BiometricsStatus } from "@bitwarden/key-management";
import { isFlatpak, isLinux, isSnapStore } from "../../utils";
@@ -33,89 +28,32 @@ const polkitPolicy = `
const policyFileName = "com.bitwarden.Bitwarden.policy";
const policyPath = "/usr/share/polkit-1/actions/";
-const SERVICE = "Bitwarden_biometric";
-
-function getLookupKeyForUser(userId: UserId): string {
- return `${userId}_user_biometric`;
-}
-
export default class OsBiometricsServiceLinux implements OsBiometricService {
- constructor(
- private biometricStateService: BiometricStateService,
- private encryptService: EncryptService,
- private cryptoFunctionService: CryptoFunctionService,
- private logService: LogService,
- ) {}
+ private biometricsSystem = biometrics_v2.initBiometricSystem();
- private _iv: string | null = null;
- // Use getKeyMaterial helper instead of direct access
- private _osKeyHalf: string | null = null;
- private clientKeyHalves = new Map();
+ constructor() {}
async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise {
- const clientKeyHalf = await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
-
- const storageDetails = await this.getStorageDetails({
- clientKeyHalfB64: clientKeyHalf ? Utils.fromBufferToB64(clientKeyHalf) : undefined,
- });
- await biometrics.setBiometricSecret(
- SERVICE,
- getLookupKeyForUser(userId),
- key.toBase64(),
- storageDetails.key_material,
- storageDetails.ivB64,
+ await biometrics_v2.provideKey(
+ this.biometricsSystem,
+ userId,
+ Buffer.from(key.toEncoded().buffer),
);
}
- async deleteBiometricKey(userId: UserId): Promise {
- try {
- await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId));
- } catch (e) {
- if (e instanceof Error && e.message === passwords.PASSWORD_NOT_FOUND) {
- this.logService.debug(
- "[OsBiometricService] Biometric key %s not found for service %s.",
- getLookupKeyForUser(userId),
- SERVICE,
- );
- } else {
- throw e;
- }
- }
- }
+ async deleteBiometricKey(userId: UserId): Promise {}
async getBiometricKey(userId: UserId): Promise {
- const success = await this.authenticateBiometric();
-
- if (!success) {
- throw new Error("Biometric authentication failed");
- }
-
- const value = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId));
-
- if (value == null || value == "") {
- return null;
- } else {
- let clientKeyPartB64: string | null = null;
- if (this.clientKeyHalves.has(userId)) {
- clientKeyPartB64 = Utils.fromBufferToB64(this.clientKeyHalves.get(userId)!);
- }
- const encValue = new EncString(value);
- this.setIv(encValue.iv);
- const storageDetails = await this.getStorageDetails({
- clientKeyHalfB64: clientKeyPartB64 ?? undefined,
- });
- const storedValue = await biometrics.getBiometricSecret(
- SERVICE,
- getLookupKeyForUser(userId),
- storageDetails.key_material,
- );
- return SymmetricCryptoKey.fromString(storedValue);
- }
+ const result = await biometrics_v2.unlock(this.biometricsSystem, userId, Buffer.from(""));
+ return result ? new SymmetricCryptoKey(Uint8Array.from(result)) : null;
}
async authenticateBiometric(): Promise {
- const hwnd = Buffer.from("");
- return await biometrics.prompt(hwnd, "");
+ return await biometrics_v2.authenticate(
+ this.biometricsSystem,
+ Buffer.from(""),
+ "Authenticate to unlock",
+ );
}
async supportsBiometrics(): Promise {
@@ -163,69 +101,9 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
});
}
- // Nulls out key material in order to force a re-derive. This should only be used in getBiometricKey
- // when we want to force a re-derive of the key material.
- private setIv(iv?: string) {
- this._iv = iv ?? null;
- this._osKeyHalf = null;
- }
-
- private async getStorageDetails({
- clientKeyHalfB64,
- }: {
- clientKeyHalfB64: string | undefined;
- }): Promise<{ key_material: biometrics.KeyMaterial; ivB64: string }> {
- if (this._osKeyHalf == null) {
- const keyMaterial = await biometrics.deriveKeyMaterial(this._iv);
- this._osKeyHalf = keyMaterial.keyB64;
- this._iv = keyMaterial.ivB64;
- }
-
- if (this._iv == null) {
- throw new Error("Initialization Vector is null");
- }
-
- return {
- key_material: {
- osKeyPartB64: this._osKeyHalf,
- clientKeyPartB64: clientKeyHalfB64,
- },
- ivB64: this._iv,
- };
- }
-
- private async getOrCreateBiometricEncryptionClientKeyHalf(
- userId: UserId,
- key: SymmetricCryptoKey,
- ): Promise {
- if (this.clientKeyHalves.has(userId)) {
- return this.clientKeyHalves.get(userId) || null;
- }
-
- // Retrieve existing key half if it exists
- let clientKeyHalf: Uint8Array | null = null;
- const encryptedClientKeyHalf =
- await this.biometricStateService.getEncryptedClientKeyHalf(userId);
- if (encryptedClientKeyHalf != null) {
- clientKeyHalf = await this.encryptService.decryptBytes(encryptedClientKeyHalf, key);
- }
- if (clientKeyHalf == null) {
- // Set a key half if it doesn't exist
- clientKeyHalf = await this.cryptoFunctionService.randomBytes(32);
- const encKey = await this.encryptService.encryptBytes(clientKeyHalf, key);
- await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
- }
-
- this.clientKeyHalves.set(userId, clientKeyHalf);
-
- return clientKeyHalf;
- }
-
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise {
- if (this.clientKeyHalves.has(userId)) {
- return BiometricsStatus.Available;
- } else {
- return BiometricsStatus.UnlockNeeded;
- }
+ return (await biometrics_v2.unlockAvailable(this.biometricsSystem, userId))
+ ? BiometricsStatus.Available
+ : BiometricsStatus.UnlockNeeded;
}
}
diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.spec.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.spec.ts
deleted file mode 100644
index f301efc70e7..00000000000
--- a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.spec.ts
+++ /dev/null
@@ -1,378 +0,0 @@
-import { randomBytes } from "node:crypto";
-
-import { BrowserWindow } from "electron";
-import { mock } from "jest-mock-extended";
-
-import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
-import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
-import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
-import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
-import { Utils } from "@bitwarden/common/platform/misc/utils";
-import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
-import { UserId } from "@bitwarden/common/types/guid";
-import { biometrics, passwords } from "@bitwarden/desktop-napi";
-import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management";
-
-import { WindowMain } from "../../main/window.main";
-
-import OsBiometricsServiceWindows from "./os-biometrics-windows.service";
-
-import OsDerivedKey = biometrics.OsDerivedKey;
-
-jest.mock("@bitwarden/desktop-napi", () => {
- return {
- biometrics: {
- available: jest.fn().mockResolvedValue(true),
- getBiometricSecret: jest.fn().mockResolvedValue(""),
- setBiometricSecret: jest.fn().mockResolvedValue(""),
- deleteBiometricSecret: jest.fn(),
- deriveKeyMaterial: jest.fn().mockResolvedValue({
- keyB64: "",
- ivB64: "",
- }),
- prompt: jest.fn().mockResolvedValue(true),
- },
- passwords: {
- getPassword: jest.fn().mockResolvedValue(null),
- deletePassword: jest.fn().mockImplementation(() => {}),
- isAvailable: jest.fn(),
- PASSWORD_NOT_FOUND: "Password not found",
- },
- };
-});
-
-describe("OsBiometricsServiceWindows", function () {
- const i18nService = mock();
- const windowMain = mock();
- const browserWindow = mock();
- const encryptionService: EncryptService = mock();
- const cryptoFunctionService: CryptoFunctionService = mock();
- const biometricStateService: BiometricStateService = mock();
- const logService = mock();
-
- let service: OsBiometricsServiceWindows;
-
- const key = new SymmetricCryptoKey(new Uint8Array(64));
- const userId = "test-user-id" as UserId;
- const serviceKey = "Bitwarden_biometric";
- const storageKey = `${userId}_user_biometric`;
-
- beforeEach(() => {
- windowMain.win = browserWindow;
-
- service = new OsBiometricsServiceWindows(
- i18nService,
- windowMain,
- logService,
- biometricStateService,
- encryptionService,
- cryptoFunctionService,
- );
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- describe("getBiometricsFirstUnlockStatusForUser", () => {
- const userId = "test-user-id" as UserId;
- it("should return Available when client key half is set", async () => {
- (service as any).clientKeyHalves = new Map();
- (service as any).clientKeyHalves.set(userId, new Uint8Array([1, 2, 3, 4]));
- const result = await service.getBiometricsFirstUnlockStatusForUser(userId);
- expect(result).toBe(BiometricsStatus.Available);
- });
- it("should return UnlockNeeded when client key half is not set", async () => {
- (service as any).clientKeyHalves = new Map();
- const result = await service.getBiometricsFirstUnlockStatusForUser(userId);
- expect(result).toBe(BiometricsStatus.UnlockNeeded);
- });
- });
-
- describe("getOrCreateBiometricEncryptionClientKeyHalf", () => {
- it("should return cached key half if already present", async () => {
- const cachedKeyHalf = new Uint8Array([10, 20, 30]);
- (service as any).clientKeyHalves.set(userId.toString(), cachedKeyHalf);
- const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
- expect(result).toBe(cachedKeyHalf);
- });
-
- it("should decrypt and return existing encrypted client key half", async () => {
- biometricStateService.getEncryptedClientKeyHalf = jest
- .fn()
- .mockResolvedValue(new Uint8Array([1, 2, 3]));
- const decrypted = new Uint8Array([4, 5, 6]);
- encryptionService.decryptBytes = jest.fn().mockResolvedValue(decrypted);
-
- const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
-
- expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith(userId);
- expect(encryptionService.decryptBytes).toHaveBeenCalledWith(new Uint8Array([1, 2, 3]), key);
- expect(result).toEqual(decrypted);
- expect((service as any).clientKeyHalves.get(userId.toString())).toEqual(decrypted);
- });
-
- it("should generate, encrypt, store, and cache a new key half if none exists", async () => {
- biometricStateService.getEncryptedClientKeyHalf = jest.fn().mockResolvedValue(null);
- const randomBytes = new Uint8Array([7, 8, 9]);
- cryptoFunctionService.randomBytes = jest.fn().mockResolvedValue(randomBytes);
- const encrypted = new Uint8Array([10, 11, 12]);
- encryptionService.encryptBytes = jest.fn().mockResolvedValue(encrypted);
- biometricStateService.setEncryptedClientKeyHalf = jest.fn().mockResolvedValue(undefined);
-
- const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
-
- expect(cryptoFunctionService.randomBytes).toHaveBeenCalledWith(32);
- expect(encryptionService.encryptBytes).toHaveBeenCalledWith(randomBytes, key);
- expect(biometricStateService.setEncryptedClientKeyHalf).toHaveBeenCalledWith(
- encrypted,
- userId,
- );
- expect(result).toEqual(randomBytes);
- expect((service as any).clientKeyHalves.get(userId.toString())).toEqual(randomBytes);
- });
- });
-
- describe("supportsBiometrics", () => {
- it("should return true if biometrics are available", async () => {
- biometrics.available = jest.fn().mockResolvedValue(true);
-
- const result = await service.supportsBiometrics();
-
- expect(result).toBe(true);
- });
-
- it("should return false if biometrics are not available", async () => {
- biometrics.available = jest.fn().mockResolvedValue(false);
-
- const result = await service.supportsBiometrics();
-
- expect(result).toBe(false);
- });
- });
-
- describe("getBiometricKey", () => {
- beforeEach(() => {
- biometrics.prompt = jest.fn().mockResolvedValue(true);
- });
-
- it("should return null when unsuccessfully authenticated biometrics", async () => {
- biometrics.prompt = jest.fn().mockResolvedValue(false);
-
- const result = await service.getBiometricKey(userId);
-
- expect(result).toBeNull();
- });
-
- it.each([null, undefined, ""])(
- "should throw error when no biometric key is found '%s'",
- async (password) => {
- passwords.getPassword = jest.fn().mockResolvedValue(password);
-
- await expect(service.getBiometricKey(userId)).rejects.toThrow(
- "Biometric key not found for user",
- );
-
- expect(passwords.getPassword).toHaveBeenCalledWith(serviceKey, storageKey);
- },
- );
-
- it.each([[false], [true]])(
- "should return the biometricKey and setBiometricSecret called if password is not encrypted and cached clientKeyHalves is %s",
- async (haveClientKeyHalves) => {
- const clientKeyHalveBytes = new Uint8Array([1, 2, 3]);
- if (haveClientKeyHalves) {
- service["clientKeyHalves"].set(userId, clientKeyHalveBytes);
- }
- const biometricKey = key.toBase64();
- passwords.getPassword = jest.fn().mockResolvedValue(biometricKey);
- biometrics.deriveKeyMaterial = jest.fn().mockResolvedValue({
- keyB64: "testKeyB64",
- ivB64: "testIvB64",
- } satisfies OsDerivedKey);
-
- const result = await service.getBiometricKey(userId);
-
- expect(result.toBase64()).toBe(biometricKey);
- expect(passwords.getPassword).toHaveBeenCalledWith(serviceKey, storageKey);
- expect(biometrics.setBiometricSecret).toHaveBeenCalledWith(
- serviceKey,
- storageKey,
- biometricKey,
- {
- osKeyPartB64: "testKeyB64",
- clientKeyPartB64: haveClientKeyHalves
- ? Utils.fromBufferToB64(clientKeyHalveBytes)
- : undefined,
- },
- "testIvB64",
- );
- },
- );
-
- it.each([[false], [true]])(
- "should return the biometricKey if password is encrypted and cached clientKeyHalves is %s",
- async (haveClientKeyHalves) => {
- const clientKeyHalveBytes = new Uint8Array([1, 2, 3]);
- if (haveClientKeyHalves) {
- service["clientKeyHalves"].set(userId, clientKeyHalveBytes);
- }
- const biometricKey = key.toBase64();
- const biometricKeyEncrypted = "2.testId|data|mac";
- passwords.getPassword = jest.fn().mockResolvedValue(biometricKeyEncrypted);
- biometrics.getBiometricSecret = jest.fn().mockResolvedValue(biometricKey);
- biometrics.deriveKeyMaterial = jest.fn().mockResolvedValue({
- keyB64: "testKeyB64",
- ivB64: "testIvB64",
- } satisfies OsDerivedKey);
-
- const result = await service.getBiometricKey(userId);
-
- expect(result.toBase64()).toBe(biometricKey);
- expect(passwords.getPassword).toHaveBeenCalledWith(serviceKey, storageKey);
- expect(biometrics.setBiometricSecret).not.toHaveBeenCalled();
- expect(biometrics.getBiometricSecret).toHaveBeenCalledWith(serviceKey, storageKey, {
- osKeyPartB64: "testKeyB64",
- clientKeyPartB64: haveClientKeyHalves
- ? Utils.fromBufferToB64(clientKeyHalveBytes)
- : undefined,
- });
- },
- );
- });
-
- describe("deleteBiometricKey", () => {
- const serviceName = "Bitwarden_biometric";
- const keyName = "test-user-id_user_biometric";
-
- it("should delete biometric key successfully", async () => {
- await service.deleteBiometricKey(userId);
-
- expect(passwords.deletePassword).toHaveBeenCalledWith(serviceName, keyName);
- });
-
- it.each([[false], [true]])("should not throw error if key found: %s", async (keyFound) => {
- if (!keyFound) {
- passwords.deletePassword = jest
- .fn()
- .mockRejectedValue(new Error(passwords.PASSWORD_NOT_FOUND));
- }
-
- await service.deleteBiometricKey(userId);
-
- expect(passwords.deletePassword).toHaveBeenCalledWith(serviceName, keyName);
- if (!keyFound) {
- expect(logService.debug).toHaveBeenCalledWith(
- "[OsBiometricService] Biometric key %s not found for service %s.",
- keyName,
- serviceName,
- );
- }
- });
-
- it("should throw error when deletePassword for key throws unexpected errors", async () => {
- const error = new Error("Unexpected error");
- passwords.deletePassword = jest.fn().mockRejectedValue(error);
-
- await expect(service.deleteBiometricKey(userId)).rejects.toThrow(error);
-
- expect(passwords.deletePassword).toHaveBeenCalledWith(serviceName, keyName);
- });
- });
-
- describe("authenticateBiometric", () => {
- const hwnd = randomBytes(32).buffer;
- const consentMessage = "Test Windows Hello Consent Message";
-
- beforeEach(() => {
- windowMain.win.getNativeWindowHandle = jest.fn().mockReturnValue(hwnd);
- i18nService.t.mockReturnValue(consentMessage);
- });
-
- it("should return true when biometric authentication is successful", async () => {
- const result = await service.authenticateBiometric();
-
- expect(result).toBe(true);
- expect(biometrics.prompt).toHaveBeenCalledWith(hwnd, consentMessage);
- });
-
- it("should return false when biometric authentication fails", async () => {
- biometrics.prompt = jest.fn().mockResolvedValue(false);
-
- const result = await service.authenticateBiometric();
-
- expect(result).toBe(false);
- expect(biometrics.prompt).toHaveBeenCalledWith(hwnd, consentMessage);
- });
- });
-
- describe("getStorageDetails", () => {
- it.each([
- ["testClientKeyHalfB64", "testIvB64"],
- [undefined, "testIvB64"],
- ["testClientKeyHalfB64", null],
- [undefined, null],
- ])(
- "should derive key material and ivB64 and return it when os key half not saved yet",
- async (clientKeyHalfB64, ivB64) => {
- service["setIv"](ivB64);
-
- const derivedKeyMaterial = {
- keyB64: "derivedKeyB64",
- ivB64: "derivedIvB64",
- };
- biometrics.deriveKeyMaterial = jest.fn().mockResolvedValue(derivedKeyMaterial);
-
- const result = await service["getStorageDetails"]({ clientKeyHalfB64 });
-
- expect(result).toEqual({
- key_material: {
- osKeyPartB64: derivedKeyMaterial.keyB64,
- clientKeyPartB64: clientKeyHalfB64,
- },
- ivB64: derivedKeyMaterial.ivB64,
- });
- expect(biometrics.deriveKeyMaterial).toHaveBeenCalledWith(ivB64);
- expect(service["_osKeyHalf"]).toEqual(derivedKeyMaterial.keyB64);
- expect(service["_iv"]).toEqual(derivedKeyMaterial.ivB64);
- },
- );
-
- it("should throw an error when deriving key material and returned iv is null", async () => {
- service["setIv"]("testIvB64");
-
- const derivedKeyMaterial = {
- keyB64: "derivedKeyB64",
- ivB64: null as string | undefined | null,
- };
- biometrics.deriveKeyMaterial = jest.fn().mockResolvedValue(derivedKeyMaterial);
-
- await expect(
- service["getStorageDetails"]({ clientKeyHalfB64: "testClientKeyHalfB64" }),
- ).rejects.toThrow("Initialization Vector is null");
-
- expect(biometrics.deriveKeyMaterial).toHaveBeenCalledWith("testIvB64");
- });
- });
-
- describe("setIv", () => {
- it("should set the iv and reset the osKeyHalf", () => {
- const iv = "testIv";
- service["_osKeyHalf"] = "testOsKeyHalf";
-
- service["setIv"](iv);
-
- expect(service["_iv"]).toBe(iv);
- expect(service["_osKeyHalf"]).toBeNull();
- });
-
- it("should set the iv to null when iv is undefined and reset the osKeyHalf", () => {
- service["_osKeyHalf"] = "testOsKeyHalf";
-
- service["setIv"](undefined);
-
- expect(service["_iv"]).toBeNull();
- expect(service["_osKeyHalf"]).toBeNull();
- });
- });
-});
diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts
index df4bb50e3ee..5a8390f5844 100644
--- a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts
+++ b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts
@@ -17,7 +17,7 @@ export default class OsBiometricsServiceWindows implements OsBiometricService {
) {}
async supportsBiometrics(): Promise {
- return await biometrics.available();
+ return await biometrics_v2.authenticateAvailable(this.biometricsSystem);
}
async getBiometricKey(userId: UserId): Promise {
@@ -61,6 +61,6 @@ export default class OsBiometricsServiceWindows implements OsBiometricService {
async runSetup(): Promise {}
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise {
- return BiometricsStatus.Available;
+ return (await biometrics_v2.hasPersistent(this.biometricsSystem, userId) || await biometrics_v2.unlockAvailable(this.biometricsSystem, userId)) ? BiometricsStatus.Available : BiometricsStatus.UnlockNeeded;
}
}