diff --git a/apps/desktop/build.ps1 b/apps/desktop/build.ps1 index fcdb331f5d8..cbec6782882 100644 --- a/apps/desktop/build.ps1 +++ b/apps/desktop/build.ps1 @@ -14,7 +14,7 @@ $comLogFile = "C:\temp\bitwarden_com_debug.log" npm run build-native && npm run build:dev && npm run pack:win:arm64 # Backup tokens -Copy-Item -Path "$bwFolder\LocalCache\Roaming\Bitwarden\data.json" -Destination $backupDataFile +# Copy-Item -Path "$bwFolder\LocalCache\Roaming\Bitwarden\data.json" -Destination $backupDataFile # Reinstall Appx Remove-AppxPackage $package && Add-AppxPackage $appx @@ -23,5 +23,5 @@ Remove-AppxPackage $package && Add-AppxPackage $appx Remove-Item -Path $comLogFile # Restore tokens -New-Item -Type Directory -Force -Path "$bwFolder\LocalCache\Roaming\Bitwarden\" -Copy-Item -Path $backupDataFile -Destination "$bwFolder\LocalCache\Roaming\Bitwarden\data.json" +# New-Item -Type Directory -Force -Path "$bwFolder\LocalCache\Roaming\Bitwarden\" +# Copy-Item -Path $backupDataFile -Destination "$bwFolder\LocalCache\Roaming\Bitwarden\data.json" diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 4182de59382..2fc15fceb7e 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -5162,12 +5162,16 @@ name = "windows_plugin_authenticator" version = "0.0.0" dependencies = [ "ciborium", + "desktop_core", + "futures", "hex", "reqwest", "serde", "serde_json", "sha2", "tokio", + "tracing", + "tracing-subscriber", "windows 0.62.2", "windows-core 0.62.2", ] diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml index fd60c1f43a2..83bf70b9dbe 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml @@ -6,6 +6,8 @@ license = { workspace = true } publish = { workspace = true } [target.'cfg(windows)'.dependencies] +desktop_core = { path = "../core" } +futures = { workspace = true } windows = { workspace = true, features = [ "Win32_Foundation", "Win32_Security", @@ -17,6 +19,8 @@ hex = { workspace = true } reqwest = { version = "0.12", features = ["json", "blocking"] } serde_json = { workspace = true } serde = { workspace = true, features = ["derive"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } ciborium = "0.2" sha2 = "0.10" tokio = { workspace = true } diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_provider.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_provider.rs index b35533790ba..1d83df4d9db 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_provider.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_provider.rs @@ -1,7 +1,9 @@ +use windows::Win32::Foundation::S_OK; use windows::Win32::System::Com::*; use windows_core::{implement, interface, IInspectable, IUnknown, Interface, HRESULT}; use crate::assert::plugin_get_assertion; +use crate::ipc2::WindowsProviderClient; use crate::make_credential::plugin_make_credential; use crate::util::debug_log; use crate::webauthn::WEBAUTHN_CREDENTIAL_LIST; @@ -130,7 +132,9 @@ pub unsafe fn parse_credential_list(credential_list: &WEBAUTHN_CREDENTIAL_LIST) } #[implement(IPluginAuthenticator)] -pub struct PluginAuthenticatorComObject; +pub struct PluginAuthenticatorComObject { + client: WindowsProviderClient, +} #[implement(IClassFactory)] pub struct Factory; @@ -142,13 +146,17 @@ impl IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl { response: *mut WebAuthnPluginOperationResponse, ) -> HRESULT { debug_log("MakeCredential() called"); + debug_log("version2"); // Convert to legacy format for internal processing if request.is_null() || response.is_null() { debug_log("MakeCredential: Invalid request or response pointers passed"); return HRESULT(-1); } - plugin_make_credential(request, response) + match plugin_make_credential(&self.client, request, response) { + Ok(()) => S_OK, + Err(err) => err, + } } unsafe fn GetAssertion( @@ -188,7 +196,11 @@ impl IClassFactory_Impl for Factory_Impl { iid: *const windows_core::GUID, object: *mut *mut core::ffi::c_void, ) -> windows_core::Result<()> { - let unknown: IInspectable = PluginAuthenticatorComObject.into(); // TODO: IUnknown ? + debug_log("Creating COM server instance."); + debug_log("Trying to connect to Bitwarden IPC"); + let client = WindowsProviderClient::connect(); + debug_log("Connected to Bitwarden IPC"); + let unknown: IInspectable = PluginAuthenticatorComObject { client }.into(); // TODO: IUnknown ? unsafe { unknown.query(iid, object).ok() } } diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/assertion.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/assertion.rs new file mode 100644 index 00000000000..ebef42151d4 --- /dev/null +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/assertion.rs @@ -0,0 +1,57 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +use super::{BitwardenError, Callback, Position, UserVerification}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyAssertionRequest { + rp_id: String, + client_data_hash: Vec, + user_verification: UserVerification, + allowed_credentials: Vec>, + window_xy: Position, + //extension_input: Vec, TODO: Implement support for extensions +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyAssertionWithoutUserInterfaceRequest { + rp_id: String, + credential_id: Vec, + user_name: String, + user_handle: Vec, + record_identifier: Option, + client_data_hash: Vec, + user_verification: UserVerification, + window_xy: Position, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyAssertionResponse { + rp_id: String, + user_handle: Vec, + signature: Vec, + client_data_hash: Vec, + authenticator_data: Vec, + credential_id: Vec, +} + +pub trait PreparePasskeyAssertionCallback: Send + Sync { + fn on_complete(&self, credential: PasskeyAssertionResponse); + fn on_error(&self, error: BitwardenError); +} + +impl Callback for Arc { + fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error> { + let credential = serde_json::from_value(credential)?; + PreparePasskeyAssertionCallback::on_complete(self.as_ref(), credential); + Ok(()) + } + + fn error(&self, error: BitwardenError) { + PreparePasskeyAssertionCallback::on_error(self.as_ref(), error); + } +} 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 new file mode 100644 index 00000000000..6db7fe389c7 --- /dev/null +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/mod.rs @@ -0,0 +1,310 @@ +use std::{ + collections::HashMap, + error::Error, + fmt::Display, + sync::{atomic::AtomicU32, Arc, Mutex, Once}, + time::Instant, +}; + +use futures::FutureExt; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use tracing::{error, info}; +use tracing_subscriber::{ + filter::{EnvFilter, LevelFilter}, + layer::SubscriberExt, + util::SubscriberInitExt, +}; + +mod assertion; +mod registration; + +pub use assertion::{ + PasskeyAssertionRequest, PasskeyAssertionResponse, PasskeyAssertionWithoutUserInterfaceRequest, + PreparePasskeyAssertionCallback, +}; +pub use registration::{ + PasskeyRegistrationRequest, PasskeyRegistrationResponse, PreparePasskeyRegistrationCallback, +}; + +use crate::util::debug_log; + +static INIT: Once = Once::new(); + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum UserVerification { + Preferred, + Required, + Discouraged, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Position { + pub x: i32, + pub y: i32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum BitwardenError { + Internal(String), +} + +impl Display for BitwardenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Internal(msg) => write!(f, "Internal error occurred: {msg}"), + } + } +} + +impl Error for BitwardenError {} + +// TODO: These have to be named differently than the actual Uniffi traits otherwise +// the generated code will lead to ambiguous trait implementations +// These are only used internally, so it doesn't matter that much +trait Callback: Send + Sync { + fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error>; + fn error(&self, error: BitwardenError); +} + +#[derive(Debug)] +/// Store the connection status between the Windows credential provider extension +/// and the desktop application's IPC server. +pub enum ConnectionStatus { + Connected, + Disconnected, +} + +pub struct WindowsProviderClient { + to_server_send: tokio::sync::mpsc::Sender, + + // We need to keep track of the callbacks so we can call them when we receive a response + response_callbacks_counter: AtomicU32, + #[allow(clippy::type_complexity)] + response_callbacks_queue: Arc, Instant)>>>, + + // Flag to track connection status - atomic for thread safety without locks + connection_status: Arc, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +/// Store native desktop status information to use for IPC communication +/// between the application and the Windows credential provider. +pub struct NativeStatus { + key: String, + value: String, +} + +// In our callback management, 0 is a reserved sequence number indicating that a message does not have a callback. +const NO_CALLBACK_INDICATOR: u32 = 0; + +impl WindowsProviderClient { + // FIXME: Remove unwraps! They panic and terminate the whole application. + #[allow(clippy::unwrap_used)] + pub fn connect() -> Self { + debug_log("YO!"); + INIT.call_once(|| { + /* + let filter = EnvFilter::builder() + .with_default_directive(LevelFilter::DEBUG.into()) + .from_env_lossy(); + + let log_file_path = "C:\\temp\\bitwarden_windows_passkey_provider.log"; + debug_log(&format!("Trying to set up log file at {log_file_path}")); + // FIXME: Remove unwrap + let file = std::fs::File::options() + .append(true) + .open(log_file_path) + .unwrap(); + let log_file = tracing_subscriber::fmt::layer().with_writer(file); + tracing_subscriber::registry() + .with(filter) + .with(log_file) + .init(); + */ + }); + tracing::debug!("Windows COM server trying to connect to Electron IPC..."); + + let (from_server_send, mut from_server_recv) = tokio::sync::mpsc::channel(32); + let (to_server_send, to_server_recv) = tokio::sync::mpsc::channel(32); + + let client = WindowsProviderClient { + to_server_send, + response_callbacks_counter: AtomicU32::new(1), // Start at 1 since 0 is reserved for "no callback" scenarios + response_callbacks_queue: Arc::new(Mutex::new(HashMap::new())), + connection_status: Arc::new(std::sync::atomic::AtomicBool::new(false)), + }; + + let path = desktop_core::ipc::path("af"); + + let queue = client.response_callbacks_queue.clone(); + let connection_status = client.connection_status.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Can't create runtime"); + + rt.spawn( + desktop_core::ipc::client::connect(path, from_server_send, to_server_recv) + .map(|r| r.map_err(|e| e.to_string())), + ); + + rt.block_on(async move { + while let Some(message) = from_server_recv.recv().await { + match serde_json::from_str::(&message) { + Ok(SerializedMessage::Command(CommandMessage::Connected)) => { + info!("Connected to server"); + connection_status.store(true, std::sync::atomic::Ordering::Relaxed); + } + Ok(SerializedMessage::Command(CommandMessage::Disconnected)) => { + info!("Disconnected from server"); + connection_status.store(false, std::sync::atomic::Ordering::Relaxed); + } + Ok(SerializedMessage::Message { + sequence_number, + value, + }) => match queue.lock().unwrap().remove(&sequence_number) { + Some((cb, request_start_time)) => { + info!( + "Time to process request: {:?}", + request_start_time.elapsed() + ); + match value { + Ok(value) => { + if let Err(e) = cb.complete(value) { + error!(error = %e, "Error deserializing message"); + } + } + Err(e) => { + error!(error = ?e, "Error processing message"); + cb.error(e) + } + } + } + None => { + error!(sequence_number, "No callback found for sequence number") + } + }, + Err(e) => { + error!(error = %e, "Error deserializing message"); + } + }; + } + }); + }); + + client + } + + pub fn send_native_status(&self, key: String, value: String) { + let status = NativeStatus { key, value }; + self.send_message(status, None); + } + + pub fn prepare_passkey_registration( + &self, + request: PasskeyRegistrationRequest, + callback: Arc, + ) { + self.send_message(request, Some(Box::new(callback))); + } + + pub fn prepare_passkey_assertion( + &self, + request: PasskeyAssertionRequest, + callback: Arc, + ) { + self.send_message(request, Some(Box::new(callback))); + } + + pub fn prepare_passkey_assertion_without_user_interface( + &self, + request: PasskeyAssertionWithoutUserInterfaceRequest, + callback: Arc, + ) { + self.send_message(request, Some(Box::new(callback))); + } + + pub fn get_connection_status(&self) -> ConnectionStatus { + let is_connected = self + .connection_status + .load(std::sync::atomic::Ordering::Relaxed); + if is_connected { + ConnectionStatus::Connected + } else { + ConnectionStatus::Disconnected + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "command", rename_all = "camelCase")] +enum CommandMessage { + Connected, + Disconnected, +} + +#[derive(Serialize, Deserialize)] +#[serde(untagged, rename_all = "camelCase")] +enum SerializedMessage { + Command(CommandMessage), + Message { + sequence_number: u32, + value: Result, + }, +} + +impl WindowsProviderClient { + #[allow(clippy::unwrap_used)] + fn add_callback(&self, callback: Box) -> u32 { + let sequence_number = self + .response_callbacks_counter + .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + + self.response_callbacks_queue + .lock() + .expect("response callbacks queue mutex should not be poisoned") + .insert(sequence_number, (callback, Instant::now())); + + sequence_number + } + + #[allow(clippy::unwrap_used)] + fn send_message( + &self, + message: impl Serialize + DeserializeOwned, + callback: Option>, + ) { + let sequence_number = if let Some(callback) = callback { + self.add_callback(callback) + } else { + NO_CALLBACK_INDICATOR + }; + + let message = serde_json::to_string(&SerializedMessage::Message { + sequence_number, + value: Ok(serde_json::to_value(message).unwrap()), + }) + .expect("Can't serialize message"); + + if let Err(e) = self.to_server_send.blocking_send(message) { + // Make sure we remove the callback from the queue if we can't send the message + if sequence_number != NO_CALLBACK_INDICATOR { + if let Some((callback, _)) = self + .response_callbacks_queue + .lock() + .expect("response callbacks queue mutex should not be poisoned") + .remove(&sequence_number) + { + callback.error(BitwardenError::Internal(format!( + "Error sending message: {e}" + ))); + } + } + } + } +} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/registration.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/registration.rs new file mode 100644 index 00000000000..3ac6808cb49 --- /dev/null +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/registration.rs @@ -0,0 +1,44 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +use super::{BitwardenError, Callback, Position, UserVerification}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyRegistrationRequest { + pub rp_id: String, + pub user_name: String, + pub user_handle: Vec, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub supported_algorithms: Vec, + pub window_xy: Position, + pub excluded_credentials: Vec>, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyRegistrationResponse { + pub rp_id: String, + pub client_data_hash: Vec, + pub credential_id: Vec, + pub attestation_object: Vec, +} + +pub trait PreparePasskeyRegistrationCallback: Send + Sync { + fn on_complete(&self, credential: PasskeyRegistrationResponse); + fn on_error(&self, error: BitwardenError); +} + +impl Callback for Arc { + fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error> { + let credential = serde_json::from_value(credential)?; + PreparePasskeyRegistrationCallback::on_complete(self.as_ref(), credential); + Ok(()) + } + + fn error(&self, error: BitwardenError) { + PreparePasskeyRegistrationCallback::on_error(self.as_ref(), error); + } +} 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 be9c805dc25..a5a69ea384e 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs @@ -8,6 +8,7 @@ mod com_buffer; mod com_provider; mod com_registration; mod ipc; +mod ipc2; mod make_credential; mod sync; mod types; @@ -30,6 +31,7 @@ use crate::util::debug_log; /// 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? debug_log("register() called..."); let r = com_registration::initialize_com_library(); @@ -43,3 +45,6 @@ pub fn register() -> std::result::Result<(), String> { Ok(()) } + +/// This sets up IPC so the plugin can request credentials from Electron. +fn setup_ipc() {} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs index 910c165c812..37472208e29 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs @@ -2,13 +2,26 @@ use serde_json; use std::alloc::{alloc, Layout}; use std::collections::HashMap; use std::mem::ManuallyDrop; +use std::sync::mpsc::Receiver; +use std::sync::Mutex; +use std::sync::{ + mpsc::{self, Sender}, + Arc, +}; +use std::time::Duration; use std::{ptr, slice}; use windows_core::{s, HRESULT}; use crate::com_provider::{ parse_credential_list, WebAuthnPluginOperationRequest, WebAuthnPluginOperationResponse, }; -use crate::types::*; +use crate::ipc2::{ + self, BitwardenError, PasskeyAssertionRequest, PasskeyAssertionResponse, + PasskeyRegistrationRequest, PasskeyRegistrationResponse, Position, + PreparePasskeyAssertionCallback, PreparePasskeyRegistrationCallback, UserVerification, + WindowsProviderClient, +}; +use crate::types::UserVerificationRequirement; use crate::util::{debug_log, delay_load, wstr_to_string, WindowsString}; use crate::webauthn::WEBAUTHN_CREDENTIAL_LIST; @@ -317,36 +330,111 @@ unsafe fn decode_make_credential_request( /// Helper for registration requests fn send_registration_request( + ipc_client: &WindowsProviderClient, transaction_id: &str, request: &WindowsRegistrationRequest, -) -> Option { +) -> Result { debug_log(&format!("Registration request data - RP ID: {}, User ID: {} bytes, User name: {}, Client data hash: {} bytes, Algorithms: {:?}, Excluded credentials: {}", request.rpid, request.user_id.len(), request.user_name, request.client_data_hash.len(), request.supported_algorithms, request.excluded_credentials.len())); + let user_verification = match request.user_verification { + UserVerificationRequirement::Discouraged => UserVerification::Discouraged, + UserVerificationRequirement::Preferred => UserVerification::Preferred, + UserVerificationRequirement::Required => UserVerification::Required, + }; let passkey_request = PasskeyRegistrationRequest { rp_id: request.rpid.clone(), - transaction_id: transaction_id.to_string(), + // transaction_id: transaction_id.to_string(), user_handle: request.user_id.clone(), user_name: request.user_name.clone(), client_data_hash: request.client_data_hash.clone(), - user_verification: request.user_verification.clone(), + user_verification, window_xy: Position { x: 400, y: 400 }, // TODO: Get actual window position supported_algorithms: request.supported_algorithms.clone(), excluded_credentials: request.excluded_credentials.clone(), }; - match serde_json::to_string(&passkey_request) { - Ok(request_json) => { - debug_log(&format!("Sending registration request: {}", request_json)); - crate::ipc::send_passkey_request(RequestType::Registration, request_json, &request.rpid) + let request_json = serde_json::to_string(&passkey_request) + .map_err(|err| format!("Failed to serialize registration request: {err}"))?; + debug_log(&format!("Sending registration request: {}", request_json)); + let callback = Arc::new(Callback::new()); + ipc_client.prepare_passkey_registration(passkey_request, callback.clone()); + callback + .wait_for_response(Duration::from_secs(30)) + .map_err(|_| "Registration request timed out".to_string())? + .map_err(|err| err.to_string()) + + /* + { + Ok(Ok(response)) => { + tracing::debug!("Received registration response from Electron: {response:?}"); + Some(response) } - Err(e) => { - debug_log(&format!( - "ERROR: Failed to serialize registration request: {}", - e - )); + Ok(Err(err)) => { + tracing::error!("Registration request failed: {err}"); None } + Err(_) => { + tracing::error!("Timed out waiting for registration response"); + None + } + } + */ + // crate::ipc::send_passkey_request(RequestType::Registration, request_json, &request.rpid) +} + +struct Callback { + tx: Mutex>>>, + rx: Mutex>>, +} + +impl Callback { + fn new() -> Self { + let (tx, rx) = mpsc::channel(); + Self { + tx: Mutex::new(Some(tx)), + rx: Mutex::new(rx), + } + } + + fn wait_for_response( + &self, + timeout: Duration, + ) -> Result, mpsc::RecvTimeoutError> { + self.rx.lock().unwrap().recv_timeout(timeout) + } + + fn send(&self, response: Result) { + match self.tx.lock().unwrap().take() { + Some(tx) => { + if let Err(_) = tx.send(response) { + tracing::error!("Windows provider channel closed before receiving IPC response from Electron") + } + } + None => { + tracing::error!("Callback channel used before response: multi-threading issue?"); + } + } + } +} + +impl PreparePasskeyRegistrationCallback for Callback { + fn on_complete(&self, credential: PasskeyRegistrationResponse) { + self.send(Ok(credential)); + } + + fn on_error(&self, error: BitwardenError) { + self.send(Err(error)) + } +} + +impl PreparePasskeyAssertionCallback for Callback { + fn on_complete(&self, credential: PasskeyAssertionResponse) { + self.send(Ok(credential)); + } + + fn on_error(&self, error: BitwardenError) { + self.send(Err(error)) } } @@ -390,7 +478,6 @@ unsafe fn create_make_credential_response( s!("WebAuthNEncodeMakeCredentialResponse"), ) .unwrap(); - let mut authenticator_data = vec![1, 2, 3, 4]; let att_fmt = webauthn_att_obj .get("fmt") .ok_or(HRESULT(-1))? @@ -480,19 +567,20 @@ unsafe fn create_make_credential_response( /// Implementation of PluginMakeCredential moved from com_provider.rs pub unsafe fn plugin_make_credential( + ipc_client: &WindowsProviderClient, request: *const WebAuthnPluginOperationRequest, response: *mut WebAuthnPluginOperationResponse, -) -> HRESULT { +) -> Result<(), HRESULT> { debug_log("=== PluginMakeCredential() called ==="); if request.is_null() { debug_log("ERROR: NULL request pointer"); - return HRESULT(-1); + return Err(HRESULT(-1)); } if response.is_null() { debug_log("ERROR: NULL response pointer"); - return HRESULT(-1); + return Err(HRESULT(-1)); } let req = &*request; @@ -500,7 +588,7 @@ pub unsafe fn plugin_make_credential( if req.encoded_request_byte_count == 0 || req.encoded_request_pointer.is_null() { debug_log("ERROR: No encoded request data provided"); - return HRESULT(-1); + return Err(HRESULT(-1)); } let encoded_request_slice = std::slice::from_raw_parts( @@ -514,213 +602,182 @@ pub unsafe fn plugin_make_credential( )); // Try to decode the request using Windows API - match decode_make_credential_request(encoded_request_slice) { - Ok(decoded_wrapper) => { - let decoded_request = decoded_wrapper.as_ref(); - debug_log("Successfully decoded make credential request using Windows API"); + let decoded_wrapper = decode_make_credential_request(encoded_request_slice).map_err(|err| { + debug_log(&format!( + "ERROR: Failed to decode make credential request: {err}" + )); + HRESULT(-1) + })?; + let decoded_request = decoded_wrapper.as_ref(); + debug_log("Successfully decoded make credential request using Windows API"); - // Extract RP information - if decoded_request.pRpInformation.is_null() { - debug_log("ERROR: RP information is null"); - return HRESULT(-1); - } - - let rp_info = &*decoded_request.pRpInformation; - - let rpid = if rp_info.pwszId.is_null() { - debug_log("ERROR: RP ID is null"); - return HRESULT(-1); - } else { - match wstr_to_string(rp_info.pwszId) { - Ok(id) => id, - Err(e) => { - debug_log(&format!("ERROR: Failed to decode RP ID: {}", e)); - return HRESULT(-1); - } - } - }; - - // let rp_name = if rp_info.pwszName.is_null() { - // String::new() - // } else { - // wstr_to_string(rp_info.pwszName).unwrap_or_default() - // }; - - // Extract user information - if decoded_request.pUserInformation.is_null() { - debug_log("ERROR: User information is null"); - return HRESULT(-1); - } - - let user = &*decoded_request.pUserInformation; - - let user_id = if user.pbId.is_null() || user.cbId == 0 { - debug_log("ERROR: User ID is required for registration"); - return HRESULT(-1); - } else { - let id_slice = std::slice::from_raw_parts(user.pbId, user.cbId as usize); - id_slice.to_vec() - }; - - let user_name = if user.pwszName.is_null() { - debug_log("ERROR: User name is required for registration"); - return HRESULT(-1); - } else { - match wstr_to_string(user.pwszName) { - Ok(name) => name, - Err(_) => { - debug_log("ERROR: Failed to decode user name"); - return HRESULT(-1); - } - } - }; - - let user_display_name = if user.pwszDisplayName.is_null() { - None - } else { - wstr_to_string(user.pwszDisplayName).ok() - }; - - let user_info = (user_id, user_name, user_display_name); - - // Extract client data hash - let client_data_hash = if decoded_request.cbClientDataHash == 0 - || decoded_request.pbClientDataHash.is_null() - { - debug_log("ERROR: Client data hash is required for registration"); - return HRESULT(-1); - } else { - let hash_slice = std::slice::from_raw_parts( - decoded_request.pbClientDataHash, - decoded_request.cbClientDataHash as usize, - ); - hash_slice.to_vec() - }; - - // Extract supported algorithms - let supported_algorithms = if decoded_request - .WebAuthNCredentialParameters - .cCredentialParameters - > 0 - && !decoded_request - .WebAuthNCredentialParameters - .pCredentialParameters - .is_null() - { - let params_count = decoded_request - .WebAuthNCredentialParameters - .cCredentialParameters as usize; - let params_ptr = decoded_request - .WebAuthNCredentialParameters - .pCredentialParameters; - - (0..params_count) - .map(|i| unsafe { &*params_ptr.add(i) }.lAlg) - .collect() - } else { - Vec::new() - }; - - // Extract user verification requirement from authenticator options - let user_verification = if !decoded_request.pAuthenticatorOptions.is_null() { - let auth_options = &*decoded_request.pAuthenticatorOptions; - match auth_options.user_verification { - 1 => Some(UserVerificationRequirement::Required), - -1 => Some(UserVerificationRequirement::Discouraged), - 0 | _ => Some(UserVerificationRequirement::Preferred), // Default or undefined - } - } else { - None - }; - - // Extract excluded credentials from credential list - let excluded_credentials = parse_credential_list(&decoded_request.CredentialList); - if !excluded_credentials.is_empty() { - debug_log(&format!( - "Found {} excluded credentials for make credential", - excluded_credentials.len() - )); - } - - // Create Windows registration request - let registration_request = WindowsRegistrationRequest { - rpid: rpid.clone(), - user_id: user_info.0, - user_name: user_info.1, - user_display_name: user_info.2, - client_data_hash, - excluded_credentials, - user_verification: user_verification.unwrap_or_default(), - supported_algorithms, - }; - - debug_log(&format!( - "Make credential request - RP: {}, User: {}", - rpid, registration_request.user_name - )); - - // Send registration request - if let Some(passkey_response) = - send_registration_request(&transaction_id, ®istration_request) - { - debug_log(&format!( - "Registration response received: {:?}", - passkey_response - )); - - // Create proper WebAuthn response from passkey_response - match passkey_response { - PasskeyResponse::RegistrationResponse { - credential_id: _, - attestation_object, - rp_id: _, - client_data_hash: _, - } => { - debug_log("Creating WebAuthn make credential response"); - match create_make_credential_response(attestation_object) { - Ok(mut webauthn_response) => { - debug_log(&format!( - "Successfully created WebAuthn response: {webauthn_response:?}" - )); - (*response).encoded_response_byte_count = - webauthn_response.len() as u32; - (*response).encoded_response_pointer = - webauthn_response.as_mut_ptr(); - debug_log(&format!("Set pointer, returning HRESULT(0)")); - _ = ManuallyDrop::new(webauthn_response); - HRESULT(0) - } - Err(e) => { - debug_log(&format!( - "ERROR: Failed to create WebAuthn response: {}", - e - )); - HRESULT(-1) - } - } - } - PasskeyResponse::Error { message } => { - debug_log(&format!("Registration request failed: {}", message)); - HRESULT(-1) - } - _ => { - debug_log("ERROR: Unexpected response type for registration request"); - HRESULT(-1) - } - } - } else { - debug_log("ERROR: No response from registration request"); - HRESULT(-1) - } - } - Err(e) => { - debug_log(&format!( - "ERROR: Failed to decode make credential request: {}", - e - )); - HRESULT(-1) - } + // Extract RP information + if decoded_request.pRpInformation.is_null() { + debug_log("ERROR: RP information is null"); + return Err(HRESULT(-1)); } + + let rp_info = &*decoded_request.pRpInformation; + + let rpid = if rp_info.pwszId.is_null() { + debug_log("ERROR: RP ID is null"); + return Err(HRESULT(-1)); + } else { + match wstr_to_string(rp_info.pwszId) { + Ok(id) => id, + Err(e) => { + debug_log(&format!("ERROR: Failed to decode RP ID: {}", e)); + return Err(HRESULT(-1)); + } + } + }; + + // let rp_name = if rp_info.pwszName.is_null() { + // String::new() + // } else { + // wstr_to_string(rp_info.pwszName).unwrap_or_default() + // }; + + // Extract user information + if decoded_request.pUserInformation.is_null() { + debug_log("ERROR: User information is null"); + return Err(HRESULT(-1)); + } + + let user = &*decoded_request.pUserInformation; + + let user_id = if user.pbId.is_null() || user.cbId == 0 { + debug_log("ERROR: User ID is required for registration"); + return Err(HRESULT(-1)); + } else { + let id_slice = std::slice::from_raw_parts(user.pbId, user.cbId as usize); + id_slice.to_vec() + }; + + let user_name = if user.pwszName.is_null() { + debug_log("ERROR: User name is required for registration"); + return Err(HRESULT(-1)); + } else { + match wstr_to_string(user.pwszName) { + Ok(name) => name, + Err(_) => { + debug_log("ERROR: Failed to decode user name"); + return Err(HRESULT(-1)); + } + } + }; + + let user_display_name = if user.pwszDisplayName.is_null() { + None + } else { + wstr_to_string(user.pwszDisplayName).ok() + }; + + let user_info = (user_id, user_name, user_display_name); + + // Extract client data hash + let client_data_hash = + if decoded_request.cbClientDataHash == 0 || decoded_request.pbClientDataHash.is_null() { + debug_log("ERROR: Client data hash is required for registration"); + return Err(HRESULT(-1)); + } else { + let hash_slice = std::slice::from_raw_parts( + decoded_request.pbClientDataHash, + decoded_request.cbClientDataHash as usize, + ); + hash_slice.to_vec() + }; + + // Extract supported algorithms + let supported_algorithms = if decoded_request + .WebAuthNCredentialParameters + .cCredentialParameters + > 0 + && !decoded_request + .WebAuthNCredentialParameters + .pCredentialParameters + .is_null() + { + let params_count = decoded_request + .WebAuthNCredentialParameters + .cCredentialParameters as usize; + let params_ptr = decoded_request + .WebAuthNCredentialParameters + .pCredentialParameters; + + (0..params_count) + .map(|i| unsafe { &*params_ptr.add(i) }.lAlg) + .collect() + } else { + Vec::new() + }; + + // Extract user verification requirement from authenticator options + let user_verification = if !decoded_request.pAuthenticatorOptions.is_null() { + let auth_options = &*decoded_request.pAuthenticatorOptions; + match auth_options.user_verification { + 1 => Some(UserVerificationRequirement::Required), + -1 => Some(UserVerificationRequirement::Discouraged), + 0 | _ => Some(UserVerificationRequirement::Preferred), // Default or undefined + } + } else { + None + }; + + // Extract excluded credentials from credential list + let excluded_credentials = parse_credential_list(&decoded_request.CredentialList); + if !excluded_credentials.is_empty() { + debug_log(&format!( + "Found {} excluded credentials for make credential", + excluded_credentials.len() + )); + } + + // Create Windows registration request + let registration_request = WindowsRegistrationRequest { + rpid: rpid.clone(), + user_id: user_info.0, + user_name: user_info.1, + user_display_name: user_info.2, + client_data_hash, + excluded_credentials, + user_verification: user_verification.unwrap_or_default(), + supported_algorithms, + }; + + debug_log(&format!( + "Make credential request - RP: {}, User: {}", + rpid, registration_request.user_name + )); + + // Send registration request + let passkey_response = + send_registration_request(ipc_client, &transaction_id, ®istration_request).map_err( + |err| { + tracing::error!("Registration request failed: {err}"); + HRESULT(-1) + }, + )?; + debug_log(&format!( + "Registration response received: {:?}", + passkey_response + )); + + // Create proper WebAuthn response from passkey_response + debug_log("Creating WebAuthn make credential response"); + let mut webauthn_response = + create_make_credential_response(passkey_response.attestation_object).map_err(|err| { + debug_log(&format!("ERROR: Failed to create WebAuthn response: {err}")); + HRESULT(-1) + })?; + debug_log(&format!( + "Successfully created WebAuthn response: {webauthn_response:?}" + )); + (*response).encoded_response_byte_count = webauthn_response.len() as u32; + (*response).encoded_response_pointer = webauthn_response.as_mut_ptr(); + debug_log(&format!("Set pointer, returning HRESULT(0)")); + _ = ManuallyDrop::new(webauthn_response); + Ok(()) } #[cfg(test)] 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 035d9df06cf..d4ff50037bf 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/util.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/util.rs @@ -78,6 +78,7 @@ pub fn file_log(msg: &str) { } pub fn debug_log(message: &str) { + tracing::debug!(message); file_log(message) } diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index ecf4e528b43..94f3670fbd7 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -193,6 +193,7 @@ export class DesktopAutofillService implements OnDestroy { } listenIpc() { + this.logService.debug("Setting up Native -> Electron IPC Handlers") ipc.autofill.listenPasskeyRegistration(async (clientId, sequenceNumber, request, callback) => { if (!(await this.configService.getFeatureFlag(NativeCredentialSyncFeatureFlag))) { this.logService.debug( @@ -216,6 +217,7 @@ export class DesktopAutofillService implements OnDestroy { controller, ); + this.logService.debug("Sending registration response to plugin via callback"); callback(null, this.convertRegistrationResponse(request, response)); } catch (error) { this.logService.error("listenPasskeyRegistration error", error); diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts index 0959a9e9ac5..67ebadd6dd5 100644 --- a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts +++ b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts @@ -83,7 +83,7 @@ export class NativeAutofillMain { ); this.ipcServer = await autofill.IpcServer.listen( - "autofill", + "af", // RegistrationCallback (error, clientId, sequenceNumber, request) => { if (error) { diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.windows.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.windows.main.ts index 4070687a2bf..97071740901 100644 --- a/apps/desktop/src/platform/main/autofill/native-autofill.windows.main.ts +++ b/apps/desktop/src/platform/main/autofill/native-autofill.windows.main.ts @@ -28,6 +28,7 @@ export class NativeAutofillWindowsMain { "message": "Failed to register windows passkey plugin" }) } + /* void passkey_authenticator.onRequest(async (error, event) => { this.logService.info("Passkey request received:", { error, event }); @@ -58,6 +59,7 @@ export class NativeAutofillWindowsMain { }); } }); + */ } private async handleAssertionRequest(request: autofill.PasskeyAssertionRequest): Promise {