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        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/// A `CryptoProvider` that encrypts IPC traffic using the Noise protocol.
23#[derive(Default)]
24pub struct NoiseCryptoProvider {
25    /// Serializes access to the persisted transport state, so that two concurrent sends cannot
26    /// read the same copy and reuse a nonce.
27    ///
28    /// Held per provider, and so per [`IpcClientImpl`](crate::IpcClientImpl), rather than per
29    /// process: the state it protects belongs to one client's session repository, and a
30    /// process-wide lock would let one client's handshake — which can wait
31    /// [`HANDSHAKE_TIMEOUT_SECS`] for a reply — stall every other client's traffic.
32    crypto_state_guard: tokio::sync::Mutex<()>,
33}
34
35impl NoiseCryptoProvider {
36    /// Creates a provider with no sessions established.
37    pub fn new() -> Self {
38        Self::default()
39    }
40}
41
42#[derive(Debug)]
43pub enum NoiseCryptoProviderError {
44    /// A protocol error (missing message, malformed message)
45    HandshakeProtocol,
46    /// A timeout waiting for a message
47    Timeout,
48    /// The destination could not be reached (the underlying transport is not connected).
49    TransportUnreachable,
50    /// Could not send via the underlying transport. `kind` is the underlying backend error's
51    /// [`IpcErrorKind`] classification.
52    TransportSend { kind: ErrorKind },
53    /// Could not receive via the underlying transport. `kind` is the underlying backend error's
54    /// [`IpcErrorKind`] classification.
55    TransportReceive { kind: ErrorKind },
56    /// A cryptographic error. In most cases, such messages are just dropped.
57    DecryptionFailure,
58}
59
60impl IpcErrorKind for NoiseCryptoProviderError {
61    fn kind(&self) -> ErrorKind {
62        match self {
63            // A bad/missing handshake frame from one peer does not affect the shared client; the
64            // peer can retry the handshake.
65            NoiseCryptoProviderError::HandshakeProtocol => ErrorKind::Other,
66            // The handshake is retryable on a subsequent send.
67            NoiseCryptoProviderError::Timeout => ErrorKind::Other,
68            // A decryption failure only affects the offending message, which is dropped.
69            NoiseCryptoProviderError::DecryptionFailure => ErrorKind::Other,
70            // An unreachable destination; the message simply could not be delivered.
71            NoiseCryptoProviderError::TransportUnreachable => ErrorKind::Unreachable,
72            // Defer to the underlying backend's classification, captured at construction.
73            NoiseCryptoProviderError::TransportSend { kind }
74            | NoiseCryptoProviderError::TransportReceive { kind } => *kind,
75        }
76    }
77}
78
79/// Classify a transport send failure: an unreachable destination becomes the dedicated
80/// [`NoiseCryptoProviderError::TransportUnreachable`], while every other failure preserves the
81/// underlying backend's fatal/recoverable classification.
82fn 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        // Wait for the handshake response (with timeout)
118        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                // For concurrent handshakes, ignore messages
126                if incoming.source.to_endpoint() != destination {
127                    continue;
128                }
129
130                // Malformed messages will cancel the handshake
131                let Ok(response_frame) = Frame::from_cbor(&incoming.payload) else {
132                    return Err(NoiseCryptoProviderError::HandshakeProtocol);
133                };
134
135                // Only accept handshake finish messages until the handshake is complete
136                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            // Both the timeout error, and errors from within the handshake loop are propagated
154            // here, hence the double question mark.
155        })??;
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
174/// Re-handshake interval in seconds. Sessions older than this will automatically
175/// re-key on the next send operation.
176const REHANDSHAKE_INTERVAL_SECS: u64 = 300;
177
178/// Timeout for waiting for a handshake response from the remote peer.
179const HANDSHAKE_TIMEOUT_SECS: u64 = 2;
180
181/// Session state for the Noise crypto provider.
182#[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        // Send operations *MUST* be serialized, otherwise nonce re-use may happen since
203        // concurrent sends may acquire the same copy of the transport state before nonce
204        // updating.
205        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            // Propagate every handshake failure, including an unreachable transport. The
243            // unreachable case surfaces as `NoiseCryptoProviderError::TransportUnreachable`
244            // (non-fatal), which the logging layers intentionally do not log — so it no longer
245            // needs to be swallowed here to avoid spam.
246            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        // Encrypt and send the payload
256        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                    // If a destination goes offline, the cryptographic session is torn down.
283                    // The next time the destination comes back online, a new handshake will be
284                    // performed. If this were not done, then the first message
285                    // would always be dropped by the destination,
286                    // after the destination process-reloads because it would not be decryptable by
287                    // the destination.
288                    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                // Every other recoverable send failure is still surfaced.
299                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            // Ensure session exists
329            let source_endpoint: crate::endpoint::Endpoint = message.source.clone().into();
330
331            // Decode outer transport frame from wire
332            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/// The raw frame that is sent via IPC.
419#[derive(Serialize, Deserialize)]
420pub(super) enum Frame {
421    // Handshake Frames
422    HandshakeStart(HandshakeStartMessage),
423    HandshakeFinish(HandshakeFinishMessage),
424    // After the handshake is done, transport frames are used to wrap ciphertexts
425    TransportFrame(TransportFrame),
426    // If crypto is invalidated, this message is sent by the device noticing
427    // the invalidation so that both sides reset the crypto.
428    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}