diff --git a/Cargo.lock b/Cargo.lock index 82d1c09b0..0272250a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -535,11 +535,15 @@ dependencies = [ "async-trait", "bitwarden-error", "bitwarden-threading", + "ciborium", "erased-serde", + "hex", "js-sys", "log", "serde", + "serde_bytes", "serde_json", + "snow", "thiserror 2.0.12", "tokio", "tsify-next", @@ -609,7 +613,7 @@ name = "bitwarden-state" version = "1.0.0" dependencies = [ "async-trait", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", ] @@ -3828,6 +3832,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 0.6.4", + "rustc_version", + "sha2", + "subtle", +] + [[package]] name = "socket2" version = "0.5.10" diff --git a/Cargo.toml b/Cargo.toml index 18668f7f6..62c2d6de9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ keywords = ["bitwarden"] # Define dependencies that are expected to be consistent across all crates [workspace.dependencies] -bitwarden = { path = "crates/bitwarden", version = "=1.0.0" } bitwarden-api-api = { path = "crates/bitwarden-api-api", version = "=1.0.0" } bitwarden-api-identity = { path = "crates/bitwarden-api-identity", version = "=1.0.0" } bitwarden-cli = { path = "crates/bitwarden-cli", version = "=1.0.0" } @@ -33,7 +32,6 @@ bitwarden-ipc = { path = "crates/bitwarden-ipc", version = "=1.0.0" } bitwarden-send = { path = "crates/bitwarden-send", version = "=1.0.0" } bitwarden-state = { path = "crates/bitwarden-state", version = "=1.0.0" } bitwarden-threading = { path = "crates/bitwarden-threading", version = "=1.0.0" } -bitwarden-sm = { path = "bitwarden_license/bitwarden-sm", version = "=1.0.0" } bitwarden-ssh = { path = "crates/bitwarden-ssh", version = "=1.0.0" } bitwarden-uuid = { path = "crates/bitwarden-uuid", version = "=1.0.0" } bitwarden-uuid-macro = { path = "crates/bitwarden-uuid-macro", version = "=1.0.0" } diff --git a/crates/bitwarden-ipc/Cargo.toml b/crates/bitwarden-ipc/Cargo.toml index a087db7b1..ce0e96dee 100644 --- a/crates/bitwarden-ipc/Cargo.toml +++ b/crates/bitwarden-ipc/Cargo.toml @@ -23,11 +23,15 @@ wasm = [ async-trait = { workspace = true } bitwarden-error = { workspace = true } bitwarden-threading = { workspace = true } +ciborium = "0.2.2" erased-serde = ">=0.4.6, <0.5" +hex = "0.4.3" js-sys = { workspace = true, optional = true } log = { workspace = true } serde = { workspace = true } +serde_bytes = "0.11.17" serde_json = { workspace = true } +snow = "0.9.6" thiserror = { workspace = true } tokio = { features = ["sync", "time", "rt"], workspace = true } tsify-next = { workspace = true, optional = true } diff --git a/crates/bitwarden-ipc/src/crypto_provider/mod.rs b/crates/bitwarden-ipc/src/crypto_provider/mod.rs new file mode 100644 index 000000000..71802e021 --- /dev/null +++ b/crates/bitwarden-ipc/src/crypto_provider/mod.rs @@ -0,0 +1 @@ +mod noise; diff --git a/crates/bitwarden-ipc/src/crypto_provider/noise/error.rs b/crates/bitwarden-ipc/src/crypto_provider/noise/error.rs new file mode 100644 index 000000000..3c66c0dd9 --- /dev/null +++ b/crates/bitwarden-ipc/src/crypto_provider/noise/error.rs @@ -0,0 +1,31 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub(super) enum HandshakeError { + #[error("Invalid cipher suite, {0}")] + InvalidCipherSuite(String), + #[error("Crypto initialization failed")] + CryptoInitializationFailed, + #[error("Invalid handshake start message")] + InvalidHandshakeStart, + #[error("Invalid handshake finish message")] + InvalidHandshakeFinish, + + #[error("Failed to send message")] + SendFailed, + #[error("Failed to receive message")] + ReceiveFailed, +} + +#[derive(Debug, Error)] +pub(super) enum PayloadError { + #[error("Crypto uninitialized")] + CryptoUninitialized, + #[error("Failed to parse payload")] + ParseFailed, + #[error("Decryption failed")] + DecryptionFailed, + + #[error("Failed to send payload")] + SendFailed, +} diff --git a/crates/bitwarden-ipc/src/crypto_provider/noise/messages.rs b/crates/bitwarden-ipc/src/crypto_provider/noise/messages.rs new file mode 100644 index 000000000..86e233e76 --- /dev/null +++ b/crates/bitwarden-ipc/src/crypto_provider/noise/messages.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; +use serde_bytes::ByteBuf; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(super) enum BitwardenNoiseFrame { + HandshakeStart { + ciphersuite: String, + payload: ByteBuf, + }, + HandshakeFinish { + payload: ByteBuf, + }, + Payload { + payload: ByteBuf, + }, +} + +#[allow(unused)] +impl BitwardenNoiseFrame { + pub(super) fn to_cbor(&self) -> Vec { + let mut buffer = Vec::new(); + ciborium::into_writer(self, &mut buffer).expect("Ciborium serialization should not fail"); + buffer + } + + pub(super) fn from_cbor(buffer: &[u8]) -> Result { + ciborium::from_reader(buffer).map_err(|_| ()) + } +} diff --git a/crates/bitwarden-ipc/src/crypto_provider/noise/mod.rs b/crates/bitwarden-ipc/src/crypto_provider/noise/mod.rs new file mode 100644 index 000000000..8f90b97a9 --- /dev/null +++ b/crates/bitwarden-ipc/src/crypto_provider/noise/mod.rs @@ -0,0 +1,3 @@ +mod error; +mod messages; +mod protocol; diff --git a/crates/bitwarden-ipc/src/crypto_provider/noise/protocol.rs b/crates/bitwarden-ipc/src/crypto_provider/noise/protocol.rs new file mode 100644 index 000000000..409e931a2 --- /dev/null +++ b/crates/bitwarden-ipc/src/crypto_provider/noise/protocol.rs @@ -0,0 +1,367 @@ +use std::{sync::Arc, vec}; + +use snow::TransportState; +use tokio::sync::Mutex; + +use crate::{ + crypto_provider::noise::{ + error::{HandshakeError, PayloadError}, + messages::BitwardenNoiseFrame, + }, + endpoint::Endpoint, + message::{IncomingMessage, OutgoingMessage}, + traits::{ + CommunicationBackend, CommunicationBackendReceiver, CryptoProvider, SessionRepository, + }, +}; + +#[allow(unused)] +const CIPHER_SUITE: &str = "Noise_NN_25519_ChaChaPoly_BLAKE2s"; + +#[allow(unused)] +struct NoiseCryptoProvider; +#[allow(unused)] +#[derive(Clone, Debug)] +struct NoiseCryptoProviderState { + state: Arc>>, +} + +impl CryptoProvider for NoiseCryptoProvider +where + Com: CommunicationBackend, + Ses: SessionRepository, +{ + type Session = NoiseCryptoProviderState; + type SendError = (); + type ReceiveError = (); + + async fn send( + &self, + communication: &Com, + sessions: &Ses, + message: crate::message::OutgoingMessage, + ) -> Result<(), Self::SendError> { + let crypto_state = sessions + .get(message.destination) + .await + .expect("Get session should not fail"); + + // Connect if no session exists + let crypto_state = if let Some(crypto_state) = crypto_state { + crypto_state + } else { + // Should failing to connect be an error? + let crypto_state = connect(communication, message.destination) + .await + .map_err(|_| ())?; + sessions + .save(message.destination, crypto_state.clone()) + .await + .expect("Save session should not fail"); + crypto_state + }; + + // Encrypt and send the message + if let Err(e) = send_payload(communication, &crypto_state, message).await { + log::error!("[IPC send] Failed to send message: {e:?}"); + } + + Ok(()) + } + + async fn receive( + &self, + receiver: &Com::Receiver, + communication: &Com, + sessions: &Ses, + ) -> Result { + loop { + // Decode the message. If receiving returns error, then this also returns error. + let message = receiver.receive().await.map_err(|_| ())?; + let payload = BitwardenNoiseFrame::from_cbor(&message.payload); + let Ok(payload) = payload else { + // Discard invalid messages + continue; + }; + + match payload { + BitwardenNoiseFrame::HandshakeStart { + ciphersuite, + payload, + } => { + // Do we need to stop if there is already a valid session? + + let state = incoming_handshake( + communication, + ciphersuite, + payload.to_vec(), + message.source, + ) + .await; + let Ok(state) = state else { + log::error!("[IPC receive] Handshake failed"); + continue; + }; + + sessions + .save(message.source, state) + .await + .expect("Save session should not fail"); + } + BitwardenNoiseFrame::Payload { payload } => { + let crypto_state = sessions + .get(message.source) + .await + .expect("Get session should not fail"); + let Some(crypto_state) = crypto_state else { + // If no session exists, we cannot decrypt the message. Do we need to + // re-init a handshake? + continue; + }; + + let Ok(decrypted_message) = + incoming_payload(&crypto_state, payload.to_vec()).await + else { + // If decryption fails, we cannot process the message. Do we need to log? + continue; + }; + + return Ok(IncomingMessage { + payload: decrypted_message, + destination: message.destination, + source: message.source, + topic: message.topic, + }); + } + BitwardenNoiseFrame::HandshakeFinish { payload: _ } => { + // Handshake finish is handled in `connect` + } + } + } + } +} + +async fn connect( + communication: &Com, + destination: Endpoint, +) -> Result { + let receiver = communication.subscribe().await; + + let mut initiator = snow::Builder::new( + CIPHER_SUITE + .parse() + .map_err(|_| HandshakeError::InvalidCipherSuite(CIPHER_SUITE.to_string()))?, + ) + .build_initiator() + .map_err(|_| HandshakeError::CryptoInitializationFailed)?; + + // Send handshake start message + 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) + .expect("Writing message to buffer should not fail"); + buffer[..res].to_vec().into() + }, + } + .to_cbor(), + destination, + topic: None, + }; + communication + .send(handshake_start_message) + .await + .map_err(|_| HandshakeError::SendFailed)?; + + // Get handshake finish message + let message = receiver + .receive() + .await + .map_err(|_| HandshakeError::ReceiveFailed)?; + let handshake_finish_frame = BitwardenNoiseFrame::from_cbor(&message.payload) + .map_err(|_| HandshakeError::InvalidHandshakeFinish)?; + let BitwardenNoiseFrame::HandshakeFinish { payload } = handshake_finish_frame else { + return Err(HandshakeError::InvalidHandshakeFinish); + }; + initiator + .read_message(&payload, &mut Vec::new()) + .map_err(|_| HandshakeError::InvalidHandshakeFinish)?; + + log::debug!( + "[Initiator] Handshake finished with hash: {}", + hex::encode(initiator.get_handshake_hash()) + ); + + // Setup state + let transport_state = initiator + .into_transport_mode() + .map_err(|_| HandshakeError::CryptoInitializationFailed)?; + Ok(NoiseCryptoProviderState { + state: Arc::new(Mutex::new(Some(transport_state))), + }) +} + +async fn incoming_handshake( + communication: &Com, + cipher_suite: String, + payload: Vec, + endpoint: Endpoint, +) -> Result { + if cipher_suite != CIPHER_SUITE { + return Err(HandshakeError::InvalidCipherSuite(cipher_suite)); + } + + let state = NoiseCryptoProviderState { + state: Arc::new(Mutex::new(None)), + }; + + let mut handshake_state = snow::Builder::new( + cipher_suite + .parse() + .map_err(|_| HandshakeError::InvalidCipherSuite(cipher_suite))?, + ) + .build_responder() + .map_err(|_| HandshakeError::CryptoInitializationFailed)?; + handshake_state + .read_message(&payload, &mut Vec::new()) + .map_err(|_| HandshakeError::InvalidHandshakeStart)?; + + log::debug!( + "[Responder] Handshake finished with hash: {}", + hex::encode(handshake_state.get_handshake_hash()) + ); + + let handshake_finish_message = OutgoingMessage { + payload: BitwardenNoiseFrame::HandshakeFinish { + payload: { + let mut buffer = vec![0u8; 65536]; + let res = handshake_state + .write_message(&[], &mut buffer) + .expect("Writing message to buffer should not fail"); + buffer[..res].to_vec().into() + }, + } + .to_cbor(), + destination: endpoint, + topic: None, + }; + communication + .send(handshake_finish_message) + .await + .map_err(|_| HandshakeError::SendFailed)?; + + let transport_state = handshake_state + .into_transport_mode() + .map_err(|_| HandshakeError::CryptoInitializationFailed)?; + state.state.lock().await.replace(transport_state); + Ok(state) +} + +async fn send_payload( + communication: &impl CommunicationBackend, + crypto_state: &NoiseCryptoProviderState, + message: OutgoingMessage, +) -> Result<(), PayloadError> { + let mut transport_state = crypto_state.state.lock().await; + let transport_state = transport_state + .as_mut() + .ok_or(PayloadError::CryptoUninitialized)?; + + let mut buffer = vec![0u8; 65536]; + let res = transport_state + .write_message(&message.payload, &mut buffer) + .expect("Writing message to buffer should not fail"); + communication + .send(OutgoingMessage { + payload: BitwardenNoiseFrame::Payload { + payload: buffer[..res].to_vec().into(), + } + .to_cbor(), + destination: message.destination, + topic: message.topic, + }) + .await + .map_err(|_| PayloadError::SendFailed) +} + +async fn incoming_payload( + state: &NoiseCryptoProviderState, + payload: Vec, +) -> Result, PayloadError> { + let mut transport_state = state.state.lock().await; + let transport_state = transport_state + .as_mut() + .ok_or(PayloadError::CryptoUninitialized)?; + let mut message = vec![0u8; 65536]; + let len = transport_state + .read_message(&payload, &mut message) + .map_err(|_| PayloadError::DecryptionFailed)?; + message.truncate(len); + Ok(message) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use crate::{ + crypto_provider::noise::protocol::NoiseCryptoProvider, + endpoint::Endpoint, + message::OutgoingMessage, + traits::{tests::TestTwoWayCommunicationBackend, InMemorySessionRepository}, + IpcClient, + }; + + #[tokio::test] + async fn ping_pong() { + let (provider_1, provider_2) = TestTwoWayCommunicationBackend::new(); + + let session_map_1 = InMemorySessionRepository::new(HashMap::new()); + let client_1 = IpcClient::new(NoiseCryptoProvider, provider_1, session_map_1); + client_1.start().await; + let mut recv_1 = client_1.subscribe(None).await.unwrap(); + + let session_map_2 = InMemorySessionRepository::new(HashMap::new()); + let client_2 = IpcClient::new(NoiseCryptoProvider, provider_2, session_map_2); + client_2.start().await; + let mut recv_2 = client_2.subscribe(None).await.unwrap(); + + let handle_1 = tokio::spawn(async move { + let mut val: u8 = 0; + for _ in 0..255 { + let message = OutgoingMessage { + payload: vec![val], + destination: Endpoint::DesktopMain, + topic: None, + }; + client_1.send(message).await.unwrap(); + let recv_message = recv_1.receive(None).await.unwrap(); + val = recv_message.payload[0] + 1; + } + }); + + let handle_2 = tokio::spawn(async move { + for _ in 0..255 { + let recv_message = recv_2.receive(None).await.unwrap(); + let val = recv_message.payload[0]; + if val == 255 { + break; + } + + client_2 + .send(OutgoingMessage { + payload: vec![val], + destination: Endpoint::DesktopMain, + topic: None, + }) + .await + .unwrap(); + } + }); + + let _ = tokio::join!(handle_1, handle_2); + } +} diff --git a/crates/bitwarden-ipc/src/lib.rs b/crates/bitwarden-ipc/src/lib.rs index 2139d277c..5e7a31ebc 100644 --- a/crates/bitwarden-ipc/src/lib.rs +++ b/crates/bitwarden-ipc/src/lib.rs @@ -1,6 +1,7 @@ #![doc = include_str!("../README.md")] mod constants; +mod crypto_provider; mod discover; mod endpoint; mod ipc_client; diff --git a/crates/bitwarden-ipc/src/traits/communication_backend.rs b/crates/bitwarden-ipc/src/traits/communication_backend.rs index 896bdd806..fb111db37 100644 --- a/crates/bitwarden-ipc/src/traits/communication_backend.rs +++ b/crates/bitwarden-ipc/src/traits/communication_backend.rs @@ -29,7 +29,7 @@ pub trait CommunicationBackend: Send + Sync + 'static { /// - Multiple concurrent receivers may be created. /// - All concurrent receivers will receive the same messages. /// - Multiple concurrent receivers and senders can coexist. - fn subscribe(&self) -> impl std::future::Future + Send + Sync; + fn subscribe(&self) -> impl std::future::Future + Send; } /// This trait defines the interface for receiving messages from the communication backend. @@ -50,7 +50,7 @@ pub trait CommunicationBackendReceiver: Send + Sync + 'static { /// to create one receiver per thread. fn receive( &self, - ) -> impl std::future::Future> + Send + Sync; + ) -> impl std::future::Future> + Send; } #[cfg(test)] @@ -58,8 +58,7 @@ pub mod tests { use std::sync::Arc; use tokio::sync::{ - broadcast::{self, Receiver, Sender}, - RwLock, + broadcast::{self, Receiver, Sender}, Mutex, RwLock, }; use super::*; @@ -141,4 +140,68 @@ pub mod tests { .expect("Failed to receive incoming message")) } } + + #[derive(Clone)] + pub struct TestTwoWayCommunicationBackend { + outgoing: broadcast::Sender, + receiver: TestTwoWayCommunicationBackendReceiver, + } + + #[derive(Clone)] + pub struct TestTwoWayCommunicationBackendReceiver { + incoming: Arc>>, + } + + impl CommunicationBackendReceiver for TestTwoWayCommunicationBackendReceiver { + type ReceiveError = (); + + async fn receive(&self) -> Result { + let mut incoming = self.incoming.lock().await; + let message = incoming.recv().await.unwrap(); + Ok(IncomingMessage { + payload: message.payload, + destination: message.destination, + source: crate::endpoint::Endpoint::DesktopMain, + topic: message.topic, + }) + } + } + + impl TestTwoWayCommunicationBackend { + pub fn new() -> (Self, Self) { + let (outgoing0, incoming0) = broadcast::channel(128); + let (outgoing1, incoming1) = broadcast::channel(128); + let one = TestTwoWayCommunicationBackend { + outgoing: outgoing0, + receiver: TestTwoWayCommunicationBackendReceiver { + incoming: Arc::new(Mutex::new(incoming1)), + }, + }; + let two = TestTwoWayCommunicationBackend { + outgoing: outgoing1, + receiver: TestTwoWayCommunicationBackendReceiver { + incoming: Arc::new(Mutex::new(incoming0)), + }, + }; + (one, two) + } + } + + impl CommunicationBackend for TestTwoWayCommunicationBackend { + type SendError = (); + type Receiver = TestTwoWayCommunicationBackendReceiver; + + async fn send(&self, message: OutgoingMessage) -> Result<(), Self::SendError> { + self.outgoing.send(message).unwrap(); + Ok(()) + } + + async fn subscribe(&self) -> Self::Receiver { + TestTwoWayCommunicationBackendReceiver { + incoming: Arc::new(Mutex::new( + self.receiver.incoming.lock().await.resubscribe(), + )), + } + } + } } diff --git a/crates/bitwarden-ipc/src/traits/crypto_provider.rs b/crates/bitwarden-ipc/src/traits/crypto_provider.rs index aefb0eb36..47aab921b 100644 --- a/crates/bitwarden-ipc/src/traits/crypto_provider.rs +++ b/crates/bitwarden-ipc/src/traits/crypto_provider.rs @@ -45,7 +45,7 @@ where receiver: &Com::Receiver, communication: &Com, sessions: &Ses, - ) -> impl std::future::Future> + Send + Sync; + ) -> impl std::future::Future> + Send; } pub struct NoEncryptionCryptoProvider; diff --git a/crates/bitwarden-ipc/src/traits/session_repository.rs b/crates/bitwarden-ipc/src/traits/session_repository.rs index 222a6bf7f..e748c2e1b 100644 --- a/crates/bitwarden-ipc/src/traits/session_repository.rs +++ b/crates/bitwarden-ipc/src/traits/session_repository.rs @@ -12,16 +12,16 @@ pub trait SessionRepository: Send + Sync + 'static { fn get( &self, destination: Endpoint, - ) -> impl std::future::Future, Self::GetError>>; + ) -> impl std::future::Future, Self::GetError>> + Send + Sync; fn save( &self, destination: Endpoint, session: Session, - ) -> impl std::future::Future>; + ) -> impl std::future::Future> + Send + Sync; fn remove( &self, destination: Endpoint, - ) -> impl std::future::Future>; + ) -> impl std::future::Future> + Send + Sync; } pub type InMemorySessionRepository = RwLock>;