mirror of
https://github.com/chatmail/core.git
synced 2026-05-04 05:46:29 +03:00
api!: remove functions for sending and receiving Autocrypt Setup Message
This commit is contained in:
@@ -2844,9 +2844,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
let lowercase_from = from.to_lowercase();
|
||||
|
||||
recipients.retain(|x| x.to_lowercase() != lowercase_from);
|
||||
if context.get_config_bool(Config::BccSelf).await?
|
||||
|| msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage
|
||||
{
|
||||
if context.get_config_bool(Config::BccSelf).await? {
|
||||
smtp::add_self_recipients(context, &mut recipients, needs_encryption).await?;
|
||||
}
|
||||
|
||||
|
||||
@@ -866,7 +866,6 @@ async fn test_add_device_msg_unlabelled() {
|
||||
assert_eq!(msg1.from_id, ContactId::DEVICE);
|
||||
assert_eq!(msg1.to_id, ContactId::SELF);
|
||||
assert!(!msg1.is_info());
|
||||
assert!(!msg1.is_setupmessage());
|
||||
|
||||
let msg2 = message::Message::load_from_db(&t, msg2_id.unwrap()).await;
|
||||
assert!(msg2.is_ok());
|
||||
@@ -899,7 +898,6 @@ async fn test_add_device_msg_labelled() -> Result<()> {
|
||||
assert_eq!(msg1.from_id, ContactId::DEVICE);
|
||||
assert_eq!(msg1.to_id, ContactId::SELF);
|
||||
assert!(!msg1.is_info());
|
||||
assert!(!msg1.is_setupmessage());
|
||||
|
||||
// check device chat
|
||||
let chat_id = msg1.chat_id;
|
||||
|
||||
@@ -234,19 +234,6 @@ pub(crate) const TIMESTAMP_SENT_TOLERANCE: i64 = 60;
|
||||
// Newer Delta Chats will remove the prefix as needed.
|
||||
pub(crate) const EDITED_PREFIX: &str = "✏️";
|
||||
|
||||
// Strings needed to render the Autocrypt Setup Message.
|
||||
// Left untranslated as not being supported/recommended workflow and as translations would require deep knowledge.
|
||||
pub(crate) const ASM_SUBJECT: &str = "Autocrypt Setup Message";
|
||||
pub(crate) const ASM_BODY: &str = "This is the Autocrypt Setup Message \
|
||||
used to transfer your end-to-end setup between clients.
|
||||
|
||||
To decrypt and use your setup, \
|
||||
open the message in an Autocrypt-compliant client \
|
||||
and enter the setup code presented on the generating device.
|
||||
|
||||
If you see this message in a chatmail client (Delta Chat, Arcane Chat, Delta Touch ...), \
|
||||
use \"Settings / Add Second Device\" instead.";
|
||||
|
||||
/// Period between `sql::housekeeping()` runs.
|
||||
pub(crate) const HOUSEKEEPING_PERIOD: i64 = 24 * 60 * 60;
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ pub enum EventType {
|
||||
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
|
||||
///
|
||||
/// However, for ongoing processes (eg. configure())
|
||||
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
|
||||
/// or for functions that are expected to fail
|
||||
/// it might be better to delay showing these events until the function has really
|
||||
/// failed (returned false). It should be sufficient to report only the *last* error
|
||||
/// in a message box then.
|
||||
|
||||
@@ -208,10 +208,10 @@ mod tests {
|
||||
/// Test that headers are parsed case-insensitively
|
||||
fn test_get_header_value_case() {
|
||||
let (headers, _) =
|
||||
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-SeTup-MessAge: v99").unwrap();
|
||||
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-GoSsIp: fooBaR").unwrap();
|
||||
assert_eq!(
|
||||
headers.get_header_value(HeaderDef::AutocryptSetupMessage),
|
||||
Some("v99".to_string())
|
||||
headers.get_header_value(HeaderDef::AutocryptGossip),
|
||||
Some("fooBaR".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get_header_value(HeaderDef::From_),
|
||||
|
||||
@@ -1974,15 +1974,6 @@ async fn needs_move_to_mvbox(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if headers
|
||||
.get_header_value(HeaderDef::AutocryptSetupMessage)
|
||||
.is_some()
|
||||
{
|
||||
// do not move setup messages;
|
||||
// there may be a non-delta device that wants to handle it
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if has_chat_version {
|
||||
Ok(true)
|
||||
} else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
|
||||
|
||||
@@ -105,10 +105,9 @@ async fn check_target_folder_combination(
|
||||
expected_destination: &str,
|
||||
accepted_chat: bool,
|
||||
outgoing: bool,
|
||||
setupmessage: bool,
|
||||
) -> Result<()> {
|
||||
println!(
|
||||
"Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}"
|
||||
"Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}"
|
||||
);
|
||||
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -125,9 +124,7 @@ async fn check_target_folder_combination(
|
||||
}
|
||||
let temp;
|
||||
|
||||
let bytes = if setupmessage {
|
||||
include_bytes!("../../test-data/message/AutocryptSetupMessage.eml")
|
||||
} else {
|
||||
let bytes = {
|
||||
temp = format!(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
{}\
|
||||
@@ -164,7 +161,7 @@ async fn check_target_folder_combination(
|
||||
assert_eq!(
|
||||
expected,
|
||||
actual.as_deref(),
|
||||
"For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}"
|
||||
"For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}: expected {expected:?}, got {actual:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -204,7 +201,6 @@ async fn test_target_folder_incoming_accepted() -> Result<()> {
|
||||
expected_destination,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -221,7 +217,6 @@ async fn test_target_folder_incoming_request() -> Result<()> {
|
||||
expected_destination,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -239,25 +234,6 @@ async fn test_target_folder_outgoing() -> Result<()> {
|
||||
expected_destination,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_setupmsg() -> Result<()> {
|
||||
// Test setupmessages
|
||||
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -28,11 +28,9 @@ use crate::tools::{
|
||||
write_file,
|
||||
};
|
||||
|
||||
mod key_transfer;
|
||||
mod transfer;
|
||||
|
||||
use ::pgp::types::KeyDetails;
|
||||
pub use key_transfer::{continue_key_transfer, initiate_key_transfer};
|
||||
pub use transfer::{BackupProvider, get_backup};
|
||||
|
||||
// Name of the database file in the backup.
|
||||
|
||||
@@ -1,363 +0,0 @@
|
||||
//! # Key transfer via Autocrypt Setup Message.
|
||||
use std::io::BufReader;
|
||||
|
||||
use anyhow::{Result, bail, ensure};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{ASM_BODY, ASM_SUBJECT};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::imex::set_self_key;
|
||||
use crate::key::{DcKey, load_self_secret_key};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::pgp;
|
||||
use crate::tools::open_file_std;
|
||||
|
||||
/// Initiates key transfer via Autocrypt Setup Message.
|
||||
///
|
||||
/// Returns setup code.
|
||||
pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
let setup_code = create_setup_code(context);
|
||||
/* this may require a keypair to be created. this may take a second ... */
|
||||
let setup_file_content = render_setup_file(context, &setup_code).await?;
|
||||
/* encrypting may also take a while ... */
|
||||
let setup_file_blob = BlobObject::create_and_deduplicate_from_bytes(
|
||||
context,
|
||||
setup_file_content.as_bytes(),
|
||||
"autocrypt-setup-message.html",
|
||||
)?;
|
||||
|
||||
let chat_id = ChatId::create_for_contact(context, ContactId::SELF).await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.param.set(Param::File, setup_file_blob.as_name());
|
||||
msg.param
|
||||
.set(Param::Filename, "autocrypt-setup-message.html");
|
||||
msg.subject = ASM_SUBJECT.to_owned();
|
||||
msg.param
|
||||
.set(Param::MimeType, "application/autocrypt-setup");
|
||||
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
msg.force_plaintext();
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
|
||||
// Enable BCC-self, because transferring a key
|
||||
// means we have a multi-device setup.
|
||||
context.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
Ok(setup_code)
|
||||
}
|
||||
|
||||
/// Continue key transfer via Autocrypt Setup Message.
|
||||
///
|
||||
/// `msg_id` is the ID of the received Autocrypt Setup Message.
|
||||
/// `setup_code` is the code entered by the user.
|
||||
pub async fn continue_key_transfer(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
setup_code: &str,
|
||||
) -> Result<()> {
|
||||
ensure!(!msg_id.is_special(), "wrong id");
|
||||
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
ensure!(
|
||||
msg.is_setupmessage(),
|
||||
"Message is no Autocrypt Setup Message."
|
||||
);
|
||||
|
||||
if let Some(filename) = msg.get_file(context) {
|
||||
let file = open_file_std(context, filename)?;
|
||||
let sc = normalize_setup_code(setup_code);
|
||||
let armored_key = decrypt_setup_file(&sc, BufReader::new(file)).await?;
|
||||
set_self_key(context, &armored_key).await?;
|
||||
context.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Message is no Autocrypt Setup Message.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders HTML body of a setup file message.
|
||||
///
|
||||
/// The `passphrase` must be at least 2 characters long.
|
||||
pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
|
||||
let passphrase_begin = if let Some(passphrase_begin) = passphrase.get(..2) {
|
||||
passphrase_begin
|
||||
} else {
|
||||
bail!("Passphrase must be at least 2 chars long.");
|
||||
};
|
||||
let private_key = load_self_secret_key(context).await?;
|
||||
let ac_headers = Some(("Autocrypt-Prefer-Encrypt", "mutual"));
|
||||
let private_key_asc = private_key.to_asc(ac_headers);
|
||||
let encr = pgp::symm_encrypt_autocrypt_setup(passphrase, private_key_asc.into_bytes())
|
||||
.await?
|
||||
.replace('\n', "\r\n");
|
||||
|
||||
let replacement = format!(
|
||||
concat!(
|
||||
"-----BEGIN PGP MESSAGE-----\r\n",
|
||||
"Passphrase-Format: numeric9x4\r\n",
|
||||
"Passphrase-Begin: {}"
|
||||
),
|
||||
passphrase_begin
|
||||
);
|
||||
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
|
||||
|
||||
let msg_subj = ASM_SUBJECT;
|
||||
let msg_body = ASM_BODY.to_string();
|
||||
let msg_body_html = msg_body.replace('\r', "").replace('\n', "<br>");
|
||||
Ok(format!(
|
||||
concat!(
|
||||
"<!DOCTYPE html>\r\n",
|
||||
"<html>\r\n",
|
||||
" <head>\r\n",
|
||||
" <title>{}</title>\r\n",
|
||||
" </head>\r\n",
|
||||
" <body>\r\n",
|
||||
" <h1>{}</h1>\r\n",
|
||||
" <p>{}</p>\r\n",
|
||||
" <pre>\r\n{}\r\n</pre>\r\n",
|
||||
" </body>\r\n",
|
||||
"</html>\r\n"
|
||||
),
|
||||
msg_subj, msg_subj, msg_body_html, pgp_msg
|
||||
))
|
||||
}
|
||||
|
||||
/// Creates a new setup code for Autocrypt Setup Message.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn create_setup_code(_context: &Context) -> String {
|
||||
let mut random_val: u16;
|
||||
let mut ret = String::new();
|
||||
|
||||
for i in 0..9 {
|
||||
loop {
|
||||
random_val = rand::random();
|
||||
if random_val as usize <= 60000 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
random_val = (random_val as usize % 10000) as u16;
|
||||
ret += &format!(
|
||||
"{}{:04}",
|
||||
if 0 != i { "-" } else { "" },
|
||||
random_val as usize
|
||||
);
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
async fn decrypt_setup_file<T: std::fmt::Debug + std::io::BufRead + Send + 'static>(
|
||||
passphrase: &str,
|
||||
file: T,
|
||||
) -> Result<String> {
|
||||
let plain_bytes = pgp::symm_decrypt(passphrase, file).await?;
|
||||
let plain_text = std::string::String::from_utf8(plain_bytes)?;
|
||||
|
||||
Ok(plain_text)
|
||||
}
|
||||
|
||||
fn normalize_setup_code(s: &str) -> String {
|
||||
let mut out = String::new();
|
||||
for c in s.chars() {
|
||||
if c.is_ascii_digit() {
|
||||
out.push(c);
|
||||
if let 4 | 9 | 14 | 19 | 24 | 29 | 34 | 39 = out.len() {
|
||||
out += "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::pgp::{HEADER_AUTOCRYPT, HEADER_SETUPCODE, split_armored_data};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use ::pgp::armor::BlockType;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_render_setup_file() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let msg = render_setup_file(&t, "hello").await.unwrap();
|
||||
println!("{}", &msg);
|
||||
// Check some substrings, indicating things got substituted.
|
||||
assert!(msg.contains("<title>Autocrypt Setup Message</title"));
|
||||
assert!(msg.contains("<h1>Autocrypt Setup Message</h1>"));
|
||||
assert!(msg.contains("<p>This is the Autocrypt Setup Message used to"));
|
||||
assert!(msg.contains("-----BEGIN PGP MESSAGE-----\r\n"));
|
||||
assert!(msg.contains("Passphrase-Format: numeric9x4\r\n"));
|
||||
assert!(msg.contains("Passphrase-Begin: he\r\n"));
|
||||
assert!(msg.contains("-----END PGP MESSAGE-----\r\n"));
|
||||
|
||||
for line in msg.rsplit_terminator('\n') {
|
||||
assert!(line.ends_with('\r'));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_render_setup_file_newline_replace() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let msg = render_setup_file(&t, "pw").await.unwrap();
|
||||
println!("{}", &msg);
|
||||
assert!(msg.contains("<p>This is the Autocrypt Setup Message used to transfer your end-to-end setup between clients.<br>"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_setup_code() {
|
||||
let t = TestContext::new().await;
|
||||
let setupcode = create_setup_code(&t);
|
||||
assert_eq!(setupcode.len(), 44);
|
||||
assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
|
||||
assert_eq!(setupcode.chars().nth(9).unwrap(), '-');
|
||||
assert_eq!(setupcode.chars().nth(14).unwrap(), '-');
|
||||
assert_eq!(setupcode.chars().nth(19).unwrap(), '-');
|
||||
assert_eq!(setupcode.chars().nth(24).unwrap(), '-');
|
||||
assert_eq!(setupcode.chars().nth(29).unwrap(), '-');
|
||||
assert_eq!(setupcode.chars().nth(34).unwrap(), '-');
|
||||
assert_eq!(setupcode.chars().nth(39).unwrap(), '-');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_setup_code() {
|
||||
let norm = normalize_setup_code("123422343234423452346234723482349234");
|
||||
assert_eq!(norm, "1234-2234-3234-4234-5234-6234-7234-8234-9234");
|
||||
|
||||
let norm =
|
||||
normalize_setup_code("\t1 2 3422343234- foo bar-- 423-45 2 34 6234723482349234 ");
|
||||
assert_eq!(norm, "1234-2234-3234-4234-5234-6234-7234-8234-9234");
|
||||
}
|
||||
|
||||
/* S_EM_SETUPFILE is a AES-256 symm. encrypted setup message created by Enigmail
|
||||
with an "encrypted session key", see RFC 4880. The code is in S_EM_SETUPCODE */
|
||||
const S_EM_SETUPCODE: &str = "1742-0185-6197-1303-7016-8412-3581-4441-0597";
|
||||
const S_EM_SETUPFILE: &str = include_str!("../../test-data/message/stress.txt");
|
||||
|
||||
// Autocrypt Setup Message payload "encrypted" with plaintext algorithm.
|
||||
const S_PLAINTEXT_SETUPFILE: &str =
|
||||
include_str!("../../test-data/message/plaintext-autocrypt-setup.txt");
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_split_and_decrypt() {
|
||||
let buf_1 = S_EM_SETUPFILE.as_bytes().to_vec();
|
||||
let (typ, headers, base64) = split_armored_data(&buf_1).unwrap();
|
||||
assert_eq!(typ, BlockType::Message);
|
||||
assert!(S_EM_SETUPCODE.starts_with(headers.get(HEADER_SETUPCODE).unwrap()));
|
||||
assert!(!headers.contains_key(HEADER_AUTOCRYPT));
|
||||
|
||||
assert!(!base64.is_empty());
|
||||
|
||||
let setup_file = S_EM_SETUPFILE;
|
||||
let decrypted = decrypt_setup_file(S_EM_SETUPCODE, setup_file.as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (typ, headers, _base64) = split_armored_data(decrypted.as_bytes()).unwrap();
|
||||
|
||||
assert_eq!(typ, BlockType::PrivateKey);
|
||||
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
|
||||
assert!(!headers.contains_key(HEADER_SETUPCODE));
|
||||
}
|
||||
|
||||
/// Tests that Autocrypt Setup Message encrypted with "plaintext" algorithm cannot be
|
||||
/// decrypted.
|
||||
///
|
||||
/// According to <https://datatracker.ietf.org/doc/html/rfc4880#section-13.4>
|
||||
/// "Implementations MUST NOT use plaintext in Symmetrically Encrypted Data packets".
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decrypt_plaintext_autocrypt_setup_message() {
|
||||
let setup_file = S_PLAINTEXT_SETUPFILE;
|
||||
let incorrect_setupcode = "0000-0000-0000-0000-0000-0000-0000-0000-0000";
|
||||
assert!(
|
||||
decrypt_setup_file(incorrect_setupcode, setup_file.as_bytes(),)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_key_transfer() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
tcm.section("Alice sends Autocrypt setup message");
|
||||
alice.set_config(Config::BccSelf, Some("0")).await?;
|
||||
let setup_code = initiate_key_transfer(alice).await?;
|
||||
|
||||
// Test that sending Autocrypt Setup Message enables `bcc_self`.
|
||||
assert_eq!(alice.get_config_bool(Config::BccSelf).await?, true);
|
||||
|
||||
// Get Autocrypt Setup Message.
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
tcm.section("Alice sets up a second device");
|
||||
let alice2 = &tcm.unconfigured().await;
|
||||
alice2.set_name("alice2");
|
||||
alice2.configure_addr("alice@example.org").await;
|
||||
alice2.recv_msg(&sent).await;
|
||||
let msg = alice2.get_last_msg().await;
|
||||
assert!(msg.is_setupmessage());
|
||||
assert_eq!(crate::key::load_self_secret_keyring(alice2).await?.len(), 0);
|
||||
|
||||
// Transfer the key.
|
||||
tcm.section("Alice imports a key from Autocrypt Setup Message");
|
||||
alice2.set_config(Config::BccSelf, Some("0")).await?;
|
||||
continue_key_transfer(alice2, msg.id, &setup_code).await?;
|
||||
assert_eq!(alice2.get_config_bool(Config::BccSelf).await?, true);
|
||||
assert_eq!(crate::key::load_self_secret_keyring(alice2).await?.len(), 1);
|
||||
|
||||
// Alice sends a message to self from the new device.
|
||||
let sent = alice2.send_text(msg.chat_id, "Test").await;
|
||||
let rcvd_msg = alice.recv_msg(&sent).await;
|
||||
assert_eq!(rcvd_msg.get_text(), "Test");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that Autocrypt Setup Messages is only clickable if it is self-sent.
|
||||
/// This prevents Bob from tricking Alice into changing the key
|
||||
/// by sending her an Autocrypt Setup Message as long as Alice's server
|
||||
/// does not allow to forge the `From:` header.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_key_transfer_non_self_sent() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let _setup_code = initiate_key_transfer(&alice).await?;
|
||||
|
||||
// Get Autocrypt Setup Message.
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
assert!(!rcvd.is_setupmessage());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests reception of Autocrypt Setup Message from K-9 6.802.
|
||||
///
|
||||
/// Unlike Autocrypt Setup Message sent by Delta Chat,
|
||||
/// this message does not contain `Autocrypt-Prefer-Encrypt` header.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_key_transfer_k_9() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
t.configure_addr("autocrypt@nine.testrun.org").await;
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/k-9-autocrypt-setup-message.eml");
|
||||
let received = receive_imf(t, raw, false).await?.unwrap();
|
||||
|
||||
let setup_code = "0655-9868-8252-5455-4232-5158-1237-5333-2638";
|
||||
continue_key_transfer(t, *received.msg_ids.last().unwrap(), setup_code).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -756,8 +756,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
/// this resulted in various number of garbage
|
||||
/// octets at the end of the key, starting from 3 octets,
|
||||
/// but possibly 4 or 5 and maybe more octets
|
||||
/// if the key is imported or transferred
|
||||
/// using Autocrypt Setup Message multiple times.
|
||||
/// if the key is imported multiple times.
|
||||
#[test]
|
||||
fn test_ignore_trailing_garbage() {
|
||||
// Test several variants of garbage.
|
||||
|
||||
@@ -30,7 +30,6 @@ use crate::location::delete_poi_location;
|
||||
use crate::log::warn;
|
||||
use crate::mimeparser::{SystemMessage, parse_message_id};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::split_armored_data;
|
||||
use crate::reaction::get_msg_reactions;
|
||||
use crate::sql;
|
||||
use crate::summary::Summary;
|
||||
@@ -1059,34 +1058,6 @@ impl Message {
|
||||
cmd != SystemMessage::Unknown
|
||||
}
|
||||
|
||||
/// Returns true if the message is an Autocrypt Setup Message.
|
||||
pub fn is_setupmessage(&self) -> bool {
|
||||
if self.viewtype != Viewtype::File {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.param.get_cmd() == SystemMessage::AutocryptSetupMessage
|
||||
}
|
||||
|
||||
/// Returns the first characters of the setup code.
|
||||
///
|
||||
/// This is used to pre-fill the first entry field of the setup code.
|
||||
pub async fn get_setupcodebegin(&self, context: &Context) -> Option<String> {
|
||||
if !self.is_setupmessage() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(filename) = self.get_file(context)
|
||||
&& let Ok(ref buf) = read_file(context, &filename).await
|
||||
&& let Ok((typ, headers, _)) = split_armored_data(buf)
|
||||
&& typ == pgp::armor::BlockType::Message
|
||||
{
|
||||
return headers.get(crate::pgp::HEADER_SETUPCODE).cloned();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Sets or unsets message text.
|
||||
pub fn set_text(&mut self, text: String) {
|
||||
self.text = text;
|
||||
|
||||
@@ -17,8 +17,7 @@ use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, Chat, PARAM_BROADCAST_SECRET, load_broadcast_secret};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{ASM_SUBJECT, BROADCAST_INCOMPATIBILITY_MSG};
|
||||
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
|
||||
use crate::constants::{BROADCAST_INCOMPATIBILITY_MSG, Chattype, DC_FROM_HANDSHAKE};
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::download::PostMsgMetadata;
|
||||
@@ -1563,14 +1562,6 @@ impl MimeFactory {
|
||||
mail_builder::headers::raw::Raw::new("auto-generated").into(),
|
||||
));
|
||||
}
|
||||
SystemMessage::AutocryptSetupMessage => {
|
||||
headers.push((
|
||||
"Autocrypt-Setup-Message",
|
||||
mail_builder::headers::raw::Raw::new("v1").into(),
|
||||
));
|
||||
|
||||
placeholdertext = Some(ASM_SUBJECT.to_string());
|
||||
}
|
||||
SystemMessage::SecurejoinMessage => {
|
||||
let step = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
if !step.is_empty() {
|
||||
|
||||
@@ -199,6 +199,9 @@ pub enum SystemMessage {
|
||||
MemberRemovedFromGroup = 5,
|
||||
|
||||
/// Autocrypt Setup Message.
|
||||
///
|
||||
/// Deprecated as of 2026-03-15, such messages should not be created
|
||||
/// but may exist in the database.
|
||||
AutocryptSetupMessage = 6,
|
||||
|
||||
/// Secure-join message.
|
||||
|
||||
109
src/pgp.rs
109
src/pgp.rs
@@ -1,15 +1,14 @@
|
||||
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
|
||||
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::io::{BufRead, Cursor};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use deltachat_contact_tools::{EmailAddress, may_be_valid_addr};
|
||||
use pgp::armor::BlockType;
|
||||
use pgp::composed::{
|
||||
ArmorOptions, Deserializable, DetachedSignature, EncryptionCaps, KeyType as PgpKeyType,
|
||||
Message, MessageBuilder, SecretKeyParamsBuilder, SignedKeyDetails, SignedPublicKey,
|
||||
SignedPublicSubKey, SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig,
|
||||
MessageBuilder, SecretKeyParamsBuilder, SignedKeyDetails, SignedPublicKey, SignedPublicSubKey,
|
||||
SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig,
|
||||
};
|
||||
use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
|
||||
use pgp::crypto::ecc_curve::ECCCurve;
|
||||
@@ -26,45 +25,9 @@ use tokio::runtime::Handle;
|
||||
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
|
||||
|
||||
pub(crate) const HEADER_SETUPCODE: &str = "passphrase-begin";
|
||||
|
||||
/// Preferred symmetric encryption algorithm.
|
||||
const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128;
|
||||
|
||||
/// Split data from PGP Armored Data as defined in <https://tools.ietf.org/html/rfc4880#section-6.2>.
|
||||
///
|
||||
/// Returns (type, headers, base64 encoded body).
|
||||
pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, String>, Vec<u8>)> {
|
||||
use std::io::Read;
|
||||
|
||||
let cursor = Cursor::new(buf);
|
||||
let mut dearmor = pgp::armor::Dearmor::new(cursor);
|
||||
|
||||
let mut bytes = Vec::with_capacity(buf.len());
|
||||
|
||||
dearmor.read_to_end(&mut bytes)?;
|
||||
let typ = dearmor.typ.context("failed to parse type")?;
|
||||
|
||||
// normalize headers
|
||||
let headers = dearmor
|
||||
.headers
|
||||
.into_iter()
|
||||
.map(|(key, values)| {
|
||||
(
|
||||
key.trim().to_lowercase(),
|
||||
values
|
||||
.last()
|
||||
.map_or_else(String::new, |s| s.trim().to_string()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok((typ, headers, bytes))
|
||||
}
|
||||
|
||||
/// Create a new key pair.
|
||||
///
|
||||
/// Both secret and public key consist of signing primary key and encryption subkey
|
||||
@@ -335,24 +298,6 @@ pub fn pk_validate(
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Symmetric encryption for the autocrypt setup message (ASM).
|
||||
pub async fn symm_encrypt_autocrypt_setup(passphrase: &str, plain: Vec<u8>) -> Result<String> {
|
||||
let passphrase = Password::from(passphrase.to_string());
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut rng = thread_rng();
|
||||
let s2k = StringToKey::new_default(&mut rng);
|
||||
let builder = MessageBuilder::from_bytes("", plain);
|
||||
let mut builder = builder.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
|
||||
builder.encrypt_with_password(s2k, &passphrase)?;
|
||||
|
||||
let encoded_msg = builder.to_armored_string(&mut rng, Default::default())?;
|
||||
|
||||
Ok(encoded_msg)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Symmetrically encrypt the message.
|
||||
/// This is used for broadcast channels and for version 2 of the Securejoin protocol.
|
||||
/// `shared secret` is the secret that will be used for symmetric encryption.
|
||||
@@ -396,23 +341,6 @@ pub async fn symm_encrypt_message(
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Symmetric decryption.
|
||||
pub async fn symm_decrypt<T: BufRead + std::fmt::Debug + 'static + Send>(
|
||||
passphrase: &str,
|
||||
ctext: T,
|
||||
) -> Result<Vec<u8>> {
|
||||
let passphrase = passphrase.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let (enc_msg, _) = Message::from_armor(ctext)?;
|
||||
let password = Password::from(passphrase);
|
||||
|
||||
let msg = enc_msg.decrypt_with_password(&password)?;
|
||||
let res = msg.decompress()?.as_data_vec()?;
|
||||
Ok(res)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Merges and minimizes OpenPGP certificates.
|
||||
///
|
||||
/// Keeps at most one direct key signature and
|
||||
@@ -587,7 +515,7 @@ mod tests {
|
||||
test_utils::{TestContext, TestContextManager, alice_keypair, bob_keypair},
|
||||
token,
|
||||
};
|
||||
use pgp::composed::Esk;
|
||||
use pgp::composed::{Esk, Message};
|
||||
use pgp::packet::PublicKeyEncryptedSessionKey;
|
||||
|
||||
async fn decrypt_bytes(
|
||||
@@ -632,33 +560,6 @@ mod tests {
|
||||
Ok((msg, ret_signature_fingerprints, content))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_armored_data_1() {
|
||||
let (typ, _headers, base64) = split_armored_data(
|
||||
b"-----BEGIN PGP MESSAGE-----\nNoVal:\n\naGVsbG8gd29ybGQ=\n-----END PGP MESSAGE-----",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(typ, BlockType::Message);
|
||||
assert!(!base64.is_empty());
|
||||
assert_eq!(
|
||||
std::string::String::from_utf8(base64).unwrap(),
|
||||
"hello world"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_armored_data_2() {
|
||||
let (typ, headers, base64) = split_armored_data(
|
||||
b"-----BEGIN PGP PRIVATE KEY BLOCK-----\nAutocrypt-Prefer-Encrypt: mutual \n\naGVsbG8gd29ybGQ=\n-----END PGP PRIVATE KEY BLOCK-----"
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(typ, BlockType::PrivateKey);
|
||||
assert!(!base64.is_empty());
|
||||
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_keypair() {
|
||||
let keypair0 = create_keypair(EmailAddress::new("foo@bar.de").unwrap()).unwrap();
|
||||
|
||||
@@ -729,8 +729,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
|
||||
let allow_creation = if mime_parser.decrypting_failed {
|
||||
false
|
||||
} else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
|
||||
&& is_dc_message == MessengerMessage::No
|
||||
} else if is_dc_message == MessengerMessage::No
|
||||
&& !context.get_config_bool(Config::IsChatmail).await?
|
||||
{
|
||||
// the message is a classic email in a classic profile
|
||||
|
||||
@@ -490,12 +490,5 @@ mod tests {
|
||||
msg.get_summary_text_without_prefix(ctx).await,
|
||||
"📎 foo.bar \u{2013} bla bla"
|
||||
); // skipping prefix used for reactions summaries
|
||||
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_from_bytes(ctx, "autocrypt-setup-message.html", b"data", None)
|
||||
.unwrap();
|
||||
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
assert_summary_texts(&msg, ctx, "📎 autocrypt-setup-message.html").await;
|
||||
// no special handling of ASM
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user