1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-31 00:33:33 +00:00

Extract Windows plugin to a separate executable

This commit is contained in:
Isaiah Inuwa
2025-12-03 15:31:17 -06:00
parent dc4acd672d
commit 4eb6c40ca7
11 changed files with 555 additions and 244 deletions

View File

@@ -114,7 +114,8 @@ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/re
</uap:Extension>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable='${executable}'
<com:ExeServer Executable='app\bitwarden_plugin_authenticator.exe'
Arguments="serve"
DisplayName="Bitwarden Passkey Manager">
<com:Class Id="0f7dc5d9-69ce-4652-8572-6877fd695062"
DisplayName="Bitwarden Passkey Manager" />

View File

@@ -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",

View File

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

View File

@@ -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<T>(&self, handler: T) -> Result<(), WinWebAuthnError>
where
T: PluginAuthenticator + Send + Sync + 'static,

View File

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

View File

@@ -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<T> {
tx: Arc<Mutex<Option<Sender<Result<T, BitwardenError>>>>>,
rx: Arc<Mutex<Receiver<Result<T, BitwardenError>>>>,

View File

@@ -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##"<svg version="1.1" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg"><path fill="#175ddc" d="M300 253.125C300 279.023 279.023 300 253.125 300H46.875C20.9766 300 0 279.023 0 253.125V46.875C0 20.9766 20.9766 0 46.875 0H253.125C279.023 0 300 20.9766 300 46.875V253.125Z"/><path fill="#fff" d="M243.105 37.6758C241.201 35.7715 238.945 34.834 236.367 34.834H63.6328C61.0254 34.834 58.7988 35.7715 56.8945 37.6758C54.9902 39.5801 54.0527 41.8359 54.0527 44.4141V159.58C54.0527 168.164 55.7227 176.689 59.0625 185.156C62.4023 193.594 66.5625 201.094 71.5137 207.656C76.4648 214.189 82.3535 220.576 89.209 226.787C96.0645 232.998 102.393 238.125 108.164 242.227C113.965 246.328 120 250.195 126.299 253.857C132.598 257.52 137.08 259.98 139.717 261.27C142.354 262.559 144.492 263.584 146.074 264.258C147.275 264.844 148.564 265.166 149.971 265.166C151.377 265.166 152.666 264.873 153.867 264.258C155.479 263.555 157.588 262.559 160.254 261.27C162.891 259.98 167.373 257.49 173.672 253.857C179.971 250.195 186.006 246.328 191.807 242.227C197.607 238.125 203.936 232.969 210.791 226.787C217.646 220.576 223.535 214.219 228.486 207.656C233.438 201.094 237.568 193.623 240.938 185.156C244.277 176.719 245.947 168.193 245.947 159.58V44.4434C245.977 41.8359 245.01 39.5801 243.105 37.6758ZM220.84 160.664C220.84 202.354 150 238.271 150 238.271V59.502H220.84C220.84 59.502 220.84 118.975 220.84 160.664Z"/></svg>"##;
// 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##"<svg version="1.1" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg"><path fill="#175ddc" d="M300 253.125C300 279.023 279.023 300 253.125 300H46.875C20.9766 300 0 279.023 0 253.125V46.875C0 20.9766 20.9766 0 46.875 0H253.125C279.023 0 300 20.9766 300 46.875V253.125Z"/><path fill="#fff" d="M243.105 37.6758C241.201 35.7715 238.945 34.834 236.367 34.834H63.6328C61.0254 34.834 58.7988 35.7715 56.8945 37.6758C54.9902 39.5801 54.0527 41.8359 54.0527 44.4141V159.58C54.0527 168.164 55.7227 176.689 59.0625 185.156C62.4023 193.594 66.5625 201.094 71.5137 207.656C76.4648 214.189 82.3535 220.576 89.209 226.787C96.0645 232.998 102.393 238.125 108.164 242.227C113.965 246.328 120 250.195 126.299 253.857C132.598 257.52 137.08 259.98 139.717 261.27C142.354 262.559 144.492 263.584 146.074 264.258C147.275 264.844 148.564 265.166 149.971 265.166C151.377 265.166 152.666 264.873 153.867 264.258C155.479 263.555 157.588 262.559 160.254 261.27C162.891 259.98 167.373 257.49 173.672 253.857C179.971 250.195 186.006 246.328 191.807 242.227C197.607 238.125 203.936 232.969 210.791 226.787C217.646 220.576 223.535 214.219 228.486 207.656C233.438 201.094 237.568 193.623 240.938 185.156C244.277 176.719 245.947 168.193 245.947 159.58V44.4434C245.977 41.8359 245.01 39.5801 243.105 37.6758ZM220.84 160.664C220.84 202.354 150 238.271 150 238.271V59.502H220.84C220.84 59.502 220.84 118.975 220.84 160.664Z"/></svg>"##;
/// 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<Option<Arc<WindowsProviderClient>>>,
/// Map of transaction IDs to cancellation tokens
callbacks: Arc<Mutex<HashMap<GUID, Sender<()>>>>,
}
impl BitwardenPluginAuthenticator {
fn get_client(&self) -> Arc<WindowsProviderClient> {
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<Vec<u8>, Box<dyn std::error::Error>> {
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<Vec<u8>, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<PluginLockStatus, Box<dyn std::error::Error>> {
// 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<LockStatusResponse, String> {
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<WindowDetails, String> {
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,
}

View File

@@ -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<dyn std::error::Error>> {
// 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<String> = 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<Option<Arc<WindowsProviderClient>>>,
/// Map of transaction IDs to cancellation tokens
callbacks: Arc<Mutex<HashMap<GUID, Sender<()>>>>,
}
impl BitwardenPluginAuthenticator {
fn get_client(&self) -> Result<Arc<WindowsProviderClient>, 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<Vec<u8>, Box<dyn std::error::Error>> {
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<Vec<u8>, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<PluginLockStatus, Box<dyn std::error::Error>> {
// 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<LockStatusResponse, String> {
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<WindowDetails, String> {
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,
}

View File

@@ -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))
*/
}
}
}

View File

@@ -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"
}
]
},

View File

@@ -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"
}
]
},