Skip to main content

bitwarden_ipc/crypto_provider/noise/
crypto_provider.rs

1use 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        NOISE_MAX_MESSAGE_LEN, NOISE_TAG_LEN,
10        handshake::{
11            CipherSuite, HandshakeFinishMessage, HandshakeInitiator, HandshakeResponder,
12            HandshakeStartMessage,
13        },
14        transport_state::{PersistentTransportState, TransportFrame},
15    },
16    error::{ErrorKind, IpcErrorKind},
17    message::{IncomingMessage, OutgoingMessage},
18    traits::{
19        CommunicationBackend, CommunicationBackendReceiver, CryptoProvider, SessionRepository,
20    },
21};
22
23/// A `CryptoProvider` that encrypts IPC traffic using the Noise protocol.
24#[derive(Default)]
25pub struct NoiseCryptoProvider {
26    /// Serializes access to the persisted transport state, so that two concurrent sends cannot
27    /// read the same copy and reuse a nonce.
28    ///
29    /// Held per provider, and so per [`IpcClientImpl`](crate::IpcClientImpl), rather than per
30    /// process: the state it protects belongs to one client's session repository, and a
31    /// process-wide lock would let one client's handshake — which can wait
32    /// [`HANDSHAKE_TIMEOUT_SECS`] for a reply — stall every other client's traffic.
33    crypto_state_guard: tokio::sync::Mutex<()>,
34}
35
36impl NoiseCryptoProvider {
37    /// Creates a provider with no sessions established.
38    pub fn new() -> Self {
39        Self::default()
40    }
41}
42
43#[derive(Debug)]
44pub enum NoiseCryptoProviderError {
45    /// A protocol error (missing message, malformed message)
46    HandshakeProtocol,
47    /// A timeout waiting for a message
48    Timeout,
49    /// The destination could not be reached (the underlying transport is not connected).
50    TransportUnreachable,
51    /// Could not send via the underlying transport. `kind` is the underlying backend error's
52    /// [`IpcErrorKind`] classification.
53    TransportSend { kind: ErrorKind },
54    /// Could not receive via the underlying transport. `kind` is the underlying backend error's
55    /// [`IpcErrorKind`] classification.
56    TransportReceive { kind: ErrorKind },
57    /// A cryptographic error. In most cases, such messages are just dropped.
58    DecryptionFailure,
59}
60
61impl IpcErrorKind for NoiseCryptoProviderError {
62    fn kind(&self) -> ErrorKind {
63        match self {
64            // A bad/missing handshake frame from one peer does not affect the shared client; the
65            // peer can retry the handshake.
66            NoiseCryptoProviderError::HandshakeProtocol => ErrorKind::Other,
67            // The handshake is retryable on a subsequent send.
68            NoiseCryptoProviderError::Timeout => ErrorKind::Other,
69            // A decryption failure only affects the offending message, which is dropped.
70            NoiseCryptoProviderError::DecryptionFailure => ErrorKind::Other,
71            // An unreachable destination; the message simply could not be delivered.
72            NoiseCryptoProviderError::TransportUnreachable => ErrorKind::Unreachable,
73            // Defer to the underlying backend's classification, captured at construction.
74            NoiseCryptoProviderError::TransportSend { kind }
75            | NoiseCryptoProviderError::TransportReceive { kind } => *kind,
76        }
77    }
78}
79
80/// Classify a transport send failure: an unreachable destination becomes the dedicated
81/// [`NoiseCryptoProviderError::TransportUnreachable`], while every other failure preserves the
82/// underlying backend's fatal/recoverable classification.
83fn transport_send_error<E: IpcErrorKind>(e: E) -> NoiseCryptoProviderError {
84    match e.kind() {
85        ErrorKind::Unreachable => NoiseCryptoProviderError::TransportUnreachable,
86        kind => NoiseCryptoProviderError::TransportSend { kind },
87    }
88}
89
90impl NoiseCryptoProvider {
91    async fn perform_handshake<Com, Ses>(
92        communication: &Com,
93        sessions: &Ses,
94        destination: crate::endpoint::Endpoint,
95    ) -> Result<(), NoiseCryptoProviderError>
96    where
97        Com: CommunicationBackend,
98        Ses: SessionRepository<NoiseCryptoProviderState>,
99    {
100        debug!("Starting noise handshake with {:?}", destination);
101
102        let mut initiator = HandshakeInitiator::new(&CipherSuite::default());
103        let message = initiator
104            .write_start_message()
105            .expect("Handshake start message should be buildable");
106        let receiver = communication.subscribe().await;
107
108        let handshake_frame = Frame::HandshakeStart(message);
109        communication
110            .send(OutgoingMessage {
111                payload: handshake_frame.to_cbor(),
112                destination: destination.clone(),
113                topic: None,
114            })
115            .await
116            .map_err(transport_send_error)?;
117
118        // Wait for the handshake response (with timeout)
119        timeout(Duration::from_secs(HANDSHAKE_TIMEOUT_SECS), async {
120            loop {
121                let incoming = receiver
122                    .receive()
123                    .await
124                    .map_err(|e| NoiseCryptoProviderError::TransportReceive { kind: e.kind() })?;
125
126                // For concurrent handshakes, ignore messages
127                if incoming.source.to_endpoint() != destination {
128                    continue;
129                }
130
131                // Malformed messages will cancel the handshake
132                let Ok(response_frame) = Frame::from_cbor(&incoming.payload) else {
133                    return Err(NoiseCryptoProviderError::HandshakeProtocol);
134                };
135
136                // Only accept handshake finish messages until the handshake is complete
137                if let Frame::HandshakeFinish(handshake_finish) = response_frame {
138                    if initiator.read_response_message(&handshake_finish).is_err() {
139                        error!("Failed to read handshake response message");
140                        return Err(NoiseCryptoProviderError::HandshakeProtocol);
141                    }
142                    break;
143                }
144            }
145            Ok(())
146        })
147        .await
148        .map_err(|_| {
149            debug!(
150                "Noise handshake with {:?} timed out after {} seconds",
151                destination, HANDSHAKE_TIMEOUT_SECS
152            );
153            NoiseCryptoProviderError::Timeout
154            // Both the timeout error, and errors from within the handshake loop are propagated
155            // here, hence the double question mark.
156        })??;
157
158        let crypto_state = NoiseCryptoProviderState {
159            state: (&mut initiator).into(),
160        };
161        sessions
162            .save(destination.clone(), crypto_state)
163            .await
164            .expect("Save session should not fail");
165
166        info!(
167            "Noise handshake with {:?} completed, session established",
168            destination
169        );
170
171        Ok(())
172    }
173}
174
175/// Re-handshake interval in seconds. Sessions older than this will automatically
176/// re-key on the next send operation.
177const REHANDSHAKE_INTERVAL_SECS: u64 = 300;
178
179/// Timeout for waiting for a handshake response from the remote peer.
180const HANDSHAKE_TIMEOUT_SECS: u64 = 2;
181
182/// Session state for the Noise crypto provider.
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct NoiseCryptoProviderState {
185    state: PersistentTransportState,
186}
187
188impl<Com, Ses> CryptoProvider<Com, Ses> for NoiseCryptoProvider
189where
190    Com: CommunicationBackend,
191    Ses: SessionRepository<NoiseCryptoProviderState>,
192{
193    type Session = NoiseCryptoProviderState;
194    type SendError = NoiseCryptoProviderError;
195    type ReceiveError = NoiseCryptoProviderError;
196
197    fn max_payload_len(&self) -> usize {
198        NOISE_MAX_MESSAGE_LEN - NOISE_TAG_LEN
199    }
200
201    async fn send(
202        &self,
203        communication: &Com,
204        sessions: &Ses,
205        message: OutgoingMessage,
206    ) -> Result<(), Self::SendError> {
207        // Send operations *MUST* be serialized, otherwise nonce re-use may happen since
208        // concurrent sends may acquire the same copy of the transport state before nonce
209        // updating.
210        let _crypto_state_guard = self.crypto_state_guard.lock().await;
211
212        let destination = message.destination.clone();
213
214        let crypto_state = sessions
215            .get(destination.clone())
216            .await
217            .expect("Get session should not fail");
218
219        let mut should_handshake = crypto_state.is_none();
220        if let Some(state) = crypto_state.as_ref()
221            && state.state.should_rehandshake(REHANDSHAKE_INTERVAL_SECS)
222        {
223            info!(
224                "Noise session with {:?} is older than {}s, re-handshaking",
225                destination, REHANDSHAKE_INTERVAL_SECS
226            );
227            sessions
228                .remove(destination.clone())
229                .await
230                .expect("Delete session should not fail");
231            should_handshake = true;
232        }
233
234        if should_handshake {
235            if crypto_state.is_none() {
236                debug!(
237                    "Noise handshake with {:?} initiated for new session establishment",
238                    destination
239                );
240            } else {
241                debug!(
242                    "Noise re-handshake with {:?} due to re-handshake interval",
243                    destination
244                );
245            }
246
247            // Propagate every handshake failure, including an unreachable transport. The
248            // unreachable case surfaces as `NoiseCryptoProviderError::TransportUnreachable`
249            // (non-fatal), which the logging layers intentionally do not log — so it no longer
250            // needs to be swallowed here to avoid spam.
251            Self::perform_handshake(communication, sessions, destination.clone()).await?;
252        }
253
254        let mut crypto_state = sessions
255            .get(destination.clone())
256            .await
257            .expect("Get session should not fail")
258            .expect("Session should exist after handshake");
259
260        // Encrypt and send the payload
261        let transport_frame = crypto_state
262            .state
263            .send(message.payload.into())
264            .map_err(|_| NoiseCryptoProviderError::DecryptionFailure)?;
265        if let Err(e) = communication
266            .send(OutgoingMessage {
267                payload: Frame::TransportFrame(transport_frame).to_cbor(),
268                destination: destination.clone(),
269                topic: message.topic,
270            })
271            .await
272            .map_err(transport_send_error)
273        {
274            match e.kind() {
275                ErrorKind::Fatal => {
276                    error!(
277                        "{:?} fatal error sending message. 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::Unreachable => {
287                    // If a destination goes offline, the cryptographic session is torn down.
288                    // The next time the destination comes back online, a new handshake will be
289                    // performed. If this were not done, then the first message
290                    // would always be dropped by the destination,
291                    // after the destination process-reloads because it would not be decryptable by
292                    // the destination.
293                    info!(
294                        "{:?} is unreachable. Clearing cryptographic sessions.",
295                        destination
296                    );
297                    sessions
298                        .remove(destination.clone())
299                        .await
300                        .expect("Delete session should not fail");
301                    return Err(e);
302                }
303                // Every other recoverable send failure is still surfaced.
304                ErrorKind::Other => {
305                    error!(
306                        "Recoverable error sending message to {:?}: {:?}",
307                        destination, e
308                    );
309                }
310            }
311        }
312
313        sessions
314            .save(destination, crypto_state)
315            .await
316            .expect("Save session should not fail");
317
318        Ok(())
319    }
320
321    async fn receive(
322        &self,
323        receiver: &Com::Receiver,
324        communication: &Com,
325        sessions: &Ses,
326    ) -> Result<IncomingMessage, Self::ReceiveError> {
327        loop {
328            let message = receiver
329                .receive()
330                .await
331                .map_err(|e| NoiseCryptoProviderError::TransportReceive { kind: e.kind() })?;
332
333            // Ensure session exists
334            let source_endpoint: crate::endpoint::Endpoint = message.source.clone().into();
335
336            // Decode outer transport frame from wire
337            let Ok(transport_frame) = Frame::from_cbor(&message.payload) else {
338                warn!("Received malformed cbor message, ignoring");
339                continue;
340            };
341
342            match transport_frame {
343                Frame::HandshakeStart(handshake_start) => {
344                    let mut responder = HandshakeResponder::new(&handshake_start.ciphersuite);
345                    responder
346                        .read_start_message(&handshake_start)
347                        .map_err(|_| NoiseCryptoProviderError::HandshakeProtocol)?;
348                    let response_message = responder
349                        .write_response_message()
350                        .map_err(|_| NoiseCryptoProviderError::HandshakeProtocol)?;
351                    let handshake_frame = Frame::HandshakeFinish(response_message);
352                    communication
353                        .send(OutgoingMessage {
354                            payload: handshake_frame.to_cbor(),
355                            destination: source_endpoint.clone(),
356                            topic: None,
357                        })
358                        .await
359                        .map_err(transport_send_error)?;
360
361                    let crypto_state = NoiseCryptoProviderState {
362                        state: (&mut responder).into(),
363                    };
364                    sessions
365                        .save(source_endpoint, crypto_state)
366                        .await
367                        .expect("Save session should not fail");
368                }
369                Frame::TransportFrame(transport_frame) => {
370                    let _crypto_state_guard = self.crypto_state_guard.lock().await;
371                    let crypto_state = sessions
372                        .get(source_endpoint.clone())
373                        .await
374                        .expect("Get session should not fail");
375                    let Some(mut state) = crypto_state else {
376                        debug!("No session for {:?}, waiting for handshake", message.source);
377                        let frame = Frame::CryptoInvalidated.to_cbor();
378                        communication
379                            .send(OutgoingMessage {
380                                payload: frame,
381                                destination: source_endpoint,
382                                topic: None,
383                            })
384                            .await
385                            .map_err(transport_send_error)?;
386                        continue;
387                    };
388
389                    let payload = state.state.receive(&transport_frame);
390                    let Ok(payload) = payload else {
391                        info!("Failed to decrypt message from {:?}", message.source);
392                        continue;
393                    };
394
395                    sessions
396                        .save(source_endpoint, state)
397                        .await
398                        .expect("Save session should not fail");
399
400                    return Ok(IncomingMessage {
401                        payload: payload.as_ref().to_vec(),
402                        destination: message.destination,
403                        source: message.source,
404                        topic: message.topic,
405                    });
406                }
407                Frame::CryptoInvalidated => {
408                    info!(
409                        "Invalidated session for {:?} due to crypto error, deleting session and waiting for handshake",
410                        message.source
411                    );
412                    sessions
413                        .remove(source_endpoint)
414                        .await
415                        .expect("Delete session should not fail");
416                }
417                _ => continue,
418            }
419        }
420    }
421}
422
423/// The raw frame that is sent via IPC.
424#[derive(Serialize, Deserialize)]
425pub(super) enum Frame {
426    // Handshake Frames
427    HandshakeStart(HandshakeStartMessage),
428    HandshakeFinish(HandshakeFinishMessage),
429    // After the handshake is done, transport frames are used to wrap ciphertexts
430    TransportFrame(TransportFrame),
431    // If crypto is invalidated, this message is sent by the device noticing
432    // the invalidation so that both sides reset the crypto.
433    CryptoInvalidated,
434}
435
436impl Frame {
437    pub(crate) fn to_cbor(&self) -> Vec<u8> {
438        let mut buffer = Vec::new();
439        ciborium::into_writer(self, &mut buffer).expect("Ciborium serialization should not fail");
440        buffer
441    }
442
443    pub(crate) fn from_cbor(buffer: &[u8]) -> Result<Self, ()> {
444        ciborium::from_reader(buffer).map_err(|_| ())
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use std::collections::HashMap;
451
452    use crate::{
453        IpcClientImpl,
454        crypto_provider::noise::crypto_provider::NoiseCryptoProvider,
455        endpoint::Endpoint,
456        ipc_client_trait::IpcClient,
457        message::OutgoingMessage,
458        traits::{InMemorySessionRepository, TestTwoWayCommunicationBackend},
459    };
460
461    #[tokio::test]
462    async fn ping_pong() {
463        let (provider_1, provider_2) = TestTwoWayCommunicationBackend::new();
464
465        let session_map_1 = InMemorySessionRepository::new(HashMap::new());
466        let client_1 = IpcClientImpl::new(NoiseCryptoProvider::new(), provider_1, session_map_1);
467        let _ = client_1.start(None).await;
468        let mut recv_1 = client_1.subscribe(None).await.unwrap();
469
470        let session_map_2 = InMemorySessionRepository::new(HashMap::new());
471        let client_2 = IpcClientImpl::new(NoiseCryptoProvider::new(), provider_2, session_map_2);
472        let _ = client_2.start(None).await;
473        let mut recv_2 = client_2.subscribe(None).await.unwrap();
474
475        let handle_1 = tokio::spawn(async move {
476            let mut val: u8 = 0;
477            for _ in 0..255 {
478                let message = OutgoingMessage {
479                    payload: vec![val],
480                    destination: Endpoint::DesktopMain,
481                    topic: None,
482                };
483                client_1.send(message).await.unwrap();
484                let recv_message = recv_1.receive(None).await.unwrap();
485                val = recv_message.payload[0] + 1;
486            }
487        });
488
489        let handle_2 = tokio::spawn(async move {
490            for _ in 0..255 {
491                let recv_message = recv_2.receive(None).await.unwrap();
492                let val = recv_message.payload[0];
493                if val == 255 {
494                    break;
495                }
496
497                client_2
498                    .send(OutgoingMessage {
499                        payload: vec![val],
500                        destination: Endpoint::DesktopMain,
501                        topic: None,
502                    })
503                    .await
504                    .unwrap();
505            }
506        });
507
508        let _ = tokio::join!(handle_1, handle_2);
509    }
510}