1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-05 19:23:19 +00:00

Remove old biometrics

This commit is contained in:
Bernd Schoolmann
2025-08-28 13:25:42 +02:00
parent e9af77078a
commit 23eef2d250
16 changed files with 316 additions and 1288 deletions

View File

@@ -34,7 +34,7 @@ impl BiometricLockSystem {
}
}
impl super::BiometricV2Trait for BiometricLockSystem {
impl super::BiometricTrait for BiometricLockSystem {
async fn authenticate(&self, _hwnd: Vec<u8>, _message: String) -> Result<bool> {
let connection = Connection::system().await?;
let proxy = AuthorityProxy::new(&connection).await?;

View File

@@ -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<u8>, _message: String) -> Result<bool> {
bail!("platform not supported");
}
async fn available() -> Result<bool> {
bail!("platform not supported");
}
fn derive_key_material(_iv_str: Option<&str>) -> Result<OsDerivedKey> {
bail!("platform not supported");
}
async fn get_biometric_secret(
_service: &str,
_account: &str,
_key_material: Option<KeyMaterial>,
) -> Result<String> {
bail!("platform not supported");
}
async fn set_biometric_secret(
_service: &str,
_account: &str,
_secret: &str,
_key_material: Option<super::KeyMaterial>,
_iv_b64: &str,
) -> Result<String> {
bail!("platform not supported");
}
}

View File

@@ -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<String>,
}
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<u8>, message: String) -> Result<bool>;
async fn available() -> Result<bool>;
fn derive_key_material(secret: Option<&str>) -> Result<OsDerivedKey>;
async fn set_biometric_secret(
service: &str,
account: &str,
secret: &str,
key_material: Option<KeyMaterial>,
iv_b64: &str,
) -> Result<String>;
async fn get_biometric_secret(
service: &str,
account: &str,
key_material: Option<KeyMaterial>,
) -> Result<String>;
}
#[allow(unused)]
fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result<String> {
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<String> {
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<GenericArray<u8, typenum::U32>> {
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::<CipherString>()
.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<u8>, message: String) -> Result<bool>;
/// Check if biometric authentication is available
async fn authenticate_available(&self) -> Result<bool>;
/// 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<bool>;
/// 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<u8>,
) -> Result<Vec<u8>>;
/// 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<bool>;
}

View File

@@ -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<u8>, _message: String) -> Result<bool> {
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<bool> {
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<OsDerivedKey> {
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<KeyMaterial>,
iv_b64: &str,
) -> Result<String> {
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<KeyMaterial>,
) -> Result<String> {
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
}

View File

@@ -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<u8>,
}
/// 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<Mutex<crate::secure_memory::dpapi::DpapiSecretKVStore>>
}
impl super::BiometricTrait for Biometric {
async fn prompt(hwnd: Vec<u8>, message: String) -> Result<bool> {
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::<UserConsentVerifier, IUserConsentVerifierInterop>()?;
let operation: IAsyncOperation<UserConsentVerificationResult> = 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<bool> {
let ucv_available = UserConsentVerifier::CheckAvailabilityAsync()?.get()?;
impl super::BiometricTrait for BiometricLockSystem {
async fn authenticate(&self, hwnd: Vec<u8>, message: String) -> Result<bool> {
windows_hello_authenticate(hwnd, message)
}
match ucv_available {
async fn authenticate_available(&self) -> Result<bool> {
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<OsDerivedKey> {
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<KeyMaterial>,
iv_b64: &str,
) -> Result<String> {
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<KeyMaterial>,
) -> Result<String> {
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<u8>) -> Result<Vec<u8>> {
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 = <Biometric as BiometricTrait>::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<bool> {
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 = <Biometric as BiometricTrait>::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() {
<Biometric as BiometricTrait>::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!(<Biometric as BiometricTrait>::available().await.unwrap())
}
#[tokio::test]
#[cfg(feature = "manual_test")]
async fn get_biometric_secret_requires_key() {
let result = <Biometric as BiometricTrait>::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 =
<Biometric as BiometricTrait>::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 =
<Biometric as BiometricTrait>::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 =
<Biometric as BiometricTrait>::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<bool> {
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<u8>, message: String) -> Result<bool> {
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::<UserConsentVerifier, IUserConsentVerifierInterop>()?;
let operation: IAsyncOperation<UserConsentVerificationResult> = 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::<u8>::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<WindowsHelloKeychainEntry> {
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<bool> {
let entry = password::get_password(KEYCHAIN_SERVICE_NAME, user_id).await?;
Ok(!entry.is_empty())
}

View File

@@ -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);
}
}

View File

@@ -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<u8>, message: String) -> Result<bool>;
/// Check if biometric authentication is available
async fn authenticate_available(&self) -> Result<bool>;
/// 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<bool>;
/// 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<u8>,
) -> Result<Vec<u8>>;
/// 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<bool>;
}

View File

@@ -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<u8>,
}
/// 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<Mutex<crate::secure_memory::dpapi::DpapiSecretKVStore>>
}
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<u8>, message: String) -> Result<bool> {
windows_hello_authenticate(hwnd, message)
}
async fn authenticate_available(&self) -> Result<bool> {
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<u8>) -> Result<Vec<u8>> {
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<bool> {
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<bool> {
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<u8>, message: String) -> Result<bool> {
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::<UserConsentVerifier, IUserConsentVerifierInterop>()?;
let operation: IAsyncOperation<UserConsentVerificationResult> = 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::<u8>::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<WindowsHelloKeychainEntry> {
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<bool> {
let entry = password::get_password(KEYCHAIN_SERVICE_NAME, user_id).await?;
Ok(!entry.is_empty())
}

View File

@@ -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);
}
}

View File

@@ -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<u8>,
},
// 1
AesCbc128_HmacSha256_B64 {
iv: [u8; 16],
mac: [u8; 32],
data: Vec<u8>,
},
// 2
AesCbc256_HmacSha256_B64 {
iv: [u8; 16],
mac: [u8; 32],
data: Vec<u8>,
},
// 3
Rsa2048_OaepSha256_B64 {
data: Vec<u8>,
},
// 4
Rsa2048_OaepSha1_B64 {
data: Vec<u8>,
},
// 5
Rsa2048_OaepSha256_HmacSha256_B64 {
mac: [u8; 32],
data: Vec<u8>,
},
// 6
Rsa2048_OaepSha1_HmacSha256_B64 {
mac: [u8; 32],
data: Vec<u8>,
},
}
// 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<u8>) -> CSParseError {
move |e: Vec<_>| CSParseError::InvalidBase64Length {
expected,
got: e.len(),
}
}
impl FromStr for CipherString {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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",
}
}
}

