From 23eef2d250bb00192f25ce5e4a02f4c9706a468d Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 28 Aug 2025 13:25:42 +0200 Subject: [PATCH] Remove old biometrics --- .../src/{biometric_v2 => biometric}/linux.rs | 2 +- .../core/src/biometric/macos.rs | 38 -- .../desktop_native/core/src/biometric/mod.rs | 193 ++------ .../unimplemented.rs | 0 .../desktop_native/core/src/biometric/unix.rs | 109 ----- .../core/src/biometric/windows.rs | 429 ++++++++++-------- .../core/src/biometric/windows_focus.rs | 57 ++- .../core/src/biometric_v2/mod.rs | 42 -- .../core/src/biometric_v2/windows.rs | 270 ----------- .../core/src/biometric_v2/windows_focus.rs | 71 --- .../core/src/crypto/cipher_string.rs | 212 --------- .../desktop_native/core/src/crypto/crypto.rs | 35 -- .../desktop_native/core/src/crypto/mod.rs | 6 - apps/desktop/desktop_native/core/src/lib.rs | 2 - apps/desktop/desktop_native/napi/index.d.ts | 30 +- apps/desktop/desktop_native/napi/src/lib.rs | 108 +---- 16 files changed, 316 insertions(+), 1288 deletions(-) rename apps/desktop/desktop_native/core/src/{biometric_v2 => biometric}/linux.rs (98%) delete mode 100644 apps/desktop/desktop_native/core/src/biometric/macos.rs rename apps/desktop/desktop_native/core/src/{biometric_v2 => biometric}/unimplemented.rs (100%) delete mode 100644 apps/desktop/desktop_native/core/src/biometric/unix.rs delete mode 100644 apps/desktop/desktop_native/core/src/biometric_v2/mod.rs delete mode 100644 apps/desktop/desktop_native/core/src/biometric_v2/windows.rs delete mode 100644 apps/desktop/desktop_native/core/src/biometric_v2/windows_focus.rs delete mode 100644 apps/desktop/desktop_native/core/src/crypto/cipher_string.rs delete mode 100644 apps/desktop/desktop_native/core/src/crypto/crypto.rs delete mode 100644 apps/desktop/desktop_native/core/src/crypto/mod.rs diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs b/apps/desktop/desktop_native/core/src/biometric/linux.rs similarity index 98% rename from apps/desktop/desktop_native/core/src/biometric_v2/linux.rs rename to apps/desktop/desktop_native/core/src/biometric/linux.rs index 23189172d36..d8ea4a8659f 100644 --- a/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs +++ b/apps/desktop/desktop_native/core/src/biometric/linux.rs @@ -34,7 +34,7 @@ impl BiometricLockSystem { } } -impl super::BiometricV2Trait for BiometricLockSystem { +impl super::BiometricTrait for BiometricLockSystem { async fn authenticate(&self, _hwnd: Vec, _message: String) -> Result { let connection = Connection::system().await?; let proxy = AuthorityProxy::new(&connection).await?; diff --git a/apps/desktop/desktop_native/core/src/biometric/macos.rs b/apps/desktop/desktop_native/core/src/biometric/macos.rs deleted file mode 100644 index ec09d566e1f..00000000000 --- a/apps/desktop/desktop_native/core/src/biometric/macos.rs +++ /dev/null @@ -1,38 +0,0 @@ -use anyhow::{bail, Result}; - -use crate::biometric::{KeyMaterial, OsDerivedKey}; - -/// The MacOS implementation of the biometric trait. -pub struct Biometric {} - -impl super::BiometricTrait for Biometric { - async fn prompt(_hwnd: Vec, _message: String) -> Result { - bail!("platform not supported"); - } - - async fn available() -> Result { - bail!("platform not supported"); - } - - fn derive_key_material(_iv_str: Option<&str>) -> Result { - bail!("platform not supported"); - } - - async fn get_biometric_secret( - _service: &str, - _account: &str, - _key_material: Option, - ) -> Result { - bail!("platform not supported"); - } - - async fn set_biometric_secret( - _service: &str, - _account: &str, - _secret: &str, - _key_material: Option, - _iv_b64: &str, - ) -> Result { - bail!("platform not supported"); - } -} diff --git a/apps/desktop/desktop_native/core/src/biometric/mod.rs b/apps/desktop/desktop_native/core/src/biometric/mod.rs index e4d51f5da9a..fd57d979a74 100644 --- a/apps/desktop/desktop_native/core/src/biometric/mod.rs +++ b/apps/desktop/desktop_native/core/src/biometric/mod.rs @@ -1,175 +1,42 @@ -use aes::cipher::generic_array::GenericArray; -use anyhow::{anyhow, Result}; +use 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 = "linux", path = "linux.rs")] +#[cfg_attr(target_os = "macos", path = "unimplemented.rs")] #[cfg_attr(target_os = "windows", path = "windows.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}; -use sha2::{Digest, Sha256}; - -use crate::crypto::{self, CipherString}; - -pub struct KeyMaterial { - pub os_key_part_b64: String, - pub client_key_part_b64: Option, -} - -pub struct OsDerivedKey { - pub key_b64: String, - pub iv_b64: String, -} +pub use biometric::BiometricLockSystem; #[allow(async_fn_in_trait)] pub trait BiometricTrait { - async fn prompt(hwnd: Vec, message: String) -> Result; - async fn available() -> Result; - fn derive_key_material(secret: Option<&str>) -> Result; - async fn set_biometric_secret( - service: &str, - account: &str, - secret: &str, - key_material: Option, - iv_b64: &str, - ) -> Result; - async fn get_biometric_secret( - service: &str, - account: &str, - key_material: Option, - ) -> Result; -} - -#[allow(unused)] -fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result { - let iv = base64_engine - .decode(iv_b64)? - .try_into() - .map_err(|e: Vec<_>| anyhow!("Expected length {}, got {}", 16, e.len()))?; - - let encrypted = crypto::encrypt_aes256(secret.as_bytes(), iv, key_material.derive_key()?)?; - - Ok(encrypted.to_string()) -} - -#[allow(unused)] -fn decrypt(secret: &CipherString, key_material: &KeyMaterial) -> Result { - if let CipherString::AesCbc256_B64 { iv, data } = secret { - let decrypted = crypto::decrypt_aes256(iv, data, key_material.derive_key()?)?; - - Ok(String::from_utf8(decrypted)?) - } else { - Err(anyhow!("Invalid cipher string")) - } -} - -impl KeyMaterial { - fn digest_material(&self) -> String { - match self.client_key_part_b64.as_deref() { - Some(client_key_part_b64) => { - format!("{}|{}", self.os_key_part_b64, client_key_part_b64) - } - None => self.os_key_part_b64.clone(), - } - } - - pub fn derive_key(&self) -> Result> { - Ok(Sha256::digest(self.digest_material())) - } -} - -#[cfg(test)] -mod tests { - use crate::biometric::{decrypt, encrypt, KeyMaterial}; - use crate::crypto::CipherString; - use base64::{engine::general_purpose::STANDARD as base64_engine, Engine}; - use std::str::FromStr; - - fn key_material() -> KeyMaterial { - KeyMaterial { - os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(), - client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()), - } - } - - #[test] - fn test_encrypt() { - let key_material = key_material(); - let iv_b64 = "l9fhDUP/wDJcKwmEzcb/3w==".to_owned(); - let secret = encrypt("secret", &key_material, &iv_b64) - .unwrap() - .parse::() - .unwrap(); - - match secret { - CipherString::AesCbc256_B64 { iv, data: _ } => { - assert_eq!(iv_b64, base64_engine.encode(iv)); - } - _ => panic!("Invalid cipher string"), - } - } - - #[test] - fn test_decrypt() { - let secret = - CipherString::from_str("0.l9fhDUP/wDJcKwmEzcb/3w==|uP4LcqoCCj5FxBDP77NV6Q==").unwrap(); // output from test_encrypt - let key_material = key_material(); - assert_eq!(decrypt(&secret, &key_material).unwrap(), "secret") - } - - #[test] - fn key_material_produces_valid_key() { - let result = key_material().derive_key().unwrap(); - assert_eq!(result.len(), 32); - } - - #[test] - fn key_material_uses_os_part() { - let mut key_material = key_material(); - let result = key_material.derive_key().unwrap(); - key_material.os_key_part_b64 = "BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(); - let result2 = key_material.derive_key().unwrap(); - assert_ne!(result, result2); - } - - #[test] - fn key_material_uses_client_part() { - let mut key_material = key_material(); - let result = key_material.derive_key().unwrap(); - key_material.client_key_part_b64 = - Some("BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()); - let result2 = key_material.derive_key().unwrap(); - assert_ne!(result, result2); - } - - #[test] - fn key_material_produces_consistent_os_only_key() { - let mut key_material = key_material(); - key_material.client_key_part_b64 = None; - let result = key_material.derive_key().unwrap(); - assert_eq!( - result, - [ - 81, 100, 62, 172, 151, 119, 182, 58, 123, 38, 129, 116, 209, 253, 66, 118, 218, - 237, 236, 155, 201, 234, 11, 198, 229, 171, 246, 144, 71, 188, 84, 246 - ] - .into() - ); - } - - #[test] - fn key_material_produces_unique_os_only_key() { - let mut key_material = key_material(); - key_material.client_key_part_b64 = None; - let result = key_material.derive_key().unwrap(); - key_material.os_key_part_b64 = "BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(); - let result2 = key_material.derive_key().unwrap(); - assert_ne!(result, result2); - } + /// Authenticate the user + async fn authenticate(&self, hwnd: Vec, message: String) -> Result; + /// Check if biometric authentication is available + async fn authenticate_available(&self) -> Result; + /// Enroll a key for persistent unlock + async fn enroll_persistent(&self, user_id: &str, key: &[u8]) -> Result<()>; + /// Clear the persistent and ephemeral keys + async fn unenroll(&self, user_id: &str) -> Result<()>; + async fn has_persistent(&self, user_id: &str) -> Result; + /// On every unlock, the client provides a key to be held for subsequent biometric unlock + async fn provide_key( + &self, + user_id: &str, + key: &[u8] + ); + /// Perform biometric unlock and return the key + async fn unlock( + &self, + user_id: &str, + hwnd: Vec, + ) -> Result>; + /// Check if biometric unlock is available based on whether a key is present and whether authentication is possible + async fn unlock_available( + &self, + user_id: &str, + ) -> Result; } diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/unimplemented.rs b/apps/desktop/desktop_native/core/src/biometric/unimplemented.rs similarity index 100% rename from apps/desktop/desktop_native/core/src/biometric_v2/unimplemented.rs rename to apps/desktop/desktop_native/core/src/biometric/unimplemented.rs diff --git a/apps/desktop/desktop_native/core/src/biometric/unix.rs b/apps/desktop/desktop_native/core/src/biometric/unix.rs deleted file mode 100644 index 60392adc9d7..00000000000 --- a/apps/desktop/desktop_native/core/src/biometric/unix.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::str::FromStr; - -use anyhow::Result; -use base64::Engine; -use rand::RngCore; -use sha2::{Digest, Sha256}; - -use crate::biometric::{base64_engine, KeyMaterial, OsDerivedKey}; -use zbus::Connection; -use zbus_polkit::policykit1::*; - -use super::{decrypt, encrypt}; -use crate::crypto::CipherString; -use anyhow::anyhow; - -/// The Unix implementation of the biometric trait. -pub struct Biometric {} - -impl super::BiometricTrait for Biometric { - async fn prompt(_hwnd: Vec, _message: String) -> Result { - let connection = Connection::system().await?; - let proxy = AuthorityProxy::new(&connection).await?; - let subject = Subject::new_for_owner(std::process::id(), None, None)?; - let details = std::collections::HashMap::new(); - let result = proxy - .check_authorization( - &subject, - "com.bitwarden.Bitwarden.unlock", - &details, - CheckAuthorizationFlags::AllowUserInteraction.into(), - "", - ) - .await; - - match result { - Ok(result) => Ok(result.is_authorized), - Err(e) => { - println!("polkit biometric error: {:?}", e); - Ok(false) - } - } - } - - async fn available() -> Result { - let connection = Connection::system().await?; - let proxy = AuthorityProxy::new(&connection).await?; - let res = proxy.enumerate_actions("en").await?; - for action in res { - if action.action_id == "com.bitwarden.Bitwarden.unlock" { - return 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(), - }; - - // there is no windows hello like interactive bio protected secret at the moment on linux - // so we use a 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 polkit 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 polkit protected keys" - ))?; - - let encrypted_secret = crate::password::get_password(service, account).await?; - let secret = CipherString::from_str(&encrypted_secret)?; - decrypt(&secret, &key_material) - } -} - -fn random_challenge() -> [u8; 16] { - let mut challenge = [0u8; 16]; - rand::rng().fill_bytes(&mut challenge); - challenge -} diff --git a/apps/desktop/desktop_native/core/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs index 99bec132edb..3cf01b0341a 100644 --- a/apps/desktop/desktop_native/core/src/biometric/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric/windows.rs @@ -1,14 +1,38 @@ -use std::{ffi::c_void, str::FromStr}; +//! This file implements Windows-Hello based biometric unlock. +//! +//! # Security +//! Note: There are two scenarios to consider, with different security implications. This section +//! describes the assumed security model and security guarantees achieved. In the required security +//! guarantee is that a locked vault - a running app - cannot be unlocked when the device (user-space) +//! is compromised in this state. +//! +//! 1. Require master password on app restart +//! In this scenario, when first unlocking the app, the app sends the user-key to this module, which holds it in secure memory, +//! protected by DPAPI. This makes it inaccessible to other processes, unless they compromise the system administrator, or kernel. +//! While the app is running this key is held in memory, even if locked. When unlocking, the app will prompt the user via +//! `windows_hello_authenticate` to get a yes/no decision on whether to release the key to the app. +//! +//! 2. Do not require master password on app restart +//! In this scenario, when enrolling, the app sends the user-key to this module, which derives the windows hello key +//! with the Windows Hello prompt. This is done by signing a per-user challenge, which produces a deterministic +//! signature which is hashed to obtain a key. This key is used to encrypt and persist the vault unlock key (user key). +//! +//! Since the keychain can be accessed by all user-space processes, the challenge is known to all userspace processes. +//! Therefore, to circumvent the security measure, the attacker would need to create a fake Windows-Hello prompt, and +//! get the user to confirm it. +use std::{ffi::c_void, sync::{atomic::AtomicBool, Arc}}; + +use aes::cipher::KeyInit; use anyhow::{anyhow, Result}; -use base64::{engine::general_purpose::STANDARD as base64_engine, Engine}; -use rand::RngCore; +use chacha20poly1305::{aead::Aead, XChaCha20Poly1305, XNonce}; use sha2::{Digest, Sha256}; +use tokio::sync::Mutex; use windows::{ - core::{factory, HSTRING}, - Security::Credentials::UI::{ + core::{factory, h, HSTRING}, + Security::{Credentials::{KeyCredentialCreationOption, KeyCredentialManager, KeyCredentialStatus, UI::{ UserConsentVerificationResult, UserConsentVerifier, UserConsentVerifierAvailability, - }, + }}, Cryptography::CryptographicBuffer}, Win32::{ Foundation::HWND, System::WinRT::IUserConsentVerifierInterop, UI::WindowsAndMessaging::GetForegroundWindow, @@ -16,224 +40,231 @@ use windows::{ }; use windows_future::IAsyncOperation; +use super::windows_focus::{focus_security_prompt, set_focus}; use crate::{ - biometric::{KeyMaterial, OsDerivedKey}, - crypto::CipherString, + password, secure_memory::* }; -use super::{decrypt, encrypt, windows_focus::set_focus}; +const KEYCHAIN_SERVICE_NAME: &str = "BitwardenBiometricsV2"; + +#[derive(serde::Serialize, serde::Deserialize)] +struct WindowsHelloKeychainEntry { + nonce: [u8; 24], + challenge: [u8; 16], + wrapped_key: Vec, +} /// The Windows OS implementation of the biometric trait. -pub struct Biometric {} +pub struct BiometricLockSystem { + // The userkeys that are held in memory MUST be protected from memory dumping attacks, to ensure + // locked vaults cannot be unlocked + secure_memory: Arc> +} -impl super::BiometricTrait for Biometric { - 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), +impl BiometricLockSystem { + pub fn new() -> Self { + Self { + secure_memory: Arc::new(Mutex::new(crate::secure_memory::dpapi::DpapiSecretKVStore::new())), } } +} - async fn available() -> Result { - let ucv_available = UserConsentVerifier::CheckAvailabilityAsync()?.get()?; +impl super::BiometricTrait for BiometricLockSystem { + async fn authenticate(&self, hwnd: Vec, message: String) -> Result { + windows_hello_authenticate(hwnd, message) + } - match ucv_available { + async fn authenticate_available(&self) -> Result { + match UserConsentVerifier::CheckAvailabilityAsync()?.get()? { UserConsentVerifierAvailability::Available => Ok(true), - UserConsentVerifierAvailability::DeviceBusy => 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(), + async fn unenroll(&self, user_id: &str) -> Result<()> { + let mut secure_memory = self.secure_memory.lock().await; + secure_memory.remove(user_id); + delete_keychain_entry(user_id).await?; + Ok(()) + } + + async fn enroll_persistent(&self, user_id: &str, key: &[u8]) -> Result<()> { + // Enrollment works by first generating a random challenge unique to the user / enrollment. Then, + // with the challenge and a Windows-Hello prompt, the "windows hello key" is derived. The windows + // hello key is used to encrypt the key to store with XChaCha20Poly1305. The bundle of nonce, + // challenge and wrapped-key are stored to the keychain + + // Each enrollment (per user) has a unique challenge, so that the windows-hello key is unique + let mut challenge = [0u8; 16]; + rand::fill(&mut challenge); + + // This key is unique to the challenge + let windows_hello_key = windows_hello_authenticate_with_crypto(&challenge)?; + + let nonce = { + let mut nonce_bytes = [0u8; 24]; + rand::fill(&mut nonce_bytes); + XNonce::clone_from_slice(&nonce_bytes) }; - // 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 }) + let wrapped_key = XChaCha20Poly1305::new(&windows_hello_key.into()).encrypt(&nonce, key).map_err(|e| anyhow!(e))?; + set_keychain_entry(user_id, &WindowsHelloKeychainEntry { + nonce: nonce.as_slice().try_into().map_err(|_| anyhow!("Invalid nonce length"))?, + challenge, + wrapped_key, + }).await?; + Ok(()) } - 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 provide_key(&self, user_id: &str, key: &[u8]) { + let mut secure_memory = self.secure_memory.lock().await; + secure_memory.put(user_id.to_string(), key); } - 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" - ))?; + async fn unlock(&self, user_id: &str, hwnd: Vec) -> Result> { + let mut secure_memory = self.secure_memory.lock().await; + if secure_memory.has(user_id) { + println!("[Windows Hello] Key is in secure memory, using UV API"); + + if self.authenticate(hwnd, "Unlock your vault".to_owned()).await? { + println!("[Windows Hello] Authentication successful"); + return secure_memory.get(user_id).clone().ok_or_else(|| anyhow!("No key found for user")); + } + Err(anyhow!("Authentication failed")) + } else { + println!("[Windows Hello] Key not in secure memory, using Signing API"); - 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) - } + let keychain_entry = get_keychain_entry(user_id).await?; + let windows_hello_key = windows_hello_authenticate_with_crypto(&keychain_entry.challenge)?; + let decrypted_key = XChaCha20Poly1305::new(&windows_hello_key.into()).decrypt(keychain_entry.nonce.as_slice().try_into().map_err(|_| anyhow!("Invalid nonce length"))?, keychain_entry.wrapped_key.as_slice()).map_err(|e| anyhow!(e))?; + secure_memory.put(user_id.to_string(), &decrypted_key.clone()); + Ok(decrypted_key) } } -} -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) + async fn unlock_available(&self, user_id: &str) -> Result { + let secure_memory = self.secure_memory.lock().await; + let has_key = secure_memory.has(user_id) || has_keychain_entry(user_id).await.unwrap_or(false); + Ok(has_key && self.authenticate_available().await.unwrap_or(false)) } - - #[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" - ); + + async fn has_persistent(&self, user_id: &str) -> Result { + Ok(get_keychain_entry(user_id).await.is_ok()) } } + +/// Get a yes/no authorization without any cryptographic backing. +/// This API has better focusing behavior +fn windows_hello_authenticate(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), + } +} + +/// Derive the symmetric encryption key from the Windows Hello signature. +/// +/// This works by signing a static challenge string with Windows Hello protected key store. The +/// signed challenge is then hashed using SHA-256 and used as the symmetric encryption key for the +/// Windows Hello protected keys. +/// +/// Windows will only sign the challenge if the user has successfully authenticated with Windows, +/// ensuring user presence. +/// +/// Note: This API has inconsistent focusing behavior when called from another window +fn windows_hello_authenticate_with_crypto(challenge: &[u8; 16]) -> Result<[u8; 32]> { + // Ugly hack: We need to focus the window via window focusing APIs until Microsoft releases a new API. + // This is unreliable, and if it does not work, the operation may fail + let stop_focusing = Arc::new(AtomicBool::new(false)); + let stop_focusing_clone = stop_focusing.clone(); + let _ = std::thread::spawn(move || loop { + if !stop_focusing_clone.load(std::sync::atomic::Ordering::Relaxed) { + focus_security_prompt(); + std::thread::sleep(std::time::Duration::from_millis(500)); + } else { + break; + } + }); + // Only stop focusing once this function exists. The focus MUST run both during the initial creation + // with RequestCreateAsync, and also with the subsequent use with RequestSignAsync. + let _guard = scopeguard::guard((), |_| { + stop_focusing.store(true, std::sync::atomic::Ordering::Relaxed); + }); + + // First create or replace the Bitwarden signing key + let result = KeyCredentialManager::RequestCreateAsync( + h!("BitwardenBiometricsV2"), + KeyCredentialCreationOption::FailIfExists, + )? + .get()?; + let result = match result.Status()? { + KeyCredentialStatus::CredentialAlreadyExists => { + KeyCredentialManager::OpenAsync(h!("BitwardenBiometricsV2"))?.get()? + } + KeyCredentialStatus::Success => result, + _ => return Err(anyhow!("Failed to create key credential")), + }; + + let signature = result.Credential()?.RequestSignAsync(&CryptographicBuffer::CreateFromByteArray(challenge.as_slice())?)?.get()?; + + if signature.Status()? == KeyCredentialStatus::Success { + let signature_buffer = signature.Result()?; + let mut signature_value = + windows::core::Array::::with_len(signature_buffer.Length().unwrap() as usize); + CryptographicBuffer::CopyToByteArray(&signature_buffer, &mut signature_value)?; + + // The signature is deterministic based on the challenge and keychain key. Thus, it can be hashed to a key. + // It is unclear what entropy this key provides. + Ok(Sha256::digest(signature_value.as_slice()).into()) + } else { + Err(anyhow!("Failed to sign data")) + } +} + +async fn set_keychain_entry(user_id: &str, entry: &WindowsHelloKeychainEntry) -> Result<()> { + let serialized_entry = serde_json::to_string(entry)?; + + password::set_password( + KEYCHAIN_SERVICE_NAME, + user_id, + &serialized_entry, + ).await?; + + Ok(()) +} + +async fn get_keychain_entry(user_id: &str) -> Result { + let entry_str = password::get_password(KEYCHAIN_SERVICE_NAME, user_id).await?; + let entry: WindowsHelloKeychainEntry = serde_json::from_str(&entry_str)?; + Ok(entry) +} + +async fn delete_keychain_entry(user_id: &str) -> Result<()> { + password::delete_password(KEYCHAIN_SERVICE_NAME, user_id).await?; + Ok(()) +} + +async fn has_keychain_entry(user_id: &str) -> Result { + let entry = password::get_password(KEYCHAIN_SERVICE_NAME, user_id).await?; + Ok(!entry.is_empty()) +} diff --git a/apps/desktop/desktop_native/core/src/biometric/windows_focus.rs b/apps/desktop/desktop_native/core/src/biometric/windows_focus.rs index ce51f82862d..c0f6efc72f9 100644 --- a/apps/desktop/desktop_native/core/src/biometric/windows_focus.rs +++ b/apps/desktop/desktop_native/core/src/biometric/windows_focus.rs @@ -1,11 +1,10 @@ use windows::{ core::s, Win32::{ - Foundation::HWND, - UI::{ - Input::KeyboardAndMouse::SetFocus, - WindowsAndMessaging::{FindWindowA, SetForegroundWindow}, - }, + Foundation::HWND, System::Threading::{AttachThreadInput, GetCurrentThreadId}, UI::{ + Input::KeyboardAndMouse::{EnableWindow, SetActiveWindow, SetCapture, SetFocus}, + WindowsAndMessaging::{BringWindowToTop, FindWindowA, GetForegroundWindow, GetWindowThreadProcessId, SetForegroundWindow, SwitchToThisWindow, SystemParametersInfoW, SPIF_SENDCHANGE, SPIF_UPDATEINIFILE, SPI_GETFOREGROUNDLOCKTIMEOUT, SPI_SETFOREGROUNDLOCKTIMEOUT}, + } }, }; @@ -22,7 +21,51 @@ pub fn focus_security_prompt() { pub(crate) fn set_focus(window: HWND) { unsafe { - let _ = SetForegroundWindow(window); - let _ = SetFocus(Some(window)); + // Windows REALLY does not like apps stealing focus, even if it is for fixing Windows-Hello bugs. + // The windows hello signing prompt NEEDS to be focused instantly, or it will error, but it does + // not focus itself. + + // This function implements forced focusing of windows using a few hacks. + // The conditions to successfully foreground a window are: + // All of the following conditions are true: + // The calling process belongs to a desktop application, not a UWP app or a Windows Store app designed for Windows 8 or 8.1. + // The foreground process has not disabled calls to SetForegroundWindow by a previous call to the LockSetForegroundWindow function. + // The foreground lock time-out has expired (see SPI_GETFOREGROUNDLOCKTIMEOUT in SystemParametersInfo). + // No menus are active. + // Additionally, at least one of the following conditions is true: + // The calling process is the foreground process. + // The calling process was started by the foreground process. + // There is currently no foreground window, and thus no foreground process. + // The calling process received the last input event. + // Either the foreground process or the calling process is being debugged. + + // Attach to the foreground thread once attached, we can foregroud, even if in the background + // Update the foreground lock timeout temporarily + let mut old_timeout = 0; + let _ = SystemParametersInfoW( + SPI_GETFOREGROUNDLOCKTIMEOUT, + 0, + Some(&mut old_timeout as *mut _ as *mut std::ffi::c_void), + windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0), + ); + let _ = SystemParametersInfoW(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, None, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); + let _scopeguard = scopeguard::guard((), |_| { + let _ = SystemParametersInfoW(SPI_SETFOREGROUNDLOCKTIMEOUT, old_timeout, None, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); + }); + + // Attach to the active window's thread + let dw_current_thread = GetCurrentThreadId(); + let dw_fg_thread = GetWindowThreadProcessId(GetForegroundWindow(), None); + + let _ = AttachThreadInput(dw_current_thread, dw_fg_thread, true); + let hwnd = window; + let _ = SetForegroundWindow(hwnd); + SetCapture(hwnd); + let _ = SetFocus(Some(hwnd)); + let _ = SetActiveWindow(hwnd); + let _ = EnableWindow(hwnd, true); + let _ = BringWindowToTop(hwnd); + SwitchToThisWindow(hwnd, true); + let _ = AttachThreadInput(dw_current_thread, dw_fg_thread, false); } } diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/mod.rs b/apps/desktop/desktop_native/core/src/biometric_v2/mod.rs deleted file mode 100644 index 7d25625c810..00000000000 --- a/apps/desktop/desktop_native/core/src/biometric_v2/mod.rs +++ /dev/null @@ -1,42 +0,0 @@ -use anyhow::{Result}; - -#[allow(clippy::module_inception)] -#[cfg_attr(target_os = "linux", path = "linux.rs")] -#[cfg_attr(target_os = "macos", path = "unimplemented.rs")] -#[cfg_attr(target_os = "windows", path = "windows.rs")] -mod biometric_v2; - -#[cfg(target_os = "windows")] -mod windows_focus; - -pub use biometric_v2::BiometricLockSystem; - -#[allow(async_fn_in_trait)] -pub trait BiometricV2Trait { - /// Authenticate the user - async fn authenticate(&self, hwnd: Vec, message: String) -> Result; - /// Check if biometric authentication is available - async fn authenticate_available(&self) -> Result; - /// Enroll a key for persistent unlock - async fn enroll_persistent(&self, user_id: &str, key: &[u8]) -> Result<()>; - /// Clear the persistent and ephemeral keys - async fn unenroll(&self, user_id: &str) -> Result<()>; - async fn has_persistent(&self, user_id: &str) -> Result; - /// On every unlock, the client provides a key to be held for subsequent biometric unlock - async fn provide_key( - &self, - user_id: &str, - key: &[u8] - ); - /// Perform biometric unlock and return the key - async fn unlock( - &self, - user_id: &str, - hwnd: Vec, - ) -> Result>; - /// Check if biometric unlock is available based on whether a key is present and whether authentication is possible - async fn unlock_available( - &self, - user_id: &str, - ) -> Result; -} diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/windows.rs b/apps/desktop/desktop_native/core/src/biometric_v2/windows.rs deleted file mode 100644 index a900d1b440b..00000000000 --- a/apps/desktop/desktop_native/core/src/biometric_v2/windows.rs +++ /dev/null @@ -1,270 +0,0 @@ -//! This file implements Windows-Hello based biometric unlock. -//! -//! # Security -//! Note: There are two scenarios to consider, with different security implications. This section -//! describes the assumed security model and security guarantees achieved. In the required security -//! guarantee is that a locked vault - a running app - cannot be unlocked when the device (user-space) -//! is compromised in this state. -//! -//! 1. Require master password on app restart -//! In this scenario, when first unlocking the app, the app sends the user-key to this module, which holds it in secure memory, -//! protected by DPAPI. This makes it inaccessible to other processes, unless they compromise the system administrator, or kernel. -//! While the app is running this key is held in memory, even if locked. When unlocking, the app will prompt the user via -//! `windows_hello_authenticate` to get a yes/no decision on whether to release the key to the app. -//! -//! 2. Do not require master password on app restart -//! In this scenario, when enrolling, the app sends the user-key to this module, which derives the windows hello key -//! with the Windows Hello prompt. This is done by signing a per-user challenge, which produces a deterministic -//! signature which is hashed to obtain a key. This key is used to encrypt and persist the vault unlock key (user key). -//! -//! Since the keychain can be accessed by all user-space processes, the challenge is known to all userspace processes. -//! Therefore, to circumvent the security measure, the attacker would need to create a fake Windows-Hello prompt, and -//! get the user to confirm it. - -use std::{ffi::c_void, sync::{atomic::AtomicBool, Arc}}; - -use aes::cipher::KeyInit; -use anyhow::{anyhow, Result}; -use chacha20poly1305::{aead::Aead, XChaCha20Poly1305, XNonce}; -use sha2::{Digest, Sha256}; -use tokio::sync::Mutex; -use windows::{ - core::{factory, h, HSTRING}, - Security::{Credentials::{KeyCredentialCreationOption, KeyCredentialManager, KeyCredentialStatus, UI::{ - UserConsentVerificationResult, UserConsentVerifier, UserConsentVerifierAvailability, - }}, Cryptography::CryptographicBuffer}, - Win32::{ - Foundation::HWND, System::WinRT::IUserConsentVerifierInterop, - UI::WindowsAndMessaging::GetForegroundWindow, - }, -}; -use windows_future::IAsyncOperation; - -use super::windows_focus::{focus_security_prompt, set_focus}; -use crate::{ - password, secure_memory::* -}; - -const KEYCHAIN_SERVICE_NAME: &str = "BitwardenBiometricsV2"; - -#[derive(serde::Serialize, serde::Deserialize)] -struct WindowsHelloKeychainEntry { - nonce: [u8; 24], - challenge: [u8; 16], - wrapped_key: Vec, -} - -/// The Windows OS implementation of the biometric trait. -pub struct BiometricLockSystem { - // The userkeys that are held in memory MUST be protected from memory dumping attacks, to ensure - // locked vaults cannot be unlocked - secure_memory: Arc> -} - -impl BiometricLockSystem { - pub fn new() -> Self { - Self { - secure_memory: Arc::new(Mutex::new(crate::secure_memory::dpapi::DpapiSecretKVStore::new())), - } - } -} - -impl super::BiometricV2Trait for BiometricLockSystem { - async fn authenticate(&self, hwnd: Vec, message: String) -> Result { - windows_hello_authenticate(hwnd, message) - } - - async fn authenticate_available(&self) -> Result { - match UserConsentVerifier::CheckAvailabilityAsync()?.get()? { - UserConsentVerifierAvailability::Available => Ok(true), - UserConsentVerifierAvailability::DeviceBusy => Ok(true), - _ => Ok(false), - } - } - - async fn unenroll(&self, user_id: &str) -> Result<()> { - let mut secure_memory = self.secure_memory.lock().await; - secure_memory.remove(user_id); - delete_keychain_entry(user_id).await?; - Ok(()) - } - - async fn enroll_persistent(&self, user_id: &str, key: &[u8]) -> Result<()> { - // Enrollment works by first generating a random challenge unique to the user / enrollment. Then, - // with the challenge and a Windows-Hello prompt, the "windows hello key" is derived. The windows - // hello key is used to encrypt the key to store with XChaCha20Poly1305. The bundle of nonce, - // challenge and wrapped-key are stored to the keychain - - // Each enrollment (per user) has a unique challenge, so that the windows-hello key is unique - let mut challenge = [0u8; 16]; - rand::fill(&mut challenge); - - // This key is unique to the challenge - let windows_hello_key = windows_hello_authenticate_with_crypto(&challenge)?; - - let nonce = { - let mut nonce_bytes = [0u8; 24]; - rand::fill(&mut nonce_bytes); - XNonce::clone_from_slice(&nonce_bytes) - }; - - let wrapped_key = XChaCha20Poly1305::new(&windows_hello_key.into()).encrypt(&nonce, key).map_err(|e| anyhow!(e))?; - set_keychain_entry(user_id, &WindowsHelloKeychainEntry { - nonce: nonce.as_slice().try_into().map_err(|_| anyhow!("Invalid nonce length"))?, - challenge, - wrapped_key, - }).await?; - Ok(()) - } - - async fn provide_key(&self, user_id: &str, key: &[u8]) { - let mut secure_memory = self.secure_memory.lock().await; - secure_memory.put(user_id.to_string(), key); - } - - async fn unlock(&self, user_id: &str, hwnd: Vec) -> Result> { - let mut secure_memory = self.secure_memory.lock().await; - if secure_memory.has(user_id) { - println!("[Windows Hello] Key is in secure memory, using UV API"); - - if self.authenticate(hwnd, "Unlock your vault".to_owned()).await? { - println!("[Windows Hello] Authentication successful"); - return secure_memory.get(user_id).clone().ok_or_else(|| anyhow!("No key found for user")); - } - Err(anyhow!("Authentication failed")) - } else { - println!("[Windows Hello] Key not in secure memory, using Signing API"); - - let keychain_entry = get_keychain_entry(user_id).await?; - let windows_hello_key = windows_hello_authenticate_with_crypto(&keychain_entry.challenge)?; - let decrypted_key = XChaCha20Poly1305::new(&windows_hello_key.into()).decrypt(keychain_entry.nonce.as_slice().try_into().map_err(|_| anyhow!("Invalid nonce length"))?, keychain_entry.wrapped_key.as_slice()).map_err(|e| anyhow!(e))?; - secure_memory.put(user_id.to_string(), &decrypted_key.clone()); - Ok(decrypted_key) - } - } - - async fn unlock_available(&self, user_id: &str) -> Result { - let secure_memory = self.secure_memory.lock().await; - let has_key = secure_memory.has(user_id) || has_keychain_entry(user_id).await.unwrap_or(false); - Ok(has_key && self.authenticate_available().await.unwrap_or(false)) - } - - async fn has_persistent(&self, user_id: &str) -> Result { - Ok(get_keychain_entry(user_id).await.is_ok()) - } -} - -/// Get a yes/no authorization without any cryptographic backing. -/// This API has better focusing behavior -fn windows_hello_authenticate(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), - } -} - -/// Derive the symmetric encryption key from the Windows Hello signature. -/// -/// This works by signing a static challenge string with Windows Hello protected key store. The -/// signed challenge is then hashed using SHA-256 and used as the symmetric encryption key for the -/// Windows Hello protected keys. -/// -/// Windows will only sign the challenge if the user has successfully authenticated with Windows, -/// ensuring user presence. -/// -/// Note: This API has inconsistent focusing behavior when called from another window -fn windows_hello_authenticate_with_crypto(challenge: &[u8; 16]) -> Result<[u8; 32]> { - // Ugly hack: We need to focus the window via window focusing APIs until Microsoft releases a new API. - // This is unreliable, and if it does not work, the operation may fail - let stop_focusing = Arc::new(AtomicBool::new(false)); - let stop_focusing_clone = stop_focusing.clone(); - let _ = std::thread::spawn(move || loop { - if !stop_focusing_clone.load(std::sync::atomic::Ordering::Relaxed) { - focus_security_prompt(); - std::thread::sleep(std::time::Duration::from_millis(500)); - } else { - break; - } - }); - // Only stop focusing once this function exists. The focus MUST run both during the initial creation - // with RequestCreateAsync, and also with the subsequent use with RequestSignAsync. - let _guard = scopeguard::guard((), |_| { - stop_focusing.store(true, std::sync::atomic::Ordering::Relaxed); - }); - - // First create or replace the Bitwarden signing key - let result = KeyCredentialManager::RequestCreateAsync( - h!("BitwardenBiometricsV2"), - KeyCredentialCreationOption::FailIfExists, - )? - .get()?; - let result = match result.Status()? { - KeyCredentialStatus::CredentialAlreadyExists => { - KeyCredentialManager::OpenAsync(h!("BitwardenBiometricsV2"))?.get()? - } - KeyCredentialStatus::Success => result, - _ => return Err(anyhow!("Failed to create key credential")), - }; - - let signature = result.Credential()?.RequestSignAsync(&CryptographicBuffer::CreateFromByteArray(challenge.as_slice())?)?.get()?; - - if signature.Status()? == KeyCredentialStatus::Success { - let signature_buffer = signature.Result()?; - let mut signature_value = - windows::core::Array::::with_len(signature_buffer.Length().unwrap() as usize); - CryptographicBuffer::CopyToByteArray(&signature_buffer, &mut signature_value)?; - - // The signature is deterministic based on the challenge and keychain key. Thus, it can be hashed to a key. - // It is unclear what entropy this key provides. - Ok(Sha256::digest(signature_value.as_slice()).into()) - } else { - Err(anyhow!("Failed to sign data")) - } -} - -async fn set_keychain_entry(user_id: &str, entry: &WindowsHelloKeychainEntry) -> Result<()> { - let serialized_entry = serde_json::to_string(entry)?; - - password::set_password( - KEYCHAIN_SERVICE_NAME, - user_id, - &serialized_entry, - ).await?; - - Ok(()) -} - -async fn get_keychain_entry(user_id: &str) -> Result { - let entry_str = password::get_password(KEYCHAIN_SERVICE_NAME, user_id).await?; - let entry: WindowsHelloKeychainEntry = serde_json::from_str(&entry_str)?; - Ok(entry) -} - -async fn delete_keychain_entry(user_id: &str) -> Result<()> { - password::delete_password(KEYCHAIN_SERVICE_NAME, user_id).await?; - Ok(()) -} - -async fn has_keychain_entry(user_id: &str) -> Result { - let entry = password::get_password(KEYCHAIN_SERVICE_NAME, user_id).await?; - Ok(!entry.is_empty()) -} diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/windows_focus.rs b/apps/desktop/desktop_native/core/src/biometric_v2/windows_focus.rs deleted file mode 100644 index c0f6efc72f9..00000000000 --- a/apps/desktop/desktop_native/core/src/biometric_v2/windows_focus.rs +++ /dev/null @@ -1,71 +0,0 @@ -use windows::{ - core::s, - Win32::{ - Foundation::HWND, System::Threading::{AttachThreadInput, GetCurrentThreadId}, UI::{ - Input::KeyboardAndMouse::{EnableWindow, SetActiveWindow, SetCapture, SetFocus}, - WindowsAndMessaging::{BringWindowToTop, FindWindowA, GetForegroundWindow, GetWindowThreadProcessId, SetForegroundWindow, SwitchToThisWindow, SystemParametersInfoW, SPIF_SENDCHANGE, SPIF_UPDATEINIFILE, SPI_GETFOREGROUNDLOCKTIMEOUT, SPI_SETFOREGROUNDLOCKTIMEOUT}, - } - }, -}; - -/// 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 { - // Windows REALLY does not like apps stealing focus, even if it is for fixing Windows-Hello bugs. - // The windows hello signing prompt NEEDS to be focused instantly, or it will error, but it does - // not focus itself. - - // This function implements forced focusing of windows using a few hacks. - // The conditions to successfully foreground a window are: - // All of the following conditions are true: - // The calling process belongs to a desktop application, not a UWP app or a Windows Store app designed for Windows 8 or 8.1. - // The foreground process has not disabled calls to SetForegroundWindow by a previous call to the LockSetForegroundWindow function. - // The foreground lock time-out has expired (see SPI_GETFOREGROUNDLOCKTIMEOUT in SystemParametersInfo). - // No menus are active. - // Additionally, at least one of the following conditions is true: - // The calling process is the foreground process. - // The calling process was started by the foreground process. - // There is currently no foreground window, and thus no foreground process. - // The calling process received the last input event. - // Either the foreground process or the calling process is being debugged. - - // Attach to the foreground thread once attached, we can foregroud, even if in the background - // Update the foreground lock timeout temporarily - let mut old_timeout = 0; - let _ = SystemParametersInfoW( - SPI_GETFOREGROUNDLOCKTIMEOUT, - 0, - Some(&mut old_timeout as *mut _ as *mut std::ffi::c_void), - windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0), - ); - let _ = SystemParametersInfoW(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, None, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); - let _scopeguard = scopeguard::guard((), |_| { - let _ = SystemParametersInfoW(SPI_SETFOREGROUNDLOCKTIMEOUT, old_timeout, None, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); - }); - - // Attach to the active window's thread - let dw_current_thread = GetCurrentThreadId(); - let dw_fg_thread = GetWindowThreadProcessId(GetForegroundWindow(), None); - - let _ = AttachThreadInput(dw_current_thread, dw_fg_thread, true); - let hwnd = window; - let _ = SetForegroundWindow(hwnd); - SetCapture(hwnd); - let _ = SetFocus(Some(hwnd)); - let _ = SetActiveWindow(hwnd); - let _ = EnableWindow(hwnd, true); - let _ = BringWindowToTop(hwnd); - SwitchToThisWindow(hwnd, true); - let _ = AttachThreadInput(dw_current_thread, dw_fg_thread, false); - } -} diff --git a/apps/desktop/desktop_native/core/src/crypto/cipher_string.rs b/apps/desktop/desktop_native/core/src/crypto/cipher_string.rs deleted file mode 100644 index 81f734bf0f2..00000000000 --- a/apps/desktop/desktop_native/core/src/crypto/cipher_string.rs +++ /dev/null @@ -1,212 +0,0 @@ -use std::{fmt::Display, str::FromStr}; - -use base64::{engine::general_purpose::STANDARD as base64_engine, Engine}; - -use crate::error::{CSParseError, Error}; - -#[allow(unused, non_camel_case_types)] -pub enum CipherString { - // 0 - AesCbc256_B64 { - iv: [u8; 16], - data: Vec, - }, - // 1 - AesCbc128_HmacSha256_B64 { - iv: [u8; 16], - mac: [u8; 32], - data: Vec, - }, - // 2 - AesCbc256_HmacSha256_B64 { - iv: [u8; 16], - mac: [u8; 32], - data: Vec, - }, - // 3 - Rsa2048_OaepSha256_B64 { - data: Vec, - }, - // 4 - Rsa2048_OaepSha1_B64 { - data: Vec, - }, - // 5 - Rsa2048_OaepSha256_HmacSha256_B64 { - mac: [u8; 32], - data: Vec, - }, - // 6 - Rsa2048_OaepSha1_HmacSha256_B64 { - mac: [u8; 32], - data: Vec, - }, -} - -// We manually implement these to make sure we don't print any sensitive data -impl std::fmt::Debug for CipherString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("CipherString") - .field("type", &self.enc_type_name()) - .finish() - } -} - -fn invalid_len_error(expected: usize) -> impl Fn(Vec) -> CSParseError { - move |e: Vec<_>| CSParseError::InvalidBase64Length { - expected, - got: e.len(), - } -} - -impl FromStr for CipherString { - type Err = Error; - - fn from_str(s: &str) -> Result { - let (enc_type, data) = s.split_once('.').ok_or(CSParseError::NoType)?; - - let parts: Vec<_> = data.split('|').collect(); - match (enc_type, parts.len()) { - ("0", 2) => { - let iv_str = parts[0]; - let data_str = parts[1]; - - let iv = base64_engine - .decode(iv_str) - .map_err(CSParseError::InvalidBase64)? - .try_into() - .map_err(invalid_len_error(16))?; - - let data = base64_engine - .decode(data_str) - .map_err(CSParseError::InvalidBase64)?; - - Ok(CipherString::AesCbc256_B64 { iv, data }) - } - - ("1" | "2", 3) => { - let iv_str = parts[0]; - let data_str = parts[1]; - let mac_str = parts[2]; - - let iv = base64_engine - .decode(iv_str) - .map_err(CSParseError::InvalidBase64)? - .try_into() - .map_err(invalid_len_error(16))?; - - let mac = base64_engine - .decode(mac_str) - .map_err(CSParseError::InvalidBase64)? - .try_into() - .map_err(invalid_len_error(32))?; - - let data = base64_engine - .decode(data_str) - .map_err(CSParseError::InvalidBase64)?; - - if enc_type == "1" { - Ok(CipherString::AesCbc128_HmacSha256_B64 { iv, mac, data }) - } else { - Ok(CipherString::AesCbc256_HmacSha256_B64 { iv, mac, data }) - } - } - - ("3" | "4", 1) => { - let data = base64_engine - .decode(data) - .map_err(CSParseError::InvalidBase64)?; - if enc_type == "3" { - Ok(CipherString::Rsa2048_OaepSha256_B64 { data }) - } else { - Ok(CipherString::Rsa2048_OaepSha1_B64 { data }) - } - } - ("5" | "6", 2) => { - unimplemented!() - } - - (enc_type, parts) => Err(CSParseError::InvalidType { - enc_type: enc_type.to_string(), - parts, - } - .into()), - } - } -} - -impl Display for CipherString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}.", self.enc_type())?; - - let mut parts = Vec::<&[u8]>::new(); - - match self { - CipherString::AesCbc256_B64 { iv, data } => { - parts.push(iv); - parts.push(data); - } - CipherString::AesCbc128_HmacSha256_B64 { iv, mac, data } => { - parts.push(iv); - parts.push(data); - parts.push(mac); - } - CipherString::AesCbc256_HmacSha256_B64 { iv, mac, data } => { - parts.push(iv); - parts.push(data); - parts.push(mac); - } - CipherString::Rsa2048_OaepSha256_B64 { data } => { - parts.push(data); - } - CipherString::Rsa2048_OaepSha1_B64 { data } => { - parts.push(data); - } - CipherString::Rsa2048_OaepSha256_HmacSha256_B64 { mac, data } => { - parts.push(data); - parts.push(mac); - } - CipherString::Rsa2048_OaepSha1_HmacSha256_B64 { mac, data } => { - parts.push(data); - parts.push(mac); - } - } - - for i in 0..parts.len() { - if i == parts.len() - 1 { - write!(f, "{}", base64_engine.encode(parts[i]))?; - } else { - write!(f, "{}|", base64_engine.encode(parts[i]))?; - } - } - - Ok(()) - } -} - -impl CipherString { - fn enc_type(&self) -> u8 { - match self { - CipherString::AesCbc256_B64 { .. } => 0, - CipherString::AesCbc128_HmacSha256_B64 { .. } => 1, - CipherString::AesCbc256_HmacSha256_B64 { .. } => 2, - CipherString::Rsa2048_OaepSha256_B64 { .. } => 3, - CipherString::Rsa2048_OaepSha1_B64 { .. } => 4, - CipherString::Rsa2048_OaepSha256_HmacSha256_B64 { .. } => 5, - CipherString::Rsa2048_OaepSha1_HmacSha256_B64 { .. } => 6, - } - } - - fn enc_type_name(&self) -> &str { - match self.enc_type() { - 0 => "AesCbc256_B64", - 1 => "AesCbc128_HmacSha256_B64", - 2 => "AesCbc256_HmacSha256_B64", - 3 => "Rsa2048_OaepSha256_B64", - 4 => "Rsa2048_OaepSha1_B64", - 5 => "Rsa2048_OaepSha256_HmacSha256_B64", - 6 => "Rsa2048_OaepSha1_HmacSha256_B64", - _ => "Unknown", - } - } -} diff --git a/apps/desktop/desktop_native/core/src/crypto/crypto.rs b/apps/desktop/desktop_native/core/src/crypto/crypto.rs deleted file mode 100644 index d9e2aec3046..00000000000 --- a/apps/desktop/desktop_native/core/src/crypto/crypto.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Cryptographic primitives used in the SDK - -use aes::cipher::{ - block_padding::Pkcs7, generic_array::GenericArray, typenum::U32, BlockDecryptMut, - BlockEncryptMut, KeyIvInit, -}; - -use crate::error::{CryptoError, Result}; - -use super::CipherString; - -pub fn decrypt_aes256(iv: &[u8; 16], data: &[u8], key: GenericArray) -> Result> { - let iv = GenericArray::from_slice(iv); - let mut data = data.to_vec(); - let decrypted_key_slice = cbc::Decryptor::::new(&key, iv) - .decrypt_padded_mut::(&mut data) - .map_err(|_| CryptoError::KeyDecrypt)?; - - // Data is decrypted in place and returns a subslice of the original Vec, to avoid cloning it, we truncate to the subslice length - let decrypted_len = decrypted_key_slice.len(); - data.truncate(decrypted_len); - - Ok(data) -} - -pub fn encrypt_aes256( - data_dec: &[u8], - iv: [u8; 16], - key: GenericArray, -) -> Result { - let data = cbc::Encryptor::::new(&key, &iv.into()) - .encrypt_padded_vec_mut::(data_dec); - - Ok(CipherString::AesCbc256_B64 { iv, data }) -} diff --git a/apps/desktop/desktop_native/core/src/crypto/mod.rs b/apps/desktop/desktop_native/core/src/crypto/mod.rs deleted file mode 100644 index 72f189509f8..00000000000 --- a/apps/desktop/desktop_native/core/src/crypto/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub use cipher_string::*; -pub use crypto::*; - -mod cipher_string; -#[allow(clippy::module_inception)] -mod crypto; diff --git a/apps/desktop/desktop_native/core/src/lib.rs b/apps/desktop/desktop_native/core/src/lib.rs index 2208edcf586..b79839c9992 100644 --- a/apps/desktop/desktop_native/core/src/lib.rs +++ b/apps/desktop/desktop_native/core/src/lib.rs @@ -2,7 +2,6 @@ pub mod autofill; pub mod autostart; pub mod biometric; pub mod clipboard; -pub mod crypto; pub mod error; pub mod ipc; pub mod password; @@ -10,7 +9,6 @@ pub mod powermonitor; pub mod process_isolation; pub mod ssh_agent; pub(crate) mod secure_memory; -pub mod biometric_v2; use zeroizing_alloc::ZeroAlloc; diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index b9eb4894aea..e9ee559e470 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -21,7 +21,7 @@ export declare namespace passwords { /** Checks if the os secure storage is available */ export function isAvailable(): Promise } -export declare namespace biometrics_v2 { +export declare namespace biometrics { export function initBiometricSystem(): BiometricLockSystem export function authenticate(biometricLockSystem: BiometricLockSystem, hwnd: Buffer, message: string): Promise export function authenticateAvailable(biometricLockSystem: BiometricLockSystem): Promise @@ -33,34 +33,6 @@ export declare namespace biometrics_v2 { export function unenroll(biometricLockSystem: BiometricLockSystem, userId: string): Promise export class BiometricLockSystem { } } -export declare namespace biometrics { - export function prompt(hwnd: Buffer, message: string): Promise - export function available(): Promise - export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string): Promise - /** - * Retrieves the biometric secret for the given service and account. - * Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist. - */ - export function getBiometricSecret(service: string, account: string, keyMaterial?: KeyMaterial | undefined | null): Promise - /** - * Derives key material from biometric data. Returns a string encoded with a - * base64 encoded key and the base64 encoded challenge used to create it - * separated by a `|` character. - * - * If the iv is provided, it will be used as the challenge. Otherwise a random challenge will be generated. - * - * `format!("|")` - */ - export function deriveKeyMaterial(iv?: string | undefined | null): Promise - export interface KeyMaterial { - osKeyPartB64: string - clientKeyPartB64?: string - } - export interface OsDerivedKey { - keyB64: string - ivB64: string - } -} export declare namespace clipboards { export function read(): Promise export function write(text: string, password: boolean): Promise diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 7cf5ea05bbf..e36c6984626 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -50,18 +50,18 @@ pub mod passwords { } #[napi] -pub mod biometrics_v2 { - use desktop_core::biometric_v2::{BiometricV2Trait}; +pub mod biometrics { + use desktop_core::biometric::{BiometricTrait}; #[napi] pub struct BiometricLockSystem { - inner: desktop_core::biometric_v2::BiometricLockSystem, + inner: desktop_core::biometric::BiometricLockSystem, } #[napi] pub fn init_biometric_system() -> napi::Result { Ok(BiometricLockSystem { - inner: desktop_core::biometric_v2::BiometricLockSystem::new() + inner: desktop_core::biometric::BiometricLockSystem::new() }) } @@ -116,106 +116,6 @@ pub mod biometrics_v2 { pub async fn unenroll(biometric_lock_system: &BiometricLockSystem, user_id: String) -> napi::Result<()> { biometric_lock_system.inner.unenroll(&user_id).await.map_err(|e| napi::Error::from_reason(e.to_string())) } - -} - -#[napi] -pub mod biometrics { - use desktop_core::biometric::{Biometric, BiometricTrait}; - - // Prompt for biometric confirmation - #[napi] - pub async fn prompt( - hwnd: napi::bindgen_prelude::Buffer, - message: String, - ) -> napi::Result { - Biometric::prompt(hwnd.into(), message) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn available() -> napi::Result { - Biometric::available() - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn set_biometric_secret( - service: String, - account: String, - secret: String, - key_material: Option, - iv_b64: String, - ) -> napi::Result { - Biometric::set_biometric_secret( - &service, - &account, - &secret, - key_material.map(|m| m.into()), - &iv_b64, - ) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Retrieves the biometric secret for the given service and account. - /// Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist. - #[napi] - pub async fn get_biometric_secret( - service: String, - account: String, - key_material: Option, - ) -> napi::Result { - Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into())) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Derives key material from biometric data. Returns a string encoded with a - /// base64 encoded key and the base64 encoded challenge used to create it - /// separated by a `|` character. - /// - /// If the iv is provided, it will be used as the challenge. Otherwise a random challenge will be generated. - /// - /// `format!("|")` - #[napi] - pub async fn derive_key_material(iv: Option) -> napi::Result { - Biometric::derive_key_material(iv.as_deref()) - .map(|k| k.into()) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi(object)] - pub struct KeyMaterial { - pub os_key_part_b64: String, - pub client_key_part_b64: Option, - } - - impl From for desktop_core::biometric::KeyMaterial { - fn from(km: KeyMaterial) -> Self { - desktop_core::biometric::KeyMaterial { - os_key_part_b64: km.os_key_part_b64, - client_key_part_b64: km.client_key_part_b64, - } - } - } - - #[napi(object)] - pub struct OsDerivedKey { - pub key_b64: String, - pub iv_b64: String, - } - - impl From for OsDerivedKey { - fn from(km: desktop_core::biometric::OsDerivedKey) -> Self { - OsDerivedKey { - key_b64: km.key_b64, - iv_b64: km.iv_b64, - } - } - } } #[napi]