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 new file mode 100644 index 00000000000..0e39b1a90e2 --- /dev/null +++ b/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.spec.ts @@ -0,0 +1,96 @@ +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 } from "@bitwarden/key-management"; + +import OsBiometricsServiceLinux from "./os-biometrics-linux.service"; + +jest.mock("@bitwarden/desktop-napi", () => ({ + biometrics: { + initBiometricSystem: jest.fn(() => "mockSystem"), + provideKey: jest.fn(), + unenroll: jest.fn(), + unlock: jest.fn(), + authenticate: jest.fn(), + authenticateAvailable: jest.fn(), + unlockAvailable: jest.fn(), + }, + passwords: { + isAvailable: jest.fn(), + }, +})); + +const mockKey = new Uint8Array(64); + +jest.mock("../../utils", () => ({ + isFlatpak: jest.fn(() => false), + isLinux: jest.fn(() => true), + isSnapStore: jest.fn(() => false), +})); + +describe("OsBiometricsServiceLinux", () => { + const userId = "user-id" as UserId; + const key = { toEncoded: () => ({ buffer: Buffer.from(mockKey) }) } as SymmetricCryptoKey; + let service: OsBiometricsServiceLinux; + + beforeEach(() => { + service = new OsBiometricsServiceLinux(); + jest.clearAllMocks(); + }); + + it("should set biometric key", async () => { + await service.setBiometricKey(userId, key); + expect(biometrics.provideKey).toHaveBeenCalled(); + }); + + it("should delete biometric key", async () => { + await service.deleteBiometricKey(userId); + expect(biometrics.unenroll).toHaveBeenCalled(); + }); + + it("should get biometric key", async () => { + (biometrics.unlock as jest.Mock).mockResolvedValue(mockKey); + const result = await service.getBiometricKey(userId); + expect(result).toBeInstanceOf(SymmetricCryptoKey); + }); + + it("should return null if no biometric key", async () => { + (biometrics.unlock as jest.Mock).mockResolvedValue(null); + const result = await service.getBiometricKey(userId); + expect(result).toBeNull(); + }); + + it("should authenticate biometric", async () => { + (biometrics.authenticate as jest.Mock).mockResolvedValue(true); + const result = await service.authenticateBiometric(); + expect(result).toBe(true); + }); + + it("should check if biometrics is supported", async () => { + (passwords.isAvailable as jest.Mock).mockResolvedValue(true); + const result = await service.supportsBiometrics(); + expect(result).toBe(true); + }); + + it("should check if setup is needed", async () => { + (biometrics.authenticateAvailable as jest.Mock).mockResolvedValue(false); + const result = await service.needsSetup(); + expect(result).toBe(true); + }); + + it("should check if can auto setup", async () => { + const result = await service.canAutoSetup(); + expect(result).toBe(true); + }); + + it("should get biometrics first unlock status for user", async () => { + (biometrics.unlockAvailable as jest.Mock).mockResolvedValue(true); + const result = await service.getBiometricsFirstUnlockStatusForUser(userId); + expect(result).toBe(BiometricsStatus.Available); + }); + + it("should return false for hasPersistentKey", async () => { + const result = await service.hasPersistentKey(userId); + expect(result).toBe(false); + }); +}); 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 new file mode 100644 index 00000000000..0caa63568d1 --- /dev/null +++ b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.spec.ts @@ -0,0 +1,114 @@ +import { mock } from "jest-mock-extended"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; +import { biometrics } from "@bitwarden/desktop-napi"; +import { BiometricsStatus } from "@bitwarden/key-management"; + + +import { WindowMain } from "../../main/window.main"; + +import OsBiometricsServiceWindows from "./os-biometrics-windows.service"; + + +jest.mock("@bitwarden/desktop-napi", () => ({ + biometrics: { + initBiometricSystem: jest.fn(() => "mockSystem"), + provideKey: jest.fn(), + unenroll: jest.fn(), + unlock: jest.fn(), + authenticate: jest.fn(), + authenticateAvailable: jest.fn(), + unlockAvailable: jest.fn(), + hasPersistent: jest.fn(), + }, + passwords: { + isAvailable: jest.fn(), + }, +})); + +const mockKey = new Uint8Array(64); + +jest.mock("../../utils", () => ({ + isFlatpak: jest.fn(() => false), + isLinux: jest.fn(() => true), + isSnapStore: jest.fn(() => false), +})); + +describe("OsBiometricsServiceWindows", () => { + const userId = "user-id" as UserId; + + let service: OsBiometricsServiceWindows; + let i18nService: I18nService; + let windowMain: WindowMain; + + beforeEach(() => { + i18nService = mock(); + windowMain = mock(); + windowMain.win.getNativeWindowHandle = jest.fn().mockReturnValue(Buffer.from([1, 2, 3, 4])); + service = new OsBiometricsServiceWindows(i18nService, windowMain); + }); + + it("should set biometric key", async () => { + await service.setBiometricKey(userId, new SymmetricCryptoKey(mockKey)); + expect(biometrics.provideKey).toHaveBeenCalled(); + }); + + it("should delete biometric key", async () => { + await service.deleteBiometricKey(userId); + expect(biometrics.unenroll).toHaveBeenCalled(); + }); + + it("should get biometric key", async () => { + (biometrics.unlock as jest.Mock).mockResolvedValue(mockKey); + const result = await service.getBiometricKey(userId); + expect(result).toBeInstanceOf(SymmetricCryptoKey); + }); + + it("should return null if no biometric key", async () => { + (biometrics.unlock as jest.Mock).mockRejectedValue(new Error("No key found")); + const result = await service.getBiometricKey(userId); + expect(result).toBeNull(); + }); + + it("should authenticate biometric", async () => { + (biometrics.authenticate as jest.Mock).mockResolvedValue(true); + const result = await service.authenticateBiometric(); + expect(result).toBe(true); + }); + + it("should check if biometrics is supported", async () => { + (biometrics.authenticateAvailable as jest.Mock).mockResolvedValue(true); + const result = await service.supportsBiometrics(); + expect(result).toBe(true); + }); + + it("should return needs setup false", async () => { + const result = await service.needsSetup(); + expect(result).toBe(false); + }); + + it("should return auto setup false", async () => { + const result = await service.canAutoSetup(); + expect(result).toBe(false); + }); + + it("should get biometrics first unlock status for user", async () => { + (biometrics.unlockAvailable as jest.Mock).mockResolvedValue(true); + const result = await service.getBiometricsFirstUnlockStatusForUser(userId); + expect(result).toBe(BiometricsStatus.Available); + }); + + it("should return false for hasPersistentKey false", async () => { + (biometrics.hasPersistent as jest.Mock).mockResolvedValue(false); + const result = await service.hasPersistentKey(userId); + expect(result).toBe(false); + }); + + it("should return false for hasPersistentKey true", async () => { + (biometrics.hasPersistent as jest.Mock).mockResolvedValue(true); + const result = await service.hasPersistentKey(userId); + expect(result).toBe(true); + }); +}); 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 c47a639033b..163e4a2c2d0 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 @@ -33,12 +33,16 @@ export default class OsBiometricsServiceWindows implements OsBiometricService { } async getBiometricKey(userId: UserId): Promise { - const key = await biometrics.unlock( - this.biometricsSystem, - userId, - this.windowMain.win.getNativeWindowHandle(), - ); - return new SymmetricCryptoKey(Uint8Array.from(key)); + try { + const key = await biometrics.unlock( + this.biometricsSystem, + userId, + this.windowMain.win.getNativeWindowHandle(), + ); + return new SymmetricCryptoKey(Uint8Array.from(key)); + } catch { + return null; + } } async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise {