import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { ElectronStateService } from "../../services/electron-state.service.abstraction"; import { WindowMain } from "../window.main"; import { BiometricsServiceAbstraction, OsBiometricService } from "./biometrics.service.abstraction"; export class BiometricsService implements BiometricsServiceAbstraction { private platformSpecificService: OsBiometricService; private clientKeyHalves = new Map(); constructor( private i18nService: I18nService, private windowMain: WindowMain, private stateService: ElectronStateService, private logService: LogService, private messagingService: MessagingService, private platform: NodeJS.Platform ) { this.loadPlatformSpecificService(this.platform); } private loadPlatformSpecificService(platform: NodeJS.Platform) { if (platform === "win32") { this.loadWindowsHelloService(); } else if (platform === "darwin") { this.loadMacOSService(); } } private loadWindowsHelloService() { // eslint-disable-next-line const BiometricWindowsMain = require("./biometric.windows.main").default; this.platformSpecificService = new BiometricWindowsMain( this.i18nService, this.windowMain, this.stateService, this.logService ); } private loadMacOSService() { // eslint-disable-next-line const BiometricDarwinMain = require("./biometric.darwin.main").default; this.platformSpecificService = new BiometricDarwinMain(this.i18nService, this.stateService); } async init() { return await this.platformSpecificService.init(); } async osSupportsBiometric() { return await this.platformSpecificService.osSupportsBiometric(); } async canAuthBiometric({ service, key, userId, }: { service: string; key: string; userId: string; }): Promise { const requireClientKeyHalf = await this.stateService.getBiometricRequirePasswordOnStart({ userId, }); const clientKeyHalfB64 = this.getClientKeyHalf(service, key); const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64; return clientKeyHalfSatisfied && (await this.osSupportsBiometric()); } async authenticateBiometric(): Promise { let result = false; this.interruptProcessReload( () => { return this.platformSpecificService.authenticateBiometric(); }, (response) => { result = response; return !response; } ); return result; } async getBiometricKey(service: string, storageKey: string): Promise { return await this.interruptProcessReload(async () => { await this.enforceClientKeyHalf(service, storageKey); return await this.platformSpecificService.getBiometricKey( service, storageKey, this.getClientKeyHalf(service, storageKey) ); }); } async setBiometricKey(service: string, storageKey: string, value: string): Promise { await this.enforceClientKeyHalf(service, storageKey); return await this.platformSpecificService.setBiometricKey( service, storageKey, value, this.getClientKeyHalf(service, storageKey) ); } /** Registers the client-side encryption key half for the OS stored Biometric key. The other half is protected by the OS.*/ async setEncryptionKeyHalf({ service, key, value, }: { service: string; key: string; value: string; }): Promise { if (value == null) { this.clientKeyHalves.delete(this.clientKeyHalfKey(service, key)); } else { this.clientKeyHalves.set(this.clientKeyHalfKey(service, key), value); } } async deleteBiometricKey(service: string, storageKey: string): Promise { this.clientKeyHalves.delete(this.clientKeyHalfKey(service, storageKey)); return await this.platformSpecificService.deleteBiometricKey(service, storageKey); } private async interruptProcessReload( callback: () => Promise, restartReloadCallback: (arg: T) => boolean = () => false ): Promise { this.messagingService.send("cancelProcessReload"); let restartReload = false; let response: T; try { response = await callback(); restartReload ||= restartReloadCallback(response); } catch { restartReload = true; } if (restartReload) { this.messagingService.send("startProcessReload"); } return response; } private clientKeyHalfKey(service: string, key: string): string { return `${service}:${key}`; } private getClientKeyHalf(service: string, key: string): string | undefined { return this.clientKeyHalves.get(this.clientKeyHalfKey(service, key)) ?? undefined; } private async enforceClientKeyHalf(service: string, storageKey: string): Promise { const requireClientKeyHalf = await this.stateService.getBiometricRequirePasswordOnStart(); const clientKeyHalfB64 = this.getClientKeyHalf(service, storageKey); if (requireClientKeyHalf && !clientKeyHalfB64) { throw new Error("Biometric key requirements not met. No client key half provided."); } } }