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:
@@ -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"
|
||||
|
||||
4
apps/desktop/desktop_native/Cargo.lock
generated
4
apps/desktop/desktop_native/Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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, ®istration_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, ®istration_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)]
|
||||
|
||||
@@ -78,6 +78,7 @@ pub fn file_log(msg: &str) {
|
||||
}
|
||||
|
||||
pub fn debug_log(message: &str) {
|
||||
tracing::debug!(message);
|
||||
file_log(message)
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -83,7 +83,7 @@ export class NativeAutofillMain {
|
||||
);
|
||||
|
||||
this.ipcServer = await autofill.IpcServer.listen(
|
||||
"autofill",
|
||||
"af",
|
||||
// RegistrationCallback
|
||||
(error, clientId, sequenceNumber, request) => {
|
||||
if (error) {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user