Skip to main content

bitwarden_ipc/crypto_provider/noise/
crypto_provider.rs

1use 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
22/// A `CryptoProvider` that encrypts IPC traffic using the Noise protocol.
23pub struct NoiseCryptoProvider;
24
25#[derive(Debug)]
26pub enum NoiseCryptoProviderError {
27    /// A protocol error (missing message, malformed message)
28    HandshakeProtocol,
29    /// A timeout waiting for a message
30    Timeout,
31    /// The destination could not be reached (the underlying transport is not connected).
32    TransportUnreachable,
33    /// Could not send via the underlying transport. `kind` is the underlying backend error's
34    /// [`IpcErrorKind`] classification.
35    TransportSend { kind: ErrorKind },
36    /// Could not receive via the underlying transport. `kind` is the underlying backend error's
37    /// [`IpcErrorKind`] classification.
38    TransportReceive { kind: ErrorKind },
39    /// A cryptographic error. In most cases, such messages are just dropped.
40    DecryptionFailure,
41}
42
43impl IpcErrorKind for NoiseCryptoProviderError {
44    fn kind(&self) -> ErrorKind {
45        match self {
46            // A bad/missing handshake frame from one peer does not affect the shared client; the
47            // peer can retry the handshake.
48            NoiseCryptoProviderError::HandshakeProtocol => ErrorKind::Other,
49            // The handshake is retryable on a subsequent send.
50            NoiseCryptoProviderError::Timeout => ErrorKind::Other,
51            // A decryption failure only affects the offending message, which is dropped.
52            NoiseCryptoProviderError::DecryptionFailure => ErrorKind::Other,
53            // An unreachable destination; the message simply could not be delivered.
54            NoiseCryptoProviderError::TransportUnreachable => ErrorKind::Unreachable,
55            // Defer to the underlying backend's classification, captured at construction.
56            NoiseCryptoProviderError::TransportSend { kind }
57            | NoiseCryptoProviderError::TransportReceive { kind } => *kind,
58        }
59    }
60}
61
62/// Classify a transport send failure: an unreachable destination becomes the dedicated
63/// [`NoiseCryptoProviderError::TransportUnreachable`], while every other failure preserves the
64/// underlying backend's fatal/recoverable classification.
65fn 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
72// Serialize send operations to prevent concurrent reads of the same persisted
73// transport state, which can cause nonce reuse.
74static 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        // Wait for the handshake response (with timeout)
106        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                // For concurrent handshakes, ignore messages
114                if incoming.source.to_endpoint() != destination {
115                    continue;
116                }
117
118                // Malformed messages will cancel the handshake
119                let Ok(response_frame) = Frame::from_cbor(&incoming.payload) else {
120                    return Err(NoiseCryptoProviderError::HandshakeProtocol);
121                };
122
123                // Only accept handshake finish messages until the handshake is complete
124                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            // Both the timeout error, and errors from within the handshake loop are propagated
142            // here, hence the double question mark.
143        })??;
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
162/// Re-handshake interval in seconds. Sessions older than this will automatically
163/// re-key on the next send operation.
164const REHANDSHAKE_INTERVAL_SECS: u64 = 300;
165
166/// Timeout for waiting for a handshake response from the remote peer.
167const HANDSHAKE_TIMEOUT_SECS: u64 = 2;
168
169/// Session state for the Noise crypto provider.
170#[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        // Send operations *MUST* be serialized, otherwise nonce re-use may happen since
191        // concurrent sends may acquire the same copy of the transport state before nonce
192        // updating.
193        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            // Propagate every handshake failure, including an unreachable transport. The
231            // unreachable case surfaces as `NoiseCryptoProviderError::TransportUnreachable`
232            // (non-fatal), which the logging layers intentionally do not log — so it no longer
233            // needs to be swallowed here to avoid spam.
234            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        // Encrypt and send the payload
244        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                    // If a destination goes offline, the cryptographic session is torn down.
271                    // The next time the destination comes back online, a new handshake will be
272                    // performed. If this were not done, then the first message
273                    // would always be dropped by the destination,
274                    // after the destination process-reloads because it would not be decryptable by
275                    // the destination.
276                    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                // Every other recoverable send failure is still surfaced.
287                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            // Ensure session exists
317            let source_endpoint: crate::endpoint::Endpoint = message.source.clone().into();
318
319            // Decode outer transport frame from wire
320            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/// The raw frame that is sent via IPC.
407#[derive(Serialize, Deserialize)]
408pub(super) enum Frame {
409    // Handshake Frames
410    HandshakeStart(HandshakeStartMessage),
411    HandshakeFinish(HandshakeFinishMessage),
412    // After the handshake is done, transport frames are used to wrap ciphertexts
413    TransportFrame(TransportFrame),
414    // If crypto is invalidated, this message is sent by the device noticing
415    // the invalidation so that both sides reset the crypto.
416    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}