1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-13 15:03:26 +00:00

Extract registration functions to separate module

This commit is contained in:
Isaiah Inuwa
2025-11-10 11:14:15 -06:00
parent e38c057c12
commit 6401fae672
8 changed files with 825 additions and 346 deletions

View File

@@ -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
}

View File

@@ -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##"<svg version="1.1" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg"><path fill="#175ddc" d="M300 253.125C300 279.023 279.023 300 253.125 300H46.875C20.9766 300 0 279.023 0 253.125V46.875C0 20.9766 20.9766 0 46.875 0H253.125C279.023 0 300 20.9766 300 46.875V253.125Z"/><path fill="#fff" d="M243.105 37.6758C241.201 35.7715 238.945 34.834 236.367 34.834H63.6328C61.0254 34.834 58.7988 35.7715 56.8945 37.6758C54.9902 39.5801 54.0527 41.8359 54.0527 44.4141V159.58C54.0527 168.164 55.7227 176.689 59.0625 185.156C62.4023 193.594 66.5625 201.094 71.5137 207.656C76.4648 214.189 82.3535 220.576 89.209 226.787C96.0645 232.998 102.393 238.125 108.164 242.227C113.965 246.328 120 250.195 126.299 253.857C132.598 257.52 137.08 259.98 139.717 261.27C142.354 262.559 144.492 263.584 146.074 264.258C147.275 264.844 148.564 265.166 149.971 265.166C151.377 265.166 152.666 264.873 153.867 264.258C155.479 263.555 157.588 262.559 160.254 261.27C162.891 259.98 167.373 257.49 173.672 253.857C179.971 250.195 186.006 246.328 191.807 242.227C197.607 238.125 203.936 232.969 210.791 226.787C217.646 220.576 223.535 214.219 228.486 207.656C233.438 201.094 237.568 193.623 240.938 185.156C244.277 176.719 245.947 168.193 245.947 159.58V44.4434C245.977 41.8359 245.01 39.5801 243.105 37.6758ZM220.84 160.664C220.84 202.354 150 238.271 150 238.271V59.502H220.84C220.84 59.502 220.84 118.975 220.84 160.664Z"/></svg>"##;
/// Parses a UUID string (with hyphens) into bytes
fn parse_uuid_to_bytes(uuid_str: &str) -> Result<Vec<u8>, String> {
let uuid_clean = uuid_str.replace("-", "");
if uuid_clean.len() != 32 {
return Err("Invalid UUID format".to_string());
}
uuid_clean
.chars()
.collect::<Vec<char>>()
.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<GUID, String> {
// 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<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> {
// 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> =
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::<pluginauthenticator::IPluginAuthenticator>(),
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<u16> = 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::<WebAuthNPluginAddAuthenticatorFnDeclaration>(
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::<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 {
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<Value, _> = 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");
}
}

View File

@@ -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##"<svg version="1.1" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg"><path fill="#175ddc" d="M300 253.125C300 279.023 279.023 300 253.125 300H46.875C20.9766 300 0 279.023 0 253.125V46.875C0 20.9766 20.9766 0 46.875 0H253.125C279.023 0 300 20.9766 300 46.875V253.125Z"/><path fill="#fff" d="M243.105 37.6758C241.201 35.7715 238.945 34.834 236.367 34.834H63.6328C61.0254 34.834 58.7988 35.7715 56.8945 37.6758C54.9902 39.5801 54.0527 41.8359 54.0527 44.4141V159.58C54.0527 168.164 55.7227 176.689 59.0625 185.156C62.4023 193.594 66.5625 201.094 71.5137 207.656C76.4648 214.189 82.3535 220.576 89.209 226.787C96.0645 232.998 102.393 238.125 108.164 242.227C113.965 246.328 120 250.195 126.299 253.857C132.598 257.52 137.08 259.98 139.717 261.27C142.354 262.559 144.492 263.584 146.074 264.258C147.275 264.844 148.564 265.166 149.971 265.166C151.377 265.166 152.666 264.873 153.867 264.258C155.479 263.555 157.588 262.559 160.254 261.27C162.891 259.98 167.373 257.49 173.672 253.857C179.971 250.195 186.006 246.328 191.807 242.227C197.607 238.125 203.936 232.969 210.791 226.787C217.646 220.576 223.535 214.219 228.486 207.656C233.438 201.094 237.568 193.623 240.938 185.156C244.277 176.719 245.947 168.193 245.947 159.58V44.4434C245.977 41.8359 245.01 39.5801 243.105 37.6758ZM220.84 160.664C220.84 202.354 150 238.271 150 238.271V59.502H220.84C220.84 59.502 220.84 118.975 220.84 160.664Z"/></svg>"##;
/// 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(())
}

View File

@@ -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<IUnknown>,
_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> =
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(())
}

View File

@@ -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<Self, Self::Error> {
// 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<PluginAddAuthenticatorResponse, WinWebAuthnError> {
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<Vec<Vec<u16>>> = options
.supported_rp_ids
.map(|ids| ids.iter().map(|id| id.to_utf16()).collect());
let supported_rp_id_ptrs: Option<Vec<*const u16>> = 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<String>,
cause: Option<Box<dyn std::error::Error>>,
}
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<E: std::error::Error + 'static>(
kind: ErrorKind,
description: &str,
cause: E,
) -> Self {
let cause: Box<dyn std::error::Error> = 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");
}
}

View File

@@ -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<String>,
/// Plugin Authenticator Logo for the Light themes.
///
/// String should contain a valid SVG 1.1 document.
pub light_theme_logo_svg: Option<String>,
// 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<String>,
/// 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<Vec<String>>,
}
impl PluginAddAuthenticatorOptions {
pub fn light_theme_logo_b64(&self) -> Option<Vec<u16>> {
self.light_theme_logo_svg
.as_ref()
.map(|svg| Self::encode_svg(&svg))
}
pub fn dark_theme_logo_b64(&self) -> Option<Vec<u16>> {
self.dark_theme_logo_svg
.as_ref()
.map(|svg| Self::encode_svg(&svg))
}
fn encode_svg(svg: &str) -> Vec<u16> {
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<WebAuthnPluginAddAuthenticatorResponse>,
}
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<NonNull<WebAuthnPluginAddAuthenticatorResponse>> for PluginAddAuthenticatorResponse {
fn from(value: NonNull<WebAuthnPluginAddAuthenticatorResponse>) -> 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<CtapVersion>,
/// 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<HashSet<String>>,
/// 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<HashSet<String>>,
/// 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<Vec<PublicKeyCredentialParameters>>,
}
impl AuthenticatorInfo {
pub fn as_ctap_bytes(&self) -> Result<Vec<u8>, 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<Value> = 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<Self, Self::Error> {
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::<Vec<char>>()
.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::<Result<Vec<u8>, 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<Value, _> = 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"
);
}
}

View File

@@ -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<HMODULE, WinWebAuthnError> {
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<u16>;
}
impl WindowsString for str {
fn to_utf16(&self) -> Vec<u16> {
// null-terminated UTF-16
self.encode_utf16().chain(std::iter::once(0)).collect()
}
}

View File

@@ -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": [