1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 15:23:33 +00:00

[PM-2094] Fix windows hello focusing behavior (#12255)

* Implement new windows focus behavior

* Fix formatting

* Fix clippy warning

* Fix clippy warning

* Fix build

* Fix build
This commit is contained in:
Bernd Schoolmann
2025-01-09 14:07:40 +01:00
committed by GitHub
parent 67a59b6072
commit a527aa9196
9 changed files with 97 additions and 93 deletions

View File

@@ -916,7 +916,6 @@ dependencies = [
"pin-project", "pin-project",
"pkcs8", "pkcs8",
"rand", "rand",
"retry",
"rsa", "rsa",
"russh-cryptovec", "russh-cryptovec",
"scopeguard", "scopeguard",
@@ -2388,15 +2387,6 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "retry"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9166d72162de3575f950507683fac47e30f6f2c3836b71b7fbc61aa517c9c5f4"
dependencies = [
"rand",
]
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.6" version = "0.9.6"

View File

@@ -6,18 +6,16 @@ version = "0.0.0"
publish = false publish = false
[features] [features]
default = ["sys"] default = [
manual_test = []
sys = [
"dep:widestring", "dep:widestring",
"dep:windows", "dep:windows",
"dep:core-foundation", "dep:core-foundation",
"dep:security-framework", "dep:security-framework",
"dep:security-framework-sys", "dep:security-framework-sys",
"dep:zbus", "dep:zbus",
"dep:zbus_polkit", "dep:zbus_polkit"
] ]
manual_test = []
[dependencies] [dependencies]
aes = "=0.8.4" aes = "=0.8.4"
@@ -36,7 +34,6 @@ futures = "=0.3.31"
interprocess = { version = "=2.2.1", features = ["tokio"] } interprocess = { version = "=2.2.1", features = ["tokio"] }
log = "=0.4.22" log = "=0.4.22"
rand = "=0.8.5" rand = "=0.8.5"
retry = "=2.0.0"
russh-cryptovec = "=0.7.3" russh-cryptovec = "=0.7.3"
scopeguard = "=1.2.0" scopeguard = "=1.2.0"
sha2 = "=0.10.8" sha2 = "=0.10.8"

View File

@@ -3,12 +3,16 @@ use anyhow::{anyhow, Result};
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
#[cfg_attr(target_os = "linux", path = "unix.rs")] #[cfg_attr(target_os = "linux", path = "unix.rs")]
#[cfg_attr(target_os = "windows", path = "windows.rs")]
#[cfg_attr(target_os = "macos", path = "macos.rs")] #[cfg_attr(target_os = "macos", path = "macos.rs")]
#[cfg_attr(target_os = "windows", path = "windows.rs")]
mod biometric; mod biometric;
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
pub use biometric::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 sha2::{Digest, Sha256};
use crate::crypto::{self, CipherString}; use crate::crypto::{self, CipherString};

View File

@@ -1,12 +1,15 @@
use std::{ffi::c_void, str::FromStr}; use std::{
ffi::c_void,
str::FromStr,
sync::{atomic::AtomicBool, Arc},
};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine}; use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
use rand::RngCore; use rand::RngCore;
use retry::delay::Fixed;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use windows::{ use windows::{
core::{factory, h, s, HSTRING}, core::{factory, h, HSTRING},
Foundation::IAsyncOperation, Foundation::IAsyncOperation,
Security::{ Security::{
Credentials::{ Credentials::{
@@ -14,17 +17,7 @@ use windows::{
}, },
Cryptography::CryptographicBuffer, Cryptography::CryptographicBuffer,
}, },
Win32::{ Win32::{Foundation::HWND, System::WinRT::IUserConsentVerifierInterop},
Foundation::HWND,
System::WinRT::IUserConsentVerifierInterop,
UI::{
Input::KeyboardAndMouse::{
keybd_event, GetAsyncKeyState, SetFocus, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP,
VK_MENU,
},
WindowsAndMessaging::{FindWindowA, SetForegroundWindow},
},
},
}; };
use crate::{ use crate::{
@@ -32,7 +25,10 @@ use crate::{
crypto::CipherString, crypto::CipherString,
}; };
use super::{decrypt, encrypt}; use super::{
decrypt, encrypt,
windows_focus::{focus_security_prompt, set_focus},
};
/// The Windows OS implementation of the biometric trait. /// The Windows OS implementation of the biometric trait.
pub struct Biometric {} pub struct Biometric {}
@@ -103,8 +99,22 @@ impl super::BiometricTrait for Biometric {
let challenge_buffer = CryptographicBuffer::CreateFromByteArray(&challenge)?; let challenge_buffer = CryptographicBuffer::CreateFromByteArray(&challenge)?;
let async_operation = result.Credential()?.RequestSignAsync(&challenge_buffer)?; let async_operation = result.Credential()?.RequestSignAsync(&challenge_buffer)?;
focus_security_prompt()?; focus_security_prompt();
let signature = async_operation.get()?;
let done = Arc::new(AtomicBool::new(false));
let done_clone = done.clone();
let _ = std::thread::spawn(move || loop {
if !done_clone.load(std::sync::atomic::Ordering::Relaxed) {
focus_security_prompt();
std::thread::sleep(std::time::Duration::from_millis(500));
} else {
break;
}
});
let signature = async_operation.get();
done.store(true, std::sync::atomic::Ordering::Relaxed);
let signature = signature?;
if signature.Status()? != KeyCredentialStatus::Success { if signature.Status()? != KeyCredentialStatus::Success {
return Err(anyhow!("Failed to sign data")); return Err(anyhow!("Failed to sign data"));
@@ -168,57 +178,6 @@ fn random_challenge() -> [u8; 16] {
challenge challenge
} }
/// Searches for a window that looks like a security prompt and set it as focused.
///
/// Gives up after 1.5 seconds with a delay of 500ms between each try.
fn focus_security_prompt() -> Result<()> {
unsafe fn try_find_and_set_focus(
class_name: windows::core::PCSTR,
) -> retry::OperationResult<(), ()> {
let hwnd = unsafe { FindWindowA(class_name, None) };
if let Ok(hwnd) = hwnd {
set_focus(hwnd);
return retry::OperationResult::Ok(());
}
retry::OperationResult::Retry(())
}
let class_name = s!("Credential Dialog Xaml Host");
retry::retry_with_index(Fixed::from_millis(500), |current_try| {
if current_try > 3 {
return retry::OperationResult::Err(());
}
unsafe { try_find_and_set_focus(class_name) }
})
.map_err(|_| anyhow!("Failed to find security prompt"))
}
fn set_focus(window: HWND) {
let mut pressed = false;
unsafe {
// Simulate holding down Alt key to bypass windows limitations
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getasynckeystate#return-value
// The most significant bit indicates if the key is currently being pressed. This means the
// value will be negative if the key is pressed.
if GetAsyncKeyState(VK_MENU.0 as i32) >= 0 {
pressed = true;
keybd_event(VK_MENU.0 as u8, 0, KEYEVENTF_EXTENDEDKEY, 0);
}
let _ = SetForegroundWindow(window);
let _ = SetFocus(window);
if pressed {
keybd_event(
VK_MENU.0 as u8,
0,
KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP,
0,
);
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@@ -0,0 +1,28 @@
use windows::{
core::s,
Win32::{
Foundation::HWND,
UI::{
Input::KeyboardAndMouse::SetFocus,
WindowsAndMessaging::{FindWindowA, SetForegroundWindow},
},
},
};
/// Searches for a window that looks like a security prompt and set it as focused.
/// Only works when the process has permission to foreground, either by being in foreground
/// Or by being given foreground permission https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setforegroundwindow#remarks
pub fn focus_security_prompt() {
let class_name = s!("Credential Dialog Xaml Host");
let hwnd = unsafe { FindWindowA(class_name, None) };
if let Ok(hwnd) = hwnd {
set_focus(hwnd);
}
}
pub(crate) fn set_focus(window: HWND) {
unsafe {
let _ = SetForegroundWindow(window);
let _ = SetFocus(window);
}
}

View File

@@ -1,16 +1,10 @@
pub mod autofill; pub mod autofill;
#[cfg(feature = "sys")]
pub mod biometric; pub mod biometric;
#[cfg(feature = "sys")]
pub mod clipboard; pub mod clipboard;
pub mod crypto; pub mod crypto;
pub mod error; pub mod error;
pub mod ipc; pub mod ipc;
#[cfg(feature = "sys")]
pub mod password; pub mod password;
#[cfg(feature = "sys")]
pub mod powermonitor; pub mod powermonitor;
#[cfg(feature = "sys")]
pub mod process_isolation; pub mod process_isolation;
#[cfg(feature = "sys")]
pub mod ssh_agent; pub mod ssh_agent;

View File

@@ -8,7 +8,7 @@ publish = false
[dependencies] [dependencies]
anyhow = "=1.0.94" anyhow = "=1.0.94"
desktop_core = { path = "../core", default-features = false } desktop_core = { path = "../core" }
futures = "=0.3.31" futures = "=0.3.31"
log = "=0.4.22" log = "=0.4.22"
simplelog = "=0.12.2" simplelog = "=0.12.2"

View File

@@ -5,6 +5,9 @@ use futures::{FutureExt, SinkExt, StreamExt};
use log::*; use log::*;
use tokio_util::codec::LengthDelimitedCodec; use tokio_util::codec::LengthDelimitedCodec;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
embed_plist::embed_info_plist!("../../../resources/info.desktop_proxy.plist"); embed_plist::embed_info_plist!("../../../resources/info.desktop_proxy.plist");
@@ -49,6 +52,9 @@ fn init_logging(log_path: &Path, console_level: LevelFilter, file_level: LevelFi
/// ///
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() { async fn main() {
#[cfg(target_os = "windows")]
let should_foreground = windows::allow_foreground();
let sock_path = desktop_core::ipc::path("bitwarden"); let sock_path = desktop_core::ipc::path("bitwarden");
let log_path = { let log_path = {
@@ -142,6 +148,9 @@ async fn main() {
// Listen to stdin and send messages to ipc processor. // Listen to stdin and send messages to ipc processor.
msg = stdin.next() => { msg = stdin.next() => {
#[cfg(target_os = "windows")]
should_foreground.store(true, std::sync::atomic::Ordering::Relaxed);
match msg { match msg {
Some(Ok(msg)) => { Some(Ok(msg)) => {
let m = String::from_utf8(msg.to_vec()).unwrap(); let m = String::from_utf8(msg.to_vec()).unwrap();

View File

@@ -0,0 +1,23 @@
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
pub fn allow_foreground() -> Arc<AtomicBool> {
let should_foreground = Arc::new(AtomicBool::new(false));
let should_foreground_clone = should_foreground.clone();
let _ = std::thread::spawn(move || loop {
if !should_foreground_clone.load(Ordering::Relaxed) {
std::thread::sleep(std::time::Duration::from_millis(100));
continue;
}
should_foreground_clone.store(false, Ordering::Relaxed);
for _ in 0..60 {
desktop_core::biometric::windows_focus::focus_security_prompt();
std::thread::sleep(std::time::Duration::from_millis(1000));
}
});
should_foreground
}