mirror of
https://github.com/bitwarden/browser
synced 2026-01-31 16:53:27 +00:00
Provide WebAuthnPlugin::perform_user_verification wrapper
This commit is contained in:
1
apps/desktop/desktop_native/Cargo.lock
generated
1
apps/desktop/desktop_native/Cargo.lock
generated
@@ -1006,6 +1006,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"typenum",
|
||||
"widestring",
|
||||
"win_webauthn",
|
||||
"windows 0.61.3",
|
||||
"windows-future",
|
||||
"zbus",
|
||||
|
||||
@@ -77,6 +77,7 @@ windows = { workspace = true, features = [
|
||||
"Win32_System_Pipes",
|
||||
], optional = true }
|
||||
windows-future = { workspace = true }
|
||||
win_webauthn = { path = "../win_webauthn" }
|
||||
|
||||
[target.'cfg(windows)'.dev-dependencies]
|
||||
keytar = { workspace = true }
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
use std::alloc;
|
||||
use std::mem::{align_of, MaybeUninit};
|
||||
use std::ptr::NonNull;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use base64::engine::{general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||
use windows::core::s;
|
||||
use windows::Win32::Foundation::{FreeLibrary, HWND};
|
||||
use windows::{
|
||||
core::{GUID, HRESULT, PCSTR},
|
||||
Win32::System::{Com::CoTaskMemAlloc, LibraryLoader::*},
|
||||
};
|
||||
use win_webauthn::{CredentialId, UserId, plugin::{Clsid, PluginCredentialDetails, PluginUserVerificationRequest, WebAuthnPlugin}};
|
||||
use windows::{Win32::Foundation::HWND, core::GUID};
|
||||
|
||||
use crate::autofill::{
|
||||
CommandResponse, RunCommand, RunCommandRequest, StatusResponse, StatusState, StatusSupport,
|
||||
@@ -71,38 +63,30 @@ fn handle_user_verification_request(
|
||||
request: UserVerificationParameters,
|
||||
) -> Result<UserVerificationResponse> {
|
||||
tracing::debug!(?request, "Handling user verification request");
|
||||
unsafe {
|
||||
let hwnd: HWND = *request.window_handle.as_ptr().cast();
|
||||
let (buf, _) = request.transaction_context[..16].split_at(16);
|
||||
let guid_u128 = buf
|
||||
.try_into()
|
||||
.map_err(|e| anyhow!("Failed to parse transaction ID as u128: {e}"))?;
|
||||
let transaction_id = GUID::from_u128(u128::from_le_bytes(guid_u128));
|
||||
let hwnd: HWND = unsafe {
|
||||
// SAFETY: We check to make sure that the vec is the expected size
|
||||
// before converting it. If the handle is invalid when passed to
|
||||
// Windows, the request will be rejected.
|
||||
if request.window_handle.len() == size_of::<HWND>() {
|
||||
*request.window_handle.as_ptr().cast()
|
||||
} else {
|
||||
return Err(anyhow!("Invalid window handle received: {:?}", request.window_handle));
|
||||
}
|
||||
};
|
||||
|
||||
let (buf, _) = request.transaction_context[..16].split_at(16);
|
||||
let guid_u128 = buf
|
||||
.try_into()
|
||||
.map_err(|e| anyhow!("Failed to parse transaction ID as u128: {e}"))?;
|
||||
let transaction_id = GUID::from_u128(u128::from_le_bytes(guid_u128));
|
||||
|
||||
let uv_request = WebAuthNPluginUserVerificationRequest {
|
||||
hwnd,
|
||||
rguidTransactionId: (&transaction_id) as *const GUID,
|
||||
pwszUsername: request.username.to_com_utf16().0,
|
||||
pwszDisplayHint: request.display_hint.to_com_utf16().0,
|
||||
};
|
||||
let uv_fn = delay_load::<WebAuthNPluginPerformUserVerification>(
|
||||
s!("webauthn.dll"),
|
||||
s!("WebAuthNPluginPerformUserVerification"),
|
||||
)
|
||||
.ok_or(anyhow!(
|
||||
"Could not load WebAuthNPluginPerformUserVerification"
|
||||
))?;
|
||||
let mut uv_response_len: u32 = 0;
|
||||
let mut uv_response: *mut u8 = std::ptr::null_mut();
|
||||
uv_fn(
|
||||
std::ptr::from_ref(&uv_request),
|
||||
&mut uv_response_len as *mut u32,
|
||||
&mut uv_response as *mut *mut u8,
|
||||
)
|
||||
.ok()
|
||||
let uv_request = PluginUserVerificationRequest {
|
||||
window_handle: hwnd,
|
||||
transaction_id: transaction_id,
|
||||
user_name: request.username,
|
||||
display_hint: Some(request.display_hint),
|
||||
};
|
||||
let _response = WebAuthnPlugin::perform_user_verification(uv_request)
|
||||
.map_err(|err| anyhow!("User Verification request failed: {err}"))?;
|
||||
}
|
||||
return Ok(UserVerificationResponse {});
|
||||
}
|
||||
|
||||
@@ -145,70 +129,48 @@ fn sync_credentials_to_windows(
|
||||
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() {
|
||||
tracing::debug!("[SYNC_TO_WIN] No credentials to sync, proceeding with empty sync");
|
||||
}
|
||||
let clsid = Clsid::try_from(plugin_clsid)
|
||||
.map_err(|err| format!("Failed to parse CLSID from string {plugin_clsid}: {err}"))?;
|
||||
let plugin = WebAuthnPlugin::new(clsid);
|
||||
|
||||
// Convert Bitwarden credentials to Windows credential details
|
||||
let mut win_credentials = Vec::new();
|
||||
|
||||
for (i, cred) in credentials.iter().enumerate() {
|
||||
let win_credentials = credentials.into_iter().enumerate().filter_map(|(i, cred)| {
|
||||
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_handle, cred.user_handle.len());
|
||||
|
||||
let cred_id = match CredentialId::try_from(cred.credential_id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
tracing::warn!("Skipping sync of credential {} because of an invalid credential ID: {err}", i + 1);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let user_id = match UserId::try_from(cred.user_handle) {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
tracing::warn!("Skipping sync of credential {} because of an invalid user ID: {err}", i + 1);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
cred.user_handle.clone(), // Pass raw bytes
|
||||
cred.user_name.clone(),
|
||||
cred.user_name.clone(), // Use user name as display name for now
|
||||
);
|
||||
|
||||
win_credentials.push(win_cred);
|
||||
let cred_details = PluginCredentialDetails {
|
||||
credential_id: cred_id,
|
||||
rp_id: cred.rp_id.clone(),
|
||||
rp_friendly_name: Some(cred.rp_id.clone()), // Use RP ID as friendly name for now
|
||||
user_id: user_id,
|
||||
user_name: cred.user_name.clone(),
|
||||
user_display_name: cred.user_name.clone(), // Use user name as display name for now
|
||||
};
|
||||
tracing::debug!(
|
||||
"[SYNC_TO_WIN] Converted credential {} to Windows format",
|
||||
i + 1
|
||||
);
|
||||
}
|
||||
Some(cred_details)
|
||||
}).collect();
|
||||
|
||||
// First try to remove all existing credentials for this plugin
|
||||
tracing::debug!("Attempting to remove all existing credentials before sync...");
|
||||
match remove_all_credentials(clsid_guid) {
|
||||
Ok(()) => {
|
||||
tracing::debug!("Successfully removed existing credentials");
|
||||
}
|
||||
Err(e) if e.contains("can't be loaded") => {
|
||||
tracing::debug!("RemoveAllCredentials function not available - this is expected for some Windows versions");
|
||||
// This is fine, the function might not exist in all versions
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::debug!("Warning: Failed to remove existing credentials: {}", e);
|
||||
// Continue anyway, as this might be the first sync or an older Windows version
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new credentials (only if we have any)
|
||||
if credentials.is_empty() {
|
||||
tracing::debug!("No credentials to add to Windows - sync completed successfully");
|
||||
Ok(())
|
||||
} else {
|
||||
tracing::debug!("Adding new credentials to Windows...");
|
||||
match add_credentials(clsid_guid, win_credentials) {
|
||||
Ok(()) => {
|
||||
tracing::debug!("Successfully synced credentials to Windows");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to add credentials to Windows: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin
|
||||
.sync_credentials(win_credentials)
|
||||
.map_err(|err| format!("Failed to synchronize credentials: {err}"))
|
||||
}
|
||||
|
||||
/// Credential data for sync operations
|
||||
@@ -219,308 +181,4 @@ struct SyncedCredential {
|
||||
pub rp_id: String,
|
||||
pub user_name: String,
|
||||
pub user_handle: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Represents a credential.
|
||||
/// Header File Name: _WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS
|
||||
/// Header File Usage: WebAuthNPluginAuthenticatorAddCredentials, etc.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
struct WebAuthnPluginCredentialDetails {
|
||||
pub credential_id_byte_count: u32,
|
||||
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: *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 WebAuthnPluginCredentialDetails {
|
||||
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 {
|
||||
// Allocate credential_id bytes with COM
|
||||
let (credential_id_pointer, credential_id_byte_count) =
|
||||
ComBuffer::from_buffer(&credential_id);
|
||||
|
||||
// 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();
|
||||
let (rp_friendly_name_ptr, _) = rp_friendly_name.to_com_utf16();
|
||||
let (user_name_ptr, _) = user_name.to_com_utf16();
|
||||
let (user_display_name_ptr, _) = user_display_name.to_com_utf16();
|
||||
|
||||
Self {
|
||||
credential_id_byte_count,
|
||||
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_id_pointer as *const u8,
|
||||
user_name: user_name_ptr as *const u16,
|
||||
user_display_name: user_display_name_ptr as *const u16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stable API function signatures - now use REFCLSID and flat arrays
|
||||
type WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration = unsafe extern "cdecl" fn(
|
||||
rclsid: *const GUID, // Changed from string to GUID reference
|
||||
cCredentialDetails: u32,
|
||||
pCredentialDetails: *const WebAuthnPluginCredentialDetails, // Flat array, not list
|
||||
) -> HRESULT;
|
||||
|
||||
/// Trait for converting strings to Windows-compatible wide strings using COM allocation
|
||||
pub trait WindowsString {
|
||||
/// Converts to null-terminated UTF-16 using COM allocation
|
||||
fn to_com_utf16(&self) -> (*mut u16, u32);
|
||||
/// Converts to Vec<u16> for temporary use (caller must keep Vec alive)
|
||||
fn to_utf16(&self) -> Vec<u16>;
|
||||
}
|
||||
|
||||
impl WindowsString for str {
|
||||
fn to_com_utf16(&self) -> (*mut u16, u32) {
|
||||
let mut wide_vec: Vec<u16> = self.encode_utf16().collect();
|
||||
wide_vec.push(0); // null terminator
|
||||
let wide_bytes: Vec<u8> = wide_vec.iter().flat_map(|&x| x.to_le_bytes()).collect();
|
||||
let (ptr, byte_count) = ComBuffer::from_buffer(&wide_bytes);
|
||||
(ptr as *mut u16, byte_count)
|
||||
}
|
||||
|
||||
fn to_utf16(&self) -> Vec<u16> {
|
||||
let mut wide_vec: Vec<u16> = self.encode_utf16().collect();
|
||||
wide_vec.push(0); // null terminator
|
||||
wide_vec
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct ComBuffer(NonNull<MaybeUninit<u8>>);
|
||||
|
||||
impl ComBuffer {
|
||||
/// Returns an COM-allocated buffer of `size`.
|
||||
fn alloc(size: usize, for_slice: bool) -> Self {
|
||||
#[expect(clippy::as_conversions)]
|
||||
{
|
||||
assert!(size <= isize::MAX as usize, "requested bad object size");
|
||||
}
|
||||
|
||||
// SAFETY: Any size is valid to pass to Windows, even `0`.
|
||||
let ptr = NonNull::new(unsafe { CoTaskMemAlloc(size) }).unwrap_or_else(|| {
|
||||
// XXX: This doesn't have to be correct, just close enough for an OK OOM error.
|
||||
let layout = alloc::Layout::from_size_align(size, align_of::<u8>()).unwrap();
|
||||
alloc::handle_alloc_error(layout)
|
||||
});
|
||||
|
||||
if for_slice {
|
||||
// Ininitialize the buffer so it can later be treated as `&mut [u8]`.
|
||||
// SAFETY: The pointer is valid and we are using a valid value for a byte-wise allocation.
|
||||
unsafe { ptr.write_bytes(0, size) };
|
||||
}
|
||||
|
||||
Self(ptr.cast())
|
||||
}
|
||||
|
||||
fn into_ptr<T>(self) -> *mut T {
|
||||
self.0.cast().as_ptr()
|
||||
}
|
||||
|
||||
/// Creates a new COM-allocated structure.
|
||||
///
|
||||
/// Note that `T` must be [Copy] to avoid any possible memory leaks.
|
||||
pub fn with_object<T: Copy>(object: T) -> *mut T {
|
||||
// NB: Vendored from Rust's alloc code since we can't yet allocate `Box` with a custom allocator.
|
||||
const MIN_ALIGN: usize = if cfg!(target_pointer_width = "64") {
|
||||
16
|
||||
} else if cfg!(target_pointer_width = "32") {
|
||||
8
|
||||
} else {
|
||||
panic!("unsupported arch")
|
||||
};
|
||||
|
||||
// SAFETY: Validate that our alignment works for a normal size-based allocation for soundness.
|
||||
let layout = const {
|
||||
let layout = alloc::Layout::new::<T>();
|
||||
assert!(layout.align() <= MIN_ALIGN);
|
||||
layout
|
||||
};
|
||||
|
||||
let buffer = Self::alloc(layout.size(), false);
|
||||
// SAFETY: `ptr` is valid for writes of `T` because we correctly allocated the right sized buffer that
|
||||
// accounts for any alignment requirements.
|
||||
//
|
||||
// Additionally, we ensure the value is treated as moved by forgetting the source.
|
||||
unsafe { buffer.0.cast::<T>().write(object) };
|
||||
buffer.into_ptr()
|
||||
}
|
||||
|
||||
pub fn from_buffer<T: AsRef<[u8]>>(buffer: T) -> (*mut u8, u32) {
|
||||
let buffer = buffer.as_ref();
|
||||
let len = buffer.len();
|
||||
let com_buffer = Self::alloc(len, true);
|
||||
|
||||
// SAFETY: `ptr` points to a valid allocation that `len` matches, and we made sure
|
||||
// the bytes were initialized. Additionally, bytes have no alignment requirements.
|
||||
unsafe {
|
||||
NonNull::slice_from_raw_parts(com_buffer.0.cast::<u8>(), len)
|
||||
.as_mut()
|
||||
.copy_from_slice(buffer)
|
||||
}
|
||||
|
||||
// Safety: The Windows API structures these buffers are placed into use `u32` (`DWORD`) to
|
||||
// represent length.
|
||||
#[expect(clippy::as_conversions)]
|
||||
(com_buffer.into_ptr(), len as u32)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
|
||||
pub fn remove_all_credentials(clsid_guid: GUID) -> std::result::Result<(), String> {
|
||||
tracing::debug!("Loading WebAuthNPluginAuthenticatorRemoveAllCredentials function...");
|
||||
|
||||
let result = unsafe {
|
||||
delay_load::<WebAuthNPluginAuthenticatorRemoveAllCredentialsFnDeclaration>(
|
||||
s!("webauthn.dll"),
|
||||
s!("WebAuthNPluginAuthenticatorRemoveAllCredentials"),
|
||||
)
|
||||
};
|
||||
|
||||
match result {
|
||||
Some(api) => {
|
||||
tracing::debug!("Function loaded successfully, calling API...");
|
||||
|
||||
let result = unsafe { api(&clsid_guid) };
|
||||
|
||||
if result.is_err() {
|
||||
let error_code = result.0;
|
||||
tracing::debug!("API call failed with HRESULT: 0x{:x}", error_code);
|
||||
|
||||
return Err(format!(
|
||||
"Error: Error response from WebAuthNPluginAuthenticatorRemoveAllCredentials()\nHRESULT: 0x{:x}\n{}",
|
||||
error_code, result.message()
|
||||
));
|
||||
}
|
||||
|
||||
tracing::debug!("API call succeeded");
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
tracing::debug!("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."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn delay_load<T>(library: PCSTR, function: PCSTR) -> Option<T> {
|
||||
let library = LoadLibraryExA(library, None, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
|
||||
|
||||
let Ok(library) = library else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let address = GetProcAddress(library, function);
|
||||
|
||||
if address.is_some() {
|
||||
return Some(std::mem::transmute_copy(&address));
|
||||
}
|
||||
|
||||
_ = FreeLibrary(library);
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn add_credentials(
|
||||
clsid_guid: GUID,
|
||||
credentials: Vec<WebAuthnPluginCredentialDetails>,
|
||||
) -> std::result::Result<(), String> {
|
||||
tracing::debug!("Loading WebAuthNPluginAuthenticatorAddCredentials function...");
|
||||
|
||||
let result = unsafe {
|
||||
delay_load::<WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration>(
|
||||
s!("webauthn.dll"),
|
||||
s!("WebAuthNPluginAuthenticatorAddCredentials"),
|
||||
)
|
||||
};
|
||||
|
||||
match result {
|
||||
Some(api) => {
|
||||
tracing::debug!("Function loaded successfully, calling API...");
|
||||
tracing::debug!("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;
|
||||
tracing::debug!("API call failed with HRESULT: 0x{:x}", error_code);
|
||||
return Err(format!(
|
||||
"Error: Error response from WebAuthNPluginAuthenticatorAddCredentials()\nHRESULT: 0x{:x}\n{}",
|
||||
error_code, result.message()
|
||||
));
|
||||
}
|
||||
|
||||
tracing::debug!("API call succeeded");
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
tracing::debug!("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."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type WebAuthNPluginAuthenticatorRemoveAllCredentialsFnDeclaration =
|
||||
unsafe extern "cdecl" fn(rclsid: *const GUID) -> HRESULT;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
struct WebAuthNPluginUserVerificationRequest {
|
||||
/// Windows handle of the top-level window displayed by the plugin and currently is in foreground as part of the ongoing webauthn operation.
|
||||
hwnd: HWND,
|
||||
|
||||
/// The webauthn transaction id from the WEBAUTHN_PLUGIN_OPERATION_REQUEST
|
||||
rguidTransactionId: *const GUID,
|
||||
|
||||
/// The username attached to the credential that is in use for this webauthn operation
|
||||
pwszUsername: *const u16,
|
||||
|
||||
/// A text hint displayed on the windows hello prompt
|
||||
pwszDisplayHint: *const u16,
|
||||
}
|
||||
|
||||
type WebAuthNPluginPerformUserVerification = unsafe extern "cdecl" fn(
|
||||
pPluginUserVerification: *const WebAuthNPluginUserVerificationRequest,
|
||||
pcbResponse: *mut u32,
|
||||
ppbResponse: *mut *mut u8,
|
||||
) -> HRESULT;
|
||||
|
||||
type WebAuthNPluginFreeUserVerificationResponse = unsafe extern "cdecl" fn(ppbResponse: *mut u8);
|
||||
}
|
||||
@@ -43,6 +43,8 @@ impl WinWebAuthnError {
|
||||
enum ErrorKind {
|
||||
DllLoad,
|
||||
Serialization,
|
||||
InvalidArguments,
|
||||
Other,
|
||||
WindowsInternal,
|
||||
}
|
||||
|
||||
@@ -51,6 +53,8 @@ impl Display for WinWebAuthnError {
|
||||
let msg = match self.kind {
|
||||
ErrorKind::Serialization => "Failed to serialize data",
|
||||
ErrorKind::DllLoad => "Failed to load function from DLL",
|
||||
ErrorKind::InvalidArguments => "Invalid arguments passed to function",
|
||||
ErrorKind::Other => "An error occurred",
|
||||
ErrorKind::WindowsInternal => "A Windows error occurred",
|
||||
};
|
||||
f.write_str(msg)?;
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
pub(crate) mod com;
|
||||
pub(crate) mod types;
|
||||
|
||||
use std::{error::Error, ptr::NonNull};
|
||||
use std::{error::Error, mem::MaybeUninit, ptr::NonNull};
|
||||
use types::*;
|
||||
use windows::core::GUID;
|
||||
use windows::{
|
||||
core::GUID,
|
||||
Win32::Foundation::{NTE_USER_CANCELLED, S_OK},
|
||||
};
|
||||
|
||||
pub use types::{
|
||||
PluginAddAuthenticatorOptions, PluginAddAuthenticatorResponse, PluginCancelOperationRequest,
|
||||
PluginGetAssertionRequest, PluginLockStatus, PluginMakeCredentialRequest,
|
||||
PluginMakeCredentialResponse,
|
||||
PluginCredentialDetails, PluginGetAssertionRequest, PluginLockStatus,
|
||||
PluginMakeCredentialRequest, PluginMakeCredentialResponse, PluginUserVerificationRequest,
|
||||
PluginUserVerificationResponse,
|
||||
};
|
||||
|
||||
use super::{ErrorKind, WinWebAuthnError};
|
||||
use crate::util::WindowsString;
|
||||
use crate::{
|
||||
plugin::com::{ComBuffer, ComBufferExt},
|
||||
util::WindowsString,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Clsid(GUID);
|
||||
@@ -137,6 +144,160 @@ impl WebAuthnPlugin {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn perform_user_verification(
|
||||
request: PluginUserVerificationRequest,
|
||||
) -> Result<PluginUserVerificationResponse, WinWebAuthnError> {
|
||||
tracing::debug!(?request, "Handling user verification 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 = std::ptr::null_mut();
|
||||
let hresult = webauthn_plugin_perform_user_verification(
|
||||
&uv_request,
|
||||
&mut response_len,
|
||||
&mut response_ptr,
|
||||
)?;
|
||||
match hresult {
|
||||
S_OK => {
|
||||
let signature = if response_len > 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
// SAFETY: Windows returned successful response code and length, so we assume that the data is initialized
|
||||
unsafe {
|
||||
// SAFETY: Windows only runs on platforms where usize >= u32;
|
||||
let len = response_len as usize;
|
||||
std::slice::from_raw_parts(response_ptr, len).to_vec()
|
||||
}
|
||||
};
|
||||
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...");
|
||||
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
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
match webauthn_plugin_authenticator_add_credentials(
|
||||
&self.clsid.0,
|
||||
credential_count,
|
||||
win_credentials.as_ptr(),
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub trait PluginAuthenticator {
|
||||
/// Process a request to create a new credential.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! authenticator requests.
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_camel_case_types)]
|
||||
|
||||
use std::{mem::MaybeUninit, ptr::NonNull};
|
||||
|
||||
@@ -13,8 +14,9 @@ use windows::{
|
||||
use windows_core::s;
|
||||
|
||||
use crate::{
|
||||
types::UserId,
|
||||
util::{webauthn_call, WindowsString},
|
||||
ErrorKind, WinWebAuthnError,
|
||||
CredentialId, ErrorKind, WinWebAuthnError,
|
||||
};
|
||||
|
||||
use crate::types::{
|
||||
@@ -218,6 +220,115 @@ fn webauthn_plugin_free_add_authenticator_response(
|
||||
pPluginAddAuthenticatorOptions: *mut WebAuthnPluginAddAuthenticatorResponse
|
||||
) -> ());
|
||||
|
||||
// Credential syncing types
|
||||
|
||||
/// Represents a credential.
|
||||
/// Header File Name: _WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS
|
||||
/// Header File Usage: WebAuthNPluginAuthenticatorAddCredentials, etc.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(super) struct WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS {
|
||||
pub credential_id_byte_count: u32,
|
||||
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: *const u8, // Changed to const
|
||||
pub user_name: *const u16, // Changed to const (LPCWSTR)
|
||||
pub user_display_name: *const u16, // Changed to const (LPCWSTR)
|
||||
}
|
||||
|
||||
/// Credential metadata to sync to Windows Hello credential autofill list.
|
||||
#[derive(Debug)]
|
||||
pub struct PluginCredentialDetails {
|
||||
/// Credential ID.
|
||||
pub credential_id: CredentialId,
|
||||
|
||||
/// Relying party ID.
|
||||
pub rp_id: String,
|
||||
|
||||
/// Relying party display name.
|
||||
pub rp_friendly_name: Option<String>,
|
||||
|
||||
/// User handle.
|
||||
pub user_id: UserId,
|
||||
|
||||
/// User name.
|
||||
///
|
||||
/// Corresponds to [`name`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name) field of WebAuthn `PublicKeyCredentialUserEntity`.
|
||||
pub user_name: String,
|
||||
|
||||
/// User name.
|
||||
///
|
||||
/// Corresponds to [`displayName`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-displayname) field of WebAuthn `PublicKeyCredentialUserEntity`.
|
||||
pub user_display_name: String,
|
||||
}
|
||||
|
||||
// Stable API function signatures - now use REFCLSID and flat arrays
|
||||
webauthn_call!("WebAuthNPluginAuthenticatorAddCredentials" as fn webauthn_plugin_authenticator_add_credentials(
|
||||
rclsid: *const GUID,
|
||||
cCredentialDetails: u32,
|
||||
pCredentialDetails: *const WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS
|
||||
) -> HRESULT);
|
||||
|
||||
webauthn_call!("WebAuthNPluginAuthenticatorRemoveAllCredentials" as fn webauthn_plugin_authenticator_remove_all_credentials(
|
||||
rclsid: *const GUID
|
||||
) -> HRESULT);
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
pub(super) struct WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST {
|
||||
/// Windows handle of the top-level window displayed by the plugin and
|
||||
/// currently is in foreground as part of the ongoing WebAuthn operation.
|
||||
pub(super) hwnd: HWND,
|
||||
|
||||
/// The WebAuthn transaction id from the WEBAUTHN_PLUGIN_OPERATION_REQUEST
|
||||
pub(super) rguidTransactionId: *const GUID,
|
||||
|
||||
/// The username attached to the credential that is in use for this WebAuthn
|
||||
/// operation.
|
||||
pub(super) pwszUsername: *const u16,
|
||||
|
||||
/// A text hint displayed on the Windows Hello prompt.
|
||||
pub(super) pwszDisplayHint: *const u16,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PluginUserVerificationRequest {
|
||||
/// Windows handle of the top-level window displayed by the plugin and
|
||||
/// currently is in foreground as part of the ongoing WebAuthn operation.
|
||||
pub window_handle: HWND,
|
||||
|
||||
/// The WebAuthn transaction id from the WEBAUTHN_PLUGIN_OPERATION_REQUEST
|
||||
pub transaction_id: GUID,
|
||||
|
||||
/// The username attached to the credential that is in use for this WebAuthn
|
||||
/// operation.
|
||||
pub user_name: String,
|
||||
|
||||
/// A text hint displayed on the Windows Hello prompt.
|
||||
pub display_hint: Option<String>,
|
||||
}
|
||||
|
||||
/// Response details from user verification.
|
||||
pub struct PluginUserVerificationResponse {
|
||||
pub transaction_id: GUID,
|
||||
/// Bytes of the signature over the response.
|
||||
pub signature: Vec<u8>,
|
||||
}
|
||||
|
||||
webauthn_call!("WebAuthNPluginPerformUserVerification" as fn webauthn_plugin_perform_user_verification(
|
||||
pPluginUserVerification: *const WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST,
|
||||
pcbResponse: *mut u32,
|
||||
ppbResponse: *mut *mut u8
|
||||
) -> HRESULT);
|
||||
|
||||
webauthn_call!("WebAuthNPluginFreeUserVerificationResponse" as fn webauthn_plugin_free_user_verification_response(
|
||||
pbResponse: *mut u8
|
||||
) -> ());
|
||||
|
||||
// Plugin Authenticator types
|
||||
|
||||
/// Used when creating and asserting credentials.
|
||||
/// Header File Name: _WEBAUTHN_PLUGIN_OPERATION_REQUEST
|
||||
/// Header File Usage: MakeCredential()
|
||||
|
||||
Reference in New Issue
Block a user