Skip to content

Commit 45a2e8b

Browse files
coroiutrmartin4
andauthored
feat(IPC framework): [PM-18039] Complete initial IPC implementation
## 🎟️ Tracking <!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. --> ## 📔 Objective Client PR bitwarden/clients#13373 ## ⏰ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## 🦮 Reviewer guidelines <!-- Suggested interactions but feel free to use (or not) as you desire! --> - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes --------- Co-authored-by: Todd Martin <[email protected]> Co-authored-by: Todd Martin <[email protected]>
1 parent 66e6231 commit 45a2e8b

File tree

19 files changed

+558
-1
lines changed

19 files changed

+558
-1
lines changed

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ bitwarden-crypto = { path = "crates/bitwarden-crypto", version = "=1.0.0" }
2727
bitwarden-exporters = { path = "crates/bitwarden-exporters", version = "=1.0.0" }
2828
bitwarden-fido = { path = "crates/bitwarden-fido", version = "=1.0.0" }
2929
bitwarden-generators = { path = "crates/bitwarden-generators", version = "=1.0.0" }
30+
bitwarden-ipc = { path = "crates/bitwarden-ipc", version = "=1.0.0" }
3031
bitwarden-send = { path = "crates/bitwarden-send", version = "=1.0.0" }
3132
bitwarden-sm = { path = "bitwarden_license/bitwarden-sm", version = "=1.0.0" }
3233
bitwarden-ssh = { path = "crates/bitwarden-ssh", version = "=1.0.0" }

crates/bitwarden-ipc/Cargo.toml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[package]
2+
name = "bitwarden-ipc"
3+
version.workspace = true
4+
authors.workspace = true
5+
edition.workspace = true
6+
rust-version.workspace = true
7+
homepage.workspace = true
8+
repository.workspace = true
9+
license-file.workspace = true
10+
keywords.workspace = true
11+
12+
[features]
13+
wasm = [
14+
"dep:tsify-next",
15+
"dep:wasm-bindgen",
16+
"dep:wasm-bindgen-futures",
17+
"dep:js-sys"
18+
] # WASM support
19+
20+
[dependencies]
21+
bitwarden-error = { workspace = true }
22+
js-sys = { workspace = true, optional = true }
23+
serde = { workspace = true }
24+
thiserror = { workspace = true }
25+
tokio = { features = ["sync"], workspace = true }
26+
tsify-next = { workspace = true, optional = true }
27+
wasm-bindgen = { workspace = true, optional = true }
28+
wasm-bindgen-futures = { workspace = true, optional = true }
29+
30+
[lints]
31+
workspace = true

crates/bitwarden-ipc/src/endpoint.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use serde::{Deserialize, Serialize};
2+
#[cfg(feature = "wasm")]
3+
use {tsify_next::Tsify, wasm_bindgen::prelude::*};
4+
5+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
6+
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
7+
pub enum Endpoint {
8+
Web { id: i32 },
9+
BrowserForeground,
10+
BrowserBackground,
11+
DesktopRenderer,
12+
DesktopMain,
13+
}

