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