Skip to content

[BEEEP/POC] Noise IPC CryptoProvider #219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking โ€œSign up for GitHubโ€, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/bitwarden-ipc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 }
12 changes: 12 additions & 0 deletions crates/bitwarden-ipc/src/error.rs
Original file line number Diff line number Diff line change
@@ -7,6 +7,9 @@

#[error("Communication error: {0}")]
Communication(Com),

#[error("Handshake error")]
HandshakeError,
}

#[derive(Clone, Debug, Error, PartialEq, Eq)]
@@ -19,6 +22,12 @@

#[error("Communication error: {0}")]
Communication(Com),

#[error("Handshake error")]
HandshakeError,

#[error("Decode Error")]
DecodeError,
}

#[derive(Clone, Debug, Error, PartialEq, Eq)]
@@ -44,6 +53,9 @@
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,

Check warning on line 58 in crates/bitwarden-ipc/src/error.rs

Codecov / codecov/patch

crates/bitwarden-ipc/src/error.rs#L57-L58

Added lines #L57 - L58 were not covered by tests
}
}
}
67 changes: 65 additions & 2 deletions crates/bitwarden-ipc/src/ipc_client.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
50 changes: 48 additions & 2 deletions crates/bitwarden-ipc/src/traits/communication_backend.rs
Original file line number Diff line number Diff line change
@@ -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<OutgoingMessage>,
incoming: Arc<Mutex<Receiver<OutgoingMessage>>>,
}

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<IncomingMessage, Self::ReceiveError> {
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,
})
}
}
}
3 changes: 3 additions & 0 deletions crates/bitwarden-ipc/src/traits/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
333 changes: 333 additions & 0 deletions crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
}

impl BitwardenIpcCryptoProtocolFrame {
fn as_cbor(&self) -> Vec<u8> {
let mut buffer = Vec::new();
#[allow(clippy::unwrap_used)]
ciborium::into_writer(self, &mut buffer).unwrap();
buffer
}

fn from_cbor(buffer: &[u8]) -> Result<Self, ()> {
ciborium::from_reader(buffer).map_err(|_| ())
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum BitwardenNoiseFrame {
HandshakeStart {
ciphersuite: String,
payload: Vec<u8>,
},
HandshakeFinish {
payload: Vec<u8>,
},
Payload {
payload: Vec<u8>,
},
}

impl BitwardenNoiseFrame {
fn as_cbor(&self) -> Vec<u8> {
let mut buffer = Vec::new();
#[allow(clippy::unwrap_used)]
ciborium::into_writer(self, &mut buffer).unwrap();
buffer
}

fn from_cbor(buffer: &[u8]) -> Result<Self, ()> {
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<Mutex<Option<TransportState>>>,
}

impl<Com, Ses> CryptoProvider<Com, Ses> for NoiseCryptoProvider
where
Com: CommunicationBackend,
Ses: SessionRepository<Session = NoiseCryptoProviderState>,
{
type Session = NoiseCryptoProviderState;
type SendError = Com::SendError;
type ReceiveError = Com::ReceiveError;

async fn send(
&self,
communication: &Com,
sessions: &Ses,
message: OutgoingMessage,
) -> Result<(), SendError<Self::SendError, Com::SendError>> {
let Ok(crypto_state_opt) = sessions.get(message.destination).await else {
panic!("Session not found");

Check warning on line 98 in crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs

Codecov / codecov/patch

crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs#L98

Added line #L98 was not covered by tests
};
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");

Check warning on line 160 in crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs

Codecov / codecov/patch

crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs#L160

Added line #L160 was not covered by tests
};
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<IncomingMessage, ReceiveError<Self::ReceiveError, Com::ReceiveError>> {
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");

Check warning on line 217 in crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs

Codecov / codecov/patch

crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs#L217

Added line #L217 was not covered by tests
};
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 warning on line 237 in crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs

Codecov / codecov/patch

crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs#L237

Added line #L237 was not covered by tests
}

// 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");

Check warning on line 261 in crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs

Codecov / codecov/patch

crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs#L261

Added line #L261 was not covered by tests
};

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");

Check warning on line 302 in crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs

Codecov / codecov/patch

crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs#L302

Added line #L302 was not covered by tests
}
}
}
// 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");

Check warning on line 313 in crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs

Codecov / codecov/patch

crates/bitwarden-ipc/src/traits/noise_crypto_provider.rs#L313

Added line #L313 was not covered by tests
};

#[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,
})
}
}
14 changes: 14 additions & 0 deletions crates/bitwarden-ipc/src/wasm/error.rs
Original file line number Diff line number Diff line change
@@ -33,6 +33,10 @@
crypto: JsValue::UNDEFINED,
communication: e,
},
SendError::HandshakeError => JsSendError {
crypto: JsValue::UNDEFINED,
communication: JsValue::UNDEFINED,
},

Check warning on line 39 in crates/bitwarden-ipc/src/wasm/error.rs

Codecov / codecov/patch

crates/bitwarden-ipc/src/wasm/error.rs#L36-L39

Added lines #L36 - L39 were not covered by tests
}
}
}
@@ -55,6 +59,16 @@
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,
},

Check warning on line 71 in crates/bitwarden-ipc/src/wasm/error.rs

Codecov / codecov/patch

crates/bitwarden-ipc/src/wasm/error.rs#L62-L71

Added lines #L62 - L71 were not covered by tests
}
}
}