diff --git a/apps/desktop/custom-appx-manifest.xml b/apps/desktop/custom-appx-manifest.xml index 1524f1cbba6..8703b91ffcd 100644 --- a/apps/desktop/custom-appx-manifest.xml +++ b/apps/desktop/custom-appx-manifest.xml @@ -114,7 +114,8 @@ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/re - diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 973f5748776..401f0d28809 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -766,6 +766,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -874,6 +883,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + [[package]] name = "desktop_core" version = "0.0.0" @@ -2095,6 +2113,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -2534,6 +2558,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -3326,6 +3356,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -3450,6 +3511,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +dependencies = [ + "crossbeam-channel", + "thiserror 2.0.12", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.30" @@ -4294,6 +4367,8 @@ dependencies = [ "serde_json", "tokio", "tracing", + "tracing-appender", + "tracing-subscriber", "win_webauthn", "windows 0.61.3", "windows-core 0.61.2", diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/build.js index e1385eb1ead..b08d9e4572c 100644 --- a/apps/desktop/desktop_native/build.js +++ b/apps/desktop/desktop_native/build.js @@ -46,6 +46,25 @@ function buildProxyBin(target, release = true) { } } +function buildWindowsPluginBin(target, release = true) { + const isWindowsTarget = (target && target.includes("windows")) || process.platform === "win32"; + if (!isWindowsTarget) { + console.log("Not compiling for Winodws, skipping Windows plugin build"); + return; + } + const targetArg = target ? `--target ${target}` : ""; + const releaseArg = release ? "--release" : ""; + const xwin = process.platform !== "win32" ? "xwin" : ""; + child_process.execSync(`cargo ${xwin} build --bin windows_plugin_authenticator ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "windows_plugin_authenticator")}); + + if (target) { + // Copy the resulting binary to the dist folder + const targetFolder = release ? "release" : "debug"; + const { nodeArch, platform } = rustTargetsMap[target]; + fs.copyFileSync(path.join(__dirname, "target", target, targetFolder, `windows_plugin_authenticator.exe`), path.join(__dirname, "dist", `windows_plugin_authenticator.${platform}-${nodeArch}.exe`)); + } +} + function buildImporterBinaries(target, release = true) { // These binaries are only built for Windows, so we can skip them on other platforms if (process.platform !== "win32") { @@ -84,20 +103,24 @@ function installTarget(target) { } if (!crossPlatform && !target) { + const isRelease = mode === "release"; console.log(`Building native modules in ${mode} mode for the native architecture`); - buildNapiModule(false, mode === "release"); - buildProxyBin(false, mode === "release"); - buildImporterBinaries(false, mode === "release"); + buildNapiModule(false, isRelease); + buildWindowsPluginBin(null, isRelease); + buildProxyBin(false, isRelease); + buildImporterBinaries(false, isRelease); buildProcessIsolation(); return; } if (target) { console.log(`Building for target: ${target} in ${mode} mode`); + const isRelease = mode === "release"; installTarget(target); - buildNapiModule(target, mode === "release"); - buildProxyBin(target, mode === "release"); - buildImporterBinaries(false, mode === "release"); + buildNapiModule(target, isRelease); + buildWindowsPluginBin(target, isRelease); + buildProxyBin(target, isRelease); + buildImporterBinaries(false, isRelease); buildProcessIsolation(); return; } @@ -115,6 +138,7 @@ if (process.platform === "linux") { platformTargets.forEach(([target, _]) => { installTarget(target); buildNapiModule(target); + buildWindowsPluginBin(target, isRelease); buildProxyBin(target); buildImporterBinaries(target); buildProcessIsolation(); diff --git a/apps/desktop/desktop_native/win_webauthn/src/plugin/mod.rs b/apps/desktop/desktop_native/win_webauthn/src/plugin/mod.rs index cc4c53c38ff..25aeed62599 100644 --- a/apps/desktop/desktop_native/win_webauthn/src/plugin/mod.rs +++ b/apps/desktop/desktop_native/win_webauthn/src/plugin/mod.rs @@ -64,7 +64,7 @@ impl WebAuthnPlugin { /// The handler should be an instance of your type that implements PluginAuthenticator. /// The same instance will be shared across all COM calls. /// - /// This only needs to be called on installation of your application. + /// This only needs to be called at the start of your application. pub fn register_server(&self, handler: T) -> Result<(), WinWebAuthnError> where T: PluginAuthenticator + Send + Sync + 'static, diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml index 4de416bb4f1..c3f2d41b771 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml @@ -9,6 +9,7 @@ publish = { workspace = true } desktop_core = { path = "../core" } futures = { workspace = true } windows = { workspace = true, features = [ + "System", "Win32_Foundation", "Win32_Security", "Win32_System_Com", @@ -19,10 +20,12 @@ windows-core = { workspace = true } serde_json = { workspace = true } serde = { workspace = true, features = ["derive"] } tracing = { workspace = true } +tracing-appender = "0.2.4" +tracing-subscriber = { workspace = true } ciborium = "0.2" tokio = { workspace = true } base64 = { workspace = true } win_webauthn = { path = "../win_webauthn" } [lints] -workspace = true +workspace = true \ No newline at end of file diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/mod.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/mod.rs index d1e3860c520..422984dbc82 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/mod.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/mod.rs @@ -307,6 +307,16 @@ pub enum CallbackError { Cancelled, } +impl Display for CallbackError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Timeout => f.write_str("Callback timed out"), + Self::Cancelled => f.write_str("Callback cancelled"), + } + } +} +impl std::error::Error for CallbackError {} + pub struct TimedCallback { tx: Arc>>>>, rx: Arc>>>, diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs index 977c95dbb71..d824a765770 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs @@ -1,77 +1,19 @@ -#![cfg(target_os = "windows")] -#![allow(non_snake_case)] -#![allow(non_camel_case_types)] +use std::collections::HashSet; -// New modular structure -mod assert; -mod ipc2; -mod make_credential; -mod types; -mod util; - -use std::{ - collections::{HashMap, HashSet}, - mem::MaybeUninit, - sync::{ - mpsc::{self, Sender}, - Arc, Mutex, - }, - time::Duration, -}; - -use base64::engine::{general_purpose::STANDARD, Engine as _}; use win_webauthn::{ - plugin::{ - PluginAddAuthenticatorOptions, PluginAuthenticator, PluginCancelOperationRequest, - PluginGetAssertionRequest, PluginLockStatus, PluginMakeCredentialRequest, WebAuthnPlugin, - }, + plugin::{PluginAddAuthenticatorOptions, WebAuthnPlugin}, AuthenticatorInfo, CtapVersion, PublicKeyCredentialParameters, }; -use windows::{ - core::GUID, - Win32::{ - Foundation::HWND, - System::Threading::{AttachThreadInput, GetCurrentThreadId}, - UI::WindowsAndMessaging::{ - AllowSetForegroundWindow, BringWindowToTop, GetForegroundWindow, - GetWindowThreadProcessId, - }, - }, -}; -use crate::ipc2::{ConnectionStatus, LockStatusResponse, TimedCallback, WindowsProviderClient}; +pub const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop"; +pub const RPID: &str = "bitwarden.com"; +pub const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062"; +pub const AAGUID: &str = "d548826e-79b4-db40-a3d8-11116f7e8349"; +pub const LOGO_SVG: &str = r##""##; -// Re-export main functionality -pub use types::UserVerificationRequirement; - -const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop"; -const RPID: &str = "bitwarden.com"; -const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062"; -const AAGUID: &str = "d548826e-79b4-db40-a3d8-11116f7e8349"; -const LOGO_SVG: &str = r##""##; - -/// Handles initialization and registration for the Bitwarden desktop app as a -/// For now, also adds the authenticator -pub fn register() -> std::result::Result<(), String> { - // TODO: Can we spawn a new named thread for debugging? +pub fn register() -> Result<(), String> { tracing::debug!("register() called..."); - - let r = WebAuthnPlugin::initialize(); - tracing::debug!( - "Initialized the com library with WebAuthnPlugin::initialize(): {:?}", - r - ); - let clsid = CLSID.try_into().expect("valid GUID string"); - let plugin = WebAuthnPlugin::new(clsid); - - let r = plugin.register_server(BitwardenPluginAuthenticator { - client: Mutex::new(None), - callbacks: Arc::new(Mutex::new(HashMap::new())), - }); - tracing::debug!("Registered the com library: {:?}", r); - - tracing::debug!("Parsing authenticator options"); let aaguid = AAGUID .try_into() .map_err(|err| format!("Invalid AAGUID `{AAGUID}`: {err}"))?; @@ -102,170 +44,5 @@ pub fn register() -> std::result::Result<(), String> { }; let response = WebAuthnPlugin::add_authenticator(options); tracing::debug!("Added the authenticator: {response:?}"); - Ok(()) } - -struct BitwardenPluginAuthenticator { - /// Client to communicate with desktop app over IPC. - client: Mutex>>, - - /// Map of transaction IDs to cancellation tokens - callbacks: Arc>>>, -} - -impl BitwardenPluginAuthenticator { - fn get_client(&self) -> Arc { - tracing::debug!("Connecting to client via IPC"); - let mut client = self.client.lock().unwrap(); - match client.as_ref().map(|c| (c, c.get_connection_status())) { - Some((_, ConnectionStatus::Disconnected)) | None => { - tracing::debug!("Connecting to desktop app"); - let c = WindowsProviderClient::connect(); - tracing::debug!("Connected to client via IPC successfully"); - _ = client.insert(Arc::new(c)); - } - _ => {} - }; - client.as_ref().unwrap().clone() - } -} - -impl PluginAuthenticator for BitwardenPluginAuthenticator { - fn make_credential( - &self, - request: PluginMakeCredentialRequest, - ) -> Result, Box> { - tracing::debug!("Received MakeCredential: {request:?}"); - let client = self.get_client(); - - let plugin_window = get_window_details(&client)?; - unsafe { - let dw_current_thread = GetCurrentThreadId(); - let dw_fg_thread = GetWindowThreadProcessId(GetForegroundWindow(), None); - let result = AttachThreadInput(dw_current_thread, dw_fg_thread, true); - tracing::debug!("AttachThreadInput() - attach? {result:?}"); - let result = BringWindowToTop(plugin_window.handle); - tracing::debug!("BringWindowToTop? {result:?}"); - let result = AttachThreadInput(dw_current_thread, dw_fg_thread, false); - tracing::debug!("AttachThreadInput() - detach? {result:?}"); - }; - let (cancel_tx, cancel_rx) = mpsc::channel(); - let transaction_id = request.transaction_id; - self.callbacks - .lock() - .expect("not poisoned") - .insert(transaction_id, cancel_tx); - let client_hwnd = request.window_handle; - let response = make_credential::make_credential(&client, request, cancel_rx); - self.callbacks - .lock() - .expect("not poisoned") - .remove(&transaction_id); - response - } - - fn get_assertion( - &self, - request: PluginGetAssertionRequest, - ) -> Result, Box> { - tracing::debug!("Received GetAssertion: {request:?}"); - let client = self.get_client(); - - let is_unlocked = get_lock_status(&client).map_or(false, |response| response.is_unlocked); - // Don't mess with the window unless we're going to need it: if the - // vault is locked or if we need to show credential selection dialog. - let needs_ui = !is_unlocked || request.allow_credentials().cCredentials != 1; - if needs_ui { - unsafe { - let plugin_window = get_window_details(&client)?; - let dw_current_thread = GetCurrentThreadId(); - let dw_fg_thread = GetWindowThreadProcessId(GetForegroundWindow(), None); - let result = AttachThreadInput(dw_current_thread, dw_fg_thread, true); - tracing::debug!("AttachThreadInput() - attach? {result:?}"); - let result = BringWindowToTop(plugin_window.handle); - tracing::debug!("BringWindowToTop? {result:?}"); - let result = AttachThreadInput(dw_current_thread, dw_fg_thread, false); - tracing::debug!("AttachThreadInput() - detach? {result:?}"); - }; - } - let (cancel_tx, cancel_rx) = mpsc::channel(); - let transaction_id = request.transaction_id; - self.callbacks - .lock() - .expect("not poisoned") - .insert(transaction_id, cancel_tx); - let response = assert::get_assertion(&client, request, cancel_rx); - self.callbacks - .lock() - .expect("not poisoned") - .remove(&transaction_id); - response - } - - fn cancel_operation( - &self, - request: PluginCancelOperationRequest, - ) -> Result<(), Box> { - let transaction_id = request.transaction_id(); - tracing::debug!(?transaction_id, "Received CancelOperation"); - - if let Some(cancellation_token) = self - .callbacks - .lock() - .expect("not poisoned") - .get(&request.transaction_id()) - { - _ = cancellation_token.send(()); - let client = self.get_client(); - let context = STANDARD.encode(transaction_id.to_u128().to_le_bytes().to_vec()); - tracing::debug!("Sending cancel operation for context: {context}"); - client.send_native_status("cancel-operation".to_string(), context); - } - Ok(()) - } - - fn lock_status(&self) -> Result> { - // If the IPC pipe is not open, then the client is not open and must be locked/logged out. - if !WindowsProviderClient::is_available() { - return Ok(PluginLockStatus::PluginLocked); - } - get_lock_status(&self.get_client()) - .map(|response| { - if response.is_unlocked { - PluginLockStatus::PluginUnlocked - } else { - PluginLockStatus::PluginLocked - } - }) - .map_err(|err| err.into()) - } -} - -fn get_lock_status(client: &WindowsProviderClient) -> Result { - let callback = Arc::new(TimedCallback::new()); - client.get_lock_status(callback.clone()); - match callback.wait_for_response(Duration::from_secs(3), None) { - Ok(Ok(response)) => Ok(response), - Ok(Err(err)) => Err(format!("GetLockStatus() call failed: {err}").into()), - Err(_) => Err(format!("GetLockStatus() call timed out").into()), - } -} - -fn get_window_details(client: &WindowsProviderClient) -> Result { - tracing::debug!("Get Window Handle!"); - let window_handle_callback = Arc::new(TimedCallback::new()); - client.get_window_handle(window_handle_callback.clone()); - let response = window_handle_callback - .wait_for_response(Duration::from_secs(3), None) - .unwrap() - .unwrap(); - tracing::debug!("Got Window Handle: {response:?}"); - response.try_into() -} - -struct WindowDetails { - is_visible: bool, - is_focused: bool, - handle: HWND, -} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/main.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/main.rs new file mode 100644 index 00000000000..f95d38e5abe --- /dev/null +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/main.rs @@ -0,0 +1,402 @@ +#![cfg(target_os = "windows")] +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] +#![windows_subsystem = "windows"] + +// New modular structure +mod assert; +mod ipc2; +mod make_credential; +mod types; +mod util; + +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, + sync::{ + mpsc::{self, Sender}, + Arc, Mutex, + }, + thread, + time::Duration, +}; + +use base64::engine::{general_purpose::STANDARD, Engine as _}; +use tracing_appender::rolling::{RollingFileAppender, Rotation}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; +use win_webauthn::{ + plugin::{ + PluginAddAuthenticatorOptions, PluginAuthenticator, PluginCancelOperationRequest, + PluginGetAssertionRequest, PluginLockStatus, PluginMakeCredentialRequest, WebAuthnPlugin, + }, + AuthenticatorInfo, CtapVersion, PublicKeyCredentialParameters, +}; +use windows::{ + core::GUID, + Foundation::Uri, + System::Launcher, + Win32::{ + Foundation::HWND, + System::Threading::{AttachThreadInput, GetCurrentThreadId}, + UI::WindowsAndMessaging::{ + BringWindowToTop, DispatchMessageA, GetForegroundWindow, GetMessageA, + GetWindowThreadProcessId, TranslateMessage, + }, + }, +}; +use windows_core::HSTRING; +use windows_plugin_authenticator::{AAGUID, AUTHENTICATOR_NAME, CLSID, LOGO_SVG, RPID}; + +use crate::ipc2::{ConnectionStatus, LockStatusResponse, TimedCallback, WindowsProviderClient}; + +// Re-export main functionality +pub use types::UserVerificationRequirement; + +/// Handles initialization and registration for the Bitwarden desktop app as a +/// For now, also adds the authenticator +fn main() -> Result<(), Box> { + // Set the custom panic hook + let default_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + default_hook(panic_info); // Call the default hook to print the panic message + + // Only pause if not running in a debugger, etc. + // On Windows, if the process is a console app, stdin/stdout might work differently + // when launched from Explorer vs a terminal. + + println!("\nProgram panicked! Press Enter to exit..."); + std::io::stdin() + .read_line(&mut String::new()) + .expect("Failed to read line"); + })); + + // the log level hierarchy is determined by: + // - if RUST_LOG is detected at runtime + // - if RUST_LOG is provided at compile time + // - default to INFO + let filter = EnvFilter::builder() + .with_default_directive( + option_env!("RUST_LOG") + .unwrap_or("info") + .parse() + .expect("should provide valid log level at compile time."), + ) + // parse directives from the RUST_LOG environment variable, + // overriding the default directive for matching targets. + .from_env_lossy(); + + let app_data_path = std::env::var("BITWARDEN_APPDATA_DIR") + .or_else(|_| std::env::var("PORTABLE_EXECUTABLE_DIR")) + .map_or_else( + |_| { + [ + &std::env::var("APPDATA").expect("%APPDATA% to be defined"), + "Bitwarden", + ] + .iter() + .collect() + }, + PathBuf::from, + ); + + let file_appender = RollingFileAppender::builder() + .rotation(Rotation::NEVER) + .filename_prefix("passkey_plugin") + .filename_suffix("log") + .build(app_data_path)?; // TODO: should we allow continuing if we can't log? + let (writer, _guard) = tracing_appender::non_blocking(file_appender); + + // With the `tracing-log` feature enabled for the `tracing_subscriber`, + // the registry below will initialize a log compatibility layer, which allows + // the subscriber to consume log::Records as though they were tracing Events. + // https://docs.rs/tracing-subscriber/latest/tracing_subscriber/util/trait.SubscriberInitExt.html#method.init + let log_file_layer = tracing_subscriber::fmt::layer() + .with_writer(writer) + .with_ansi(false); + tracing_subscriber::registry() + .with(filter) + .with(log_file_layer) + .try_init()?; + let args: Vec = std::env::args().collect(); + tracing::debug!("Launched with arguments: {args:?}"); + let command = args.get(1).map(|s| s.as_str()); + match command { + Some("add") => add_authenticator()?, + Some("serve") => run_server()?, + Some(invalid) => { + tracing::error!( + "Invalid command argument passed: {invalid}. Specify one of [add, serve]" + ); + return Err(format!( + "No command argument passed: {invalid}. Specify one of [add, serve]" + ))?; + } + None => { + tracing::error!("No command argument passed. Specify one of [add, serve]"); + return Err("No command argument passed. Specify one of [add, serve]")?; + } + }; + tracing::debug!("Starting loop"); + + loop { + let mut msg_ptr = std::mem::MaybeUninit::uninit(); + unsafe { + GetMessageA(msg_ptr.as_mut_ptr(), None, 0, 0) + .ok() + .inspect_err(|err| { + tracing::error!("Received error while waiting for message: {err}") + })?; + tracing::debug!("Received message, dispatching"); + let msg = msg_ptr.assume_init_ref(); + let result = TranslateMessage(msg); + tracing::debug!("Message translated? {result:?}"); + let result = DispatchMessageA(msg); + tracing::debug!("Received result from message handler: {result:?}"); + } + } +} + +fn add_authenticator() -> Result<(), String> { + tracing::debug!("register() called..."); + let clsid = CLSID.try_into().expect("valid GUID string"); + let aaguid = AAGUID + .try_into() + .map_err(|err| format!("Invalid AAGUID `{AAGUID}`: {err}"))?; + let options = PluginAddAuthenticatorOptions { + authenticator_name: AUTHENTICATOR_NAME.to_string(), + clsid, + rp_id: Some(RPID.to_string()), + light_theme_logo_svg: Some(LOGO_SVG.to_string()), + dark_theme_logo_svg: Some(LOGO_SVG.to_string()), + authenticator_info: AuthenticatorInfo { + versions: HashSet::from([CtapVersion::Fido2_0, CtapVersion::Fido2_1]), + aaguid: aaguid, + options: Some(HashSet::from([ + "rk".to_string(), + "up".to_string(), + "uv".to_string(), + ])), + transports: Some(HashSet::from([ + "internal".to_string(), + "hybrid".to_string(), + ])), + algorithms: Some(vec![PublicKeyCredentialParameters { + alg: -7, + typ: "public-key".to_string(), + }]), + }, + supported_rp_ids: None, + }; + let response = WebAuthnPlugin::add_authenticator(options); + tracing::debug!("Added the authenticator: {response:?}"); + Ok(()) +} + +fn run_server() -> Result<(), String> { + tracing::debug!("Setting up COM server"); + let r = WebAuthnPlugin::initialize(); + tracing::debug!( + "Initialized the com library with WebAuthnPlugin::initialize(): {:?}", + r + ); + + let clsid = CLSID.try_into().expect("valid GUID string"); + let plugin = WebAuthnPlugin::new(clsid); + + let r = plugin.register_server(BitwardenPluginAuthenticator { + client: Mutex::new(None), + callbacks: Arc::new(Mutex::new(HashMap::new())), + }); + tracing::debug!("Registered the com library: {:?}", r); + Ok(()) +} + +struct BitwardenPluginAuthenticator { + /// Client to communicate with desktop app over IPC. + client: Mutex>>, + + /// Map of transaction IDs to cancellation tokens + callbacks: Arc>>>, +} + +impl BitwardenPluginAuthenticator { + fn get_client(&self) -> Result, String> { + // 20 * 200ms = 4 seconds. + for i in 1..=20 { + tracing::debug!("Connecting to client via IPC, attempt {i}"); + let mut client = self.client.lock().unwrap(); + match client.as_ref().map(|c| (c, c.get_connection_status())) { + Some((_, ConnectionStatus::Disconnected)) | None => { + tracing::debug!("Connecting to desktop app"); + // Attempt to launch, and retry for IPC availability in a loop. + if !WindowsProviderClient::is_available() { + if i == 1 { + let uri = Uri::CreateUri(&HSTRING::from("bitwarden://webauthn")) + .expect("valid URI"); + _ = Launcher::LaunchUriAsync(&uri); + } + let wait_time = Duration::from_millis(200); + tracing::debug!( + "Launching main client, trying again to connect to IPC in {wait_time:?}" + ); + thread::sleep(wait_time); + continue; + } + let c = WindowsProviderClient::connect(); + // This isn't actually connected yet, but it should be soon since we tested that the named pipe is available. + // The plugin IPC client will attempt to wait for + // the main application's named pipe to become available in another thread. + tracing::debug!( + "Initiated IPC connection attempt. The connection should resolve later." + ); + _ = client.insert(Arc::new(c)); + } + _ => {} + }; + return Ok(client.as_ref().unwrap().clone()); + } + Err("Exhausted retries to connect to IPC".to_string()) + } +} + +impl PluginAuthenticator for BitwardenPluginAuthenticator { + fn make_credential( + &self, + request: PluginMakeCredentialRequest, + ) -> Result, Box> { + tracing::debug!("Received MakeCredential: {request:?}"); + let client = self.get_client()?; + + let plugin_window = get_window_details(&client)?; + unsafe { + let dw_current_thread = GetCurrentThreadId(); + let dw_fg_thread = GetWindowThreadProcessId(GetForegroundWindow(), None); + let result = AttachThreadInput(dw_current_thread, dw_fg_thread, true); + tracing::debug!("AttachThreadInput() - attach? {result:?}"); + let result = BringWindowToTop(plugin_window.handle); + tracing::debug!("BringWindowToTop? {result:?}"); + let result = AttachThreadInput(dw_current_thread, dw_fg_thread, false); + tracing::debug!("AttachThreadInput() - detach? {result:?}"); + }; + let (cancel_tx, cancel_rx) = mpsc::channel(); + let transaction_id = request.transaction_id; + self.callbacks + .lock() + .expect("not poisoned") + .insert(transaction_id, cancel_tx); + let response = make_credential::make_credential(&client, request, cancel_rx); + self.callbacks + .lock() + .expect("not poisoned") + .remove(&transaction_id); + response + } + + fn get_assertion( + &self, + request: PluginGetAssertionRequest, + ) -> Result, Box> { + tracing::debug!("Received GetAssertion: {request:?}"); + let client = self.get_client()?; + + let is_unlocked = get_lock_status(&client).map_or(false, |response| response.is_unlocked); + // Don't mess with the window unless we're going to need it: if the + // vault is locked or if we need to show credential selection dialog. + let needs_ui = !is_unlocked || request.allow_credentials().cCredentials != 1; + if needs_ui { + unsafe { + let plugin_window = get_window_details(&client)?; + let dw_current_thread = GetCurrentThreadId(); + let dw_fg_thread = GetWindowThreadProcessId(GetForegroundWindow(), None); + let result = AttachThreadInput(dw_current_thread, dw_fg_thread, true); + tracing::debug!("AttachThreadInput() - attach? {result:?}"); + let result = BringWindowToTop(plugin_window.handle); + tracing::debug!("BringWindowToTop? {result:?}"); + let result = AttachThreadInput(dw_current_thread, dw_fg_thread, false); + tracing::debug!("AttachThreadInput() - detach? {result:?}"); + }; + } + let (cancel_tx, cancel_rx) = mpsc::channel(); + let transaction_id = request.transaction_id; + self.callbacks + .lock() + .expect("not poisoned") + .insert(transaction_id, cancel_tx); + let response = assert::get_assertion(&client, request, cancel_rx); + self.callbacks + .lock() + .expect("not poisoned") + .remove(&transaction_id); + response + } + + fn cancel_operation( + &self, + request: PluginCancelOperationRequest, + ) -> Result<(), Box> { + let transaction_id = request.transaction_id(); + tracing::debug!(?transaction_id, "Received CancelOperation"); + + if let Some(cancellation_token) = self + .callbacks + .lock() + .expect("not poisoned") + .get(&request.transaction_id()) + { + _ = cancellation_token.send(()); + let client = self.get_client()?; + let context = STANDARD.encode(transaction_id.to_u128().to_le_bytes().to_vec()); + tracing::debug!("Sending cancel operation for context: {context}"); + client.send_native_status("cancel-operation".to_string(), context); + } + Ok(()) + } + + fn lock_status(&self) -> Result> { + // If the IPC pipe is not open, then the client is not open and must be locked/logged out. + if !WindowsProviderClient::is_available() { + return Ok(PluginLockStatus::PluginLocked); + } + let client = self.get_client()?; + get_lock_status(&client) + .map(|response| { + if response.is_unlocked { + PluginLockStatus::PluginUnlocked + } else { + PluginLockStatus::PluginLocked + } + }) + .map_err(|err| err.into()) + } +} + +fn get_lock_status(client: &WindowsProviderClient) -> Result { + let callback = Arc::new(TimedCallback::new()); + client.get_lock_status(callback.clone()); + match callback.wait_for_response(Duration::from_secs(3), None) { + Ok(Ok(response)) => Ok(response), + Ok(Err(err)) => Err(format!("GetLockStatus() call failed: {err}").into()), + Err(_) => Err(format!("GetLockStatus() call timed out").into()), + } +} + +fn get_window_details(client: &WindowsProviderClient) -> Result { + tracing::debug!("Attempting to retrieve window handle"); + let window_handle_callback = Arc::new(TimedCallback::new()); + client.get_window_handle(window_handle_callback.clone()); + let callback_response = window_handle_callback + .wait_for_response(Duration::from_secs(30), None) + .map_err(|err| format!("Callback failed waiting for a window handle: {err}"))?; + let response = callback_response + .map_err(|err| format!("Failed to get window details: {err}"))? + .try_into(); + tracing::debug!("Got Window Handle: {response:?}"); + response +} + +#[derive(Debug)] +struct WindowDetails { + is_visible: bool, + is_focused: bool, + handle: HWND, +} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/util.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/util.rs index 297cdcf496c..426e9ff783e 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/util.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/util.rs @@ -26,8 +26,14 @@ impl HwndExt for HWND { (window.bottom + window.top) / 2, ); + tracing::debug!("Coordinates for {:?}: {center:?}", *self); + // when running as a separate process, we're not DPI aware, so the pixels are logical pixels + return Ok(center); + /* // Convert from physical to logical pixels + tracing::debug!("Getting DPI for {:?}", *self); let dpi = GetDpiForWindow(*self); + tracing::debug!("DPI: {dpi}"); if dpi == BASE_DPI { return Ok(center); } @@ -38,6 +44,7 @@ impl HwndExt for HWND { ); Ok((scaled_center.0 as i32, scaled_center.1 as i32)) + */ } } } diff --git a/apps/desktop/electron-builder.beta.json b/apps/desktop/electron-builder.beta.json index 2673dd213a3..aae4a1a7823 100644 --- a/apps/desktop/electron-builder.beta.json +++ b/apps/desktop/electron-builder.beta.json @@ -33,12 +33,16 @@ }, "extraFiles": [ { - "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", + "from": "desktop_native/dist/desktop_proxy.win32-${arch}.exe", "to": "desktop_proxy.exe" }, { - "from": "desktop_native/dist/bitwarden_chromium_import_helper.${platform}-${arch}.exe", + "from": "desktop_native/dist/bitwarden_chromium_import_helper.win32-${arch}.exe", "to": "bitwarden_chromium_import_helper.exe" + }, + { + "from": "desktop_native/dist/windows_plugin_authenticator.win32-${arch}.exe", + "to": "bitwarden_plugin_authenticator.exe" } ] }, diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 14302b9634c..ba602651bbc 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -96,12 +96,20 @@ }, "extraFiles": [ { - "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", + "from": "desktop_native/dist/desktop_proxy.win32-${arch}.exe", "to": "desktop_proxy.exe" }, { - "from": "desktop_native/dist/bitwarden_chromium_import_helper.${platform}-${arch}.exe", + "from": "desktop_native/dist/bitwarden_chromium_import_helper.win32-${arch}.exe", "to": "bitwarden_chromium_import_helper.exe" + }, + { + "from": "desktop_native/dist/windows_plugin_authenticator.win32-${arch}.exe", + "to": "bitwarden_plugin_authenticator.exe" + }, + { + "from": "desktop_native/dist/windows_plugin_authenticator.win32-${arch}.pdb", + "to": "bitwarden_plugin_authenticator.pdb" } ] },