Skip to content

Commit

Permalink
Create uniffi package and use it from the extension
Browse files Browse the repository at this point in the history
  • Loading branch information
dani-garcia committed Dec 6, 2024
1 parent 9fcc4f0 commit cb2f7c3
Show file tree
Hide file tree
Showing 15 changed files with 866 additions and 13 deletions.
483 changes: 482 additions & 1 deletion apps/desktop/desktop_native/Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/desktop/desktop_native/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[workspace]
resolver = "2"
members = ["napi", "core", "proxy"]
members = ["napi", "core", "proxy", "macos_provider"]
1 change: 1 addition & 0 deletions apps/desktop/desktop_native/macos_provider/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BitwardenMacosProviderFFI.xcframework
30 changes: 30 additions & 0 deletions apps/desktop/desktop_native/macos_provider/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
41 changes: 41 additions & 0 deletions apps/desktop/desktop_native/macos_provider/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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
176 changes: 176 additions & 0 deletions apps/desktop/desktop_native/macos_provider/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#![cfg(target_os = "macos")]

use std::{
collections::HashMap,
sync::{atomic::AtomicU32, Arc, Mutex},
};

use futures::FutureExt;
use log::{error, info, warn};
use registration::{PasskeyRegistrationRequest, PreparePasskeyRegistrationCallback};
use serde::{de::DeserializeOwned, Deserialize, Serialize};

uniffi::setup_scaffolding!();

mod registration;

#[derive(uniffi::Enum, Debug, Serialize, Deserialize)]
pub enum UserVerification {
Preferred,
Required,
Discouraged,
}

#[derive(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,
response_callbacks_queue: Arc<Mutex<HashMap<u32, Box<dyn Callback>>>>,
}

#[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::Connected) => {
info!("Connected to server");
}
Ok(SerializedMessage::Disconnected) => {
info!("Disconnected from server");
}
Ok(SerializedMessage::Message {
sequence_number,
value,
}) => match queue.lock().unwrap().remove(&sequence_number) {
Some(cb) => match value {
Ok(value) => {
if let Err(e) = cb.complete(value) {
error!("Error deserializing message: {}", e);
}
}
Err(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>,
) {
warn!("prepare_passkey_registration: {:?}", request);

self.send_message(request, Box::new(callback));
}
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "command")]
enum SerializedMessage {
Connected,
Disconnected,
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);

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
)));
}
}
}
}
41 changes: 41 additions & 0 deletions apps/desktop/desktop_native/macos_provider/src/registration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::sync::Arc;

use serde::{Deserialize, Serialize};

use crate::{BitwardenError, Callback, UserVerification};

#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
pub struct PasskeyRegistrationRequest {
relying_party_id: String,
user_name: String,
user_handle: Vec<u8>,

client_data_hash: Vec<u8>,
user_verification: UserVerification,
}

#[derive(uniffi::Record, Serialize, Deserialize)]
pub struct PasskeyRegistrationCredential {
relying_party: 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: PasskeyRegistrationCredential);
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);
}
}
3 changes: 3 additions & 0 deletions apps/desktop/desktop_native/macos_provider/uniffi-bindgen.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
uniffi::uniffi_bindgen_main()
}
4 changes: 4 additions & 0 deletions apps/desktop/desktop_native/macos_provider/uniffi.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[bindings.swift]
ffi_module_name = "BitwardenMacosProviderFFI"
module_name = "BitwardenMacosProvider"
generate_immutable_records = true
1 change: 1 addition & 0 deletions apps/desktop/macos/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BitwardenMacosProvider.swift
Loading

0 comments on commit cb2f7c3

Please sign in to comment.