sbd_e2e_crypto_client/
protocol.rs

1//! This adds end-to-end encryption for peer communications over the base
2//! sbd communication protocol via
3//! [libsodium secretstream](https://doc.libsodium.org/secret-key_cryptography/secretstream).
4//!
5//! ## Message Type Header
6//!
7//! Adds a single-byte header to messages sent.
8//!
9//! Messages with bytes other than the following three should be ignored
10//! for future compatibility.
11//!
12//! - `0x10` - NewStream -- must be followed by 24 byte secret stream header.
13//! - `0x11` - Message -- encrypted message including abytes.
14//! - `0x12` - RequestNewStream -- only this single byte.
15//!
16//! ## Message Type Handling
17//!
18//! - When sending a message to a new (or not recent) peer, clients MUST
19//!   establish a new outgoing (encryption) secret stream state and send the
20//!   24 byte header in a "NewStream" message.
21//! - On receiving a "RequestNewStream" message, clients MUST establish a
22//!   new outgoing (encryption) secret stream state and send the 24 byte header
23//!   in a "NewStream" message.
24//! - On receiving a "NewStream" message, clients MUST establish a new incoming
25//!   (decryption) secret stream state.
26//! - On receiving a "Message" that cannot be decrypted, clients MUST
27//!   (1) ignore the message, (2) delete any incoming (decryption) state, and
28//!   (3) send a "RequestNewStream" message. Any message receipt tracking or
29//!   re-requesting will not be handled by this library, but may be added by
30//!   implementors as a layer on top of this.
31
32/// Start a new stream.
33pub const T_NEW_STREAM: u8 = 0x10;
34
35/// Encrypted stream message.
36pub const T_MESSAGE: u8 = 0x11;
37
38/// Request start of new stream.
39pub const T_REQ_NEW_STREAM: u8 = 0x12;
40
41/// E2e crypto protocol enum.
42///
43/// The enum variant fields are all shallow clone parts of the "full" field:
44/// - `full` the entire message send/recv via sbd-client.
45/// - `pub_key` the pub_key peer send/recv to/from.
46/// - `base_msg` the base message send/recv to/from the peer.
47/// - `...` all additional fields are broken out by enum variant.
48#[derive(PartialEq)]
49pub enum Protocol {
50    /// Message indicating a new stream state should be created
51    /// along with the secretstream header for doing so.
52    NewStream {
53        #[allow(missing_docs)]
54        full: bytes::Bytes,
55        #[allow(missing_docs)]
56        pub_key: bytes::Bytes,
57        #[allow(missing_docs)]
58        base_msg: bytes::Bytes,
59        #[allow(missing_docs)]
60        header: bytes::Bytes,
61    },
62
63    /// An encrypted message.
64    Message {
65        #[allow(missing_docs)]
66        full: bytes::Bytes,
67        #[allow(missing_docs)]
68        pub_key: bytes::Bytes,
69        #[allow(missing_docs)]
70        base_msg: bytes::Bytes,
71        #[allow(missing_docs)]
72        message: bytes::Bytes,
73    },
74
75    /// Request for a new decryption stream state.
76    RequestNewStream {
77        #[allow(missing_docs)]
78        full: bytes::Bytes,
79        #[allow(missing_docs)]
80        pub_key: bytes::Bytes,
81        #[allow(missing_docs)]
82        base_msg: bytes::Bytes,
83    },
84}
85
86impl std::fmt::Debug for Protocol {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        match self {
89            Self::NewStream { .. } => {
90                f.debug_struct("Protocol::NewStream").finish()
91            }
92            Self::Message { .. } => {
93                f.debug_struct("Protocol::Message").finish()
94            }
95            Self::RequestNewStream { .. } => {
96                f.debug_struct("Protocol::RequestNewStream").finish()
97            }
98        }
99    }
100}
101
102impl Protocol {
103    /// Parse a protocol message from complete message bytes.
104    /// If None, this message should be ignored.
105    pub fn from_full(full: bytes::Bytes) -> Option<Self> {
106        if full.len() < 33 {
107            return None;
108        }
109        let pub_key = full.slice(..32);
110        let base_msg = full.slice(32..);
111        Some(match full[32] {
112            T_NEW_STREAM => {
113                if base_msg.len() != 25 {
114                    return None;
115                }
116                let header = full.slice(33..);
117                Self::NewStream {
118                    full,
119                    pub_key,
120                    base_msg,
121                    header,
122                }
123            }
124            T_MESSAGE => {
125                let message = full.slice(33..);
126                Self::Message {
127                    full,
128                    pub_key,
129                    base_msg,
130                    message,
131                }
132            }
133            T_REQ_NEW_STREAM => {
134                if base_msg.len() != 1 {
135                    return None;
136                }
137                Self::RequestNewStream {
138                    full,
139                    pub_key,
140                    base_msg,
141                }
142            }
143            _ => return None,
144        })
145    }
146
147    /// Create a "NewStream" message type.
148    /// Panics if the pub_key is not 32 bytes or the header is not 24.
149    /// Why not take a `&[u8; N]` you ask? It's just a lot harder to work
150    /// with in rust...
151    pub fn new_stream(pub_key: &[u8], header: &[u8]) -> Self {
152        let mut out = bytes::BytesMut::with_capacity(32 + 1 + 24);
153        out.extend_from_slice(&pub_key[..32]);
154        out.extend_from_slice(&[T_NEW_STREAM]);
155        out.extend_from_slice(&header[..24]);
156        // unwrap because we know the content
157        Self::from_full(out.freeze()).unwrap()
158    }
159
160    /// Create a "Message" message type.
161    /// Panics if the pub_key is not 32 bytes.
162    /// Why not take a `&[u8; N]` you ask? It's just a lot harder to work
163    /// with in rust...
164    pub fn message(pub_key: &[u8], message: &[u8]) -> Self {
165        let mut out = bytes::BytesMut::with_capacity(32 + 1 + message.len());
166        out.extend_from_slice(&pub_key[..32]);
167        out.extend_from_slice(&[T_MESSAGE]);
168        out.extend_from_slice(message);
169        // unwrap because we know the content
170        Self::from_full(out.freeze()).unwrap()
171    }
172
173    /// Create a "RequestNewStream" message type.
174    /// Panics if the pub_key is not 32 bytes.
175    /// Why not take a `&[u8; N]` you ask? It's just a lot harder to work
176    /// with in rust...
177    pub fn request_new_stream(pub_key: &[u8]) -> Self {
178        let mut out = bytes::BytesMut::with_capacity(32 + 1);
179        out.extend_from_slice(&pub_key[..32]);
180        out.extend_from_slice(&[T_REQ_NEW_STREAM]);
181        // unwrap because we know the content
182        Self::from_full(out.freeze()).unwrap()
183    }
184
185    /// Get the full bytes of this protocol message.
186    pub fn full(&self) -> &bytes::Bytes {
187        match self {
188            Self::NewStream { full, .. } => full,
189            Self::Message { full, .. } => full,
190            Self::RequestNewStream { full, .. } => full,
191        }
192    }
193
194    /// Get the pub_key bytes of this protocol message.
195    pub fn pub_key(&self) -> &bytes::Bytes {
196        match self {
197            Self::NewStream { pub_key, .. } => pub_key,
198            Self::Message { pub_key, .. } => pub_key,
199            Self::RequestNewStream { pub_key, .. } => pub_key,
200        }
201    }
202
203    /// Get the base_msg bytes of this protocol message.
204    pub fn base_msg(&self) -> &bytes::Bytes {
205        match self {
206            Self::NewStream { base_msg, .. } => base_msg,
207            Self::Message { base_msg, .. } => base_msg,
208            Self::RequestNewStream { base_msg, .. } => base_msg,
209        }
210    }
211}
212
213#[cfg(test)]
214mod test {
215    use super::*;
216
217    const PUB_KEY: &[u8] = &[4; 32];
218    const HEADER: &[u8] = &[5; 24];
219
220    #[inline(always)]
221    fn valid_roundtrip(orig: &Protocol) {
222        let new = Protocol::from_full(orig.full().clone()).unwrap();
223        assert_eq!(orig, &new);
224        assert_eq!(orig.full(), new.full());
225    }
226
227    #[test]
228    #[should_panic]
229    fn bad_pk_size() {
230        Protocol::request_new_stream(&[4; 31]);
231    }
232
233    #[test]
234    #[should_panic]
235    fn bad_hdr_size() {
236        Protocol::new_stream(PUB_KEY, &[5; 23]);
237    }
238
239    #[test]
240    fn invalid_type() {
241        let mut exp_other = bytes::BytesMut::new();
242        exp_other.extend_from_slice(PUB_KEY);
243        exp_other.extend_from_slice(&[0x42]);
244        exp_other.extend_from_slice(b"not a thing");
245        let exp_other = exp_other.freeze();
246        assert!(Protocol::from_full(exp_other.clone()).is_none());
247    }
248
249    #[test]
250    fn new_stream() {
251        let ns = Protocol::new_stream(PUB_KEY, HEADER);
252        valid_roundtrip(&ns);
253
254        let mut exp_base_msg = Vec::new();
255        exp_base_msg.push(T_NEW_STREAM);
256        exp_base_msg.extend_from_slice(HEADER);
257
258        assert!(matches!(ns, Protocol::NewStream {
259            pub_key,
260            base_msg,
261            header,
262            ..
263        } if pub_key.as_ref() == PUB_KEY
264            && base_msg.as_ref() == exp_base_msg
265            && header.as_ref() == HEADER
266        ));
267    }
268
269    #[test]
270    fn message() {
271        let ns = Protocol::message(PUB_KEY, b"hello");
272        valid_roundtrip(&ns);
273
274        let mut exp_base_msg = Vec::new();
275        exp_base_msg.push(T_MESSAGE);
276        exp_base_msg.extend_from_slice(b"hello");
277
278        assert!(matches!(ns, Protocol::Message {
279            pub_key,
280            base_msg,
281            message,
282            ..
283        } if pub_key.as_ref() == PUB_KEY
284            && base_msg.as_ref() == exp_base_msg
285            && message.as_ref() == b"hello"
286        ));
287    }
288
289    #[test]
290    fn req_new_stream() {
291        let ns = Protocol::request_new_stream(PUB_KEY);
292        valid_roundtrip(&ns);
293
294        assert!(matches!(ns, Protocol::RequestNewStream {
295            pub_key,
296            base_msg,
297            ..
298        } if pub_key.as_ref() == PUB_KEY && base_msg.as_ref() == [0x12]));
299    }
300}