From a5ccb5e25d8ce9e5f0dcc9841256c3f3507976e0 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 19 Nov 2025 23:57:36 -0600 Subject: [PATCH] Define a framework for implementing plugin authenticator methods. --- .../windows_plugin_authenticator/src/lib.rs | 67 +++++- .../src/win_webauthn/com.rs | 221 +++++++++++++++++- .../src/win_webauthn/mod.rs | 16 +- 3 files changed, 288 insertions(+), 16 deletions(-) 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 9aa975f08dc..fd4d0cc40ea 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs @@ -13,14 +13,21 @@ mod util; mod webauthn; mod win_webauthn; -use std::collections::HashSet; +use std::{collections::HashSet, sync::Arc, time::Duration}; // Re-export main functionality pub use types::UserVerificationRequirement; use win_webauthn::{PluginAddAuthenticatorOptions, WebAuthnPlugin}; -use crate::win_webauthn::{AuthenticatorInfo, CtapVersion, PublicKeyCredentialParameters}; +use crate::{ + ipc2::{ConnectionStatus, TimedCallback, WindowsProviderClient}, + win_webauthn::{ + AuthenticatorInfo, CtapVersion, PluginAuthenticator, PluginCancelOperationRequest, + PluginGetAssertionRequest, PluginLockStatus, PluginMakeCredentialRequest, + PublicKeyCredentialParameters, + }, +}; const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop"; const RPID: &str = "bitwarden.com"; @@ -42,7 +49,8 @@ pub fn register() -> std::result::Result<(), String> { let clsid = CLSID.try_into().expect("valid GUID string"); let plugin = WebAuthnPlugin::new(clsid); - let r = plugin.register_server(); + + let r = plugin.register_server(BitwardenPluginAuthenticator); tracing::debug!("Registered the com library: {:?}", r); tracing::debug!("Parsing authenticator options"); @@ -79,3 +87,56 @@ pub fn register() -> std::result::Result<(), String> { Ok(()) } + +struct BitwardenPluginAuthenticator; + +impl BitwardenPluginAuthenticator { + fn get_client(&self) -> WindowsProviderClient { + tracing::debug!("Connecting to client via IPC"); + let client = WindowsProviderClient::connect(); + tracing::debug!("Connected to client via IPC successfully"); + client + } +} + +impl PluginAuthenticator for BitwardenPluginAuthenticator { + fn make_credential( + &self, + request: PluginMakeCredentialRequest, + ) -> Result, Box> { + tracing::debug!("Received MakeCredential: {request:?}"); + Err(format!("MakeCredential not implemented").into()) + } + + fn get_assertion( + &self, + request: PluginGetAssertionRequest, + ) -> Result, Box> { + tracing::debug!("Received GetAssertion: {request:?}"); + Err(format!("GetAssertion not implemented").into()) + } + + fn cancel_operation( + &self, + request: PluginCancelOperationRequest, + ) -> Result<(), Box> { + Ok(()) + } + + fn lock_status(&self) -> Result> { + let callback = Arc::new(TimedCallback::new()); + let client = self.get_client(); + client.get_lock_status(callback.clone()); + match callback.wait_for_response(Duration::from_secs(3)) { + Ok(Ok(response)) => { + if response.is_unlocked { + Ok(PluginLockStatus::PluginUnlocked) + } else { + Ok(PluginLockStatus::PluginLocked) + } + } + Ok(Err(err)) => Err(format!("GetLockStatus() call failed: {err}").into()), + Err(_) => Err(format!("GetLockStatus() call timed out").into()), + } + } +} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/com.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/com.rs index 7a180c4f300..82bfe09ccaa 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/com.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/com.rs @@ -1,14 +1,32 @@ //! Functions for interacting with Windows COM. -use std::ptr; +use std::{ + alloc, + error::Error, + mem::MaybeUninit, + ptr::{self, NonNull}, + sync::{Arc, OnceLock}, +}; use windows::{ - core::{implement, ComObjectInterface, IUnknown, GUID}, - Win32::System::Com::*, + core::{implement, interface, ComObjectInterface, IUnknown, GUID, HRESULT}, + Win32::{ + Foundation::{E_FAIL, E_INVALIDARG, S_OK}, + System::Com::*, + }, +}; +use windows_core::{IInspectable, Interface}; + +use crate::win_webauthn::types::{ + PluginCancelOperationRequest, PluginGetAssertionRequest, PluginLockStatus, + PluginMakeCredentialRequest, WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST, + WEBAUTHN_PLUGIN_OPERATION_REQUEST, WEBAUTHN_PLUGIN_OPERATION_RESPONSE, }; use super::{ErrorKind, WinWebAuthnError}; +static HANDLER: OnceLock> = OnceLock::new(); + #[implement(IClassFactory)] pub struct Factory; @@ -16,21 +34,204 @@ impl IClassFactory_Impl for Factory_Impl { fn CreateInstance( &self, _outer: windows::core::Ref, - _iid: *const windows::core::GUID, - _object: *mut *mut core::ffi::c_void, + iid: *const windows::core::GUID, + object: *mut *mut core::ffi::c_void, ) -> windows::core::Result<()> { - unimplemented!() + let handler = match HANDLER.get() { + Some(handler) => handler, + None => { + tracing::error!("Cannot create COM class object instance because the handler is not initialized. register_server() must be called before starting the COM server."); + return Err(E_FAIL.into()); + } + }.clone(); + let unknown: IInspectable = PluginAuthenticatorComObject { handler }.into(); + unsafe { unknown.query(iid, object).ok() } } fn LockServer(&self, _lock: windows::core::BOOL) -> windows::core::Result<()> { - unimplemented!() + // TODO: Implement lock server + Ok(()) + } +} + +pub trait PluginAuthenticator { + /// Process a request to create a new credential. + /// + /// Returns a [CTAP authenticatorMakeCredential response structure](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#authenticatormakecredential-response-structure). + fn make_credential( + &self, + request: PluginMakeCredentialRequest, + ) -> Result, Box>; + + /// Process a request to assert a credential. + /// + /// Returns a [CTAP authenticatorGetAssertion response structure](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#authenticatorgetassertion-response-structure). + fn get_assertion(&self, request: PluginGetAssertionRequest) -> Result, Box>; + + /// Cancel an ongoing operation. + fn cancel_operation(&self, request: PluginCancelOperationRequest) + -> Result<(), Box>; + + /// Retrieve lock status. + fn lock_status(&self) -> Result>; +} + +// IPluginAuthenticator interface +#[interface("d26bcf6f-b54c-43ff-9f06-d5bf148625f7")] +pub unsafe trait IPluginAuthenticator: windows::core::IUnknown { + fn MakeCredential( + &self, + request: *const WEBAUTHN_PLUGIN_OPERATION_REQUEST, + response: *mut WEBAUTHN_PLUGIN_OPERATION_RESPONSE, + ) -> HRESULT; + fn GetAssertion( + &self, + request: *const WEBAUTHN_PLUGIN_OPERATION_REQUEST, + response: *mut WEBAUTHN_PLUGIN_OPERATION_RESPONSE, + ) -> HRESULT; + fn CancelOperation(&self, request: *const WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST) -> HRESULT; + fn GetLockStatus(&self, lock_status: *mut PluginLockStatus) -> HRESULT; +} + +#[implement(IPluginAuthenticator)] +struct PluginAuthenticatorComObject { + handler: Arc, +} + +impl IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl { + unsafe fn MakeCredential( + &self, + request: *const WEBAUTHN_PLUGIN_OPERATION_REQUEST, + response: *mut WEBAUTHN_PLUGIN_OPERATION_RESPONSE, + ) -> HRESULT { + tracing::debug!("MakeCredential called"); + // Convert to legacy format for internal processing + if request.is_null() || response.is_null() { + tracing::debug!("MakeCredential: Invalid request or response pointers passed"); + return HRESULT(-1); + } + // TODO: verify request signature + return HRESULT(-1); + + /* + match self.handler.make_credential(request) { + Ok(response) => { + // todo DECODE + tracing::debug!("MakeCredential completed successfully"); + S_OK + } + Err(err) => { + tracing::error!("MakeCredential failed: {err}"); + HRESULT(-1) + } + } + */ + } + + unsafe fn GetAssertion( + &self, + request: *const WEBAUTHN_PLUGIN_OPERATION_REQUEST, + response: *mut WEBAUTHN_PLUGIN_OPERATION_RESPONSE, + ) -> HRESULT { + tracing::debug!("GetAssertion called"); + if request.is_null() || response.is_null() { + tracing::warn!("GetAssertion called with invalid arguments"); + return E_INVALIDARG; + } + // TODO: verify request signature + let assertion_request: PluginGetAssertionRequest = + match NonNull::new(request as *mut WEBAUTHN_PLUGIN_OPERATION_REQUEST) + .map(PluginGetAssertionRequest::try_from) + { + Some(Ok(assertion_request)) => assertion_request, + Some(Err(err)) => { + tracing::error!("Could not deserialize GetAssertion request: {err}"); + return E_FAIL; + } + None => { + tracing::warn!("GetOperation received null request"); + return E_INVALIDARG; + } + }; + match self.handler.get_assertion(assertion_request) { + Ok(assertion_response) => { + let response = &mut *response; + response.cbEncodedResponse = assertion_response.len() as u32; + std::ptr::copy_nonoverlapping( + assertion_response.as_ptr(), + response.pbEncodedResponse, + assertion_response.len(), + ); + tracing::error!("GetAssertion completed successfully"); + S_OK + } + Err(err) => { + tracing::error!("GetAssertion failed: {err}"); + E_FAIL + } + } + } + + unsafe fn CancelOperation( + &self, + request: *const WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST, + ) -> HRESULT { + tracing::debug!("CancelOperation called"); + let request = match NonNull::new(request as *mut WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST) { + Some(request) => request, + None => { + tracing::warn!("Received null CancelOperation request"); + return E_INVALIDARG; + } + }; + + match self.handler.cancel_operation(request.into()) { + Ok(()) => { + tracing::error!("CancelOperation completed successfully"); + S_OK + } + Err(err) => { + tracing::error!("CancelOperation failed: {err}"); + E_FAIL + } + } + } + + unsafe fn GetLockStatus(&self, lock_status: *mut PluginLockStatus) -> HRESULT { + tracing::debug!( + "GetLockStatus() called ", + std::process::id(), + std::thread::current().id() + ); + if lock_status.is_null() { + return HRESULT(-2147024809); // E_INVALIDARG + } + + match self.handler.lock_status() { + Ok(status) => { + tracing::debug!("GetLockStatus received {status:?}"); + *lock_status = status; + S_OK + } + Err(err) => { + tracing::error!("GetLockStatus failed: {err}"); + E_FAIL + } + } } } /// Registers the plugin authenticator COM library with Windows. -pub(super) fn register_server(clsid: &GUID) -> Result<(), WinWebAuthnError> { - static FACTORY: windows::core::StaticComObject = - crate::com_provider::Factory.into_static(); +pub(super) fn register_server(clsid: &GUID, handler: T) -> Result<(), WinWebAuthnError> +where + T: PluginAuthenticator + Send + Sync + 'static, +{ + // Store the handler as a static so it can be initialized + HANDLER.set(Arc::new(handler)).map_err(|_| { + WinWebAuthnError::new(ErrorKind::WindowsInternal, "Handler already initialized") + })?; + + static FACTORY: windows::core::StaticComObject = Factory.into_static(); unsafe { CoRegisterClassObject( ptr::from_ref(clsid), diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/mod.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/mod.rs index 879342ca01f..1bc650b20a6 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/mod.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/mod.rs @@ -7,9 +7,13 @@ use std::{error::Error, fmt::Display, ptr::NonNull}; use windows::core::GUID; pub use types::{ - AuthenticatorInfo, CtapVersion, PluginAddAuthenticatorOptions, PublicKeyCredentialParameters, + AuthenticatorInfo, CtapVersion, PluginAddAuthenticatorOptions, PluginCancelOperationRequest, + PluginGetAssertionRequest, PluginLockStatus, PluginMakeCredentialRequest, + PublicKeyCredentialParameters, }; +pub use com::PluginAuthenticator; + use crate::win_webauthn::{ types::{ webauthn_plugin_add_authenticator, PluginAddAuthenticatorResponse, @@ -59,9 +63,15 @@ impl WebAuthnPlugin { /// Registers a COM server with Windows. /// + /// 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. - pub fn register_server(&self) -> Result<(), WinWebAuthnError> { - com::register_server(&self.clsid.0) + pub fn register_server(&self, handler: T) -> Result<(), WinWebAuthnError> + where + T: PluginAuthenticator + Send + Sync + 'static, + { + com::register_server(&self.clsid.0, handler) } /// Initializes the COM library for use on the calling thread,