ssb_multiformats/
multihash.rs

1//! Implementation of [ssb multihashes](https://spec.scuttlebutt.nz/feed/datatypes.html#multihash).
2use std::fmt;
3use std::io::{self, Write};
4
5use serde::{
6    de::{Deserialize, Deserializer, Error},
7    ser::{Serialize, Serializer},
8};
9
10use super::{base64, serde, skip_prefix, split_at_byte};
11
12/// A multihash that owns its data.
13#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)]
14pub enum Multihash {
15    /// An ssb [message](https://spec.scuttlebutt.nz/feed/messages.html).
16    Message([u8; 32]),
17    /// An ssb [blob](TODO).
18    Blob([u8; 32]),
19}
20
21enum Target {
22    Message,
23    Blob,
24}
25
26impl Multihash {
27    /// Parses a
28    /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multihash-legacy-encoding)
29    /// into a `Multihash`.
30    pub fn from_legacy(mut s: &[u8]) -> Result<(Multihash, &[u8]), DecodeLegacyError> {
31        let target;
32
33        if let Some(tail) = skip_prefix(s, b"%") {
34            s = tail;
35            target = Target::Message;
36        } else {
37            let tail = skip_prefix(s, b"&").ok_or(DecodeLegacyError::Sigil)?;
38
39            s = tail;
40            target = Target::Blob;
41        }
42
43        let (data, suffix) = split_at_byte(s, 0x2E).ok_or(DecodeLegacyError::NoDot)?;
44
45        let tail = skip_prefix(suffix, SHA256_SUFFIX).ok_or(DecodeLegacyError::UnknownSuffix)?;
46
47        if data.len() != SHA256_BASE64_LEN {
48            return Err(DecodeLegacyError::Sha256WrongSize);
49        }
50
51        if data[SHA256_BASE64_LEN - 2] == b"="[0] {
52            return Err(DecodeLegacyError::Sha256WrongSize);
53        }
54
55        if data[SHA256_BASE64_LEN - 1] != b"="[0] {
56            return Err(DecodeLegacyError::Sha256WrongSize);
57        }
58
59        let mut dec_data = [0_u8; 32];
60        base64::decode_config_slice(data, base64::STANDARD, &mut dec_data[..])
61            .map_err(DecodeLegacyError::InvalidBase64)
62            .map(|_| {
63                let multihash = match target {
64                    Target::Blob => Multihash::Blob(dec_data),
65                    Target::Message => Multihash::Message(dec_data),
66                };
67                (multihash, tail)
68            })
69    }
70
71    /// Serialize a `Multihash` into a writer, using the
72    /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multihash-legacy-encoding).
73    pub fn to_legacy<W: Write>(&self, w: &mut W) -> Result<(), io::Error> {
74        match self {
75            Multihash::Message(ref bytes) => {
76                w.write_all(b"%")?;
77                Multihash::write_legacy_hash_and_suffix(bytes, w)
78            }
79            Multihash::Blob(ref bytes) => {
80                w.write_all(b"&")?;
81                Multihash::write_legacy_hash_and_suffix(bytes, w)
82            }
83        }
84    }
85
86    fn write_legacy_hash_and_suffix<W: Write>(bytes: &[u8], w: &mut W) -> Result<(), io::Error> {
87        let data = base64::encode_config(bytes, base64::STANDARD);
88        w.write_all(data.as_bytes())?;
89
90        w.write_all(b".")?;
91        w.write_all(SHA256_SUFFIX)
92    }
93
94    /// Serialize a `Multihash` into an owned byte vector, using the
95    /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multihash-legacy-encoding).
96    pub fn to_legacy_vec(&self) -> Vec<u8> {
97        let mut out = Vec::with_capacity(SSB_SHA256_ENCODED_LEN);
98        self.to_legacy(&mut out).unwrap();
99        out
100    }
101
102    /// Serialize a `Multihash` into an owned string, using the
103    /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multihash-legacy-encoding).
104    pub fn to_legacy_string(&self) -> String {
105        unsafe { String::from_utf8_unchecked(self.to_legacy_vec()) }
106    }
107}
108
109impl Serialize for Multihash {
110    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
111    where
112        S: Serializer,
113    {
114        serializer.serialize_str(&self.to_legacy_string())
115    }
116}
117
118impl<'de> Deserialize<'de> for Multihash {
119    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
120    where
121        D: Deserializer<'de>,
122    {
123        let s = String::deserialize(deserializer)?;
124        Multihash::from_legacy(s.as_bytes())
125            .map(|(mh, _)| mh)
126            .map_err(|err| D::Error::custom(format!("Invalid multihash: {}", err)))
127    }
128}
129
130/// Everything that can go wrong when decoding a `Multihash` from the legacy encoding.
131#[derive(Debug, PartialEq, Eq, Clone)]
132pub enum DecodeLegacyError {
133    /// Input did not start with the `"@"` sigil.
134    Sigil,
135    /// Input did not contain a `"."` to separate the data from the suffix.
136    NoDot,
137    /// The base64 portion of the key was invalid.
138    InvalidBase64(base64::DecodeError),
139    /// The suffix is not known to this ssb implementation.
140    UnknownSuffix,
141    /// The suffix declares a sha256 hash, but the data length does not match.
142    Sha256WrongSize,
143}
144
145impl fmt::Display for DecodeLegacyError {
146    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
147        match self {
148            DecodeLegacyError::Sigil => write!(f, "Invalid sigil"),
149            DecodeLegacyError::InvalidBase64(ref err) => write!(f, "{}", err),
150            DecodeLegacyError::NoDot => write!(f, "No dot"),
151            DecodeLegacyError::UnknownSuffix => write!(f, "Unknown suffix"),
152            DecodeLegacyError::Sha256WrongSize => write!(f, "Data of wrong length"),
153        }
154    }
155}
156
157impl std::error::Error for DecodeLegacyError {}
158
159/// The legacy suffix indicating the sha256 cryptographic primitive.
160const SHA256_SUFFIX: &[u8] = b"sha256";
161/// Length of a base64 encoded sha256 hash digest.
162const SHA256_BASE64_LEN: usize = 44;
163/// Length of a legacy-encoded ssb `Multihash` which uses the sha256 cryptographic primitive.
164const SSB_SHA256_ENCODED_LEN: usize = SHA256_BASE64_LEN + 9;
165
166#[test]
167fn test_from_legacy() {
168    assert!(
169        Multihash::from_legacy(b"%MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rc=.sha256").is_ok()
170    );
171    assert!(
172        Multihash::from_legacy(b"&MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rc=.sha256").is_ok()
173    );
174    assert!(
175        Multihash::from_legacy(b"%MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rd=.sha256").is_err()
176    );
177    assert!(
178        Multihash::from_legacy(b"@MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rc=.sha256").is_err()
179    );
180    assert!(
181        Multihash::from_legacy(b"%MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rc=.tha256").is_err()
182    );
183    assert!(
184        Multihash::from_legacy(b"%MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rc=sha256").is_err()
185    );
186    assert!(
187        Multihash::from_legacy(b"%MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rc.sha256").is_err()
188    );
189    assert!(
190        Multihash::from_legacy(b"%MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rc==.sha256").is_err()
191    );
192}