From ff9402804aa7ea3bc416b909a0dfd8b1ec654141 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 5 Nov 2025 15:03:39 -0600 Subject: [PATCH] Update windows_plugin_authenticator to stable interface --- .../src/com_provider.rs | 173 +++------ .../src/com_registration.rs | 80 ++-- .../windows_plugin_authenticator/src/sync.rs | 107 ++--- .../src/webauthn.rs | 367 +++++++++++++----- 4 files changed, 400 insertions(+), 327 deletions(-) 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 11b7b26b798..2d6024722d1 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 @@ -37,13 +37,13 @@ pub struct ExperimentalWebAuthnPluginOperationRequest { pub encoded_request_pointer: *mut u8, } -/// Used when creating and asserting credentials with EXPERIMENTAL2 interface. -/// Header File Name: _EXPERIMENTAL2_WEBAUTHN_PLUGIN_OPERATION_REQUEST -/// Header File Usage: EXPERIMENTAL_MakeCredential() -/// EXPERIMENTAL_GetAssertion() +/// Used when creating and asserting credentials with stable interface. +/// Header File Name: _WEBAUTHN_PLUGIN_OPERATION_REQUEST +/// Header File Usage: MakeCredential() +/// GetAssertion() #[repr(C)] #[derive(Debug, Copy, Clone)] -pub struct Experimental2WebAuthnPluginOperationRequest { +pub struct WebAuthnPluginOperationRequest { pub window_handle: windows::Win32::Foundation::HWND, pub transaction_id: windows_core::GUID, pub request_signature_byte_count: u32, @@ -53,74 +53,45 @@ pub struct Experimental2WebAuthnPluginOperationRequest { pub encoded_request_pointer: *mut u8, } - /// Used as a response when creating and asserting credentials. -/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE -/// Header File Usage: EXPERIMENTAL_PluginMakeCredential() -/// EXPERIMENTAL_PluginGetAssertion() +/// Header File Name: _WEBAUTHN_PLUGIN_OPERATION_RESPONSE +/// Header File Usage: MakeCredential() +/// GetAssertion() #[repr(C)] #[derive(Debug, Copy, Clone)] -pub struct ExperimentalWebAuthnPluginOperationResponse { +pub struct WebAuthnPluginOperationResponse { pub encoded_response_byte_count: u32, pub encoded_response_pointer: *mut u8, } /// Used to cancel an operation. -/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST -/// Header File Usage: EXPERIMENTAL_PluginCancelOperation() +/// Header File Name: _WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST +/// Header File Usage: CancelOperation() #[repr(C)] #[derive(Debug, Copy, Clone)] -pub struct ExperimentalWebAuthnPluginCancelOperationRequest { +pub struct WebAuthnPluginCancelOperationRequest { pub transaction_id: windows_core::GUID, pub request_signature_byte_count: u32, pub request_signature_pointer: *mut u8, } -#[interface("e6466e9a-b2f3-47c5-b88d-89bc14a8d998")] -pub unsafe trait EXPERIMENTAL_IPluginAuthenticator: windows_core::IUnknown { - fn EXPERIMENTAL_PluginMakeCredential( - &self, - request: *const ExperimentalWebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, - ) -> HRESULT; - fn EXPERIMENTAL_PluginGetAssertion( - &self, - request: *const ExperimentalWebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, - ) -> HRESULT; - fn EXPERIMENTAL_PluginCancelOperation( - &self, - request: *const ExperimentalWebAuthnPluginCancelOperationRequest, - ) -> HRESULT; - fn EXPERIMENTAL_GetLockStatus( - &self, - lock_status: *mut PluginLockStatus, - ) -> HRESULT; -} - +// Stable IPluginAuthenticator interface #[interface("d26bcf6f-b54c-43ff-9f06-d5bf148625f7")] -pub unsafe trait EXPERIMENTAL2_IPluginAuthenticator: windows_core::IUnknown { - fn EXPERIMENTAL_MakeCredential( +pub unsafe trait IPluginAuthenticator: windows_core::IUnknown { + fn MakeCredential( &self, - request: *const Experimental2WebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, + request: *const WebAuthnPluginOperationRequest, + response: *mut WebAuthnPluginOperationResponse, ) -> HRESULT; - fn EXPERIMENTAL_GetAssertion( + fn GetAssertion( &self, - request: *const Experimental2WebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, - ) -> HRESULT; - fn EXPERIMENTAL_CancelOperation( - &self, - request: *const ExperimentalWebAuthnPluginCancelOperationRequest, - ) -> HRESULT; - fn EXPERIMENTAL_GetLockStatus( - &self, - lock_status: *mut PluginLockStatus, + request: *const WebAuthnPluginOperationRequest, + response: *mut WebAuthnPluginOperationResponse, ) -> HRESULT; + fn CancelOperation(&self, request: *const WebAuthnPluginCancelOperationRequest) -> HRESULT; + fn GetLockStatus(&self, lock_status: *mut PluginLockStatus) -> HRESULT; } - pub unsafe fn parse_credential_list(credential_list: &WEBAUTHN_CREDENTIAL_LIST) -> Vec> { let mut allowed_credentials = Vec::new(); @@ -174,57 +145,20 @@ pub unsafe fn parse_credential_list(credential_list: &WEBAUTHN_CREDENTIAL_LIST) allowed_credentials } -#[implement(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL2_IPluginAuthenticator)] +#[implement(IPluginAuthenticator)] pub struct PluginAuthenticatorComObject; #[implement(IClassFactory)] pub struct Factory; -impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl { - unsafe fn EXPERIMENTAL_PluginMakeCredential( +impl IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl { + unsafe fn MakeCredential( &self, - request: *const ExperimentalWebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, + request: *const WebAuthnPluginOperationRequest, + response: *mut WebAuthnPluginOperationResponse, ) -> HRESULT { - experimental_plugin_make_credential(request, response) - } - - unsafe fn EXPERIMENTAL_PluginGetAssertion( - &self, - request: *const ExperimentalWebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, - ) -> HRESULT { - experimental_plugin_get_assertion(request, response) - } - - unsafe fn EXPERIMENTAL_PluginCancelOperation( - &self, - _request: *const ExperimentalWebAuthnPluginCancelOperationRequest, - ) -> HRESULT { - debug_log("EXPERIMENTAL_PluginCancelOperation() called"); - HRESULT(0) - } - - unsafe fn EXPERIMENTAL_GetLockStatus( - &self, - lock_status: *mut PluginLockStatus, - ) -> HRESULT { - debug_log("EXPERIMENTAL_GetLockStatus() called"); - if lock_status.is_null() { - return HRESULT(-2147024809); // E_INVALIDARG - } - *lock_status = PluginLockStatus::PluginUnlocked; - HRESULT(0) - } -} - -impl EXPERIMENTAL2_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl { - unsafe fn EXPERIMENTAL_MakeCredential( - &self, - request: *const Experimental2WebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, - ) -> HRESULT { - debug_log("EXPERIMENTAL2_MakeCredential() called"); + debug_log("MakeCredential() called"); + // Convert to legacy format for internal processing let legacy_request = ExperimentalWebAuthnPluginOperationRequest { window_handle: (*request).window_handle, transaction_id: (*request).transaction_id, @@ -233,15 +167,27 @@ impl EXPERIMENTAL2_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Im encoded_request_byte_count: (*request).encoded_request_byte_count, encoded_request_pointer: (*request).encoded_request_pointer, }; - experimental_plugin_make_credential(&legacy_request, response) + + let mut legacy_response: *mut ExperimentalWebAuthnPluginOperationResponse = ptr::null_mut(); + let result = experimental_plugin_make_credential(&legacy_request, &mut legacy_response); + + if result.is_ok() && !legacy_response.is_null() { + // Copy response data + (*response).encoded_response_byte_count = + (*legacy_response).encoded_response_byte_count; + (*response).encoded_response_pointer = (*legacy_response).encoded_response_pointer; + } + + result } - unsafe fn EXPERIMENTAL_GetAssertion( + unsafe fn GetAssertion( &self, - request: *const Experimental2WebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, + request: *const WebAuthnPluginOperationRequest, + response: *mut WebAuthnPluginOperationResponse, ) -> HRESULT { - debug_log("EXPERIMENTAL2_GetAssertion() called"); + debug_log("GetAssertion() called"); + // Convert to legacy format for internal processing let legacy_request = ExperimentalWebAuthnPluginOperationRequest { window_handle: (*request).window_handle, transaction_id: (*request).transaction_id, @@ -250,22 +196,30 @@ impl EXPERIMENTAL2_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Im encoded_request_byte_count: (*request).encoded_request_byte_count, encoded_request_pointer: (*request).encoded_request_pointer, }; - experimental_plugin_get_assertion(&legacy_request, response) + + let mut legacy_response: *mut ExperimentalWebAuthnPluginOperationResponse = ptr::null_mut(); + let result = experimental_plugin_get_assertion(&legacy_request, &mut legacy_response); + + if result.is_ok() && !legacy_response.is_null() { + // Copy response data + (*response).encoded_response_byte_count = + (*legacy_response).encoded_response_byte_count; + (*response).encoded_response_pointer = (*legacy_response).encoded_response_pointer; + } + + result } - unsafe fn EXPERIMENTAL_CancelOperation( + unsafe fn CancelOperation( &self, - _request: *const ExperimentalWebAuthnPluginCancelOperationRequest, + _request: *const WebAuthnPluginCancelOperationRequest, ) -> HRESULT { - debug_log("EXPERIMENTAL2_CancelOperation() called"); + debug_log("CancelOperation() called"); HRESULT(0) } - unsafe fn EXPERIMENTAL_GetLockStatus( - &self, - lock_status: *mut PluginLockStatus, - ) -> HRESULT { - debug_log("EXPERIMENTAL2_GetLockStatus() called"); + unsafe fn GetLockStatus(&self, lock_status: *mut PluginLockStatus) -> HRESULT { + debug_log("GetLockStatus() called"); if lock_status.is_null() { return HRESULT(-2147024809); // E_INVALIDARG } @@ -274,7 +228,6 @@ impl EXPERIMENTAL2_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Im } } - impl IClassFactory_Impl for Factory_Impl { fn CreateInstance( &self, diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_registration.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_registration.rs index 2b0ab414aac..ed138f80406 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_registration.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_registration.rs @@ -34,10 +34,10 @@ fn parse_uuid_to_bytes(uuid_str: &str) -> Result, String> { .collect() } -/// Converts the CLSID constant string to a GUID -fn parse_clsid_to_guid() -> Result { +/// Converts a CLSID string to a GUID +pub(crate) fn parse_clsid_to_guid_str(clsid_str: &str) -> Result { // Remove hyphens and parse as hex - let clsid_clean = CLSID.replace("-", ""); + let clsid_clean = clsid_str.replace("-", ""); if clsid_clean.len() != 32 { return Err("Invalid CLSID format".to_string()); } @@ -49,6 +49,11 @@ fn parse_clsid_to_guid() -> Result { Ok(GUID::from_u128(clsid_u128)) } +/// Converts the CLSID constant string to a GUID +fn parse_clsid_to_guid() -> Result { + parse_clsid_to_guid_str(CLSID) +} + /// Generates CBOR-encoded authenticator info according to FIDO CTAP2 specifications /// See: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetInfo fn generate_cbor_authenticator_info() -> Result, String> { @@ -174,41 +179,35 @@ pub fn add_authenticator() -> std::result::Result<(), String> { let authenticator_name: HSTRING = AUTHENTICATOR_NAME.into(); let authenticator_name_ptr = PCWSTR(authenticator_name.as_ptr()).as_ptr(); - let clsid: HSTRING = format!("{{{}}}", CLSID).into(); - let clsid_ptr = PCWSTR(clsid.as_ptr()).as_ptr(); + // Parse CLSID into GUID structure + let clsid_guid = parse_clsid_to_guid() + .map_err(|e| format!("Failed to parse CLSID to GUID: {}", e))?; let relying_party_id: HSTRING = RPID.into(); let relying_party_id_ptr = PCWSTR(relying_party_id.as_ptr()).as_ptr(); // Generate CBOR authenticator info dynamically - let mut authenticator_info_bytes = generate_cbor_authenticator_info() + let authenticator_info_bytes = generate_cbor_authenticator_info() .map_err(|e| format!("Failed to generate authenticator info: {}", e))?; - let add_authenticator_options = ExperimentalWebAuthnPluginAddAuthenticatorOptions { + let add_authenticator_options = WebAuthnPluginAddAuthenticatorOptions { authenticator_name: authenticator_name_ptr, - plugin_clsid: clsid_ptr, + rclsid: &clsid_guid, // Changed to GUID reference rpid: relying_party_id_ptr, - light_theme_logo: ptr::null(), - dark_theme_logo: ptr::null(), + light_theme_logo_svg: ptr::null(), // Renamed field + dark_theme_logo_svg: ptr::null(), // Renamed field cbor_authenticator_info_byte_count: authenticator_info_bytes.len() as u32, - cbor_authenticator_info: authenticator_info_bytes.as_mut_ptr(), + cbor_authenticator_info: authenticator_info_bytes.as_ptr(), // Use as_ptr() not as_mut_ptr() + supported_rp_ids_count: 0, // NEW field: 0 means all RPs supported + supported_rp_ids: ptr::null(), // NEW field }; - let plugin_signing_public_key_byte_count: u32 = 0; - let mut plugin_signing_public_key: c_uchar = 0; - let plugin_signing_public_key_ptr = &mut plugin_signing_public_key; - - let mut add_response = ExperimentalWebAuthnPluginAddAuthenticatorResponse { - plugin_operation_signing_key_byte_count: plugin_signing_public_key_byte_count, - plugin_operation_signing_key: plugin_signing_public_key_ptr, - }; - let mut add_response_ptr: *mut ExperimentalWebAuthnPluginAddAuthenticatorResponse = - &mut add_response; + let mut add_response_ptr: *mut WebAuthnPluginAddAuthenticatorResponse = ptr::null_mut(); let result = unsafe { - delay_load::( + delay_load::( s!("webauthn.dll"), - s!("EXPERIMENTAL_WebAuthNPluginAddAuthenticator"), + s!("WebAuthNPluginAddAuthenticator"), // Stable function name ) }; @@ -218,24 +217,45 @@ pub fn add_authenticator() -> std::result::Result<(), String> { if result.is_err() { return Err(format!( - "Error: Error response from EXPERIMENTAL_WebAuthNPluginAddAuthenticator()\n{}", + "Error: Error response from WebAuthNPluginAddAuthenticator()\n{}", result.message() )); } + // Free the response if needed + if !add_response_ptr.is_null() { + free_add_authenticator_response(add_response_ptr); + } + Ok(()) }, None => { - Err(String::from("Error: Can't complete add_authenticator(), as the function EXPERIMENTAL_WebAuthNPluginAddAuthenticator can't be found.")) + Err(String::from("Error: Can't complete add_authenticator(), as the function WebAuthNPluginAddAuthenticator can't be found.")) } } } -type EXPERIMENTAL_WebAuthNPluginAddAuthenticatorFnDeclaration = unsafe extern "cdecl" fn( - pPluginAddAuthenticatorOptions: *const ExperimentalWebAuthnPluginAddAuthenticatorOptions, - ppPluginAddAuthenticatorResponse: *mut *mut ExperimentalWebAuthnPluginAddAuthenticatorResponse, -) - -> HRESULT; +fn free_add_authenticator_response(response: *mut WebAuthnPluginAddAuthenticatorResponse) { + let result = unsafe { + delay_load::( + s!("webauthn.dll"), + s!("WebAuthNPluginFreeAddAuthenticatorResponse"), + ) + }; + + if let Some(api) = result { + unsafe { api(response) }; + } +} + +type WebAuthNPluginAddAuthenticatorFnDeclaration = unsafe extern "cdecl" fn( + pPluginAddAuthenticatorOptions: *const WebAuthnPluginAddAuthenticatorOptions, + ppPluginAddAuthenticatorResponse: *mut *mut WebAuthnPluginAddAuthenticatorResponse, +) -> HRESULT; + +type WebAuthNPluginFreeAddAuthenticatorResponseFnDeclaration = unsafe extern "cdecl" fn( + pPluginAddAuthenticatorResponse: *mut WebAuthnPluginAddAuthenticatorResponse, +); #[cfg(test)] mod tests { diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/sync.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/sync.rs index e42f9f65156..25cdcea9dcf 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/sync.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/sync.rs @@ -1,6 +1,7 @@ use hex; use serde_json; +use crate::com_registration::parse_clsid_to_guid_str; use crate::ipc::send_passkey_request; use crate::types::*; use crate::util::{debug_log, wstr_to_string}; @@ -57,8 +58,9 @@ pub fn sync_credentials_to_windows( plugin_clsid )); - // Format CLSID with curly braces to match Windows registration format - let formatted_clsid = format!("{{{}}}", plugin_clsid); + // Parse CLSID string to GUID + let clsid_guid = parse_clsid_to_guid_str(plugin_clsid) + .map_err(|e| format!("Failed to parse CLSID: {}", e))?; if credentials.is_empty() { debug_log("[SYNC_TO_WIN] No credentials to sync, proceeding with empty sync"); @@ -79,10 +81,10 @@ pub fn sync_credentials_to_windows( hex::encode(&cred.user_handle) }; - debug_log(&format!("[SYNC_TO_WIN] Converting credential {}: RP ID: {}, User: {}, Credential ID: {} ({} bytes), User ID: {} ({} bytes)", + debug_log(&format!("[SYNC_TO_WIN] Converting credential {}: RP ID: {}, User: {}, Credential ID: {} ({} bytes), User ID: {} ({} bytes)", i + 1, cred.rp_id, cred.user_name, truncated_cred_id, cred.credential_id.len(), truncated_user_id, cred.user_handle.len())); - let win_cred = ExperimentalWebAuthnPluginCredentialDetails::create_from_bytes( + let win_cred = WebAuthnPluginCredentialDetails::create_from_bytes( cred.credential_id.clone(), // Pass raw bytes cred.rp_id.clone(), cred.rp_id.clone(), // Use RP ID as friendly name for now @@ -98,15 +100,9 @@ pub fn sync_credentials_to_windows( )); } - // Create credentials list - let credentials_list = ExperimentalWebAuthnPluginCredentialDetailsList::create( - formatted_clsid.clone(), - win_credentials, - ); - // First try to remove all existing credentials for this plugin debug_log("Attempting to remove all existing credentials before sync..."); - match remove_all_credentials(formatted_clsid.clone()) { + match remove_all_credentials(clsid_guid) { Ok(()) => { debug_log("Successfully removed existing credentials"); } @@ -129,7 +125,7 @@ pub fn sync_credentials_to_windows( Ok(()) } else { debug_log("Adding new credentials to Windows..."); - match add_credentials(credentials_list) { + match add_credentials(clsid_guid, win_credentials) { Ok(()) => { debug_log("Successfully synced credentials to Windows"); Ok(()) @@ -152,91 +148,36 @@ pub fn get_credentials_from_windows(plugin_clsid: &str) -> Result { + match get_all_credentials(clsid_guid) { + Ok(credentials) => { debug_log(&format!( "Retrieved {} credentials from Windows", - credentials_list.credential_count + credentials.len() )); let mut bitwarden_credentials = Vec::new(); // Convert Windows credentials to Bitwarden format - unsafe { - let credentials_array = std::slice::from_raw_parts( - credentials_list.credentials, - credentials_list.credential_count as usize, - ); + for cred in credentials { + let synced_cred = SyncedCredential { + credential_id: cred.credential_id, + rp_id: cred.rpid, + user_name: cred.user_name, + user_handle: cred.user_id, + }; - for &cred_ptr in credentials_array { - if !cred_ptr.is_null() { - let cred = &*cred_ptr; + debug_log(&format!("Converted Windows credential: RP ID: {}, User: {}, Credential ID: {} bytes", + synced_cred.rp_id, synced_cred.user_name, synced_cred.credential_id.len())); - // Convert credential data back to Bitwarden format - let credential_id = if cred.credential_id_byte_count > 0 - && !cred.credential_id_pointer.is_null() - { - let id_slice = std::slice::from_raw_parts( - cred.credential_id_pointer, - cred.credential_id_byte_count as usize, - ); - // Assume it's hex-encoded, try to decode - hex::decode(std::str::from_utf8(id_slice).unwrap_or("")) - .unwrap_or_else(|_| id_slice.to_vec()) - } else { - Vec::new() - }; - - let rp_id = if !cred.rpid.is_null() { - wstr_to_string(cred.rpid).unwrap_or_default() - } else { - String::new() - }; - - let user_name = if !cred.user_name.is_null() { - wstr_to_string(cred.user_name).unwrap_or_default() - } else { - String::new() - }; - - let user_id = - if cred.user_id_byte_count > 0 && !cred.user_id_pointer.is_null() { - // Convert from UTF-8 bytes back to Vec - let user_id_slice = std::slice::from_raw_parts( - cred.user_id_pointer, - cred.user_id_byte_count as usize, - ); - // Try to decode as hex string, or use raw bytes - let user_id_str = std::str::from_utf8(user_id_slice).unwrap_or(""); - hex::decode(user_id_str).unwrap_or_else(|_| user_id_slice.to_vec()) - } else { - Vec::new() - }; - - let synced_cred = SyncedCredential { - credential_id, - rp_id, - user_name, - user_handle: user_id, - }; - - debug_log(&format!("Converted Windows credential: RP ID: {}, User: {}, Credential ID: {} bytes", - synced_cred.rp_id, synced_cred.user_name, synced_cred.credential_id.len())); - - bitwarden_credentials.push(synced_cred); - } - } + bitwarden_credentials.push(synced_cred); } Ok(bitwarden_credentials) } - Ok(None) => { - debug_log("No credentials found in Windows"); - Ok(Vec::new()) - } Err(e) => { debug_log(&format!( "ERROR: Failed to get credentials from Windows: {}", diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/webauthn.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/webauthn.rs index d42dc74e080..e64242ce6e5 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/webauthn.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/webauthn.rs @@ -1,8 +1,8 @@ /* - This file exposes safe functions and types for interacting with the experimental - Windows WebAuthn API defined here: + This file exposes safe functions and types for interacting with the stable + Windows WebAuthn Plugin API defined here: - https://github.com/microsoft/webauthn/blob/master/experimental/webauthn.h + https://github.com/microsoft/webauthn/blob/master/webauthnplugin.h */ use windows_core::*; @@ -21,49 +21,54 @@ pub struct ExperimentalWebAuthnCtapCborAuthenticatorOptions { pub require_resident_key: i32, // LONG lRequireResidentKey: +1=TRUE, 0=Not defined, -1=FALSE } -/// Used when adding a Windows plugin authenticator. -/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS -/// Header File Usage: EXPERIMENTAL_WebAuthNPluginAddAuthenticator() +/// Used when adding a Windows plugin authenticator (stable API). +/// Header File Name: _WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS +/// Header File Usage: WebAuthNPluginAddAuthenticator() #[repr(C)] #[derive(Debug, Copy, Clone)] -pub struct ExperimentalWebAuthnPluginAddAuthenticatorOptions { - pub authenticator_name: *const u16, - pub plugin_clsid: *const u16, - pub rpid: *const u16, - pub light_theme_logo: *const u16, - pub dark_theme_logo: *const u16, +pub struct WebAuthnPluginAddAuthenticatorOptions { + pub authenticator_name: *const u16, // LPCWSTR + pub rclsid: *const GUID, // REFCLSID (changed from string) + pub rpid: *const u16, // LPCWSTR (optional) + pub light_theme_logo_svg: *const u16, // LPCWSTR (optional, base64 SVG) + pub dark_theme_logo_svg: *const u16, // LPCWSTR (optional, base64 SVG) pub cbor_authenticator_info_byte_count: u32, - pub cbor_authenticator_info: *const u8, + pub cbor_authenticator_info: *const u8, // const BYTE* + pub supported_rp_ids_count: u32, // NEW in stable + pub supported_rp_ids: *const *const u16, // NEW in stable: array of LPCWSTR } -/// Used as a response type when adding a Windows plugin authenticator. -/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE -/// Header File Usage: EXPERIMENTAL_WebAuthNPluginAddAuthenticator() -/// EXPERIMENTAL_WebAuthNPluginFreeAddAuthenticatorResponse() +/// Used as a response type when adding a Windows plugin authenticator (stable API). +/// Header File Name: _WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE +/// Header File Usage: WebAuthNPluginAddAuthenticator() +/// WebAuthNPluginFreeAddAuthenticatorResponse() #[repr(C)] #[derive(Debug, Copy, Clone)] -pub struct ExperimentalWebAuthnPluginAddAuthenticatorResponse { +pub struct WebAuthnPluginAddAuthenticatorResponse { pub plugin_operation_signing_key_byte_count: u32, pub plugin_operation_signing_key: *mut u8, } /// Represents a credential. -/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS -/// Header File Usage: _EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS_LIST +/// Header File Name: _WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS +/// Header File Usage: WebAuthNPluginAuthenticatorAddCredentials, etc. #[repr(C)] #[derive(Debug, Copy, Clone)] -pub struct ExperimentalWebAuthnPluginCredentialDetails { +pub struct WebAuthnPluginCredentialDetails { pub credential_id_byte_count: u32, - pub credential_id_pointer: *mut u8, - pub rpid: *mut u16, - pub rp_friendly_name: *mut u16, + pub credential_id_pointer: *const u8, // Changed to const in stable + pub rpid: *const u16, // Changed to const (LPCWSTR) + pub rp_friendly_name: *const u16, // Changed to const (LPCWSTR) pub user_id_byte_count: u32, - pub user_id_pointer: *mut u8, // Should be *mut u8 like credential_id_pointer - pub user_name: *mut u16, - pub user_display_name: *mut u16, + pub user_id_pointer: *const u8, // Changed to const + pub user_name: *const u16, // Changed to const (LPCWSTR) + pub user_display_name: *const u16, // Changed to const (LPCWSTR) } -impl ExperimentalWebAuthnPluginCredentialDetails { +// Keep experimental version for internal use +pub type ExperimentalWebAuthnPluginCredentialDetails = WebAuthnPluginCredentialDetails; + +impl WebAuthnPluginCredentialDetails { pub fn create_from_bytes( credential_id: Vec, rpid: String, @@ -72,12 +77,11 @@ impl ExperimentalWebAuthnPluginCredentialDetails { user_name: String, user_display_name: String, ) -> Self { - // Convert credential_id bytes to hex string, then allocate with COM - let (credential_id_pointer, credential_id_byte_count) = ComBuffer::from_buffer(credential_id); + // Allocate credential_id bytes with COM + let (credential_id_pointer, credential_id_byte_count) = ComBuffer::from_buffer(&credential_id); - // Convert user_id bytes to hex string, then allocate with COM - let user_id_string = hex::encode(&user_id); - let (user_id_pointer, user_id_byte_count) = ComBuffer::from_buffer(user_id_string.as_bytes()); + // Allocate user_id bytes with COM + let (user_id_pointer, user_id_byte_count) = ComBuffer::from_buffer(&user_id); // Convert strings to null-terminated wide strings using trait methods let (rpid_ptr, _) = rpid.to_com_utf16(); @@ -87,22 +91,40 @@ impl ExperimentalWebAuthnPluginCredentialDetails { Self { credential_id_byte_count, - credential_id_pointer, - rpid: rpid_ptr, - rp_friendly_name: rp_friendly_name_ptr, + credential_id_pointer: credential_id_pointer as *const u8, + rpid: rpid_ptr as *const u16, + rp_friendly_name: rp_friendly_name_ptr as *const u16, user_id_byte_count, - user_id_pointer, - user_name: user_name_ptr, - user_display_name: user_display_name_ptr, + user_id_pointer: user_id_pointer as *const u8, + user_name: user_name_ptr as *const u16, + user_display_name: user_display_name_ptr as *const u16, } } } -/// Represents a list of credentials. -/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS_LIST -/// Header File Usage: EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials() -/// EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentials() -/// EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentials() +// Keep backward compat alias +impl ExperimentalWebAuthnPluginCredentialDetails { + pub fn create_from_bytes( + credential_id: Vec, + rpid: String, + rp_friendly_name: String, + user_id: Vec, + user_name: String, + user_display_name: String, + ) -> Self { + WebAuthnPluginCredentialDetails::create_from_bytes( + credential_id, + rpid, + rp_friendly_name, + user_id, + user_name, + user_display_name, + ) + } +} + +/// Represents a list of credentials - kept for backwards compatibility +/// The stable API takes flat arrays directly, not this list structure #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct ExperimentalWebAuthnPluginCredentialDetailsList { @@ -126,7 +148,7 @@ impl ExperimentalWebAuthnPluginCredentialDetailsList { .collect(); let credentials_len = credential_pointers.len(); - + // Allocate the array of pointers using COM as well let credentials_pointer = if credentials_len > 0 { let pointer_array_bytes = credential_pointers.len() * std::mem::size_of::<*mut ExperimentalWebAuthnPluginCredentialDetails>(); @@ -143,7 +165,7 @@ impl ExperimentalWebAuthnPluginCredentialDetailsList { // Convert CLSID to wide string using trait method let (clsid_ptr, _) = clsid.to_com_utf16(); - + Self { plugin_clsid: clsid_ptr, credential_count: credentials_len as u32, @@ -152,52 +174,71 @@ impl ExperimentalWebAuthnPluginCredentialDetailsList { } } -pub type EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration = +// Stable API function signatures - now use REFCLSID and flat arrays +pub type WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration = unsafe extern "cdecl" fn( - pCredentialDetailsList: *mut ExperimentalWebAuthnPluginCredentialDetailsList, + rclsid: *const GUID, // Changed from string to GUID reference + cCredentialDetails: u32, + pCredentialDetails: *const WebAuthnPluginCredentialDetails, // Flat array, not list ) -> HRESULT; -pub type EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentialsFnDeclaration = +pub type WebAuthNPluginAuthenticatorRemoveCredentialsFnDeclaration = unsafe extern "cdecl" fn( - pCredentialDetailsList: *mut ExperimentalWebAuthnPluginCredentialDetailsList, + rclsid: *const GUID, + cCredentialDetails: u32, + pCredentialDetails: *const WebAuthnPluginCredentialDetails, ) -> HRESULT; -pub type EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentialsFnDeclaration = +pub type WebAuthNPluginAuthenticatorGetAllCredentialsFnDeclaration = unsafe extern "cdecl" fn( - pwszPluginClsId: *const u16, - ppCredentialDetailsList: *mut *mut ExperimentalWebAuthnPluginCredentialDetailsList, + rclsid: *const GUID, + pcCredentialDetails: *mut u32, // Out param for count + ppCredentialDetailsArray: *mut *mut WebAuthnPluginCredentialDetails, // Out param for array ) -> HRESULT; -pub type EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentialsFnDeclaration = +pub type WebAuthNPluginAuthenticatorFreeCredentialDetailsArrayFnDeclaration = unsafe extern "cdecl" fn( - pwszPluginClsId: *const u16, + cCredentialDetails: u32, + pCredentialDetailsArray: *mut WebAuthnPluginCredentialDetails, + ); + +pub type WebAuthNPluginAuthenticatorRemoveAllCredentialsFnDeclaration = + unsafe extern "cdecl" fn( + rclsid: *const GUID, ) -> HRESULT; pub fn add_credentials( - mut credentials_list: ExperimentalWebAuthnPluginCredentialDetailsList, + clsid_guid: GUID, + credentials: Vec, ) -> std::result::Result<(), String> { - debug_log("Loading EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials function..."); - + debug_log("Loading WebAuthNPluginAuthenticatorAddCredentials function..."); + let result = unsafe { - delay_load::( + delay_load::( s!("webauthn.dll"), - s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials"), + s!("WebAuthNPluginAuthenticatorAddCredentials"), ) }; match result { Some(api) => { debug_log("Function loaded successfully, calling API..."); - debug_log(&format!("Credential list: plugin_clsid valid: {}, credential_count: {}", - !credentials_list.plugin_clsid.is_null(), credentials_list.credential_count)); - - let result = unsafe { api(&mut credentials_list) }; + debug_log(&format!("Adding {} credentials", credentials.len())); + + let credential_count = credentials.len() as u32; + let credentials_ptr = if credentials.is_empty() { + std::ptr::null() + } else { + credentials.as_ptr() + }; + + let result = unsafe { api(&clsid_guid, credential_count, credentials_ptr) }; if result.is_err() { let error_code = result.0; debug_log(&format!("API call failed with HRESULT: 0x{:x}", error_code)); return Err(format!( - "Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials()\nHRESULT: 0x{:x}\n{}", + "Error: Error response from WebAuthNPluginAuthenticatorAddCredentials()\nHRESULT: 0x{:x}\n{}", error_code, result.message() )); } @@ -206,29 +247,41 @@ pub fn add_credentials( Ok(()) }, None => { - debug_log("Failed to load EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials function from webauthn.dll"); - Err(String::from("Error: Can't complete add_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials can't be loaded.")) + debug_log("Failed to load WebAuthNPluginAuthenticatorAddCredentials function from webauthn.dll"); + Err(String::from("Error: Can't complete add_credentials(), as the function WebAuthNPluginAuthenticatorAddCredentials can't be loaded.")) } } } pub fn remove_credentials( - mut credentials_list: ExperimentalWebAuthnPluginCredentialDetailsList, + clsid_guid: GUID, + credentials: Vec, ) -> std::result::Result<(), String> { + debug_log("Loading WebAuthNPluginAuthenticatorRemoveCredentials function..."); + let result = unsafe { - delay_load::( + delay_load::( s!("webauthn.dll"), - s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentials"), + s!("WebAuthNPluginAuthenticatorRemoveCredentials"), ) }; match result { Some(api) => { - let result = unsafe { api(&mut credentials_list) }; + debug_log(&format!("Removing {} credentials", credentials.len())); + + let credential_count = credentials.len() as u32; + let credentials_ptr = if credentials.is_empty() { + std::ptr::null() + } else { + credentials.as_ptr() + }; + + let result = unsafe { api(&clsid_guid, credential_count, credentials_ptr) }; if result.is_err() { return Err(format!( - "Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentials()\n{}", + "Error: Error response from WebAuthNPluginAuthenticatorRemoveCredentials()\n{}", result.message() )); } @@ -236,75 +289,181 @@ pub fn remove_credentials( Ok(()) }, None => { - Err(String::from("Error: Can't complete remove_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentials can't be loaded.")) + Err(String::from("Error: Can't complete remove_credentials(), as the function WebAuthNPluginAuthenticatorRemoveCredentials can't be loaded.")) } } } +// Helper struct to hold owned credential data +#[derive(Debug, Clone)] +pub struct OwnedCredentialDetails { + pub credential_id: Vec, + pub rpid: String, + pub rp_friendly_name: String, + pub user_id: Vec, + pub user_name: String, + pub user_display_name: String, +} + pub fn get_all_credentials( - plugin_clsid: String, -) -> std::result::Result, String> { + clsid_guid: GUID, +) -> std::result::Result, String> { + debug_log("Loading WebAuthNPluginAuthenticatorGetAllCredentials function..."); + let result = unsafe { - delay_load::( + delay_load::( s!("webauthn.dll"), - s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentials"), + s!("WebAuthNPluginAuthenticatorGetAllCredentials"), ) }; match result { Some(api) => { - // Create the wide string and keep it alive during the API call - let clsid_wide = plugin_clsid.to_utf16(); - let mut credentials_list_ptr: *mut ExperimentalWebAuthnPluginCredentialDetailsList = std::ptr::null_mut(); - - let result = unsafe { api(clsid_wide.as_ptr(), &mut credentials_list_ptr) }; + let mut credential_count: u32 = 0; + let mut credentials_array_ptr: *mut WebAuthnPluginCredentialDetails = std::ptr::null_mut(); + + let result = unsafe { api(&clsid_guid, &mut credential_count, &mut credentials_array_ptr) }; if result.is_err() { return Err(format!( - "Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentials()\n{}", + "Error: Error response from WebAuthNPluginAuthenticatorGetAllCredentials()\n{}", result.message() )); } - if credentials_list_ptr.is_null() { - Ok(None) - } else { - // Note: The caller is responsible for managing the memory of the returned list - Ok(Some(unsafe { *credentials_list_ptr })) + if credentials_array_ptr.is_null() || credential_count == 0 { + debug_log("No credentials returned"); + return Ok(Vec::new()); } + + // Deep copy the credential data before Windows frees it + let credentials_slice = unsafe { + std::slice::from_raw_parts(credentials_array_ptr, credential_count as usize) + }; + + let mut owned_credentials = Vec::new(); + for cred in credentials_slice { + unsafe { + // Copy credential ID bytes + let credential_id = if !cred.credential_id_pointer.is_null() && cred.credential_id_byte_count > 0 { + std::slice::from_raw_parts(cred.credential_id_pointer, cred.credential_id_byte_count as usize).to_vec() + } else { + Vec::new() + }; + + // Copy user ID bytes + let user_id = if !cred.user_id_pointer.is_null() && cred.user_id_byte_count > 0 { + std::slice::from_raw_parts(cred.user_id_pointer, cred.user_id_byte_count as usize).to_vec() + } else { + Vec::new() + }; + + // Copy string fields + let rpid = if !cred.rpid.is_null() { + String::from_utf16_lossy(std::slice::from_raw_parts( + cred.rpid, + (0..).position(|i| *cred.rpid.offset(i) == 0).unwrap_or(0) + )) + } else { + String::new() + }; + + let rp_friendly_name = if !cred.rp_friendly_name.is_null() { + String::from_utf16_lossy(std::slice::from_raw_parts( + cred.rp_friendly_name, + (0..).position(|i| *cred.rp_friendly_name.offset(i) == 0).unwrap_or(0) + )) + } else { + String::new() + }; + + let user_name = if !cred.user_name.is_null() { + String::from_utf16_lossy(std::slice::from_raw_parts( + cred.user_name, + (0..).position(|i| *cred.user_name.offset(i) == 0).unwrap_or(0) + )) + } else { + String::new() + }; + + let user_display_name = if !cred.user_display_name.is_null() { + String::from_utf16_lossy(std::slice::from_raw_parts( + cred.user_display_name, + (0..).position(|i| *cred.user_display_name.offset(i) == 0).unwrap_or(0) + )) + } else { + String::new() + }; + + owned_credentials.push(OwnedCredentialDetails { + credential_id, + rpid, + rp_friendly_name, + user_id, + user_name, + user_display_name, + }); + } + } + + // Free the array using the Windows API - this frees everything including strings + free_credential_details_array(credential_count, credentials_array_ptr); + + debug_log(&format!("Retrieved {} credentials", owned_credentials.len())); + Ok(owned_credentials) }, None => { - Err(String::from("Error: Can't complete get_all_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentials can't be loaded.")) + Err(String::from("Error: Can't complete get_all_credentials(), as the function WebAuthNPluginAuthenticatorGetAllCredentials can't be loaded.")) } } } -pub fn remove_all_credentials( - plugin_clsid: String, -) -> std::result::Result<(), String> { - debug_log("Loading EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials function..."); - +fn free_credential_details_array( + credential_count: u32, + credentials_array: *mut WebAuthnPluginCredentialDetails, +) { + if credentials_array.is_null() { + return; + } + let result = unsafe { - delay_load::( + delay_load::( s!("webauthn.dll"), - s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials"), + s!("WebAuthNPluginAuthenticatorFreeCredentialDetailsArray"), + ) + }; + + if let Some(api) = result { + unsafe { api(credential_count, credentials_array) }; + } else { + debug_log("Warning: Could not load WebAuthNPluginAuthenticatorFreeCredentialDetailsArray"); + } +} + +pub fn remove_all_credentials( + clsid_guid: GUID, +) -> std::result::Result<(), String> { + debug_log("Loading WebAuthNPluginAuthenticatorRemoveAllCredentials function..."); + + let result = unsafe { + delay_load::( + s!("webauthn.dll"), + s!("WebAuthNPluginAuthenticatorRemoveAllCredentials"), ) }; match result { Some(api) => { debug_log("Function loaded successfully, calling API..."); - // Create the wide string and keep it alive during the API call - let clsid_wide = plugin_clsid.to_utf16(); - - let result = unsafe { api(clsid_wide.as_ptr()) }; + + let result = unsafe { api(&clsid_guid) }; if result.is_err() { let error_code = result.0; debug_log(&format!("API call failed with HRESULT: 0x{:x}", error_code)); - + return Err(format!( - "Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials()\nHRESULT: 0x{:x}\n{}", + "Error: Error response from WebAuthNPluginAuthenticatorRemoveAllCredentials()\nHRESULT: 0x{:x}\n{}", error_code, result.message() )); } @@ -313,8 +472,8 @@ pub fn remove_all_credentials( Ok(()) }, None => { - debug_log("Failed to load EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials function from webauthn.dll"); - Err(String::from("Error: Can't complete remove_all_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials can't be loaded.")) + debug_log("Failed to load WebAuthNPluginAuthenticatorRemoveAllCredentials function from webauthn.dll"); + Err(String::from("Error: Can't complete remove_all_credentials(), as the function WebAuthNPluginAuthenticatorRemoveAllCredentials can't be loaded.")) } } }