diff --git a/.vscode/settings.json b/.vscode/settings.json
index 8f89bc03b8c..3d6870b2f44 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -8,5 +8,6 @@
},
"rust-analyzer.linkedProjects": ["apps/desktop/desktop_native/Cargo.toml"],
"typescript.tsdk": "node_modules/typescript/lib",
- "eslint.useFlatConfig": true
+ "eslint.useFlatConfig": true,
+ "rust-analyzer.server.path": null
}
diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_registration.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_registration.rs
deleted file mode 100644
index 68e92145b77..00000000000
--- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_registration.rs
+++ /dev/null
@@ -1,338 +0,0 @@
-use std::ptr;
-
-use base64::{engine::general_purpose::STANDARD, Engine as _};
-use windows::core::{s, ComObjectInterface, GUID, HRESULT, HSTRING, PCWSTR};
-use windows::Win32::System::Com::*;
-
-use crate::com_provider;
-use crate::util::delay_load;
-use crate::webauthn::*;
-use ciborium::value::Value;
-
-const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop";
-const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062";
-const RPID: &str = "bitwarden.com";
-const AAGUID: &str = "d548826e-79b4-db40-a3d8-11116f7e8349";
-const LOGO_SVG: &str = r##""##;
-
-/// Parses a UUID string (with hyphens) into bytes
-fn parse_uuid_to_bytes(uuid_str: &str) -> Result, String> {
- let uuid_clean = uuid_str.replace("-", "");
- if uuid_clean.len() != 32 {
- return Err("Invalid UUID format".to_string());
- }
-
- uuid_clean
- .chars()
- .collect::>()
- .chunks(2)
- .map(|chunk| {
- let hex_str: String = chunk.iter().collect();
- u8::from_str_radix(&hex_str, 16)
- .map_err(|_| format!("Invalid hex character in UUID: {}", hex_str))
- })
- .collect()
-}
-
-/// Converts a CLSID string to a GUID
-pub(crate) fn parse_clsid_to_guid_str(clsid_str: &str) -> Result {
- // Remove hyphens and parse as hex
- let clsid_clean = clsid_str.replace("-", "");
- if clsid_clean.len() != 32 {
- return Err("Invalid CLSID format".to_string());
- }
-
- // Convert to u128 and create GUID
- let clsid_u128 = u128::from_str_radix(&clsid_clean, 16)
- .map_err(|_| "Failed to parse CLSID as hex".to_string())?;
-
- Ok(GUID::from_u128(clsid_u128))
-}
-
-/// Converts the CLSID constant string to a GUID
-fn parse_clsid_to_guid() -> Result {
- parse_clsid_to_guid_str(CLSID)
-}
-
-/// Generates CBOR-encoded authenticator info according to FIDO CTAP2 specifications
-/// See: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetInfo
-fn generate_cbor_authenticator_info() -> Result, String> {
- // Parse AAGUID from string format to bytes
- let aaguid_bytes = parse_uuid_to_bytes(AAGUID)?;
-
- // Create the authenticator info map according to CTAP2 spec
- // Using Vec<(Value, Value)> because that's what ciborium::Value::Map expects
- let mut authenticator_info = Vec::new();
-
- // 1: versions - Array of supported FIDO versions
- authenticator_info.push((
- Value::Integer(1.into()),
- Value::Array(vec![
- Value::Text("FIDO_2_0".to_string()),
- Value::Text("FIDO_2_1".to_string()),
- ]),
- ));
-
- // 2: extensions - Array of supported extensions (empty for now)
- authenticator_info.push((Value::Integer(2.into()), Value::Array(vec![])));
-
- // 3: aaguid - 16-byte AAGUID
- authenticator_info.push((Value::Integer(3.into()), Value::Bytes(aaguid_bytes)));
-
- // 4: options - Map of supported options
- let options = vec![
- (Value::Text("rk".to_string()), Value::Bool(true)), // resident key
- (Value::Text("up".to_string()), Value::Bool(true)), // user presence
- (Value::Text("uv".to_string()), Value::Bool(true)), // user verification
- ];
- authenticator_info.push((Value::Integer(4.into()), Value::Map(options)));
-
- // 9: transports - Array of supported transports
- authenticator_info.push((
- Value::Integer(9.into()),
- Value::Array(vec![
- Value::Text("internal".to_string()),
- Value::Text("hybrid".to_string()),
- ]),
- ));
-
- // 10: algorithms - Array of supported algorithms
- let algorithm = vec![
- (Value::Text("alg".to_string()), Value::Integer((-7).into())), // ES256
- (
- Value::Text("type".to_string()),
- Value::Text("public-key".to_string()),
- ),
- ];
- authenticator_info.push((
- Value::Integer(10.into()),
- Value::Array(vec![Value::Map(algorithm)]),
- ));
-
- // Encode to CBOR
- let mut buffer = Vec::new();
- ciborium::ser::into_writer(&Value::Map(authenticator_info), &mut buffer)
- .map_err(|e| format!("Failed to encode CBOR: {}", e))?;
-
- Ok(buffer)
-}
-
-/// Initializes the COM library for use on the calling thread,
-/// and registers + sets the security values.
-pub fn initialize_com_library() -> std::result::Result<(), String> {
- let result = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
-
- if result.is_err() {
- return Err(format!(
- "Error: couldn't initialize the COM library\n{}",
- result.message()
- ));
- }
-
- match unsafe {
- CoInitializeSecurity(
- None,
- -1,
- None,
- None,
- RPC_C_AUTHN_LEVEL_DEFAULT,
- RPC_C_IMP_LEVEL_IMPERSONATE,
- None,
- EOAC_NONE,
- None,
- )
- } {
- Ok(_) => Ok(()),
- Err(e) => Err(format!(
- "Error: couldn't initialize COM security\n{}",
- e.message()
- )),
- }
-}
-
-/// Registers the Bitwarden Plugin Authenticator COM library with Windows.
-pub fn register_com_library() -> std::result::Result<(), String> {
- static FACTORY: windows::core::StaticComObject =
- com_provider::Factory.into_static();
- let clsid_guid = parse_clsid_to_guid().map_err(|e| format!("Failed to parse CLSID: {}", e))?;
- let clsid: *const GUID = &clsid_guid;
-
- match unsafe {
- CoRegisterClassObject(
- clsid,
- FACTORY.as_interface_ref(),
- //FACTORY.as_interface::(),
- CLSCTX_LOCAL_SERVER,
- REGCLS_MULTIPLEUSE,
- )
- } {
- Ok(_) => Ok(()),
- Err(e) => Err(format!(
- "Error: couldn't register the COM library\n{}",
- e.message()
- )),
- }
-}
-
-/// Adds Bitwarden as a plugin authenticator.
-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();
-
- // 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();
-
- // Base64-encode the SVG as required by Windows API
- let logo_b64: String = STANDARD.encode(LOGO_SVG);
- let logo_b64_buf: Vec = logo_b64.encode_utf16().chain(std::iter::once(0)).collect();
-
- // Generate CBOR authenticator info dynamically
- let authenticator_info_bytes = generate_cbor_authenticator_info()
- .map_err(|e| format!("Failed to generate authenticator info: {}", e))?;
-
- let add_authenticator_options = WebAuthnPluginAddAuthenticatorOptions {
- authenticator_name: authenticator_name_ptr,
- rclsid: &clsid_guid, // Changed to GUID reference
- rpid: relying_party_id_ptr,
- light_theme_logo_svg: logo_b64_buf.as_ptr(),
- dark_theme_logo_svg: logo_b64_buf.as_ptr(),
- cbor_authenticator_info_byte_count: authenticator_info_bytes.len() as u32,
- 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 mut add_response_ptr: *mut WebAuthnPluginAddAuthenticatorResponse = ptr::null_mut();
-
- let result = unsafe {
- delay_load::(
- s!("webauthn.dll"),
- s!("WebAuthNPluginAddAuthenticator"), // Stable function name
- )
- };
-
- match result {
- Some(api) => {
- let result = unsafe { api(&add_authenticator_options, &mut add_response_ptr) };
-
- if result.is_err() {
- return Err(format!(
- "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 WebAuthNPluginAddAuthenticator can't be found."))
- }
- }
-}
-
-fn free_add_authenticator_response(response: *mut WebAuthnPluginAddAuthenticatorResponse) {
- let result = unsafe {
- delay_load::(
- s!("webauthn.dll"),
- s!("WebAuthNPluginFreeAddAuthenticatorResponse"),
- )
- };
-
- if let Some(api) = result {
- unsafe { api(response) };
- }
-}
-
-type WebAuthNPluginAddAuthenticatorFnDeclaration = unsafe extern "cdecl" fn(
- pPluginAddAuthenticatorOptions: *const WebAuthnPluginAddAuthenticatorOptions,
- ppPluginAddAuthenticatorResponse: *mut *mut WebAuthnPluginAddAuthenticatorResponse,
-) -> HRESULT;
-
-type WebAuthNPluginFreeAddAuthenticatorResponseFnDeclaration = unsafe extern "cdecl" fn(
- pPluginAddAuthenticatorResponse: *mut WebAuthnPluginAddAuthenticatorResponse,
-);
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_generate_cbor_authenticator_info() {
- let result = generate_cbor_authenticator_info();
- assert!(result.is_ok(), "CBOR generation should succeed");
-
- let cbor_bytes = result.unwrap();
- assert!(!cbor_bytes.is_empty(), "CBOR bytes should not be empty");
-
- // Verify the CBOR can be decoded back
- let decoded: Result = ciborium::de::from_reader(&cbor_bytes[..]);
- assert!(decoded.is_ok(), "Generated CBOR should be valid");
-
- // Verify it's a map with expected keys
- if let Value::Map(map) = decoded.unwrap() {
- assert!(
- map.iter().any(|(k, _)| k == &Value::Integer(1.into())),
- "Should contain versions (key 1)"
- );
- assert!(
- map.iter().any(|(k, _)| k == &Value::Integer(2.into())),
- "Should contain extensions (key 2)"
- );
- assert!(
- map.iter().any(|(k, _)| k == &Value::Integer(3.into())),
- "Should contain aaguid (key 3)"
- );
- assert!(
- map.iter().any(|(k, _)| k == &Value::Integer(4.into())),
- "Should contain options (key 4)"
- );
- assert!(
- map.iter().any(|(k, _)| k == &Value::Integer(9.into())),
- "Should contain transports (key 9)"
- );
- assert!(
- map.iter().any(|(k, _)| k == &Value::Integer(10.into())),
- "Should contain algorithms (key 10)"
- );
- } else {
- panic!("CBOR should decode to a map");
- }
-
- // Print the generated CBOR for verification
- println!("Generated CBOR hex: {}", hex::encode(&cbor_bytes));
- }
-
- #[test]
- fn test_aaguid_parsing() {
- let result = parse_uuid_to_bytes(AAGUID);
- assert!(result.is_ok(), "AAGUID parsing should succeed");
-
- let aaguid_bytes = result.unwrap();
- assert_eq!(aaguid_bytes.len(), 16, "AAGUID should be 16 bytes");
- assert_eq!(aaguid_bytes[0], 0xd5, "First byte should be 0xd5");
- assert_eq!(aaguid_bytes[1], 0x48, "Second byte should be 0x48");
-
- // Verify full expected AAGUID
- let expected_hex = "d548826e79b4db40a3d811116f7e8349";
- let expected_bytes = hex::decode(expected_hex).unwrap();
- assert_eq!(
- aaguid_bytes, expected_bytes,
- "AAGUID should match expected value"
- );
- }
-
- #[test]
- fn test_parse_clsid_to_guid() {
- let result = parse_clsid_to_guid();
- assert!(result.is_ok(), "CLSID parsing should succeed");
- }
-}
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 73c085ef83b..9aa975f08dc 100644
--- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs
+++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs
@@ -6,31 +6,76 @@
mod assert;
mod com_buffer;
mod com_provider;
-mod com_registration;
mod ipc2;
mod make_credential;
mod types;
mod util;
mod webauthn;
+mod win_webauthn;
+
+use std::collections::HashSet;
// Re-export main functionality
-pub use com_registration::{add_authenticator, initialize_com_library, register_com_library};
pub use types::UserVerificationRequirement;
+use win_webauthn::{PluginAddAuthenticatorOptions, WebAuthnPlugin};
+
+use crate::win_webauthn::{AuthenticatorInfo, CtapVersion, PublicKeyCredentialParameters};
+
+const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop";
+const RPID: &str = "bitwarden.com";
+const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062";
+const AAGUID: &str = "d548826e-79b4-db40-a3d8-11116f7e8349";
+const LOGO_SVG: &str = r##""##;
+
/// Handles initialization and registration for the Bitwarden desktop app as a
/// For now, also adds the authenticator
pub fn register() -> std::result::Result<(), String> {
// TODO: Can we spawn a new named thread for debugging?
tracing::debug!("register() called...");
- let r = com_registration::initialize_com_library();
- tracing::debug!("Initialized the com library: {:?}", r);
+ let r = WebAuthnPlugin::initialize();
+ tracing::debug!(
+ "Initialized the com library with WebAuthnPlugin::initialize(): {:?}",
+ r
+ );
- let r = com_registration::register_com_library();
+ let clsid = CLSID.try_into().expect("valid GUID string");
+ let plugin = WebAuthnPlugin::new(clsid);
+ let r = plugin.register_server();
tracing::debug!("Registered the com library: {:?}", r);
- let r = com_registration::add_authenticator();
- tracing::debug!("Added the authenticator: {:?}", r);
+ tracing::debug!("Parsing authenticator options");
+ let aaguid = AAGUID
+ .try_into()
+ .map_err(|err| format!("Invalid AAGUID `{AAGUID}`: {err}"))?;
+ let options = PluginAddAuthenticatorOptions {
+ authenticator_name: AUTHENTICATOR_NAME.to_string(),
+ clsid,
+ rp_id: Some(RPID.to_string()),
+ light_theme_logo_svg: Some(LOGO_SVG.to_string()),
+ dark_theme_logo_svg: Some(LOGO_SVG.to_string()),
+ authenticator_info: AuthenticatorInfo {
+ versions: HashSet::from([CtapVersion::Fido2_0, CtapVersion::Fido2_1]),
+ aaguid: aaguid,
+ options: Some(HashSet::from([
+ "rk".to_string(),
+ "up".to_string(),
+ "uv".to_string(),
+ ])),
+ transports: Some(HashSet::from([
+ "internal".to_string(),
+ "hybrid".to_string(),
+ ])),
+ algorithms: Some(vec![PublicKeyCredentialParameters {
+ alg: -7,
+ typ: "public-key".to_string(),
+ }]),
+ },
+ supported_rp_ids: None,
+ };
+ let response = WebAuthnPlugin::add_authenticator(options);
+ tracing::debug!("Added the authenticator: {response:?}");
Ok(())
}
diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/com.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/com.rs
new file mode 100644
index 00000000000..7a180c4f300
--- /dev/null
+++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/com.rs
@@ -0,0 +1,90 @@
+//! Functions for interacting with Windows COM.
+
+use std::ptr;
+
+use windows::{
+ core::{implement, ComObjectInterface, IUnknown, GUID},
+ Win32::System::Com::*,
+};
+
+use super::{ErrorKind, WinWebAuthnError};
+
+#[implement(IClassFactory)]
+pub struct Factory;
+
+impl IClassFactory_Impl for Factory_Impl {
+ fn CreateInstance(
+ &self,
+ _outer: windows::core::Ref,
+ _iid: *const windows::core::GUID,
+ _object: *mut *mut core::ffi::c_void,
+ ) -> windows::core::Result<()> {
+ unimplemented!()
+ }
+
+ fn LockServer(&self, _lock: windows::core::BOOL) -> windows::core::Result<()> {
+ unimplemented!()
+ }
+}
+
+/// Registers the plugin authenticator COM library with Windows.
+pub(super) fn register_server(clsid: &GUID) -> Result<(), WinWebAuthnError> {
+ static FACTORY: windows::core::StaticComObject =
+ crate::com_provider::Factory.into_static();
+ unsafe {
+ CoRegisterClassObject(
+ ptr::from_ref(clsid),
+ FACTORY.as_interface_ref(),
+ CLSCTX_LOCAL_SERVER,
+ REGCLS_MULTIPLEUSE,
+ )
+ }
+ .map_err(|err| {
+ WinWebAuthnError::with_cause(
+ ErrorKind::WindowsInternal,
+ "Couldn't register the COM library with Windows",
+ err,
+ )
+ })?;
+ Ok(())
+}
+
+/// Initializes the COM library for use on the calling thread,
+/// and registers + sets the security values.
+pub(super) fn initialize() -> std::result::Result<(), WinWebAuthnError> {
+ let result = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
+
+ if result.is_err() {
+ return Err(WinWebAuthnError::with_cause(
+ ErrorKind::WindowsInternal,
+ "Could not initialize the COM library",
+ windows::core::Error::from_hresult(result),
+ ));
+ }
+
+ unsafe {
+ CoInitializeSecurity(
+ None,
+ -1,
+ None,
+ None,
+ RPC_C_AUTHN_LEVEL_DEFAULT,
+ RPC_C_IMP_LEVEL_IMPERSONATE,
+ None,
+ EOAC_NONE,
+ None,
+ )
+ }
+ .map_err(|err| {
+ WinWebAuthnError::with_cause(
+ ErrorKind::WindowsInternal,
+ "Could not initialize COM security",
+ err,
+ )
+ })
+}
+
+pub(super) fn uninitialize() -> std::result::Result<(), WinWebAuthnError> {
+ unsafe { CoUninitialize() };
+ Ok(())
+}
diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/mod.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/mod.rs
new file mode 100644
index 00000000000..879342ca01f
--- /dev/null
+++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/mod.rs
@@ -0,0 +1,208 @@
+mod com;
+mod types;
+mod util;
+
+use std::{error::Error, fmt::Display, ptr::NonNull};
+
+use windows::core::GUID;
+
+pub use types::{
+ AuthenticatorInfo, CtapVersion, PluginAddAuthenticatorOptions, PublicKeyCredentialParameters,
+};
+
+use crate::win_webauthn::{
+ types::{
+ webauthn_plugin_add_authenticator, PluginAddAuthenticatorResponse,
+ WebAuthnPluginAddAuthenticatorResponse, WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS,
+ },
+ util::WindowsString,
+};
+
+#[derive(Clone, Copy)]
+pub struct Clsid(GUID);
+
+impl TryFrom<&str> for Clsid {
+ type Error = WinWebAuthnError;
+
+ fn try_from(value: &str) -> Result {
+ // Remove hyphens and parse as hex
+ let clsid_clean = value.replace("-", "").replace("{", "").replace("}", "");
+ if clsid_clean.len() != 32 {
+ return Err(WinWebAuthnError::new(
+ ErrorKind::Serialization,
+ "Invalid CLSID format",
+ ));
+ }
+
+ // Convert to u128 and create GUID
+ let clsid_u128 = u128::from_str_radix(&clsid_clean, 16).map_err(|err| {
+ WinWebAuthnError::with_cause(
+ ErrorKind::Serialization,
+ "Failed to parse CLSID as hex",
+ err,
+ )
+ })?;
+
+ let clsid = Clsid(GUID::from_u128(clsid_u128));
+ Ok(clsid)
+ }
+}
+
+pub struct WebAuthnPlugin {
+ clsid: Clsid,
+}
+
+impl WebAuthnPlugin {
+ pub fn new(clsid: Clsid) -> Self {
+ WebAuthnPlugin { clsid }
+ }
+
+ /// Registers a COM server with Windows.
+ ///
+ /// This only needs to be called on installation of your application.
+ pub fn register_server(&self) -> Result<(), WinWebAuthnError> {
+ com::register_server(&self.clsid.0)
+ }
+
+ /// Initializes the COM library for use on the calling thread,
+ /// and registers + sets the security values.
+ pub fn initialize() -> Result<(), WinWebAuthnError> {
+ com::initialize()
+ }
+
+ /// Adds this implementation as a Windows WebAuthn plugin.
+ ///
+ /// This only needs to be called on installation of your application.
+ pub fn add_authenticator(
+ options: PluginAddAuthenticatorOptions,
+ ) -> Result {
+ let mut response_ptr: *mut WebAuthnPluginAddAuthenticatorResponse = std::ptr::null_mut();
+
+ // We need to be careful to use .as_ref() to ensure that we're not
+ // sending dangling pointers to API.
+ let authenticator_name = options.authenticator_name.to_utf16();
+
+ let rp_id = options.rp_id.as_ref().map(|rp_id| rp_id.to_utf16());
+ let pwszPluginRpId = rp_id.as_ref().map_or(std::ptr::null(), |v| v.as_ptr());
+
+ let light_logo_b64 = options.light_theme_logo_b64();
+ let pwszLightThemeLogoSvg = light_logo_b64
+ .as_ref()
+ .map_or(std::ptr::null(), |v| v.as_ptr());
+ let dark_logo_b64 = options.dark_theme_logo_b64();
+ let pwszDarkThemeLogoSvg = dark_logo_b64
+ .as_ref()
+ .map_or(std::ptr::null(), |v| v.as_ptr());
+
+ let authenticator_info = options.authenticator_info.as_ctap_bytes()?;
+
+ let supported_rp_ids: Option>> = options
+ .supported_rp_ids
+ .map(|ids| ids.iter().map(|id| id.to_utf16()).collect());
+ let supported_rp_id_ptrs: Option> = supported_rp_ids
+ .as_ref()
+ .map(|ids| ids.iter().map(Vec::as_ptr).collect());
+ let pbSupportedRpIds = supported_rp_id_ptrs
+ .as_ref()
+ .map_or(std::ptr::null(), |v| v.as_ptr());
+
+ let options_c = WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS {
+ pwszAuthenticatorName: authenticator_name.as_ptr(),
+ rclsid: &options.clsid.0,
+ pwszPluginRpId,
+ pwszLightThemeLogoSvg,
+ pwszDarkThemeLogoSvg,
+ cbAuthenticatorInfo: authenticator_info.len() as u32,
+ pbAuthenticatorInfo: authenticator_info.as_ptr(),
+ cSupportedRpIds: supported_rp_id_ptrs.map_or(0, |ids| ids.len() as u32),
+ pbSupportedRpIds,
+ };
+ let result = webauthn_plugin_add_authenticator(&options_c, &mut response_ptr)?;
+ result.ok().map_err(|err| {
+ WinWebAuthnError::with_cause(
+ ErrorKind::WindowsInternal,
+ "Failed to add authenticator",
+ err,
+ )
+ })?;
+
+ if let Some(response) = NonNull::new(response_ptr) {
+ Ok(response.into())
+ } else {
+ Err(WinWebAuthnError::new(
+ ErrorKind::WindowsInternal,
+ "WebAuthNPluginAddAuthenticatorResponse returned null",
+ ))
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct WinWebAuthnError {
+ kind: ErrorKind,
+ description: Option,
+ cause: Option>,
+}
+
+impl WinWebAuthnError {
+ pub(crate) fn new(kind: ErrorKind, description: &str) -> Self {
+ Self {
+ kind,
+ description: Some(description.to_string()),
+ cause: None,
+ }
+ }
+
+ pub(crate) fn with_cause(
+ kind: ErrorKind,
+ description: &str,
+ cause: E,
+ ) -> Self {
+ let cause: Box = Box::new(cause);
+ Self {
+ kind,
+ description: Some(description.to_string()),
+ cause: Some(cause),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum ErrorKind {
+ DllLoad,
+ Serialization,
+ WindowsInternal,
+}
+
+impl Display for WinWebAuthnError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let msg = match self.kind {
+ ErrorKind::Serialization => "Failed to serialize data",
+ ErrorKind::DllLoad => "Failed to load function from DLL",
+ ErrorKind::WindowsInternal => "A Windows error occurred",
+ };
+ f.write_str(msg)?;
+ if let Some(d) = &self.description {
+ write!(f, ": {d}")?;
+ }
+ if let Some(e) = &self.cause {
+ write!(f, ". Caused by: {e}")?;
+ }
+ Ok(())
+ }
+}
+
+impl Error for WinWebAuthnError {}
+
+#[cfg(test)]
+mod tests {
+ use super::Clsid;
+
+ const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062";
+
+ #[test]
+ fn test_parse_clsid_to_guid() {
+ let result = Clsid::try_from(CLSID);
+ assert!(result.is_ok(), "CLSID parsing should succeed");
+ }
+}
diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/types.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/types.rs
new file mode 100644
index 00000000000..5b1291ad0f8
--- /dev/null
+++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/types.rs
@@ -0,0 +1,432 @@
+//! Types and functions defined in the Windows WebAuthn API.
+
+use std::{collections::HashSet, ptr::NonNull};
+
+use base64::{engine::general_purpose::STANDARD, Engine as _};
+use ciborium::Value;
+use windows::{
+ core::{GUID, HRESULT},
+ Win32::System::LibraryLoader::GetProcAddress,
+};
+use windows_core::s;
+
+use crate::win_webauthn::{util::WindowsString, Clsid, ErrorKind, WinWebAuthnError};
+
+macro_rules! webauthn_call {
+ ($symbol:literal as fn $fn_name:ident($($arg:ident: $arg_type:ty,)+) -> $result_type:ty) => (
+ pub(super) fn $fn_name($($arg: $arg_type),*) -> Result<$result_type, WinWebAuthnError> {
+ let library = super::util::load_webauthn_lib()?;
+ let response = unsafe {
+ let address = GetProcAddress(library, s!($symbol)).ok_or(
+ WinWebAuthnError::new(
+ ErrorKind::DllLoad,
+ &format!(
+ "Failed to load function {}",
+ $symbol
+ ),
+ ),
+ )?;
+
+ let function: unsafe extern "cdecl" fn(
+ $($arg: $arg_type),*
+ ) -> $result_type = std::mem::transmute_copy(&address);
+ function($($arg),*)
+ };
+ super::util::free_webauthn_lib(library)?;
+ Ok(response)
+ }
+ )
+}
+
+/// 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(super) struct WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS {
+ /// Authenticator Name
+ pub(super) pwszAuthenticatorName: *const u16,
+
+ /// Plugin COM ClsId
+ pub(super) rclsid: *const GUID,
+
+ /// Plugin RPID
+ ///
+ /// Required for a nested WebAuthN call originating from a plugin.
+ pub(super) pwszPluginRpId: *const u16,
+
+ /// Plugin Authenticator Logo for the Light themes. base64-encoded SVG 1.1
+ ///
+ /// The data should be encoded as `UTF16(BASE64(UTF8(svg_text)))`.
+ pub(super) pwszLightThemeLogoSvg: *const u16,
+
+ /// Plugin Authenticator Logo for the Dark themes. base64-encoded SVG 1.1
+ ///
+ /// The data should be encoded as `UTF16(BASE64(UTF8(svg_text)))`.
+ pub(super) pwszDarkThemeLogoSvg: *const u16,
+
+ pub(super) cbAuthenticatorInfo: u32,
+ /// CTAP CBOR-encoded authenticatorGetInfo output
+ pub(super) pbAuthenticatorInfo: *const u8,
+
+ pub(super) cSupportedRpIds: u32,
+ /// List of supported RP IDs (Relying Party IDs).
+ ///
+ /// Should be null if all RPs are supported.
+ pub(super) pbSupportedRpIds: *const *const u16,
+}
+
+pub struct PluginAddAuthenticatorOptions {
+ /// Authenticator Name
+ pub authenticator_name: String,
+
+ /// Plugin COM ClsId
+ pub clsid: Clsid,
+
+ /// Plugin RPID
+ ///
+ /// Required for a nested WebAuthN call originating from a plugin.
+ pub rp_id: Option,
+
+ /// Plugin Authenticator Logo for the Light themes.
+ ///
+ /// String should contain a valid SVG 1.1 document.
+ pub light_theme_logo_svg: Option,
+
+ // Plugin Authenticator Logo for the Dark themes. Bytes of SVG 1.1.
+ ///
+ /// String should contain a valid SVG 1.1 element.
+ pub dark_theme_logo_svg: Option,
+
+ /// CTAP authenticatorGetInfo values
+ pub authenticator_info: AuthenticatorInfo,
+
+ /// List of supported RP IDs (Relying Party IDs).
+ ///
+ /// Should be [None] if all RPs are supported.
+ pub supported_rp_ids: Option>,
+}
+
+impl PluginAddAuthenticatorOptions {
+ pub fn light_theme_logo_b64(&self) -> Option> {
+ self.light_theme_logo_svg
+ .as_ref()
+ .map(|svg| Self::encode_svg(&svg))
+ }
+
+ pub fn dark_theme_logo_b64(&self) -> Option> {
+ self.dark_theme_logo_svg
+ .as_ref()
+ .map(|svg| Self::encode_svg(&svg))
+ }
+
+ fn encode_svg(svg: &str) -> Vec {
+ let logo_b64: String = STANDARD.encode(svg);
+ logo_b64.to_utf16()
+ }
+}
+
+/// Used as a response type when adding a Windows plugin authenticator.
+/// Header File Name: _WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE
+/// Header File Usage: WebAuthNPluginAddAuthenticator()
+/// WebAuthNPluginFreeAddAuthenticatorResponse()
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub(super) struct WebAuthnPluginAddAuthenticatorResponse {
+ pub plugin_operation_signing_key_byte_count: u32,
+ pub plugin_operation_signing_key: *mut u8,
+}
+
+type WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE = WebAuthnPluginAddAuthenticatorResponse;
+
+/// Safe wrapper around [WebAuthnPluginAddAuthenticatorResponse]
+#[derive(Debug)]
+pub struct PluginAddAuthenticatorResponse {
+ inner: NonNull,
+}
+
+impl PluginAddAuthenticatorResponse {
+ pub fn plugin_operation_signing_key(&self) -> &[u8] {
+ unsafe {
+ let p = &*self.inner.as_ptr();
+ std::slice::from_raw_parts(
+ p.plugin_operation_signing_key,
+ p.plugin_operation_signing_key_byte_count as usize,
+ )
+ }
+ }
+}
+
+impl From> for PluginAddAuthenticatorResponse {
+ fn from(value: NonNull) -> Self {
+ Self { inner: value }
+ }
+}
+
+impl Drop for PluginAddAuthenticatorResponse {
+ fn drop(&mut self) {
+ unsafe {
+ // SAFETY: This should only fail if:
+ // - we cannot load the webauthn.dll, which we already have if we have constructed this type, or
+ // - we spelled the function wrong, which is a library error.
+ webauthn_plugin_free_add_authenticator_response(self.inner.as_mut())
+ .expect("function to load properly");
+ }
+ }
+}
+
+webauthn_call!("WebAuthNPluginAddAuthenticator" as
+fn webauthn_plugin_add_authenticator(
+ pPluginAddAuthenticatorOptions: *const WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS,
+ ppPluginAddAuthenticatorResponse: *mut *mut WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE,
+) -> HRESULT);
+
+webauthn_call!("WebAuthNPluginFreeAddAuthenticatorResponse" as
+fn webauthn_plugin_free_add_authenticator_response(
+ pPluginAddAuthenticatorOptions: *mut WebAuthnPluginAddAuthenticatorResponse,
+) -> ());
+
+/// List of its supported protocol versions and extensions, its AAGUID, and
+/// other aspects of its overall capabilities.
+pub struct AuthenticatorInfo {
+ /// List of supported versions.
+ pub versions: HashSet,
+
+ /// The claimed AAGUID. 16 bytes in length and encoded the same as
+ /// MakeCredential AuthenticatorData, as specified in [WebAuthn].
+ ///
+ /// Note: even though the name has "guid" in it, this is actually an RFC4122
+ /// UUID, which is serialized differently than a Windows GUID.
+ pub aaguid: Uuid,
+
+ /// List of supported options.
+ pub options: Option>,
+
+ /// List of supported transports. Values are taken from the
+ /// AuthenticatorTransport enum in [WebAuthn]. The list MUST NOT include
+ /// duplicate values nor be empty if present. Platforms MUST tolerate
+ /// unknown values.
+ pub transports: Option>,
+
+ /// List of supported algorithms for credential generation, as specified in
+ /// [WebAuthn]. The array is ordered from most preferred to least preferred
+ /// and MUST NOT include duplicate entries nor be empty if present.
+ /// PublicKeyCredentialParameters' algorithm identifiers are values that
+ /// SHOULD be registered in the IANA COSE Algorithms registry
+ /// [IANA-COSE-ALGS-REG].
+ pub algorithms: Option>,
+}
+
+impl AuthenticatorInfo {
+ pub fn as_ctap_bytes(&self) -> Result, super::WinWebAuthnError> {
+ // Create the authenticator info map according to CTAP2 spec
+ // Using Vec<(Value, Value)> because that's what ciborium::Value::Map expects
+ let mut authenticator_info = Vec::new();
+
+ // 1: versions - Array of supported FIDO versions
+ let versions = self
+ .versions
+ .iter()
+ .map(|v| Value::Text(v.into()))
+ .collect();
+ authenticator_info.push((Value::Integer(1.into()), Value::Array(versions)));
+
+ // 2: extensions - Array of supported extensions (empty for now)
+ authenticator_info.push((Value::Integer(2.into()), Value::Array(vec![])));
+
+ // 3: aaguid - 16-byte AAGUID
+ authenticator_info.push((
+ Value::Integer(3.into()),
+ Value::Bytes(self.aaguid.0.to_vec()),
+ ));
+
+ // 4: options - Map of supported options
+ if let Some(options) = &self.options {
+ let options = options
+ .iter()
+ .map(|o| (Value::Text(o.into()), Value::Bool(true)))
+ .collect();
+ authenticator_info.push((Value::Integer(4.into()), Value::Map(options)));
+ }
+
+ // 9: transports - Array of supported transports
+ if let Some(transports) = &self.transports {
+ let transports = transports.iter().map(|t| Value::Text(t.clone())).collect();
+ authenticator_info.push((Value::Integer(9.into()), Value::Array(transports)));
+ }
+
+ // 10: algorithms - Array of supported algorithms
+ if let Some(algorithms) = &self.algorithms {
+ let algorithms: Vec = algorithms
+ .iter()
+ .map(|a| {
+ Value::Map(vec![
+ (Value::Text("alg".to_string()), Value::Integer(a.alg.into())),
+ (Value::Text("type".to_string()), Value::Text(a.typ.clone())),
+ ])
+ })
+ .collect();
+ authenticator_info.push((Value::Integer(10.into()), Value::Array(algorithms)));
+ }
+
+ // Encode to CBOR
+ let mut buffer = Vec::new();
+ ciborium::ser::into_writer(&Value::Map(authenticator_info), &mut buffer).map_err(|e| {
+ WinWebAuthnError::with_cause(
+ ErrorKind::Serialization,
+ "Failed to serialize authenticator info into CBOR",
+ e,
+ )
+ })?;
+
+ Ok(buffer)
+ }
+}
+
+// A UUID is not the same as a Windows GUID
+/// An RFC4122 UUID.
+pub struct Uuid([u8; 16]);
+
+impl TryFrom<&str> for Uuid {
+ type Error = WinWebAuthnError;
+
+ fn try_from(value: &str) -> Result {
+ let uuid_clean = value.replace("-", "").replace("{", "").replace("}", "");
+ if uuid_clean.len() != 32 {
+ return Err(WinWebAuthnError::new(
+ ErrorKind::Serialization,
+ "Invalid UUID format",
+ ));
+ }
+
+ let bytes = uuid_clean
+ .chars()
+ .collect::>()
+ .chunks(2)
+ .map(|chunk| {
+ let hex_str: String = chunk.iter().collect();
+ u8::from_str_radix(&hex_str, 16).map_err(|_| {
+ WinWebAuthnError::new(
+ ErrorKind::Serialization,
+ &format!("Invalid hex character in UUID: {}", hex_str),
+ )
+ })
+ })
+ .collect::, WinWebAuthnError>>()?;
+
+ // SAFETY: We already checked the length of the string before, so this should result in the correct number of bytes.
+ let b: [u8; 16] = bytes.try_into().expect("16 bytes to be parsed");
+ Ok(Uuid(b))
+ }
+}
+
+#[derive(Hash, Eq, PartialEq)]
+pub enum CtapVersion {
+ Fido2_0,
+ Fido2_1,
+}
+
+pub struct PublicKeyCredentialParameters {
+ pub alg: i32,
+ pub typ: String,
+}
+
+impl From<&CtapVersion> for String {
+ fn from(value: &CtapVersion) -> Self {
+ match value {
+ CtapVersion::Fido2_0 => "FIDO_2_0",
+ CtapVersion::Fido2_1 => "FIDO_2_1",
+ }
+ .to_string()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const AAGUID: &str = "d548826e-79b4-db40-a3d8-11116f7e8349";
+ #[test]
+ fn test_generate_cbor_authenticator_info() {
+ let aaguid = Uuid::try_from(AAGUID).unwrap();
+ let authenticator_info = AuthenticatorInfo {
+ versions: HashSet::from([CtapVersion::Fido2_0, CtapVersion::Fido2_1]),
+ aaguid: aaguid,
+ options: Some(HashSet::from([
+ "rk".to_string(),
+ "up".to_string(),
+ "uv".to_string(),
+ ])),
+ transports: Some(HashSet::from([
+ "internal".to_string(),
+ "hybrid".to_string(),
+ ])),
+ algorithms: Some(vec![PublicKeyCredentialParameters {
+ alg: -7,
+ typ: "public-key".to_string(),
+ }]),
+ };
+ let result = authenticator_info.as_ctap_bytes();
+ assert!(result.is_ok(), "CBOR generation should succeed");
+
+ let cbor_bytes = result.unwrap();
+ assert!(!cbor_bytes.is_empty(), "CBOR bytes should not be empty");
+
+ // Verify the CBOR can be decoded back
+ let decoded: Result = ciborium::de::from_reader(&cbor_bytes[..]);
+ assert!(decoded.is_ok(), "Generated CBOR should be valid");
+
+ // Verify it's a map with expected keys
+ if let Value::Map(map) = decoded.unwrap() {
+ assert!(
+ map.iter().any(|(k, _)| k == &Value::Integer(1.into())),
+ "Should contain versions (key 1)"
+ );
+ assert!(
+ map.iter().any(|(k, _)| k == &Value::Integer(2.into())),
+ "Should contain extensions (key 2)"
+ );
+ assert!(
+ map.iter().any(|(k, _)| k == &Value::Integer(3.into())),
+ "Should contain aaguid (key 3)"
+ );
+ assert!(
+ map.iter().any(|(k, _)| k == &Value::Integer(4.into())),
+ "Should contain options (key 4)"
+ );
+ assert!(
+ map.iter().any(|(k, _)| k == &Value::Integer(9.into())),
+ "Should contain transports (key 9)"
+ );
+ assert!(
+ map.iter().any(|(k, _)| k == &Value::Integer(10.into())),
+ "Should contain algorithms (key 10)"
+ );
+ } else {
+ panic!("CBOR should decode to a map");
+ }
+
+ // Print the generated CBOR for verification
+ println!("Generated CBOR hex: {}", hex::encode(&cbor_bytes));
+ }
+
+ #[test]
+ fn test_aaguid_parsing() {
+ let result = Uuid::try_from(AAGUID);
+ assert!(result.is_ok(), "AAGUID parsing should succeed");
+
+ let aaguid_bytes = result.unwrap();
+ assert_eq!(aaguid_bytes.0.len(), 16, "AAGUID should be 16 bytes");
+ assert_eq!(aaguid_bytes.0[0], 0xd5, "First byte should be 0xd5");
+ assert_eq!(aaguid_bytes.0[1], 0x48, "Second byte should be 0x48");
+
+ // Verify full expected AAGUID
+ let expected_hex = "d548826e79b4db40a3d811116f7e8349";
+ let expected_bytes = hex::decode(expected_hex).unwrap();
+ assert_eq!(
+ &aaguid_bytes.0[..],
+ expected_bytes,
+ "AAGUID should match expected value"
+ );
+ }
+}
diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/util.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/util.rs
new file mode 100644
index 00000000000..6f5807aecc4
--- /dev/null
+++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/util.rs
@@ -0,0 +1,40 @@
+use windows::{
+ core::s,
+ Win32::{
+ Foundation::{FreeLibrary, HMODULE},
+ System::LibraryLoader::{LoadLibraryExA, LOAD_LIBRARY_SEARCH_SYSTEM32},
+ },
+};
+
+use crate::win_webauthn::{ErrorKind, WinWebAuthnError};
+
+pub(super) fn load_webauthn_lib() -> Result {
+ unsafe {
+ LoadLibraryExA(s!("webauthn.dll"), None, LOAD_LIBRARY_SEARCH_SYSTEM32).map_err(|err| {
+ WinWebAuthnError::with_cause(ErrorKind::DllLoad, "Failed to load webauthn.dll", err)
+ })
+ }
+}
+
+pub(super) fn free_webauthn_lib(library: HMODULE) -> Result<(), WinWebAuthnError> {
+ unsafe {
+ FreeLibrary(library).map_err(|err| {
+ WinWebAuthnError::with_cause(
+ ErrorKind::WindowsInternal,
+ "Failed to free webauthn.dll library",
+ err,
+ )
+ })
+ }
+}
+
+pub(super) trait WindowsString {
+ fn to_utf16(&self) -> Vec;
+}
+
+impl WindowsString for str {
+ fn to_utf16(&self) -> Vec {
+ // null-terminated UTF-16
+ self.encode_utf16().chain(std::iter::once(0)).collect()
+ }
+}
diff --git a/clients.code-workspace b/clients.code-workspace
index f7d86d2a242..1cbae0be20d 100644
--- a/clients.code-workspace
+++ b/clients.code-workspace
@@ -66,6 +66,7 @@
"typescript.preferences.importModuleSpecifier": "project-relative",
"javascript.preferences.importModuleSpecifier": "project-relative",
"typescript.tsdk": "root/node_modules/typescript/lib",
+ "rust-analyzer.cargo.target": "aarch64-pc-windows-msvc",
},
"extensions": {
"recommendations": [