1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

[PM-9126] COM Object Registration (#13278)

* PM-9126: Initial scaffolding for com object registration

* PM-9126: Clean Up PACOMObject trait and impl

* PM-9126: Add unsafe tests

* PM-9126: Clean up registration PR with a working CoRegisterClassObject call

* PM-9126: Add AddAuthenticator fn call

* PM-9126: Load AddAuthenticator fn call dynamically

* PM-9126: Add AddAuthenticator experiments

* PR-9126: add brackets around guids

* PM-9126: clean up part 1

* PM-9126: Cleanup changes

* Only call the register function if on Windows

* PM-9126: Block two generated types that create issues for the i686-pc-windows-msvc target

* PM-9126: Refine bindings file

* PM-9126: Address PR comments part 1

* PM-9126: Address PR comments part 2

* PM-9126: Return result in napi layer

* PM-9126: Propogate error from add authenticator call

* PM-9126: Change for version update
This commit is contained in:
Colton Hurst
2025-04-03 17:11:04 -04:00
committed by GitHub
parent 64bbdcd638
commit e23a353543
17 changed files with 329 additions and 32 deletions

2
.github/CODEOWNERS vendored
View File

@@ -122,7 +122,7 @@ apps/desktop/src/autofill @bitwarden/team-autofill-dev
libs/common/src/autofill @bitwarden/team-autofill-dev
apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev
apps/desktop/src/app/components/fido2placeholder.component.ts @bitwarden/team-autofill-dev
apps/desktop/desktop_native/windows-plugin-authenticator @bitwarden/team-autofill-dev
apps/desktop/desktop_native/windows_plugin_authenticator @bitwarden/team-autofill-dev
# DuckDuckGo integration
apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev
apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-dev

View File

@@ -996,6 +996,7 @@ dependencies = [
"tokio-stream",
"tokio-util",
"windows-registry",
"windows_plugin_authenticator",
]
[[package]]
@@ -3534,13 +3535,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-plugin-authenticator"
version = "0.0.0"
dependencies = [
"bindgen",
]
[[package]]
name = "windows-registry"
version = "0.4.0"
@@ -3737,6 +3731,16 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_plugin_authenticator"
version = "0.0.0"
dependencies = [
"bindgen",
"hex",
"windows 0.61.1",
"windows-core 0.61.0",
]
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"

View File

@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["napi", "core", "proxy", "macos_provider", "windows-plugin-authenticator"]
members = ["napi", "core", "proxy", "macos_provider", "windows_plugin_authenticator"]
[workspace.package]
version = "0.0.0"
@@ -58,6 +58,7 @@ typenum = "=1.17.0"
uniffi = "=0.28.3"
widestring = "=1.1.0"
windows = "=0.61.1"
windows-core = "=0.61.0"
windows-future = "=0.2.0"
windows-registry = "=0.4.0"
zbus = "=4.4.0"

View File

@@ -28,6 +28,7 @@ tokio-stream = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows-registry = { workspace = true }
windows_plugin_authenticator = { path = "../windows_plugin_authenticator" }
[build-dependencies]
napi-build = { workspace = true }

View File

@@ -182,3 +182,6 @@ export declare namespace autofill {
export declare namespace crypto {
export function argon2(secret: Buffer, salt: Buffer, iterations: number, memory: number, parallelism: number): Promise<Buffer>
}
export declare namespace passkey_authenticator {
export function register(): void
}

View File

@@ -1,6 +1,7 @@
#[macro_use]
extern crate napi_derive;
mod passkey_authenticator_internal;
mod registry;
#[napi]
@@ -796,3 +797,13 @@ pub mod crypto {
.map(Buffer::from)
}
}
#[napi]
pub mod passkey_authenticator {
#[napi]
pub fn register() -> napi::Result<()> {
crate::passkey_authenticator_internal::register().map_err(|e| {
napi::Error::from_reason(format!("Passkey registration failed - Error: {e} - {e:?}"))
})
}
}

View File

@@ -0,0 +1,5 @@
use anyhow::{bail, Result};
pub fn register() -> Result<()> {
bail!("Not implemented")
}

View File

@@ -0,0 +1,4 @@
#[cfg_attr(target_os = "windows", path = "windows.rs")]
#[cfg_attr(not(target_os = "windows"), path = "dummy.rs")]
mod internal;
pub use internal::*;

View File

@@ -0,0 +1,7 @@
use anyhow::{anyhow, Result};
pub fn register() -> Result<()> {
windows_plugin_authenticator::register().map_err(|e| anyhow!(e))?;
Ok(())
}

View File

@@ -1,10 +0,0 @@
[package]
name = "windows-plugin-authenticator"
edition = { workspace = true }
license = { workspace = true }
version = { workspace = true }
publish = { workspace = true }
[target.'cfg(target_os = "windows")'.build-dependencies]
bindgen = { workspace = true }

View File

@@ -1,11 +0,0 @@
#![cfg(target_os = "windows")]
mod pa;
pub fn get_version_number() -> u64 {
unsafe { pa::WebAuthNGetApiVersionNumber() }.into()
}
pub fn add_authenticator() {
unimplemented!();
}

View File

@@ -0,0 +1,14 @@
[package]
name = "windows_plugin_authenticator"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
publish = { workspace = true }
[target.'cfg(target_os = "windows")'.build-dependencies]
bindgen = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows = { workspace = true, features = ["Win32_Foundation", "Win32_Security", "Win32_System_Com", "Win32_System_LibraryLoader" ] }
windows-core = { workspace = true }
hex = { workspace = true }

View File

@@ -10,12 +10,16 @@ fn windows() {
let bindings = bindgen::Builder::default()
.header("pluginauthenticator.hpp")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.allowlist_type("DWORD")
.allowlist_type("PBYTE")
.allowlist_type("EXPERIMENTAL.*")
.allowlist_function("WebAuthNGetApiVersionNumber")
.generate()
.expect("Unable to generate bindings.");
bindings
.write_to_file(format!(
"{}\\windows_pluginauthenticator_bindings.rs",
"{}\\windows_plugin_authenticator_bindings.rs",
out_dir
))
.expect("Couldn't write bindings.");

View File

@@ -0,0 +1,264 @@
#![cfg(target_os = "windows")]
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
mod pa;
use pa::{
DWORD, EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST,
EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST,
EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE,
EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE,
EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE, PBYTE,
};
use std::ffi::c_uchar;
use std::ptr;
use windows::Win32::Foundation::*;
use windows::Win32::System::Com::*;
use windows::Win32::System::LibraryLoader::*;
use windows_core::*;
const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop Authenticator";
//const AAGUID: &str = "d548826e-79b4-db40-a3d8-11116f7e8349";
const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062";
const RPID: &str = "bitwarden.com";
/// Returns the current Windows WebAuthN version.
pub fn get_version_number() -> u32 {
unsafe { pa::WebAuthNGetApiVersionNumber() }
}
/// Handles initialization and registration for the Bitwarden desktop app as a
/// plugin authenticator with Windows.
/// For now, also adds the authenticator
pub fn register() -> std::result::Result<(), String> {
initialize_com_library()?;
register_com_library()?;
add_authenticator()?;
Ok(())
}
/// Initializes the COM library for use on the calling thread,
/// and registers + sets the security values.
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.
fn register_com_library() -> std::result::Result<(), String> {
static FACTORY: windows_core::StaticComObject<Factory> = Factory().into_static();
let clsid: *const GUID = &GUID::from_u128(0xa98925d161f640de9327dc418fcb2ff4);
match unsafe {
CoRegisterClassObject(
clsid,
FACTORY.as_interface_ref(),
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.
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();
let clsid: HSTRING = format!("{{{}}}", CLSID).into();
let clsid_ptr = PCWSTR(clsid.as_ptr()).as_ptr();
let relying_party_id: HSTRING = RPID.into();
let relying_party_id_ptr = PCWSTR(relying_party_id.as_ptr()).as_ptr();
// let aaguid: HSTRING = format!("{{{}}}", AAGUID).into();
// let aaguid_ptr = PCWSTR(aaguid.as_ptr()).as_ptr();
// Example authenticator info blob
let cbor_authenticator_info = "A60182684649444F5F325F30684649444F5F325F310282637072666B686D61632D7365637265740350D548826E79B4DB40A3D811116F7E834904A362726BF5627570F5627576F5098168696E7465726E616C0A81A263616C672664747970656A7075626C69632D6B6579";
let mut authenticator_info_bytes = hex::decode(cbor_authenticator_info).unwrap();
let add_authenticator_options = EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS {
pwszAuthenticatorName: authenticator_name_ptr,
pwszPluginClsId: clsid_ptr,
pwszPluginRpId: relying_party_id_ptr,
pwszLightThemeLogo: ptr::null(), // unused by Windows
pwszDarkThemeLogo: ptr::null(), // unused by Windows
cbAuthenticatorInfo: authenticator_info_bytes.len() as u32,
pbAuthenticatorInfo: authenticator_info_bytes.as_mut_ptr(),
};
let plugin_signing_public_key_byte_count: DWORD = 0;
let mut plugin_signing_public_key: c_uchar = 0;
let plugin_signing_public_key_ptr: PBYTE = &mut plugin_signing_public_key;
let mut add_response = EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE {
cbOpSignPubKey: plugin_signing_public_key_byte_count,
pbOpSignPubKey: plugin_signing_public_key_ptr,
};
let mut add_response_ptr: *mut EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE =
&mut add_response;
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAddAuthenticatorFnDeclaration>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNPluginAddAuthenticator"),
)
};
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 EXPERIMENTAL_WebAuthNPluginAddAuthenticator()\n{}",
result.message()
));
}
Ok(())
},
None => {
Err(String::from("Error: Can't complete add_authenticator(), as the function EXPERIMENTAL_WebAuthNPluginAddAuthenticator can't be found."))
}
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS {
pub pwszAuthenticatorName: *const u16,
pub pwszPluginClsId: *const u16,
pub pwszPluginRpId: *const u16,
pub pwszLightThemeLogo: *const u16,
pub pwszDarkThemeLogo: *const u16,
pub cbAuthenticatorInfo: u32,
pub pbAuthenticatorInfo: *const u8,
}
type EXPERIMENTAL_WebAuthNPluginAddAuthenticatorFnDeclaration = unsafe extern "cdecl" fn(
pPluginAddAuthenticatorOptions: *const EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS,
ppPluginAddAuthenticatorResponse: *mut EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE,
)
-> HRESULT;
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
}
#[interface("e6466e9a-b2f3-47c5-b88d-89bc14a8d998")]
unsafe trait EXPERIMENTAL_IPluginAuthenticator: IUnknown {
fn EXPERIMENTAL_PluginMakeCredential(
&self,
request: EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST,
response: *mut EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE,
) -> HRESULT;
fn EXPERIMENTAL_PluginGetAssertion(
&self,
request: EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST,
response: *mut EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE,
) -> HRESULT;
fn EXPERIMENTAL_PluginCancelOperation(
&self,
request: EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST,
) -> HRESULT;
}
#[implement(EXPERIMENTAL_IPluginAuthenticator)]
struct PACOMObject;
impl EXPERIMENTAL_IPluginAuthenticator_Impl for PACOMObject_Impl {
unsafe fn EXPERIMENTAL_PluginMakeCredential(
&self,
_request: EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST,
_response: *mut EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE,
) -> HRESULT {
HRESULT(0)
}
unsafe fn EXPERIMENTAL_PluginGetAssertion(
&self,
_request: EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST,
_response: *mut EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE,
) -> HRESULT {
HRESULT(0)
}
unsafe fn EXPERIMENTAL_PluginCancelOperation(
&self,
_request: EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST,
) -> HRESULT {
HRESULT(0)
}
}
#[implement(IClassFactory)]
struct Factory();
impl IClassFactory_Impl for Factory_Impl {
fn CreateInstance(
&self,
outer: Ref<IUnknown>,
iid: *const GUID,
object: *mut *mut core::ffi::c_void,
) -> Result<()> {
assert!(outer.is_null());
let unknown: IInspectable = PACOMObject.into();
unsafe { unknown.query(iid, object).ok() }
}
fn LockServer(&self, lock: BOOL) -> Result<()> {
assert!(lock.as_bool());
Ok(())
}
}

View File

@@ -11,5 +11,5 @@
include!(concat!(
env!("OUT_DIR"),
"/windows_pluginauthenticator_bindings.rs"
"/windows_plugin_authenticator_bindings.rs"
));