diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index d734d2f1737..f5b21e57178 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -1006,6 +1006,7 @@ dependencies = [ "tracing", "typenum", "widestring", + "win_webauthn", "windows 0.61.3", "windows-future", "zbus", diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 6e7ad240144..491c8b7a5f9 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -77,6 +77,7 @@ windows = { workspace = true, features = [ "Win32_System_Pipes", ], optional = true } windows-future = { workspace = true } +win_webauthn = { path = "../win_webauthn" } [target.'cfg(windows)'.dev-dependencies] keytar = { workspace = true } diff --git a/apps/desktop/desktop_native/core/src/autofill/windows.rs b/apps/desktop/desktop_native/core/src/autofill/windows.rs index 868c570b125..b91fdd93f68 100644 --- a/apps/desktop/desktop_native/core/src/autofill/windows.rs +++ b/apps/desktop/desktop_native/core/src/autofill/windows.rs @@ -1,15 +1,7 @@ -use std::alloc; -use std::mem::{align_of, MaybeUninit}; -use std::ptr::NonNull; - use anyhow::{anyhow, Result}; use base64::engine::{general_purpose::URL_SAFE_NO_PAD, Engine}; -use windows::core::s; -use windows::Win32::Foundation::{FreeLibrary, HWND}; -use windows::{ - core::{GUID, HRESULT, PCSTR}, - Win32::System::{Com::CoTaskMemAlloc, LibraryLoader::*}, -}; +use win_webauthn::{CredentialId, UserId, plugin::{Clsid, PluginCredentialDetails, PluginUserVerificationRequest, WebAuthnPlugin}}; +use windows::{Win32::Foundation::HWND, core::GUID}; use crate::autofill::{ CommandResponse, RunCommand, RunCommandRequest, StatusResponse, StatusState, StatusSupport, @@ -71,38 +63,30 @@ fn handle_user_verification_request( request: UserVerificationParameters, ) -> Result { tracing::debug!(?request, "Handling user verification request"); - unsafe { - let hwnd: HWND = *request.window_handle.as_ptr().cast(); + let (buf, _) = request.transaction_context[..16].split_at(16); + let guid_u128 = buf + .try_into() + .map_err(|e| anyhow!("Failed to parse transaction ID as u128: {e}"))?; + let transaction_id = GUID::from_u128(u128::from_le_bytes(guid_u128)); + let hwnd: HWND = unsafe { + // SAFETY: We check to make sure that the vec is the expected size + // before converting it. If the handle is invalid when passed to + // Windows, the request will be rejected. + if request.window_handle.len() == size_of::() { + *request.window_handle.as_ptr().cast() + } else { + return Err(anyhow!("Invalid window handle received: {:?}", request.window_handle)); + } + }; - let (buf, _) = request.transaction_context[..16].split_at(16); - let guid_u128 = buf - .try_into() - .map_err(|e| anyhow!("Failed to parse transaction ID as u128: {e}"))?; - let transaction_id = GUID::from_u128(u128::from_le_bytes(guid_u128)); - - let uv_request = WebAuthNPluginUserVerificationRequest { - hwnd, - rguidTransactionId: (&transaction_id) as *const GUID, - pwszUsername: request.username.to_com_utf16().0, - pwszDisplayHint: request.display_hint.to_com_utf16().0, - }; - let uv_fn = delay_load::( - s!("webauthn.dll"), - s!("WebAuthNPluginPerformUserVerification"), - ) - .ok_or(anyhow!( - "Could not load WebAuthNPluginPerformUserVerification" - ))?; - let mut uv_response_len: u32 = 0; - let mut uv_response: *mut u8 = std::ptr::null_mut(); - uv_fn( - std::ptr::from_ref(&uv_request), - &mut uv_response_len as *mut u32, - &mut uv_response as *mut *mut u8, - ) - .ok() + let uv_request = PluginUserVerificationRequest { + window_handle: hwnd, + transaction_id: transaction_id, + user_name: request.username, + display_hint: Some(request.display_hint), + }; + let _response = WebAuthnPlugin::perform_user_verification(uv_request) .map_err(|err| anyhow!("User Verification request failed: {err}"))?; - } return Ok(UserVerificationResponse {}); } @@ -145,70 +129,48 @@ fn sync_credentials_to_windows( 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() { - tracing::debug!("[SYNC_TO_WIN] No credentials to sync, proceeding with empty sync"); - } + let clsid = Clsid::try_from(plugin_clsid) + .map_err(|err| format!("Failed to parse CLSID from string {plugin_clsid}: {err}"))?; + let plugin = WebAuthnPlugin::new(clsid); // Convert Bitwarden credentials to Windows credential details - let mut win_credentials = Vec::new(); - - for (i, cred) in credentials.iter().enumerate() { + let win_credentials = credentials.into_iter().enumerate().filter_map(|(i, cred)| { tracing::debug!("[SYNC_TO_WIN] Converting credential {}: RP ID: {}, User: {}, Credential ID: {:?} ({} bytes), User ID: {:?} ({} bytes)", i + 1, cred.rp_id, cred.user_name, &cred.credential_id, cred.credential_id.len(), &cred.user_handle, cred.user_handle.len()); + + let cred_id = match CredentialId::try_from(cred.credential_id) { + Ok(id) => id, + Err(err) => { + tracing::warn!("Skipping sync of credential {} because of an invalid credential ID: {err}", i + 1); + return None; + } + }; + let user_id = match UserId::try_from(cred.user_handle) { + Ok(id) => id, + Err(err) => { + tracing::warn!("Skipping sync of credential {} because of an invalid user ID: {err}", i + 1); + return None; + } + }; - 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 - cred.user_handle.clone(), // Pass raw bytes - cred.user_name.clone(), - cred.user_name.clone(), // Use user name as display name for now - ); - - win_credentials.push(win_cred); + let cred_details = PluginCredentialDetails { + credential_id: cred_id, + rp_id: cred.rp_id.clone(), + rp_friendly_name: Some(cred.rp_id.clone()), // Use RP ID as friendly name for now + user_id: user_id, + user_name: cred.user_name.clone(), + user_display_name: cred.user_name.clone(), // Use user name as display name for now + }; tracing::debug!( "[SYNC_TO_WIN] Converted credential {} to Windows format", i + 1 ); - } + Some(cred_details) + }).collect(); - // First try to remove all existing credentials for this plugin - tracing::debug!("Attempting to remove all existing credentials before sync..."); - match remove_all_credentials(clsid_guid) { - Ok(()) => { - tracing::debug!("Successfully removed existing credentials"); - } - Err(e) if e.contains("can't be loaded") => { - tracing::debug!("RemoveAllCredentials function not available - this is expected for some Windows versions"); - // This is fine, the function might not exist in all versions - } - Err(e) => { - tracing::debug!("Warning: Failed to remove existing credentials: {}", e); - // Continue anyway, as this might be the first sync or an older Windows version - } - } - - // Add the new credentials (only if we have any) - if credentials.is_empty() { - tracing::debug!("No credentials to add to Windows - sync completed successfully"); - Ok(()) - } else { - tracing::debug!("Adding new credentials to Windows..."); - match add_credentials(clsid_guid, win_credentials) { - Ok(()) => { - tracing::debug!("Successfully synced credentials to Windows"); - Ok(()) - } - Err(e) => { - tracing::error!("Failed to add credentials to Windows: {}", e); - Err(e) - } - } - } + plugin + .sync_credentials(win_credentials) + .map_err(|err| format!("Failed to synchronize credentials: {err}")) } /// Credential data for sync operations @@ -219,308 +181,4 @@ struct SyncedCredential { pub rp_id: String, pub user_name: String, pub user_handle: Vec, -} - -/// Represents a credential. -/// Header File Name: _WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS -/// Header File Usage: WebAuthNPluginAuthenticatorAddCredentials, etc. -#[repr(C)] -#[derive(Debug, Copy, Clone)] -struct WebAuthnPluginCredentialDetails { - pub credential_id_byte_count: u32, - 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: *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 WebAuthnPluginCredentialDetails { - 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 { - // Allocate credential_id bytes with COM - let (credential_id_pointer, credential_id_byte_count) = - ComBuffer::from_buffer(&credential_id); - - // 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(); - let (rp_friendly_name_ptr, _) = rp_friendly_name.to_com_utf16(); - let (user_name_ptr, _) = user_name.to_com_utf16(); - let (user_display_name_ptr, _) = user_display_name.to_com_utf16(); - - Self { - credential_id_byte_count, - 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_id_pointer as *const u8, - user_name: user_name_ptr as *const u16, - user_display_name: user_display_name_ptr as *const u16, - } - } -} - -// Stable API function signatures - now use REFCLSID and flat arrays -type WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration = unsafe extern "cdecl" fn( - rclsid: *const GUID, // Changed from string to GUID reference - cCredentialDetails: u32, - pCredentialDetails: *const WebAuthnPluginCredentialDetails, // Flat array, not list -) -> HRESULT; - -/// Trait for converting strings to Windows-compatible wide strings using COM allocation -pub trait WindowsString { - /// Converts to null-terminated UTF-16 using COM allocation - fn to_com_utf16(&self) -> (*mut u16, u32); - /// Converts to Vec for temporary use (caller must keep Vec alive) - fn to_utf16(&self) -> Vec; -} - -impl WindowsString for str { - fn to_com_utf16(&self) -> (*mut u16, u32) { - let mut wide_vec: Vec = self.encode_utf16().collect(); - wide_vec.push(0); // null terminator - let wide_bytes: Vec = wide_vec.iter().flat_map(|&x| x.to_le_bytes()).collect(); - let (ptr, byte_count) = ComBuffer::from_buffer(&wide_bytes); - (ptr as *mut u16, byte_count) - } - - fn to_utf16(&self) -> Vec { - let mut wide_vec: Vec = self.encode_utf16().collect(); - wide_vec.push(0); // null terminator - wide_vec - } -} - -#[repr(transparent)] -pub struct ComBuffer(NonNull>); - -impl ComBuffer { - /// Returns an COM-allocated buffer of `size`. - fn alloc(size: usize, for_slice: bool) -> Self { - #[expect(clippy::as_conversions)] - { - assert!(size <= isize::MAX as usize, "requested bad object size"); - } - - // SAFETY: Any size is valid to pass to Windows, even `0`. - let ptr = NonNull::new(unsafe { CoTaskMemAlloc(size) }).unwrap_or_else(|| { - // XXX: This doesn't have to be correct, just close enough for an OK OOM error. - let layout = alloc::Layout::from_size_align(size, align_of::()).unwrap(); - alloc::handle_alloc_error(layout) - }); - - if for_slice { - // Ininitialize the buffer so it can later be treated as `&mut [u8]`. - // SAFETY: The pointer is valid and we are using a valid value for a byte-wise allocation. - unsafe { ptr.write_bytes(0, size) }; - } - - Self(ptr.cast()) - } - - fn into_ptr(self) -> *mut T { - self.0.cast().as_ptr() - } - - /// Creates a new COM-allocated structure. - /// - /// Note that `T` must be [Copy] to avoid any possible memory leaks. - pub fn with_object(object: T) -> *mut T { - // NB: Vendored from Rust's alloc code since we can't yet allocate `Box` with a custom allocator. - const MIN_ALIGN: usize = if cfg!(target_pointer_width = "64") { - 16 - } else if cfg!(target_pointer_width = "32") { - 8 - } else { - panic!("unsupported arch") - }; - - // SAFETY: Validate that our alignment works for a normal size-based allocation for soundness. - let layout = const { - let layout = alloc::Layout::new::(); - assert!(layout.align() <= MIN_ALIGN); - layout - }; - - let buffer = Self::alloc(layout.size(), false); - // SAFETY: `ptr` is valid for writes of `T` because we correctly allocated the right sized buffer that - // accounts for any alignment requirements. - // - // Additionally, we ensure the value is treated as moved by forgetting the source. - unsafe { buffer.0.cast::().write(object) }; - buffer.into_ptr() - } - - pub fn from_buffer>(buffer: T) -> (*mut u8, u32) { - let buffer = buffer.as_ref(); - let len = buffer.len(); - let com_buffer = Self::alloc(len, true); - - // SAFETY: `ptr` points to a valid allocation that `len` matches, and we made sure - // the bytes were initialized. Additionally, bytes have no alignment requirements. - unsafe { - NonNull::slice_from_raw_parts(com_buffer.0.cast::(), len) - .as_mut() - .copy_from_slice(buffer) - } - - // Safety: The Windows API structures these buffers are placed into use `u32` (`DWORD`) to - // represent length. - #[expect(clippy::as_conversions)] - (com_buffer.into_ptr(), len as u32) - } -} - -/// 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_str.replace("-", ""); - if clsid_clean.len() != 32 { - return Err("Invalid CLSID format".to_string()); - } - - // Convert to u128 and create GUID - let clsid_u128 = u128::from_str_radix(&clsid_clean, 16) - .map_err(|_| "Failed to parse CLSID as hex".to_string())?; - - Ok(GUID::from_u128(clsid_u128)) -} - -pub fn remove_all_credentials(clsid_guid: GUID) -> std::result::Result<(), String> { - tracing::debug!("Loading WebAuthNPluginAuthenticatorRemoveAllCredentials function..."); - - let result = unsafe { - delay_load::( - s!("webauthn.dll"), - s!("WebAuthNPluginAuthenticatorRemoveAllCredentials"), - ) - }; - - match result { - Some(api) => { - tracing::debug!("Function loaded successfully, calling API..."); - - let result = unsafe { api(&clsid_guid) }; - - if result.is_err() { - let error_code = result.0; - tracing::debug!("API call failed with HRESULT: 0x{:x}", error_code); - - return Err(format!( - "Error: Error response from WebAuthNPluginAuthenticatorRemoveAllCredentials()\nHRESULT: 0x{:x}\n{}", - error_code, result.message() - )); - } - - tracing::debug!("API call succeeded"); - Ok(()) - } - None => { - tracing::debug!("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.")) - } - } -} - -pub unsafe fn delay_load(library: PCSTR, function: PCSTR) -> Option { - let library = LoadLibraryExA(library, None, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); - - let Ok(library) = library else { - return None; - }; - - let address = GetProcAddress(library, function); - - if address.is_some() { - return Some(std::mem::transmute_copy(&address)); - } - - _ = FreeLibrary(library); - - None -} - -fn add_credentials( - clsid_guid: GUID, - credentials: Vec, -) -> std::result::Result<(), String> { - tracing::debug!("Loading WebAuthNPluginAuthenticatorAddCredentials function..."); - - let result = unsafe { - delay_load::( - s!("webauthn.dll"), - s!("WebAuthNPluginAuthenticatorAddCredentials"), - ) - }; - - match result { - Some(api) => { - tracing::debug!("Function loaded successfully, calling API..."); - tracing::debug!("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; - tracing::debug!("API call failed with HRESULT: 0x{:x}", error_code); - return Err(format!( - "Error: Error response from WebAuthNPluginAuthenticatorAddCredentials()\nHRESULT: 0x{:x}\n{}", - error_code, result.message() - )); - } - - tracing::debug!("API call succeeded"); - Ok(()) - } - None => { - tracing::debug!("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.")) - } - } -} - -type WebAuthNPluginAuthenticatorRemoveAllCredentialsFnDeclaration = - unsafe extern "cdecl" fn(rclsid: *const GUID) -> HRESULT; - -#[repr(C)] -#[derive(Debug)] -struct WebAuthNPluginUserVerificationRequest { - /// Windows handle of the top-level window displayed by the plugin and currently is in foreground as part of the ongoing webauthn operation. - hwnd: HWND, - - /// The webauthn transaction id from the WEBAUTHN_PLUGIN_OPERATION_REQUEST - rguidTransactionId: *const GUID, - - /// The username attached to the credential that is in use for this webauthn operation - pwszUsername: *const u16, - - /// A text hint displayed on the windows hello prompt - pwszDisplayHint: *const u16, -} - -type WebAuthNPluginPerformUserVerification = unsafe extern "cdecl" fn( - pPluginUserVerification: *const WebAuthNPluginUserVerificationRequest, - pcbResponse: *mut u32, - ppbResponse: *mut *mut u8, -) -> HRESULT; - -type WebAuthNPluginFreeUserVerificationResponse = unsafe extern "cdecl" fn(ppbResponse: *mut u8); +} \ No newline at end of file diff --git a/apps/desktop/desktop_native/win_webauthn/src/lib.rs b/apps/desktop/desktop_native/win_webauthn/src/lib.rs index e9efc802a92..7bbefd8ba9d 100644 --- a/apps/desktop/desktop_native/win_webauthn/src/lib.rs +++ b/apps/desktop/desktop_native/win_webauthn/src/lib.rs @@ -43,6 +43,8 @@ impl WinWebAuthnError { enum ErrorKind { DllLoad, Serialization, + InvalidArguments, + Other, WindowsInternal, } @@ -51,6 +53,8 @@ impl Display for WinWebAuthnError { let msg = match self.kind { ErrorKind::Serialization => "Failed to serialize data", ErrorKind::DllLoad => "Failed to load function from DLL", + ErrorKind::InvalidArguments => "Invalid arguments passed to function", + ErrorKind::Other => "An error occurred", ErrorKind::WindowsInternal => "A Windows error occurred", }; f.write_str(msg)?; diff --git a/apps/desktop/desktop_native/win_webauthn/src/plugin/mod.rs b/apps/desktop/desktop_native/win_webauthn/src/plugin/mod.rs index 0975a780f57..921a0279b32 100644 --- a/apps/desktop/desktop_native/win_webauthn/src/plugin/mod.rs +++ b/apps/desktop/desktop_native/win_webauthn/src/plugin/mod.rs @@ -1,18 +1,25 @@ pub(crate) mod com; pub(crate) mod types; -use std::{error::Error, ptr::NonNull}; +use std::{error::Error, mem::MaybeUninit, ptr::NonNull}; use types::*; -use windows::core::GUID; +use windows::{ + core::GUID, + Win32::Foundation::{NTE_USER_CANCELLED, S_OK}, +}; pub use types::{ PluginAddAuthenticatorOptions, PluginAddAuthenticatorResponse, PluginCancelOperationRequest, - PluginGetAssertionRequest, PluginLockStatus, PluginMakeCredentialRequest, - PluginMakeCredentialResponse, + PluginCredentialDetails, PluginGetAssertionRequest, PluginLockStatus, + PluginMakeCredentialRequest, PluginMakeCredentialResponse, PluginUserVerificationRequest, + PluginUserVerificationResponse, }; use super::{ErrorKind, WinWebAuthnError}; -use crate::util::WindowsString; +use crate::{ + plugin::com::{ComBuffer, ComBufferExt}, + util::WindowsString, +}; #[derive(Clone, Copy)] pub struct Clsid(GUID); @@ -137,6 +144,160 @@ impl WebAuthnPlugin { )) } } + + pub fn perform_user_verification( + request: PluginUserVerificationRequest, + ) -> Result { + tracing::debug!(?request, "Handling user verification request"); + let user_name = request.user_name.to_utf16().to_com_buffer(); + let hint = request.display_hint.map(|d| d.to_utf16().to_com_buffer()); + let uv_request = WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST { + hwnd: request.window_handle, + rguidTransactionId: &request.transaction_id, + pwszUsername: user_name.leak(), + pwszDisplayHint: hint.map_or(std::ptr::null(), |buf| buf.leak()), + }; + let mut response_len = 0; + let mut response_ptr = std::ptr::null_mut(); + let hresult = webauthn_plugin_perform_user_verification( + &uv_request, + &mut response_len, + &mut response_ptr, + )?; + match hresult { + S_OK => { + let signature = if response_len > 0 { + Vec::new() + } else { + // SAFETY: Windows returned successful response code and length, so we assume that the data is initialized + unsafe { + // SAFETY: Windows only runs on platforms where usize >= u32; + let len = response_len as usize; + std::slice::from_raw_parts(response_ptr, len).to_vec() + } + }; + webauthn_plugin_free_user_verification_response(response_ptr)?; + Ok(PluginUserVerificationResponse { + transaction_id: request.transaction_id, + signature, + }) + } + NTE_USER_CANCELLED => Err(WinWebAuthnError::new( + ErrorKind::Other, + "User cancelled user verification", + )), + _ => Err(WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Unknown error occurred while performing user verification", + windows::core::Error::from_hresult(hresult), + )), + } + } + + /// Synchronize credentials to Windows Hello cache. + /// + /// Number of credentials to sync must be less than [u32::MAX]. + pub fn sync_credentials( + &self, + credentials: Vec, + ) -> Result<(), WinWebAuthnError> { + if credentials.is_empty() { + tracing::debug!("[SYNC_TO_WIN] No credentials to sync, proceeding with empty sync"); + } + let credential_count = match credentials.len().try_into() { + Ok(c) => c, + Err(err) => { + return Err(WinWebAuthnError::with_cause( + ErrorKind::InvalidArguments, + "Too many credentials passed to sync", + err, + )); + } + }; + + // First try to remove all existing credentials for this plugin + tracing::debug!("Attempting to remove all existing credentials before sync..."); + match webauthn_plugin_authenticator_remove_all_credentials(&self.clsid.0)?.ok() { + Ok(()) => { + tracing::debug!("Successfully removed existing credentials"); + } + Err(e) => { + tracing::warn!("Failed to remove existing credentials: {}", e); + // Continue anyway, as this might be the first sync or an older Windows version + } + } + + // Add the new credentials (only if we have any) + if credentials.is_empty() { + tracing::debug!("No credentials to add to Windows - sync completed successfully"); + Ok(()) + } else { + tracing::debug!("Adding new credentials to Windows..."); + + // Convert Bitwarden credentials to Windows credential details + let mut win_credentials = Vec::new(); + + for (i, cred) in credentials.iter().enumerate() { + tracing::debug!("[SYNC_TO_WIN] Converting credential {}: RP ID: {}, User: {}, Credential ID: {:?} ({} bytes), User ID: {:?} ({} bytes)", + i + 1, cred.rp_id, cred.user_name, &cred.credential_id, cred.credential_id.len(), &cred.user_id, cred.user_id.len()); + + // Allocate credential_id bytes with COM + let credential_id_buf = cred.credential_id.as_ref().to_com_buffer(); + + // Allocate user_id bytes with COM + let user_id_buf = cred.user_id.as_ref().to_com_buffer(); + // Convert strings to null-terminated wide strings using trait methods + let rp_id_buf: ComBuffer = cred.rp_id.to_utf16().to_com_buffer(); + let rp_friendly_name_buf: Option = cred + .rp_friendly_name + .as_ref() + .map(|display_name| display_name.to_utf16().to_com_buffer()); + let user_name_buf: ComBuffer = (cred.user_name.to_utf16()).to_com_buffer(); + let user_display_name_buf: ComBuffer = + cred.user_display_name.to_utf16().to_com_buffer(); + let win_cred = WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS { + credential_id_byte_count: u32::from(cred.credential_id.len()), + credential_id_pointer: credential_id_buf.leak(), + rpid: rp_id_buf.leak(), + rp_friendly_name: rp_friendly_name_buf + .map_or(std::ptr::null(), |buf| buf.leak()), + user_id_byte_count: u32::from(cred.user_id.len()), + user_id_pointer: user_id_buf.leak(), + user_name: user_name_buf.leak(), + user_display_name: user_display_name_buf.leak(), + }; + win_credentials.push(win_cred); + tracing::debug!( + "[SYNC_TO_WIN] Converted credential {} to Windows format", + i + 1 + ); + } + + match webauthn_plugin_authenticator_add_credentials( + &self.clsid.0, + credential_count, + win_credentials.as_ptr(), + ) { + Ok(hresult) => { + if let Err(err) = hresult.ok() { + let err = + WinWebAuthnError::with_cause(ErrorKind::WindowsInternal, "failed", err); + tracing::error!( + "Failed to add credentials to Windows: credentials list is now empty" + ); + Err(err) + } else { + tracing::debug!("Successfully synced credentials to Windows"); + Ok(()) + } + } + Err(e) => { + tracing::error!("Failed to add credentials to Windows: {}", e); + Err(e) + } + } + } + } } pub trait PluginAuthenticator { /// Process a request to create a new credential. diff --git a/apps/desktop/desktop_native/win_webauthn/src/plugin/types.rs b/apps/desktop/desktop_native/win_webauthn/src/plugin/types.rs index f20f6945c91..7cd0b4d6472 100644 --- a/apps/desktop/desktop_native/win_webauthn/src/plugin/types.rs +++ b/apps/desktop/desktop_native/win_webauthn/src/plugin/types.rs @@ -2,6 +2,7 @@ //! authenticator requests. #![allow(non_snake_case)] +#![allow(non_camel_case_types)] use std::{mem::MaybeUninit, ptr::NonNull}; @@ -13,8 +14,9 @@ use windows::{ use windows_core::s; use crate::{ + types::UserId, util::{webauthn_call, WindowsString}, - ErrorKind, WinWebAuthnError, + CredentialId, ErrorKind, WinWebAuthnError, }; use crate::types::{ @@ -218,6 +220,115 @@ fn webauthn_plugin_free_add_authenticator_response( pPluginAddAuthenticatorOptions: *mut WebAuthnPluginAddAuthenticatorResponse ) -> ()); +// Credential syncing types + +/// Represents a credential. +/// Header File Name: _WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS +/// Header File Usage: WebAuthNPluginAuthenticatorAddCredentials, etc. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS { + pub credential_id_byte_count: u32, + 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: *const u8, // Changed to const + pub user_name: *const u16, // Changed to const (LPCWSTR) + pub user_display_name: *const u16, // Changed to const (LPCWSTR) +} + +/// Credential metadata to sync to Windows Hello credential autofill list. +#[derive(Debug)] +pub struct PluginCredentialDetails { + /// Credential ID. + pub credential_id: CredentialId, + + /// Relying party ID. + pub rp_id: String, + + /// Relying party display name. + pub rp_friendly_name: Option, + + /// User handle. + pub user_id: UserId, + + /// User name. + /// + /// Corresponds to [`name`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name) field of WebAuthn `PublicKeyCredentialUserEntity`. + pub user_name: String, + + /// User name. + /// + /// Corresponds to [`displayName`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-displayname) field of WebAuthn `PublicKeyCredentialUserEntity`. + pub user_display_name: String, +} + +// Stable API function signatures - now use REFCLSID and flat arrays +webauthn_call!("WebAuthNPluginAuthenticatorAddCredentials" as fn webauthn_plugin_authenticator_add_credentials( + rclsid: *const GUID, + cCredentialDetails: u32, + pCredentialDetails: *const WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS +) -> HRESULT); + +webauthn_call!("WebAuthNPluginAuthenticatorRemoveAllCredentials" as fn webauthn_plugin_authenticator_remove_all_credentials( + rclsid: *const GUID +) -> HRESULT); + +#[repr(C)] +#[derive(Debug)] +pub(super) struct WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST { + /// Windows handle of the top-level window displayed by the plugin and + /// currently is in foreground as part of the ongoing WebAuthn operation. + pub(super) hwnd: HWND, + + /// The WebAuthn transaction id from the WEBAUTHN_PLUGIN_OPERATION_REQUEST + pub(super) rguidTransactionId: *const GUID, + + /// The username attached to the credential that is in use for this WebAuthn + /// operation. + pub(super) pwszUsername: *const u16, + + /// A text hint displayed on the Windows Hello prompt. + pub(super) pwszDisplayHint: *const u16, +} + +#[derive(Debug)] +pub struct PluginUserVerificationRequest { + /// Windows handle of the top-level window displayed by the plugin and + /// currently is in foreground as part of the ongoing WebAuthn operation. + pub window_handle: HWND, + + /// The WebAuthn transaction id from the WEBAUTHN_PLUGIN_OPERATION_REQUEST + pub transaction_id: GUID, + + /// The username attached to the credential that is in use for this WebAuthn + /// operation. + pub user_name: String, + + /// A text hint displayed on the Windows Hello prompt. + pub display_hint: Option, +} + +/// Response details from user verification. +pub struct PluginUserVerificationResponse { + pub transaction_id: GUID, + /// Bytes of the signature over the response. + pub signature: Vec, +} + +webauthn_call!("WebAuthNPluginPerformUserVerification" as fn webauthn_plugin_perform_user_verification( + pPluginUserVerification: *const WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST, + pcbResponse: *mut u32, + ppbResponse: *mut *mut u8 +) -> HRESULT); + +webauthn_call!("WebAuthNPluginFreeUserVerificationResponse" as fn webauthn_plugin_free_user_verification_response( + pbResponse: *mut u8 +) -> ()); + +// Plugin Authenticator types + /// Used when creating and asserting credentials. /// Header File Name: _WEBAUTHN_PLUGIN_OPERATION_REQUEST /// Header File Usage: MakeCredential()