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 6415443bfbc..97e1d322a0e 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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserId } from "@bitwarden/common/types/guid"; import { BiometricsService } from "@bitwarden/key-management"; @@ -6,10 +7,10 @@ import { BiometricsService } from "@bitwarden/key-management"; * specifically for the main process. */ export abstract class DesktopBiometricsService extends BiometricsService { - abstract setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise; + abstract setBiometricProtectedUnlockKeyForUser( + userId: UserId, + value: SymmetricCryptoKey, + ): Promise; abstract deleteBiometricUnlockKeyForUser(userId: UserId): Promise; - abstract setupBiometrics(): Promise; - - abstract setClientKeyHalfForUser(userId: UserId, value: string | null): Promise; } 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 index fe40aad54d9..e270c4cc50f 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts @@ -1,5 +1,6 @@ import { ipcMain } from "electron"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { UserId } from "@bitwarden/common/types/guid"; @@ -37,17 +38,12 @@ export class MainBiometricsIPCListener { } return await this.biometricService.setBiometricProtectedUnlockKeyForUser( message.userId as UserId, - message.key, + SymmetricCryptoKey.fromString(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(); diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts b/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts index 09a4dcef4b3..213f3d48a98 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts @@ -1,10 +1,10 @@ import { mock, MockProxy } from "jest-mock-extended"; +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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { EncryptionType } 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 { UserId } from "@bitwarden/common/types/guid"; import { BiometricsService, @@ -13,6 +13,7 @@ import { } from "@bitwarden/key-management"; import { WindowMain } from "../../main/window.main"; +import { MainCryptoFunctionService } from "../../platform/main/main-crypto-function.service"; import { MainBiometricsService } from "./main-biometrics.service"; import OsBiometricsServiceLinux from "./os-biometrics-linux.service"; @@ -27,21 +28,25 @@ jest.mock("@bitwarden/desktop-napi", () => { }; }); +const unlockKey = new SymmetricCryptoKey(new Uint8Array(64)); + describe("MainBiometricsService", function () { const i18nService = mock(); const windowMain = mock(); const logService = mock(); - const messagingService = mock(); const biometricStateService = mock(); + const cryptoFunctionService = mock(); + const encryptService = mock(); it("Should call the platformspecific methods", async () => { const sut = new MainBiometricsService( i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); const mockService = mock(); @@ -57,9 +62,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, "win32", biometricStateService, + encryptService, + cryptoFunctionService, ); const internalService = (sut as any).osBiometricsService; @@ -72,9 +78,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, "darwin", biometricStateService, + encryptService, + cryptoFunctionService, ); const internalService = (sut as any).osBiometricsService; expect(internalService).not.toBeNull(); @@ -86,9 +93,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, "linux", biometricStateService, + encryptService, + cryptoFunctionService, ); const internalService = (sut as any).osBiometricsService; @@ -106,9 +114,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); innerService = mock(); @@ -131,9 +140,9 @@ describe("MainBiometricsService", function () { ]; 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); + innerService.supportsBiometrics.mockResolvedValue(supportsBiometric as boolean); + innerService.needsSetup.mockResolvedValue(needsSetup as boolean); + innerService.canAutoSetup.mockResolvedValue(canAutoSetup as boolean); const actual = await sut.getBiometricsStatus(); expect(actual).toBe(expected); @@ -175,12 +184,23 @@ describe("MainBiometricsService", function () { 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"); + if (!requirePasswordOnStart) { + (sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest + .fn() + .mockResolvedValue(BiometricsStatus.Available); + } else { + if (hasKeyHalf) { + (sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest + .fn() + .mockResolvedValue(BiometricsStatus.Available); + } else { + (sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest + .fn() + .mockResolvedValue(BiometricsStatus.UnlockNeeded); + } } + const userId = "test" as UserId; const actual = await sut.getBiometricsStatusForUser(userId); expect(actual).toBe(expected); } @@ -193,50 +213,17 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); const osBiometricsService = mock(); (sut as any).osBiometricsService = osBiometricsService; await sut.setupBiometrics(); - expect(osBiometricsService.osBiometricsSetup).toHaveBeenCalled(); - }); - }); - - describe("setClientKeyHalfForUser", () => { - let sut: MainBiometricsService; - - beforeEach(() => { - sut = new MainBiometricsService( - i18nService, - windowMain, - logService, - messagingService, - process.platform, - biometricStateService, - ); - }); - - it("should set the client key half for the user", async () => { - const userId = "test" as UserId; - const keyHalf = "testKeyHalf"; - - await sut.setClientKeyHalfForUser(userId, keyHalf); - - expect((sut as any).clientKeyHalves.has(userId)).toBe(true); - expect((sut as any).clientKeyHalves.get(userId)).toBe(keyHalf); - }); - - it("should reset the client key half for the user", async () => { - const userId = "test" as UserId; - - await sut.setClientKeyHalfForUser(userId, null); - - expect((sut as any).clientKeyHalves.has(userId)).toBe(true); - expect((sut as any).clientKeyHalves.get(userId)).toBe(null); + expect(osBiometricsService.runSetup).toHaveBeenCalled(); }); }); @@ -246,9 +233,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); const osBiometricsService = mock(); (sut as any).osBiometricsService = osBiometricsService; @@ -268,9 +256,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); osBiometricsService = mock(); (sut as any).osBiometricsService = osBiometricsService; @@ -278,34 +267,24 @@ describe("MainBiometricsService", function () { it("should return null if no biometric key is returned ", async () => { const userId = "test" as UserId; - (sut as any).clientKeyHalves.set(userId, "testKeyHalf"); - + osBiometricsService.getBiometricKey.mockResolvedValue(null); const userKey = await sut.unlockWithBiometricsForUser(userId); expect(userKey).toBeNull(); - expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith( - "Bitwarden_biometric", - `${userId}_user_biometric`, - "testKeyHalf", - ); + expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(userId); }); it("should return the biometric key if a valid key is returned", async () => { const userId = "test" as UserId; - (sut as any).clientKeyHalves.set(userId, "testKeyHalf"); - const biometricKey = Utils.fromBufferToB64(new Uint8Array(64)); + const biometricKey = new SymmetricCryptoKey(new Uint8Array(64)); osBiometricsService.getBiometricKey.mockResolvedValue(biometricKey); const userKey = await sut.unlockWithBiometricsForUser(userId); expect(userKey).not.toBeNull(); - expect(userKey!.keyB64).toBe(biometricKey); + expect(userKey!.keyB64).toBe(biometricKey.toBase64()); expect(userKey!.inner().type).toBe(EncryptionType.AesCbc256_HmacSha256_B64); - expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith( - "Bitwarden_biometric", - `${userId}_user_biometric`, - "testKeyHalf", - ); + expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(userId); }); }); @@ -318,37 +297,21 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); osBiometricsService = mock(); (sut as any).osBiometricsService = osBiometricsService; }); - it("should throw an error if no client key half is provided", async () => { - const userId = "test" as UserId; - const unlockKey = "testUnlockKey"; - - await expect(sut.setBiometricProtectedUnlockKeyForUser(userId, unlockKey)).rejects.toThrow( - "No client key half provided for user", - ); - }); - it("should call the platform specific setBiometricKey method", async () => { const userId = "test" as UserId; - const unlockKey = "testUnlockKey"; - - (sut as any).clientKeyHalves.set(userId, "testKeyHalf"); await sut.setBiometricProtectedUnlockKeyForUser(userId, unlockKey); - expect(osBiometricsService.setBiometricKey).toHaveBeenCalledWith( - "Bitwarden_biometric", - `${userId}_user_biometric`, - unlockKey, - "testKeyHalf", - ); + expect(osBiometricsService.setBiometricKey).toHaveBeenCalledWith(userId, unlockKey); }); }); @@ -358,9 +321,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); const osBiometricsService = mock(); (sut as any).osBiometricsService = osBiometricsService; @@ -369,10 +333,7 @@ describe("MainBiometricsService", function () { await sut.deleteBiometricUnlockKeyForUser(userId); - expect(osBiometricsService.deleteBiometricKey).toHaveBeenCalledWith( - "Bitwarden_biometric", - `${userId}_user_biometric`, - ); + expect(osBiometricsService.deleteBiometricKey).toHaveBeenCalledWith(userId); }); }); @@ -384,9 +345,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); }); @@ -413,9 +375,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); const shouldAutoPrompt = await sut.getShouldAutopromptNow(); 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 cf80fa5f7f3..a6a0e532655 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts @@ -1,6 +1,7 @@ +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 { 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"; @@ -13,16 +14,16 @@ 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, + platform: NodeJS.Platform, private biometricStateService: BiometricStateService, + private encryptService: EncryptService, + private cryptoFunctionService: CryptoFunctionService, ) { super(); if (platform === "win32") { @@ -32,6 +33,9 @@ export class MainBiometricsService extends DesktopBiometricsService { this.i18nService, this.windowMain, this.logService, + this.biometricStateService, + this.encryptService, + this.cryptoFunctionService, ); } else if (platform === "darwin") { // eslint-disable-next-line @@ -40,7 +44,11 @@ export class MainBiometricsService extends DesktopBiometricsService { } else if (platform === "linux") { // eslint-disable-next-line const OsBiometricsServiceLinux = require("./os-biometrics-linux.service").default; - this.osBiometricsService = new OsBiometricsServiceLinux(this.i18nService, this.windowMain); + this.osBiometricsService = new OsBiometricsServiceLinux( + this.biometricStateService, + this.encryptService, + this.cryptoFunctionService, + ); } else { throw new Error("Unsupported platform"); } @@ -55,11 +63,11 @@ export class MainBiometricsService extends DesktopBiometricsService { * @returns the status of the biometrics of the platform */ async getBiometricsStatus(): Promise { - if (!(await this.osBiometricsService.osSupportsBiometric())) { + if (!(await this.osBiometricsService.supportsBiometrics())) { return BiometricsStatus.HardwareUnavailable; } else { - if (await this.osBiometricsService.osBiometricsNeedsSetup()) { - if (await this.osBiometricsService.osBiometricsCanAutoSetup()) { + if (await this.osBiometricsService.needsSetup()) { + if (await this.osBiometricsService.canAutoSetup()) { return BiometricsStatus.AutoSetupNeeded; } else { return BiometricsStatus.ManualSetupNeeded; @@ -80,20 +88,12 @@ export class MainBiometricsService extends DesktopBiometricsService { 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; + return await this.osBiometricsService.getBiometricsFirstUnlockStatusForUser(userId); } async authenticateBiometric(): Promise { @@ -101,11 +101,7 @@ export class MainBiometricsService extends DesktopBiometricsService { } async setupBiometrics(): Promise { - return await this.osBiometricsService.osBiometricsSetup(); - } - - async setClientKeyHalfForUser(userId: UserId, value: string | null): Promise { - this.clientKeyHalves.set(userId, value); + return await this.osBiometricsService.runSetup(); } async authenticateWithBiometrics(): Promise { @@ -113,43 +109,23 @@ export class MainBiometricsService extends DesktopBiometricsService { } async unlockWithBiometricsForUser(userId: UserId): Promise { - const biometricKey = await this.osBiometricsService.getBiometricKey( - "Bitwarden_biometric", - `${userId}_user_biometric`, - this.clientKeyHalves.get(userId) ?? undefined, - ); - if (biometricKey == null) { - return null; - } - - return SymmetricCryptoKey.fromString(biometricKey) as UserKey; + return (await this.osBiometricsService.getBiometricKey(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) ?? undefined, - ); + async setBiometricProtectedUnlockKeyForUser( + userId: UserId, + key: SymmetricCryptoKey, + ): Promise { + return await this.osBiometricsService.setBiometricKey(userId, key); } async deleteBiometricUnlockKeyForUser(userId: UserId): Promise { - return await this.osBiometricsService.deleteBiometricKey( - "Bitwarden_biometric", - `${userId}_user_biometric`, - ); + return await this.osBiometricsService.deleteBiometricKey(userId); } /** * 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 + * 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 { 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 fb150f2a653..8d3c8e9795f 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,10 +1,14 @@ import { spawn } from "child_process"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +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 { biometrics, passwords } from "@bitwarden/desktop-napi"; +import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management"; -import { WindowMain } from "../../main/window.main"; import { isFlatpak, isLinux, isSnapStore } from "../../utils"; import { OsBiometricService } from "./os-biometrics.service"; @@ -28,59 +32,62 @@ 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 i18nservice: I18nService, - private windowMain: WindowMain, + private biometricStateService: BiometricStateService, + private encryptService: EncryptService, + private cryptoFunctionService: CryptoFunctionService, ) {} private _iv: string | null = null; // Use getKeyMaterial helper instead of direct access private _osKeyHalf: string | null = null; + private clientKeyHalves = new Map(); - async setBiometricKey( - service: string, - key: string, - value: string, - clientKeyPartB64: string | undefined, - ): Promise { + async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise { + const clientKeyPartB64 = Utils.fromBufferToB64( + await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key), + ); const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 }); await biometrics.setBiometricSecret( - service, - key, - value, + SERVICE, + getLookupKeyForUser(userId), + key.toBase64(), storageDetails.key_material, storageDetails.ivB64, ); } - async deleteBiometricKey(service: string, key: string): Promise { - await passwords.deletePassword(service, key); + async deleteBiometricKey(userId: UserId): Promise { + await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId)); } - async getBiometricKey( - service: string, - storageKey: string, - clientKeyPartB64: string | undefined, - ): 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, storageKey); + const value = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId)); if (value == null || value == "") { return null; } else { + const clientKeyHalf = this.clientKeyHalves.get(userId); + const clientKeyPartB64 = Utils.fromBufferToB64(clientKeyHalf); const encValue = new EncString(value); this.setIv(encValue.iv); const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 }); const storedValue = await biometrics.getBiometricSecret( - service, - storageKey, + SERVICE, + getLookupKeyForUser(userId), storageDetails.key_material, ); - return storedValue; + return SymmetricCryptoKey.fromString(storedValue); } } @@ -89,7 +96,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService { return await biometrics.prompt(hwnd, ""); } - async osSupportsBiometric(): Promise { + 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 @@ -99,7 +106,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService { return await passwords.isAvailable(); } - async osBiometricsNeedsSetup(): Promise { + async needsSetup(): Promise { if (isSnapStore()) { return false; } @@ -108,7 +115,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService { return !(await biometrics.available()); } - async osBiometricsCanAutoSetup(): Promise { + 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 @@ -116,7 +123,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService { return isLinux() && !isSnapStore() && !isFlatpak(); } - async osBiometricsSetup(): Promise { + async runSetup(): Promise { const process = spawn("pkexec", [ "bash", "-c", @@ -165,4 +172,46 @@ export default class OsBiometricsServiceLinux implements OsBiometricService { ivB64: this._iv, }; } + + private async getOrCreateBiometricEncryptionClientKeyHalf( + userId: UserId, + key: SymmetricCryptoKey, + ): Promise { + const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); + if (!requireClientKeyHalf) { + return null; + } + + 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 + const keyBytes = await this.cryptoFunctionService.randomBytes(32); + const encKey = await this.encryptService.encryptBytes(keyBytes, key); + await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId); + } + + this.clientKeyHalves.set(userId, clientKeyHalf); + + return clientKeyHalf; + } + + async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise { + 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; + } } diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-mac.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-mac.service.ts index e361084726a..004495b6da9 100644 --- a/apps/desktop/src/key-management/biometrics/os-biometrics-mac.service.ts +++ b/apps/desktop/src/key-management/biometrics/os-biometrics-mac.service.ts @@ -1,14 +1,22 @@ import { systemPreferences } from "electron"; 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 { passwords } from "@bitwarden/desktop-napi"; +import { BiometricsStatus } from "@bitwarden/key-management"; import { OsBiometricService } from "./os-biometrics.service"; +const SERVICE = "Bitwarden_biometric"; +function getLookupKeyForUser(userId: UserId): string { + return `${userId}_user_biometric`; +} + export default class OsBiometricsServiceMac implements OsBiometricService { constructor(private i18nservice: I18nService) {} - async osSupportsBiometric(): Promise { + async supportsBiometrics(): Promise { return systemPreferences.canPromptTouchID(); } @@ -21,44 +29,52 @@ export default class OsBiometricsServiceMac implements OsBiometricService { } } - async getBiometricKey(service: string, key: string): Promise { + async getBiometricKey(userId: UserId): Promise { const success = await this.authenticateBiometric(); if (!success) { throw new Error("Biometric authentication failed"); } + const keyB64 = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId)); + if (keyB64 == null) { + return null; + } - return await passwords.getPassword(service, key); + return SymmetricCryptoKey.fromString(keyB64); } - async setBiometricKey(service: string, key: string, value: string): Promise { - if (await this.valueUpToDate(service, key, value)) { + async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise { + if (await this.valueUpToDate(userId, key)) { return; } - return await passwords.setPassword(service, key, value); + return await passwords.setPassword(SERVICE, getLookupKeyForUser(userId), key.toBase64()); } - async deleteBiometricKey(service: string, key: string): Promise { - return await passwords.deletePassword(service, key); + async deleteBiometricKey(user: UserId): Promise { + return await passwords.deletePassword(SERVICE, getLookupKeyForUser(user)); } - private async valueUpToDate(service: string, key: string, value: string): Promise { + private async valueUpToDate(user: UserId, key: SymmetricCryptoKey): Promise { try { - const existing = await passwords.getPassword(service, key); - return existing === value; + const existing = await passwords.getPassword(SERVICE, getLookupKeyForUser(user)); + return existing === key.toBase64(); } catch { return false; } } - async osBiometricsNeedsSetup() { + async needsSetup() { return false; } - async osBiometricsCanAutoSetup(): Promise { + async canAutoSetup(): Promise { return false; } - async osBiometricsSetup(): Promise {} + async runSetup(): Promise {} + + async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise { + return BiometricsStatus.Available; + } } 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..d0fd8682f2a --- /dev/null +++ b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.spec.ts @@ -0,0 +1,143 @@ +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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management"; + +import OsBiometricsServiceWindows from "./os-biometrics-windows.service"; + +jest.mock("@bitwarden/desktop-napi", () => ({ + biometrics: { + available: jest.fn(), + setBiometricSecret: jest.fn(), + getBiometricSecret: jest.fn(), + deriveKeyMaterial: jest.fn(), + prompt: jest.fn(), + }, + passwords: { + getPassword: jest.fn(), + deletePassword: jest.fn(), + }, +})); + +describe("OsBiometricsServiceWindows", () => { + let service: OsBiometricsServiceWindows; + let biometricStateService: BiometricStateService; + + beforeEach(() => { + const i18nService = mock(); + const logService = mock(); + biometricStateService = mock(); + const encryptionService = mock(); + const cryptoFunctionService = mock(); + service = new OsBiometricsServiceWindows( + i18nService, + null, + logService, + biometricStateService, + encryptionService, + cryptoFunctionService, + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("getBiometricsFirstUnlockStatusForUser", () => { + const userId = "test-user-id" as UserId; + it("should return Available when requirePasswordOnRestart is false", async () => { + biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(false); + const result = await service.getBiometricsFirstUnlockStatusForUser(userId); + expect(result).toBe(BiometricsStatus.Available); + }); + it("should return Available when requirePasswordOnRestart is true and client key half is set", async () => { + biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true); + (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 requirePasswordOnRestart is true and client key half is not set", async () => { + biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true); + (service as any).clientKeyHalves = new Map(); + const result = await service.getBiometricsFirstUnlockStatusForUser(userId); + expect(result).toBe(BiometricsStatus.UnlockNeeded); + }); + }); + + describe("getOrCreateBiometricEncryptionClientKeyHalf", () => { + const userId = "test-user-id" as UserId; + const key = new SymmetricCryptoKey(new Uint8Array(64)); + let encryptionService: EncryptService; + let cryptoFunctionService: CryptoFunctionService; + + beforeEach(() => { + encryptionService = mock(); + cryptoFunctionService = mock(); + service = new OsBiometricsServiceWindows( + mock(), + null, + mock(), + biometricStateService, + encryptionService, + cryptoFunctionService, + ); + }); + + it("should return null if getRequirePasswordOnRestart is false", async () => { + biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(false); + const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key); + expect(result).toBeNull(); + }); + + it("should return cached key half if already present", async () => { + biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true); + 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.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true); + 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.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true); + 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).toBeNull(); + expect((service as any).clientKeyHalves.get(userId.toString())).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 53647549295..dc4f8674d7f 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 @@ -1,10 +1,14 @@ +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 { EncryptionType } 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 { biometrics, passwords } from "@bitwarden/desktop-napi"; +import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management"; import { WindowMain } from "../../main/window.main"; @@ -13,87 +17,107 @@ import { OsBiometricService } from "./os-biometrics.service"; const KEY_WITNESS_SUFFIX = "_witness"; const WITNESS_VALUE = "known key"; +const SERVICE = "Bitwarden_biometric"; +function getLookupKeyForUser(userId: UserId): string { + return `${userId}_user_biometric`; +} + 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 private _osKeyHalf: string | null = null; + private clientKeyHalves = new Map(); constructor( private i18nService: I18nService, private windowMain: WindowMain, private logService: LogService, + private biometricStateService: BiometricStateService, + private encryptService: EncryptService, + private cryptoFunctionService: CryptoFunctionService, ) {} - async osSupportsBiometric(): Promise { + async supportsBiometrics(): Promise { return await biometrics.available(); } - async getBiometricKey( - service: string, - storageKey: string, - clientKeyHalfB64: string, - ): Promise { - const value = await passwords.getPassword(service, storageKey); + async getBiometricKey(userId: UserId): Promise { + const value = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId)); + let clientKeyHalfB64: string | null = null; + if (this.clientKeyHalves.has(userId)) { + clientKeyHalfB64 = Utils.fromBufferToB64(this.clientKeyHalves.get(userId)); + } if (value == null || value == "") { return null; } else if (!EncString.isSerializedEncString(value)) { // Update to format encrypted with client key half const storageDetails = await this.getStorageDetails({ - clientKeyHalfB64, + clientKeyHalfB64: clientKeyHalfB64, }); await biometrics.setBiometricSecret( - service, - storageKey, + SERVICE, + getLookupKeyForUser(userId), value, storageDetails.key_material, storageDetails.ivB64, ); - return value; + return SymmetricCryptoKey.fromString(value); } else { const encValue = new EncString(value); this.setIv(encValue.iv); const storageDetails = await this.getStorageDetails({ - clientKeyHalfB64, + clientKeyHalfB64: clientKeyHalfB64, }); - return await biometrics.getBiometricSecret(service, storageKey, storageDetails.key_material); + return SymmetricCryptoKey.fromString( + await biometrics.getBiometricSecret( + SERVICE, + getLookupKeyForUser(userId), + storageDetails.key_material, + ), + ); } } - async setBiometricKey( - service: string, - storageKey: string, - value: string, - clientKeyPartB64: string | undefined, - ): Promise { - const parsedValue = SymmetricCryptoKey.fromString(value); - if (await this.valueUpToDate({ value: parsedValue, clientKeyPartB64, service, storageKey })) { + async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise { + const clientKeyHalf = await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key); + + if ( + await this.valueUpToDate({ + value: key, + clientKeyPartB64: Utils.fromBufferToB64(clientKeyHalf), + service: SERVICE, + storageKey: getLookupKeyForUser(userId), + }) + ) { return; } - const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 }); + const storageDetails = await this.getStorageDetails({ + clientKeyHalfB64: Utils.fromBufferToB64(clientKeyHalf), + }); const storedValue = await biometrics.setBiometricSecret( - service, - storageKey, - value, + SERVICE, + getLookupKeyForUser(userId), + key.toBase64(), storageDetails.key_material, storageDetails.ivB64, ); const parsedStoredValue = new EncString(storedValue); await this.storeValueWitness( - parsedValue, + key, parsedStoredValue, - service, - storageKey, - clientKeyPartB64, + SERVICE, + getLookupKeyForUser(userId), + Utils.fromBufferToB64(clientKeyHalf), ); } - async deleteBiometricKey(service: string, key: string): Promise { - await passwords.deletePassword(service, key); - await passwords.deletePassword(service, key + KEY_WITNESS_SUFFIX); + async deleteBiometricKey(userId: UserId): Promise { + await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId)); + await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId) + KEY_WITNESS_SUFFIX); } async authenticateBiometric(): Promise { @@ -240,13 +264,58 @@ export default class OsBiometricsServiceWindows implements OsBiometricService { return result; } - async osBiometricsNeedsSetup() { + async needsSetup() { return false; } - async osBiometricsCanAutoSetup(): Promise { + async canAutoSetup(): Promise { return false; } - async osBiometricsSetup(): Promise {} + async runSetup(): Promise {} + + async getOrCreateBiometricEncryptionClientKeyHalf( + userId: UserId, + key: SymmetricCryptoKey, + ): Promise { + const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); + if (!requireClientKeyHalf) { + return null; + } + + if (this.clientKeyHalves.has(userId)) { + return this.clientKeyHalves.get(userId); + } + + // 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 + const keyBytes = await this.cryptoFunctionService.randomBytes(32); + const encKey = await this.encryptService.encryptBytes(keyBytes, key); + await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId); + } + + this.clientKeyHalves.set(userId, clientKeyHalf); + + return clientKeyHalf; + } + + async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise { + const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); + if (!requireClientKeyHalf) { + return BiometricsStatus.Available; + } + + if (this.clientKeyHalves.has(userId)) { + return BiometricsStatus.Available; + } else { + return BiometricsStatus.UnlockNeeded; + } + } } diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics.service.ts index f5132200149..63e0527c034 100644 --- a/apps/desktop/src/key-management/biometrics/os-biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/os-biometrics.service.ts @@ -1,32 +1,28 @@ +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsStatus } from "@bitwarden/key-management"; + export interface OsBiometricService { - osSupportsBiometric(): Promise; + supportsBiometrics(): 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; + needsSetup(): 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; + canAutoSetup(): Promise; /** * Starts automatic biometric setup, which places the required configuration files / changes the required settings. */ - osBiometricsSetup: () => Promise; + runSetup(): 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; + getBiometricKey(userId: UserId): Promise; + setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise; + deleteBiometricKey(userId: UserId): Promise; + getBiometricsFirstUnlockStatusForUser(userId: UserId): 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 index 1404d65ae51..c7ed88d390f 100644 --- a/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts @@ -34,8 +34,14 @@ export class RendererBiometricsService extends DesktopBiometricsService { return await ipc.keyManagement.biometric.getBiometricsStatusForUser(id); } - async setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise { - return await ipc.keyManagement.biometric.setBiometricProtectedUnlockKeyForUser(userId, value); + async setBiometricProtectedUnlockKeyForUser( + userId: UserId, + value: SymmetricCryptoKey, + ): Promise { + return await ipc.keyManagement.biometric.setBiometricProtectedUnlockKeyForUser( + userId, + value.toBase64(), + ); } async deleteBiometricUnlockKeyForUser(userId: UserId): Promise { @@ -46,10 +52,6 @@ export class RendererBiometricsService extends DesktopBiometricsService { return await ipc.keyManagement.biometric.setupBiometrics(); } - async setClientKeyHalfForUser(userId: UserId, value: string | null): Promise { - return await ipc.keyManagement.biometric.setClientKeyHalf(userId, value); - } - async getShouldAutopromptNow(): Promise { return await ipc.keyManagement.biometric.getShouldAutoprompt(); } diff --git a/apps/desktop/src/key-management/electron-key.service.spec.ts b/apps/desktop/src/key-management/electron-key.service.spec.ts index 7a0464f5e27..730ad7e4652 100644 --- a/apps/desktop/src/key-management/electron-key.service.spec.ts +++ b/apps/desktop/src/key-management/electron-key.service.spec.ts @@ -9,14 +9,11 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils"; -import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { BiometricStateService, KdfConfigService } from "@bitwarden/key-management"; import { - makeEncString, - makeStaticByteArray, makeSymmetricCryptoKey, FakeAccountService, mockAccountServiceWith, @@ -80,7 +77,6 @@ describe("ElectronKeyService", () => { await keyService.setUserKey(userKey, mockUserId); - expect(biometricService.setClientKeyHalfForUser).not.toHaveBeenCalled(); expect(biometricService.setBiometricProtectedUnlockKeyForUser).not.toHaveBeenCalled(); expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled(); expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId); @@ -96,14 +92,12 @@ describe("ElectronKeyService", () => { await keyService.setUserKey(userKey, mockUserId); - expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith(mockUserId, null); expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith( mockUserId, - userKey.keyB64, + userKey, ); expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled(); expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId); - expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith(mockUserId); }); describe("require password on start enabled", () => { @@ -111,73 +105,11 @@ describe("ElectronKeyService", () => { biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true); }); - it("sets new biometric client key half and biometric unlock key when no biometric client key half stored", async () => { - const clientKeyHalfBytes = makeStaticByteArray(32); - const clientKeyHalf = Utils.fromBufferToUtf8(clientKeyHalfBytes); - const encryptedClientKeyHalf = makeEncString(); - biometricStateService.getEncryptedClientKeyHalf.mockResolvedValue(null); - cryptoFunctionService.randomBytes.mockResolvedValue( - clientKeyHalfBytes.buffer as CsprngArray, - ); - encryptService.encryptString.mockResolvedValue(encryptedClientKeyHalf); - + it("sets biometric key", async () => { await keyService.setUserKey(userKey, mockUserId); - expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith( - mockUserId, - clientKeyHalf, - ); expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith( mockUserId, - userKey.keyB64, - ); - expect(biometricStateService.setEncryptedClientKeyHalf).toHaveBeenCalledWith( - encryptedClientKeyHalf, - mockUserId, - ); - expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith( - mockUserId, - ); - expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith( - mockUserId, - ); - expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith( - mockUserId, - ); - expect(cryptoFunctionService.randomBytes).toHaveBeenCalledWith(32); - expect(encryptService.encryptString).toHaveBeenCalledWith(clientKeyHalf, userKey); - }); - - it("sets decrypted biometric client key half and biometric unlock key when existing biometric client key half stored", async () => { - const encryptedClientKeyHalf = makeEncString(); - const clientKeyHalf = Utils.fromBufferToUtf8(makeStaticByteArray(32)); - biometricStateService.getEncryptedClientKeyHalf.mockResolvedValue( - encryptedClientKeyHalf, - ); - encryptService.decryptString.mockResolvedValue(clientKeyHalf); - - await keyService.setUserKey(userKey, mockUserId); - - expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith( - mockUserId, - clientKeyHalf, - ); - expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith( - mockUserId, - userKey.keyB64, - ); - expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled(); - expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith( - mockUserId, - ); - expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith( - mockUserId, - ); - expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith( - mockUserId, - ); - expect(encryptService.decryptString).toHaveBeenCalledWith( - encryptedClientKeyHalf, userKey, ); }); diff --git a/apps/desktop/src/key-management/electron-key.service.ts b/apps/desktop/src/key-management/electron-key.service.ts index d31e717e7a5..8a6fbfa085f 100644 --- a/apps/desktop/src/key-management/electron-key.service.ts +++ b/apps/desktop/src/key-management/electron-key.service.ts @@ -8,9 +8,7 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils"; import { StateProvider } from "@bitwarden/common/platform/state"; -import { CsprngString } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { @@ -77,10 +75,7 @@ export class ElectronKeyService extends DefaultKeyService { } private 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(userKey, userId); - await this.biometricService.setClientKeyHalfForUser(userId, clientEncKeyHalf); - await this.biometricService.setBiometricProtectedUnlockKeyForUser(userId, userKey.keyB64); + await this.biometricService.setBiometricProtectedUnlockKeyForUser(userId, userKey); } protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId: UserId): Promise { @@ -91,34 +86,4 @@ export class ElectronKeyService extends DefaultKeyService { await this.biometricService.deleteBiometricUnlockKeyForUser(userId); await super.clearAllStoredUserKeys(userId); } - - private async getBiometricEncryptionClientKeyHalf( - userKey: UserKey, - userId: UserId, - ): Promise { - const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); - if (!requireClientKeyHalf) { - return null; - } - - // Retrieve existing key half if it exists - let clientKeyHalf: CsprngString | null = null; - const encryptedClientKeyHalf = - await this.biometricStateService.getEncryptedClientKeyHalf(userId); - if (encryptedClientKeyHalf != null) { - clientKeyHalf = (await this.encryptService.decryptString( - encryptedClientKeyHalf, - userKey, - )) as CsprngString; - } - if (clientKeyHalf == null) { - // Set a key half if it doesn't exist - const keyBytes = await this.cryptoFunctionService.randomBytes(32); - clientKeyHalf = Utils.fromBufferToUtf8(keyBytes) as CsprngString; - const encKey = await this.encryptService.encryptString(clientKeyHalf, userKey); - await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId); - } - - return clientKeyHalf; - } } diff --git a/apps/desktop/src/key-management/preload.ts b/apps/desktop/src/key-management/preload.ts index 3e90c27ab03..7f8576b8472 100644 --- a/apps/desktop/src/key-management/preload.ts +++ b/apps/desktop/src/key-management/preload.ts @@ -25,12 +25,13 @@ const biometric = { action: BiometricAction.GetStatusForUser, userId: userId, } satisfies BiometricMessage), - setBiometricProtectedUnlockKeyForUser: (userId: string, value: string): Promise => - ipcRenderer.invoke("biometric", { + setBiometricProtectedUnlockKeyForUser: (userId: string, keyB64: string): Promise => { + return ipcRenderer.invoke("biometric", { action: BiometricAction.SetKeyForUser, userId: userId, - key: value, - } satisfies BiometricMessage), + key: keyB64, + } satisfies BiometricMessage); + }, deleteBiometricUnlockKeyForUser: (userId: string): Promise => ipcRenderer.invoke("biometric", { action: BiometricAction.RemoveKeyForUser, @@ -40,12 +41,6 @@ const biometric = { ipcRenderer.invoke("biometric", { action: BiometricAction.Setup, } satisfies BiometricMessage), - setClientKeyHalf: (userId: string, value: string | null): Promise => - ipcRenderer.invoke("biometric", { - action: BiometricAction.SetClientKeyHalf, - userId: userId, - key: value, - } satisfies BiometricMessage), getShouldAutoprompt: (): Promise => ipcRenderer.invoke("biometric", { action: BiometricAction.GetShouldAutoprompt, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 5e0ea7f9fac..7d97805e9be 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -10,6 +10,7 @@ import { Subject, firstValueFrom } from "rxjs"; import { SsoUrlService } from "@bitwarden/auth/common"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { ClientType } from "@bitwarden/common/enums"; +import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation"; import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service"; import { Message, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- For dependency creation @@ -187,14 +188,19 @@ export class Main { this.desktopSettingsService = new DesktopSettingsService(stateProvider); const biometricStateService = new DefaultBiometricStateService(stateProvider); - + const encryptService = new EncryptServiceImplementation( + this.mainCryptoFunctionService, + this.logService, + true, + ); this.biometricsService = new MainBiometricsService( this.i18nService, this.windowMain, this.logService, - this.messagingService, process.platform, biometricStateService, + encryptService, + this.mainCryptoFunctionService, ); this.windowMain = new WindowMain( diff --git a/apps/desktop/src/types/biometric-message.ts b/apps/desktop/src/types/biometric-message.ts index 7616b265005..9711b49496d 100644 --- a/apps/desktop/src/types/biometric-message.ts +++ b/apps/desktop/src/types/biometric-message.ts @@ -9,8 +9,6 @@ export enum BiometricAction { SetKeyForUser = "setKeyForUser", RemoveKeyForUser = "removeKeyForUser", - SetClientKeyHalf = "setClientKeyHalf", - Setup = "setup", GetShouldAutoprompt = "getShouldAutoprompt", @@ -18,21 +16,13 @@ export enum BiometricAction { } export type BiometricMessage = - | { - action: BiometricAction.SetClientKeyHalf; - userId: string; - key: string | null; - } | { action: BiometricAction.SetKeyForUser; userId: string; key: string; } | { - action: Exclude< - BiometricAction, - BiometricAction.SetClientKeyHalf | BiometricAction.SetKeyForUser - >; + action: Exclude; userId?: string; data?: any; };