Skip to main content

bitwarden_ipc/fragmentation/
mod.rs

1//! Splits payloads larger than one crypto provider message into fragments and reassembles them on
2//! the receiving side.
3//!
4//! Fragmentation runs above the [`CryptoProvider`](crate::traits::CryptoProvider), so each fragment
5//! is an ordinary [`OutgoingMessage`](crate::message::OutgoingMessage) that the provider encrypts
6//! on its own. A fragment's topic is the original topic behind the reserved [`FRAGMENT_TOPIC`]
7//! prefix, and its payload is a [`FragmentFrame`]: a CBOR [`FragmentHeader`] followed by the raw
8//! fragment data, so the data is copied once on each side. Peers without fragmentation support
9//! deliver such messages to subscribers as an unknown topic.
10//!
11//! The sender awaits each fragment before sending the next and the communication backends deliver
12//! in order, so fragments are appended as they arrive and any gap discards the message.
13//!
14//! Every peer is untrusted, so the [`Reassembler`] bounds the reassembled size, and the number of
15//! incomplete messages and the bytes reserved for them, both per source and per pool. Web pages are
16//! numerous and untrusted, so all web sources share a smaller pool that cannot consume the pool of
17//! the other sources. An incomplete message is discarded when no fragment arrives for
18//! [`REASSEMBLY_IDLE_TIMEOUT`], or [`REASSEMBLY_TIMEOUT`] after its first fragment.
19
20mod reassembler;
21mod split;
22
23pub(crate) use reassembler::{REASSEMBLY_IDLE_TIMEOUT, REASSEMBLY_TIMEOUT, Reassembler};
24use serde::{Deserialize, Serialize};
25pub(crate) use split::{FragmentationError, Fragments};
26
27/// Reserved topic that marks a message as a fragment. A fragment of a message with a topic carries
28/// `IpcFragment/<topic>`.
29pub(crate) const FRAGMENT_TOPIC: &str = "IpcFragment";
30
31/// Largest payload that can be sent, and the largest reassembled payload a receiver accepts. IPC is
32/// meant for messages, so larger data has to be streamed some other way.
33pub(crate) const MAX_PAYLOAD_SIZE: usize = 5 * 1024 * 1024;
34
35/// Upper bound on the encoded size of a [`FragmentHeader`].
36const MAX_HEADER_LEN: usize = 128;
37
38/// Metadata that identifies a fragment within its message.
39#[derive(Debug, Serialize, Deserialize)]
40struct FragmentHeader {
41    /// Random per message, so fragments of concurrent sends from one sender don't mix.
42    message_id: u64,
43    index: u32,
44    /// Size of the reassembled payload, so a receiver can reject it before buffering.
45    total_len: u32,
46}
47
48/// Payload of a fragment message: the [`FragmentHeader`] in CBOR followed directly by the raw
49/// data, so the data is not re-encoded.
50struct FragmentFrame<'a> {
51    header: FragmentHeader,
52    data: &'a [u8],
53}
54
55impl<'a> FragmentFrame<'a> {
56    fn encode(&self) -> Vec<u8> {
57        let mut payload = Vec::with_capacity(MAX_HEADER_LEN + self.data.len());
58        ciborium::into_writer(&self.header, &mut payload)
59            .expect("Ciborium serialization should not fail");
60        payload.extend_from_slice(self.data);
61        payload
62    }
63
64    fn decode(payload: &'a [u8]) -> Option<Self> {
65        let mut data = payload;
66        let header = ciborium::from_reader(&mut data).ok()?;
67        Some(Self { header, data })
68    }
69}
70
71/// Topic of the message a fragment belongs to, which may itself have no topic.
72type OriginalTopic = Option<String>;
73
74#[cfg(test)]
75pub(crate) mod test_support {
76    use super::*;
77    use crate::{
78        endpoint::{Endpoint, HostId, Source},
79        message::{IncomingMessage, OutgoingMessage},
80    };
81
82    /// Crypto provider message limit used by tests that call [`Fragments::split`] directly.
83    pub(crate) const MAX_LEN: usize = 64 * 1024;
84    pub(crate) const FRAGMENT_LEN: usize = MAX_LEN - MAX_HEADER_LEN;
85
86    pub(crate) fn large_payload(len: usize, seed: u8) -> Vec<u8> {
87        (0..len).map(|i| (i % 251) as u8 ^ seed).collect()
88    }
89
90    pub(crate) fn foreground_source(id: i32) -> Source {
91        Source::BrowserForeground { id }
92    }
93
94    pub(crate) fn web_source(tab_id: i32) -> Source {
95        Source::Web {
96            tab_id,
97            document_id: "doc".to_owned(),
98            origin: "https://example.com".to_owned(),
99        }
100    }
101
102    pub(crate) fn outgoing(payload: Vec<u8>, topic: &str) -> OutgoingMessage {
103        OutgoingMessage {
104            payload,
105            destination: Endpoint::BrowserBackground { id: HostId::Own },
106            topic: Some(topic.to_owned()),
107        }
108    }
109
110    pub(crate) fn incoming(message: OutgoingMessage, source: Source) -> IncomingMessage {
111        IncomingMessage {
112            payload: message.payload,
113            destination: message.destination,
114            source,
115            topic: message.topic,
116        }
117    }
118
119    /// Builds a fragment of a message on topic `bulk` that carries 16 bytes of data.
120    pub(crate) fn crafted(
121        source: Source,
122        message_id: u64,
123        index: u32,
124        total_len: usize,
125    ) -> IncomingMessage {
126        let payload = FragmentFrame {
127            header: FragmentHeader {
128                message_id,
129                index,
130                total_len: total_len as u32,
131            },
132            data: &[0; 16],
133        }
134        .encode();
135        incoming(
136            outgoing(payload, &Fragments::fragment_topic(Some("bulk"))),
137            source,
138        )
139    }
140
141    pub(crate) fn split_all(
142        message: OutgoingMessage,
143        max_payload_len: usize,
144    ) -> Vec<OutgoingMessage> {
145        Fragments::split(message, max_payload_len)
146            .unwrap()
147            .collect()
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::{test_support::*, *};
154
155    #[test]
156    fn fragment_topic_round_trips_and_other_topics_pass_through() {
157        for topic in [None, Some(""), Some("bulk"), Some("a/b")] {
158            assert_eq!(
159                Reassembler::original_topic(Some(&Fragments::fragment_topic(topic))),
160                Some(topic.map(str::to_owned))
161            );
162        }
163        for topic in [
164            None,
165            Some("bulk"),
166            Some("IpcFragments"),
167            Some("IpcFragment-x"),
168        ] {
169            assert_eq!(Reassembler::original_topic(topic), None);
170        }
171    }
172
173    #[test]
174    fn largest_header_fits_and_decodes_up_to_its_data() {
175        let header = || FragmentHeader {
176            message_id: u64::MAX,
177            index: u32::MAX,
178            total_len: u32::MAX,
179        };
180        let empty = FragmentFrame {
181            header: header(),
182            data: &[],
183        };
184        assert!(empty.encode().len() <= MAX_HEADER_LEN);
185
186        let data = large_payload(FRAGMENT_LEN, 0);
187        let payload = FragmentFrame {
188            header: header(),
189            data: &data,
190        }
191        .encode();
192        assert!(payload.len() <= MAX_LEN);
193        let decoded = FragmentFrame::decode(&payload).unwrap();
194        assert_eq!(decoded.header.message_id, u64::MAX);
195        assert_eq!(decoded.data, data);
196    }
197}