mirror of
https://github.com/bitwarden/browser
synced 2026-02-22 04:14:04 +00:00
375 lines
15 KiB
Rust
375 lines
15 KiB
Rust
pub(crate) mod com;
|
|
pub(crate) mod crypto;
|
|
pub(crate) mod types;
|
|
|
|
use std::{error::Error, mem::MaybeUninit, ptr::NonNull};
|
|
|
|
use types::*;
|
|
pub use types::{
|
|
PluginAddAuthenticatorOptions, PluginAddAuthenticatorResponse, PluginCancelOperationRequest,
|
|
PluginCredentialDetails, PluginGetAssertionRequest, PluginLockStatus,
|
|
PluginMakeCredentialRequest, PluginMakeCredentialResponse, PluginUserVerificationRequest,
|
|
PluginUserVerificationResponse,
|
|
};
|
|
use windows::{
|
|
core::GUID,
|
|
Win32::Foundation::{NTE_USER_CANCELLED, S_OK},
|
|
};
|
|
|
|
use super::{ErrorKind, WinWebAuthnError};
|
|
use crate::{
|
|
plugin::{
|
|
com::{ComBuffer, ComBufferExt},
|
|
crypto::VerifyingKey,
|
|
},
|
|
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.
|
|
///
|
|
/// The handler should be an instance of your type that implements PluginAuthenticator.
|
|
/// The same instance will be shared across all COM calls.
|
|
///
|
|
/// This only needs to be called at the start of your application.
|
|
pub fn register_server<T>(&self, handler: T) -> Result<(), WinWebAuthnError>
|
|
where
|
|
T: PluginAuthenticator + Send + Sync + 'static,
|
|
{
|
|
com::register_server(&self.clsid.0, handler)
|
|
}
|
|
|
|
/// Uninitializes the COM library for the calling thread.
|
|
pub fn shutdown_server() -> Result<(), WinWebAuthnError> {
|
|
com::shutdown_server()
|
|
}
|
|
|
|
/// 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> {
|
|
#![allow(non_snake_case)]
|
|
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 the OS.
|
|
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,
|
|
};
|
|
unsafe {
|
|
// SAFETY: We are holding references to all the input data beyond the OS call, so it is
|
|
// valid during the call.
|
|
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) {
|
|
// SAFETY: The pointer was allocated by a successful call to
|
|
// webauthn_plugin_add_authenticator, so we trust that it's valid.
|
|
Ok(PluginAddAuthenticatorResponse::try_from_ptr(response))
|
|
} else {
|
|
Err(WinWebAuthnError::new(
|
|
ErrorKind::WindowsInternal,
|
|
"WebAuthNPluginAddAuthenticatorResponse returned null",
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Perform user verification related to an associated MakeCredential or GetAssertion request.
|
|
pub fn perform_user_verification(
|
|
&self,
|
|
request: PluginUserVerificationRequest,
|
|
operation_request: &[u8],
|
|
) -> Result<PluginUserVerificationResponse, WinWebAuthnError> {
|
|
tracing::debug!(?request, "Handling user verification request");
|
|
|
|
// Get pub key
|
|
let pub_key = crypto::get_user_verification_public_key(&self.clsid.0)?;
|
|
|
|
// Send UV request
|
|
let user_name = request.user_name.to_utf16().to_com_buffer();
|
|
let hint = request.display_hint.map(|d| d.to_utf16().to_com_buffer());
|
|
let uv_request = WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST {
|
|
hwnd: request.window_handle,
|
|
rguidTransactionId: &request.transaction_id,
|
|
pwszUsername: user_name.leak(),
|
|
pwszDisplayHint: hint.map_or(std::ptr::null(), |buf| buf.leak()),
|
|
};
|
|
let mut response_len = 0;
|
|
let mut response_ptr = MaybeUninit::uninit();
|
|
unsafe {
|
|
let hresult = webauthn_plugin_perform_user_verification(
|
|
&uv_request,
|
|
&mut response_len,
|
|
response_ptr.as_mut_ptr(),
|
|
)?;
|
|
match hresult {
|
|
S_OK => {
|
|
// SAFETY: Windows returned successful response code and length, so we
|
|
// assume that the data and length are initialized
|
|
let response_ptr = response_ptr.assume_init();
|
|
// SAFETY: Windows only runs on platforms where usize >= u32;
|
|
let len = response_len as usize;
|
|
let signature = std::slice::from_raw_parts(response_ptr, len).to_vec();
|
|
pub_key.verify_signature(operation_request, &signature)?;
|
|
webauthn_plugin_free_user_verification_response(response_ptr)?;
|
|
Ok(PluginUserVerificationResponse {
|
|
transaction_id: request.transaction_id,
|
|
signature,
|
|
})
|
|
}
|
|
NTE_USER_CANCELLED => Err(WinWebAuthnError::new(
|
|
ErrorKind::Other,
|
|
"User cancelled user verification",
|
|
)),
|
|
_ => Err(WinWebAuthnError::with_cause(
|
|
ErrorKind::WindowsInternal,
|
|
"Unknown error occurred while performing user verification",
|
|
windows::core::Error::from_hresult(hresult),
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Synchronize credentials to Windows Hello cache.
|
|
///
|
|
/// Number of credentials to sync must be less than [u32::MAX].
|
|
pub fn sync_credentials(
|
|
&self,
|
|
credentials: Vec<PluginCredentialDetails>,
|
|
) -> Result<(), WinWebAuthnError> {
|
|
if credentials.is_empty() {
|
|
tracing::debug!("[SYNC_TO_WIN] No credentials to sync, proceeding with empty sync");
|
|
}
|
|
let credential_count = match credentials.len().try_into() {
|
|
Ok(c) => c,
|
|
Err(err) => {
|
|
return Err(WinWebAuthnError::with_cause(
|
|
ErrorKind::InvalidArguments,
|
|
"Too many credentials passed to sync",
|
|
err,
|
|
));
|
|
}
|
|
};
|
|
|
|
// First try to remove all existing credentials for this plugin
|
|
tracing::debug!("Attempting to remove all existing credentials before sync...");
|
|
// SAFETY: API definition matches actual DLL.
|
|
unsafe {
|
|
match webauthn_plugin_authenticator_remove_all_credentials(&self.clsid.0)?.ok() {
|
|
Ok(()) => {
|
|
tracing::debug!("Successfully removed existing credentials");
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!("Failed to remove existing credentials: {}", e);
|
|
// Continue anyway, as this might be the first sync or an older Windows version
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the new credentials (only if we have any)
|
|
if credentials.is_empty() {
|
|
tracing::debug!("No credentials to add to Windows - sync completed successfully");
|
|
Ok(())
|
|
} else {
|
|
tracing::debug!("Adding new credentials to Windows...");
|
|
|
|
// Convert Bitwarden credentials to Windows credential details
|
|
// All buffers must be allocated with the COM task allocator to be passed over COM.
|
|
// The receiver is responsible for freeing the COM memory, which is why we leak all the
|
|
// buffers here.
|
|
let mut win_credentials = Vec::new();
|
|
for (i, cred) in credentials.iter().enumerate() {
|
|
tracing::debug!("[SYNC_TO_WIN] Converting credential {}: RP ID: {}, User: {}, Credential ID: {:?} ({} bytes), User ID: {:?} ({} bytes)",
|
|
i + 1, cred.rp_id, cred.user_name, &cred.credential_id, cred.credential_id.len(), &cred.user_id, cred.user_id.len());
|
|
|
|
// Allocate credential_id bytes with COM
|
|
let credential_id_buf = cred.credential_id.as_ref().to_com_buffer();
|
|
|
|
// Allocate user_id bytes with COM
|
|
let user_id_buf = cred.user_id.as_ref().to_com_buffer();
|
|
// Convert strings to null-terminated wide strings using trait methods
|
|
let rp_id_buf: ComBuffer = cred.rp_id.to_utf16().to_com_buffer();
|
|
let rp_friendly_name_buf: Option<ComBuffer> = cred
|
|
.rp_friendly_name
|
|
.as_ref()
|
|
.map(|display_name| display_name.to_utf16().to_com_buffer());
|
|
let user_name_buf: ComBuffer = (cred.user_name.to_utf16()).to_com_buffer();
|
|
let user_display_name_buf: ComBuffer =
|
|
cred.user_display_name.to_utf16().to_com_buffer();
|
|
let win_cred = WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS {
|
|
credential_id_byte_count: u32::from(cred.credential_id.len()),
|
|
credential_id_pointer: credential_id_buf.leak(),
|
|
rpid: rp_id_buf.leak(),
|
|
rp_friendly_name: rp_friendly_name_buf
|
|
.map_or(std::ptr::null(), |buf| buf.leak()),
|
|
user_id_byte_count: u32::from(cred.user_id.len()),
|
|
user_id_pointer: user_id_buf.leak(),
|
|
user_name: user_name_buf.leak(),
|
|
user_display_name: user_display_name_buf.leak(),
|
|
};
|
|
win_credentials.push(win_cred);
|
|
tracing::debug!(
|
|
"[SYNC_TO_WIN] Converted credential {} to Windows format",
|
|
i + 1
|
|
);
|
|
}
|
|
|
|
// SAFETY: The pointer to win_credentials lives longer than the call to
|
|
// webauthn_plugin_authenticator_add_credentials(). The nested
|
|
// buffers are allocated with COM, which the OS is responsible for
|
|
// cleaning up.
|
|
let result = unsafe {
|
|
webauthn_plugin_authenticator_add_credentials(
|
|
&self.clsid.0,
|
|
credential_count,
|
|
win_credentials.as_ptr(),
|
|
)
|
|
};
|
|
match result {
|
|
Ok(hresult) => {
|
|
if let Err(err) = hresult.ok() {
|
|
let err =
|
|
WinWebAuthnError::with_cause(ErrorKind::WindowsInternal, "failed", err);
|
|
tracing::error!(
|
|
"Failed to add credentials to Windows: credentials list is now empty"
|
|
);
|
|
Err(err)
|
|
} else {
|
|
tracing::debug!("Successfully synced credentials to Windows");
|
|
Ok(())
|
|
}
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to add credentials to Windows: {}", e);
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Retrieve the public key used to sign operation requests.
|
|
pub fn operation_signing_public_key(&self) -> Result<VerifyingKey, WinWebAuthnError> {
|
|
crypto::get_operation_signing_public_key(&self.clsid.0)
|
|
}
|
|
|
|
/// Retrieve the public key used to sign user verification responses.
|
|
pub fn user_verification_public_key(&self) -> Result<VerifyingKey, WinWebAuthnError> {
|
|
crypto::get_user_verification_public_key(&self.clsid.0)
|
|
}
|
|
}
|
|
pub trait PluginAuthenticator {
|
|
/// Process a request to create a new credential.
|
|
///
|
|
/// Returns a [CTAP authenticatorMakeCredential response structure](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#authenticatormakecredential-response-structure).
|
|
fn make_credential(
|
|
&self,
|
|
request: PluginMakeCredentialRequest,
|
|
) -> Result<Vec<u8>, Box<dyn Error>>;
|
|
|
|
/// Process a request to assert a credential.
|
|
///
|
|
/// Returns a [CTAP authenticatorGetAssertion response structure](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#authenticatorgetassertion-response-structure).
|
|
fn get_assertion(&self, request: PluginGetAssertionRequest) -> Result<Vec<u8>, Box<dyn Error>>;
|
|
|
|
/// Cancel an ongoing operation.
|
|
fn cancel_operation(&self, request: PluginCancelOperationRequest)
|
|
-> Result<(), Box<dyn Error>>;
|
|
|
|
/// Retrieve lock status.
|
|
fn lock_status(&self) -> Result<PluginLockStatus, Box<dyn Error>>;
|
|
}
|
|
|
|
#[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");
|
|
}
|
|
}
|