diff --git a/apps/desktop/desktop_native/autofill_provider/src/assertion.rs b/apps/desktop/desktop_native/autofill_provider/src/assertion.rs index ef792c4f562..64b5c2baac5 100644 --- a/apps/desktop/desktop_native/autofill_provider/src/assertion.rs +++ b/apps/desktop/desktop_native/autofill_provider/src/assertion.rs @@ -9,36 +9,119 @@ use crate::{BitwardenError, Callback, Position, UserVerification}; #[cfg_attr(target_os = "macos", derive(uniffi::Record))] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +/// Request to assert a credential. pub struct PasskeyAssertionRequest { + /// Relying Party ID for the request. pub rp_id: String, + + /// SHA-256 hash of the `clientDataJSON` for the assertion request. pub client_data_hash: Vec, + + /// User verification preference. pub user_verification: UserVerification, + + /// List of allowed credential IDs. pub allowed_credentials: Vec>, + + /// Coordinates of the center of the WebAuthn client's window, relative to + /// the top-left point on the screen. + /// # Operating System Differences + /// + /// ## macOS + /// Note that macOS APIs gives points relative to the bottom-left point on the + /// screen by default, so the y-coordinate will be flipped. + /// + /// ## Windows + /// On Windows, this must be logical pixels, not physical pixels. pub window_xy: Position, + #[cfg(not(target_os = "macos"))] + /// Byte string representing the native OS window handle for the WebAuthn client. + /// # Operating System Differences + /// + /// ## macOS + /// Unused. + /// + /// ## Windows + /// On Windows, this is a HWND. pub client_window_handle: Vec, + #[cfg(not(target_os = "macos"))] + /// Native context required for callbacks to the OS. Format differs on the OS. + /// # Operating System Differences + /// + /// ## macOS + /// Unused. + /// + /// ## Windows + /// On Windows, this is a base64-string representing the following data: + /// `request transaction id (GUID, 16 bytes) || SHA-256(pluginOperationRequest)` pub context: String, // pub extension_input: Vec, TODO: Implement support for extensions } +/// Request to assert a credential without user interaction. #[cfg_attr(target_os = "macos", derive(uniffi::Record))] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PasskeyAssertionWithoutUserInterfaceRequest { + /// Relying Party ID. pub rp_id: String, + + /// The allowed credential ID for the request. pub credential_id: Vec, + #[cfg(target_os = "macos")] + /// The user name for the credential that was previously given to the OS. pub user_name: String, + #[cfg(target_os = "macos")] + /// The user ID for the credential that was previously given to the OS. pub user_handle: Vec, + #[cfg(target_os = "macos")] + /// The app-specific local identifier for the credential, in our case, the + /// cipher ID. pub record_identifier: Option, + + /// SHA-256 hash of the `clientDataJSON` for the assertion request. pub client_data_hash: Vec, + + /// User verification preference. pub user_verification: UserVerification, + + /// Coordinates of the center of the WebAuthn client's window, relative to + /// the top-left point on the screen. + /// # Operating System Differences + /// + /// ## macOS + /// Note that macOS APIs gives points relative to the bottom-left point on the + /// screen by default, so the y-coordinate will be flipped. + /// + /// ## Windows + /// On Windows, this must be logical pixels, not physical pixels. pub window_xy: Position, + #[cfg(not(target_os = "macos"))] + /// Byte string representing the native OS window handle for the WebAuthn client. + /// # Operating System Differences + /// + /// ## macOS + /// Unused. + /// + /// ## Windows + /// On Windows, this is a HWND. pub client_window_handle: Vec, + + // #[cfg(not(target_os = "macos"))] + /// Native context required for callbacks to the OS. Format differs on the OS. + /// # Operating System Differences + /// + /// ## macOS + /// Unused. + /// + /// ## Windows + /// On Windows, this is `request transaction id () || SHA-256(pluginOperationRequest)`. #[cfg(not(target_os = "macos"))] pub context: String, } @@ -46,18 +129,34 @@ pub struct PasskeyAssertionWithoutUserInterfaceRequest { #[cfg_attr(target_os = "macos", derive(uniffi::Record))] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +/// Response for a passkey assertion request. pub struct PasskeyAssertionResponse { + /// Relying Party ID. pub rp_id: String, + + /// The user ID for the credential that was previously given to the OS. pub user_handle: Vec, + + /// The signature for the WebAuthn attestation response. pub signature: Vec, + + /// SHA-256 hash of the `clientDataJSON` used in the assertion. pub client_data_hash: Vec, + + /// The WebAuthn authenticator data structure. pub authenticator_data: Vec, + + /// The ID for the attested credential. pub credential_id: Vec, } #[cfg_attr(target_os = "macos", uniffi::export(with_foreign))] +/// Callback to process a response to passkey assertion request. pub trait PreparePasskeyAssertionCallback: Send + Sync { + /// Function to call if a successful response is returned. fn on_complete(&self, credential: PasskeyAssertionResponse); + + /// Function to call if an error response is returned. fn on_error(&self, error: BitwardenError); } @@ -80,6 +179,6 @@ impl PreparePasskeyAssertionCallback for TimedCallback } fn on_error(&self, error: BitwardenError) { - self.send(Err(error)) + self.send(Err(error)); } } diff --git a/apps/desktop/desktop_native/autofill_provider/src/lib.rs b/apps/desktop/desktop_native/autofill_provider/src/lib.rs index 3b948add84e..6d7e2b01a17 100644 --- a/apps/desktop/desktop_native/autofill_provider/src/lib.rs +++ b/apps/desktop/desktop_native/autofill_provider/src/lib.rs @@ -52,6 +52,7 @@ static INIT: Once = Once::new(); #[cfg_attr(target_os = "macos", derive(uniffi::Enum))] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +/// User verification preference for WebAuthn requests. pub enum UserVerification { Preferred, Required, @@ -61,6 +62,7 @@ pub enum UserVerification { #[cfg_attr(target_os = "macos", derive(uniffi::Record))] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +/// Coordinates representing a point on the screen. pub struct Position { pub x: i32, pub y: i32, @@ -100,6 +102,49 @@ pub enum ConnectionStatus { } #[cfg_attr(target_os = "macos", derive(uniffi::Object))] +/// A client to send and receive messages to the autofill service on the desktop +/// client. +/// +/// # Usage +/// +/// In order to accommodate desktop app startup delays and non-blocking +/// requirements for native providers, this initialization of the client is +/// non-blocking. When calling [`AutofillProviderClient::connect()`], the +/// connection is not established immediately, but may be established later in +/// the background or may fail to be established. +/// +/// Before calling [`AutofillProviderClient::connect()`], first check whether +/// the desktop app is running with [`AutofillProviderClient::is_available`], +/// and attempt to start it if it is not running. Then, attempt to connect, retrying as necessary. +/// Before calling any other methods, check the connection status using +/// [`AutofillProviderClient::get_connection_status()`]. +/// +/// # Example +/// +/// ```no_run +/// fn establish_connection() -> Option { +/// if !AutofillProviderClient::is_available() { +/// // Start application +/// } +/// let max_attempts = 20; +/// let delay_ms = Duration::from_millis(300); +/// +/// for attempt in 0..=max_attempts { +/// let client = AutofillProviderClient::connect(); +/// if attempt != 0 { +/// // Use whatever sleep method is appropriate +/// std::thread::sleep(delay + 100 * attempt); +/// } +/// if let ConnectionStatus::Connected = client.get_connection_status() { +/// return client; +/// } +/// }; +/// } +/// +/// if let Some(client) = establish_connection() { +/// // use client here +/// } +/// ``` pub struct AutofillProviderClient { to_server_send: tokio::sync::mpsc::Sender, @@ -127,14 +172,17 @@ const NO_CALLBACK_INDICATOR: u32 = 0; // These methods are not currently needed in macOS and/or cannot be exported via FFI impl AutofillProviderClient { + /// Whether the client is immediately available for connection. pub fn is_available() -> bool { desktop_core::ipc::path("af").exists() } + /// Request the desktop client's lock status. pub fn get_lock_status(&self, callback: Arc) { self.send_message(LockStatusRequest {}, Some(Box::new(callback))); } + /// Requests details about the desktop client's native window. pub fn get_window_handle(&self, callback: Arc) { self.send_message( WindowHandleQueryRequest::default(), @@ -146,6 +194,9 @@ impl AutofillProviderClient { #[cfg_attr(target_os = "macos", uniffi::export)] impl AutofillProviderClient { #[cfg_attr(target_os = "macos", uniffi::constructor)] + /// Asynchronously initiates a connection to the autofill service on the desktop client. + /// + /// See documentation at the top-level of [this struct][AutofillProviderClient] for usage information. pub fn connect() -> Self { #[cfg(target_os = "macos")] INIT.call_once(|| { @@ -163,7 +214,7 @@ impl AutofillProviderClient { .init(); }); - tracing::debug!("Autofill provider attempting to connect to Electron IPC..."); + tracing::trace!("Autofill provider attempting 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); @@ -220,12 +271,12 @@ impl AutofillProviderClient { } Err(e) => { error!(error = ?e, "Error processing message"); - cb.error(e) + cb.error(e); } } } None => { - error!(sequence_number, "No callback found for sequence number") + error!(sequence_number, "No callback found for sequence number"); } }, Err(e) => { @@ -239,11 +290,13 @@ impl AutofillProviderClient { client } + /// Send a one-way key-value message to the desktop client. pub fn send_native_status(&self, key: String, value: String) { let status = NativeStatus { key, value }; self.send_message(status, None); } + /// Send a request to create a new passkey to the desktop client. pub fn prepare_passkey_registration( &self, request: PasskeyRegistrationRequest, @@ -252,6 +305,7 @@ impl AutofillProviderClient { self.send_message(request, Some(Box::new(callback))); } + /// Send a request to assert a passkey to the desktop client. pub fn prepare_passkey_assertion( &self, request: PasskeyAssertionRequest, @@ -260,6 +314,7 @@ impl AutofillProviderClient { self.send_message(request, Some(Box::new(callback))); } + /// Send a request to assert a passkey, without prompting the user, to the desktop client. pub fn prepare_passkey_assertion_without_user_interface( &self, request: PasskeyAssertionWithoutUserInterfaceRequest, @@ -268,6 +323,7 @@ impl AutofillProviderClient { self.send_message(request, Some(Box::new(callback))); } + /// Return the status this client's connection to the desktop client. pub fn get_connection_status(&self) -> ConnectionStatus { let is_connected = self .connection_status @@ -322,24 +378,7 @@ impl AutofillProviderClient { NO_CALLBACK_INDICATOR }; - fn inner( - sequence_number: u32, - message: impl Serialize + DeserializeOwned, - tx: &tokio::sync::mpsc::Sender, - ) -> Result<(), String> { - let value = serde_json::to_value(message) - .map_err(|err| format!("Could not represent message as JSON: {err}"))?; - let message = SerializedMessage::Message { - sequence_number, - value: Ok(value), - }; - let json = serde_json::to_string(&message) - .map_err(|err| format!("Could not serialize message as JSON: {err}"))?; - tx.blocking_send(json) - .map_err(|err| format!("Error sending message: {err}"))?; - Ok(()) - } - if let Err(e) = inner(sequence_number, message, &self.to_server_send) { + if let Err(e) = send_message_helper(sequence_number, message, &self.to_server_send) { // 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 @@ -357,7 +396,27 @@ impl AutofillProviderClient { } } +// Wrapped in Result<> to allow using ? for clarity. +fn send_message_helper( + sequence_number: u32, + message: impl Serialize + DeserializeOwned, + tx: &tokio::sync::mpsc::Sender, +) -> Result<(), String> { + let value = serde_json::to_value(message) + .map_err(|err| format!("Could not represent message as JSON: {err}"))?; + let message = SerializedMessage::Message { + sequence_number, + value: Ok(value), + }; + let json = serde_json::to_string(&message) + .map_err(|err| format!("Could not serialize message as JSON: {err}"))?; + tx.blocking_send(json) + .map_err(|err| format!("Error sending message: {err}"))?; + Ok(()) +} + #[derive(Debug)] +/// Types of errors for callbacks. pub enum CallbackError { Timeout, Cancelled, @@ -375,6 +434,7 @@ impl std::error::Error for CallbackError {} type CallbackResponse = Result; +/// An implementation of a callback handler that can take a deadline. pub struct TimedCallback { tx: Arc>>>>, rx: Arc>>>, @@ -387,6 +447,7 @@ impl Default for TimedCallback { } impl TimedCallback { + /// Instantiates a new callback handler. pub fn new() -> Self { let (tx, rx) = mpsc::channel(); Self { @@ -395,6 +456,19 @@ impl TimedCallback { } } + /// Block the current thread until either a response is received, or the + /// specified timeout has passed. + /// + /// # Usage + /// ``` + /// 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()), + /// } + /// ``` pub fn wait_for_response( &self, timeout: Duration, @@ -432,7 +506,7 @@ impl TimedCallback { err } Ok(err @ Err(CallbackError::Timeout)) => { - tracing::debug!("Request timed out, dropping."); + tracing::warn!("Request timed out, dropping."); err } Err(RecvTimeoutError::Timeout) => Err(CallbackError::Timeout), @@ -444,7 +518,7 @@ impl TimedCallback { match self.tx.lock().expect("not poisoned").take() { Some(tx) => { if tx.send(response).is_err() { - tracing::error!("Windows provider channel closed before receiving IPC response from Electron") + tracing::error!("Windows provider channel closed before receiving IPC response from Electron"); } } None => { @@ -460,6 +534,8 @@ impl PreparePasskeyRegistrationCallback for TimedCallback { } } +/// Callback to process a response to a lock status request. pub trait GetLockStatusCallback: Send + Sync { + /// Function to call if a successful response is returned. fn on_complete(&self, response: LockStatusResponse); + + /// Function to call if an error response is returned. fn on_error(&self, error: BitwardenError); } @@ -36,6 +43,6 @@ impl GetLockStatusCallback for TimedCallback { } fn on_error(&self, error: BitwardenError) { - self.send(Err(error)) + self.send(Err(error)); } } diff --git a/apps/desktop/desktop_native/autofill_provider/src/registration.rs b/apps/desktop/desktop_native/autofill_provider/src/registration.rs index 6aba0ce4fbe..ff1c4930d35 100644 --- a/apps/desktop/desktop_native/autofill_provider/src/registration.rs +++ b/apps/desktop/desktop_native/autofill_provider/src/registration.rs @@ -7,34 +7,90 @@ use crate::{BitwardenError, Callback, Position, UserVerification}; #[cfg_attr(target_os = "macos", derive(uniffi::Record))] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +/// Request to create a credential. pub struct PasskeyRegistrationRequest { + /// Relying Party ID for the request. pub rp_id: String, + + /// The user name for the credential that was previously given to the OS. pub user_name: String, + + /// The user ID for the credential that was previously given to the OS. pub user_handle: Vec, + + /// SHA-256 hash of the `clientDataJSON` for the registration request. pub client_data_hash: Vec, + + /// User verification preference. pub user_verification: UserVerification, + + /// Supported key algorithms in COSE format. pub supported_algorithms: Vec, + + /// Coordinates of the center of the WebAuthn client's window, relative to + /// the top-left point on the screen. + /// # Operating System Differences + /// + /// ## macOS + /// Note that macOS APIs gives points relative to the bottom-left point on the + /// screen by default, so the y-coordinate will be flipped. + /// + /// ## Windows + /// On Windows, this must be logical pixels, not physical pixels. pub window_xy: Position, + + /// List of excluded credential IDs. pub excluded_credentials: Vec>, + #[cfg(not(target_os = "macos"))] + /// Byte string representing the native OS window handle for the WebAuthn client. + /// # Operating System Differences + /// + /// ## macOS + /// Unused. + /// + /// ## Windows + /// On Windows, this is a HWND. pub client_window_handle: Vec, + #[cfg(not(target_os = "macos"))] + /// Native context required for callbacks to the OS. Format differs by OS. + /// # Operating System Differences + /// + /// ## macOS + /// Unused. + /// + /// ## Windows + /// On Windows, this is a base64-string representing the following data: + /// `request transaction id (GUID, 16 bytes) || SHA-256(pluginOperationRequest)` pub context: String, } #[cfg_attr(target_os = "macos", derive(uniffi::Record))] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +/// Response for a passkey registration request. pub struct PasskeyRegistrationResponse { + /// Relying Party ID. pub rp_id: String, + + /// SHA-256 hash of the `clientDataJSON` used in the registration. pub client_data_hash: Vec, + + /// The ID for the created credential. pub credential_id: Vec, + + /// WebAuthn attestation object. pub attestation_object: Vec, } #[cfg_attr(target_os = "macos", uniffi::export(with_foreign))] +/// Callback to process a response to passkey registration request. pub trait PreparePasskeyRegistrationCallback: Send + Sync { + /// Function to call if a successful response is returned. fn on_complete(&self, credential: PasskeyRegistrationResponse); + + /// Function to call if an error response is returned. fn on_error(&self, error: BitwardenError); } diff --git a/apps/desktop/desktop_native/autofill_provider/src/util.rs b/apps/desktop/desktop_native/autofill_provider/src/util.rs index 8880b7b71b8..68a398c895c 100644 --- a/apps/desktop/desktop_native/autofill_provider/src/util.rs +++ b/apps/desktop/desktop_native/autofill_provider/src/util.rs @@ -7,7 +7,7 @@ pub(crate) fn deserialize_b64<'de, D: Deserializer<'de>>( } struct Base64Visitor; -impl<'de> Visitor<'de> for Base64Visitor { +impl Visitor<'_> for Base64Visitor { type Value = Vec; fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { diff --git a/apps/desktop/desktop_native/autofill_provider/src/window_handle_query.rs b/apps/desktop/desktop_native/autofill_provider/src/window_handle_query.rs index 5f5bc526edc..676610f56cc 100644 --- a/apps/desktop/desktop_native/autofill_provider/src/window_handle_query.rs +++ b/apps/desktop/desktop_native/autofill_provider/src/window_handle_query.rs @@ -5,16 +5,38 @@ use serde::{Deserialize, Serialize}; use crate::{BitwardenError, Callback, TimedCallback}; #[derive(Debug, Default, Serialize, Deserialize)] +/// Request to get the window handle of the desktop client. pub(super) struct WindowHandleQueryRequest { #[serde(rename = "windowHandle")] + /// base64-encoded byte string representing native window handle. + /// # Operating System Differences + /// + /// ## macOS + /// Unused. + /// + /// ## Windows + /// On Windows, this is an HWND. window_handle: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] +/// Response to window handle request. pub struct WindowHandleQueryResponse { + /// Whether the desktop client is currently visible. pub is_visible: bool, + + /// Whether the desktop client is currently focused. pub is_focused: bool, + + /// Byte string representing the native OS window handle for the desktop client. + /// # Operating System Differences + /// + /// ## macOS + /// Unused. + /// + /// ## Windows + /// On Windows, this is a HWND. #[serde(deserialize_with = "crate::util::deserialize_b64")] pub handle: Vec, } @@ -31,8 +53,12 @@ impl Callback for Arc { } } +/// Callback to process a response to a window handle query request. pub trait GetWindowHandleQueryCallback: Send + Sync { + /// Function to call if a successful response is returned. fn on_complete(&self, response: WindowHandleQueryResponse); + + /// Function to call if an error response is returned. fn on_error(&self, error: BitwardenError); } @@ -42,6 +68,6 @@ impl GetWindowHandleQueryCallback for TimedCallback { } fn on_error(&self, error: BitwardenError) { - self.send(Err(error)) + self.send(Err(error)); } }