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