1use std::collections::BTreeMap;
6use std::fmt;
7
8use anyhow::{Context as _, Result, bail};
9
10use crate::key::{DcKey, SignedPublicKey};
11
12#[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#[derive(Debug)]
42pub struct Aheader {
43 pub addr: String,
44 pub public_key: SignedPublicKey,
45 pub prefer_encrypt: EncryptPreference,
46
47 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 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 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 #[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 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 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 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}