deltachat/imex/
key_transfer.rs

1//! # Key transfer via Autocrypt Setup Message.
2use std::io::BufReader;
3
4use rand::{thread_rng, Rng};
5
6use anyhow::{bail, ensure, Result};
7
8use crate::blob::BlobObject;
9use crate::chat::{self, ChatId};
10use crate::config::Config;
11use crate::constants::{ASM_BODY, ASM_SUBJECT};
12use crate::contact::ContactId;
13use crate::context::Context;
14use crate::imex::set_self_key;
15use crate::key::{load_self_secret_key, DcKey};
16use crate::message::{Message, MsgId, Viewtype};
17use crate::mimeparser::SystemMessage;
18use crate::param::Param;
19use crate::pgp;
20use crate::tools::open_file_std;
21
22/// Initiates key transfer via Autocrypt Setup Message.
23///
24/// Returns setup code.
25pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
26    let setup_code = create_setup_code(context);
27    /* this may require a keypair to be created. this may take a second ... */
28    let setup_file_content = render_setup_file(context, &setup_code).await?;
29    /* encrypting may also take a while ... */
30    let setup_file_blob = BlobObject::create_and_deduplicate_from_bytes(
31        context,
32        setup_file_content.as_bytes(),
33        "autocrypt-setup-message.html",
34    )?;
35
36    let chat_id = ChatId::create_for_contact(context, ContactId::SELF).await?;
37    let mut msg = Message::new(Viewtype::File);
38    msg.param.set(Param::File, setup_file_blob.as_name());
39    msg.param
40        .set(Param::Filename, "autocrypt-setup-message.html");
41    msg.subject = ASM_SUBJECT.to_owned();
42    msg.param
43        .set(Param::MimeType, "application/autocrypt-setup");
44    msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
45    msg.force_plaintext();
46    msg.param.set_int(Param::SkipAutocrypt, 1);
47
48    // Enable BCC-self, because transferring a key
49    // means we have a multi-device setup.
50    context.set_config_bool(Config::BccSelf, true).await?;
51
52    chat::send_msg(context, chat_id, &mut msg).await?;
53    Ok(setup_code)
54}
55
56/// Continue key transfer via Autocrypt Setup Message.
57///
58/// `msg_id` is the ID of the received Autocrypt Setup Message.
59/// `setup_code` is the code entered by the user.
60pub async fn continue_key_transfer(
61    context: &Context,
62    msg_id: MsgId,
63    setup_code: &str,
64) -> Result<()> {
65    ensure!(!msg_id.is_special(), "wrong id");
66
67    let msg = Message::load_from_db(context, msg_id).await?;
68    ensure!(
69        msg.is_setupmessage(),
70        "Message is no Autocrypt Setup Message."
71    );
72
73    if let Some(filename) = msg.get_file(context) {
74        let file = open_file_std(context, filename)?;
75        let sc = normalize_setup_code(setup_code);
76        let armored_key = decrypt_setup_file(&sc, BufReader::new(file)).await?;
77        set_self_key(context, &armored_key).await?;
78        context.set_config_bool(Config::BccSelf, true).await?;
79
80        Ok(())
81    } else {
82        bail!("Message is no Autocrypt Setup Message.");
83    }
84}
85
86/// Renders HTML body of a setup file message.
87///
88/// The `passphrase` must be at least 2 characters long.
89pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
90    let passphrase_begin = if let Some(passphrase_begin) = passphrase.get(..2) {
91        passphrase_begin
92    } else {
93        bail!("Passphrase must be at least 2 chars long.");
94    };
95    let private_key = load_self_secret_key(context).await?;
96    let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await? {
97        false => None,
98        true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
99    };
100    let private_key_asc = private_key.to_asc(ac_headers);
101    let encr = pgp::symm_encrypt(passphrase, private_key_asc.into_bytes())
102        .await?
103        .replace('\n', "\r\n");
104
105    let replacement = format!(
106        concat!(
107            "-----BEGIN PGP MESSAGE-----\r\n",
108            "Passphrase-Format: numeric9x4\r\n",
109            "Passphrase-Begin: {}"
110        ),
111        passphrase_begin
112    );
113    let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
114
115    let msg_subj = ASM_SUBJECT;
116    let msg_body = ASM_BODY.to_string();
117    let msg_body_html = msg_body.replace('\r', "").replace('\n', "<br>");
118    Ok(format!(
119        concat!(
120            "<!DOCTYPE html>\r\n",
121            "<html>\r\n",
122            "  <head>\r\n",
123            "    <title>{}</title>\r\n",
124            "  </head>\r\n",
125            "  <body>\r\n",
126            "    <h1>{}</h1>\r\n",
127            "    <p>{}</p>\r\n",
128            "    <pre>\r\n{}\r\n</pre>\r\n",
129            "  </body>\r\n",
130            "</html>\r\n"
131        ),
132        msg_subj, msg_subj, msg_body_html, pgp_msg
133    ))
134}
135
136/// Creates a new setup code for Autocrypt Setup Message.
137fn create_setup_code(_context: &Context) -> String {
138    let mut random_val: u16;
139    let mut rng = thread_rng();
140    let mut ret = String::new();
141
142    for i in 0..9 {
143        loop {
144            random_val = rng.gen();
145            if random_val as usize <= 60000 {
146                break;
147            }
148        }
149        random_val = (random_val as usize % 10000) as u16;
150        ret += &format!(
151            "{}{:04}",
152            if 0 != i { "-" } else { "" },
153            random_val as usize
154        );
155    }
156
157    ret
158}
159
160async fn decrypt_setup_file<T: std::fmt::Debug + std::io::BufRead + Send + 'static>(
161    passphrase: &str,
162    file: T,
163) -> Result<String> {
164    let plain_bytes = pgp::symm_decrypt(passphrase, file).await?;
165    let plain_text = std::string::String::from_utf8(plain_bytes)?;
166
167    Ok(plain_text)
168}
169
170fn normalize_setup_code(s: &str) -> String {
171    let mut out = String::new();
172    for c in s.chars() {
173        if c.is_ascii_digit() {
174            out.push(c);
175            if let 4 | 9 | 14 | 19 | 24 | 29 | 34 | 39 = out.len() {
176                out += "-"
177            }
178        }
179    }
180    out
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
188    use crate::receive_imf::receive_imf;
189    use crate::test_utils::{TestContext, TestContextManager};
190    use ::pgp::armor::BlockType;
191
192    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
193    async fn test_render_setup_file() {
194        let t = TestContext::new_alice().await;
195        let msg = render_setup_file(&t, "hello").await.unwrap();
196        println!("{}", &msg);
197        // Check some substrings, indicating things got substituted.
198        assert!(msg.contains("<title>Autocrypt Setup Message</title"));
199        assert!(msg.contains("<h1>Autocrypt Setup Message</h1>"));
200        assert!(msg.contains("<p>This is the Autocrypt Setup Message used to"));
201        assert!(msg.contains("-----BEGIN PGP MESSAGE-----\r\n"));
202        assert!(msg.contains("Passphrase-Format: numeric9x4\r\n"));
203        assert!(msg.contains("Passphrase-Begin: he\r\n"));
204        assert!(msg.contains("-----END PGP MESSAGE-----\r\n"));
205
206        for line in msg.rsplit_terminator('\n') {
207            assert!(line.ends_with('\r'));
208        }
209    }
210
211    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
212    async fn test_render_setup_file_newline_replace() {
213        let t = TestContext::new_alice().await;
214        let msg = render_setup_file(&t, "pw").await.unwrap();
215        println!("{}", &msg);
216        assert!(msg.contains("<p>This is the Autocrypt Setup Message used to transfer your end-to-end setup between clients.<br>"));
217    }
218
219    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
220    async fn test_create_setup_code() {
221        let t = TestContext::new().await;
222        let setupcode = create_setup_code(&t);
223        assert_eq!(setupcode.len(), 44);
224        assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
225        assert_eq!(setupcode.chars().nth(9).unwrap(), '-');
226        assert_eq!(setupcode.chars().nth(14).unwrap(), '-');
227        assert_eq!(setupcode.chars().nth(19).unwrap(), '-');
228        assert_eq!(setupcode.chars().nth(24).unwrap(), '-');
229        assert_eq!(setupcode.chars().nth(29).unwrap(), '-');
230        assert_eq!(setupcode.chars().nth(34).unwrap(), '-');
231        assert_eq!(setupcode.chars().nth(39).unwrap(), '-');
232    }
233
234    #[test]
235    fn test_normalize_setup_code() {
236        let norm = normalize_setup_code("123422343234423452346234723482349234");
237        assert_eq!(norm, "1234-2234-3234-4234-5234-6234-7234-8234-9234");
238
239        let norm =
240            normalize_setup_code("\t1 2 3422343234- foo bar-- 423-45 2 34 6234723482349234      ");
241        assert_eq!(norm, "1234-2234-3234-4234-5234-6234-7234-8234-9234");
242    }
243
244    /* S_EM_SETUPFILE is a AES-256 symm. encrypted setup message created by Enigmail
245    with an "encrypted session key", see RFC 4880.  The code is in S_EM_SETUPCODE */
246    const S_EM_SETUPCODE: &str = "1742-0185-6197-1303-7016-8412-3581-4441-0597";
247    const S_EM_SETUPFILE: &str = include_str!("../../test-data/message/stress.txt");
248
249    // Autocrypt Setup Message payload "encrypted" with plaintext algorithm.
250    const S_PLAINTEXT_SETUPFILE: &str =
251        include_str!("../../test-data/message/plaintext-autocrypt-setup.txt");
252
253    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
254    async fn test_split_and_decrypt() {
255        let buf_1 = S_EM_SETUPFILE.as_bytes().to_vec();
256        let (typ, headers, base64) = split_armored_data(&buf_1).unwrap();
257        assert_eq!(typ, BlockType::Message);
258        assert!(S_EM_SETUPCODE.starts_with(headers.get(HEADER_SETUPCODE).unwrap()));
259        assert!(!headers.contains_key(HEADER_AUTOCRYPT));
260
261        assert!(!base64.is_empty());
262
263        let setup_file = S_EM_SETUPFILE;
264        let decrypted = decrypt_setup_file(S_EM_SETUPCODE, setup_file.as_bytes())
265            .await
266            .unwrap();
267
268        let (typ, headers, _base64) = split_armored_data(decrypted.as_bytes()).unwrap();
269
270        assert_eq!(typ, BlockType::PrivateKey);
271        assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
272        assert!(!headers.contains_key(HEADER_SETUPCODE));
273    }
274
275    /// Tests that Autocrypt Setup Message encrypted with "plaintext" algorithm cannot be
276    /// decrypted.
277    ///
278    /// According to <https://datatracker.ietf.org/doc/html/rfc4880#section-13.4>
279    /// "Implementations MUST NOT use plaintext in Symmetrically Encrypted Data packets".
280    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
281    async fn test_decrypt_plaintext_autocrypt_setup_message() {
282        let setup_file = S_PLAINTEXT_SETUPFILE;
283        let incorrect_setupcode = "0000-0000-0000-0000-0000-0000-0000-0000-0000";
284        assert!(
285            decrypt_setup_file(incorrect_setupcode, setup_file.as_bytes(),)
286                .await
287                .is_err()
288        );
289    }
290
291    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
292    async fn test_key_transfer() -> Result<()> {
293        let alice = TestContext::new_alice().await;
294
295        alice.set_config(Config::BccSelf, Some("0")).await?;
296        let setup_code = initiate_key_transfer(&alice).await?;
297
298        // Test that sending Autocrypt Setup Message enables `bcc_self`.
299        assert_eq!(alice.get_config_bool(Config::BccSelf).await?, true);
300
301        // Get Autocrypt Setup Message.
302        let sent = alice.pop_sent_msg().await;
303
304        // Alice sets up a second device.
305        let alice2 = TestContext::new().await;
306        alice2.set_name("alice2");
307        alice2.configure_addr("alice@example.org").await;
308        alice2.recv_msg(&sent).await;
309        let msg = alice2.get_last_msg().await;
310        assert!(msg.is_setupmessage());
311        assert_eq!(
312            crate::key::load_self_secret_keyring(&alice2).await?.len(),
313            0
314        );
315
316        // Transfer the key.
317        alice2.set_config(Config::BccSelf, Some("0")).await?;
318        continue_key_transfer(&alice2, msg.id, &setup_code).await?;
319        assert_eq!(alice2.get_config_bool(Config::BccSelf).await?, true);
320        assert_eq!(
321            crate::key::load_self_secret_keyring(&alice2).await?.len(),
322            1
323        );
324
325        // Alice sends a message to self from the new device.
326        let sent = alice2.send_text(msg.chat_id, "Test").await;
327        let rcvd_msg = alice.recv_msg(&sent).await;
328        assert_eq!(rcvd_msg.get_text(), "Test");
329
330        Ok(())
331    }
332
333    /// Tests that Autocrypt Setup Messages is only clickable if it is self-sent.
334    /// This prevents Bob from tricking Alice into changing the key
335    /// by sending her an Autocrypt Setup Message as long as Alice's server
336    /// does not allow to forge the `From:` header.
337    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
338    async fn test_key_transfer_non_self_sent() -> Result<()> {
339        let mut tcm = TestContextManager::new();
340        let alice = tcm.alice().await;
341        let bob = tcm.bob().await;
342
343        let _setup_code = initiate_key_transfer(&alice).await?;
344
345        // Get Autocrypt Setup Message.
346        let sent = alice.pop_sent_msg().await;
347
348        let rcvd = bob.recv_msg(&sent).await;
349        assert!(!rcvd.is_setupmessage());
350
351        Ok(())
352    }
353
354    /// Tests reception of Autocrypt Setup Message from K-9 6.802.
355    ///
356    /// Unlike Autocrypt Setup Message sent by Delta Chat,
357    /// this message does not contain `Autocrypt-Prefer-Encrypt` header.
358    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
359    async fn test_key_transfer_k_9() -> Result<()> {
360        let t = &TestContext::new().await;
361        t.configure_addr("autocrypt@nine.testrun.org").await;
362
363        let raw = include_bytes!("../../test-data/message/k-9-autocrypt-setup-message.eml");
364        let received = receive_imf(t, raw, false).await?.unwrap();
365
366        let setup_code = "0655-9868-8252-5455-4232-5158-1237-5333-2638";
367        continue_key_transfer(t, *received.msg_ids.last().unwrap(), setup_code).await?;
368
369        Ok(())
370    }
371}