mirror of
https://github.com/bitwarden/browser
synced 2025-12-21 10:43:35 +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:
@@ -545,12 +545,256 @@ pub mod ipc {
|
||||
|
||||
#[napi]
|
||||
pub mod autofill {
|
||||
use desktop_core::ipc::server::{Message, MessageType};
|
||||
use napi::threadsafe_function::{
|
||||
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
#[napi]
|
||||
pub async fn run_command(value: String) -> napi::Result<String> {
|
||||
desktop_core::autofill::run_command(value)
|
||||
.await
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde:: Deserialize)]
|
||||
pub enum BitwardenError {
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
#[napi(string_enum)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum UserVerification {
|
||||
#[napi(value = "preferred")]
|
||||
Preferred,
|
||||
#[napi(value = "required")]
|
||||
Required,
|
||||
#[napi(value = "discouraged")]
|
||||
Discouraged,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(bound = "T: Serialize + DeserializeOwned")]
|
||||
pub struct PasskeyMessage<T: Serialize + DeserializeOwned> {
|
||||
pub sequence_number: u32,
|
||||
pub value: Result<T, BitwardenError>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[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>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(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>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionRequest {
|
||||
pub rp_id: String,
|
||||
pub credential_id: Vec<u8>,
|
||||
pub user_name: String,
|
||||
pub user_handle: Vec<u8>,
|
||||
pub record_identifier: Option<String>,
|
||||
pub client_data_hash: Vec<u8>,
|
||||
pub user_verification: UserVerification,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionResponse {
|
||||
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>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub struct IpcServer {
|
||||
server: desktop_core::ipc::server::Server,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl IpcServer {
|
||||
/// Create and start the IPC server without blocking.
|
||||
///
|
||||
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
|
||||
/// @param callback This function will be called whenever a message is received from a client.
|
||||
#[napi(factory)]
|
||||
pub async fn listen(
|
||||
name: String,
|
||||
// Ideally we'd have a single callback that has an enum containing the request values,
|
||||
// but NAPI doesn't support that just yet
|
||||
#[napi(
|
||||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void"
|
||||
)]
|
||||
registration_callback: ThreadsafeFunction<
|
||||
(u32, u32, PasskeyRegistrationRequest),
|
||||
ErrorStrategy::CalleeHandled,
|
||||
>,
|
||||
#[napi(
|
||||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void"
|
||||
)]
|
||||
assertion_callback: ThreadsafeFunction<
|
||||
(u32, u32, PasskeyAssertionRequest),
|
||||
ErrorStrategy::CalleeHandled,
|
||||
>,
|
||||
) -> napi::Result<Self> {
|
||||
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
|
||||
tokio::spawn(async move {
|
||||
while let Some(Message {
|
||||
client_id,
|
||||
kind,
|
||||
message,
|
||||
}) = recv.recv().await
|
||||
{
|
||||
match kind {
|
||||
// TODO: We're ignoring the connection and disconnection messages for now
|
||||
MessageType::Connected | MessageType::Disconnected => continue,
|
||||
MessageType::Message => {
|
||||
let Some(message) = message else {
|
||||
println!("[ERROR] Message is empty");
|
||||
continue;
|
||||
};
|
||||
|
||||
match serde_json::from_str::<PasskeyMessage<PasskeyAssertionRequest>>(
|
||||
&message,
|
||||
) {
|
||||
Ok(msg) => {
|
||||
let value = msg
|
||||
.value
|
||||
.map(|value| (client_id, msg.sequence_number, value))
|
||||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||||
|
||||
assertion_callback
|
||||
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[ERROR] Error deserializing message1: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
match serde_json::from_str::<PasskeyMessage<PasskeyRegistrationRequest>>(
|
||||
&message,
|
||||
) {
|
||||
Ok(msg) => {
|
||||
let value = msg
|
||||
.value
|
||||
.map(|value| (client_id, msg.sequence_number, value))
|
||||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||||
registration_callback
|
||||
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[ERROR] Error deserializing message2: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
println!("[ERROR] Received an unknown message2: {message:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let path = desktop_core::ipc::path(&name);
|
||||
|
||||
let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| {
|
||||
napi::Error::from_reason(format!(
|
||||
"Error listening to server - Path: {path:?} - Error: {e} - {e:?}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(IpcServer { server })
|
||||
}
|
||||
|
||||
/// Return the path to the IPC server.
|
||||
#[napi]
|
||||
pub fn get_path(&self) -> String {
|
||||
self.server.path.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
/// Stop the IPC server.
|
||||
#[napi]
|
||||
pub fn stop(&self) -> napi::Result<()> {
|
||||
self.server.stop();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn complete_registration(
|
||||
&self,
|
||||
client_id: u32,
|
||||
sequence_number: u32,
|
||||
response: PasskeyRegistrationResponse,
|
||||
) -> napi::Result<u32> {
|
||||
let message = PasskeyMessage {
|
||||
sequence_number,
|
||||
value: Ok(response),
|
||||
};
|
||||
self.send(client_id, serde_json::to_string(&message).unwrap())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn complete_assertion(
|
||||
&self,
|
||||
client_id: u32,
|
||||
sequence_number: u32,
|
||||
response: PasskeyAssertionResponse,
|
||||
) -> napi::Result<u32> {
|
||||
let message = PasskeyMessage {
|
||||
sequence_number,
|
||||
value: Ok(response),
|
||||
};
|
||||
self.send(client_id, serde_json::to_string(&message).unwrap())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn complete_error(
|
||||
&self,
|
||||
client_id: u32,
|
||||
sequence_number: u32,
|
||||
error: String,
|
||||
) -> napi::Result<u32> {
|
||||
let message: PasskeyMessage<()> = PasskeyMessage {
|
||||
sequence_number,
|
||||
value: Err(BitwardenError::Internal(error)),
|
||||
};
|
||||
self.send(client_id, serde_json::to_string(&message).unwrap())
|
||||
}
|
||||
|
||||
// TODO: Add a way to send a message to a specific client?
|
||||
fn send(&self, _client_id: u32, message: String) -> napi::Result<u32> {
|
||||
self.server
|
||||
.send(message)
|
||||
.map_err(|e| {
|
||||
napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}"))
|
||||
})
|
||||
// NAPI doesn't support u64 or usize, so we need to convert to u32
|
||||
.map(|u| u32::try_from(u).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
|
||||
Reference in New Issue
Block a user