1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-02 09:43:29 +00:00

Wire up Windows IPC for make credential

This commit is contained in:
Isaiah Inuwa
2025-11-08 17:25:45 -06:00
parent 81d5765965
commit 0dae3a9c5b
13 changed files with 728 additions and 230 deletions

View File

@@ -14,7 +14,7 @@ $comLogFile = "C:\temp\bitwarden_com_debug.log"
npm run build-native && npm run build:dev && npm run pack:win:arm64
# Backup tokens
Copy-Item -Path "$bwFolder\LocalCache\Roaming\Bitwarden\data.json" -Destination $backupDataFile
# Copy-Item -Path "$bwFolder\LocalCache\Roaming\Bitwarden\data.json" -Destination $backupDataFile
# Reinstall Appx
Remove-AppxPackage $package && Add-AppxPackage $appx
@@ -23,5 +23,5 @@ Remove-AppxPackage $package && Add-AppxPackage $appx
Remove-Item -Path $comLogFile
# Restore tokens
New-Item -Type Directory -Force -Path "$bwFolder\LocalCache\Roaming\Bitwarden\"
Copy-Item -Path $backupDataFile -Destination "$bwFolder\LocalCache\Roaming\Bitwarden\data.json"
# New-Item -Type Directory -Force -Path "$bwFolder\LocalCache\Roaming\Bitwarden\"
# Copy-Item -Path $backupDataFile -Destination "$bwFolder\LocalCache\Roaming\Bitwarden\data.json"

View File

@@ -5162,12 +5162,16 @@ name = "windows_plugin_authenticator"
version = "0.0.0"
dependencies = [
"ciborium",
"desktop_core",
"futures",
"hex",
"reqwest",
"serde",
"serde_json",
"sha2",
"tokio",
"tracing",
"tracing-subscriber",
"windows 0.62.2",
"windows-core 0.62.2",
]

View File