crates/bitwarden-ipc/src/error.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use thiserror::Error;
2+
3+
#[derive(Clone, Debug, Error, PartialEq, Eq)]
4+
pub enum SendError<Crypto, Com> {
5+
#[error("Crypto error: {0}")]
6+
CryptoError(Crypto),
7+
8+
#[error("Communication error: {0}")]
9+
CommunicationError(Com),
10+
}
11+
12+
#[derive(Clone, Debug, Error, PartialEq, Eq)]
13+
pub enum ReceiveError<Crypto, Com> {
14+
#[error("Crypto error: {0}")]
15+
CryptoError(Crypto),
16+
17+
#[error("Communication error: {0}")]
18+
CommunicationError(Com),
19+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
use crate::{
2+
error::{ReceiveError, SendError},
3+
message::{IncomingMessage, OutgoingMessage},
4+
traits::{CommunicationBackend, CryptoProvider, SessionRepository},
5+
};
6+
7+
pub struct IpcClient<Crypto, Com, Ses>
8+
where
9+
Crypto: CryptoProvider<Com, Ses>,
10+
Com: CommunicationBackend,
11+
Ses: SessionRepository<Session = Crypto::Session>,
12+
{
13+
crypto: Crypto,
14+
communication: Com,
15+
sessions: Ses,
16+
}
17+
18+
impl<Crypto, Com, Ses> IpcClient<Crypto, Com, Ses>
19+
where
20+
Crypto: CryptoProvider<Com, Ses>,
21+
Com: CommunicationBackend,
22+
Ses: SessionRepository<Session = Crypto::Session>,
23+
{
24+
pub fn new(crypto: Crypto, communication: Com, sessions: Ses) -> Self {
25+
Self {
26+
crypto,
27+
communication,
28+
sessions,
29+
}
30+
}
31+
32+
pub async fn send(
33+
&self,
34+
message: OutgoingMessage,
35+
) -> Result<(), SendError<Crypto::SendError, Com::SendError>> {
36+
self.crypto
37+
.send(&self.communication, &self.sessions, message)
38+
.await
39+
}
40+
41+
pub async fn receive(
42+
&self,
43+
) -> Result<IncomingMessage, ReceiveError<Crypto::ReceiveError, Com::ReceiveError>> {
44+
self.crypto
45+
.receive(&self.communication, &self.sessions)
46+
.await
47+
}
48+
}
49+
50+
#[cfg(test)]
51+
mod tests {
52+
use std::collections::HashMap;
53+
54+
use super::*;
55+
use crate::{endpoint::Endpoint, traits::InMemorySessionRepository};
56+
57+
struct TestCommunicationProvider;
58+
59+
impl CommunicationBackend for TestCommunicationProvider {
60+
type SendError = ();
61+
type ReceiveError = ();
62+
63+
async fn send(&self, _message: OutgoingMessage) -> Result<(), Self::SendError> {
64+
todo!()
65+
}
66+
67+
async fn receive(&self) -> Result<IncomingMessage, Self::ReceiveError> {
68+
todo!()
69+
}
70+
}
71+
72+
struct TestCryptoProvider {
73+
send_result: Result<(), SendError<String, ()>>,
74+
receive_result: Result<IncomingMessage, ReceiveError<String, ()>>,
75+
}
76+
77+
type TestSessionRepository = InMemorySessionRepository<String>;
78+
impl CryptoProvider<TestCommunicationProvider, TestSessionRepository> for TestCryptoProvider {
79+
type Session = String;
80+
type SendError = String;
81+
type ReceiveError = String;
82+
83+
async fn receive(
84+
&self,
85+
_communication: &TestCommunicationProvider,
86+
_sessions: &TestSessionRepository,
87+
) -> Result<IncomingMessage, ReceiveError<String, ()>> {
88+
self.receive_result.clone()
89+
}
90+
91+
async fn send(
92+
&self,
93+
_communication: &TestCommunicationProvider,
94+
_sessions: &TestSessionRepository,
95+
_message: OutgoingMessage,
96+
) -> Result<
97+
(),
98+
SendError<
99+
Self::SendError,
100+
<TestCommunicationProvider as CommunicationBackend>::SendError,
101+
>,
102+
> {
103+
self.send_result.clone()
104+
}
105+
}
106+
107+
#[tokio::test]
108+
async fn returns_send_error_when_crypto_provider_returns_error() {
109+
let message = OutgoingMessage {
110+
data: vec![],
111+
destination: Endpoint::BrowserBackground,
112+
};
113+
let crypto_provider = TestCryptoProvider {
114+
send_result: Err(SendError::CryptoError("Crypto error".to_string())),
115+
receive_result: Err(ReceiveError::CryptoError(
116+
"Should not have be called".to_string(),
117+
)),
118+
};
119+
let communication_provider = TestCommunicationProvider;
120+
let session_map = TestSessionRepository::new(HashMap::new());
121+
let client = IpcClient::new(crypto_provider, communication_provider, session_map);
122+
123+
let error = client.send(message).await.unwrap_err();
124+
125+
assert_eq!(error, SendError::CryptoError("Crypto error".to_string()));
126+
}
127+
128+
#[tokio::test]
129+
async fn returns_receive_error_when_crypto_provider_returns_error() {
130+
let crypto_provider = TestCryptoProvider {
131+
send_result: Ok(()),
132+
receive_result: Err(ReceiveError::CryptoError("Crypto error".to_string())),
133+
};
134+
let communication_provider = TestCommunicationProvider;
135+
let session_map = TestSessionRepository::new(HashMap::new());
136+
let client = IpcClient::new(crypto_provider, communication_provider, session_map);
137+
138+
let error = client.receive().await.unwrap_err();
139+
140+
assert_eq!(error, ReceiveError::CryptoError("Crypto error".to_string()));
141+
}
142+
}

crates/bitwarden-ipc/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
mod endpoint;
2+
mod error;
3+
mod ipc_client;
4+
mod message;
5+
mod traits;
6+
7+
// Re-export types to make sure wasm_bindgen picks them up
8+
#[cfg(feature = "wasm")]
9+
pub mod wasm;
10+
11+
pub use ipc_client::IpcClient;

crates/bitwarden-ipc/src/message.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use serde::{Deserialize, Serialize};
2+
#[cfg(feature = "wasm")]
3+
use {tsify_next::Tsify, wasm_bindgen::prelude::*};
4+
5+
use crate::endpoint::Endpoint;
6+
7+
#[derive(Clone, Debug, Deserialize, Serialize)]
8+
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
9+
pub struct OutgoingMessage {
10+
pub data: Vec<u8>,
11+
pub destination: Endpoint,
12+
}
13+
14+
#[derive(Clone, Debug, Deserialize, Serialize)]
15+
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
16+
pub struct IncomingMessage {
17+
pub data: Vec<u8>,
18+
pub destination: Endpoint,
19+
pub source: Endpoint,
20+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use crate::message::{IncomingMessage, OutgoingMessage};
2+
3+
/// This trait defines the interface that will be used to send and receive messages over IPC.
4+
/// It is up to the platform to implement this trait and any necessary thread synchronization and
5+
/// broadcasting.
6+
pub trait CommunicationBackend {
7+
type SendError;
8+
type ReceiveError;
9+
10+
/// Send a message to the destination specified in the message. This function may be called
11+
/// from any thread at any time. The implementation will handle any necessary synchronization.
12+
fn send(
13+
&self,
14+
message: OutgoingMessage,
15+
) -> impl std::future::Future<Output = Result<(), Self::SendError>>;
16+
17+
/// Receive a message. This function will block asynchronously until a message is received.
18+
/// Multiple calls to this function may be made from different threads, in which case all
19+
/// threads will receive the same message.
20+
fn receive(
21+
&self,
22+
) -> impl std::future::Future<Output = Result<IncomingMessage, Self::ReceiveError>>;
23+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use super::{CommunicationBackend, SessionRepository};
2+
use crate::{
3+
error::{ReceiveError, SendError},
4+
message::{IncomingMessage, OutgoingMessage},
5+
};
6+
7+
pub trait CryptoProvider<Com, Ses>
8+
where
9+
Com: CommunicationBackend,
10+
Ses: SessionRepository<Session = Self::Session>,
11+
{
12+
type Session;
13+
type SendError;
14+
type ReceiveError;
15+
16+
fn send(
17+
&self,
18+
communication: &Com,
19+
sessions: &Ses,
20+
message: OutgoingMessage,
21+
) -> impl std::future::Future<Output = Result<(), SendError<Self::SendError, Com::SendError>>>;
22+
fn receive(
23+
&self,
24+
communication: &Com,
25+
sessions: &Ses,
26+
) -> impl std::future::Future<
27+
Output = Result<IncomingMessage, ReceiveError<Self::ReceiveError, Com::ReceiveError>>,
28+
>;
29+
}
30+
31+
pub struct NoEncryptionCryptoProvider;
32+
33+
impl<Com, Ses> CryptoProvider<Com, Ses> for NoEncryptionCryptoProvider
34+
where
35+
Com: CommunicationBackend,
36+
Ses: SessionRepository<Session = ()>,
37+
{
38+
type Session = ();
39+
type SendError = Com::SendError;
40+
type ReceiveError = Com::ReceiveError;
41+
42+
async fn send(
43+
&self,
44+
communication: &Com,
45+
_sessions: &Ses,
46+
message: OutgoingMessage,
47+
) -> Result<(), SendError<Self::SendError, Com::SendError>> {
48+
communication
49+
.send(message)
50+
.await
51+
.map_err(SendError::CommunicationError)
52+
}
53+
54+
async fn receive(
55+
&self,
56+
communication: &Com,
57+
_sessions: &Ses,
58+
) -> Result<IncomingMessage, ReceiveError<Self::ReceiveError, Com::ReceiveError>> {
59+
communication
60+
.receive()
61+
.await
62+
.map_err(ReceiveError::CommunicationError)
63+
}
64+
}

0 commit comments

Comments
 (0)