View File

@@ -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<u8, U32>) -> Result<Vec<u8>> {
let iv = GenericArray::from_slice(iv);
let mut data = data.to_vec();
let decrypted_key_slice = cbc::Decryptor::<aes::Aes256>::new(&key, iv)
.decrypt_padded_mut::<Pkcs7>(&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<u8, U32>,
) -> Result<CipherString> {
let data = cbc::Encryptor::<aes::Aes256>::new(&key, &iv.into())
.encrypt_padded_vec_mut::<Pkcs7>(data_dec);
Ok(CipherString::AesCbc256_B64 { iv, data })
}

View File

@@ -1,6 +0,0 @@
pub use cipher_string::*;
pub use crypto::*;
mod cipher_string;
#[allow(clippy::module_inception)]
mod crypto;

View File

@@ -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;

View File

@@ -21,7 +21,7 @@ export declare namespace passwords {
/** Checks if the os secure storage is available */
export function isAvailable(): Promise<boolean>
}
export declare namespace biometrics_v2 {
export declare namespace biometrics {
export function initBiometricSystem(): BiometricLockSystem
export function authenticate(biometricLockSystem: BiometricLockSystem, hwnd: Buffer, message: string): Promise<boolean>
export function authenticateAvailable(biometricLockSystem: BiometricLockSystem): Promise<boolean>
@@ -33,34 +33,6 @@ export declare namespace biometrics_v2 {
export function unenroll(biometricLockSystem: BiometricLockSystem, userId: string): Promise<void>
export class BiometricLockSystem { }
}
export declare namespace biometrics {
export function prompt(hwnd: Buffer, message: string): Promise<boolean>
export function available(): Promise<boolean>
export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string): Promise<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.
*/
export function getBiometricSecret(service: string, account: string, keyMaterial?: KeyMaterial | undefined | null): Promise<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!("<key_base64>|<iv_base64>")`
*/
export function deriveKeyMaterial(iv?: string | undefined | null): Promise<OsDerivedKey>
export interface KeyMaterial {
osKeyPartB64: string
clientKeyPartB64?: string
}
export interface OsDerivedKey {
keyB64: string
ivB64: string
}
}
export declare namespace clipboards {
export function read(): Promise<string>
export function write(text: string, password: boolean): Promise<void>

View File

@@ -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<BiometricLockSystem> {
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<bool> {
Biometric::prompt(hwnd.into(), message)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn available() -> napi::Result<bool> {
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<KeyMaterial>,
iv_b64: String,
) -> napi::Result<String> {
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<KeyMaterial>,
) -> napi::Result<String> {
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!("<key_base64>|<iv_base64>")`
#[napi]
pub async fn derive_key_material(iv: Option<String>) -> napi::Result<OsDerivedKey> {
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<String>,
}
impl From<KeyMaterial> 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<desktop_core::biometric::OsDerivedKey> for OsDerivedKey {
fn from(km: desktop_core::biometric::OsDerivedKey) -> Self {
OsDerivedKey {
key_b64: km.key_b64,
iv_b64: km.iv_b64,
}
}
}
}
#[napi]