@@ -6,6 +6,8 @@ license = { workspace = true }
publish = { workspace = true }
[target.'cfg(windows)'.dependencies]
desktop_core = { path = "../core" }
futures = { workspace = true }
windows = { workspace = true, features = [
"Win32_Foundation",
"Win32_Security",
@@ -17,6 +19,8 @@ hex = { workspace = true }
reqwest = { version = "0.12", features = ["json", "blocking"] }
serde_json = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
ciborium = "0.2"
sha2 = "0.10"
tokio = { workspace = true }

View File

@@ -1,7 +1,9 @@
use windows::Win32::Foundation::S_OK;
use windows::Win32::System::Com::*;
use windows_core::{implement, interface, IInspectable, IUnknown, Interface, HRESULT};
use crate::assert::plugin_get_assertion;
use crate::ipc2::WindowsProviderClient;
use crate::make_credential::plugin_make_credential;
use crate::util::debug_log;
use crate::webauthn::WEBAUTHN_CREDENTIAL_LIST;
@@ -130,7 +132,9 @@ pub unsafe fn parse_credential_list(credential_list: &WEBAUTHN_CREDENTIAL_LIST)
}
#[implement(IPluginAuthenticator)]
pub struct PluginAuthenticatorComObject;
pub struct PluginAuthenticatorComObject {
client: WindowsProviderClient,
}
#[implement(IClassFactory)]
pub struct Factory;
@@ -142,13 +146,17 @@ impl IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl {
response: *mut WebAuthnPluginOperationResponse,
) -> HRESULT {
debug_log("MakeCredential() called");
debug_log("version2");
// Convert to legacy format for internal processing
if request.is_null() || response.is_null() {
debug_log("MakeCredential: Invalid request or response pointers passed");
return HRESULT(-1);
}
plugin_make_credential(request, response)
match plugin_make_credential(&self.client, request, response) {
Ok(()) => S_OK,
Err(err) => err,
}
}
unsafe fn GetAssertion(
@@ -188,7 +196,11 @@ impl IClassFactory_Impl for Factory_Impl {
iid: *const windows_core::GUID,
object: *mut *mut core::ffi::c_void,
) -> windows_core::Result<()> {
let unknown: IInspectable = PluginAuthenticatorComObject.into(); // TODO: IUnknown ?
debug_log("Creating COM server instance.");
debug_log("Trying to connect to Bitwarden IPC");
let client = WindowsProviderClient::connect();
debug_log("Connected to Bitwarden IPC");
let unknown: IInspectable = PluginAuthenticatorComObject { client }.into(); // TODO: IUnknown ?
unsafe { unknown.query(iid, object).ok() }
}

View File

@@ -0,0 +1,57 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use super::{BitwardenError, Callback, Position, UserVerification};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyAssertionRequest {
rp_id: String,
client_data_hash: Vec<u8>,
user_verification: UserVerification,
allowed_credentials: Vec<Vec<u8>>,
window_xy: Position,
//extension_input: Vec<u8>, TODO: Implement support for extensions
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyAssertionWithoutUserInterfaceRequest {
rp_id: String,
credential_id: Vec<u8>,
user_name: String,
user_handle: Vec<u8>,
record_identifier: Option<String>,
client_data_hash: Vec<u8>,
user_verification: UserVerification,
window_xy: Position,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyAssertionResponse {
rp_id: String,
user_handle: Vec<u8>,
signature: Vec<u8>,
client_data_hash: Vec<u8>,
authenticator_data: Vec<u8>,
credential_id: Vec<u8>,
}
pub trait PreparePasskeyAssertionCallback: Send + Sync {
fn on_complete(&self, credential: PasskeyAssertionResponse);
fn on_error(&self, error: BitwardenError);
}
impl Callback for Arc<dyn PreparePasskeyAssertionCallback> {
fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error> {
let credential = serde_json::from_value(credential)?;
PreparePasskeyAssertionCallback::on_complete(self.as_ref(), credential);
Ok(())
}
fn error(&self, error: BitwardenError) {
PreparePasskeyAssertionCallback::on_error(self.as_ref(), error);
}
}

View File

@@ -0,0 +1,310 @@
use std::{
collections::HashMap,
error::Error,
fmt::Display,
sync::{atomic::AtomicU32, Arc, Mutex, Once},
time::Instant,
};
use futures::FutureExt;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tracing::{error, info};
use tracing_subscriber::{
filter::{EnvFilter, LevelFilter},
layer::SubscriberExt,
util::SubscriberInitExt,
};
mod assertion;
mod registration;
pub use assertion::{
PasskeyAssertionRequest, PasskeyAssertionResponse, PasskeyAssertionWithoutUserInterfaceRequest,
PreparePasskeyAssertionCallback,
};
pub use registration::{
PasskeyRegistrationRequest, PasskeyRegistrationResponse, PreparePasskeyRegistrationCallback,
};
use crate::util::debug_log;
static INIT: Once = Once::new();
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum UserVerification {
Preferred,
Required,
Discouraged,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Position {
pub x: i32,
pub y: i32,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum BitwardenError {
Internal(String),
}
impl Display for BitwardenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Internal(msg) => write!(f, "Internal error occurred: {msg}"),
}
}
}
impl Error for BitwardenError {}
// TODO: These have to be named differently than the actual Uniffi traits otherwise
// the generated code will lead to ambiguous trait implementations
// These are only used internally, so it doesn't matter that much
trait Callback: Send + Sync {
fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error>;
fn error(&self, error: BitwardenError);
}
#[derive(Debug)]
/// Store the connection status between the Windows credential provider extension
/// and the desktop application's IPC server.
pub enum ConnectionStatus {
Connected,
Disconnected,
}
pub struct WindowsProviderClient {
to_server_send: tokio::sync::mpsc::Sender<String>,
// We need to keep track of the callbacks so we can call them when we receive a response
response_callbacks_counter: AtomicU32,
#[allow(clippy::type_complexity)]
response_callbacks_queue: Arc<Mutex<HashMap<u32, (Box<dyn Callback>, Instant)>>>,
// Flag to track connection status - atomic for thread safety without locks
connection_status: Arc<std::sync::atomic::AtomicBool>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Store native desktop status information to use for IPC communication
/// between the application and the Windows credential provider.
pub struct NativeStatus {
key: String,
value: String,
}
// In our callback management, 0 is a reserved sequence number indicating that a message does not have a callback.
const NO_CALLBACK_INDICATOR: u32 = 0;
impl WindowsProviderClient {
// FIXME: Remove unwraps! They panic and terminate the whole application.
#[allow(clippy::unwrap_used)]
pub fn connect() -> Self {
debug_log("YO!");
INIT.call_once(|| {
/*
let filter = EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.into())
.from_env_lossy();
let log_file_path = "C:\\temp\\bitwarden_windows_passkey_provider.log";
debug_log(&format!("Trying to set up log file at {log_file_path}"));
// FIXME: Remove unwrap
let file = std::fs::File::options()
.append(true)
.open(log_file_path)
.unwrap();
let log_file = tracing_subscriber::fmt::layer().with_writer(file);
tracing_subscriber::registry()
.with(filter)
.with(log_file)
.init();
*/
});
tracing::debug!("Windows COM server trying to connect to Electron IPC...");
let (from_server_send, mut from_server_recv) = tokio::sync::mpsc::channel(32);
let (to_server_send, to_server_recv) = tokio::sync::mpsc::channel(32);
let client = WindowsProviderClient {
to_server_send,
response_callbacks_counter: AtomicU32::new(1), // Start at 1 since 0 is reserved for "no callback" scenarios
response_callbacks_queue: Arc::new(Mutex::new(HashMap::new())),
connection_status: Arc::new(std::sync::atomic::AtomicBool::new(false)),
};
let path = desktop_core::ipc::path("af");
let queue = client.response_callbacks_queue.clone();
let connection_status = client.connection_status.clone();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Can't create runtime");
rt.spawn(
desktop_core::ipc::client::connect(path, from_server_send, to_server_recv)
.map(|r| r.map_err(|e| e.to_string())),
);
rt.block_on(async move {
while let Some(message) = from_server_recv.recv().await {
match serde_json::from_str::<SerializedMessage>(&message) {
Ok(SerializedMessage::Command(CommandMessage::Connected)) => {
info!("Connected to server");
connection_status.store(true, std::sync::atomic::Ordering::Relaxed);
}
Ok(SerializedMessage::Command(CommandMessage::Disconnected)) => {
info!("Disconnected from server");
connection_status.store(false, std::sync::atomic::Ordering::Relaxed);
}
Ok(SerializedMessage::Message {
sequence_number,
value,
}) => match queue.lock().unwrap().remove(&sequence_number) {
Some((cb, request_start_time)) => {
info!(
"Time to process request: {:?}",
request_start_time.elapsed()
);
match value {
Ok(value) => {
if let Err(e) = cb.complete(value) {
error!(error = %e, "Error deserializing message");
}
}
Err(e) => {
error!(error = ?e, "Error processing message");
cb.error(e)
}
}
}
None => {
error!(sequence_number, "No callback found for sequence number")
}
},
Err(e) => {
error!(error = %e, "Error deserializing message");
}
};
}
});
});
client
}
pub fn send_native_status(&self, key: String, value: String) {
let status = NativeStatus { key, value };
self.send_message(status, None);
}
pub fn prepare_passkey_registration(
&self,
request: PasskeyRegistrationRequest,
callback: Arc<dyn PreparePasskeyRegistrationCallback>,
) {
self.send_message(request, Some(Box::new(callback)));
}
pub fn prepare_passkey_assertion(
&self,
request: PasskeyAssertionRequest,
callback: Arc<dyn PreparePasskeyAssertionCallback>,
) {
self.send_message(request, Some(Box::new(callback)));
}
pub fn prepare_passkey_assertion_without_user_interface(
&self,
request: PasskeyAssertionWithoutUserInterfaceRequest,
callback: Arc<dyn PreparePasskeyAssertionCallback>,
) {
self.send_message(request, Some(Box::new(callback)));
}
pub fn get_connection_status(&self) -> ConnectionStatus {
let is_connected = self
.connection_status
.load(std::sync::atomic::Ordering::Relaxed);
if is_connected {
ConnectionStatus::Connected
} else {
ConnectionStatus::Disconnected
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "command", rename_all = "camelCase")]
enum CommandMessage {
Connected,
Disconnected,
}
#[derive(Serialize, Deserialize)]
#[serde(untagged, rename_all = "camelCase")]
enum SerializedMessage {
Command(CommandMessage),
Message {
sequence_number: u32,
value: Result<serde_json::Value, BitwardenError>,
},
}
impl WindowsProviderClient {
#[allow(clippy::unwrap_used)]
fn add_callback(&self, callback: Box<dyn Callback>) -> u32 {
let sequence_number = self
.response_callbacks_counter
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.response_callbacks_queue
.lock()
.expect("response callbacks queue mutex should not be poisoned")
.insert(sequence_number, (callback, Instant::now()));
sequence_number
}
#[allow(clippy::unwrap_used)]
fn send_message(
&self,
message: impl Serialize + DeserializeOwned,
callback: Option<Box<dyn Callback>>,
) {
let sequence_number = if let Some(callback) = callback {
self.add_callback(callback)
} else {
NO_CALLBACK_INDICATOR
};
let message = serde_json::to_string(&SerializedMessage::Message {
sequence_number,
value: Ok(serde_json::to_value(message).unwrap()),
})
.expect("Can't serialize message");
if let Err(e) = self.to_server_send.blocking_send(message) {
// Make sure we remove the callback from the queue if we can't send the message
if sequence_number != NO_CALLBACK_INDICATOR {
if let Some((callback, _)) = self
.response_callbacks_queue
.lock()
.expect("response callbacks queue mutex should not be poisoned")
.remove(&sequence_number)
{
callback.error(BitwardenError::Internal(format!(
"Error sending message: {e}"
)));
}
}
}
}
}

View File

@@ -0,0 +1,44 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use super::{BitwardenError, Callback, Position, UserVerification};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyRegistrationRequest {
pub rp_id: String,
pub user_name: String,
pub user_handle: Vec<u8>,
pub client_data_hash: Vec<u8>,
pub user_verification: UserVerification,
pub supported_algorithms: Vec<i32>,
pub window_xy: Position,
pub excluded_credentials: Vec<Vec<u8>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyRegistrationResponse {
pub rp_id: String,
pub client_data_hash: Vec<u8>,
pub credential_id: Vec<u8>,
pub attestation_object: Vec<u8>,
}
pub trait PreparePasskeyRegistrationCallback: Send + Sync {
fn on_complete(&self, credential: PasskeyRegistrationResponse);
fn on_error(&self, error: BitwardenError);
}
impl Callback for Arc<dyn PreparePasskeyRegistrationCallback> {
fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error> {
let credential = serde_json::from_value(credential)?;
PreparePasskeyRegistrationCallback::on_complete(self.as_ref(), credential);
Ok(())
}
fn error(&self, error: BitwardenError) {
PreparePasskeyRegistrationCallback::on_error(self.as_ref(), error);
}
}

View File

@@ -8,6 +8,7 @@ mod com_buffer;
mod com_provider;
mod com_registration;
mod ipc;
mod ipc2;
mod make_credential;
mod sync;
mod types;
@@ -30,6 +31,7 @@ use crate::util::debug_log;
/// Handles initialization and registration for the Bitwarden desktop app as a
/// For now, also adds the authenticator
pub fn register() -> std::result::Result<(), String> {
// TODO: Can we spawn a new named thread for debugging?
debug_log("register() called...");
let r = com_registration::initialize_com_library();
@@ -43,3 +45,6 @@ pub fn register() -> std::result::Result<(), String> {
Ok(())
}
/// This sets up IPC so the plugin can request credentials from Electron.
fn setup_ipc() {}

View File

@@ -2,13 +2,26 @@ use serde_json;
use std::alloc::{alloc, Layout};
use std::collections::HashMap;
use std::mem::ManuallyDrop;
use std::sync::mpsc::Receiver;
use std::sync::Mutex;
use std::sync::{
mpsc::{self, Sender},
Arc,
};
use std::time::Duration;
use std::{ptr, slice};
use windows_core::{s, HRESULT};
use crate::com_provider::{
parse_credential_list, WebAuthnPluginOperationRequest, WebAuthnPluginOperationResponse,
};
use crate::types::*;
use crate::ipc2::{
self, BitwardenError, PasskeyAssertionRequest, PasskeyAssertionResponse,
PasskeyRegistrationRequest, PasskeyRegistrationResponse, Position,
PreparePasskeyAssertionCallback, PreparePasskeyRegistrationCallback, UserVerification,
WindowsProviderClient,
};
use crate::types::UserVerificationRequirement;
use crate::util::{debug_log, delay_load, wstr_to_string, WindowsString};
use crate::webauthn::WEBAUTHN_CREDENTIAL_LIST;
@@ -317,36 +330,111 @@ unsafe fn decode_make_credential_request(
/// Helper for registration requests
fn send_registration_request(
ipc_client: &WindowsProviderClient,
transaction_id: &str,
request: &WindowsRegistrationRequest,
) -> Option<PasskeyResponse> {
) -> Result<PasskeyRegistrationResponse, String> {
debug_log(&format!("Registration request data - RP ID: {}, User ID: {} bytes, User name: {}, Client data hash: {} bytes, Algorithms: {:?}, Excluded credentials: {}",
request.rpid, request.user_id.len(), request.user_name, request.client_data_hash.len(), request.supported_algorithms, request.excluded_credentials.len()));
let user_verification = match request.user_verification {
UserVerificationRequirement::Discouraged => UserVerification::Discouraged,
UserVerificationRequirement::Preferred => UserVerification::Preferred,
UserVerificationRequirement::Required => UserVerification::Required,
};
let passkey_request = PasskeyRegistrationRequest {
rp_id: request.rpid.clone(),
transaction_id: transaction_id.to_string(),
// transaction_id: transaction_id.to_string(),
user_handle: request.user_id.clone(),
user_name: request.user_name.clone(),
client_data_hash: request.client_data_hash.clone(),
user_verification: request.user_verification.clone(),
user_verification,
window_xy: Position { x: 400, y: 400 }, // TODO: Get actual window position
supported_algorithms: request.supported_algorithms.clone(),
excluded_credentials: request.excluded_credentials.clone(),
};
match serde_json::to_string(&passkey_request) {
Ok(request_json) => {
debug_log(&format!("Sending registration request: {}", request_json));
crate::ipc::send_passkey_request(RequestType::Registration, request_json, &request.rpid)
let request_json = serde_json::to_string(&passkey_request)
.map_err(|err| format!("Failed to serialize registration request: {err}"))?;
debug_log(&format!("Sending registration request: {}", request_json));
let callback = Arc::new(Callback::new());
ipc_client.prepare_passkey_registration(passkey_request, callback.clone());
callback
.wait_for_response(Duration::from_secs(30))
.map_err(|_| "Registration request timed out".to_string())?
.map_err(|err| err.to_string())
/*
{
Ok(Ok(response)) => {
tracing::debug!("Received registration response from Electron: {response:?}");
Some(response)
}
Err(e) => {
debug_log(&format!(
"ERROR: Failed to serialize registration request: {}",
e
));
Ok(Err(err)) => {
tracing::error!("Registration request failed: {err}");
None
}
Err(_) => {
tracing::error!("Timed out waiting for registration response");
None
}
}
*/
// crate::ipc::send_passkey_request(RequestType::Registration, request_json, &request.rpid)
}
struct Callback<T> {
tx: Mutex<Option<Sender<Result<T, BitwardenError>>>>,
rx: Mutex<Receiver<Result<T, BitwardenError>>>,
}
impl<T> Callback<T> {
fn new() -> Self {
let (tx, rx) = mpsc::channel();
Self {
tx: Mutex::new(Some(tx)),
rx: Mutex::new(rx),
}
}
fn wait_for_response(
&self,
timeout: Duration,
) -> Result<Result<T, BitwardenError>, mpsc::RecvTimeoutError> {
self.rx.lock().unwrap().recv_timeout(timeout)
}
fn send(&self, response: Result<T, BitwardenError>) {
match self.tx.lock().unwrap().take() {
Some(tx) => {
if let Err(_) = tx.send(response) {
tracing::error!("Windows provider channel closed before receiving IPC response from Electron")
}
}
None => {
tracing::error!("Callback channel used before response: multi-threading issue?");
}
}
}
}
impl PreparePasskeyRegistrationCallback for Callback<PasskeyRegistrationResponse> {
fn on_complete(&self, credential: PasskeyRegistrationResponse) {
self.send(Ok(credential));
}
fn on_error(&self, error: BitwardenError) {
self.send(Err(error))
}
}
impl PreparePasskeyAssertionCallback for Callback<PasskeyAssertionResponse> {
fn on_complete(&self, credential: PasskeyAssertionResponse) {
self.send(Ok(credential));
}
fn on_error(&self, error: BitwardenError) {
self.send(Err(error))
}
}
@@ -390,7 +478,6 @@ unsafe fn create_make_credential_response(
s!("WebAuthNEncodeMakeCredentialResponse"),
)
.unwrap();
let mut authenticator_data = vec![1, 2, 3, 4];
let att_fmt = webauthn_att_obj
.get("fmt")
.ok_or(HRESULT(-1))?
@@ -480,19 +567,20 @@ unsafe fn create_make_credential_response(
/// Implementation of PluginMakeCredential moved from com_provider.rs
pub unsafe fn plugin_make_credential(
ipc_client: &WindowsProviderClient,
request: *const WebAuthnPluginOperationRequest,
response: *mut WebAuthnPluginOperationResponse,
) -> HRESULT {
) -> Result<(), HRESULT> {
debug_log("=== PluginMakeCredential() called ===");
if request.is_null() {
debug_log("ERROR: NULL request pointer");
return HRESULT(-1);
return Err(HRESULT(-1));
}
if response.is_null() {
debug_log("ERROR: NULL response pointer");
return HRESULT(-1);
return Err(HRESULT(-1));
}
let req = &*request;
@@ -500,7 +588,7 @@ pub unsafe fn plugin_make_credential(
if req.encoded_request_byte_count == 0 || req.encoded_request_pointer.is_null() {
debug_log("ERROR: No encoded request data provided");
return HRESULT(-1);
return Err(HRESULT(-1));
}
let encoded_request_slice = std::slice::from_raw_parts(
@@ -514,213 +602,182 @@ pub unsafe fn plugin_make_credential(
));
// Try to decode the request using Windows API
match decode_make_credential_request(encoded_request_slice) {
Ok(decoded_wrapper) => {
let decoded_request = decoded_wrapper.as_ref();
debug_log("Successfully decoded make credential request using Windows API");
let decoded_wrapper = decode_make_credential_request(encoded_request_slice).map_err(|err| {
debug_log(&format!(
"ERROR: Failed to decode make credential request: {err}"
));
HRESULT(-1)
})?;
let decoded_request = decoded_wrapper.as_ref();
debug_log("Successfully decoded make credential request using Windows API");
// Extract RP information
if decoded_request.pRpInformation.is_null() {
debug_log("ERROR: RP information is null");
return HRESULT(-1);
}
let rp_info = &*decoded_request.pRpInformation;
let rpid = if rp_info.pwszId.is_null() {
debug_log("ERROR: RP ID is null");
return HRESULT(-1);
} else {
match wstr_to_string(rp_info.pwszId) {
Ok(id) => id,
Err(e) => {
debug_log(&format!("ERROR: Failed to decode RP ID: {}", e));
return HRESULT(-1);
}
}
};
// let rp_name = if rp_info.pwszName.is_null() {
// String::new()
// } else {
// wstr_to_string(rp_info.pwszName).unwrap_or_default()
// };
// Extract user information
if decoded_request.pUserInformation.is_null() {
debug_log("ERROR: User information is null");
return HRESULT(-1);
}
let user = &*decoded_request.pUserInformation;
let user_id = if user.pbId.is_null() || user.cbId == 0 {
debug_log("ERROR: User ID is required for registration");
return HRESULT(-1);
} else {
let id_slice = std::slice::from_raw_parts(user.pbId, user.cbId as usize);
id_slice.to_vec()
};
let user_name = if user.pwszName.is_null() {
debug_log("ERROR: User name is required for registration");
return HRESULT(-1);
} else {
match wstr_to_string(user.pwszName) {
Ok(name) => name,
Err(_) => {
debug_log("ERROR: Failed to decode user name");
return HRESULT(-1);
}
}
};
let user_display_name = if user.pwszDisplayName.is_null() {
None
} else {
wstr_to_string(user.pwszDisplayName).ok()
};
let user_info = (user_id, user_name, user_display_name);
// Extract client data hash
let client_data_hash = if decoded_request.cbClientDataHash == 0
|| decoded_request.pbClientDataHash.is_null()
{
debug_log("ERROR: Client data hash is required for registration");
return HRESULT(-1);
} else {
let hash_slice = std::slice::from_raw_parts(
decoded_request.pbClientDataHash,
decoded_request.cbClientDataHash as usize,
);
hash_slice.to_vec()
};
// Extract supported algorithms
let supported_algorithms = if decoded_request
.WebAuthNCredentialParameters
.cCredentialParameters
> 0
&& !decoded_request
.WebAuthNCredentialParameters
.pCredentialParameters
.is_null()
{
let params_count = decoded_request
.WebAuthNCredentialParameters
.cCredentialParameters as usize;
let params_ptr = decoded_request
.WebAuthNCredentialParameters
.pCredentialParameters;
(0..params_count)
.map(|i| unsafe { &*params_ptr.add(i) }.lAlg)
.collect()
} else {
Vec::new()
};
// Extract user verification requirement from authenticator options
let user_verification = if !decoded_request.pAuthenticatorOptions.is_null() {
let auth_options = &*decoded_request.pAuthenticatorOptions;
match auth_options.user_verification {
1 => Some(UserVerificationRequirement::Required),
-1 => Some(UserVerificationRequirement::Discouraged),
0 | _ => Some(UserVerificationRequirement::Preferred), // Default or undefined
}
} else {
None
};
// Extract excluded credentials from credential list
let excluded_credentials = parse_credential_list(&decoded_request.CredentialList);
if !excluded_credentials.is_empty() {
debug_log(&format!(
"Found {} excluded credentials for make credential",
excluded_credentials.len()
));
}
// Create Windows registration request
let registration_request = WindowsRegistrationRequest {
rpid: rpid.clone(),
user_id: user_info.0,
user_name: user_info.1,
user_display_name: user_info.2,
client_data_hash,
excluded_credentials,
user_verification: user_verification.unwrap_or_default(),
supported_algorithms,
};
debug_log(&format!(
"Make credential request - RP: {}, User: {}",
rpid, registration_request.user_name
));
// Send registration request
if let Some(passkey_response) =
send_registration_request(&transaction_id, &registration_request)
{
debug_log(&format!(
"Registration response received: {:?}",
passkey_response
));
// Create proper WebAuthn response from passkey_response
match passkey_response {
PasskeyResponse::RegistrationResponse {
credential_id: _,
attestation_object,
rp_id: _,
client_data_hash: _,
} => {
debug_log("Creating WebAuthn make credential response");
match create_make_credential_response(attestation_object) {
Ok(mut webauthn_response) => {
debug_log(&format!(
"Successfully created WebAuthn response: {webauthn_response:?}"
));
(*response).encoded_response_byte_count =
webauthn_response.len() as u32;
(*response).encoded_response_pointer =
webauthn_response.as_mut_ptr();
debug_log(&format!("Set pointer, returning HRESULT(0)"));
_ = ManuallyDrop::new(webauthn_response);
HRESULT(0)
}
Err(e) => {
debug_log(&format!(
"ERROR: Failed to create WebAuthn response: {}",
e
));
HRESULT(-1)
}
}
}
PasskeyResponse::Error { message } => {
debug_log(&format!("Registration request failed: {}", message));
HRESULT(-1)
}
_ => {
debug_log("ERROR: Unexpected response type for registration request");
HRESULT(-1)
}
}
} else {
debug_log("ERROR: No response from registration request");
HRESULT(-1)
}
}
Err(e) => {
debug_log(&format!(
"ERROR: Failed to decode make credential request: {}",
e
));
HRESULT(-1)
}
// Extract RP information
if decoded_request.pRpInformation.is_null() {
debug_log("ERROR: RP information is null");
return Err(HRESULT(-1));
}
let rp_info = &*decoded_request.pRpInformation;
let rpid = if rp_info.pwszId.is_null() {
debug_log("ERROR: RP ID is null");
return Err(HRESULT(-1));
} else {
match wstr_to_string(rp_info.pwszId) {
Ok(id) => id,
Err(e) => {
debug_log(&format!("ERROR: Failed to decode RP ID: {}", e));
return Err(HRESULT(-1));
}
}
};
// let rp_name = if rp_info.pwszName.is_null() {
// String::new()
// } else {
// wstr_to_string(rp_info.pwszName).unwrap_or_default()
// };
// Extract user information
if decoded_request.pUserInformation.is_null() {
debug_log("ERROR: User information is null");
return Err(HRESULT(-1));
}
let user = &*decoded_request.pUserInformation;
let user_id = if user.pbId.is_null() || user.cbId == 0 {
debug_log("ERROR: User ID is required for registration");
return Err(HRESULT(-1));
} else {
let id_slice = std::slice::from_raw_parts(user.pbId, user.cbId as usize);
id_slice.to_vec()
};
let user_name = if user.pwszName.is_null() {
debug_log("ERROR: User name is required for registration");
return Err(HRESULT(-1));
} else {
match wstr_to_string(user.pwszName) {
Ok(name) => name,
Err(_) => {
debug_log("ERROR: Failed to decode user name");
return Err(HRESULT(-1));
}
}
};
let user_display_name = if user.pwszDisplayName.is_null() {
None
} else {
wstr_to_string(user.pwszDisplayName).ok()
};
let user_info = (user_id, user_name, user_display_name);
// Extract client data hash
let client_data_hash =
if decoded_request.cbClientDataHash == 0 || decoded_request.pbClientDataHash.is_null() {
debug_log("ERROR: Client data hash is required for registration");
return Err(HRESULT(-1));
} else {
let hash_slice = std::slice::from_raw_parts(
decoded_request.pbClientDataHash,
decoded_request.cbClientDataHash as usize,
);
hash_slice.to_vec()
};
// Extract supported algorithms
let supported_algorithms = if decoded_request
.WebAuthNCredentialParameters
.cCredentialParameters
> 0
&& !decoded_request
.WebAuthNCredentialParameters
.pCredentialParameters
.is_null()
{
let params_count = decoded_request
.WebAuthNCredentialParameters
.cCredentialParameters as usize;
let params_ptr = decoded_request
.WebAuthNCredentialParameters
.pCredentialParameters;
(0..params_count)
.map(|i| unsafe { &*params_ptr.add(i) }.lAlg)
.collect()
} else {
Vec::new()
};
// Extract user verification requirement from authenticator options
let user_verification = if !decoded_request.pAuthenticatorOptions.is_null() {
let auth_options = &*decoded_request.pAuthenticatorOptions;
match auth_options.user_verification {
1 => Some(UserVerificationRequirement::Required),
-1 => Some(UserVerificationRequirement::Discouraged),
0 | _ => Some(UserVerificationRequirement::Preferred), // Default or undefined
}
} else {
None
};
// Extract excluded credentials from credential list
let excluded_credentials = parse_credential_list(&decoded_request.CredentialList);
if !excluded_credentials.is_empty() {
debug_log(&format!(
"Found {} excluded credentials for make credential",
excluded_credentials.len()
));
}
// Create Windows registration request
let registration_request = WindowsRegistrationRequest {
rpid: rpid.clone(),
user_id: user_info.0,
user_name: user_info.1,
user_display_name: user_info.2,
client_data_hash,
excluded_credentials,
user_verification: user_verification.unwrap_or_default(),
supported_algorithms,
};
debug_log(&format!(
"Make credential request - RP: {}, User: {}",
rpid, registration_request.user_name
));
// Send registration request
let passkey_response =
send_registration_request(ipc_client, &transaction_id, &registration_request).map_err(
|err| {
tracing::error!("Registration request failed: {err}");
HRESULT(-1)
},
)?;
debug_log(&format!(
"Registration response received: {:?}",
passkey_response
));
// Create proper WebAuthn response from passkey_response
debug_log("Creating WebAuthn make credential response");
let mut webauthn_response =
create_make_credential_response(passkey_response.attestation_object).map_err(|err| {
debug_log(&format!("ERROR: Failed to create WebAuthn response: {err}"));
HRESULT(-1)
})?;
debug_log(&format!(
"Successfully created WebAuthn response: {webauthn_response:?}"
));
(*response).encoded_response_byte_count = webauthn_response.len() as u32;
(*response).encoded_response_pointer = webauthn_response.as_mut_ptr();
debug_log(&format!("Set pointer, returning HRESULT(0)"));
_ = ManuallyDrop::new(webauthn_response);
Ok(())
}
#[cfg(test)]

View File

@@ -78,6 +78,7 @@ pub fn file_log(msg: &str) {
}
pub fn debug_log(message: &str) {
tracing::debug!(message);
file_log(message)
}

View File

@@ -193,6 +193,7 @@ export class DesktopAutofillService implements OnDestroy {
}
listenIpc() {
this.logService.debug("Setting up Native -> Electron IPC Handlers")
ipc.autofill.listenPasskeyRegistration(async (clientId, sequenceNumber, request, callback) => {
if (!(await this.configService.getFeatureFlag(NativeCredentialSyncFeatureFlag))) {
this.logService.debug(
@@ -216,6 +217,7 @@ export class DesktopAutofillService implements OnDestroy {
controller,
);
this.logService.debug("Sending registration response to plugin via callback");
callback(null, this.convertRegistrationResponse(request, response));
} catch (error) {
this.logService.error("listenPasskeyRegistration error", error);

View File

@@ -83,7 +83,7 @@ export class NativeAutofillMain {
);
this.ipcServer = await autofill.IpcServer.listen(
"autofill",
"af",
// RegistrationCallback
(error, clientId, sequenceNumber, request) => {
if (error) {

View File

@@ -28,6 +28,7 @@ export class NativeAutofillWindowsMain {
"message": "Failed to register windows passkey plugin"
})
}
/*
void passkey_authenticator.onRequest(async (error, event) => {
this.logService.info("Passkey request received:", { error, event });
@@ -58,6 +59,7 @@ export class NativeAutofillWindowsMain {
});
}
});
*/
}
private async handleAssertionRequest(request: autofill.PasskeyAssertionRequest): Promise<string> {