Skip to main content

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