1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-01 09:13:54 +00:00

Update windows_plugin_authenticator to stable interface

This commit is contained in:
Isaiah Inuwa
2025-11-05 15:03:39 -06:00
parent 852832aa8b
commit ff9402804a
4 changed files with 400 additions and 327 deletions

View File

@@ -37,13 +37,13 @@ pub struct ExperimentalWebAuthnPluginOperationRequest {
pub encoded_request_pointer: *mut u8,
}
/// Used when creating and asserting credentials with EXPERIMENTAL2 interface.
/// Header File Name: _EXPERIMENTAL2_WEBAUTHN_PLUGIN_OPERATION_REQUEST
/// Header File Usage: EXPERIMENTAL_MakeCredential()
/// EXPERIMENTAL_GetAssertion()
/// Used when creating and asserting credentials with stable interface.
/// Header File Name: _WEBAUTHN_PLUGIN_OPERATION_REQUEST
/// Header File Usage: MakeCredential()
/// GetAssertion()
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct Experimental2WebAuthnPluginOperationRequest {
pub struct WebAuthnPluginOperationRequest {
pub window_handle: windows::Win32::Foundation::HWND,
pub transaction_id: windows_core::GUID,
pub request_signature_byte_count: u32,
@@ -53,74 +53,45 @@ pub struct Experimental2WebAuthnPluginOperationRequest {
pub encoded_request_pointer: *mut u8,
}
/// Used as a response when creating and asserting credentials.
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE
/// Header File Usage: EXPERIMENTAL_PluginMakeCredential()
/// EXPERIMENTAL_PluginGetAssertion()
/// Header File Name: _WEBAUTHN_PLUGIN_OPERATION_RESPONSE
/// Header File Usage: MakeCredential()
/// GetAssertion()
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ExperimentalWebAuthnPluginOperationResponse {
pub struct WebAuthnPluginOperationResponse {
pub encoded_response_byte_count: u32,
pub encoded_response_pointer: *mut u8,
}
/// Used to cancel an operation.
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST
/// Header File Usage: EXPERIMENTAL_PluginCancelOperation()
/// Header File Name: _WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST
/// Header File Usage: CancelOperation()
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ExperimentalWebAuthnPluginCancelOperationRequest {
pub struct WebAuthnPluginCancelOperationRequest {
pub transaction_id: windows_core::GUID,
pub request_signature_byte_count: u32,
pub request_signature_pointer: *mut u8,
}
#[interface("e6466e9a-b2f3-47c5-b88d-89bc14a8d998")]
pub unsafe trait EXPERIMENTAL_IPluginAuthenticator: windows_core::IUnknown {
fn EXPERIMENTAL_PluginMakeCredential(
&self,
request: *const ExperimentalWebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT;
fn EXPERIMENTAL_PluginGetAssertion(
&self,
request: *const ExperimentalWebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT;
fn EXPERIMENTAL_PluginCancelOperation(
&self,
request: *const ExperimentalWebAuthnPluginCancelOperationRequest,
) -> HRESULT;
fn EXPERIMENTAL_GetLockStatus(
&self,
lock_status: *mut PluginLockStatus,
) -> HRESULT;
}
// Stable IPluginAuthenticator interface
#[interface("d26bcf6f-b54c-43ff-9f06-d5bf148625f7")]
pub unsafe trait EXPERIMENTAL2_IPluginAuthenticator: windows_core::IUnknown {
fn EXPERIMENTAL_MakeCredential(
pub unsafe trait IPluginAuthenticator: windows_core::IUnknown {
fn MakeCredential(
&self,
request: *const Experimental2WebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
request: *const WebAuthnPluginOperationRequest,
response: *mut WebAuthnPluginOperationResponse,
) -> HRESULT;
fn EXPERIMENTAL_GetAssertion(
fn GetAssertion(
&self,
request: *const Experimental2WebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT;
fn EXPERIMENTAL_CancelOperation(
&self,
request: *const ExperimentalWebAuthnPluginCancelOperationRequest,
) -> HRESULT;
fn EXPERIMENTAL_GetLockStatus(
&self,
lock_status: *mut PluginLockStatus,
request: *const WebAuthnPluginOperationRequest,
response: *mut WebAuthnPluginOperationResponse,
) -> HRESULT;
fn CancelOperation(&self, request: *const WebAuthnPluginCancelOperationRequest) -> HRESULT;
fn GetLockStatus(&self, lock_status: *mut PluginLockStatus) -> HRESULT;
}
pub unsafe fn parse_credential_list(credential_list: &WEBAUTHN_CREDENTIAL_LIST) -> Vec<Vec<u8>> {
let mut allowed_credentials = Vec::new();
@@ -174,57 +145,20 @@ pub unsafe fn parse_credential_list(credential_list: &WEBAUTHN_CREDENTIAL_LIST)
allowed_credentials
}
#[implement(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL2_IPluginAuthenticator)]
#[implement(IPluginAuthenticator)]
pub struct PluginAuthenticatorComObject;
#[implement(IClassFactory)]
pub struct Factory;
impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl {
unsafe fn EXPERIMENTAL_PluginMakeCredential(
impl IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl {
unsafe fn MakeCredential(
&self,
request: *const ExperimentalWebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
request: *const WebAuthnPluginOperationRequest,
response: *mut WebAuthnPluginOperationResponse,
) -> HRESULT {
experimental_plugin_make_credential(request, response)
}
unsafe fn EXPERIMENTAL_PluginGetAssertion(
&self,
request: *const ExperimentalWebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT {
experimental_plugin_get_assertion(request, response)
}
unsafe fn EXPERIMENTAL_PluginCancelOperation(
&self,
_request: *const ExperimentalWebAuthnPluginCancelOperationRequest,
) -> HRESULT {
debug_log("EXPERIMENTAL_PluginCancelOperation() called");
HRESULT(0)
}
unsafe fn EXPERIMENTAL_GetLockStatus(
&self,
lock_status: *mut PluginLockStatus,
) -> HRESULT {
debug_log("EXPERIMENTAL_GetLockStatus() called");
if lock_status.is_null() {
return HRESULT(-2147024809); // E_INVALIDARG
}
*lock_status = PluginLockStatus::PluginUnlocked;
HRESULT(0)
}
}
impl EXPERIMENTAL2_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl {
unsafe fn EXPERIMENTAL_MakeCredential(
&self,
request: *const Experimental2WebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT {
debug_log("EXPERIMENTAL2_MakeCredential() called");
debug_log("MakeCredential() called");
// Convert to legacy format for internal processing
let legacy_request = ExperimentalWebAuthnPluginOperationRequest {
window_handle: (*request).window_handle,
transaction_id: (*request).transaction_id,
@@ -233,15 +167,27 @@ impl EXPERIMENTAL2_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Im
encoded_request_byte_count: (*request).encoded_request_byte_count,
encoded_request_pointer: (*request).encoded_request_pointer,
};
experimental_plugin_make_credential(&legacy_request, response)
let mut legacy_response: *mut ExperimentalWebAuthnPluginOperationResponse = ptr::null_mut();
let result = experimental_plugin_make_credential(&legacy_request, &mut legacy_response);
if result.is_ok() && !legacy_response.is_null() {
// Copy response data
(*response).encoded_response_byte_count =
(*legacy_response).encoded_response_byte_count;
(*response).encoded_response_pointer = (*legacy_response).encoded_response_pointer;
}
result
}
unsafe fn EXPERIMENTAL_GetAssertion(
unsafe fn GetAssertion(
&self,
request: *const Experimental2WebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
request: *const WebAuthnPluginOperationRequest,
response: *mut WebAuthnPluginOperationResponse,
) -> HRESULT {
debug_log("EXPERIMENTAL2_GetAssertion() called");
debug_log("GetAssertion() called");
// Convert to legacy format for internal processing
let legacy_request = ExperimentalWebAuthnPluginOperationRequest {
window_handle: (*request).window_handle,
transaction_id: (*request).transaction_id,
@@ -250,22 +196,30 @@ impl EXPERIMENTAL2_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Im
encoded_request_byte_count: (*request).encoded_request_byte_count,
encoded_request_pointer: (*request).encoded_request_pointer,
};
experimental_plugin_get_assertion(&legacy_request, response)
let mut legacy_response: *mut ExperimentalWebAuthnPluginOperationResponse = ptr::null_mut();
let result = experimental_plugin_get_assertion(&legacy_request, &mut legacy_response);
if result.is_ok() && !legacy_response.is_null() {
// Copy response data
(*response).encoded_response_byte_count =
(*legacy_response).encoded_response_byte_count;
(*response).encoded_response_pointer = (*legacy_response).encoded_response_pointer;
}
result
}
unsafe fn EXPERIMENTAL_CancelOperation(
unsafe fn CancelOperation(
&self,
_request: *const ExperimentalWebAuthnPluginCancelOperationRequest,
_request: *const WebAuthnPluginCancelOperationRequest,
) -> HRESULT {
debug_log("EXPERIMENTAL2_CancelOperation() called");
debug_log("CancelOperation() called");
HRESULT(0)
}
unsafe fn EXPERIMENTAL_GetLockStatus(
&self,
lock_status: *mut PluginLockStatus,
) -> HRESULT {
debug_log("EXPERIMENTAL2_GetLockStatus() called");
unsafe fn GetLockStatus(&self, lock_status: *mut PluginLockStatus) -> HRESULT {
debug_log("GetLockStatus() called");
if lock_status.is_null() {
return HRESULT(-2147024809); // E_INVALIDARG
}
@@ -274,7 +228,6 @@ impl EXPERIMENTAL2_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Im
}
}
impl IClassFactory_Impl for Factory_Impl {
fn CreateInstance(
&self,

View File

@@ -34,10 +34,10 @@ fn parse_uuid_to_bytes(uuid_str: &str) -> Result<Vec<u8>, String> {
.collect()
}
/// Converts the CLSID constant string to a GUID
fn parse_clsid_to_guid() -> Result<GUID, String> {
/// Converts a CLSID string to a GUID
pub(crate) fn parse_clsid_to_guid_str(clsid_str: &str) -> Result<GUID, String> {
// Remove hyphens and parse as hex
let clsid_clean = CLSID.replace("-", "");
let clsid_clean = clsid_str.replace("-", "");
if clsid_clean.len() != 32 {
return Err("Invalid CLSID format".to_string());
}
@@ -49,6 +49,11 @@ fn parse_clsid_to_guid() -> Result<GUID, String> {
Ok(GUID::from_u128(clsid_u128))
}
/// Converts the CLSID constant string to a GUID
fn parse_clsid_to_guid() -> Result<GUID, String> {
parse_clsid_to_guid_str(CLSID)
}
/// Generates CBOR-encoded authenticator info according to FIDO CTAP2 specifications
/// See: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetInfo
fn generate_cbor_authenticator_info() -> Result<Vec<u8>, String> {
@@ -174,41 +179,35 @@ pub fn add_authenticator() -> std::result::Result<(), String> {
let authenticator_name: HSTRING = AUTHENTICATOR_NAME.into();
let authenticator_name_ptr = PCWSTR(authenticator_name.as_ptr()).as_ptr();
let clsid: HSTRING = format!("{{{}}}", CLSID).into();
let clsid_ptr = PCWSTR(clsid.as_ptr()).as_ptr();
// Parse CLSID into GUID structure
let clsid_guid = parse_clsid_to_guid()
.map_err(|e| format!("Failed to parse CLSID to GUID: {}", e))?;
let relying_party_id: HSTRING = RPID.into();
let relying_party_id_ptr = PCWSTR(relying_party_id.as_ptr()).as_ptr();
// Generate CBOR authenticator info dynamically
let mut authenticator_info_bytes = generate_cbor_authenticator_info()
let authenticator_info_bytes = generate_cbor_authenticator_info()
.map_err(|e| format!("Failed to generate authenticator info: {}", e))?;
let add_authenticator_options = ExperimentalWebAuthnPluginAddAuthenticatorOptions {
let add_authenticator_options = WebAuthnPluginAddAuthenticatorOptions {
authenticator_name: authenticator_name_ptr,
plugin_clsid: clsid_ptr,
rclsid: &clsid_guid, // Changed to GUID reference
rpid: relying_party_id_ptr,
light_theme_logo: ptr::null(),
dark_theme_logo: ptr::null(),
light_theme_logo_svg: ptr::null(), // Renamed field
dark_theme_logo_svg: ptr::null(), // Renamed field
cbor_authenticator_info_byte_count: authenticator_info_bytes.len() as u32,
cbor_authenticator_info: authenticator_info_bytes.as_mut_ptr(),
cbor_authenticator_info: authenticator_info_bytes.as_ptr(), // Use as_ptr() not as_mut_ptr()
supported_rp_ids_count: 0, // NEW field: 0 means all RPs supported
supported_rp_ids: ptr::null(), // NEW field
};
let plugin_signing_public_key_byte_count: u32 = 0;
let mut plugin_signing_public_key: c_uchar = 0;
let plugin_signing_public_key_ptr = &mut plugin_signing_public_key;
let mut add_response = ExperimentalWebAuthnPluginAddAuthenticatorResponse {
plugin_operation_signing_key_byte_count: plugin_signing_public_key_byte_count,
plugin_operation_signing_key: plugin_signing_public_key_ptr,
};
let mut add_response_ptr: *mut ExperimentalWebAuthnPluginAddAuthenticatorResponse =
&mut add_response;
let mut add_response_ptr: *mut WebAuthnPluginAddAuthenticatorResponse = ptr::null_mut();
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAddAuthenticatorFnDeclaration>(
delay_load::<WebAuthNPluginAddAuthenticatorFnDeclaration>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNPluginAddAuthenticator"),
s!("WebAuthNPluginAddAuthenticator"), // Stable function name
)
};
@@ -218,24 +217,45 @@ pub fn add_authenticator() -> std::result::Result<(), String> {
if result.is_err() {
return Err(format!(
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAddAuthenticator()\n{}",
"Error: Error response from WebAuthNPluginAddAuthenticator()\n{}",
result.message()
));
}
// Free the response if needed
if !add_response_ptr.is_null() {
free_add_authenticator_response(add_response_ptr);
}
Ok(())
},
None => {
Err(String::from("Error: Can't complete add_authenticator(), as the function EXPERIMENTAL_WebAuthNPluginAddAuthenticator can't be found."))
Err(String::from("Error: Can't complete add_authenticator(), as the function WebAuthNPluginAddAuthenticator can't be found."))
}
}
}
type EXPERIMENTAL_WebAuthNPluginAddAuthenticatorFnDeclaration = unsafe extern "cdecl" fn(
pPluginAddAuthenticatorOptions: *const ExperimentalWebAuthnPluginAddAuthenticatorOptions,
ppPluginAddAuthenticatorResponse: *mut *mut ExperimentalWebAuthnPluginAddAuthenticatorResponse,
)
-> HRESULT;
fn free_add_authenticator_response(response: *mut WebAuthnPluginAddAuthenticatorResponse) {
let result = unsafe {
delay_load::<WebAuthNPluginFreeAddAuthenticatorResponseFnDeclaration>(
s!("webauthn.dll"),
s!("WebAuthNPluginFreeAddAuthenticatorResponse"),
)
};
if let Some(api) = result {
unsafe { api(response) };
}
}
type WebAuthNPluginAddAuthenticatorFnDeclaration = unsafe extern "cdecl" fn(
pPluginAddAuthenticatorOptions: *const WebAuthnPluginAddAuthenticatorOptions,
ppPluginAddAuthenticatorResponse: *mut *mut WebAuthnPluginAddAuthenticatorResponse,
) -> HRESULT;
type WebAuthNPluginFreeAddAuthenticatorResponseFnDeclaration = unsafe extern "cdecl" fn(
pPluginAddAuthenticatorResponse: *mut WebAuthnPluginAddAuthenticatorResponse,
);
#[cfg(test)]
mod tests {

View File

@@ -1,6 +1,7 @@
use hex;
use serde_json;
use crate::com_registration::parse_clsid_to_guid_str;
use crate::ipc::send_passkey_request;
use crate::types::*;
use crate::util::{debug_log, wstr_to_string};
@@ -57,8 +58,9 @@ pub fn sync_credentials_to_windows(
plugin_clsid
));
// Format CLSID with curly braces to match Windows registration format
let formatted_clsid = format!("{{{}}}", plugin_clsid);
// Parse CLSID string to GUID
let clsid_guid = parse_clsid_to_guid_str(plugin_clsid)
.map_err(|e| format!("Failed to parse CLSID: {}", e))?;
if credentials.is_empty() {
debug_log("[SYNC_TO_WIN] No credentials to sync, proceeding with empty sync");
@@ -79,10 +81,10 @@ pub fn sync_credentials_to_windows(
hex::encode(&cred.user_handle)
};
debug_log(&format!("[SYNC_TO_WIN] Converting credential {}: RP ID: {}, User: {}, Credential ID: {} ({} bytes), User ID: {} ({} bytes)",
debug_log(&format!("[SYNC_TO_WIN] Converting credential {}: RP ID: {}, User: {}, Credential ID: {} ({} bytes), User ID: {} ({} bytes)",
i + 1, cred.rp_id, cred.user_name, truncated_cred_id, cred.credential_id.len(), truncated_user_id, cred.user_handle.len()));
let win_cred = ExperimentalWebAuthnPluginCredentialDetails::create_from_bytes(
let win_cred = WebAuthnPluginCredentialDetails::create_from_bytes(
cred.credential_id.clone(), // Pass raw bytes
cred.rp_id.clone(),
cred.rp_id.clone(), // Use RP ID as friendly name for now
@@ -98,15 +100,9 @@ pub fn sync_credentials_to_windows(
));
}
// Create credentials list
let credentials_list = ExperimentalWebAuthnPluginCredentialDetailsList::create(
formatted_clsid.clone(),
win_credentials,
);
// First try to remove all existing credentials for this plugin
debug_log("Attempting to remove all existing credentials before sync...");
match remove_all_credentials(formatted_clsid.clone()) {
match remove_all_credentials(clsid_guid) {
Ok(()) => {
debug_log("Successfully removed existing credentials");
}
@@ -129,7 +125,7 @@ pub fn sync_credentials_to_windows(
Ok(())
} else {
debug_log("Adding new credentials to Windows...");
match add_credentials(credentials_list) {
match add_credentials(clsid_guid, win_credentials) {
Ok(()) => {
debug_log("Successfully synced credentials to Windows");
Ok(())
@@ -152,91 +148,36 @@ pub fn get_credentials_from_windows(plugin_clsid: &str) -> Result<Vec<SyncedCred
plugin_clsid
));
// Format CLSID with curly braces to match Windows registration format
let formatted_clsid = format!("{{{}}}", plugin_clsid);
// Parse CLSID string to GUID
let clsid_guid = parse_clsid_to_guid_str(plugin_clsid)
.map_err(|e| format!("Failed to parse CLSID: {}", e))?;
match get_all_credentials(formatted_clsid) {
Ok(Some(credentials_list)) => {
match get_all_credentials(clsid_guid) {
Ok(credentials) => {
debug_log(&format!(
"Retrieved {} credentials from Windows",
credentials_list.credential_count
credentials.len()
));
let mut bitwarden_credentials = Vec::new();
// Convert Windows credentials to Bitwarden format
unsafe {
let credentials_array = std::slice::from_raw_parts(
credentials_list.credentials,
credentials_list.credential_count as usize,
);
for cred in credentials {
let synced_cred = SyncedCredential {
credential_id: cred.credential_id,
rp_id: cred.rpid,
user_name: cred.user_name,
user_handle: cred.user_id,
};
for &cred_ptr in credentials_array {
if !cred_ptr.is_null() {
let cred = &*cred_ptr;
debug_log(&format!("Converted Windows credential: RP ID: {}, User: {}, Credential ID: {} bytes",
synced_cred.rp_id, synced_cred.user_name, synced_cred.credential_id.len()));
// Convert credential data back to Bitwarden format
let credential_id = if cred.credential_id_byte_count > 0
&& !cred.credential_id_pointer.is_null()
{
let id_slice = std::slice::from_raw_parts(
cred.credential_id_pointer,
cred.credential_id_byte_count as usize,
);
// Assume it's hex-encoded, try to decode
hex::decode(std::str::from_utf8(id_slice).unwrap_or(""))
.unwrap_or_else(|_| id_slice.to_vec())
} else {
Vec::new()
};
let rp_id = if !cred.rpid.is_null() {
wstr_to_string(cred.rpid).unwrap_or_default()
} else {
String::new()
};
let user_name = if !cred.user_name.is_null() {
wstr_to_string(cred.user_name).unwrap_or_default()
} else {
String::new()
};
let user_id =
if cred.user_id_byte_count > 0 && !cred.user_id_pointer.is_null() {
// Convert from UTF-8 bytes back to Vec<u8>
let user_id_slice = std::slice::from_raw_parts(
cred.user_id_pointer,
cred.user_id_byte_count as usize,
);
// Try to decode as hex string, or use raw bytes
let user_id_str = std::str::from_utf8(user_id_slice).unwrap_or("");
hex::decode(user_id_str).unwrap_or_else(|_| user_id_slice.to_vec())
} else {
Vec::new()
};
let synced_cred = SyncedCredential {
credential_id,
rp_id,
user_name,
user_handle: user_id,
};
debug_log(&format!("Converted Windows credential: RP ID: {}, User: {}, Credential ID: {} bytes",
synced_cred.rp_id, synced_cred.user_name, synced_cred.credential_id.len()));
bitwarden_credentials.push(synced_cred);
}
}
bitwarden_credentials.push(synced_cred);
}
Ok(bitwarden_credentials)
}
Ok(None) => {
debug_log("No credentials found in Windows");
Ok(Vec::new())
}
Err(e) => {
debug_log(&format!(
"ERROR: Failed to get credentials from Windows: {}",

View File

@@ -1,8 +1,8 @@
/*
This file exposes safe functions and types for interacting with the experimental
Windows WebAuthn API defined here:
This file exposes safe functions and types for interacting with the stable
Windows WebAuthn Plugin API defined here:
https://github.com/microsoft/webauthn/blob/master/experimental/webauthn.h
https://github.com/microsoft/webauthn/blob/master/webauthnplugin.h
*/
use windows_core::*;
@@ -21,49 +21,54 @@ pub struct ExperimentalWebAuthnCtapCborAuthenticatorOptions {
pub require_resident_key: i32, // LONG lRequireResidentKey: +1=TRUE, 0=Not defined, -1=FALSE
}
/// Used when adding a Windows plugin authenticator.
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS
/// Header File Usage: EXPERIMENTAL_WebAuthNPluginAddAuthenticator()
/// Used when adding a Windows plugin authenticator (stable API).
/// Header File Name: _WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS
/// Header File Usage: WebAuthNPluginAddAuthenticator()
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ExperimentalWebAuthnPluginAddAuthenticatorOptions {
pub authenticator_name: *const u16,
pub plugin_clsid: *const u16,
pub rpid: *const u16,
pub light_theme_logo: *const u16,
pub dark_theme_logo: *const u16,
pub struct WebAuthnPluginAddAuthenticatorOptions {
pub authenticator_name: *const u16, // LPCWSTR
pub rclsid: *const GUID, // REFCLSID (changed from string)
pub rpid: *const u16, // LPCWSTR (optional)
pub light_theme_logo_svg: *const u16, // LPCWSTR (optional, base64 SVG)
pub dark_theme_logo_svg: *const u16, // LPCWSTR (optional, base64 SVG)
pub cbor_authenticator_info_byte_count: u32,
pub cbor_authenticator_info: *const u8,
pub cbor_authenticator_info: *const u8, // const BYTE*
pub supported_rp_ids_count: u32, // NEW in stable
pub supported_rp_ids: *const *const u16, // NEW in stable: array of LPCWSTR
}
/// Used as a response type when adding a Windows plugin authenticator.
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE
/// Header File Usage: EXPERIMENTAL_WebAuthNPluginAddAuthenticator()
/// EXPERIMENTAL_WebAuthNPluginFreeAddAuthenticatorResponse()
/// Used as a response type when adding a Windows plugin authenticator (stable API).
/// Header File Name: _WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE
/// Header File Usage: WebAuthNPluginAddAuthenticator()
/// WebAuthNPluginFreeAddAuthenticatorResponse()
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ExperimentalWebAuthnPluginAddAuthenticatorResponse {
pub struct WebAuthnPluginAddAuthenticatorResponse {
pub plugin_operation_signing_key_byte_count: u32,
pub plugin_operation_signing_key: *mut u8,
}
/// Represents a credential.
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS
/// Header File Usage: _EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS_LIST
/// Header File Name: _WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS
/// Header File Usage: WebAuthNPluginAuthenticatorAddCredentials, etc.
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ExperimentalWebAuthnPluginCredentialDetails {
pub struct WebAuthnPluginCredentialDetails {
pub credential_id_byte_count: u32,
pub credential_id_pointer: *mut u8,
pub rpid: *mut u16,
pub rp_friendly_name: *mut u16,
pub credential_id_pointer: *const u8, // Changed to const in stable
pub rpid: *const u16, // Changed to const (LPCWSTR)
pub rp_friendly_name: *const u16, // Changed to const (LPCWSTR)
pub user_id_byte_count: u32,
pub user_id_pointer: *mut u8, // Should be *mut u8 like credential_id_pointer
pub user_name: *mut u16,
pub user_display_name: *mut u16,
pub user_id_pointer: *const u8, // Changed to const
pub user_name: *const u16, // Changed to const (LPCWSTR)
pub user_display_name: *const u16, // Changed to const (LPCWSTR)
}
impl ExperimentalWebAuthnPluginCredentialDetails {
// Keep experimental version for internal use
pub type ExperimentalWebAuthnPluginCredentialDetails = WebAuthnPluginCredentialDetails;
impl WebAuthnPluginCredentialDetails {
pub fn create_from_bytes(
credential_id: Vec<u8>,
rpid: String,
@@ -72,12 +77,11 @@ impl ExperimentalWebAuthnPluginCredentialDetails {
user_name: String,
user_display_name: String,
) -> Self {
// Convert credential_id bytes to hex string, then allocate with COM
let (credential_id_pointer, credential_id_byte_count) = ComBuffer::from_buffer(credential_id);
// Allocate credential_id bytes with COM
let (credential_id_pointer, credential_id_byte_count) = ComBuffer::from_buffer(&credential_id);
// Convert user_id bytes to hex string, then allocate with COM
let user_id_string = hex::encode(&user_id);
let (user_id_pointer, user_id_byte_count) = ComBuffer::from_buffer(user_id_string.as_bytes());
// Allocate user_id bytes with COM
let (user_id_pointer, user_id_byte_count) = ComBuffer::from_buffer(&user_id);
// Convert strings to null-terminated wide strings using trait methods
let (rpid_ptr, _) = rpid.to_com_utf16();
@@ -87,22 +91,40 @@ impl ExperimentalWebAuthnPluginCredentialDetails {
Self {
credential_id_byte_count,
credential_id_pointer,
rpid: rpid_ptr,
rp_friendly_name: rp_friendly_name_ptr,
credential_id_pointer: credential_id_pointer as *const u8,
rpid: rpid_ptr as *const u16,
rp_friendly_name: rp_friendly_name_ptr as *const u16,
user_id_byte_count,
user_id_pointer,
user_name: user_name_ptr,
user_display_name: user_display_name_ptr,
user_id_pointer: user_id_pointer as *const u8,
user_name: user_name_ptr as *const u16,
user_display_name: user_display_name_ptr as *const u16,
}
}
}
/// Represents a list of credentials.
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS_LIST
/// Header File Usage: EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials()
/// EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentials()
/// EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentials()
// Keep backward compat alias
impl ExperimentalWebAuthnPluginCredentialDetails {
pub fn create_from_bytes(
credential_id: Vec<u8>,
rpid: String,
rp_friendly_name: String,
user_id: Vec<u8>,
user_name: String,
user_display_name: String,
) -> Self {
WebAuthnPluginCredentialDetails::create_from_bytes(
credential_id,
rpid,
rp_friendly_name,
user_id,
user_name,
user_display_name,
)
}
}
/// Represents a list of credentials - kept for backwards compatibility
/// The stable API takes flat arrays directly, not this list structure
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ExperimentalWebAuthnPluginCredentialDetailsList {
@@ -126,7 +148,7 @@ impl ExperimentalWebAuthnPluginCredentialDetailsList {
.collect();
let credentials_len = credential_pointers.len();
// Allocate the array of pointers using COM as well
let credentials_pointer = if credentials_len > 0 {
let pointer_array_bytes = credential_pointers.len() * std::mem::size_of::<*mut ExperimentalWebAuthnPluginCredentialDetails>();
@@ -143,7 +165,7 @@ impl ExperimentalWebAuthnPluginCredentialDetailsList {
// Convert CLSID to wide string using trait method
let (clsid_ptr, _) = clsid.to_com_utf16();
Self {
plugin_clsid: clsid_ptr,
credential_count: credentials_len as u32,
@@ -152,52 +174,71 @@ impl ExperimentalWebAuthnPluginCredentialDetailsList {
}
}
pub type EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration =
// Stable API function signatures - now use REFCLSID and flat arrays
pub type WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration =
unsafe extern "cdecl" fn(
pCredentialDetailsList: *mut ExperimentalWebAuthnPluginCredentialDetailsList,
rclsid: *const GUID, // Changed from string to GUID reference
cCredentialDetails: u32,
pCredentialDetails: *const WebAuthnPluginCredentialDetails, // Flat array, not list
) -> HRESULT;
pub type EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentialsFnDeclaration =
pub type WebAuthNPluginAuthenticatorRemoveCredentialsFnDeclaration =
unsafe extern "cdecl" fn(
pCredentialDetailsList: *mut ExperimentalWebAuthnPluginCredentialDetailsList,
rclsid: *const GUID,
cCredentialDetails: u32,
pCredentialDetails: *const WebAuthnPluginCredentialDetails,
) -> HRESULT;
pub type EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentialsFnDeclaration =
pub type WebAuthNPluginAuthenticatorGetAllCredentialsFnDeclaration =
unsafe extern "cdecl" fn(
pwszPluginClsId: *const u16,
ppCredentialDetailsList: *mut *mut ExperimentalWebAuthnPluginCredentialDetailsList,
rclsid: *const GUID,
pcCredentialDetails: *mut u32, // Out param for count
ppCredentialDetailsArray: *mut *mut WebAuthnPluginCredentialDetails, // Out param for array
) -> HRESULT;
pub type EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentialsFnDeclaration =
pub type WebAuthNPluginAuthenticatorFreeCredentialDetailsArrayFnDeclaration =
unsafe extern "cdecl" fn(
pwszPluginClsId: *const u16,
cCredentialDetails: u32,
pCredentialDetailsArray: *mut WebAuthnPluginCredentialDetails,
);
pub type WebAuthNPluginAuthenticatorRemoveAllCredentialsFnDeclaration =
unsafe extern "cdecl" fn(
rclsid: *const GUID,
) -> HRESULT;
pub fn add_credentials(
mut credentials_list: ExperimentalWebAuthnPluginCredentialDetailsList,
clsid_guid: GUID,
credentials: Vec<WebAuthnPluginCredentialDetails>,
) -> std::result::Result<(), String> {
debug_log("Loading EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials function...");
debug_log("Loading WebAuthNPluginAuthenticatorAddCredentials function...");
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration>(
delay_load::<WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials"),
s!("WebAuthNPluginAuthenticatorAddCredentials"),
)
};
match result {
Some(api) => {
debug_log("Function loaded successfully, calling API...");
debug_log(&format!("Credential list: plugin_clsid valid: {}, credential_count: {}",
!credentials_list.plugin_clsid.is_null(), credentials_list.credential_count));
let result = unsafe { api(&mut credentials_list) };
debug_log(&format!("Adding {} credentials", credentials.len()));
let credential_count = credentials.len() as u32;
let credentials_ptr = if credentials.is_empty() {
std::ptr::null()
} else {
credentials.as_ptr()
};
let result = unsafe { api(&clsid_guid, credential_count, credentials_ptr) };
if result.is_err() {
let error_code = result.0;
debug_log(&format!("API call failed with HRESULT: 0x{:x}", error_code));
return Err(format!(
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials()\nHRESULT: 0x{:x}\n{}",
"Error: Error response from WebAuthNPluginAuthenticatorAddCredentials()\nHRESULT: 0x{:x}\n{}",
error_code, result.message()
));
}
@@ -206,29 +247,41 @@ pub fn add_credentials(
Ok(())
},
None => {
debug_log("Failed to load EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials function from webauthn.dll");
Err(String::from("Error: Can't complete add_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials can't be loaded."))
debug_log("Failed to load WebAuthNPluginAuthenticatorAddCredentials function from webauthn.dll");
Err(String::from("Error: Can't complete add_credentials(), as the function WebAuthNPluginAuthenticatorAddCredentials can't be loaded."))
}
}
}
pub fn remove_credentials(
mut credentials_list: ExperimentalWebAuthnPluginCredentialDetailsList,
clsid_guid: GUID,
credentials: Vec<WebAuthnPluginCredentialDetails>,
) -> std::result::Result<(), String> {
debug_log("Loading WebAuthNPluginAuthenticatorRemoveCredentials function...");
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentialsFnDeclaration>(
delay_load::<WebAuthNPluginAuthenticatorRemoveCredentialsFnDeclaration>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentials"),
s!("WebAuthNPluginAuthenticatorRemoveCredentials"),
)
};
match result {
Some(api) => {
let result = unsafe { api(&mut credentials_list) };
debug_log(&format!("Removing {} credentials", credentials.len()));
let credential_count = credentials.len() as u32;
let credentials_ptr = if credentials.is_empty() {
std::ptr::null()
} else {
credentials.as_ptr()
};
let result = unsafe { api(&clsid_guid, credential_count, credentials_ptr) };
if result.is_err() {
return Err(format!(
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentials()\n{}",
"Error: Error response from WebAuthNPluginAuthenticatorRemoveCredentials()\n{}",
result.message()
));
}
@@ -236,75 +289,181 @@ pub fn remove_credentials(
Ok(())
},
None => {
Err(String::from("Error: Can't complete remove_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentials can't be loaded."))
Err(String::from("Error: Can't complete remove_credentials(), as the function WebAuthNPluginAuthenticatorRemoveCredentials can't be loaded."))
}
}
}
// Helper struct to hold owned credential data
#[derive(Debug, Clone)]
pub struct OwnedCredentialDetails {
pub credential_id: Vec<u8>,
pub rpid: String,
pub rp_friendly_name: String,
pub user_id: Vec<u8>,
pub user_name: String,
pub user_display_name: String,
}
pub fn get_all_credentials(
plugin_clsid: String,
) -> std::result::Result<Option<ExperimentalWebAuthnPluginCredentialDetailsList>, String> {
clsid_guid: GUID,
) -> std::result::Result<Vec<OwnedCredentialDetails>, String> {
debug_log("Loading WebAuthNPluginAuthenticatorGetAllCredentials function...");
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentialsFnDeclaration>(
delay_load::<WebAuthNPluginAuthenticatorGetAllCredentialsFnDeclaration>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentials"),
s!("WebAuthNPluginAuthenticatorGetAllCredentials"),
)
};
match result {
Some(api) => {
// Create the wide string and keep it alive during the API call
let clsid_wide = plugin_clsid.to_utf16();
let mut credentials_list_ptr: *mut ExperimentalWebAuthnPluginCredentialDetailsList = std::ptr::null_mut();
let result = unsafe { api(clsid_wide.as_ptr(), &mut credentials_list_ptr) };
let mut credential_count: u32 = 0;
let mut credentials_array_ptr: *mut WebAuthnPluginCredentialDetails = std::ptr::null_mut();
let result = unsafe { api(&clsid_guid, &mut credential_count, &mut credentials_array_ptr) };
if result.is_err() {
return Err(format!(
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentials()\n{}",
"Error: Error response from WebAuthNPluginAuthenticatorGetAllCredentials()\n{}",
result.message()
));
}
if credentials_list_ptr.is_null() {
Ok(None)
} else {
// Note: The caller is responsible for managing the memory of the returned list
Ok(Some(unsafe { *credentials_list_ptr }))
if credentials_array_ptr.is_null() || credential_count == 0 {
debug_log("No credentials returned");
return Ok(Vec::new());
}
// Deep copy the credential data before Windows frees it
let credentials_slice = unsafe {
std::slice::from_raw_parts(credentials_array_ptr, credential_count as usize)
};
let mut owned_credentials = Vec::new();
for cred in credentials_slice {
unsafe {
// Copy credential ID bytes
let credential_id = if !cred.credential_id_pointer.is_null() && cred.credential_id_byte_count > 0 {
std::slice::from_raw_parts(cred.credential_id_pointer, cred.credential_id_byte_count as usize).to_vec()
} else {
Vec::new()
};
// Copy user ID bytes
let user_id = if !cred.user_id_pointer.is_null() && cred.user_id_byte_count > 0 {
std::slice::from_raw_parts(cred.user_id_pointer, cred.user_id_byte_count as usize).to_vec()
} else {
Vec::new()
};
// Copy string fields
let rpid = if !cred.rpid.is_null() {
String::from_utf16_lossy(std::slice::from_raw_parts(
cred.rpid,
(0..).position(|i| *cred.rpid.offset(i) == 0).unwrap_or(0)
))
} else {
String::new()
};
let rp_friendly_name = if !cred.rp_friendly_name.is_null() {
String::from_utf16_lossy(std::slice::from_raw_parts(
cred.rp_friendly_name,
(0..).position(|i| *cred.rp_friendly_name.offset(i) == 0).unwrap_or(0)
))
} else {
String::new()
};
let user_name = if !cred.user_name.is_null() {
String::from_utf16_lossy(std::slice::from_raw_parts(
cred.user_name,
(0..).position(|i| *cred.user_name.offset(i) == 0).unwrap_or(0)
))
} else {
String::new()
};
let user_display_name = if !cred.user_display_name.is_null() {
String::from_utf16_lossy(std::slice::from_raw_parts(
cred.user_display_name,
(0..).position(|i| *cred.user_display_name.offset(i) == 0).unwrap_or(0)
))
} else {
String::new()
};
owned_credentials.push(OwnedCredentialDetails {
credential_id,
rpid,
rp_friendly_name,
user_id,
user_name,
user_display_name,
});
}
}
// Free the array using the Windows API - this frees everything including strings
free_credential_details_array(credential_count, credentials_array_ptr);
debug_log(&format!("Retrieved {} credentials", owned_credentials.len()));
Ok(owned_credentials)
},
None => {
Err(String::from("Error: Can't complete get_all_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentials can't be loaded."))
Err(String::from("Error: Can't complete get_all_credentials(), as the function WebAuthNPluginAuthenticatorGetAllCredentials can't be loaded."))
}
}
}
pub fn remove_all_credentials(
plugin_clsid: String,
) -> std::result::Result<(), String> {
debug_log("Loading EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials function...");
fn free_credential_details_array(
credential_count: u32,
credentials_array: *mut WebAuthnPluginCredentialDetails,
) {
if credentials_array.is_null() {
return;
}
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentialsFnDeclaration>(
delay_load::<WebAuthNPluginAuthenticatorFreeCredentialDetailsArrayFnDeclaration>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials"),
s!("WebAuthNPluginAuthenticatorFreeCredentialDetailsArray"),
)
};
if let Some(api) = result {
unsafe { api(credential_count, credentials_array) };
} else {
debug_log("Warning: Could not load WebAuthNPluginAuthenticatorFreeCredentialDetailsArray");
}
}
pub fn remove_all_credentials(
clsid_guid: GUID,
) -> std::result::Result<(), String> {
debug_log("Loading WebAuthNPluginAuthenticatorRemoveAllCredentials function...");
let result = unsafe {
delay_load::<WebAuthNPluginAuthenticatorRemoveAllCredentialsFnDeclaration>(
s!("webauthn.dll"),
s!("WebAuthNPluginAuthenticatorRemoveAllCredentials"),
)
};
match result {
Some(api) => {
debug_log("Function loaded successfully, calling API...");
// Create the wide string and keep it alive during the API call
let clsid_wide = plugin_clsid.to_utf16();
let result = unsafe { api(clsid_wide.as_ptr()) };
let result = unsafe { api(&clsid_guid) };
if result.is_err() {
let error_code = result.0;
debug_log(&format!("API call failed with HRESULT: 0x{:x}", error_code));
return Err(format!(
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials()\nHRESULT: 0x{:x}\n{}",
"Error: Error response from WebAuthNPluginAuthenticatorRemoveAllCredentials()\nHRESULT: 0x{:x}\n{}",
error_code, result.message()
));
}
@@ -313,8 +472,8 @@ pub fn remove_all_credentials(
Ok(())
},
None => {
debug_log("Failed to load EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials function from webauthn.dll");
Err(String::from("Error: Can't complete remove_all_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials can't be loaded."))
debug_log("Failed to load WebAuthNPluginAuthenticatorRemoveAllCredentials function from webauthn.dll");
Err(String::from("Error: Can't complete remove_all_credentials(), as the function WebAuthNPluginAuthenticatorRemoveAllCredentials can't be loaded."))
}
}
}