From bd7c6f3c7abe85b0adb7f44a5a54fd3f48885cfe Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 1 Dec 2025 10:16:36 +0100 Subject: [PATCH] [PM-26570] Remove biometrics v1 (#17629) * Remove biometrics v1 * Cargo fmt * Fix windows build * Apply prettier * Remove proxy code * Fix build * Fix * Fix tests * Remove v2 flag --- .../desktop_native/core/src/biometric/mod.rs | 10 +- .../biometric/{macos.rs => unimplemented.rs} | 2 +- .../core/src/biometric/windows.rs | 240 ----------- .../core/src/biometric/windows_focus.rs | 28 -- apps/desktop/desktop_native/proxy/src/main.rs | 9 - .../desktop_native/proxy/src/windows.rs | 23 -- .../src/app/accounts/settings.component.html | 3 +- .../app/accounts/settings.component.spec.ts | 6 +- .../src/app/accounts/settings.component.ts | 18 +- .../biometrics/desktop.biometrics.service.ts | 3 - .../main-biometrics-ipc.listener.ts | 4 - .../main-biometrics.service.spec.ts | 75 +--- .../biometrics/main-biometrics.service.ts | 24 +- .../os-biometrics-windows.service.spec.ts | 378 ------------------ .../os-biometrics-windows.service.ts | 214 ---------- .../biometrics/renderer-biometrics.service.ts | 8 - apps/desktop/src/key-management/preload.ts | 8 - .../biometric-message-handler.service.spec.ts | 15 - .../biometric-message-handler.service.ts | 7 - apps/desktop/src/types/biometric-message.ts | 3 - libs/common/src/enums/feature-flag.enum.ts | 2 - 21 files changed, 14 insertions(+), 1066 deletions(-) rename apps/desktop/desktop_native/core/src/biometric/{macos.rs => unimplemented.rs} (94%) delete mode 100644 apps/desktop/desktop_native/core/src/biometric/windows.rs delete mode 100644 apps/desktop/desktop_native/core/src/biometric/windows_focus.rs delete mode 100644 apps/desktop/desktop_native/proxy/src/windows.rs delete mode 100644 apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.spec.ts delete mode 100644 apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts diff --git a/apps/desktop/desktop_native/core/src/biometric/mod.rs b/apps/desktop/desktop_native/core/src/biometric/mod.rs index 937c67ff30a..6ed5fbe08a7 100644 --- a/apps/desktop/desktop_native/core/src/biometric/mod.rs +++ b/apps/desktop/desktop_native/core/src/biometric/mod.rs @@ -3,16 +3,12 @@ use anyhow::{anyhow, Result}; #[allow(clippy::module_inception)] #[cfg_attr(target_os = "linux", path = "unix.rs")] -#[cfg_attr(target_os = "macos", path = "macos.rs")] -#[cfg_attr(target_os = "windows", path = "windows.rs")] +#[cfg_attr(target_os = "macos", path = "unimplemented.rs")] +#[cfg_attr(target_os = "windows", path = "unimplemented.rs")] mod biometric; -pub use biometric::Biometric; - -#[cfg(target_os = "windows")] -pub mod windows_focus; - use base64::{engine::general_purpose::STANDARD as base64_engine, Engine}; +pub use biometric::Biometric; use sha2::{Digest, Sha256}; use crate::crypto::{self, CipherString}; diff --git a/apps/desktop/desktop_native/core/src/biometric/macos.rs b/apps/desktop/desktop_native/core/src/biometric/unimplemented.rs similarity index 94% rename from apps/desktop/desktop_native/core/src/biometric/macos.rs rename to apps/desktop/desktop_native/core/src/biometric/unimplemented.rs index ec09d566e1f..3f3d034924a 100644 --- a/apps/desktop/desktop_native/core/src/biometric/macos.rs +++ b/apps/desktop/desktop_native/core/src/biometric/unimplemented.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Result}; use crate::biometric::{KeyMaterial, OsDerivedKey}; -/// The MacOS implementation of the biometric trait. +/// Unimplemented stub for unsupported platforms pub struct Biometric {} impl super::BiometricTrait for Biometric { diff --git a/apps/desktop/desktop_native/core/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs deleted file mode 100644 index f72282d9284..00000000000 --- a/apps/desktop/desktop_native/core/src/biometric/windows.rs +++ /dev/null @@ -1,240 +0,0 @@ -use std::{ffi::c_void, str::FromStr}; - -use anyhow::{anyhow, Result}; -use base64::{engine::general_purpose::STANDARD as base64_engine, Engine}; -use rand::RngCore; -use sha2::{Digest, Sha256}; -use windows::{ - core::{factory, HSTRING}, - Security::Credentials::UI::{ - UserConsentVerificationResult, UserConsentVerifier, UserConsentVerifierAvailability, - }, - Win32::{ - Foundation::HWND, System::WinRT::IUserConsentVerifierInterop, - UI::WindowsAndMessaging::GetForegroundWindow, - }, -}; -use windows_future::IAsyncOperation; - -use super::{decrypt, encrypt, windows_focus::set_focus}; -use crate::{ - biometric::{KeyMaterial, OsDerivedKey}, - crypto::CipherString, -}; - -/// The Windows OS implementation of the biometric trait. -pub struct Biometric {} - -impl super::BiometricTrait for Biometric { - // FIXME: Remove unwraps! They panic and terminate the whole application. - #[allow(clippy::unwrap_used)] - async fn prompt(hwnd: Vec, message: String) -> Result { - let h = isize::from_le_bytes(hwnd.clone().try_into().unwrap()); - - let h = h as *mut c_void; - let window = HWND(h); - - // The Windows Hello prompt is displayed inside the application window. For best result we - // should set the window to the foreground and focus it. - set_focus(window); - - // Windows Hello prompt must be in foreground, focused, otherwise the face or fingerprint - // unlock will not work. We get the current foreground window, which will either be the - // Bitwarden desktop app or the browser extension. - let foreground_window = unsafe { GetForegroundWindow() }; - - let interop = factory::()?; - let operation: IAsyncOperation = unsafe { - interop.RequestVerificationForWindowAsync(foreground_window, &HSTRING::from(message))? - }; - let result = operation.get()?; - - match result { - UserConsentVerificationResult::Verified => Ok(true), - _ => Ok(false), - } - } - - async fn available() -> Result { - let ucv_available = UserConsentVerifier::CheckAvailabilityAsync()?.get()?; - - match ucv_available { - UserConsentVerifierAvailability::Available => Ok(true), - // TODO: look into removing this and making the check more ad-hoc - UserConsentVerifierAvailability::DeviceBusy => Ok(true), - _ => Ok(false), - } - } - - fn derive_key_material(challenge_str: Option<&str>) -> Result { - let challenge: [u8; 16] = match challenge_str { - Some(challenge_str) => base64_engine - .decode(challenge_str)? - .try_into() - .map_err(|e: Vec<_>| anyhow!("Expect length {}, got {}", 16, e.len()))?, - None => random_challenge(), - }; - - // Uses a key derived from the iv. This key is not intended to add any security - // but only a place-holder - let key = Sha256::digest(challenge); - let key_b64 = base64_engine.encode(key); - let iv_b64 = base64_engine.encode(challenge); - Ok(OsDerivedKey { key_b64, iv_b64 }) - } - - async fn set_biometric_secret( - service: &str, - account: &str, - secret: &str, - key_material: Option, - iv_b64: &str, - ) -> Result { - let key_material = key_material.ok_or(anyhow!( - "Key material is required for Windows Hello protected keys" - ))?; - - let encrypted_secret = encrypt(secret, &key_material, iv_b64)?; - crate::password::set_password(service, account, &encrypted_secret).await?; - Ok(encrypted_secret) - } - - async fn get_biometric_secret( - service: &str, - account: &str, - key_material: Option, - ) -> Result { - let key_material = key_material.ok_or(anyhow!( - "Key material is required for Windows Hello protected keys" - ))?; - - let encrypted_secret = crate::password::get_password(service, account).await?; - match CipherString::from_str(&encrypted_secret) { - Ok(secret) => { - // If the secret is a CipherString, it is encrypted and we need to decrypt it. - let secret = decrypt(&secret, &key_material)?; - Ok(secret) - } - Err(_) => { - // If the secret is not a CipherString, it is not encrypted and we can return it - // directly. - Ok(encrypted_secret) - } - } - } -} - -fn random_challenge() -> [u8; 16] { - let mut challenge = [0u8; 16]; - rand::rng().fill_bytes(&mut challenge); - challenge -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::biometric::BiometricTrait; - - #[test] - fn test_derive_key_material() { - let iv_input = "l9fhDUP/wDJcKwmEzcb/3w=="; - let result = ::derive_key_material(Some(iv_input)).unwrap(); - let key = base64_engine.decode(result.key_b64).unwrap(); - assert_eq!(key.len(), 32); - assert_eq!(result.iv_b64, iv_input) - } - - #[test] - fn test_derive_key_material_no_iv() { - let result = ::derive_key_material(None).unwrap(); - let key = base64_engine.decode(result.key_b64).unwrap(); - assert_eq!(key.len(), 32); - let iv = base64_engine.decode(result.iv_b64).unwrap(); - assert_eq!(iv.len(), 16); - } - - #[tokio::test] - #[cfg(feature = "manual_test")] - async fn test_prompt() { - ::prompt( - vec![0, 0, 0, 0, 0, 0, 0, 0], - String::from("Hello from Rust"), - ) - .await - .unwrap(); - } - - #[tokio::test] - #[cfg(feature = "manual_test")] - async fn test_available() { - assert!(::available().await.unwrap()) - } - - #[tokio::test] - #[cfg(feature = "manual_test")] - async fn get_biometric_secret_requires_key() { - let result = ::get_biometric_secret("", "", None).await; - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().to_string(), - "Key material is required for Windows Hello protected keys" - ); - } - - #[tokio::test] - #[cfg(feature = "manual_test")] - async fn get_biometric_secret_handles_unencrypted_secret() { - let test = "test"; - let secret = "password"; - let key_material = KeyMaterial { - os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(), - client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()), - }; - crate::password::set_password(test, test, secret) - .await - .unwrap(); - let result = - ::get_biometric_secret(test, test, Some(key_material)) - .await - .unwrap(); - crate::password::delete_password("test", "test") - .await - .unwrap(); - assert_eq!(result, secret); - } - - #[tokio::test] - #[cfg(feature = "manual_test")] - async fn get_biometric_secret_handles_encrypted_secret() { - let test = "test"; - let secret = - CipherString::from_str("0.l9fhDUP/wDJcKwmEzcb/3w==|uP4LcqoCCj5FxBDP77NV6Q==").unwrap(); // output from test_encrypt - let key_material = KeyMaterial { - os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(), - client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()), - }; - crate::password::set_password(test, test, &secret.to_string()) - .await - .unwrap(); - - let result = - ::get_biometric_secret(test, test, Some(key_material)) - .await - .unwrap(); - crate::password::delete_password("test", "test") - .await - .unwrap(); - assert_eq!(result, "secret"); - } - - #[tokio::test] - async fn set_biometric_secret_requires_key() { - let result = - ::set_biometric_secret("", "", "", None, "").await; - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().to_string(), - "Key material is required for Windows Hello protected keys" - ); - } -} diff --git a/apps/desktop/desktop_native/core/src/biometric/windows_focus.rs b/apps/desktop/desktop_native/core/src/biometric/windows_focus.rs deleted file mode 100644 index ce51f82862d..00000000000 --- a/apps/desktop/desktop_native/core/src/biometric/windows_focus.rs +++ /dev/null @@ -1,28 +0,0 @@ -use windows::{ - core::s, - Win32::{ - Foundation::HWND, - UI::{ - Input::KeyboardAndMouse::SetFocus, - WindowsAndMessaging::{FindWindowA, SetForegroundWindow}, - }, - }, -}; - -/// Searches for a window that looks like a security prompt and set it as focused. -/// Only works when the process has permission to foreground, either by being in foreground -/// Or by being given foreground permission https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setforegroundwindow#remarks -pub fn focus_security_prompt() { - let class_name = s!("Credential Dialog Xaml Host"); - let hwnd = unsafe { FindWindowA(class_name, None) }; - if let Ok(hwnd) = hwnd { - set_focus(hwnd); - } -} - -pub(crate) fn set_focus(window: HWND) { - unsafe { - let _ = SetForegroundWindow(window); - let _ = SetFocus(Some(window)); - } -} diff --git a/apps/desktop/desktop_native/proxy/src/main.rs b/apps/desktop/desktop_native/proxy/src/main.rs index 21957d8ba32..a2a0b834bca 100644 --- a/apps/desktop/desktop_native/proxy/src/main.rs +++ b/apps/desktop/desktop_native/proxy/src/main.rs @@ -8,9 +8,6 @@ use tracing_subscriber::{ fmt, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter, Layer as _, }; -#[cfg(target_os = "windows")] -mod windows; - #[cfg(target_os = "macos")] embed_plist::embed_info_plist!("../../../resources/info.desktop_proxy.plist"); @@ -64,9 +61,6 @@ fn init_logging(log_path: &Path, console_level: LevelFilter, file_level: LevelFi #[allow(clippy::unwrap_used)] #[tokio::main(flavor = "current_thread")] async fn main() { - #[cfg(target_os = "windows")] - let should_foreground = windows::allow_foreground(); - let sock_path = desktop_core::ipc::path("bw"); let log_path = { @@ -158,9 +152,6 @@ async fn main() { // Listen to stdin and send messages to ipc processor. msg = stdin.next() => { - #[cfg(target_os = "windows")] - should_foreground.store(true, std::sync::atomic::Ordering::Relaxed); - match msg { Some(Ok(msg)) => { let msg = String::from_utf8(msg.to_vec()).unwrap(); diff --git a/apps/desktop/desktop_native/proxy/src/windows.rs b/apps/desktop/desktop_native/proxy/src/windows.rs deleted file mode 100644 index cb0656fc7f8..00000000000 --- a/apps/desktop/desktop_native/proxy/src/windows.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; - -pub fn allow_foreground() -> Arc { - let should_foreground = Arc::new(AtomicBool::new(false)); - let should_foreground_clone = should_foreground.clone(); - let _ = std::thread::spawn(move || loop { - if !should_foreground_clone.load(Ordering::Relaxed) { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - should_foreground_clone.store(false, Ordering::Relaxed); - - for _ in 0..60 { - desktop_core::biometric::windows_focus::focus_security_prompt(); - std::thread::sleep(std::time::Duration::from_millis(1000)); - } - }); - - should_foreground -} diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index bf3c46a311f..8abd84ee39c 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -101,8 +101,7 @@ supportsBiometric && form.value.biometric && isWindows && - (userHasMasterPassword || (form.value.pin && userHasPinSet)) && - isWindowsV2BiometricsEnabled + (userHasMasterPassword || (form.value.pin && userHasPinSet)) " >
diff --git a/apps/desktop/src/app/accounts/settings.component.spec.ts b/apps/desktop/src/app/accounts/settings.component.spec.ts index 115f7436979..a424f230778 100644 --- a/apps/desktop/src/app/accounts/settings.component.spec.ts +++ b/apps/desktop/src/app/accounts/settings.component.spec.ts @@ -302,7 +302,6 @@ describe("SettingsComponent", () => { describe("windows desktop", () => { beforeEach(() => { platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop); - desktopBiometricsService.isWindowsV2BiometricsEnabled.mockResolvedValue(true); // Recreate component to apply the correct device fixture = TestBed.createComponent(SettingsComponent); @@ -449,7 +448,6 @@ describe("SettingsComponent", () => { desktopBiometricsService.hasPersistentKey.mockResolvedValue(enrolled); await component.ngOnInit(); - component.isWindowsV2BiometricsEnabled = true; component.isWindows = true; component.form.value.requireMasterPasswordOnAppRestart = true; component.userHasMasterPassword = false; @@ -558,7 +556,6 @@ describe("SettingsComponent", () => { desktopBiometricsService.hasPersistentKey.mockResolvedValue(false); await component.ngOnInit(); - component.isWindowsV2BiometricsEnabled = true; component.isWindows = true; component.form.value.requireMasterPasswordOnAppRestart = requireMasterPasswordOnAppRestart; @@ -659,6 +656,7 @@ describe("SettingsComponent", () => { describe("windows test cases", () => { beforeEach(() => { platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop); + keyService.userKey$ = jest.fn().mockReturnValue(of(mockUserKey)); component.isWindows = true; component.isLinux = false; @@ -683,8 +681,6 @@ describe("SettingsComponent", () => { describe("when windows v2 biometrics is enabled", () => { beforeEach(() => { - component.isWindowsV2BiometricsEnabled = true; - keyService.userKey$ = jest.fn().mockReturnValue(of(mockUserKey)); }); diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index c0798f1bdf0..68863312ffe 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -148,7 +148,6 @@ export class SettingsComponent implements OnInit, OnDestroy { userHasPinSet: boolean; pinEnabled$: Observable = of(true); - isWindowsV2BiometricsEnabled: boolean = false; consolidatedSessionTimeoutComponent$: Observable; @@ -297,8 +296,6 @@ export class SettingsComponent implements OnInit, OnDestroy { async ngOnInit() { this.vaultTimeoutOptions = await this.generateVaultTimeoutOptions(); - this.isWindowsV2BiometricsEnabled = await this.biometricsService.isWindowsV2BiometricsEnabled(); - const activeAccount = await firstValueFrom(this.accountService.activeAccount$); // Autotype is for Windows initially @@ -621,7 +618,6 @@ export class SettingsComponent implements OnInit, OnDestroy { // On Windows if a user turned off PIN without having a MP and has biometrics + require MP/PIN on restart enabled. if ( this.isWindows && - this.isWindowsV2BiometricsEnabled && this.supportsBiometric && this.form.value.requireMasterPasswordOnAppRestart && this.form.value.biometric && @@ -682,14 +678,12 @@ export class SettingsComponent implements OnInit, OnDestroy { this.form.controls.autoPromptBiometrics.setValue(false); await this.biometricStateService.setPromptAutomatically(false); - if (this.isWindowsV2BiometricsEnabled) { - // If the user doesn't have a MP or PIN then they have to use biometrics on app restart. - if (!this.userHasMasterPassword && !this.userHasPinSet) { - // Allow biometric unlock on app restart so the user doesn't get into a bad state. - await this.enrollPersistentBiometricIfNeeded(activeUserId); - } else { - this.form.controls.requireMasterPasswordOnAppRestart.setValue(true); - } + // If the user doesn't have a MP or PIN then they have to use biometrics on app restart. + if (!this.userHasMasterPassword && !this.userHasPinSet) { + // Allow biometric unlock on app restart so the user doesn't get into a bad state. + await this.enrollPersistentBiometricIfNeeded(activeUserId); + } else { + this.form.controls.requireMasterPasswordOnAppRestart.setValue(true); } } else if (this.isLinux) { // Similar to Windows 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 04a2f389781..59d9dcce7fe 100644 --- a/apps/desktop/src/key-management/biometrics/desktop.biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/desktop.biometrics.service.ts @@ -16,9 +16,6 @@ export abstract class DesktopBiometricsService extends BiometricsService { abstract enrollPersistent(userId: UserId, key: SymmetricCryptoKey): Promise; abstract hasPersistentKey(userId: UserId): Promise; /* Enables the v2 biometrics re-write. This will stay enabled until the application is restarted. */ - abstract enableWindowsV2Biometrics(): Promise; - abstract isWindowsV2BiometricsEnabled(): Promise; - /* Enables the v2 biometrics re-write. This will stay enabled until the application is restarted. */ abstract enableLinuxV2Biometrics(): Promise; abstract isLinuxV2BiometricsEnabled(): 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 db7c7c8f7fa..e30e4af3bac 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 @@ -58,10 +58,6 @@ export class MainBiometricsIPCListener { message.userId as UserId, SymmetricCryptoKey.fromString(message.key as string), ); - case BiometricAction.EnableWindowsV2: - return await this.biometricService.enableWindowsV2Biometrics(); - case BiometricAction.IsWindowsV2Enabled: - return await this.biometricService.isWindowsV2BiometricsEnabled(); case BiometricAction.EnableLinuxV2: return await this.biometricService.enableLinuxV2Biometrics(); case BiometricAction.IsLinuxV2Enabled: 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 be9e1f841e1..0cbe032aa76 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 @@ -20,7 +20,6 @@ import { MainBiometricsService } from "./main-biometrics.service"; import { WindowsBiometricsSystem } from "./native-v2"; import OsBiometricsServiceLinux from "./os-biometrics-linux.service"; import OsBiometricsServiceMac from "./os-biometrics-mac.service"; -import OsBiometricsServiceWindows from "./os-biometrics-windows.service"; import { OsBiometricService } from "./os-biometrics.service"; jest.mock("@bitwarden/desktop-napi", () => { @@ -61,7 +60,7 @@ describe("MainBiometricsService", function () { const internalService = (sut as any).osBiometricsService; expect(internalService).not.toBeNull(); - expect(internalService).toBeInstanceOf(OsBiometricsServiceWindows); + expect(internalService).toBeInstanceOf(WindowsBiometricsSystem); }); it("Should create a biometrics service specific for MacOs", () => { @@ -289,78 +288,6 @@ describe("MainBiometricsService", function () { }); }); - describe("enableWindowsV2Biometrics", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("enables Windows V2 biometrics when platform is win32 and not already enabled", async () => { - const sut = new MainBiometricsService( - i18nService, - windowMain, - logService, - "win32", - biometricStateService, - encryptService, - cryptoFunctionService, - ); - - await sut.enableWindowsV2Biometrics(); - - expect(logService.info).toHaveBeenCalledWith( - "[BiometricsMain] Loading native biometrics module v2 for windows", - ); - expect(await sut.isWindowsV2BiometricsEnabled()).toBe(true); - const internalService = (sut as any).osBiometricsService; - expect(internalService).not.toBeNull(); - expect(internalService).toBeInstanceOf(WindowsBiometricsSystem); - }); - - it("should not enable Windows V2 biometrics when platform is not win32", async () => { - const sut = new MainBiometricsService( - i18nService, - windowMain, - logService, - "darwin", - biometricStateService, - encryptService, - cryptoFunctionService, - ); - - await sut.enableWindowsV2Biometrics(); - - expect(logService.info).not.toHaveBeenCalled(); - expect(await sut.isWindowsV2BiometricsEnabled()).toBe(false); - }); - - it("should not enable Windows V2 biometrics when already enabled", async () => { - const sut = new MainBiometricsService( - i18nService, - windowMain, - logService, - "win32", - biometricStateService, - encryptService, - cryptoFunctionService, - ); - - // Enable it first - await sut.enableWindowsV2Biometrics(); - - // Enable it again - await sut.enableWindowsV2Biometrics(); - - expect(logService.info).toHaveBeenCalledWith( - "[BiometricsMain] Loading native biometrics module v2 for windows", - ); - expect(logService.info).toHaveBeenCalledTimes(1); - expect(await sut.isWindowsV2BiometricsEnabled()).toBe(true); - const internalService = (sut as any).osBiometricsService; - expect(internalService).not.toBeNull(); - expect(internalService).toBeInstanceOf(WindowsBiometricsSystem); - }); - }); - describe("pass through methods that call platform specific osBiometricsService methods", () => { const userId = newGuid() as UserId; let sut: MainBiometricsService; 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 da532828314..ad9ec62afcc 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts @@ -16,7 +16,6 @@ import { OsBiometricService } from "./os-biometrics.service"; export class MainBiometricsService extends DesktopBiometricsService { private osBiometricsService: OsBiometricService; private shouldAutoPrompt = true; - private windowsV2BiometricsEnabled = false; private linuxV2BiometricsEnabled = false; constructor( @@ -30,15 +29,10 @@ export class MainBiometricsService extends DesktopBiometricsService { ) { super(); if (platform === "win32") { - // eslint-disable-next-line - const OsBiometricsServiceWindows = require("./os-biometrics-windows.service").default; - this.osBiometricsService = new OsBiometricsServiceWindows( + this.osBiometricsService = new WindowsBiometricsSystem( this.i18nService, this.windowMain, this.logService, - this.biometricStateService, - this.encryptService, - this.cryptoFunctionService, ); } else if (platform === "darwin") { // eslint-disable-next-line @@ -156,22 +150,6 @@ export class MainBiometricsService extends DesktopBiometricsService { return await this.osBiometricsService.hasPersistentKey(userId); } - async enableWindowsV2Biometrics(): Promise { - if (this.platform === "win32" && !this.windowsV2BiometricsEnabled) { - this.logService.info("[BiometricsMain] Loading native biometrics module v2 for windows"); - this.osBiometricsService = new WindowsBiometricsSystem( - this.i18nService, - this.windowMain, - this.logService, - ); - this.windowsV2BiometricsEnabled = true; - } - } - - async isWindowsV2BiometricsEnabled(): Promise { - return this.windowsV2BiometricsEnabled; - } - async enableLinuxV2Biometrics(): Promise { if (this.platform === "linux" && !this.linuxV2BiometricsEnabled) { this.logService.info("[BiometricsMain] Loading native biometrics module v2 for linux"); diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.spec.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.spec.ts deleted file mode 100644 index f301efc70e7..00000000000 --- a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.spec.ts +++ /dev/null @@ -1,378 +0,0 @@ -import { randomBytes } from "node:crypto"; - -import { BrowserWindow } from "electron"; -import { mock } from "jest-mock-extended"; - -import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { UserId } from "@bitwarden/common/types/guid"; -import { biometrics, passwords } from "@bitwarden/desktop-napi"; -import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management"; - -import { WindowMain } from "../../main/window.main"; - -import OsBiometricsServiceWindows from "./os-biometrics-windows.service"; - -import OsDerivedKey = biometrics.OsDerivedKey; - -jest.mock("@bitwarden/desktop-napi", () => { - return { - biometrics: { - available: jest.fn().mockResolvedValue(true), - getBiometricSecret: jest.fn().mockResolvedValue(""), - setBiometricSecret: jest.fn().mockResolvedValue(""), - deleteBiometricSecret: jest.fn(), - deriveKeyMaterial: jest.fn().mockResolvedValue({ - keyB64: "", - ivB64: "", - }), - prompt: jest.fn().mockResolvedValue(true), - }, - passwords: { - getPassword: jest.fn().mockResolvedValue(null), - deletePassword: jest.fn().mockImplementation(() => {}), - isAvailable: jest.fn(), - PASSWORD_NOT_FOUND: "Password not found", - }, - }; -}); - -describe("OsBiometricsServiceWindows", function () { - const i18nService = mock(); - const windowMain = mock(); - const browserWindow = mock(); - const encryptionService: EncryptService = mock(); - const cryptoFunctionService: CryptoFunctionService = mock(); - const biometricStateService: BiometricStateService = mock(); - const logService = mock(); - - let service: OsBiometricsServiceWindows; - - const key = new SymmetricCryptoKey(new Uint8Array(64)); - const userId = "test-user-id" as UserId; - const serviceKey = "Bitwarden_biometric"; - const storageKey = `${userId}_user_biometric`; - - beforeEach(() => { - windowMain.win = browserWindow; - - service = new OsBiometricsServiceWindows( - i18nService, - windowMain, - logService, - biometricStateService, - encryptionService, - cryptoFunctionService, - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("getBiometricsFirstUnlockStatusForUser", () => { - const userId = "test-user-id" as UserId; - it("should return Available when client key half is set", async () => { - (service as any).clientKeyHalves = new Map(); - (service as any).clientKeyHalves.set(userId, new Uint8Array([1, 2, 3, 4])); - const result = await service.getBiometricsFirstUnlockStatusForUser(userId); - expect(result).toBe(BiometricsStatus.Available); - }); - it("should return UnlockNeeded when client key half is not set", async () => { - (service as any).clientKeyHalves = new Map(); - const result = await service.getBiometricsFirstUnlockStatusForUser(userId); - expect(result).toBe(BiometricsStatus.UnlockNeeded); - }); - }); - - describe("getOrCreateBiometricEncryptionClientKeyHalf", () => { - it("should return cached key half if already present", async () => { - const cachedKeyHalf = new Uint8Array([10, 20, 30]); - (service as any).clientKeyHalves.set(userId.toString(), cachedKeyHalf); - const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key); - expect(result).toBe(cachedKeyHalf); - }); - - it("should decrypt and return existing encrypted client key half", async () => { - biometricStateService.getEncryptedClientKeyHalf = jest - .fn() - .mockResolvedValue(new Uint8Array([1, 2, 3])); - const decrypted = new Uint8Array([4, 5, 6]); - encryptionService.decryptBytes = jest.fn().mockResolvedValue(decrypted); - - const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key); - - expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith(userId); - expect(encryptionService.decryptBytes).toHaveBeenCalledWith(new Uint8Array([1, 2, 3]), key); - expect(result).toEqual(decrypted); - expect((service as any).clientKeyHalves.get(userId.toString())).toEqual(decrypted); - }); - - it("should generate, encrypt, store, and cache a new key half if none exists", async () => { - biometricStateService.getEncryptedClientKeyHalf = jest.fn().mockResolvedValue(null); - const randomBytes = new Uint8Array([7, 8, 9]); - cryptoFunctionService.randomBytes = jest.fn().mockResolvedValue(randomBytes); - const encrypted = new Uint8Array([10, 11, 12]); - encryptionService.encryptBytes = jest.fn().mockResolvedValue(encrypted); - biometricStateService.setEncryptedClientKeyHalf = jest.fn().mockResolvedValue(undefined); - - const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key); - - expect(cryptoFunctionService.randomBytes).toHaveBeenCalledWith(32); - expect(encryptionService.encryptBytes).toHaveBeenCalledWith(randomBytes, key); - expect(biometricStateService.setEncryptedClientKeyHalf).toHaveBeenCalledWith( - encrypted, - userId, - ); - expect(result).toEqual(randomBytes); - expect((service as any).clientKeyHalves.get(userId.toString())).toEqual(randomBytes); - }); - }); - - describe("supportsBiometrics", () => { - it("should return true if biometrics are available", async () => { - biometrics.available = jest.fn().mockResolvedValue(true); - - const result = await service.supportsBiometrics(); - - expect(result).toBe(true); - }); - - it("should return false if biometrics are not available", async () => { - biometrics.available = jest.fn().mockResolvedValue(false); - - const result = await service.supportsBiometrics(); - - expect(result).toBe(false); - }); - }); - - describe("getBiometricKey", () => { - beforeEach(() => { - biometrics.prompt = jest.fn().mockResolvedValue(true); - }); - - it("should return null when unsuccessfully authenticated biometrics", async () => { - biometrics.prompt = jest.fn().mockResolvedValue(false); - - const result = await service.getBiometricKey(userId); - - expect(result).toBeNull(); - }); - - it.each([null, undefined, ""])( - "should throw error when no biometric key is found '%s'", - async (password) => { - passwords.getPassword = jest.fn().mockResolvedValue(password); - - await expect(service.getBiometricKey(userId)).rejects.toThrow( - "Biometric key not found for user", - ); - - expect(passwords.getPassword).toHaveBeenCalledWith(serviceKey, storageKey); - }, - ); - - it.each([[false], [true]])( - "should return the biometricKey and setBiometricSecret called if password is not encrypted and cached clientKeyHalves is %s", - async (haveClientKeyHalves) => { - const clientKeyHalveBytes = new Uint8Array([1, 2, 3]); - if (haveClientKeyHalves) { - service["clientKeyHalves"].set(userId, clientKeyHalveBytes); - } - const biometricKey = key.toBase64(); - passwords.getPassword = jest.fn().mockResolvedValue(biometricKey); - biometrics.deriveKeyMaterial = jest.fn().mockResolvedValue({ - keyB64: "testKeyB64", - ivB64: "testIvB64", - } satisfies OsDerivedKey); - - const result = await service.getBiometricKey(userId); - - expect(result.toBase64()).toBe(biometricKey); - expect(passwords.getPassword).toHaveBeenCalledWith(serviceKey, storageKey); - expect(biometrics.setBiometricSecret).toHaveBeenCalledWith( - serviceKey, - storageKey, - biometricKey, - { - osKeyPartB64: "testKeyB64", - clientKeyPartB64: haveClientKeyHalves - ? Utils.fromBufferToB64(clientKeyHalveBytes) - : undefined, - }, - "testIvB64", - ); - }, - ); - - it.each([[false], [true]])( - "should return the biometricKey if password is encrypted and cached clientKeyHalves is %s", - async (haveClientKeyHalves) => { - const clientKeyHalveBytes = new Uint8Array([1, 2, 3]); - if (haveClientKeyHalves) { - service["clientKeyHalves"].set(userId, clientKeyHalveBytes); - } - const biometricKey = key.toBase64(); - const biometricKeyEncrypted = "2.testId|data|mac"; - passwords.getPassword = jest.fn().mockResolvedValue(biometricKeyEncrypted); - biometrics.getBiometricSecret = jest.fn().mockResolvedValue(biometricKey); - biometrics.deriveKeyMaterial = jest.fn().mockResolvedValue({ - keyB64: "testKeyB64", - ivB64: "testIvB64", - } satisfies OsDerivedKey); - - const result = await service.getBiometricKey(userId); - - expect(result.toBase64()).toBe(biometricKey); - expect(passwords.getPassword).toHaveBeenCalledWith(serviceKey, storageKey); - expect(biometrics.setBiometricSecret).not.toHaveBeenCalled(); - expect(biometrics.getBiometricSecret).toHaveBeenCalledWith(serviceKey, storageKey, { - osKeyPartB64: "testKeyB64", - clientKeyPartB64: haveClientKeyHalves - ? Utils.fromBufferToB64(clientKeyHalveBytes) - : undefined, - }); - }, - ); - }); - - describe("deleteBiometricKey", () => { - const serviceName = "Bitwarden_biometric"; - const keyName = "test-user-id_user_biometric"; - - it("should delete biometric key successfully", async () => { - await service.deleteBiometricKey(userId); - - expect(passwords.deletePassword).toHaveBeenCalledWith(serviceName, keyName); - }); - - it.each([[false], [true]])("should not throw error if key found: %s", async (keyFound) => { - if (!keyFound) { - passwords.deletePassword = jest - .fn() - .mockRejectedValue(new Error(passwords.PASSWORD_NOT_FOUND)); - } - - await service.deleteBiometricKey(userId); - - expect(passwords.deletePassword).toHaveBeenCalledWith(serviceName, keyName); - if (!keyFound) { - expect(logService.debug).toHaveBeenCalledWith( - "[OsBiometricService] Biometric key %s not found for service %s.", - keyName, - serviceName, - ); - } - }); - - it("should throw error when deletePassword for key throws unexpected errors", async () => { - const error = new Error("Unexpected error"); - passwords.deletePassword = jest.fn().mockRejectedValue(error); - - await expect(service.deleteBiometricKey(userId)).rejects.toThrow(error); - - expect(passwords.deletePassword).toHaveBeenCalledWith(serviceName, keyName); - }); - }); - - describe("authenticateBiometric", () => { - const hwnd = randomBytes(32).buffer; - const consentMessage = "Test Windows Hello Consent Message"; - - beforeEach(() => { - windowMain.win.getNativeWindowHandle = jest.fn().mockReturnValue(hwnd); - i18nService.t.mockReturnValue(consentMessage); - }); - - it("should return true when biometric authentication is successful", async () => { - const result = await service.authenticateBiometric(); - - expect(result).toBe(true); - expect(biometrics.prompt).toHaveBeenCalledWith(hwnd, consentMessage); - }); - - it("should return false when biometric authentication fails", async () => { - biometrics.prompt = jest.fn().mockResolvedValue(false); - - const result = await service.authenticateBiometric(); - - expect(result).toBe(false); - expect(biometrics.prompt).toHaveBeenCalledWith(hwnd, consentMessage); - }); - }); - - describe("getStorageDetails", () => { - it.each([ - ["testClientKeyHalfB64", "testIvB64"], - [undefined, "testIvB64"], - ["testClientKeyHalfB64", null], - [undefined, null], - ])( - "should derive key material and ivB64 and return it when os key half not saved yet", - async (clientKeyHalfB64, ivB64) => { - service["setIv"](ivB64); - - const derivedKeyMaterial = { - keyB64: "derivedKeyB64", - ivB64: "derivedIvB64", - }; - biometrics.deriveKeyMaterial = jest.fn().mockResolvedValue(derivedKeyMaterial); - - const result = await service["getStorageDetails"]({ clientKeyHalfB64 }); - - expect(result).toEqual({ - key_material: { - osKeyPartB64: derivedKeyMaterial.keyB64, - clientKeyPartB64: clientKeyHalfB64, - }, - ivB64: derivedKeyMaterial.ivB64, - }); - expect(biometrics.deriveKeyMaterial).toHaveBeenCalledWith(ivB64); - expect(service["_osKeyHalf"]).toEqual(derivedKeyMaterial.keyB64); - expect(service["_iv"]).toEqual(derivedKeyMaterial.ivB64); - }, - ); - - it("should throw an error when deriving key material and returned iv is null", async () => { - service["setIv"]("testIvB64"); - - const derivedKeyMaterial = { - keyB64: "derivedKeyB64", - ivB64: null as string | undefined | null, - }; - biometrics.deriveKeyMaterial = jest.fn().mockResolvedValue(derivedKeyMaterial); - - await expect( - service["getStorageDetails"]({ clientKeyHalfB64: "testClientKeyHalfB64" }), - ).rejects.toThrow("Initialization Vector is null"); - - expect(biometrics.deriveKeyMaterial).toHaveBeenCalledWith("testIvB64"); - }); - }); - - describe("setIv", () => { - it("should set the iv and reset the osKeyHalf", () => { - const iv = "testIv"; - service["_osKeyHalf"] = "testOsKeyHalf"; - - service["setIv"](iv); - - expect(service["_iv"]).toBe(iv); - expect(service["_osKeyHalf"]).toBeNull(); - }); - - it("should set the iv to null when iv is undefined and reset the osKeyHalf", () => { - service["_osKeyHalf"] = "testOsKeyHalf"; - - service["setIv"](undefined); - - expect(service["_iv"]).toBeNull(); - expect(service["_osKeyHalf"]).toBeNull(); - }); - }); -}); diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts deleted file mode 100644 index a32d4678427..00000000000 --- a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { UserId } from "@bitwarden/common/types/guid"; -import { biometrics, passwords } from "@bitwarden/desktop-napi"; -import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management"; - -import { WindowMain } from "../../main/window.main"; - -import { OsBiometricService } from "./os-biometrics.service"; - -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 enrollPersistent(userId: UserId, key: SymmetricCryptoKey): Promise {} - - async hasPersistentKey(userId: UserId): Promise { - return false; - } - - async supportsBiometrics(): Promise { - return await biometrics.available(); - } - - async getBiometricKey(userId: UserId): Promise { - const success = await this.authenticateBiometric(); - if (!success) { - return null; - } - - const value = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId)); - if (value == null || value == "") { - throw new Error("Biometric key not found for user"); - } - - let clientKeyHalfB64: string | null = null; - if (this.clientKeyHalves.has(userId)) { - clientKeyHalfB64 = Utils.fromBufferToB64(this.clientKeyHalves.get(userId)!); - } - - if (!EncString.isSerializedEncString(value)) { - // Update to format encrypted with client key half - const storageDetails = await this.getStorageDetails({ - clientKeyHalfB64: clientKeyHalfB64 ?? undefined, - }); - - await biometrics.setBiometricSecret( - SERVICE, - getLookupKeyForUser(userId), - value, - storageDetails.key_material, - storageDetails.ivB64, - ); - return SymmetricCryptoKey.fromString(value); - } else { - const encValue = new EncString(value); - this.setIv(encValue.iv); - const storageDetails = await this.getStorageDetails({ - clientKeyHalfB64: clientKeyHalfB64 ?? undefined, - }); - return SymmetricCryptoKey.fromString( - await biometrics.getBiometricSecret( - SERVICE, - getLookupKeyForUser(userId), - storageDetails.key_material, - ), - ); - } - } - - async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise { - const clientKeyHalf = await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key); - - const storageDetails = await this.getStorageDetails({ - clientKeyHalfB64: Utils.fromBufferToB64(clientKeyHalf), - }); - await biometrics.setBiometricSecret( - SERVICE, - getLookupKeyForUser(userId), - key.toBase64(), - storageDetails.key_material, - storageDetails.ivB64, - ); - } - - async deleteBiometricKey(userId: UserId): Promise { - try { - await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId)); - } catch (e) { - if (e instanceof Error && e.message === passwords.PASSWORD_NOT_FOUND) { - this.logService.debug( - "[OsBiometricService] Biometric key %s not found for service %s.", - getLookupKeyForUser(userId), - SERVICE, - ); - } else { - throw e; - } - } - } - - /** - * Prompts Windows Hello - */ - async authenticateBiometric(): Promise { - const hwnd = this.windowMain.win.getNativeWindowHandle(); - return await biometrics.prompt(hwnd, this.i18nService.t("windowsHelloConsentMessage")); - } - - private async getStorageDetails({ - clientKeyHalfB64, - }: { - clientKeyHalfB64: string | undefined; - }): Promise<{ key_material: biometrics.KeyMaterial; ivB64: string }> { - if (this._osKeyHalf == null) { - const keyMaterial = await biometrics.deriveKeyMaterial(this._iv); - this._osKeyHalf = keyMaterial.keyB64; - this._iv = keyMaterial.ivB64; - } - - if (this._iv == null) { - throw new Error("Initialization Vector is null"); - } - - const result = { - key_material: { - osKeyPartB64: this._osKeyHalf, - clientKeyPartB64: clientKeyHalfB64, - }, - ivB64: this._iv, - }; - - // napi-rs fails to convert null values - if (result.key_material.clientKeyPartB64 == null) { - delete result.key_material.clientKeyPartB64; - } - return result; - } - - // Nulls out key material in order to force a re-derive. This should only be used in getBiometricKey - // when we want to force a re-derive of the key material. - private setIv(iv?: string) { - this._iv = iv ?? null; - this._osKeyHalf = null; - } - - async needsSetup() { - return false; - } - - async canAutoSetup(): Promise { - return false; - } - - async runSetup(): Promise {} - - async getOrCreateBiometricEncryptionClientKeyHalf( - userId: UserId, - key: SymmetricCryptoKey, - ): Promise { - 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 - clientKeyHalf = await this.cryptoFunctionService.randomBytes(32); - const encKey = await this.encryptService.encryptBytes(clientKeyHalf, key); - await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId); - } - - this.clientKeyHalves.set(userId, clientKeyHalf); - - return clientKeyHalf; - } - - async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise { - if (this.clientKeyHalves.has(userId)) { - return BiometricsStatus.Available; - } else { - return BiometricsStatus.UnlockNeeded; - } - } -} 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 63d2225b7c6..8e28d3ca614 100644 --- a/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts @@ -77,14 +77,6 @@ export class RendererBiometricsService extends DesktopBiometricsService { return await ipc.keyManagement.biometric.hasPersistentKey(userId); } - async enableWindowsV2Biometrics(): Promise { - return await ipc.keyManagement.biometric.enableWindowsV2Biometrics(); - } - - async isWindowsV2BiometricsEnabled(): Promise { - return await ipc.keyManagement.biometric.isWindowsV2BiometricsEnabled(); - } - async enableLinuxV2Biometrics(): Promise { return await ipc.keyManagement.biometric.enableLinuxV2Biometrics(); } diff --git a/apps/desktop/src/key-management/preload.ts b/apps/desktop/src/key-management/preload.ts index d317b1f6ce0..844a81ff8e3 100644 --- a/apps/desktop/src/key-management/preload.ts +++ b/apps/desktop/src/key-management/preload.ts @@ -61,14 +61,6 @@ const biometric = { action: BiometricAction.HasPersistentKey, userId: userId, } satisfies BiometricMessage), - enableWindowsV2Biometrics: (): Promise => - ipcRenderer.invoke("biometric", { - action: BiometricAction.EnableWindowsV2, - } satisfies BiometricMessage), - isWindowsV2BiometricsEnabled: (): Promise => - ipcRenderer.invoke("biometric", { - action: BiometricAction.IsWindowsV2Enabled, - } satisfies BiometricMessage), enableLinuxV2Biometrics: (): Promise => ipcRenderer.invoke("biometric", { action: BiometricAction.EnableLinuxV2, diff --git a/apps/desktop/src/services/biometric-message-handler.service.spec.ts b/apps/desktop/src/services/biometric-message-handler.service.spec.ts index dec1e63d5e8..49d346bfa3a 100644 --- a/apps/desktop/src/services/biometric-message-handler.service.spec.ts +++ b/apps/desktop/src/services/biometric-message-handler.service.spec.ts @@ -503,19 +503,4 @@ describe("BiometricMessageHandlerService", () => { }, ); }); - - describe("init", () => { - it("enables Windows v2 biometrics when feature flag enabled", async () => { - configService.getFeatureFlag.mockReturnValue(true); - - await service.init(); - expect(biometricsService.enableWindowsV2Biometrics).toHaveBeenCalled(); - }); - it("does not enable Windows v2 biometrics when feature flag disabled", async () => { - configService.getFeatureFlag.mockReturnValue(false); - - await service.init(); - expect(biometricsService.enableWindowsV2Biometrics).not.toHaveBeenCalled(); - }); - }); }); diff --git a/apps/desktop/src/services/biometric-message-handler.service.ts b/apps/desktop/src/services/biometric-message-handler.service.ts index 022ccffcc91..ca4ea14c1e0 100644 --- a/apps/desktop/src/services/biometric-message-handler.service.ts +++ b/apps/desktop/src/services/biometric-message-handler.service.ts @@ -119,13 +119,6 @@ export class BiometricMessageHandlerService { "[BiometricMessageHandlerService] Initializing biometric message handler", ); - const windowsV2Enabled = await this.configService.getFeatureFlag( - FeatureFlag.WindowsBiometricsV2, - ); - if (windowsV2Enabled) { - await this.biometricsService.enableWindowsV2Biometrics(); - } - const linuxV2Enabled = await this.configService.getFeatureFlag(FeatureFlag.LinuxBiometricsV2); if (linuxV2Enabled) { await this.biometricsService.enableLinuxV2Biometrics(); diff --git a/apps/desktop/src/types/biometric-message.ts b/apps/desktop/src/types/biometric-message.ts index 62aa9fb1ce2..7ae20945e96 100644 --- a/apps/desktop/src/types/biometric-message.ts +++ b/apps/desktop/src/types/biometric-message.ts @@ -17,9 +17,6 @@ export enum BiometricAction { EnrollPersistent = "enrollPersistent", HasPersistentKey = "hasPersistentKey", - EnableWindowsV2 = "enableWindowsV2", - IsWindowsV2Enabled = "isWindowsV2Enabled", - EnableLinuxV2 = "enableLinuxV2", IsLinuxV2Enabled = "isLinuxV2Enabled", } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index cf60dca5d24..be2ea75203c 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -40,7 +40,6 @@ export enum FeatureFlag { EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation", ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings", PM25174_DisableType0Decryption = "pm-25174-disable-type-0-decryption", - WindowsBiometricsV2 = "pm-25373-windows-biometrics-v2", LinuxBiometricsV2 = "pm-26340-linux-biometrics-v2", UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data", NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change", @@ -140,7 +139,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.EnrollAeadOnKeyRotation]: FALSE, [FeatureFlag.ForceUpdateKDFSettings]: FALSE, [FeatureFlag.PM25174_DisableType0Decryption]: FALSE, - [FeatureFlag.WindowsBiometricsV2]: FALSE, [FeatureFlag.LinuxBiometricsV2]: FALSE, [FeatureFlag.UnlockWithMasterPasswordUnlockData]: FALSE, [FeatureFlag.NoLogoutOnKdfChange]: FALSE,