1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-07 04:03:29 +00:00

Add autofill IPC client methods needed for Windows IPC

This commit is contained in:
Isaiah Inuwa
2025-12-20 00:14:36 -06:00
parent ea2b3f72f4
commit 8c07744133
11 changed files with 377 additions and 68 deletions

View File

@@ -328,6 +328,7 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
name = "autofill_provider"
version = "0.0.0"
dependencies = [
"base64",
"desktop_core",
"futures",
"serde",

View File

@@ -6,7 +6,7 @@ version = { workspace = true }
publish = { workspace = true }
[lib]
crate-type = ["staticlib", "cdylib"]
crate-type = ["lib", "staticlib", "cdylib"]
bench = false
[[bin]]
@@ -14,15 +14,16 @@ name = "uniffi-bindgen"
path = "uniffi-bindgen.rs"
[dependencies]
uniffi = { workspace = true, features = ["cli"] }
[target.'cfg(target_os = "macos")'.dependencies]
base64 = { workspace = true}
desktop_core = { path = "../core" }
futures = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
tracing = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
uniffi = { workspace = true, features = ["cli"] }
tracing-subscriber = { workspace = true }
tracing-oslog = "=0.3.0"

View File

@@ -1,3 +1,7 @@
# Autofill Provider
A library for native autofill providers to interact with a host Bitwarden desktop app.
# Explainer: Mac OS Native Passkey Provider
This document describes the changes introduced in https://github.com/bitwarden/clients/pull/13963, where we introduce the MacOS Native Passkey Provider. It gives the high level explanation of the architecture and some of the quirks and additional good to know context.

View File

@@ -2,8 +2,12 @@
cd "$(dirname "$0")"
rm -r BitwardenMacosProviderFFI.xcframework
rm -r tmp
if [ -d "BitwardenMacosProviderFFI.xcframework" ]; then
rm -r "BitwardenMacosProviderFFI.xcframework"
fi
if [ -d "tmp" ]; then
rm -r "tmp"
fi
mkdir -p ./tmp/target/universal-darwin/release/

View File

@@ -2,44 +2,60 @@ use std::sync::Arc;
use serde::{Deserialize, Serialize};
#[cfg(not(target_os = "macos"))]
use crate::TimedCallback;
use crate::{BitwardenError, Callback, Position, UserVerification};
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
#[cfg_attr(target_os = "macos", derive(uniffi::Record))]
#[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
pub rp_id: String,
pub client_data_hash: Vec<u8>,
pub user_verification: UserVerification,
pub allowed_credentials: Vec<Vec<u8>>,
pub window_xy: Position,
#[cfg(not(target_os = "macos"))]
pub client_window_handle: Vec<u8>,
#[cfg(not(target_os = "macos"))]
pub context: String,
// pub extension_input: Vec<u8>, TODO: Implement support for extensions
}
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
#[cfg_attr(target_os = "macos", derive(uniffi::Record))]
#[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,
pub rp_id: String,
pub credential_id: Vec<u8>,
#[cfg(target_os = "macos")]
pub user_name: String,
#[cfg(target_os = "macos")]
pub user_handle: Vec<u8>,
#[cfg(target_os = "macos")]
pub record_identifier: Option<String>,
pub client_data_hash: Vec<u8>,
pub user_verification: UserVerification,
pub window_xy: Position,
#[cfg(not(target_os = "macos"))]
pub client_window_handle: Vec<u8>,
#[cfg(not(target_os = "macos"))]
pub context: String,
}
#[derive(uniffi::Record, Serialize, Deserialize)]
#[cfg_attr(target_os = "macos", derive(uniffi::Record))]
#[derive(Debug, 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 rp_id: String,
pub user_handle: Vec<u8>,
pub signature: Vec<u8>,
pub client_data_hash: Vec<u8>,
pub authenticator_data: Vec<u8>,
pub credential_id: Vec<u8>,
}
#[uniffi::export(with_foreign)]
#[cfg_attr(target_os = "macos", uniffi::export(with_foreign))]
pub trait PreparePasskeyAssertionCallback: Send + Sync {
fn on_complete(&self, credential: PasskeyAssertionResponse);
fn on_error(&self, error: BitwardenError);
@@ -56,3 +72,14 @@ impl Callback for Arc<dyn PreparePasskeyAssertionCallback> {
PreparePasskeyAssertionCallback::on_error(self.as_ref(), error);
}
}
#[cfg(not(target_os = "macos"))]
impl PreparePasskeyAssertionCallback for TimedCallback<PasskeyAssertionResponse> {
fn on_complete(&self, credential: PasskeyAssertionResponse) {
self.send(Ok(credential));
}
fn on_error(&self, error: BitwardenError) {
self.send(Err(error))
}
}

