ssb_multiformats/
multibox.rs

1//! Implementation of [ssb multiboxes](https://spec.scuttlebutt.nz/feed/datatypes.html#multibox).
2use std::fmt;
3use std::io::{self, Write};
4
5use super::{base64, skip_prefix, split_at_byte};
6
7#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)]
8/// A multibox that owns its data. This does no decryption, it stores cyphertext.
9pub enum Multibox {
10    // https://ssbc.github.io/scuttlebutt-protocol-guide/#private-messages
11    PrivateBox(Vec<u8>),
12    Other(u64, Vec<u8>),
13}
14
15impl Multibox {
16    /// Creates a new private box multibox with the given secret text (*not* base64 encoded).
17    pub fn new_private_box(secret: Vec<u8>) -> Multibox {
18        Multibox::PrivateBox(secret)
19    }
20
21    /// Creates a multibox with the given identifier and the given secret text (*not* base64 encoded).
22    pub fn new_multibox(id: u64, secret: Vec<u8>) -> Multibox {
23        match id {
24            0 => Multibox::new_private_box(secret),
25            _ => Multibox::Other(id, secret),
26        }
27    }
28
29    /// Parses a
30    /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multibox-legacy-encoding)
31    /// into a `Multibox`, also returning the remaining input on success.
32    pub fn from_legacy(s: &[u8]) -> Result<(Multibox, &[u8]), DecodeLegacyError> {
33        let (data, suffix) = split_at_byte(s, 0x2E).ok_or(DecodeLegacyError::NoDot)?;
34
35        base64::decode_config(data, base64::STANDARD)
36            .map_err(DecodeLegacyError::InvalidBase64)
37            .and_then(|cypher_raw| {
38                if data.len() % 4 != 0 {
39                    return Err(DecodeLegacyError::NoncanonicPadding);
40                }
41
42                let tail = skip_prefix(suffix, b"box").ok_or(DecodeLegacyError::InvalidSuffix)?;
43
44                match decode_base32_id(tail).ok_or(DecodeLegacyError::InvalidSuffix)? {
45                    (0, tail) => Ok((Multibox::PrivateBox(cypher_raw), tail)),
46                    (id, tail) => Ok((Multibox::Other(id, cypher_raw), tail)),
47                }
48            })
49    }
50
51    /// Serialize a `Multibox` into a writer, using the
52    /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multibox-legacy-encoding).
53    pub fn to_legacy<W: Write>(&self, w: &mut W) -> Result<(), io::Error> {
54        match self {
55            Multibox::PrivateBox(ref bytes) => {
56                let data = base64::encode_config(bytes, base64::STANDARD);
57                w.write_all(data.as_bytes())?;
58
59                w.write_all(b".box")
60            }
61
62            Multibox::Other(id, ref bytes) => {
63                let data = base64::encode_config(bytes, base64::STANDARD);
64                w.write_all(data.as_bytes())?;
65
66                w.write_all(b".box")?;
67                w.write_all(&encode_base32_id(*id)[..])
68            }
69        }
70    }
71
72    /// Serialize a `Multibox` into an owned byte vector, using the
73    /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multibox-legacy-encoding).
74    pub fn to_legacy_vec(&self) -> Vec<u8> {
75        let capacity = match self {
76            Multibox::PrivateBox(ref cyphertext) => ((cyphertext.len() * 4) / 3) + 4,
77            Multibox::Other(id, ref cyphertext) => {
78                ((cyphertext.len() * 4) / 3) + 4 + id_len_base32(*id)
79            }
80        };
81
82        let mut out = Vec::with_capacity(capacity);
83        self.to_legacy(&mut out).unwrap();
84        out
85    }
86
87    /// Serialize a `Multibox` into an owned string, using the
88    /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multibox-legacy-encoding).
89    pub fn to_legacy_string(&self) -> String {
90        unsafe { String::from_utf8_unchecked(self.to_legacy_vec()) }
91    }
92}
93
94/// Everything that can go wrong when decoding a `Multibox` from the legacy encoding.
95#[derive(Debug, PartialEq, Eq, Clone)]
96pub enum DecodeLegacyError {
97    /// Input did not contain a `"."` to separate the data from the suffix.
98    NoDot,
99    /// The base64 portion of the box was invalid.
100    InvalidBase64(base64::DecodeError),
101    /// The base64 portion of the box did not use the correct amount of padding.
102    NoncanonicPadding,
103    /// The suffix is not well-formed.
104    InvalidSuffix,
105}
106
107impl fmt::Display for DecodeLegacyError {
108    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
109        match self {
110            DecodeLegacyError::InvalidBase64(ref err) => write!(f, "{}", err),
111            DecodeLegacyError::NoncanonicPadding => write!(f, "Incorrect number of padding '='s"),
112            DecodeLegacyError::NoDot => write!(f, "No dot"),
113            DecodeLegacyError::InvalidSuffix => write!(f, "Invalid suffix"),
114        }
115    }
116}
117
118impl std::error::Error for DecodeLegacyError {}
119
120// Decode the legacy format id of a multibox (canonic crockford base32, no leading zeros, at most 2^64 - 1).
121// Stops decoding when encountering end of input, a non-base32 character, or at the maximum identifier length.
122// In all these cases, it returns `Some(decoded)`, `None` is only returned if the first input
123// character is a zero or if a large identifier has a non-canonical first character.
124fn decode_base32_id(s: &[u8]) -> Option<(u64, &[u8])> {
125    if s.get(0) == Some(&0x30) {
126        return None; // Id may not begin with a zero.
127    }
128
129    let mut acc: u64 = 0; // The id is built up in this variable.
130
131    for i in 0..13 {
132        // 13 is the maximum length of an identifier
133        match s.get(i) {
134            None => return Some((acc, &[][..])), // end of input
135            Some(c) => {
136                if i == 12 && s[0] > 0x46 {
137                    // Noncanonical first character.
138                    return None;
139                }
140
141                let dec = match c {
142                    0x30 => 0,
143                    0x31 => 1,
144                    0x32 => 2,
145                    0x33 => 3,
146                    0x34 => 4,
147                    0x35 => 5,
148                    0x36 => 6,
149                    0x37 => 7,
150                    0x38 => 8,
151                    0x39 => 9,
152                    0x41 => 10,
153                    0x42 => 11,
154                    0x43 => 12,
155                    0x44 => 13,
156                    0x45 => 14,
157                    0x46 => 15,
158                    0x47 => 16,
159                    0x48 => 17,
160                    0x4A => 18,
161                    0x4B => 19,
162                    0x4D => 20,
163                    0x4E => 21,
164                    0x50 => 22,
165                    0x51 => 23,
166                    0x52 => 24,
167                    0x53 => 25,
168                    0x54 => 26,
169                    0x56 => 27,
170                    0x57 => 28,
171                    0x58 => 29,
172                    0x59 => 30,
173                    0x5A => 31,
174                    _ => return Some((acc, &s[i..])), // non-base32 input byte
175                };
176                acc <<= 5;
177                acc += dec;
178            }
179        }
180    }
181    // Reached maximum length of an identifier, return the decoded value and the remaining input.
182    Some((acc, &s[13..]))
183}
184
185fn id_len_base32(id: u64) -> usize {
186    (68 - id.leading_zeros() as usize) / 5
187}
188
189// Produces the canonical base32 encoding used for legacy multibox identifiers.
190fn encode_base32_id(id: u64) -> Vec<u8> {
191    let len = id_len_base32(id); // how many bytes of output will this create?
192    let mut out = Vec::with_capacity(len);
193
194    for i in 0..len {
195        let offset = ((len - 1) - i) * 5; // offset to the least-significant bit of the five bits to encode.
196        let to_encode = (id >> offset) & 0b11111; // the five bits to encode (and leading zeros)
197
198        // the symbol to write to the output
199        let symbol = match to_encode {
200            0 => 0x30,
201            1 => 0x31,
202            2 => 0x32,
203            3 => 0x33,
204            4 => 0x34,
205            5 => 0x35,
206            6 => 0x36,
207            7 => 0x37,
208            8 => 0x38,
209            9 => 0x39,
210            10 => 0x41,
211            11 => 0x42,
212            12 => 0x43,
213            13 => 0x44,
214            14 => 0x45,
215            15 => 0x46,
216            16 => 0x47,
217            17 => 0x48,
218            18 => 0x4A,
219            19 => 0x4B,
220            20 => 0x4D,
221            21 => 0x4E,
222            22 => 0x50,
223            23 => 0x51,
224            24 => 0x52,
225            25 => 0x53,
226            26 => 0x54,
227            27 => 0x56,
228            28 => 0x57,
229            29 => 0x58,
230            30 => 0x59,
231            31 => 0x5A,
232            _ => unreachable!(),
233        };
234
235        out.push(symbol);
236    }
237
238    out
239}
240
241#[test]
242fn test_from_legacy() {
243    assert!(Multibox::from_legacy(b"lB==.box").is_err());
244    assert!(Multibox::from_legacy(b"lA==.box0").is_err());
245    assert!(Multibox::from_legacy(b"lA==.box01").is_err());
246    assert!(Multibox::from_legacy(b"lA==.boxG0123456789AB").is_err());
247    assert!(Multibox::from_legacy(b"lA==.boxF0123456789AB").is_ok());
248
249    match Multibox::from_legacy(b".box").unwrap().0 {
250        Multibox::PrivateBox(data) => assert_eq!(data.len(), 0),
251        _ => panic!(),
252    }
253
254    assert_matches!(
255        Multibox::from_legacy(b"lA==.box").unwrap().0,
256        Multibox::PrivateBox(..)
257    );
258    assert_matches!(
259        Multibox::from_legacy(b"lA==.boxa").unwrap().0,
260        Multibox::PrivateBox(..)
261    );
262    assert_matches!(
263        Multibox::from_legacy(b"lA==.boxU").unwrap().0,
264        Multibox::PrivateBox(..)
265    );
266    assert_matches!(
267        Multibox::from_legacy(b"lA==.box\"").unwrap().0,
268        Multibox::PrivateBox(..)
269    );
270    assert_matches!(
271        Multibox::from_legacy(b"lA==.box1").unwrap().0,
272        Multibox::Other(1, _)
273    );
274    assert_matches!(
275        Multibox::from_legacy(b"lA==.boxV").unwrap().0,
276        Multibox::Other(27, _)
277    );
278    assert_matches!(
279        Multibox::from_legacy(b"lA==.box11").unwrap().0,
280        Multibox::Other(0b00001_00001, _)
281    );
282    assert_matches!(
283        Multibox::from_legacy(b".boxNN").unwrap().0,
284        Multibox::Other(0b10101_10101, _)
285    );
286}
287
288#[test]
289fn test_to_legacy() {
290    assert_eq!(Multibox::new_private_box(vec![]).to_legacy_vec(), b".box");
291    assert_eq!(
292        Multibox::new_multibox(0b10101_10101, vec![]).to_legacy_vec(),
293        b".boxNN"
294    );
295}