deltachat/
aheader.rs

1//! # Autocrypt header module.
2//!
3//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
4
5use std::collections::BTreeMap;
6use std::fmt;
7use std::str::FromStr;
8
9use anyhow::{bail, Context as _, Error, Result};
10
11use crate::key::{DcKey, SignedPublicKey};
12
13/// Possible values for encryption preference
14#[derive(PartialEq, Eq, Debug, Default, Clone, Copy, FromPrimitive, ToPrimitive)]
15#[repr(u8)]
16pub enum EncryptPreference {
17    #[default]
18    NoPreference = 0,
19    Mutual = 1,
20    Reset = 20,
21}
22
23impl fmt::Display for EncryptPreference {
24    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
25        match *self {
26            EncryptPreference::Mutual => write!(fmt, "mutual"),
27            EncryptPreference::NoPreference => write!(fmt, "nopreference"),
28            EncryptPreference::Reset => write!(fmt, "reset"),
29        }
30    }
31}
32
33impl FromStr for EncryptPreference {
34    type Err = Error;
35
36    fn from_str(s: &str) -> Result<Self> {
37        match s {
38            "mutual" => Ok(EncryptPreference::Mutual),
39            "nopreference" => Ok(EncryptPreference::NoPreference),
40            _ => bail!("Cannot parse encryption preference {}", s),
41        }
42    }
43}
44
45/// Autocrypt header
46#[derive(Debug)]
47pub struct Aheader {
48    pub addr: String,
49    pub public_key: SignedPublicKey,
50    pub prefer_encrypt: EncryptPreference,
51}
52
53impl Aheader {
54    /// Creates new autocrypt header
55    pub fn new(
56        addr: String,
57        public_key: SignedPublicKey,
58        prefer_encrypt: EncryptPreference,
59    ) -> Self {
60        Aheader {
61            addr,
62            public_key,
63            prefer_encrypt,
64        }
65    }
66}
67
68impl fmt::Display for Aheader {
69    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
70        write!(fmt, "addr={};", self.addr.to_lowercase())?;
71        if self.prefer_encrypt == EncryptPreference::Mutual {
72            write!(fmt, " prefer-encrypt=mutual;")?;
73        }
74
75        // adds a whitespace every 78 characters, this allows
76        // email crate to wrap the lines according to RFC 5322
77        // (which may insert a linebreak before every whitespace)
78        let keydata = self.public_key.to_base64().chars().enumerate().fold(
79            String::new(),
80            |mut res, (i, c)| {
81                if i % 78 == 78 - "keydata=".len() {
82                    res.push(' ')
83                }
84                res.push(c);
85                res
86            },
87        );
88        write!(fmt, " keydata={keydata}")
89    }
90}
91
92impl FromStr for Aheader {
93    type Err = Error;
94
95    fn from_str(s: &str) -> Result<Self> {
96        let mut attributes: BTreeMap<String, String> = s
97            .split(';')
98            .filter_map(|a| {
99                let attribute: Vec<&str> = a.trim().splitn(2, '=').collect();
100                match &attribute[..] {
101                    [key, value] => Some((key.trim().to_string(), value.trim().to_string())),
102                    _ => None,
103                }
104            })
105            .collect();
106
107        let addr = match attributes.remove("addr") {
108            Some(addr) => addr,
109            None => bail!("Autocrypt header has no addr"),
110        };
111        let public_key: SignedPublicKey = attributes
112            .remove("keydata")
113            .context("keydata attribute is not found")
114            .and_then(|raw| {
115                SignedPublicKey::from_base64(&raw).context("autocrypt key cannot be decoded")
116            })
117            .and_then(|key| {
118                key.verify()
119                    .and(Ok(key))
120                    .context("autocrypt key cannot be verified")
121            })?;
122
123        let prefer_encrypt = attributes
124            .remove("prefer-encrypt")
125            .and_then(|raw| raw.parse().ok())
126            .unwrap_or_default();
127
128        // Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
129        // Autocrypt-Level0: unknown attribute, treat the header as invalid
130        if attributes.keys().any(|k| !k.starts_with('_')) {
131            bail!("Unknown Autocrypt attribute found");
132        }
133
134        Ok(Aheader {
135            addr,
136            public_key,
137            prefer_encrypt,
138        })
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    const RAWKEY: &str = "xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJgWL7O3y/g4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6CJx8Wsv8ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKKbhH7Lu8880iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1KvVL3gTTllHOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbGUuZGU+wsCJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087LscCFPiFNyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VNHtODXjAgdLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Dddfxo3ao72rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCvSJa61i+v81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vauf1zK8JgCu3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+G7uXMMOtkb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjmkRl3ToKCLhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09/JM/Fw4sWVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHRTR1pcrWvBuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivXurm8zwGrwdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9MtrmZkZw+ktEk6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb+F//f0k0j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNgwm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc=";
147
148    #[test]
149    fn test_from_str() -> Result<()> {
150        let h: Aheader =
151            format!("addr=me@mail.com; prefer-encrypt=mutual; keydata={RAWKEY}").parse()?;
152
153        assert_eq!(h.addr, "me@mail.com");
154        assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
155        Ok(())
156    }
157
158    // EncryptPreference::Reset is an internal value, parser should never return it
159    #[test]
160    fn test_from_str_reset() -> Result<()> {
161        let raw = format!("addr=reset@example.com; prefer-encrypt=reset; keydata={RAWKEY}");
162        let h: Aheader = raw.parse()?;
163
164        assert_eq!(h.addr, "reset@example.com");
165        assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
166        Ok(())
167    }
168
169    #[test]
170    fn test_from_str_non_critical() -> Result<()> {
171        let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={RAWKEY}");
172        let h: Aheader = raw.parse()?;
173
174        assert_eq!(h.addr, "me@mail.com");
175        assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
176        Ok(())
177    }
178
179    #[test]
180    fn test_from_str_superflous_critical() {
181        let raw = format!("addr=me@mail.com; _foo=one; _bar=two; other=me; keydata={RAWKEY}");
182        assert!(raw.parse::<Aheader>().is_err());
183    }
184
185    #[test]
186    fn test_good_headers() -> Result<()> {
187        let fixed_header = concat!(
188            "addr=a@b.example.org; prefer-encrypt=mutual; ",
189            "keydata=xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJg",
190            " WL7O3y/g4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6",
191            " CJx8Wsv8ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKK",
192            " bhH7Lu8880iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1Kv",
193            " VL3gTTllHOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbG",
194            " UuZGU+wsCJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaK",
195            " rc9mtiMLAAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087",
196            " LscCFPiFNyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VN",
197            " HtODXjAgdLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Ddd",
198            " fxo3ao72rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCv",
199            " SJa61i+v81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vau",
200            " f1zK8JgCu3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+",
201            " G7uXMMOtkb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjm",
202            " kRl3ToKCLhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09",
203            " /JM/Fw4sWVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHR",
204            " TR1pcrWvBuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaK",
205            " rc9mtiMLAAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivX",
206            " urm8zwGrwdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9Mtrm",
207            " ZkZw+ktEk6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb",
208            " +F//f0k0j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNg",
209            " wm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc="
210        );
211
212        let ah = Aheader::from_str(fixed_header)?;
213        assert_eq!(ah.addr, "a@b.example.org");
214        assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
215        assert_eq!(format!("{ah}"), fixed_header);
216
217        let rendered = ah.to_string();
218        assert_eq!(rendered, fixed_header);
219
220        let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n   prefer-encrypt = mutual ; keydata = {RAWKEY}"))?;
221        assert_eq!(ah.addr, "a@b.example.org");
222        assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
223
224        Aheader::from_str(&format!(
225            "addr=a@b.example.org; prefer-encrypt=ignoreUnknownValues; keydata={RAWKEY}"
226        ))?;
227
228        Aheader::from_str(&format!("addr=a@b.example.org; keydata={RAWKEY}"))?;
229        Ok(())
230    }
231
232    #[test]
233    fn test_bad_headers() {
234        assert!(Aheader::from_str("").is_err());
235        assert!(Aheader::from_str("foo").is_err());
236        assert!(Aheader::from_str("\n\n\n").is_err());
237        assert!(Aheader::from_str(" ;;").is_err());
238        assert!(Aheader::from_str("addr=a@t.de; unknown=1; keydata=jau").is_err());
239    }
240
241    #[test]
242    fn test_display_aheader() {
243        assert!(format!(
244            "{}",
245            Aheader::new(
246                "test@example.com".to_string(),
247                SignedPublicKey::from_base64(RAWKEY).unwrap(),
248                EncryptPreference::Mutual
249            )
250        )
251        .contains("prefer-encrypt=mutual;"));
252
253        // According to Autocrypt Level 1 specification,
254        // only "prefer-encrypt=mutual;" can be used.
255        // If the setting is nopreference, the whole attribute is omitted.
256        assert!(!format!(
257            "{}",
258            Aheader::new(
259                "test@example.com".to_string(),
260                SignedPublicKey::from_base64(RAWKEY).unwrap(),
261                EncryptPreference::NoPreference
262            )
263        )
264        .contains("prefer-encrypt"));
265
266        // Always lowercase the address in the header.
267        assert!(format!(
268            "{}",
269            Aheader::new(
270                "TeSt@eXaMpLe.cOm".to_string(),
271                SignedPublicKey::from_base64(RAWKEY).unwrap(),
272                EncryptPreference::Mutual
273            )
274        )
275        .contains("test@example.com"));
276    }
277}