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; } }