diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2550f0fddbe..37591ae5c36 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index d5a93c4bd75..82259155b37 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -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" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 3dd7102bad9..9b1e5c2666c 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -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" diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index f5b6d6b7c04..d59581a5e2e 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -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 } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index ca1fe29e254..6ded4e3db93 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -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 } +export declare namespace passkey_authenticator { + export function register(): void +} diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index f02be2b27b6..8cbc526487e 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -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:?}")) + }) + } +} diff --git a/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/dummy.rs b/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/dummy.rs new file mode 100644 index 00000000000..bcd929c16b4 --- /dev/null +++ b/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/dummy.rs @@ -0,0 +1,5 @@ +use anyhow::{bail, Result}; + +pub fn register() -> Result<()> { + bail!("Not implemented") +} diff --git a/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/mod.rs b/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/mod.rs new file mode 100644 index 00000000000..68929408ec7 --- /dev/null +++ b/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/mod.rs @@ -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::*; diff --git a/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs b/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs new file mode 100644 index 00000000000..4ff51f5bce4 --- /dev/null +++ b/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs @@ -0,0 +1,7 @@ +use anyhow::{anyhow, Result}; + +pub fn register() -> Result<()> { + windows_plugin_authenticator::register().map_err(|e| anyhow!(e))?; + + Ok(()) +} diff --git a/apps/desktop/desktop_native/windows-plugin-authenticator/Cargo.toml b/apps/desktop/desktop_native/windows-plugin-authenticator/Cargo.toml deleted file mode 100644 index d58a6ecd748..00000000000 --- a/apps/desktop/desktop_native/windows-plugin-authenticator/Cargo.toml +++ /dev/null @@ -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 } - diff --git a/apps/desktop/desktop_native/windows-plugin-authenticator/src/lib.rs b/apps/desktop/desktop_native/windows-plugin-authenticator/src/lib.rs deleted file mode 100644 index e226000e6fa..00000000000 --- a/apps/desktop/desktop_native/windows-plugin-authenticator/src/lib.rs +++ /dev/null @@ -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!(); -} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml new file mode 100644 index 00000000000..72a8505389e --- /dev/null +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml @@ -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 } diff --git a/apps/desktop/desktop_native/windows-plugin-authenticator/README.md b/apps/desktop/desktop_native/windows_plugin_authenticator/README.md similarity index 100% rename from apps/desktop/desktop_native/windows-plugin-authenticator/README.md rename to apps/desktop/desktop_native/windows_plugin_authenticator/README.md diff --git a/apps/desktop/desktop_native/windows-plugin-authenticator/build.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/build.rs similarity index 70% rename from apps/desktop/desktop_native/windows-plugin-authenticator/build.rs rename to apps/desktop/desktop_native/windows_plugin_authenticator/build.rs index 7bc311fb12d..022004c7bd1 100644 --- a/apps/desktop/desktop_native/windows-plugin-authenticator/build.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/build.rs @@ -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."); diff --git a/apps/desktop/desktop_native/windows-plugin-authenticator/pluginauthenticator.hpp b/apps/desktop/desktop_native/windows_plugin_authenticator/pluginauthenticator.hpp similarity index 100% rename from apps/desktop/desktop_native/windows-plugin-authenticator/pluginauthenticator.hpp rename to apps/desktop/desktop_native/windows_plugin_authenticator/pluginauthenticator.hpp diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs new file mode 100644 index 00000000000..fe2e35df2f8 --- /dev/null +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs @@ -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().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::( + 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(library: PCSTR, function: PCSTR) -> Option { + 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, + 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(()) + } +} diff --git a/apps/desktop/desktop_native/windows-plugin-authenticator/src/pa.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs similarity index 85% rename from apps/desktop/desktop_native/windows-plugin-authenticator/src/pa.rs rename to apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs index 3da5a77a243..7c93399594d 100644 --- a/apps/desktop/desktop_native/windows-plugin-authenticator/src/pa.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs @@ -11,5 +11,5 @@ include!(concat!( env!("OUT_DIR"), - "/windows_pluginauthenticator_bindings.rs" + "/windows_plugin_authenticator_bindings.rs" ));