View File

@@ -1,35 +1,58 @@
#![cfg(target_os = "macos")]
#![allow(clippy::disallowed_macros)] // uniffi macros trip up clippy's evaluation
mod assertion;
mod lock_status;
mod registration;
mod util;
mod window_handle_query;
use std::{
collections::HashMap,
sync::{atomic::AtomicU32, Arc, Mutex, Once},
time::Instant,
error::Error,
fmt::Display,
sync::{
atomic::AtomicU32,
mpsc::{self, Receiver, RecvTimeoutError, Sender},
Arc, Mutex,
},
time::{Duration, Instant},
};
#[cfg(target_os = "macos")]
use std::sync::Once;
use futures::FutureExt;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tracing::{error, info};
#[cfg(target_os = "macos")]
use tracing_subscriber::{
filter::{EnvFilter, LevelFilter},
layer::SubscriberExt,
util::SubscriberInitExt,
};
uniffi::setup_scaffolding!();
use crate::{
lock_status::{GetLockStatusCallback, LockStatusRequest},
window_handle_query::{GetWindowHandleQueryCallback, WindowHandleQueryRequest},
};
mod assertion;
mod registration;
use assertion::{
PasskeyAssertionRequest, PasskeyAssertionWithoutUserInterfaceRequest,
pub use assertion::{
PasskeyAssertionRequest, PasskeyAssertionResponse, PasskeyAssertionWithoutUserInterfaceRequest,
PreparePasskeyAssertionCallback,
};
use registration::{PasskeyRegistrationRequest, PreparePasskeyRegistrationCallback};
pub use lock_status::LockStatusResponse;
pub use registration::{
PasskeyRegistrationRequest, PasskeyRegistrationResponse, PreparePasskeyRegistrationCallback,
};
pub use window_handle_query::WindowHandleQueryResponse;
#[cfg(target_os = "macos")]
uniffi::setup_scaffolding!();
#[cfg(target_os = "macos")]
static INIT: Once = Once::new();
#[derive(uniffi::Enum, Debug, Serialize, Deserialize)]
#[cfg_attr(target_os = "macos", derive(uniffi::Enum))]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum UserVerification {
Preferred,
@@ -37,18 +60,30 @@ pub enum UserVerification {
Discouraged,
}
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
#[cfg_attr(target_os = "macos", derive(uniffi::Record))]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Position {
pub x: i32,
pub y: i32,
}
#[derive(Debug, uniffi::Error, Serialize, Deserialize)]
#[cfg_attr(target_os = "macos", derive(uniffi::Error))]
#[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
@@ -57,16 +92,17 @@ trait Callback: Send + Sync {
fn error(&self, error: BitwardenError);
}
#[derive(uniffi::Enum, Debug)]
/// Store the connection status between the macOS credential provider extension
#[cfg_attr(target_os = "macos", derive(uniffi::Enum))]
#[derive(Debug)]
/// Store the connection status between the credential provider extension
/// and the desktop application's IPC server.
pub enum ConnectionStatus {
Connected,
Disconnected,
}
#[derive(uniffi::Object)]
pub struct MacOSProviderClient {
#[cfg_attr(target_os = "macos", derive(uniffi::Object))]
pub struct AutofillProviderClient {
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
@@ -81,7 +117,7 @@ pub struct MacOSProviderClient {
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Store native desktop status information to use for IPC communication
/// between the application and the macOS credential provider.
/// between the application and the credential provider.
pub struct NativeStatus {
key: String,
value: String,
@@ -91,12 +127,31 @@ pub struct NativeStatus {
// have a callback.
const NO_CALLBACK_INDICATOR: u32 = 0;
#[uniffi::export]
impl MacOSProviderClient {
// These methods are not currently needed in macOS and/or cannot be exported via FFI
impl AutofillProviderClient {
pub fn is_available() -> bool {
desktop_core::ipc::path("af").exists()
}
pub fn get_lock_status(&self, callback: Arc<dyn GetLockStatusCallback>) {
self.send_message(LockStatusRequest {}, Some(Box::new(callback)));
}
pub fn get_window_handle(&self, callback: Arc<dyn GetWindowHandleQueryCallback>) {
self.send_message(
WindowHandleQueryRequest::default(),
Some(Box::new(callback)),
);
}
}
#[cfg_attr(target_os = "macos", uniffi::export)]
impl AutofillProviderClient {
// FIXME: Remove unwraps! They panic and terminate the whole application.
#[allow(clippy::unwrap_used)]
#[uniffi::constructor]
#[cfg_attr(target_os = "macos", uniffi::constructor)]
pub fn connect() -> Self {
#[cfg(target_os = "macos")]
INIT.call_once(|| {
let filter = EnvFilter::builder()
// Everything logs at `INFO`
@@ -112,10 +167,12 @@ impl MacOSProviderClient {
.init();
});
tracing::debug!("Autofill provider attempting 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 = MacOSProviderClient {
let client = AutofillProviderClient {
to_server_send,
response_callbacks_counter: AtomicU32::new(1), /* Start at 1 since 0 is reserved for
* "no callback" scenarios */
@@ -244,7 +301,7 @@ enum SerializedMessage {
},
}
impl MacOSProviderClient {
impl AutofillProviderClient {
#[allow(clippy::unwrap_used)]
fn add_callback(&self, callback: Box<dyn Callback>) -> u32 {
let sequence_number = self
@@ -294,3 +351,94 @@ impl MacOSProviderClient {
}
}
}
#[derive(Debug)]
pub enum CallbackError {
Timeout,
Cancelled,
}
impl Display for CallbackError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Timeout => f.write_str("Callback timed out"),
Self::Cancelled => f.write_str("Callback cancelled"),
}
}
}
impl std::error::Error for CallbackError {}
pub struct TimedCallback<T> {
tx: Arc<Mutex<Option<Sender<Result<T, BitwardenError>>>>>,
rx: Arc<Mutex<Receiver<Result<T, BitwardenError>>>>,
}
impl<T: Send + 'static> TimedCallback<T> {
pub fn new() -> Self {
let (tx, rx) = mpsc::channel();
Self {
tx: Arc::new(Mutex::new(Some(tx))),
rx: Arc::new(Mutex::new(rx)),
}
}
pub fn wait_for_response(
&self,
timeout: Duration,
cancellation_token: Option<Receiver<()>>,
) -> Result<Result<T, BitwardenError>, CallbackError> {
let (tx, rx) = mpsc::channel();
if let Some(cancellation_token) = cancellation_token {
let tx2 = tx.clone();
let cancellation_token = Mutex::new(cancellation_token);
std::thread::spawn(move || {
if let Ok(()) = cancellation_token.lock().unwrap().recv_timeout(timeout) {
tracing::debug!("Forwarding cancellation");
_ = tx2.send(Err(CallbackError::Cancelled));
}
});
}
let response_rx = self.rx.clone();
std::thread::spawn(move || {
if let Ok(response) = response_rx.lock().unwrap().recv_timeout(timeout) {
_ = tx.send(Ok(response));
}
});
match rx.recv_timeout(timeout) {
Ok(Ok(response)) => Ok(response),
Ok(err @ Err(CallbackError::Cancelled)) => {
tracing::debug!("Received cancellation, dropping.");
err
}
Ok(err @ Err(CallbackError::Timeout)) => {
tracing::debug!("Request timed out, dropping.");
err
}
Err(RecvTimeoutError::Timeout) => Err(CallbackError::Timeout),
Err(_) => Err(CallbackError::Cancelled),
}
}
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 TimedCallback<PasskeyRegistrationResponse> {
fn on_complete(&self, credential: PasskeyRegistrationResponse) {
self.send(Ok(credential));
}
fn on_error(&self, error: BitwardenError) {
self.send(Err(error))
}
}

View File

@@ -0,0 +1,41 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::{BitwardenError, Callback, TimedCallback};
#[derive(Debug, Serialize, Deserialize)]
pub(super) struct LockStatusRequest {}
#[derive(Debug, Deserialize)]
pub struct LockStatusResponse {
#[serde(rename = "isUnlocked")]
pub is_unlocked: bool,
}
impl Callback for Arc<dyn GetLockStatusCallback> {
fn complete(&self, response: serde_json::Value) -> Result<(), serde_json::Error> {
let response = serde_json::from_value(response)?;
self.as_ref().on_complete(response);
Ok(())
}
fn error(&self, error: BitwardenError) {
self.as_ref().on_error(error);
}
}
pub trait GetLockStatusCallback: Send + Sync {
fn on_complete(&self, response: LockStatusResponse);
fn on_error(&self, error: BitwardenError);
}
impl GetLockStatusCallback for TimedCallback<LockStatusResponse> {
fn on_complete(&self, response: LockStatusResponse) {
self.send(Ok(response));
}
fn on_error(&self, error: BitwardenError) {
self.send(Err(error))
}
}

View File

@@ -4,29 +4,35 @@ use serde::{Deserialize, Serialize};
use crate::{BitwardenError, Callback, Position, UserVerification};
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
#[cfg_attr(target_os = "macos", derive(uniffi::Record))]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyRegistrationRequest {
rp_id: String,
user_name: String,
user_handle: Vec<u8>,
client_data_hash: Vec<u8>,
user_verification: UserVerification,
supported_algorithms: Vec<i32>,
window_xy: Position,
excluded_credentials: Vec<Vec<u8>>,
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>>,
#[cfg(not(target_os = "macos"))]
pub client_window_handle: Vec<u8>,
#[cfg(not(target_os = "macos"))]
pub context: String,
}
#[derive(uniffi::Record, Serialize, Deserialize)]
#[cfg_attr(target_os = "macos", derive(uniffi::Record))]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyRegistrationResponse {
rp_id: String,
client_data_hash: Vec<u8>,
credential_id: Vec<u8>,
attestation_object: Vec<u8>,
pub rp_id: String,
pub client_data_hash: Vec<u8>,
pub credential_id: Vec<u8>,
pub attestation_object: Vec<u8>,
}
#[uniffi::export(with_foreign)]
#[cfg_attr(target_os = "macos", uniffi::export(with_foreign))]
pub trait PreparePasskeyRegistrationCallback: Send + Sync {
fn on_complete(&self, credential: PasskeyRegistrationResponse);
fn on_error(&self, error: BitwardenError);

View File

@@ -0,0 +1,24 @@
use serde::{de::Visitor, Deserializer};
pub(crate) fn deserialize_b64<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<u8>, D::Error> {
deserializer.deserialize_str(Base64Visitor {})
}
struct Base64Visitor;
impl<'de> Visitor<'de> for Base64Visitor {
type Value = Vec<u8>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("A valid base64 string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
use base64::{engine::general_purpose::STANDARD, Engine as _};
STANDARD.decode(v).map_err(|err| E::custom(err))
}
}

View File

@@ -0,0 +1,47 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::{BitwardenError, Callback, TimedCallback};
#[derive(Debug, Default, Serialize, Deserialize)]
pub(super) struct WindowHandleQueryRequest {
#[serde(rename = "windowHandle")]
window_handle: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WindowHandleQueryResponse {
pub is_visible: bool,
pub is_focused: bool,
#[serde(deserialize_with = "crate::util::deserialize_b64")]
pub handle: Vec<u8>,
}
impl Callback for Arc<dyn GetWindowHandleQueryCallback> {
fn complete(&self, response: serde_json::Value) -> Result<(), serde_json::Error> {
let response = serde_json::from_value(response)?;
self.as_ref().on_complete(response);
Ok(())
}
fn error(&self, error: BitwardenError) {
self.as_ref().on_error(error);
}
}
pub trait GetWindowHandleQueryCallback: Send + Sync {
fn on_complete(&self, response: WindowHandleQueryResponse);
fn on_error(&self, error: BitwardenError);
}
impl GetWindowHandleQueryCallback for TimedCallback<WindowHandleQueryResponse> {
fn on_complete(&self, response: WindowHandleQueryResponse) {
self.send(Ok(response));
}
fn on_error(&self, error: BitwardenError) {
self.send(Err(error))
}
}

View File

@@ -1,3 +1,9 @@
#[cfg(target_os = "macos")]
fn main() {
uniffi::uniffi_bindgen_main()
}
#[cfg(not(target_os = "macos"))]
fn main() {
unimplemented!("uniffi-bindgen is not enabled on this target.");
}