diff --git a/Cargo.lock b/Cargo.lock index 6024913f..114ed492 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -549,10 +549,12 @@ name = "bitwarden-ipc" version = "1.0.0" dependencies = [ "bitwarden-error", + "ciborium", "js-sys", "serde", "serde_json", - "thiserror 1.0.69", + "snow", + "thiserror 2.0.12", "tokio", "tsify-next", "wasm-bindgen", @@ -1254,6 +1256,7 @@ dependencies = [ "fiat-crypto", "rustc_version", "subtle", + "zeroize", ] [[package]] @@ -2358,7 +2361,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -3671,6 +3674,22 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "snow" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" +dependencies = [ + "aes-gcm", + "blake2", + "chacha20poly1305", + "curve25519-dalek", + "rand_core", + "rustc_version", + "sha2", + "subtle", +] + [[package]] name = "socket2" version = "0.5.8" diff --git a/crates/bitwarden-ipc/Cargo.toml b/crates/bitwarden-ipc/Cargo.toml index 44b5ab31..0fff02d1 100644 --- a/crates/bitwarden-ipc/Cargo.toml +++ b/crates/bitwarden-ipc/Cargo.toml @@ -19,9 +19,11 @@ wasm = [ [dependencies] bitwarden-error = { workspace = true } +ciborium = "0.2.2" js-sys = { workspace = true, optional = true } serde = { workspace = true } serde_json = { workspace = true } +snow = "0.9.6" thiserror = { workspace = true } tokio = { features = ["sync", "time"], workspace = true } tsify-next = { workspace = true, optional = true } diff --git a/crates/bitwarden-ipc/src/error.rs b/crates/bitwarden-ipc/src/error.rs index 16575ee6..a45465f1 100644 --- a/crates/bitwarden-ipc/src/error.rs +++ b/crates/bitwarden-ipc/src/error.rs @@ -7,6 +7,9 @@ pub enum SendError { #[error("Communication error: {0}")] Communication(Com), + + #[error("Handshake error")] + HandshakeError, } #[derive(Clone, Debug, Error, PartialEq, Eq)] @@ -19,6 +22,12 @@ pub enum ReceiveError { #[error("Communication error: {0}")] Communication(Com), + + #[error("Handshake error")] + HandshakeError, + + #[error("Decode Error")] + DecodeError, } #[derive(Clone, Debug, Error, PartialEq, Eq)] @@ -44,6 +53,9 @@ impl From> ReceiveError::Timeout => TypedReceiveError::Timeout, ReceiveError::Crypto(crypto) => TypedReceiveError::Crypto(crypto), ReceiveError::Communication(com) => TypedReceiveError::Communication(com), + // todo + ReceiveError::HandshakeError => TypedReceiveError::Timeout, + ReceiveError::DecodeError => TypedReceiveError::Timeout, } } } diff --git a/crates/bitwarden-ipc/src/ipc_client.rs b/crates/bitwarden-ipc/src/ipc_client.rs index e7fb13a1..a765ef31 100644 --- a/crates/bitwarden-ipc/src/ipc_client.rs +++ b/crates/bitwarden-ipc/src/ipc_client.rs @@ -102,8 +102,11 @@ mod tests { use crate::{ endpoint::Endpoint, traits::{ - tests::{TestCommunicationBackend, TestCommunicationBackendReceiveError}, - InMemorySessionRepository, NoEncryptionCryptoProvider, + tests::{ + TestCommunicationBackend, TestCommunicationBackendReceiveError, + TestTwoWayCommunicationBackend, + }, + InMemorySessionRepository, NoEncryptionCryptoProvider, NoiseCryptoProvider, }, }; @@ -369,4 +372,64 @@ mod tests { Err(TypedReceiveError::Typing(serde_json::Error { .. })) )); } + + #[tokio::test] + async fn communication_provider_ping_pong() { + let (sender_communication_provider, receiver_communication_provider) = + TestTwoWayCommunicationBackend::new(); + + let a = tokio::spawn(async move { + let receiver_crypto_provider = NoiseCryptoProvider; + let receiver_session_map = InMemorySessionRepository::new(HashMap::new()); + let receiver_client = IpcClient::new( + receiver_crypto_provider, + receiver_communication_provider.clone(), + receiver_session_map, + ); + + for i in 0..10 { + let recv_message = receiver_client.receive(None, None).await.unwrap(); + println!( + "A: Received Message {:?}", + String::from_utf8(recv_message.payload.clone()) + ); + let message = OutgoingMessage { + payload: format!("Hello, world! {}", i).as_bytes().to_vec(), + destination: Endpoint::BrowserBackground, + topic: None, + }; + println!("A: Sending Message {:?}", message); + receiver_client.send(message.clone()).await.unwrap(); + } + }); + + let b = tokio::spawn(async move { + let sender_crypto_provider = NoiseCryptoProvider; + let sender_session_map = InMemorySessionRepository::new(HashMap::new()); + let sender_client = IpcClient::new( + sender_crypto_provider, + sender_communication_provider.clone(), + sender_session_map, + ); + + for i in 0..10 { + let message = OutgoingMessage { + payload: format!("Hello, world! {}", i).as_bytes().to_vec(), + destination: Endpoint::BrowserBackground, + topic: None, + }; + println!("B: Sending Message {:?}", message); + sender_client.send(message.clone()).await.unwrap(); + + let recv_message = sender_client.receive(None, None).await.unwrap(); + println!( + "B: Received Message {:?}", + String::from_utf8(recv_message.payload.clone()) + ); + assert_eq!(recv_message.payload, message.payload); + } + }); + + let _ = tokio::join!(a, b); + } } diff --git a/crates/bitwarden-ipc/src/traits/communication_backend.rs b/crates/bitwarden-ipc/src/traits/communication_backend.rs index 7e3b721e..c4e5098c 100644 --- a/crates/bitwarden-ipc/src/traits/communication_backend.rs +++ b/crates/bitwarden-ipc/src/traits/communication_backend.rs @@ -23,10 +23,13 @@ pub trait CommunicationBackend { } #[cfg(test)] pub mod tests { - use std::{collections::VecDeque, rc::Rc}; + use std::{collections::VecDeque, rc::Rc, sync::Arc}; use thiserror::Error; - use tokio::sync::RwLock; + use tokio::sync::{ + mpsc::{self, Receiver, Sender}, + Mutex, RwLock, + }; use super::*; @@ -80,4 +83,47 @@ pub mod tests { } } } + + #[derive(Debug, Clone)] + pub struct TestTwoWayCommunicationBackend { + outgoing: Sender, + incoming: Arc>>, + } + + impl TestTwoWayCommunicationBackend { + pub fn new() -> (Self, Self) { + let (outgoing0, incoming0) = mpsc::channel(10); + let (outgoing1, incoming1) = mpsc::channel(10); + let one = TestTwoWayCommunicationBackend { + outgoing: outgoing0, + incoming: Arc::new(Mutex::new(incoming1)), + }; + let two = TestTwoWayCommunicationBackend { + outgoing: outgoing1, + incoming: Arc::new(Mutex::new(incoming0)), + }; + (one, two) + } + } + + impl CommunicationBackend for TestTwoWayCommunicationBackend { + type SendError = (); + type ReceiveError = TestCommunicationBackendReceiveError; + + async fn send(&self, message: OutgoingMessage) -> Result<(), Self::SendError> { + self.outgoing.send(message).await.unwrap(); + Ok(()) + } + + async fn receive(&self) -> Result { + let mut receiver = self.incoming.lock().await; + let message = receiver.recv().await.unwrap(); + Ok(IncomingMessage { + payload: message.payload, + destination: message.destination, + source: crate::endpoint::Endpoint::DesktopRenderer, + topic: None, + }) + } + } } diff --git a/crates/bitwarden-ipc/src/traits/mod.rs b/crates/bitwarden-ipc/src/traits/mod.rs index 73a15a6a..628fb3ea 100644 --- a/crates/bitwarden-ipc/src/traits/mod.rs +++ b/crates/bitwarden-ipc/src/traits/mod.rs @@ -1,9 +1,12 @@ mod communication_backend; mod crypto_provider; +mod noise_crypto_provider; mod session_repository; #[cfg(test)] pub use communication_backend::tests; pub use communication_backend::CommunicationBackend; pub use crypto_provider::{CryptoProvider, NoEncryptionCryptoProvider}; +#[cfg(test)] +pub use noise_crypto_provider::NoiseCryptoProvider; pub use session_repository::{InMemorySessionRepository, SessionRepository}; diff --git a/crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs b/crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs new file mode 100644 index 00000000..5f7b64f9 --- /dev/null +++ b/crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs @@ -0,0 +1,333 @@ +use core::panic; +use std::{ + sync::{Arc, Mutex}, + vec, +}; + +use serde::{Deserialize, Serialize}; +use snow::TransportState; + +use super::{CommunicationBackend, CryptoProvider, SessionRepository}; +use crate::{ + error::{ReceiveError, SendError}, + message::{IncomingMessage, OutgoingMessage}, +}; + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +enum BitwardenCryptoProtocolIdentifier { + Noise, +} + +/// The Bitwarden IPC protocol is can have different crypto protocols. +/// Currently there is exactly one - Noise - implemented. +#[derive(Clone, Debug, Deserialize, Serialize)] +struct BitwardenIpcCryptoProtocolFrame { + protocol_identifier: BitwardenCryptoProtocolIdentifier, + protocol_frame: Vec, +} + +impl BitwardenIpcCryptoProtocolFrame { + fn as_cbor(&self) -> Vec { + let mut buffer = Vec::new(); + #[allow(clippy::unwrap_used)] + ciborium::into_writer(self, &mut buffer).unwrap(); + buffer + } + + fn from_cbor(buffer: &[u8]) -> Result { + ciborium::from_reader(buffer).map_err(|_| ()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum BitwardenNoiseFrame { + HandshakeStart { + ciphersuite: String, + payload: Vec, + }, + HandshakeFinish { + payload: Vec, + }, + Payload { + payload: Vec, + }, +} + +impl BitwardenNoiseFrame { + fn as_cbor(&self) -> Vec { + let mut buffer = Vec::new(); + #[allow(clippy::unwrap_used)] + ciborium::into_writer(self, &mut buffer).unwrap(); + buffer + } + + fn from_cbor(buffer: &[u8]) -> Result { + ciborium::from_reader(buffer).map_err(|_| ()) + } + + fn to_crypto_protocol_frame(&self) -> BitwardenIpcCryptoProtocolFrame { + BitwardenIpcCryptoProtocolFrame { + protocol_identifier: BitwardenCryptoProtocolIdentifier::Noise, + protocol_frame: self.as_cbor(), + } + } +} + +pub struct NoiseCryptoProvider; +#[derive(Clone, Debug)] +pub struct NoiseCryptoProviderState { + state: Arc>>, +} + +impl CryptoProvider for NoiseCryptoProvider +where + Com: CommunicationBackend, + Ses: SessionRepository, +{ + type Session = NoiseCryptoProviderState; + type SendError = Com::SendError; + type ReceiveError = Com::ReceiveError; + + async fn send( + &self, + communication: &Com, + sessions: &Ses, + message: OutgoingMessage, + ) -> Result<(), SendError> { + let Ok(crypto_state_opt) = sessions.get(message.destination).await else { + panic!("Session not found"); + }; + let crypto_state = match crypto_state_opt { + Some(state) => state, + None => { + let new_state = NoiseCryptoProviderState { + state: Arc::new(Mutex::new(None)), + }; + // todo + sessions + .save(message.destination, new_state.clone()) + .await + .map_err(|_| SendError::HandshakeError)?; + new_state + } + }; + + // Session is not established yet. Establish it. + #[allow(clippy::unwrap_used)] + if crypto_state.state.lock().unwrap().is_none() { + let cipher_suite = "Noise_NN_25519_ChaChaPoly_BLAKE2s"; + let mut initiator = snow::Builder::new( + cipher_suite + .parse() + .map_err(|_| SendError::HandshakeError)?, + ) + .build_initiator() + .unwrap(); + + // Send Handshake One + let handshake_start_message = OutgoingMessage { + payload: BitwardenNoiseFrame::HandshakeStart { + ciphersuite: cipher_suite.to_string(), + payload: { + let mut buffer = vec![0u8; 65536]; + let res = initiator + .write_message(&[], &mut buffer) + .map_err(|_| SendError::HandshakeError)?; + buffer[..res].to_vec() + }, + } + .to_crypto_protocol_frame() + .as_cbor(), + destination: message.destination, + topic: None, + }; + communication + .send(handshake_start_message) + .await + .map_err(SendError::Communication)?; + + // Receive Handshake Two + let message = communication + .receive() + .await + .map_err(|_| SendError::HandshakeError)?; + let frame = BitwardenIpcCryptoProtocolFrame::from_cbor(&message.payload) + .map_err(|_| SendError::HandshakeError)?; + let handshake_finish_frame = + BitwardenNoiseFrame::from_cbor(frame.protocol_frame.as_slice()) + .map_err(|_| SendError::HandshakeError)?; + let BitwardenNoiseFrame::HandshakeFinish { payload } = handshake_finish_frame else { + panic!("Expected Handshake Two"); + }; + initiator + .read_message(&payload, &mut Vec::new()) + .map_err(|_| SendError::HandshakeError)?; + + let transport_state = initiator + .into_transport_mode() + .map_err(|_| SendError::HandshakeError)?; + let mut state = crypto_state + .state + .lock() + .map_err(|_| SendError::HandshakeError)?; + *state = Some(transport_state); + } + + // Send the payload + let payload_message = OutgoingMessage { + payload: BitwardenNoiseFrame::Payload { + payload: { + #[allow(clippy::unwrap_used)] + let mut transport_state = crypto_state.state.lock().unwrap(); + // todo error type + let transport_state = + transport_state.as_mut().ok_or(SendError::HandshakeError)?; + let mut buf = vec![0u8; 65536]; + let len = transport_state + .write_message(message.payload.as_slice(), &mut buf) + .map_err(|_| SendError::HandshakeError)?; + buf = buf[..len].to_vec(); + println!("Send payload: {:?}", buf); + buf + }, + } + .to_crypto_protocol_frame() + .as_cbor(), + destination: message.destination, + topic: message.topic, + }; + communication + .send(payload_message) + .await + .map_err(SendError::Communication)?; + + Ok(()) + } + + async fn receive( + &self, + communication: &Com, + sessions: &Ses, + ) -> Result> { + let mut message = communication + .receive() + .await + .map_err(ReceiveError::Communication)?; + let Ok(crypto_state_opt) = sessions.get(message.destination).await else { + panic!("Session not found"); + }; + let crypto_state = match crypto_state_opt { + Some(state) => state, + None => { + let new_state = NoiseCryptoProviderState { + state: Arc::new(Mutex::new(None)), + }; + sessions + .save(message.destination, new_state.clone()) + .await + // todo + .map_err(|_| ReceiveError::HandshakeError)?; + new_state + } + }; + + let crypto_protocol_frame = BitwardenIpcCryptoProtocolFrame::from_cbor(&message.payload) + .map_err(|_| ReceiveError::DecodeError)?; + if crypto_protocol_frame.protocol_identifier != BitwardenCryptoProtocolIdentifier::Noise { + panic!("Invalid protocol identifier"); + } + + // Check if session is established + #[allow(clippy::unwrap_used)] + if crypto_state.state.lock().unwrap().is_none() { + let protocol_frame = + BitwardenNoiseFrame::from_cbor(crypto_protocol_frame.protocol_frame.as_slice()) + .map_err(|_| ReceiveError::DecodeError)?; + match protocol_frame { + BitwardenNoiseFrame::HandshakeStart { + ciphersuite, + payload, + } => { + let supported_ciphersuite = "Noise_NN_25519_ChaChaPoly_BLAKE2s"; + let mut responder = if ciphersuite == supported_ciphersuite { + snow::Builder::new( + supported_ciphersuite + .parse() + .map_err(|_| ReceiveError::HandshakeError)?, + ) + .build_responder() + .unwrap() + } else { + panic!("Invalid protocol params"); + }; + + responder + .read_message(payload.as_slice(), &mut Vec::new()) + .unwrap(); + + let handshake_finish_message = OutgoingMessage { + payload: BitwardenNoiseFrame::HandshakeFinish { + payload: { + let mut buffer = vec![0u8; 65536]; + let res = responder + .write_message(&[], &mut buffer) + .map_err(|_| ReceiveError::HandshakeError)?; + buffer[..res].to_vec() + }, + } + .to_crypto_protocol_frame() + .as_cbor(), + destination: message.destination, + topic: None, + }; + communication + .send(handshake_finish_message) + .await + .map_err(|_| ReceiveError::HandshakeError)?; + { + let mut transport_state = crypto_state.state.lock().unwrap(); + *transport_state = Some( + responder + .into_transport_mode() + .map_err(|_| ReceiveError::HandshakeError)?, + ); + } + + message = communication + .receive() + .await + .map_err(ReceiveError::Communication)?; + } + _ => { + panic!("Invalid protocol frame"); + } + } + } + // Session is established. Read the payload. + let crypto_protocol_frame = BitwardenIpcCryptoProtocolFrame::from_cbor(&message.payload) + .map_err(|_| ReceiveError::DecodeError)?; + let protocol_frame = + BitwardenNoiseFrame::from_cbor(crypto_protocol_frame.protocol_frame.as_slice()) + .map_err(|_| ReceiveError::DecodeError)?; + let BitwardenNoiseFrame::Payload { payload } = protocol_frame else { + panic!("Expected Payload"); + }; + + #[allow(clippy::unwrap_used)] + let mut transport_state = crypto_state.state.lock().unwrap(); + #[allow(clippy::unwrap_used)] + let transport_state = transport_state.as_mut().unwrap(); + Ok(IncomingMessage { + payload: { + let mut buf = vec![0u8; 65536]; + let len = transport_state + .read_message(payload.as_slice(), &mut buf) + .map_err(|_| ReceiveError::DecodeError)?; + buf[..len].to_vec() + }, + destination: message.destination, + source: message.source, + topic: message.topic, + }) + } +} diff --git a/crates/bitwarden-ipc/src/wasm/error.rs b/crates/bitwarden-ipc/src/wasm/error.rs index 44a9703a..3c56ccd4 100644 --- a/crates/bitwarden-ipc/src/wasm/error.rs +++ b/crates/bitwarden-ipc/src/wasm/error.rs @@ -33,6 +33,10 @@ impl From> for JsSendError { crypto: JsValue::UNDEFINED, communication: e, }, + SendError::HandshakeError => JsSendError { + crypto: JsValue::UNDEFINED, + communication: JsValue::UNDEFINED, + }, } } } @@ -55,6 +59,16 @@ impl From> for JsReceiveError { crypto: JsValue::UNDEFINED, communication: e, }, + ReceiveError::HandshakeError => JsReceiveError { + timeout: false, + crypto: JsValue::UNDEFINED, + communication: JsValue::UNDEFINED, + }, + ReceiveError::DecodeError => JsReceiveError { + timeout: false, + crypto: JsValue::UNDEFINED, + communication: JsValue::UNDEFINED, + }, } } }