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

[PM-9473] Add messaging for macOS passkey extension and desktop (#10768)

* Add messaging for macos passkey provider

* fix: credential id conversion

* Make build.sh executable

Co-authored-by: Colton Hurst <colton@coltonhurst.com>

* chore: add TODO

---------

Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
This commit is contained in:
Daniel García
2024-12-19 09:00:21 +01:00
committed by GitHub
parent 456046e095
commit 51f6594d4b
37 changed files with 1935 additions and 149 deletions

View File

@@ -0,0 +1 @@
BitwardenMacosProviderFFI.xcframework

View File

@@ -0,0 +1,30 @@
[package]
name = "macos_provider"
license = "GPL-3.0"
version = "0.0.0"
edition = "2021"
publish = false
[[bin]]
name = "uniffi-bindgen"
path = "uniffi-bindgen.rs"
[lib]
crate-type = ["staticlib", "cdylib"]
bench = false
[dependencies]
desktop_core = { path = "../core" }
futures = "=0.3.31"
log = "0.4.22"
serde = { version = "1.0.205", features = ["derive"] }
serde_json = "1.0.122"
tokio = { version = "1.39.2", features = ["sync"] }
tokio-util = "0.7.11"
uniffi = { version = "0.28.0", features = ["cli"] }
[target.'cfg(target_os = "macos")'.dependencies]
oslog = "0.2.0"
[build-dependencies]
uniffi = { version = "0.28.0", features = ["build"] }

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
cd "$(dirname "$0")"
rm -r BitwardenMacosProviderFFI.xcframework
rm -r tmp
mkdir -p ./tmp/target/universal-darwin/release/
cargo build --package macos_provider --target aarch64-apple-darwin --release
cargo build --package macos_provider --target x86_64-apple-darwin --release
# Create universal libraries
lipo -create ../target/aarch64-apple-darwin/release/libmacos_provider.a \
../target/x86_64-apple-darwin/release/libmacos_provider.a \
-output ./tmp/target/universal-darwin/release/libmacos_provider.a
# Generate swift bindings
cargo run --bin uniffi-bindgen --features uniffi/cli generate \
../target/aarch64-apple-darwin/release/libmacos_provider.dylib \
--library \
--language swift \
--no-format \
--out-dir tmp/bindings
# Move generated swift bindings
mkdir -p ../../macos/autofill-extension/
mv ./tmp/bindings/*.swift ../../macos/autofill-extension/
# Massage the generated files to fit xcframework
mkdir tmp/Headers
mv ./tmp/bindings/*.h ./tmp/Headers/
cat ./tmp/bindings/*.modulemap > ./tmp/Headers/module.modulemap
# Build xcframework
xcodebuild -create-xcframework \
-library ./tmp/target/universal-darwin/release/libmacos_provider.a \
-headers ./tmp/Headers \
-output ./BitwardenMacosProviderFFI.xcframework
# Cleanup temporary files
rm -r tmp

View File

@@ -0,0 +1,46 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::{BitwardenError, Callback, UserVerification};
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyAssertionRequest {
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,
}
#[derive(uniffi::Record, 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>,
}
#[uniffi::export(with_foreign)]
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,205 @@
#![cfg(target_os = "macos")]
use std::{
collections::HashMap,
sync::{atomic::AtomicU32, Arc, Mutex},
time::Instant,
};
use futures::FutureExt;
use log::{error, info};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
uniffi::setup_scaffolding!();
mod assertion;
mod registration;
use assertion::{PasskeyAssertionRequest, PreparePasskeyAssertionCallback};
use registration::{PasskeyRegistrationRequest, PreparePasskeyRegistrationCallback};
#[derive(uniffi::Enum, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum UserVerification {
Preferred,
Required,
Discouraged,
}
#[derive(Debug, uniffi::Error, Serialize, Deserialize)]
pub enum BitwardenError {
Internal(String),
}
// 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(uniffi::Object)]
pub struct MacOSProviderClient {
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)>>>,
}
#[uniffi::export]
impl MacOSProviderClient {
#[uniffi::constructor]
pub fn connect() -> Self {
let _ = oslog::OsLogger::new("com.bitwarden.desktop.autofill-extension")
.level_filter(log::LevelFilter::Trace)
.init();
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 {
to_server_send,
response_callbacks_counter: AtomicU32::new(0),
response_callbacks_queue: Arc::new(Mutex::new(HashMap::new())),
};
let path = desktop_core::ipc::path("autofill");
let queue = client.response_callbacks_queue.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");
}
Ok(SerializedMessage::Command(CommandMessage::Disconnected)) => {
info!("Disconnected from server");
}
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 deserializing message: {e}");
}
}
Err(e) => {
error!("Error processing message: {e:?}");
cb.error(e)
}
}
}
None => {
error!("No callback found for sequence number: {sequence_number}")
}
},
Err(e) => {
error!("Error deserializing message: {e}");
}
};
}
});
});
client
}
pub fn prepare_passkey_registration(
&self,
request: PasskeyRegistrationRequest,
callback: Arc<dyn PreparePasskeyRegistrationCallback>,
) {
self.send_message(request, Box::new(callback));
}
pub fn prepare_passkey_assertion(
&self,
request: PasskeyAssertionRequest,
callback: Arc<dyn PreparePasskeyAssertionCallback>,
) {
self.send_message(request, Box::new(callback));
}
}
#[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 MacOSProviderClient {
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()
.unwrap()
.insert(sequence_number, (callback, Instant::now()));
sequence_number
}
fn send_message(
&self,
message: impl Serialize + DeserializeOwned,
callback: Box<dyn Callback>,
) {
let sequence_number = self.add_callback(callback);
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 let Some((cb, _)) = self
.response_callbacks_queue
.lock()
.unwrap()
.remove(&sequence_number)
{
cb.error(BitwardenError::Internal(format!(
"Error sending message: {}",
e
)));
}
}
}
}

View File

@@ -0,0 +1,43 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::{BitwardenError, Callback, UserVerification};
#[derive(uniffi::Record, 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>,
}
#[derive(uniffi::Record, 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>,
}
#[uniffi::export(with_foreign)]
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

@@ -0,0 +1,3 @@
fn main() {
uniffi::uniffi_bindgen_main()
}

View File

@@ -0,0 +1,4 @@
[bindings.swift]
ffi_module_name = "BitwardenMacosProviderFFI"
module_name = "BitwardenMacosProvider"
generate_immutable_records = true