bitwarden_ipc/crypto_provider/noise/
crypto_provider.rs1use std::{sync::LazyLock, time::Duration};
2
3use bitwarden_threading::time::timeout;
4use serde::{Deserialize, Serialize};
5use tracing::{debug, error, info, warn};
6
7use crate::{
8 crypto_provider::noise::{
9 handshake::{
10 CipherSuite, HandshakeFinishMessage, HandshakeInitiator, HandshakeResponder,
11 HandshakeStartMessage,
12 },
13 transport_state::{PersistentTransportState, TransportFrame},
14 },
15 error::{ErrorKind, IpcErrorKind},
16 message::{IncomingMessage, OutgoingMessage},
17 traits::{
18 CommunicationBackend, CommunicationBackendReceiver, CryptoProvider, SessionRepository,
19 },
20};
21
22pub struct NoiseCryptoProvider;
24
25#[derive(Debug)]
26pub enum NoiseCryptoProviderError {
27 HandshakeProtocol,
29 Timeout,
31 TransportUnreachable,
33 TransportSend { kind: ErrorKind },
36 TransportReceive { kind: ErrorKind },
39 DecryptionFailure,
41}
42
43impl IpcErrorKind for NoiseCryptoProviderError {
44 fn kind(&self) -> ErrorKind {
45 match self {
46 NoiseCryptoProviderError::HandshakeProtocol => ErrorKind::Other,
49 NoiseCryptoProviderError::Timeout => ErrorKind::Other,
51 NoiseCryptoProviderError::DecryptionFailure => ErrorKind::Other,
53 NoiseCryptoProviderError::TransportUnreachable => ErrorKind::Unreachable,
55 NoiseCryptoProviderError::TransportSend { kind }
57 | NoiseCryptoProviderError::TransportReceive { kind } => *kind,
58 }
59 }
60}
61
62fn transport_send_error<E: IpcErrorKind>(e: E) -> NoiseCryptoProviderError {
66 match e.kind() {
67 ErrorKind::Unreachable => NoiseCryptoProviderError::TransportUnreachable,
68 kind => NoiseCryptoProviderError::TransportSend { kind },
69 }
70}
71
72static CRYPTO_STATE_GUARD: LazyLock<tokio::sync::Mutex<()>> =
75 LazyLock::new(|| tokio::sync::Mutex::new(()));
76
77impl NoiseCryptoProvider {
78 async fn perform_handshake<Com, Ses>(
79 communication: &Com,
80 sessions: &Ses,
81 destination: crate::endpoint::Endpoint,
82 ) -> Result<(), NoiseCryptoProviderError>
83 where
84 Com: CommunicationBackend,
85 Ses: SessionRepository<NoiseCryptoProviderState>,
86 {
87 debug!("Starting noise handshake with {:?}", destination);
88
89 let mut initiator = HandshakeInitiator::new(&CipherSuite::default());
90 let message = initiator
91 .write_start_message()
92 .expect("Handshake start message should be buildable");
93 let receiver = communication.subscribe().await;
94
95 let handshake_frame = Frame::HandshakeStart(message);
96 communication
97 .send(OutgoingMessage {
98 payload: handshake_frame.to_cbor(),
99 destination: destination.clone(),
100 topic: None,
101 })
102 .await
103 .map_err(transport_send_error)?;
104
105 timeout(Duration::from_secs(HANDSHAKE_TIMEOUT_SECS), async {
107 loop {
108 let incoming = receiver
109 .receive()
110 .await
111 .map_err(|e| NoiseCryptoProviderError::TransportReceive { kind: e.kind() })?;
112
113 if incoming.source.to_endpoint() != destination {
115 continue;
116 }
117
118 let Ok(response_frame) = Frame::from_cbor(&incoming.payload) else {
120 return Err(NoiseCryptoProviderError::HandshakeProtocol);
121 };
122
123 if let Frame::HandshakeFinish(handshake_finish) = response_frame {
125 if initiator.read_response_message(&handshake_finish).is_err() {
126 error!("Failed to read handshake response message");
127 return Err(NoiseCryptoProviderError::HandshakeProtocol);
128 }
129 break;
130 }
131 }
132 Ok(())
133 })
134 .await
135 .map_err(|_| {
136 info!(
137 "Noise handshake with {:?} timed out after {} seconds",
138 destination, HANDSHAKE_TIMEOUT_SECS
139 );
140 NoiseCryptoProviderError::Timeout
141 })??;
144
145 let crypto_state = NoiseCryptoProviderState {
146 state: (&mut initiator).into(),
147 };
148 sessions
149 .save(destination.clone(), crypto_state)
150 .await
151 .expect("Save session should not fail");
152
153 info!(
154 "Noise handshake with {:?} completed, session established",
155 destination
156 );
157
158 Ok(())
159 }
160}
161
162const REHANDSHAKE_INTERVAL_SECS: u64 = 300;
165
166const HANDSHAKE_TIMEOUT_SECS: u64 = 2;
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct NoiseCryptoProviderState {
172 state: PersistentTransportState,
173}
174
175impl<Com, Ses> CryptoProvider<Com, Ses> for NoiseCryptoProvider
176where
177 Com: CommunicationBackend,
178 Ses: SessionRepository<NoiseCryptoProviderState>,
179{
180 type Session = NoiseCryptoProviderState;
181 type SendError = NoiseCryptoProviderError;
182 type ReceiveError = NoiseCryptoProviderError;
183
184 async fn send(
185 &self,
186 communication: &Com,
187 sessions: &Ses,
188 message: OutgoingMessage,
189 ) -> Result<(), Self::SendError> {
190 let _crypto_state_guard = CRYPTO_STATE_GUARD.lock().await;
194
195 let destination = message.destination.clone();
196
197 let crypto_state = sessions
198 .get(destination.clone())
199 .await
200 .expect("Get session should not fail");
201
202 let mut should_handshake = crypto_state.is_none();
203 if let Some(state) = crypto_state.as_ref()
204 && state.state.should_rehandshake(REHANDSHAKE_INTERVAL_SECS)
205 {
206 info!(
207 "Noise session with {:?} is older than {}s, re-handshaking",
208 destination, REHANDSHAKE_INTERVAL_SECS
209 );
210 sessions
211 .remove(destination.clone())
212 .await
213 .expect("Delete session should not fail");
214 should_handshake = true;
215 }
216
217 if should_handshake {
218 if crypto_state.is_none() {
219 debug!(
220 "Noise handshake with {:?} initiated for new session establishment",
221 destination
222 );
223 } else {
224 debug!(
225 "Noise re-handshake with {:?} due to re-handshake interval",
226 destination
227 );
228 }
229
230 Self::perform_handshake(communication, sessions, destination.clone()).await?;
235 }
236
237 let mut crypto_state = sessions
238 .get(destination.clone())
239 .await
240 .expect("Get session should not fail")
241 .expect("Session should exist after handshake");
242
243 let transport_frame = crypto_state
245 .state
246 .send(message.payload.into())
247 .map_err(|_| NoiseCryptoProviderError::DecryptionFailure)?;
248 if let Err(e) = communication
249 .send(OutgoingMessage {
250 payload: Frame::TransportFrame(transport_frame).to_cbor(),
251 destination: destination.clone(),
252 topic: message.topic,
253 })
254 .await
255 .map_err(transport_send_error)
256 {
257 match e.kind() {
258 ErrorKind::Fatal => {
259 error!(
260 "{:?} fatal error sending message. Clearing cryptographic sessions.",
261 destination
262 );
263 sessions
264 .remove(destination.clone())
265 .await
266 .expect("Delete session should not fail");
267 return Err(e);
268 }
269 ErrorKind::Unreachable => {
270 info!(
277 "{:?} is unreachable. Clearing cryptographic sessions.",
278 destination
279 );
280 sessions
281 .remove(destination.clone())
282 .await
283 .expect("Delete session should not fail");
284 return Err(e);
285 }
286 ErrorKind::Other => {
288 error!(
289 "Recoverable error sending message to {:?}: {:?}",
290 destination, e
291 );
292 }
293 }
294 }
295
296 sessions
297 .save(destination, crypto_state)
298 .await
299 .expect("Save session should not fail");
300
301 Ok(())
302 }
303
304 async fn receive(
305 &self,
306 receiver: &Com::Receiver,
307 communication: &Com,
308 sessions: &Ses,
309 ) -> Result<IncomingMessage, Self::ReceiveError> {
310 loop {
311 let message = receiver
312 .receive()
313 .await
314 .map_err(|e| NoiseCryptoProviderError::TransportReceive { kind: e.kind() })?;
315
316 let source_endpoint: crate::endpoint::Endpoint = message.source.clone().into();
318
319 let Ok(transport_frame) = Frame::from_cbor(&message.payload) else {
321 warn!("Received malformed cbor message, ignoring");
322 continue;
323 };
324
325 match transport_frame {
326 Frame::HandshakeStart(handshake_start) => {
327 let mut responder = HandshakeResponder::new(&handshake_start.ciphersuite);
328 responder
329 .read_start_message(&handshake_start)
330 .map_err(|_| NoiseCryptoProviderError::HandshakeProtocol)?;
331 let response_message = responder
332 .write_response_message()
333 .map_err(|_| NoiseCryptoProviderError::HandshakeProtocol)?;
334 let handshake_frame = Frame::HandshakeFinish(response_message);
335 communication
336 .send(OutgoingMessage {
337 payload: handshake_frame.to_cbor(),
338 destination: source_endpoint.clone(),
339 topic: None,
340 })
341 .await
342 .map_err(transport_send_error)?;
343
344 let crypto_state = NoiseCryptoProviderState {
345 state: (&mut responder).into(),
346 };
347 sessions
348 .save(source_endpoint, crypto_state)
349 .await
350 .expect("Save session should not fail");
351 }
352 Frame::TransportFrame(transport_frame) => {
353 let _crypto_state_guard = CRYPTO_STATE_GUARD.lock().await;
354 let crypto_state = sessions
355 .get(source_endpoint.clone())
356 .await
357 .expect("Get session should not fail");
358 let Some(mut state) = crypto_state else {
359 debug!("No session for {:?}, waiting for handshake", message.source);
360 let frame = Frame::CryptoInvalidated.to_cbor();
361 communication
362 .send(OutgoingMessage {
363 payload: frame,
364 destination: source_endpoint,
365 topic: None,
366 })
367 .await
368 .map_err(transport_send_error)?;
369 continue;
370 };
371
372 let payload = state.state.receive(&transport_frame);
373 let Ok(payload) = payload else {
374 info!("Failed to decrypt message from {:?}", message.source);
375 continue;
376 };
377
378 sessions
379 .save(source_endpoint, state)
380 .await
381 .expect("Save session should not fail");
382
383 return Ok(IncomingMessage {
384 payload: payload.as_ref().to_vec(),
385 destination: message.destination,
386 source: message.source,
387 topic: message.topic,
388 });
389 }
390 Frame::CryptoInvalidated => {
391 info!(
392 "Invalidated session for {:?} due to crypto error, deleting session and waiting for handshake",
393 message.source
394 );
395 sessions
396 .remove(source_endpoint)
397 .await
398 .expect("Delete session should not fail");
399 }
400 _ => continue,
401 }
402 }
403 }
404}
405
406#[derive(Serialize, Deserialize)]
408pub(super) enum Frame {
409 HandshakeStart(HandshakeStartMessage),
411 HandshakeFinish(HandshakeFinishMessage),
412 TransportFrame(TransportFrame),
414 CryptoInvalidated,
417}
418
419impl Frame {
420 pub(crate) fn to_cbor(&self) -> Vec<u8> {
421 let mut buffer = Vec::new();
422 ciborium::into_writer(self, &mut buffer).expect("Ciborium serialization should not fail");
423 buffer
424 }
425
426 pub(crate) fn from_cbor(buffer: &[u8]) -> Result<Self, ()> {
427 ciborium::from_reader(buffer).map_err(|_| ())
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use std::collections::HashMap;
434
435 use crate::{
436 IpcClientImpl,
437 crypto_provider::noise::crypto_provider::NoiseCryptoProvider,
438 endpoint::Endpoint,
439 ipc_client_trait::IpcClient,
440 message::OutgoingMessage,
441 traits::{InMemorySessionRepository, TestTwoWayCommunicationBackend},
442 };
443
444 #[tokio::test]
445 async fn ping_pong() {
446 let (provider_1, provider_2) = TestTwoWayCommunicationBackend::new();
447
448 let session_map_1 = InMemorySessionRepository::new(HashMap::new());
449 let client_1 = IpcClientImpl::new(NoiseCryptoProvider, provider_1, session_map_1);
450 let _ = client_1.start(None).await;
451 let mut recv_1 = client_1.subscribe(None).await.unwrap();
452
453 let session_map_2 = InMemorySessionRepository::new(HashMap::new());
454 let client_2 = IpcClientImpl::new(NoiseCryptoProvider, provider_2, session_map_2);
455 let _ = client_2.start(None).await;
456 let mut recv_2 = client_2.subscribe(None).await.unwrap();
457
458 let handle_1 = tokio::spawn(async move {
459 let mut val: u8 = 0;
460 for _ in 0..255 {
461 let message = OutgoingMessage {
462 payload: vec![val],
463 destination: Endpoint::DesktopMain,
464 topic: None,
465 };
466 client_1.send(message).await.unwrap();
467 let recv_message = recv_1.receive(None).await.unwrap();
468 val = recv_message.payload[0] + 1;
469 }
470 });
471
472 let handle_2 = tokio::spawn(async move {
473 for _ in 0..255 {
474 let recv_message = recv_2.receive(None).await.unwrap();
475 let val = recv_message.payload[0];
476 if val == 255 {
477 break;
478 }
479
480 client_2
481 .send(OutgoingMessage {
482 payload: vec![val],
483 destination: Endpoint::DesktopMain,
484 topic: None,
485 })
486 .await
487 .unwrap();
488 }
489 });
490
491 let _ = tokio::join!(handle_1, handle_2);
492 }
493}