From f8e8d5fd3f1399525728a74c87f92e42451b3450 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 19 Nov 2025 23:57:36 -0600 Subject: [PATCH] Add method to construct credential using COM allocator. --- .../src/win_webauthn/com.rs | 80 +++++++++++++++++++ .../src/win_webauthn/types.rs | 28 +++++++ .../src/win_webauthn/util.rs | 15 +++- 3 files changed, 122 insertions(+), 1 deletion(-) 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 index 82bfe09ccaa..25e7632c839 100644 --- 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 @@ -289,3 +289,83 @@ pub(super) fn uninitialize() -> std::result::Result<(), WinWebAuthnError> { unsafe { CoUninitialize() }; Ok(()) } + +#[repr(transparent)] +pub(super) struct ComBuffer(NonNull>); + +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::()).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(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(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::(); + 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::().write(object) }; + buffer.into_ptr() + } + + pub fn from_buffer>(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::(), 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) + } +} 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 index 42a69671585..97f3398ae6c 100644 --- 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 @@ -645,6 +645,34 @@ pub struct CredentialEx { inner: NonNull, } +impl CredentialEx { + fn new_for_com( + version: u32, + id: &CredentialId, + credential_type: &str, + transports: &[CtapTransport], + ) -> Self { + let (pwszCredentialType, _) = credential_type.to_com_utf16(); + let (pbId, cbId) = ComBuffer::from_buffer(&id); + let ptr = unsafe { + let mut uninit: MaybeUninit = MaybeUninit::uninit(); + let ptr = uninit.as_mut_ptr(); + std::ptr::write( + ptr, + WEBAUTHN_CREDENTIAL_EX { + dwVersion: version, + cbId, + pbId, + pwszCredentialType, + dwTransports: transports.iter().map(|t| t.clone() as u32).sum(), + }, + ); + NonNull::new_unchecked(ptr) + }; + Self { inner: ptr } + } +} + impl AsRef for CredentialEx { fn as_ref(&self) -> &WEBAUTHN_CREDENTIAL_EX { // SAFETY: We initialize memory manually in constructors. 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 index 6f5807aecc4..cbd3d53d329 100644 --- 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 @@ -6,7 +6,7 @@ use windows::{ }, }; -use crate::win_webauthn::{ErrorKind, WinWebAuthnError}; +use crate::win_webauthn::{com::ComBuffer, ErrorKind, WinWebAuthnError}; pub(super) fn load_webauthn_lib() -> Result { unsafe { @@ -30,6 +30,9 @@ pub(super) fn free_webauthn_lib(library: HMODULE) -> Result<(), WinWebAuthnError pub(super) trait WindowsString { fn to_utf16(&self) -> Vec; + + // Copies a string to a buffer from the OLE allocator + fn to_com_utf16(&self) -> (*mut u16, u32); } impl WindowsString for str { @@ -37,4 +40,14 @@ impl WindowsString for str { // null-terminated UTF-16 self.encode_utf16().chain(std::iter::once(0)).collect() } + + fn to_com_utf16(&self) -> (*mut u16, u32) { + let wide_bytes: Vec = self + .to_utf16() + .into_iter() + .flat_map(|x| x.to_le_bytes()) + .collect(); + let (ptr, byte_count) = ComBuffer::from_buffer(&wide_bytes); + (ptr as *mut u16, byte_count) + } }