From dc8077c7a025a89eadc8131612878d1f9c77b1be Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 10 Jun 2025 18:32:34 +0200 Subject: [PATCH] Move clientkeyhalf to os platform implementation --- .../main-biometrics.service.spec.ts | 11 +- .../biometrics/main-biometrics.service.ts | 95 +++---------- .../biometrics/os-biometrics-linux.service.ts | 105 ++++++++++---- .../biometrics/os-biometrics-mac.service.ts | 44 ++++-- .../os-biometrics-windows.service.ts | 132 +++++++++++++----- .../biometrics/os-biometrics.service.ts | 28 ++-- apps/desktop/src/main.ts | 1 - 7 files changed, 239 insertions(+), 177 deletions(-) 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 d2c29f4d291..e3927b06954 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 @@ -5,7 +5,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { 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 { @@ -148,9 +147,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); @@ -221,7 +220,7 @@ describe("MainBiometricsService", function () { await sut.setupBiometrics(); - expect(osBiometricsService.osBiometricsSetup).toHaveBeenCalled(); + expect(osBiometricsService.runSetup).toHaveBeenCalled(); }); }); @@ -282,7 +281,7 @@ describe("MainBiometricsService", function () { 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); 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 1c157b1a7b6..9cb408aa597 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts @@ -2,8 +2,6 @@ import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/a 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 { 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 { UserKey } from "@bitwarden/common/types/key"; @@ -16,15 +14,13 @@ 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, @@ -37,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 @@ -45,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"); } @@ -60,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; @@ -85,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 { @@ -106,7 +101,7 @@ export class MainBiometricsService extends DesktopBiometricsService { } async setupBiometrics(): Promise { - return await this.osBiometricsService.osBiometricsSetup(); + return await this.osBiometricsService.runSetup(); } async authenticateWithBiometrics(): Promise { @@ -114,51 +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, key: SymmetricCryptoKey, ): Promise { - const service = "Bitwarden_biometric"; - const storageKey = `${userId}_user_biometric`; - if (!this.clientKeyHalves.has(userId)) { - const clientKeyHalf = await this.getOrCreateBiometricEncryptionClientKeyHalf(key, userId); - if (clientKeyHalf == null) { - throw new Error("Client key half is required for biometric unlock but not set."); - } else { - this.clientKeyHalves.set(userId, Utils.fromBufferToB64(clientKeyHalf)); - } - } - - return await this.osBiometricsService.setBiometricKey( - service, - storageKey, - key.toBase64(), - this.clientKeyHalves.get(userId) ?? undefined, - ); + 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 mpt 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 { @@ -176,30 +143,4 @@ export class MainBiometricsService extends DesktopBiometricsService { async canEnableBiometricUnlock(): Promise { return true; } - - private async getOrCreateBiometricEncryptionClientKeyHalf( - key: SymmetricCryptoKey, - userId: UserId, - ): Promise { - const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); - if (!requireClientKeyHalf) { - return 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); - } - - return clientKeyHalf; - } } 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 c55980068b5..12388a78297 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.toString()); + 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); } } @@ -90,7 +97,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService { return true; } - 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 @@ -100,7 +107,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService { return await passwords.isAvailable(); } - async osBiometricsNeedsSetup(): Promise { + async needsSetup(): Promise { if (isSnapStore()) { return false; } @@ -110,7 +117,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService { return false; } - 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 @@ -118,7 +125,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService { return isLinux() && !isSnapStore() && !isFlatpak(); } - async osBiometricsSetup(): Promise { + async runSetup(): Promise { const process = spawn("pkexec", [ "bash", "-c", @@ -167,4 +174,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.toString(), 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.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts index 53647549295..bb97a371261 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,103 @@ 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)); 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: null, // todo fix }); 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: null, // todo fix }); - 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 +260,55 @@ 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 {} + + private async getOrCreateBiometricEncryptionClientKeyHalf( + userId: UserId, + key: SymmetricCryptoKey, + ): Promise { + const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); + if (!requireClientKeyHalf) { + return null; + } + + if (this.clientKeyHalves.has(userId.toString())) { + return this.clientKeyHalves.get(userId.toString()); + } + + // 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.toString(), 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.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics.service.ts index f5132200149..d6216b53d3b 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/main.ts b/apps/desktop/src/main.ts index a8b567edb08..0cf0189a23d 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -192,7 +192,6 @@ export class Main { this.i18nService, this.windowMain, this.logService, - this.messagingService, process.platform, biometricStateService, encryptService,