diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 5558e641da8..0e34d818763 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -194,7 +194,7 @@ export declare namespace passkey_authenticator { credentialId: string rpId: string userName: string - userId: string + userHandle: string } export interface PasskeyAssertionRequest { rpId: string @@ -206,7 +206,7 @@ export declare namespace passkey_authenticator { export interface PasskeyRegistrationRequest { rpId: string transactionId: string - userId: Array + userHandle: Array userName: string clientDataHash: Array userVerification: boolean diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index bb404fa211c..5d2a6074e45 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -825,7 +825,7 @@ pub mod passkey_authenticator { pub credential_id: String, // base64url encoded pub rp_id: String, pub user_name: String, - pub user_id: String, // base64url encoded + pub user_handle: String, // base64url encoded } #[napi(object)] @@ -846,7 +846,7 @@ pub mod passkey_authenticator { pub struct PasskeyRegistrationRequest { pub rp_id: String, pub transaction_id: String, - pub user_id: Vec, + pub user_handle: Vec, pub user_name: String, pub client_data_hash: Vec, pub user_verification: bool, diff --git a/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs b/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs index 85a77fef04b..5dcc9d1b576 100644 --- a/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs +++ b/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs @@ -98,7 +98,7 @@ impl From for SyncedCredential { .encode(&cred.credential_id), rp_id: cred.rp_id, user_name: cred.user_name, - user_id: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&cred.user_id), + user_handle: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&cred.user_handle), } } } @@ -112,8 +112,8 @@ impl From for windows_plugin_authenticator::SyncedCredential { .unwrap_or_default(), rp_id: cred.rp_id, user_name: cred.user_name, - user_id: base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(&cred.user_id) + user_handle: base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(&cred.user_handle) .unwrap_or_default(), } } @@ -134,10 +134,10 @@ pub fn sync_credentials_to_windows(credentials: Vec) -> napi:: } else { cred.credential_id.clone() }; - let truncated_user_id = if cred.user_id.len() > 16 { - format!("{}...", &cred.user_id[..16]) + let truncated_user_id = if cred.user_handle.len() > 16 { + format!("{}...", &cred.user_handle[..16]) } else { - cred.user_id.clone() + cred.user_handle.clone() }; log::info!( "[NAPI] Credential {}: RP={}, User={}, CredID={}, UserID={}", diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs index 59f769bf5d3..774773a368e 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs @@ -1,18 +1,19 @@ +use serde_json; use std::alloc::{alloc, Layout}; use std::ptr; -use serde_json; -use windows_core::{HRESULT, s}; +use windows_core::{s, HRESULT}; +use crate::com_provider::ExperimentalWebAuthnPluginOperationResponse; use crate::types::*; use crate::utils::{self as util, delay_load}; -use crate::com_provider::ExperimentalWebAuthnPluginOperationResponse; +use crate::webauthn::WEBAUTHN_CREDENTIAL_LIST; // Windows API types for WebAuthn (from webauthn.h.sample) #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST { pub dwVersion: u32, - pub pwszRpId: *const u16, // PCWSTR + pub pwszRpId: *const u16, // PCWSTR pub cbRpId: u32, pub pbRpId: *const u8, pub cbClientDataHash: u32, @@ -20,28 +21,13 @@ pub struct EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST { pub CredentialList: WEBAUTHN_CREDENTIAL_LIST, pub cbCborExtensionsMap: u32, pub pbCborExtensionsMap: *const u8, - pub pAuthenticatorOptions: *const crate::webauthn::ExperimentalWebAuthnCtapCborAuthenticatorOptions, + pub pAuthenticatorOptions: + *const crate::webauthn::ExperimentalWebAuthnCtapCborAuthenticatorOptions, // Add other fields as needed... } -pub type PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST = *mut EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST; - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct WEBAUTHN_CREDENTIAL_EX { - pub dwVersion: u32, - pub cbId: u32, - pub pbId: *const u8, - pub pwszCredentialType: *const u16, // LPCWSTR - pub dwTransports: u32, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct WEBAUTHN_CREDENTIAL_LIST { - pub cCredentials: u32, - pub ppCredentials: *const *const WEBAUTHN_CREDENTIAL_EX, -} +pub type PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST = + *mut EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST; // Windows API function signatures for decoding get assertion requests type EXPERIMENTAL_WebAuthNDecodeGetAssertionRequestFn = unsafe extern "stdcall" fn( @@ -61,10 +47,13 @@ pub struct DecodedGetAssertionRequest { } impl DecodedGetAssertionRequest { - fn new(ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST, free_fn: Option) -> Self { + fn new( + ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST, + free_fn: Option, + ) -> Self { Self { ptr, free_fn } } - + pub fn as_ref(&self) -> &EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST { unsafe { &*self.ptr } } @@ -75,44 +64,55 @@ impl Drop for DecodedGetAssertionRequest { if !self.ptr.is_null() { if let Some(free_fn) = self.free_fn { util::message("Freeing decoded get assertion request"); - unsafe { free_fn(self.ptr); } + unsafe { + free_fn(self.ptr); + } } } } } // Function to decode get assertion request using Windows API -pub unsafe fn decode_get_assertion_request(encoded_request: &[u8]) -> Result { +pub unsafe fn decode_get_assertion_request( + encoded_request: &[u8], +) -> Result { util::message("Attempting to decode get assertion request using Windows API"); - + // Load the Windows WebAuthn API function let decode_fn: Option = delay_load( s!("webauthn.dll"), - s!("EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest") + s!("EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest"), ); - - let decode_fn = decode_fn.ok_or("Failed to load EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest from webauthn.dll")?; - + + let decode_fn = decode_fn + .ok_or("Failed to load EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest from webauthn.dll")?; + // Load the free function let free_fn: Option = delay_load( s!("webauthn.dll"), - s!("EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequest") + s!("EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequest"), ); - - let mut pp_get_assertion_request: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST = ptr::null_mut(); - + + let mut pp_get_assertion_request: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST = + ptr::null_mut(); + let result = decode_fn( encoded_request.len() as u32, encoded_request.as_ptr(), &mut pp_get_assertion_request, ); - + if result.is_err() || pp_get_assertion_request.is_null() { - return Err(format!("EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest failed with HRESULT: {}", result.0)); + return Err(format!( + "EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest failed with HRESULT: {}", + result.0 + )); } - - - Ok(DecodedGetAssertionRequest::new(pp_get_assertion_request, free_fn)) + + Ok(DecodedGetAssertionRequest::new( + pp_get_assertion_request, + free_fn, + )) } /// Windows WebAuthn assertion request context @@ -124,65 +124,11 @@ pub struct WindowsAssertionRequest { pub user_verification: UserVerificationRequirement, } -/// Windows WebAuthn registration request context -#[derive(Debug, Clone)] -pub struct WindowsRegistrationRequest { - pub rpid: String, - pub user_id: Vec, - pub user_name: String, - pub user_display_name: Option, - pub client_data_hash: Vec, - pub excluded_credentials: Vec>, - pub user_verification: UserVerificationRequirement, - pub supported_algorithms: Vec, -} - -/// Parse allowed credentials from WEBAUTHN_CREDENTIAL_LIST -pub unsafe fn parse_credential_list(credential_list: &WEBAUTHN_CREDENTIAL_LIST) -> Vec> { - let mut allowed_credentials = Vec::new(); - - if credential_list.cCredentials == 0 || credential_list.ppCredentials.is_null() { - util::message("No credentials in credential list"); - return allowed_credentials; - } - - util::message(&format!("Parsing {} credentials from credential list", credential_list.cCredentials)); - - // ppCredentials is an array of pointers to WEBAUTHN_CREDENTIAL_EX - let credentials_array = std::slice::from_raw_parts( - credential_list.ppCredentials, - credential_list.cCredentials as usize - ); - - for (i, &credential_ptr) in credentials_array.iter().enumerate() { - if credential_ptr.is_null() { - util::message(&format!("WARNING: Credential {} is null, skipping", i)); - continue; - } - - let credential = &*credential_ptr; - - if credential.cbId == 0 || credential.pbId.is_null() { - util::message(&format!("WARNING: Credential {} has invalid ID, skipping", i)); - continue; - } - - // Extract credential ID bytes - let credential_id_slice = std::slice::from_raw_parts( - credential.pbId, - credential.cbId as usize - ); - - allowed_credentials.push(credential_id_slice.to_vec()); - util::message(&format!("Parsed credential {}: {} bytes", i, credential.cbId)); - } - - util::message(&format!("Successfully parsed {} allowed credentials", allowed_credentials.len())); - allowed_credentials -} - /// Helper for assertion requests -pub fn send_assertion_request(transaction_id: &str, request: &WindowsAssertionRequest) -> Option { +pub fn send_assertion_request( + transaction_id: &str, + request: &WindowsAssertionRequest, +) -> Option { let passkey_request = PasskeyAssertionRequest { rp_id: request.rpid.clone(), transaction_id: transaction_id.to_string(), @@ -190,17 +136,24 @@ pub fn send_assertion_request(transaction_id: &str, request: &WindowsAssertionRe allowed_credentials: request.allowed_credentials.clone(), user_verification: request.user_verification.clone(), }; - - util::message(&format!("Assertion request data - RP ID: {}, Client data hash: {} bytes, Allowed credentials: {}", - request.rpid, request.client_data_hash.len(), request.allowed_credentials.len())); - + + util::message(&format!( + "Assertion request data - RP ID: {}, Client data hash: {} bytes, Allowed credentials: {}", + request.rpid, + request.client_data_hash.len(), + request.allowed_credentials.len() + )); + match serde_json::to_string(&passkey_request) { Ok(request_json) => { util::message(&format!("Sending assertion request: {}", request_json)); crate::ipc::send_passkey_request(RequestType::Assertion, request_json, &request.rpid) - }, + } Err(e) => { - util::message(&format!("ERROR: Failed to serialize assertion request: {}", e)); + util::message(&format!( + "ERROR: Failed to serialize assertion request: {}", + e + )); None } } @@ -211,79 +164,93 @@ pub unsafe fn create_get_assertion_response( credential_id: Vec, authenticator_data: Vec, signature: Vec, - user_handle: Vec + user_handle: Vec, ) -> std::result::Result<*mut ExperimentalWebAuthnPluginOperationResponse, HRESULT> { // Construct a CTAP2 response with the proper structure - + // Create CTAP2 GetAssertion response map according to CTAP2 specification let mut cbor_response: Vec<(ciborium::Value, ciborium::Value)> = Vec::new(); - + // [1] credential (optional) - Always include credential descriptor let credential_map = vec![ - (ciborium::Value::Text("id".to_string()), ciborium::Value::Bytes(credential_id.clone())), - (ciborium::Value::Text("type".to_string()), ciborium::Value::Text("public-key".to_string())), + ( + ciborium::Value::Text("id".to_string()), + ciborium::Value::Bytes(credential_id.clone()), + ), + ( + ciborium::Value::Text("type".to_string()), + ciborium::Value::Text("public-key".to_string()), + ), ]; cbor_response.push(( - ciborium::Value::Integer(1.into()), - ciborium::Value::Map(credential_map) + ciborium::Value::Integer(1.into()), + ciborium::Value::Map(credential_map), )); - + // [2] authenticatorData (required) cbor_response.push(( - ciborium::Value::Integer(2.into()), - ciborium::Value::Bytes(authenticator_data) + ciborium::Value::Integer(2.into()), + ciborium::Value::Bytes(authenticator_data), )); - + // [3] signature (required) cbor_response.push(( - ciborium::Value::Integer(3.into()), - ciborium::Value::Bytes(signature) + ciborium::Value::Integer(3.into()), + ciborium::Value::Bytes(signature), )); - + // [4] user (optional) - include if user handle is provided if !user_handle.is_empty() { - let user_map = vec![ - (ciborium::Value::Text("id".to_string()), ciborium::Value::Bytes(user_handle)), - ]; + let user_map = vec![( + ciborium::Value::Text("id".to_string()), + ciborium::Value::Bytes(user_handle), + )]; cbor_response.push(( - ciborium::Value::Integer(4.into()), - ciborium::Value::Map(user_map) + ciborium::Value::Integer(4.into()), + ciborium::Value::Map(user_map), )); } - + let cbor_value = ciborium::Value::Map(cbor_response); - + // Encode to CBOR with error handling let mut cbor_data = Vec::new(); if let Err(e) = ciborium::ser::into_writer(&cbor_value, &mut cbor_data) { - util::message(&format!("ERROR: Failed to encode CBOR assertion response: {:?}", e)); + util::message(&format!( + "ERROR: Failed to encode CBOR assertion response: {:?}", + e + )); return Err(HRESULT(-1)); } - + let response_len = cbor_data.len(); - + // Allocate memory for the response data let layout = Layout::from_size_align(response_len, 1).map_err(|_| HRESULT(-1))?; let response_ptr = alloc(layout); if response_ptr.is_null() { return Err(HRESULT(-1)); } - + // Copy response data ptr::copy_nonoverlapping(cbor_data.as_ptr(), response_ptr, response_len); - + // Allocate memory for the response structure let response_layout = Layout::new::(); - let operation_response_ptr = alloc(response_layout) as *mut ExperimentalWebAuthnPluginOperationResponse; + let operation_response_ptr = + alloc(response_layout) as *mut ExperimentalWebAuthnPluginOperationResponse; if operation_response_ptr.is_null() { return Err(HRESULT(-1)); } - + // Initialize the response - ptr::write(operation_response_ptr, ExperimentalWebAuthnPluginOperationResponse { - encoded_response_byte_count: response_len as u32, - encoded_response_pointer: response_ptr, - }); - + ptr::write( + operation_response_ptr, + ExperimentalWebAuthnPluginOperationResponse { + encoded_response_byte_count: response_len as u32, + encoded_response_pointer: response_ptr, + }, + ); + Ok(operation_response_ptr) -} \ No newline at end of file +} 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 159af6c9a87..03b5714901b 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_provider.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_provider.rs @@ -1,11 +1,18 @@ +use std::ptr; use windows::Win32::System::Com::*; use windows_core::{implement, interface, IInspectable, IUnknown, Interface, HRESULT}; -use std::ptr; +use crate::assert::{ + create_get_assertion_response, decode_get_assertion_request, send_assertion_request, + WindowsAssertionRequest, +}; +use crate::make_credential::{ + create_make_credential_response, decode_make_credential_request, send_registration_request, + WindowsRegistrationRequest, +}; use crate::types::*; use crate::utils::{self as util, wstr_to_string}; -use crate::assert::{WindowsAssertionRequest, decode_get_assertion_request, create_get_assertion_response, send_assertion_request, parse_credential_list}; -use crate::make_credential::{WindowsRegistrationRequest, decode_make_credential_request, create_make_credential_response, send_registration_request}; +use crate::webauthn::WEBAUTHN_CREDENTIAL_LIST; /// Used when creating and asserting credentials. /// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST @@ -62,6 +69,59 @@ pub unsafe trait EXPERIMENTAL_IPluginAuthenticator: windows_core::IUnknown { ) -> HRESULT; } +pub unsafe fn parse_credential_list(credential_list: &WEBAUTHN_CREDENTIAL_LIST) -> Vec> { + let mut allowed_credentials = Vec::new(); + + if credential_list.cCredentials == 0 || credential_list.ppCredentials.is_null() { + util::message("No credentials in credential list"); + return allowed_credentials; + } + + util::message(&format!( + "Parsing {} credentials from credential list", + credential_list.cCredentials + )); + + // ppCredentials is an array of pointers to WEBAUTHN_CREDENTIAL_EX + let credentials_array = std::slice::from_raw_parts( + credential_list.ppCredentials, + credential_list.cCredentials as usize, + ); + + for (i, &credential_ptr) in credentials_array.iter().enumerate() { + if credential_ptr.is_null() { + util::message(&format!("WARNING: Credential {} is null, skipping", i)); + continue; + } + + let credential = &*credential_ptr; + + if credential.cbId == 0 || credential.pbId.is_null() { + util::message(&format!( + "WARNING: Credential {} has invalid ID, skipping", + i + )); + continue; + } + + // Extract credential ID bytes + let credential_id_slice = + std::slice::from_raw_parts(credential.pbId, credential.cbId as usize); + + allowed_credentials.push(credential_id_slice.to_vec()); + util::message(&format!( + "Parsed credential {}: {} bytes", + i, credential.cbId + )); + } + + util::message(&format!( + "Successfully parsed {} allowed credentials", + allowed_credentials.len() + )); + allowed_credentials +} + #[implement(EXPERIMENTAL_IPluginAuthenticator)] pub struct PluginAuthenticatorComObject; @@ -75,51 +135,60 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, ) -> HRESULT { util::message("=== EXPERIMENTAL_PluginMakeCredential() called ==="); - + if request.is_null() { util::message("ERROR: NULL request pointer"); return HRESULT(-1); } - + if response.is_null() { util::message("ERROR: NULL response pointer"); return HRESULT(-1); } - + let req = &*request; let transaction_id = format!("{:?}", req.transaction_id); - + util::message(&format!("Transaction ID: {}", transaction_id)); util::message(&format!("Window Handle: {:?}", req.window_handle)); - util::message(&format!("Request Signature Byte Count: {}", req.request_signature_byte_count)); - util::message(&format!("Encoded Request Byte Count: {}", req.encoded_request_byte_count)); - + util::message(&format!( + "Request Signature Byte Count: {}", + req.request_signature_byte_count + )); + util::message(&format!( + "Encoded Request Byte Count: {}", + req.encoded_request_byte_count + )); + if req.encoded_request_byte_count == 0 || req.encoded_request_pointer.is_null() { util::message("ERROR: No encoded request data provided"); return HRESULT(-1); } - + let encoded_request_slice = std::slice::from_raw_parts( - req.encoded_request_pointer, - req.encoded_request_byte_count as usize + req.encoded_request_pointer, + req.encoded_request_byte_count as usize, ); - - util::message(&format!("Encoded request: {} bytes", encoded_request_slice.len())); - + + util::message(&format!( + "Encoded request: {} bytes", + encoded_request_slice.len() + )); + // Try to decode the request using Windows API match decode_make_credential_request(encoded_request_slice) { Ok(decoded_wrapper) => { let decoded_request = decoded_wrapper.as_ref(); util::message("Successfully decoded make credential request using Windows API"); - + // Extract RP information if decoded_request.pRpInformation.is_null() { util::message("ERROR: RP information is null"); return HRESULT(-1); } - + let rp_info = &*decoded_request.pRpInformation; - + let rpid = if rp_info.pwszId.is_null() { util::message("ERROR: RP ID is null"); return HRESULT(-1); @@ -132,21 +201,21 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp } } }; - + let rp_name = if rp_info.pwszName.is_null() { String::new() } else { wstr_to_string(rp_info.pwszName).unwrap_or_default() }; - + // Extract user information if decoded_request.pUserInformation.is_null() { util::message("ERROR: User information is null"); return HRESULT(-1); } - + let user = &*decoded_request.pUserInformation; - + let user_id = if user.pbId.is_null() || user.cbId == 0 { util::message("ERROR: User ID is required for registration"); return HRESULT(-1); @@ -154,7 +223,7 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp let id_slice = std::slice::from_raw_parts(user.pbId, user.cbId as usize); id_slice.to_vec() }; - + let user_name = if user.pwszName.is_null() { util::message("ERROR: User name is required for registration"); return HRESULT(-1); @@ -167,51 +236,65 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp } } }; - + let user_display_name = if user.pwszDisplayName.is_null() { None } else { wstr_to_string(user.pwszDisplayName).ok() }; - + let user_info = (user_id, user_name, user_display_name); - + // Extract client data hash - let client_data_hash = if decoded_request.cbClientDataHash == 0 || decoded_request.pbClientDataHash.is_null() { + let client_data_hash = if decoded_request.cbClientDataHash == 0 + || decoded_request.pbClientDataHash.is_null() + { util::message("ERROR: Client data hash is required for registration"); return HRESULT(-1); } else { let hash_slice = std::slice::from_raw_parts( - decoded_request.pbClientDataHash, - decoded_request.cbClientDataHash as usize + decoded_request.pbClientDataHash, + decoded_request.cbClientDataHash as usize, ); hash_slice.to_vec() }; - + // Extract RP ID raw bytes for authenticator data - let rpid_bytes = if decoded_request.cbRpId > 0 && !decoded_request.pbRpId.is_null() { + let rpid_bytes = if decoded_request.cbRpId > 0 && !decoded_request.pbRpId.is_null() + { let rpid_slice = std::slice::from_raw_parts( decoded_request.pbRpId, - decoded_request.cbRpId as usize + decoded_request.cbRpId as usize, ); rpid_slice.to_vec() } else { rpid.as_bytes().to_vec() }; - + // Extract supported algorithms - let supported_algorithms = if decoded_request.WebAuthNCredentialParameters.cCredentialParameters > 0 && - !decoded_request.WebAuthNCredentialParameters.pCredentialParameters.is_null() { - let params_count = decoded_request.WebAuthNCredentialParameters.cCredentialParameters as usize; - let params_ptr = decoded_request.WebAuthNCredentialParameters.pCredentialParameters; - + let supported_algorithms = if decoded_request + .WebAuthNCredentialParameters + .cCredentialParameters + > 0 + && !decoded_request + .WebAuthNCredentialParameters + .pCredentialParameters + .is_null() + { + let params_count = decoded_request + .WebAuthNCredentialParameters + .cCredentialParameters as usize; + let params_ptr = decoded_request + .WebAuthNCredentialParameters + .pCredentialParameters; + (0..params_count) .map(|i| unsafe { &*params_ptr.add(i) }.lAlg) .collect() } else { Vec::new() }; - + // Extract user verification requirement from authenticator options let user_verification = if !decoded_request.pAuthenticatorOptions.is_null() { let auth_options = &*decoded_request.pAuthenticatorOptions; @@ -223,13 +306,16 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp } else { None }; - + // Extract excluded credentials from credential list (for make credential, these are credentials to exclude) let excluded_credentials = parse_credential_list(&decoded_request.CredentialList); if !excluded_credentials.is_empty() { - util::message(&format!("Found {} excluded credentials for make credential", excluded_credentials.len())); + util::message(&format!( + "Found {} excluded credentials for make credential", + excluded_credentials.len() + )); } - + // Create Windows registration request let registration_request = WindowsRegistrationRequest { rpid: rpid.clone(), @@ -241,40 +327,55 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp user_verification: user_verification.unwrap_or_default(), supported_algorithms, }; - - util::message(&format!("Make credential request - RP: {}, User: {}", - rpid, - registration_request.user_name)); - + + util::message(&format!( + "Make credential request - RP: {}, User: {}", + rpid, registration_request.user_name + )); + // Send registration request - if let Some(passkey_response) = send_registration_request(&transaction_id, ®istration_request) { - util::message(&format!("Registration response received: {:?}", passkey_response)); - + if let Some(passkey_response) = + send_registration_request(&transaction_id, ®istration_request) + { + util::message(&format!( + "Registration response received: {:?}", + passkey_response + )); + // Create proper WebAuthn response from passkey_response match passkey_response { - PasskeyResponse::RegistrationResponse { credential_id, attestation_object } => { + PasskeyResponse::RegistrationResponse { + credential_id, + attestation_object, + } => { util::message("Creating WebAuthn make credential response"); - - match create_make_credential_response(credential_id, attestation_object) { + + match create_make_credential_response(credential_id, attestation_object) + { Ok(webauthn_response) => { util::message("Successfully created WebAuthn response"); *response = webauthn_response; HRESULT(0) - }, + } Err(e) => { - util::message(&format!("ERROR: Failed to create WebAuthn response: {}", e)); + util::message(&format!( + "ERROR: Failed to create WebAuthn response: {}", + e + )); *response = ptr::null_mut(); HRESULT(-1) } } - }, + } PasskeyResponse::Error { message } => { util::message(&format!("Registration request failed: {}", message)); *response = ptr::null_mut(); HRESULT(-1) - }, + } _ => { - util::message("ERROR: Unexpected response type for registration request"); + util::message( + "ERROR: Unexpected response type for registration request", + ); *response = ptr::null_mut(); HRESULT(-1) } @@ -284,9 +385,12 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp *response = ptr::null_mut(); HRESULT(-1) } - }, + } Err(e) => { - util::message(&format!("ERROR: Failed to decode make credential request: {}", e)); + util::message(&format!( + "ERROR: Failed to decode make credential request: {}", + e + )); *response = ptr::null_mut(); HRESULT(-1) } @@ -299,35 +403,38 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, ) -> HRESULT { util::message("EXPERIMENTAL_PluginGetAssertion() called"); - + // Validate input parameters if request.is_null() || response.is_null() { util::message("Invalid parameters passed to EXPERIMENTAL_PluginGetAssertion"); return HRESULT(-1); } - + let req = &*request; let transaction_id = format!("{:?}", req.transaction_id); - - util::message(&format!("Get assertion request - Transaction: {}", transaction_id)); - + + util::message(&format!( + "Get assertion request - Transaction: {}", + transaction_id + )); + if req.encoded_request_byte_count == 0 || req.encoded_request_pointer.is_null() { util::message("ERROR: No encoded request data provided"); *response = ptr::null_mut(); return HRESULT(-1); } - + let encoded_request_slice = std::slice::from_raw_parts( - req.encoded_request_pointer, - req.encoded_request_byte_count as usize + req.encoded_request_pointer, + req.encoded_request_byte_count as usize, ); - + // Try to decode the request using Windows API match decode_get_assertion_request(encoded_request_slice) { Ok(decoded_wrapper) => { let decoded_request = decoded_wrapper.as_ref(); util::message("Successfully decoded get assertion request using Windows API"); - + // Extract RP information let rpid = if decoded_request.pwszRpId.is_null() { util::message("ERROR: RP ID is null"); @@ -343,20 +450,22 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp } } }; - + // Extract client data hash - let client_data_hash = if decoded_request.cbClientDataHash == 0 || decoded_request.pbClientDataHash.is_null() { + let client_data_hash = if decoded_request.cbClientDataHash == 0 + || decoded_request.pbClientDataHash.is_null() + { util::message("ERROR: Client data hash is required for assertion"); *response = ptr::null_mut(); return HRESULT(-1); } else { let hash_slice = std::slice::from_raw_parts( - decoded_request.pbClientDataHash, - decoded_request.cbClientDataHash as usize + decoded_request.pbClientDataHash, + decoded_request.cbClientDataHash as usize, ); hash_slice.to_vec() }; - + // Extract user verification requirement from authenticator options let user_verification = if !decoded_request.pAuthenticatorOptions.is_null() { let auth_options = &*decoded_request.pAuthenticatorOptions; @@ -368,10 +477,10 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp } else { None }; - + // Extract allowed credentials from credential list let allowed_credentials = parse_credential_list(&decoded_request.CredentialList); - + // Create Windows assertion request let assertion_request = WindowsAssertionRequest { rpid: rpid.clone(), @@ -379,36 +488,60 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp allowed_credentials: allowed_credentials.clone(), user_verification: user_verification.unwrap_or_default(), }; - - util::message(&format!("Get assertion request - RP: {}, Allowed credentials: {}", rpid, allowed_credentials.len())); - + + util::message(&format!( + "Get assertion request - RP: {}, Allowed credentials: {}", + rpid, + allowed_credentials.len() + )); + // Send assertion request - if let Some(passkey_response) = send_assertion_request(&transaction_id, &assertion_request) { - util::message(&format!("Assertion response received: {:?}", passkey_response)); - + if let Some(passkey_response) = + send_assertion_request(&transaction_id, &assertion_request) + { + util::message(&format!( + "Assertion response received: {:?}", + passkey_response + )); + // Create proper WebAuthn response from passkey_response match passkey_response { - PasskeyResponse::AssertionResponse { credential_id, authenticator_data, signature, user_handle } => { + PasskeyResponse::AssertionResponse { + credential_id, + authenticator_data, + signature, + user_handle, + } => { util::message("Creating WebAuthn get assertion response"); - - match create_get_assertion_response(credential_id, authenticator_data, signature, user_handle) { + + match create_get_assertion_response( + credential_id, + authenticator_data, + signature, + user_handle, + ) { Ok(webauthn_response) => { - util::message("Successfully created WebAuthn assertion response"); + util::message( + "Successfully created WebAuthn assertion response", + ); *response = webauthn_response; HRESULT(0) - }, + } Err(e) => { - util::message(&format!("ERROR: Failed to create WebAuthn assertion response: {}", e)); + util::message(&format!( + "ERROR: Failed to create WebAuthn assertion response: {}", + e + )); *response = ptr::null_mut(); HRESULT(-1) } } - }, + } PasskeyResponse::Error { message } => { util::message(&format!("Assertion request failed: {}", message)); *response = ptr::null_mut(); HRESULT(-1) - }, + } _ => { util::message("ERROR: Unexpected response type for assertion request"); *response = ptr::null_mut(); @@ -420,9 +553,12 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp *response = ptr::null_mut(); HRESULT(-1) } - }, + } Err(e) => { - util::message(&format!("ERROR: Failed to decode get assertion request: {}", e)); + util::message(&format!( + "ERROR: Failed to decode get assertion request: {}", + e + )); *response = ptr::null_mut(); HRESULT(-1) } @@ -445,11 +581,11 @@ impl IClassFactory_Impl for Factory_Impl { iid: *const windows_core::GUID, object: *mut *mut core::ffi::c_void, ) -> windows_core::Result<()> { - let unknown: IInspectable = PluginAuthenticatorComObject.into(); // TODO: IUnknown ? + let unknown: IInspectable = PluginAuthenticatorComObject.into(); // TODO: IUnknown ? unsafe { unknown.query(iid, object).ok() } } fn LockServer(&self, lock: windows_core::BOOL) -> windows_core::Result<()> { Ok(()) } -} \ No newline at end of file +} 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 218d65a54db..fbeefdd879e 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs @@ -9,23 +9,26 @@ use windows_core::*; // New modular structure mod assert; +mod com_buffer; +mod com_provider; +mod com_registration; +mod ipc; mod make_credential; mod sync; -mod ipc; -mod com_registration; -pub mod utils; mod types; +pub mod utils; mod webauthn; -mod com_provider; -mod com_buffer; - // Re-export main functionality -pub use sync::{send_sync_request, sync_credentials_to_windows, get_credentials_from_windows}; -pub use ipc::{set_request_sender, send_passkey_request}; -pub use types::{PasskeyRequest, PasskeyResponse, SyncedCredential, RequestEvent, RequestType, UserVerificationRequirement}; -pub use assert::{WindowsAssertionRequest, WindowsRegistrationRequest}; -pub use com_registration::{initialize_com_library, register_com_library, add_authenticator}; +pub use assert::WindowsAssertionRequest; +pub use com_registration::{add_authenticator, initialize_com_library, register_com_library}; +pub use ipc::{send_passkey_request, set_request_sender}; +pub use make_credential::WindowsRegistrationRequest; +pub use sync::{get_credentials_from_windows, send_sync_request, sync_credentials_to_windows}; +pub use types::{ + PasskeyRequest, PasskeyResponse, RequestEvent, RequestType, SyncedCredential, + UserVerificationRequirement, +}; // Re-export utilities pub use utils as util; @@ -60,7 +63,7 @@ pub fn register() -> std::result::Result<(), String> { let r = syncCredentials(); util::message(&format!("sync credentials: {:?}", r)); - + if let Err(e) = r { util::message(&format!("syncCredentials failed: {}", e)); } @@ -72,22 +75,17 @@ fn syncCredentials() -> std::result::Result<(), String> { // Create a test credential using the new sync module with more realistic data let test_credential = types::SyncedCredential { credential_id: vec![ - 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, - 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, - 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, + 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, ], // 32 byte credential ID rp_id: "webauthn.io".to_string(), user_name: "testuser".to_string(), - user_id: vec![0x75, 0x73, 0x65, 0x72, 0x31, 0x32, 0x33, 0x34], // "user1234" as bytes + user_handle: vec![0x75, 0x73, 0x65, 0x72, 0x31, 0x32, 0x33, 0x34], // "user1234" as bytes }; - + let credentials = vec![test_credential]; - + // Use the sync module to sync credentials sync_credentials_to_windows(credentials, CLSID) } - - - - diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs index 44cf8099318..7cb63e047ff 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs @@ -1,31 +1,44 @@ +use serde_json; use std::alloc::{alloc, Layout}; use std::ptr; -use serde_json; -use windows_core::{HRESULT, s}; +use windows_core::{s, HRESULT}; +use crate::com_provider::ExperimentalWebAuthnPluginOperationResponse; use crate::types::*; use crate::utils::{self as util, delay_load}; -use crate::com_provider::ExperimentalWebAuthnPluginOperationResponse; -use crate::assert::{WindowsRegistrationRequest, parse_credential_list}; +use crate::webauthn::WEBAUTHN_CREDENTIAL_LIST; + +/// Windows WebAuthn registration request context +#[derive(Debug, Clone)] +pub struct WindowsRegistrationRequest { + pub rpid: String, + pub user_id: Vec, + pub user_name: String, + pub user_display_name: Option, + pub client_data_hash: Vec, + pub excluded_credentials: Vec>, + pub user_verification: UserVerificationRequirement, + pub supported_algorithms: Vec, +} // Windows API types for WebAuthn (from webauthn.h.sample) #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct WEBAUTHN_RP_ENTITY_INFORMATION { pub dwVersion: u32, - pub pwszId: *const u16, // PCWSTR - pub pwszName: *const u16, // PCWSTR - pub pwszIcon: *const u16, // PCWSTR + pub pwszId: *const u16, // PCWSTR + pub pwszName: *const u16, // PCWSTR + pub pwszIcon: *const u16, // PCWSTR } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct WEBAUTHN_USER_ENTITY_INFORMATION { pub dwVersion: u32, - pub cbId: u32, // DWORD - pub pbId: *const u8, // PBYTE - pub pwszName: *const u16, // PCWSTR - pub pwszIcon: *const u16, // PCWSTR + pub cbId: u32, // DWORD + pub pbId: *const u8, // PBYTE + pub pwszName: *const u16, // PCWSTR + pub pwszIcon: *const u16, // PCWSTR pub pwszDisplayName: *const u16, // PCWSTR } @@ -33,8 +46,8 @@ pub struct WEBAUTHN_USER_ENTITY_INFORMATION { #[derive(Debug, Copy, Clone)] pub struct WEBAUTHN_COSE_CREDENTIAL_PARAMETER { pub dwVersion: u32, - pub pwszCredentialType: *const u16, // LPCWSTR - pub lAlg: i32, // LONG - COSE algorithm identifier + pub pwszCredentialType: *const u16, // LPCWSTR + pub lAlg: i32, // LONG - COSE algorithm identifier } #[repr(C)] @@ -44,23 +57,6 @@ pub struct WEBAUTHN_COSE_CREDENTIAL_PARAMETERS { pub pCredentialParameters: *const WEBAUTHN_COSE_CREDENTIAL_PARAMETER, } -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct WEBAUTHN_CREDENTIAL_EX { - pub dwVersion: u32, - pub cbId: u32, - pub pbId: *const u8, - pub pwszCredentialType: *const u16, // LPCWSTR - pub dwTransports: u32, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct WEBAUTHN_CREDENTIAL_LIST { - pub cCredentials: u32, - pub ppCredentials: *const *const WEBAUTHN_CREDENTIAL_EX, -} - // Make Credential Request structure (from sample header) #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -72,15 +68,17 @@ pub struct EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST { pub pbClientDataHash: *const u8, pub pRpInformation: *const WEBAUTHN_RP_ENTITY_INFORMATION, pub pUserInformation: *const WEBAUTHN_USER_ENTITY_INFORMATION, - pub WebAuthNCredentialParameters: WEBAUTHN_COSE_CREDENTIAL_PARAMETERS, // Matches C++ sample + pub WebAuthNCredentialParameters: WEBAUTHN_COSE_CREDENTIAL_PARAMETERS, // Matches C++ sample pub CredentialList: WEBAUTHN_CREDENTIAL_LIST, pub cbCborExtensionsMap: u32, pub pbCborExtensionsMap: *const u8, - pub pAuthenticatorOptions: *const crate::webauthn::ExperimentalWebAuthnCtapCborAuthenticatorOptions, + pub pAuthenticatorOptions: + *const crate::webauthn::ExperimentalWebAuthnCtapCborAuthenticatorOptions, // Add other fields as needed... } -pub type PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST = *mut EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST; +pub type PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST = + *mut EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST; // Windows API function signatures type EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequestFn = unsafe extern "stdcall" fn( @@ -100,10 +98,13 @@ pub struct DecodedMakeCredentialRequest { } impl DecodedMakeCredentialRequest { - fn new(ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, free_fn: Option) -> Self { + fn new( + ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, + free_fn: Option, + ) -> Self { Self { ptr, free_fn } } - + pub fn as_ref(&self) -> &EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST { unsafe { &*self.ptr } } @@ -114,16 +115,20 @@ impl Drop for DecodedMakeCredentialRequest { if !self.ptr.is_null() { if let Some(free_fn) = self.free_fn { util::message("Freeing decoded make credential request"); - unsafe { free_fn(self.ptr); } + unsafe { + free_fn(self.ptr); + } } } } } // Function to decode make credential request using Windows API -pub unsafe fn decode_make_credential_request(encoded_request: &[u8]) -> Result { +pub unsafe fn decode_make_credential_request( + encoded_request: &[u8], +) -> Result { util::message("Attempting to decode make credential request using Windows API"); - + // Try to load the Windows API decode function let decode_fn = match delay_load::( s!("webauthn.dll"), @@ -131,102 +136,120 @@ pub unsafe fn decode_make_credential_request(encoded_request: &[u8]) -> Result func, None => { - return Err("Failed to load EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest from webauthn.dll".to_string()); + return Err( + "Failed to load EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest from webauthn.dll" + .to_string(), + ); } }; - + // Try to load the free function (optional, might not be available in all versions) let free_fn = delay_load::( s!("webauthn.dll"), s!("EXPERIMENTAL_WebAuthNFreeDecodedMakeCredentialRequest"), ); - - + // Prepare parameters for the API call let cb_encoded = encoded_request.len() as u32; let pb_encoded = encoded_request.as_ptr(); - let mut pp_make_credential_request: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST = std::ptr::null_mut(); - - + let mut pp_make_credential_request: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST = + std::ptr::null_mut(); + // Call the Windows API function - let result = decode_fn( - cb_encoded, - pb_encoded, - &mut pp_make_credential_request, - ); - + let result = decode_fn(cb_encoded, pb_encoded, &mut pp_make_credential_request); + // Check if the call succeeded (following C++ THROW_IF_FAILED pattern) if result.is_err() { - util::message(&format!("ERROR: EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest failed with HRESULT: 0x{:08x}", result.0)); - return Err(format!("Windows API call failed with HRESULT: 0x{:08x}", result.0)); + util::message(&format!( + "ERROR: EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest failed with HRESULT: 0x{:08x}", + result.0 + )); + return Err(format!( + "Windows API call failed with HRESULT: 0x{:08x}", + result.0 + )); } - + if pp_make_credential_request.is_null() { util::message("ERROR: Windows API succeeded but returned null pointer"); return Err("Windows API returned null pointer".to_string()); } - - - Ok(DecodedMakeCredentialRequest::new(pp_make_credential_request, free_fn)) + + Ok(DecodedMakeCredentialRequest::new( + pp_make_credential_request, + free_fn, + )) } /// Helper for registration requests -pub fn send_registration_request(transaction_id: &str, request: &WindowsRegistrationRequest) -> Option { +pub fn send_registration_request( + transaction_id: &str, + request: &WindowsRegistrationRequest, +) -> Option { util::message(&format!("Registration request data - RP ID: {}, User ID: {} bytes, User name: {}, Client data hash: {} bytes, Algorithms: {:?}, Excluded credentials: {}", request.rpid, request.user_id.len(), request.user_name, request.client_data_hash.len(), request.supported_algorithms, request.excluded_credentials.len())); - + let passkey_request = PasskeyRegistrationRequest { rp_id: request.rpid.clone(), transaction_id: transaction_id.to_string(), - user_id: request.user_id.clone(), + user_handle: request.user_id.clone(), user_name: request.user_name.clone(), client_data_hash: request.client_data_hash.clone(), user_verification: request.user_verification.clone(), supported_algorithms: request.supported_algorithms.clone(), excluded_credentials: request.excluded_credentials.clone(), }; - + match serde_json::to_string(&passkey_request) { Ok(request_json) => { util::message(&format!("Sending registration request: {}", request_json)); crate::ipc::send_passkey_request(RequestType::Registration, request_json, &request.rpid) - }, + } Err(e) => { - util::message(&format!("ERROR: Failed to serialize registration request: {}", e)); + util::message(&format!( + "ERROR: Failed to serialize registration request: {}", + e + )); None } } } /// Creates a WebAuthn make credential response from Bitwarden's registration response -pub unsafe fn create_make_credential_response(credential_id: Vec, attestation_object: Vec) -> std::result::Result<*mut ExperimentalWebAuthnPluginOperationResponse, HRESULT> { +pub unsafe fn create_make_credential_response( + credential_id: Vec, + attestation_object: Vec, +) -> std::result::Result<*mut ExperimentalWebAuthnPluginOperationResponse, HRESULT> { // Use the attestation object directly as the encoded response let response_data = attestation_object; let response_len = response_data.len(); - + // Allocate memory for the response data let layout = Layout::from_size_align(response_len, 1).map_err(|_| HRESULT(-1))?; let response_ptr = alloc(layout); if response_ptr.is_null() { return Err(HRESULT(-1)); } - + // Copy response data ptr::copy_nonoverlapping(response_data.as_ptr(), response_ptr, response_len); - + // Allocate memory for the response structure let response_layout = Layout::new::(); - let operation_response_ptr = alloc(response_layout) as *mut ExperimentalWebAuthnPluginOperationResponse; + let operation_response_ptr = + alloc(response_layout) as *mut ExperimentalWebAuthnPluginOperationResponse; if operation_response_ptr.is_null() { return Err(HRESULT(-1)); } - + // Initialize the response - ptr::write(operation_response_ptr, ExperimentalWebAuthnPluginOperationResponse { - encoded_response_byte_count: response_len as u32, - encoded_response_pointer: response_ptr, - }); - + ptr::write( + operation_response_ptr, + ExperimentalWebAuthnPluginOperationResponse { + encoded_response_byte_count: response_len as u32, + encoded_response_pointer: response_ptr, + }, + ); + Ok(operation_response_ptr) } - 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 b8d71e454da..64bffce725d 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/sync.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/sync.rs @@ -1,103 +1,128 @@ -use serde_json; use hex; +use serde_json; +use crate::ipc::send_passkey_request; use crate::types::*; use crate::utils::{self as util, wstr_to_string}; use crate::webauthn::*; -use crate::ipc::send_passkey_request; /// Helper for sync requests - requests credentials from Electron for a specific RP ID pub fn send_sync_request(rpid: &str) -> Option { - util::message(&format!("[SYNC] send_sync_request called for RP ID: {}", rpid)); - + util::message(&format!( + "[SYNC] send_sync_request called for RP ID: {}", + rpid + )); + let request = PasskeySyncRequest { rp_id: rpid.to_string(), }; - + util::message(&format!("[SYNC] Created sync request for RP ID: {}", rpid)); - + match serde_json::to_string(&request) { Ok(request_json) => { - util::message(&format!("[SYNC] Serialized sync request to JSON: {}", request_json)); + util::message(&format!( + "[SYNC] Serialized sync request to JSON: {}", + request_json + )); util::message(&format!("[SYNC] Sending sync request to Electron via IPC")); let response = send_passkey_request(RequestType::Sync, request_json, rpid); match &response { - Some(resp) => util::message(&format!("[SYNC] Received response from Electron: {:?}", resp)), + Some(resp) => util::message(&format!( + "[SYNC] Received response from Electron: {:?}", + resp + )), None => util::message("[SYNC] No response received from Electron"), } response - }, + } Err(e) => { - util::message(&format!("[SYNC] ERROR: Failed to serialize sync request: {}", e)); + util::message(&format!( + "[SYNC] ERROR: Failed to serialize sync request: {}", + e + )); None } } } /// Initiates credential sync from Electron to Windows - called when Electron wants to push credentials to Windows -pub fn sync_credentials_to_windows(credentials: Vec, plugin_clsid: &str) -> Result<(), String> { - util::message(&format!("[SYNC_TO_WIN] sync_credentials_to_windows called with {} credentials for plugin CLSID: {}", credentials.len(), plugin_clsid)); - +pub fn sync_credentials_to_windows( + credentials: Vec, + plugin_clsid: &str, +) -> Result<(), String> { + util::message(&format!( + "[SYNC_TO_WIN] sync_credentials_to_windows called with {} credentials for plugin CLSID: {}", + credentials.len(), + plugin_clsid + )); + // Format CLSID with curly braces to match Windows registration format let formatted_clsid = format!("{{{}}}", plugin_clsid); - + if credentials.is_empty() { util::message("[SYNC_TO_WIN] No credentials to sync, proceeding with empty sync"); } - + // Convert Bitwarden credentials to Windows credential details let mut win_credentials = Vec::new(); - + for (i, cred) in credentials.iter().enumerate() { let truncated_cred_id = if cred.credential_id.len() > 16 { format!("{}...", hex::encode(&cred.credential_id[..16])) } else { hex::encode(&cred.credential_id) }; - let truncated_user_id = if cred.user_id.len() > 16 { - format!("{}...", hex::encode(&cred.user_id[..16])) + let truncated_user_id = if cred.user_handle.len() > 16 { + format!("{}...", hex::encode(&cred.user_handle[..16])) } else { - hex::encode(&cred.user_id) + hex::encode(&cred.user_handle) }; - + util::message(&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_id.len())); - + 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( 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_id.clone(), // Pass raw bytes + 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); - util::message(&format!("[SYNC_TO_WIN] Converted credential {} to Windows format", i + 1)); + util::message(&format!( + "[SYNC_TO_WIN] Converted credential {} to Windows format", + i + 1 + )); } - + // Create credentials list let credentials_list = ExperimentalWebAuthnPluginCredentialDetailsList::create( formatted_clsid.clone(), - win_credentials + win_credentials, ); - + // First try to remove all existing credentials for this plugin util::message("Attempting to remove all existing credentials before sync..."); match remove_all_credentials(formatted_clsid.clone()) { Ok(()) => { util::message("Successfully removed existing credentials"); - }, + } Err(e) if e.contains("can't be loaded") => { util::message("RemoveAllCredentials function not available - this is expected for some Windows versions"); // This is fine, the function might not exist in all versions - }, + } Err(e) => { - util::message(&format!("Warning: Failed to remove existing credentials: {}", e)); + util::message(&format!( + "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() { util::message("No credentials to add to Windows - sync completed successfully"); @@ -108,9 +133,12 @@ pub fn sync_credentials_to_windows(credentials: Vec, plugin_cl Ok(()) => { util::message("Successfully synced credentials to Windows"); Ok(()) - }, + } Err(e) => { - util::message(&format!("ERROR: Failed to add credentials to Windows: {}", e)); + util::message(&format!( + "ERROR: Failed to add credentials to Windows: {}", + e + )); Err(e) } } @@ -119,89 +147,102 @@ pub fn sync_credentials_to_windows(credentials: Vec, plugin_cl /// Gets all credentials from Windows for a specific plugin - used when Electron requests current state pub fn get_credentials_from_windows(plugin_clsid: &str) -> Result, String> { - util::message(&format!("Getting all credentials from Windows for plugin CLSID: {}", plugin_clsid)); - + util::message(&format!( + "Getting all credentials from Windows for plugin CLSID: {}", + plugin_clsid + )); + // Format CLSID with curly braces to match Windows registration format let formatted_clsid = format!("{{{}}}", plugin_clsid); - + match get_all_credentials(formatted_clsid) { Ok(Some(credentials_list)) => { - util::message(&format!("Retrieved {} credentials from Windows", credentials_list.credential_count)); - + util::message(&format!( + "Retrieved {} credentials from Windows", + credentials_list.credential_count + )); + 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 + credentials_list.credential_count as usize, ); - + for &cred_ptr in credentials_array { if !cred_ptr.is_null() { let cred = &*cred_ptr; - + // Convert credential data back to Bitwarden format - let credential_id = if cred.credential_id_byte_count > 0 && !cred.credential_id_pointer.is_null() { + 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 + 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()) + 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 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_id, + user_handle: user_id, }; - + util::message(&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); } } } - + Ok(bitwarden_credentials) - }, + } Ok(None) => { util::message("No credentials found in Windows"); Ok(Vec::new()) - }, + } Err(e) => { - util::message(&format!("ERROR: Failed to get credentials from Windows: {}", e)); + util::message(&format!( + "ERROR: Failed to get credentials from Windows: {}", + e + )); Err(e) } } -} \ No newline at end of file +} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/types.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/types.rs index ed1ddfe0d68..d6957bba306 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/types.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/types.rs @@ -5,7 +5,7 @@ use tokio::sync::oneshot; #[serde(rename_all = "lowercase")] pub enum UserVerificationRequirement { Required, - Preferred, + Preferred, Discouraged, } @@ -53,12 +53,12 @@ pub struct PasskeyAssertionRequest { pub struct PasskeyRegistrationRequest { pub rp_id: String, pub transaction_id: String, - pub user_id: Vec, + pub user_handle: Vec, pub user_name: String, pub client_data_hash: Vec, pub user_verification: UserVerificationRequirement, - pub supported_algorithms: Vec, // COSE algorithm identifiers - pub excluded_credentials: Vec>, // Credentials to exclude from creation + pub supported_algorithms: Vec, // COSE algorithm identifiers + pub excluded_credentials: Vec>, // Credentials to exclude from creation } /// Sync request structure @@ -80,26 +80,22 @@ pub enum PasskeyRequest { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum PasskeyResponse { - #[serde(rename = "assertion_response",rename_all = "camelCase")] + #[serde(rename = "assertion_response", rename_all = "camelCase")] AssertionResponse { credential_id: Vec, authenticator_data: Vec, signature: Vec, user_handle: Vec, }, - #[serde(rename = "registration_response",rename_all = "camelCase")] + #[serde(rename = "registration_response", rename_all = "camelCase")] RegistrationResponse { credential_id: Vec, attestation_object: Vec, }, - #[serde(rename = "sync_response",rename_all = "camelCase")] - SyncResponse { - credentials: Vec, - }, - #[serde(rename = "error",rename_all = "camelCase")] - Error { - message: String, - }, + #[serde(rename = "sync_response", rename_all = "camelCase")] + SyncResponse { credentials: Vec }, + #[serde(rename = "error", rename_all = "camelCase")] + Error { message: String }, } /// Credential data for sync operations @@ -109,7 +105,7 @@ pub struct SyncedCredential { pub credential_id: Vec, pub rp_id: String, pub user_name: String, - pub user_id: Vec, + pub user_handle: Vec, } /// Request type enumeration for type discrimination @@ -126,4 +122,4 @@ pub struct RequestEvent { pub request_type: RequestType, pub request_json: String, pub response_sender: oneshot::Sender, -} \ No newline at end of file +} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/utils.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/utils.rs index 83bda347c88..9ca1de01993 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/utils.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/utils.rs @@ -1,10 +1,10 @@ use std::ffi::OsString; use std::os::windows::ffi::OsStrExt; -use std::fs::{OpenOptions, create_dir_all}; +use std::fs::{create_dir_all, OpenOptions}; use std::io::Write; -use std::time::{SystemTime, UNIX_EPOCH}; use std::path::Path; +use std::time::{SystemTime, UNIX_EPOCH}; use windows::Win32::Foundation::*; use windows::Win32::System::LibraryLoader::*; @@ -59,17 +59,13 @@ impl WindowsString for String { pub fn file_log(msg: &str) { let log_path = "C:\\temp\\bitwarden_com_debug.log"; - + // Create the temp directory if it doesn't exist if let Some(parent) = Path::new(log_path).parent() { let _ = create_dir_all(parent); } - - if let Ok(mut file) = OpenOptions::new() - .create(true) - .append(true) - .open(log_path) - { + + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_path) { let now = SystemTime::now(); let timestamp = match now.duration_since(UNIX_EPOCH) { Ok(duration) => { @@ -79,10 +75,10 @@ pub fn file_log(msg: &str) { let mins = (total_secs / 60) % 60; let hours = (total_secs / 3600) % 24; format!("{:02}:{:02}:{:02}.{:03}", hours, mins, secs, millis) - }, - Err(_) => "??:??:??.???".to_string() + } + Err(_) => "??:??:??.???".to_string(), }; - + let _ = writeln!(file, "[{}] {}", timestamp, msg); } } @@ -92,18 +88,20 @@ pub fn message(message: &str) { } // Helper function to convert Windows wide string (UTF-16) to Rust String -pub unsafe fn wstr_to_string(wstr_ptr: *const u16) -> std::result::Result { +pub unsafe fn wstr_to_string( + wstr_ptr: *const u16, +) -> std::result::Result { if wstr_ptr.is_null() { return Ok(String::new()); } - + // Find the length of the null-terminated wide string let mut len = 0; while *wstr_ptr.add(len) != 0 { len += 1; } - + // Convert to Rust string let wide_slice = std::slice::from_raw_parts(wstr_ptr, len); String::from_utf16(wide_slice) -} \ No newline at end of file +} 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 65f94fccea6..67bfd903b54 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/webauthn.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/webauthn.rs @@ -407,10 +407,27 @@ pub fn remove_all_credentials( } } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct WEBAUTHN_CREDENTIAL_EX { + pub dwVersion: u32, + pub cbId: u32, + pub pbId: *const u8, + pub pwszCredentialType: *const u16, // LPCWSTR + pub dwTransports: u32, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct WEBAUTHN_CREDENTIAL_LIST { + pub cCredentials: u32, + pub ppCredentials: *const *const WEBAUTHN_CREDENTIAL_EX, +} + // Forward declarations for Windows types we need type WEBAUTHN_ASSERTION = *const u8; // Placeholder - would need actual definition type PCWEBAUTHN_USER_ENTITY_INFORMATION = *const u8; // Placeholder - would need actual definition -type WEBAUTHN_CREDENTIAL_LIST = *const u8; // Placeholder - would need actual definition +//type WEBAUTHN_CREDENTIAL_LIST = *const u8; // Placeholder - would need actual definition type EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS = *const u8; // Placeholder type EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION = *const u8; // Placeholder diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts index 5b7dd5e1df9..5bf1fd7d30e 100644 --- a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts +++ b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts @@ -121,7 +121,7 @@ export class NativeAutofillMain { rpId: request.rpId, clientDataHash: request.clientDataHash, userName: request.userName, - userHandle: request.userId, + userHandle: request.userHandle, userVerification: autofill.UserVerification.Required, supportedAlgorithms: request.supportedAlgorithms, windowXy: { x: 400, y: 400 }, @@ -221,7 +221,7 @@ export class NativeAutofillMain { private async sendAndOptionallyWait( channel: string, data: any, - options?: { waitForResponse?: boolean; timeout?: number; responseChannel?: string }, + options?: { waitForResponse?: boolean; timeout?: number }, ): Promise { if (!options?.waitForResponse) { // Just send without waiting for response (existing behavior) @@ -232,7 +232,6 @@ export class NativeAutofillMain { // Use clientId and sequenceNumber as the tracking key const trackingKey = `${data.clientId}_${data.sequenceNumber}`; - const responseChannel = options.responseChannel || `${channel}_response`; const timeout = options.timeout || 30000; // 30 second default timeout // Send the original data without adding requestId @@ -268,27 +267,29 @@ export class NativeAutofillMain { async init() { this.initWindows(); - ipcMain.handle("autofill.syncPasskeys", async (event, data: NativeAutofillSyncParams): Promise => { - this.logService.info("autofill.syncPasskeys", data); - const { credentials } = data; - const mapped = credentials.map((cred: NativeAutofillFido2Credential) => { - const x: passkey_authenticator.SyncedCredential = { - credentialId: cred.credentialId, - rpId: cred.rpId, - userName: cred.userName, - userId: cred.userHandle - }; - this.logService.info("Mapped credential:", x); - return x; - }); + ipcMain.handle( + "autofill.syncPasskeys", + async (event, data: NativeAutofillSyncParams): Promise => { + this.logService.info("autofill.syncPasskeys", data); + const { credentials } = data; + const mapped = credentials.map((cred: NativeAutofillFido2Credential) => { + const x: passkey_authenticator.SyncedCredential = { + credentialId: cred.credentialId, + rpId: cred.rpId, + userName: cred.userName, + userHandle: cred.userHandle, + }; + this.logService.info("Mapped credential:", x); + return x; + }); - this.logService.info("Syncing passkeys to Windows:", mapped); - - passkey_authenticator.syncCredentialsToWindows(mapped); + this.logService.info("Syncing passkeys to Windows:", mapped); - return "worked"; - }); + passkey_authenticator.syncCredentialsToWindows(mapped); + return "worked"; + }, + ); ipcMain.handle( "autofill.runCommand", @@ -383,7 +384,6 @@ export class NativeAutofillMain { } }); - ipcMain.on("autofill.completePasskeySync", (event, data) => { this.logService.warning("autofill.completePasskeySync", data); const { clientId, sequenceNumber, response, requestId } = data;