Add AEAP transition (#3385)

This commit is contained in:
Hocuri
2022-07-05 14:20:01 +02:00
committed by GitHub
parent 9f4646e8bd
commit e60164b5f3
15 changed files with 868 additions and 287 deletions

View File

@@ -437,9 +437,8 @@ mod tests {
use std::string::ToString;
use crate::constants;
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use num_traits::FromPrimitive;
#[test]
@@ -557,68 +556,4 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_change_primary_self_addr() -> Result<()> {
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Alice sends a message to Bob
let alice_bob_chat = alice.create_chat(&bob).await;
let sent = alice.send_text(alice_bob_chat.id, "Hi").await;
let bob_msg = bob.recv_msg(&sent).await;
bob_msg.chat_id.accept(&bob).await?;
assert_eq!(bob_msg.text.unwrap(), "Hi");
// Alice changes her self address and reconfigures
// (ensure_secret_key_exists() is called during configure)
alice
.set_primary_self_addr("alice@someotherdomain.xyz")
.await?;
crate::e2ee::ensure_secret_key_exists(&alice).await?;
assert_eq!(
alice.get_primary_self_addr().await?,
"alice@someotherdomain.xyz"
);
// Bob sends a message to Alice, encrypting to her previous key
let sent = bob.send_text(bob_msg.chat_id, "hi back").await;
// Alice set up message forwarding so that she still receives
// the message with her new address
let alice_msg = alice.recv_msg(&sent).await;
assert_eq!(alice_msg.text, Some("hi back".to_string()));
assert_eq!(alice_msg.get_showpadlock(), true);
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
// Even if Bob sends a message to Alice without In-Reply-To,
// it's still assigned to the 1:1 chat with Bob and not to
// a group (without secondary addresses, an ad-hoc group
// would be created)
receive_imf(
&alice,
b"From: bob@example.net
To: alice@example.org
Chat-Version: 1.0
Message-ID: <456@example.com>
Message w/out In-Reply-To
",
false,
)
.await?;
let alice_msg = alice.get_last_msg().await;
assert_eq!(
alice_msg.text,
Some("Message w/out In-Reply-To".to_string())
);
assert_eq!(alice_msg.get_showpadlock(), false);
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
Ok(())
}
}

View File

@@ -8,11 +8,13 @@ use num_traits::FromPrimitive;
use crate::aheader::{Aheader, EncryptPreference};
use crate::config::Config;
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::headerdef::HeaderDef;
use crate::headerdef::HeaderDefMap;
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::log::LogExt;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::pgp;
@@ -131,6 +133,56 @@ impl EncryptHelper {
}
}
/// Applies Autocrypt header to Autocrypt peer state and saves it into the database.
///
/// If we already know this fingerprint from another contact's peerstate, return that
/// peerstate in order to make AEAP work, but don't save it into the db yet.
///
/// Returns updated peerstate.
pub(crate) async fn get_autocrypt_peerstate(
context: &Context,
from: &str,
autocrypt_header: Option<&Aheader>,
message_time: i64,
) -> Result<Option<Peerstate>> {
let mut peerstate;
// Apply Autocrypt header
if let Some(header) = autocrypt_header {
// The "from_nongossiped_fingerprint" part is for AEAP:
// If we know this fingerprint from another addr,
// we may want to do a transition from this other addr
// (and keep its peerstate)
peerstate = Peerstate::from_nongossiped_fingerprint_or_addr(
context,
&header.public_key.fingerprint(),
from,
)
.await?;
if let Some(ref mut peerstate) = peerstate {
if addr_cmp(&peerstate.addr, from) {
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
}
// If `peerstate.addr` and `from` differ, this means that
// someone is using the same key but a different addr, probably
// because they made an AEAP transition.
// But we don't know if that's legit until we checked the
// signatures, so wait until then with writing anything
// to the database.
} else {
let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
} else {
peerstate = Peerstate::from_addr(context, from).await?;
}
Ok(peerstate)
}
/// Tries to decrypt a message, but only if it is structured as an
/// Autocrypt message.
///
@@ -140,10 +192,42 @@ impl EncryptHelper {
/// If the message is wrongly signed, this will still return the decrypted
/// message but the HashSet will be empty.
pub async fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
decryption_info: &DecryptionInfo,
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
// Possibly perform decryption
let public_keyring_for_validate = keyring_from_peerstate(&decryption_info.peerstate);
let context = context;
let encrypted_data_part = match get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
{
None => {
// not an autocrypt mime message, abort and ignore
return Ok(None);
}
Some(res) => res,
};
info!(context, "Detected Autocrypt-mime message");
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context)
.await
.context("failed to get own keyring")?;
decrypt_part(
encrypted_data_part,
private_keyring,
public_keyring_for_validate,
)
.await
}
pub async fn create_decryption_info(
context: &Context,
mail: &ParsedMail<'_>,
message_time: i64,
) -> Result<(Option<Vec<u8>>, HashSet<Fingerprint>)> {
) -> Result<DecryptionInfo> {
let from = mail
.headers
.get_header(HeaderDef::From_)
@@ -152,56 +236,34 @@ pub async fn try_decrypt(
.map(|from| from.addr)
.unwrap_or_default();
let mut peerstate = Peerstate::from_addr(context, &from).await?;
let autocrypt_header = Aheader::from_headers(&from, &mail.headers)
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
.flatten();
// Apply Autocrypt header
match Aheader::from_headers(&from, &mail.headers) {
Ok(Some(ref header)) => {
if let Some(ref mut peerstate) = peerstate {
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else {
let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
}
Ok(None) => {}
Err(err) => warn!(context, "Failed to parse Autocrypt header: {}", err),
}
let peerstate =
get_autocrypt_peerstate(context, &from, autocrypt_header.as_ref(), message_time).await?;
// Possibly perform decryption
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
Ok(DecryptionInfo {
from,
autocrypt_header,
peerstate,
message_time,
})
}
if let Some(ref mut peerstate) = peerstate {
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.add(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
public_keyring_for_validate.add(key.clone());
}
}
let (out_mail, signatures) =
match decrypt_if_autocrypt_message(context, mail, public_keyring_for_validate).await? {
Some((out_mail, signatures)) => (Some(out_mail), signatures),
None => (None, Default::default()),
};
if let Some(mut peerstate) = peerstate {
// If message is not encrypted and it is not a read receipt, degrade encryption.
if out_mail.is_none()
&& message_time > peerstate.last_seen_autocrypt
&& !contains_report(mail)
{
peerstate.degrade_encryption(message_time);
peerstate.save_to_db(&context.sql, false).await?;
}
}
Ok((out_mail, signatures))
#[derive(Debug)]
pub struct DecryptionInfo {
/// The From address. This is the address from the unnencrypted, outer
/// From header.
pub from: String,
pub autocrypt_header: Option<Aheader>,
/// The peerstate that will be used to validate the signatures
pub peerstate: Option<Peerstate>,
/// The timestamp when the message was sent.
/// If this is older than the peerstate's last_seen, this probably
/// means out-of-order message arrival, We don't modify the
/// peerstate in this case.
pub message_time: i64,
}
/// Returns a reference to the encrypted payload of a valid PGP/MIME message.
@@ -283,32 +345,16 @@ fn get_attachment_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMai
}
}
async fn decrypt_if_autocrypt_message(
context: &Context,
mail: &ParsedMail<'_>,
public_keyring_for_validate: Keyring<SignedPublicKey>,
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let encrypted_data_part = match get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
{
None => {
// not an autocrypt mime message, abort and ignore
return Ok(None);
fn keyring_from_peerstate(peerstate: &Option<Peerstate>) -> Keyring<SignedPublicKey> {
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
if let Some(ref peerstate) = *peerstate {
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.add(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
public_keyring_for_validate.add(key.clone());
}
Some(res) => res,
};
info!(context, "Detected Autocrypt-mime message");
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context)
.await
.context("failed to get own keyring")?;
decrypt_part(
encrypted_data_part,
private_keyring,
public_keyring_for_validate,
)
.await
}
public_keyring_for_validate
}
/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847.
@@ -357,7 +403,7 @@ async fn decrypt_part(
return Ok(Some((content, valid_detached_signatures)));
} else {
// If the message was wrongly or not signed, still return the plain text.
// The caller has to check the signatures then.
// The caller has to check if the signatures set is empty then.
return Ok(Some((plain, ret_valid_signatures)));
}
@@ -380,18 +426,6 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
false
}
/// Checks if a MIME structure contains a multipart/report part.
///
/// As reports are often unencrypted, we do not reset the Autocrypt header in
/// this case.
///
/// However, Delta Chat itself has no problem with encrypted multipart/report
/// parts and MUAs should be encouraged to encrpyt multipart/reports as well so
/// that we could use the normal Autocrypt processing.
fn contains_report(mail: &ParsedMail<'_>) -> bool {
mail.ctype.mimetype == "multipart/report"
}
/// Ensures a private key exists for the configured user.
///
/// Normally the private key is generated when the first message is

View File

@@ -108,3 +108,5 @@ pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";
#[cfg(test)]
mod test_utils;
#[cfg(test)]
mod tests;

View File

@@ -496,9 +496,9 @@ impl<'a> MimeFactory<'a> {
// Start with Internet Message Format headers in the order of the standard example
// <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.1.1>.
headers
.unprotected
.push(Header::new_with_value("From".into(), vec![from]).unwrap());
let from_header = Header::new_with_value("From".into(), vec![from]).unwrap();
headers.unprotected.push(from_header.clone());
if let Some(sender_displayname) = &self.sender_displayname {
let sender =
Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
@@ -666,6 +666,8 @@ impl<'a> MimeFactory<'a> {
};
let outer_message = if is_encrypted {
headers.protected.push(from_header);
// Store protected headers in the inner message.
let message = headers
.protected

View File

@@ -13,7 +13,7 @@ use once_cell::sync::Lazy;
use crate::aheader::Aheader;
use crate::blob::BlobObject;
use crate::constants::{DC_DESIRED_TEXT_LEN, DC_ELLIPSIS};
use crate::contact::{addr_normalize, ContactId};
use crate::contact::{addr_cmp, addr_normalize, ContactId};
use crate::context::Context;
use crate::dehtml::dehtml;
use crate::e2ee;
@@ -47,6 +47,9 @@ pub struct MimeMessage {
/// Addresses are normalized and lowercased:
pub recipients: Vec<SingleInfo>,
pub from: Vec<SingleInfo>,
/// Whether the From address was repeated in the signed part
/// (and we know that the signer intended to send from this address)
pub from_is_signed: bool,
pub list_post: Option<String>,
pub chat_disposition_notification_to: Option<SingleInfo>,
pub decrypting_failed: bool,
@@ -216,67 +219,94 @@ impl MimeMessage {
// Memory location for a possible decrypted message.
let mut mail_raw = Vec::new();
let mut gossiped_addr = Default::default();
let mut from_is_signed = false;
let mut decryption_info =
e2ee::create_decryption_info(context, &mail, message_time).await?;
// `signatures` is non-empty exactly if the message was encrypted and correctly signed.
let (mail, signatures, warn_empty_signature) =
match e2ee::try_decrypt(context, &mail, message_time).await {
Ok((raw, signatures)) => {
if let Some(raw) = raw {
// Encrypted, but maybe unsigned message. Only if
// `signatures` set is non-empty, it is a valid
// autocrypt message.
match e2ee::try_decrypt(context, &mail, &decryption_info).await {
Ok(Some((raw, signatures))) => {
// Encrypted, but maybe unsigned message. Only if
// `signatures` set is non-empty, it is a valid
// autocrypt message.
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(context, "decrypted message mime-body:");
println!("{}", String::from_utf8_lossy(&mail_raw));
}
// Handle any gossip headers if the mail was encrypted. See section
// "3.6 Key Gossip" of <https://autocrypt.org/autocrypt-spec-1.1.0.pdf>
// but only if the mail was correctly signed:
if !signatures.is_empty() {
let gossip_headers =
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
gossiped_addr = update_gossip_peerstates(
context,
message_time,
&mail,
gossip_headers,
)
.await?;
}
// let known protected headers from the decrypted
// part override the unencrypted top-level
// Signature was checked for original From, so we
// do not allow overriding it.
let mut throwaway_from = from.clone();
// We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe.
// See <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
headers.remove("subject");
MimeMessage::merge_headers(
context,
&mut headers,
&mut recipients,
&mut throwaway_from,
&mut list_post,
&mut chat_disposition_notification_to,
&decrypted_mail.headers,
);
(Ok(decrypted_mail), signatures, true)
} else {
// Message was not encrypted
(Ok(mail), signatures, false)
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(context, "decrypted message mime-body:");
println!("{}", String::from_utf8_lossy(&mail_raw));
}
// Handle any gossip headers if the mail was encrypted. See section
// "3.6 Key Gossip" of <https://autocrypt.org/autocrypt-spec-1.1.0.pdf>
// but only if the mail was correctly signed:
if !signatures.is_empty() {
let gossip_headers =
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
gossiped_addr =
update_gossip_peerstates(context, message_time, &mail, gossip_headers)
.await?;
}
// let known protected headers from the decrypted
// part override the unencrypted top-level
// Signature was checked for original From, so we
// do not allow overriding it.
let mut signed_from = Vec::new();
// We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe.
// See <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
headers.remove("subject");
MimeMessage::merge_headers(
context,
&mut headers,
&mut recipients,
&mut signed_from,
&mut list_post,
&mut chat_disposition_notification_to,
&decrypted_mail.headers,
);
if let Some(signed_from) = signed_from.first() {
if let Some(from) = from.first() {
if addr_cmp(&signed_from.addr, &from.addr) {
from_is_signed = true;
} else {
// There is a From: header in the encrypted &
// signed part, but it doesn't match the outer one.
// This _might_ be because the sender's mail server
// replaced the sending address, e.g. in a mailing list.
// Or it's because someone is doing some replay attack
// - OTOH, I can't come up with an attack scenario
// where this would be useful.
warn!(
context,
"From header in signed part does't match the outer one"
);
}
}
}
(Ok(decrypted_mail), signatures, true)
}
Ok(None) => {
// Message was not encrypted.
// If it is not a read receipt, degrade encryption.
if let Some(peerstate) = &mut decryption_info.peerstate {
if message_time > peerstate.last_seen_autocrypt
&& mail.ctype.mimetype != "multipart/report"
{
peerstate.degrade_encryption(message_time);
peerstate.save_to_db(&context.sql, false).await?;
}
}
(Ok(mail), HashSet::new(), false)
}
Err(err) => {
warn!(context, "decryption failed: {}", err);
(Err(err), Default::default(), true)
(Err(err), HashSet::new(), true)
}
};
@@ -286,6 +316,7 @@ impl MimeMessage {
recipients,
list_post,
from,
from_is_signed,
chat_disposition_notification_to,
decrypting_failed: mail.is_err(),
@@ -349,6 +380,13 @@ impl MimeMessage {
parser.decoded_data = mail_raw;
}
crate::peerstate::maybe_do_aeap_transition(context, &mut decryption_info, &parser).await?;
if let Some(peerstate) = decryption_info.peerstate {
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
}
Ok(parser)
}

View File

@@ -4,8 +4,10 @@ use std::collections::HashSet;
use std::fmt;
use crate::aheader::{Aheader, EncryptPreference};
use crate::chat::{self};
use crate::chat::{self, is_contact_in_chat, Chat};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::{addr_cmp, Contact, Origin};
use crate::context::Context;
use crate::events::EventType;
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
@@ -13,7 +15,7 @@ use crate::message::Message;
use crate::mimeparser::SystemMessage;
use crate::sql::Sql;
use crate::stock_str;
use anyhow::{bail, Result};
use anyhow::{Context as _, Result};
use num_traits::FromPrimitive;
#[derive(Debug)]
@@ -144,26 +146,41 @@ impl Peerstate {
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
FROM acpeerstates \
WHERE addr=? COLLATE NOCASE;";
WHERE addr=? COLLATE NOCASE LIMIT 1;";
Self::from_stmt(context, query, paramsv![addr]).await
}
pub async fn from_fingerprint(
context: &Context,
_sql: &Sql,
fingerprint: &Fingerprint,
) -> Result<Option<Peerstate>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
FROM acpeerstates \
WHERE public_key_fingerprint=? COLLATE NOCASE \
OR gossip_key_fingerprint=? COLLATE NOCASE \
ORDER BY public_key_fingerprint=? DESC;";
WHERE public_key_fingerprint=? \
OR gossip_key_fingerprint=? \
ORDER BY public_key_fingerprint=? DESC LIMIT 1;";
let fp = fingerprint.hex();
Self::from_stmt(context, query, paramsv![fp, fp, fp]).await
}
pub async fn from_nongossiped_fingerprint_or_addr(
context: &Context,
fingerprint: &Fingerprint,
addr: &str,
) -> Result<Option<Peerstate>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
FROM acpeerstates \
WHERE public_key_fingerprint=? \
OR addr=? COLLATE NOCASE \
ORDER BY public_key_fingerprint=? DESC, last_seen DESC LIMIT 1;";
let fp = fingerprint.hex();
Self::from_stmt(context, query, paramsv![fp, addr, fp]).await
}
async fn from_stmt(
context: &Context,
query: &str,
@@ -220,6 +237,10 @@ impl Peerstate {
Ok(peerstate)
}
/// Re-calculate `self.public_key_fingerprint` and `self.gossip_key_fingerprint`.
/// If one of them was changed, `self.fingerprint_changed` is set to `true`.
///
/// Call this after you changed `self.public_key` or `self.gossip_key`.
pub fn recalc_fingerprint(&mut self) {
if let Some(ref public_key) = self.public_key {
let old_public_fingerprint = self.public_key_fingerprint.take();
@@ -261,61 +282,8 @@ impl Peerstate {
self.to_save = Some(ToSave::All);
}
/// Adds a warning to the chat corresponding to peerstate if fingerprint has changed.
pub(crate) async fn handle_fingerprint_change(
&self,
context: &Context,
timestamp: i64,
) -> Result<()> {
if context.is_self_addr(&self.addr).await? {
// Do not try to search all the chats with self.
return Ok(());
}
if self.fingerprint_changed {
if let Some(contact_id) = context
.sql
.query_get_value("SELECT id FROM contacts WHERE addr=?;", paramsv![self.addr])
.await?
{
let chats = Chatlist::try_load(context, 0, None, contact_id).await?;
let msg = stock_str::contact_setup_changed(context, self.addr.clone()).await;
for (chat_id, msg_id) in chats.iter() {
let timestamp_sort = if let Some(msg_id) = msg_id {
let lastmsg = Message::load_from_db(context, *msg_id).await?;
lastmsg.timestamp_sort
} else {
context
.sql
.query_get_value(
"SELECT created_timestamp FROM chats WHERE id=?;",
paramsv![chat_id],
)
.await?
.unwrap_or(0)
};
chat::add_info_msg_with_cmd(
context,
*chat_id,
&msg,
SystemMessage::Unknown,
timestamp_sort,
Some(timestamp),
None,
None,
)
.await?;
context.emit_event(EventType::ChatModified(*chat_id));
}
} else {
bail!("contact with peerstate.addr {:?} not found", &self.addr);
}
}
Ok(())
}
pub fn apply_header(&mut self, header: &Aheader, message_time: i64) {
if self.addr.to_lowercase() != header.addr.to_lowercase() {
if !addr_cmp(&self.addr, &header.addr) {
return;
}
@@ -521,6 +489,175 @@ impl Peerstate {
false
}
}
/// Add an info message to all the chats with this contact, informing about
/// a [`PeerstateChange`].
///
/// Also, in the case of an address change (AEAP), replace the old address
/// with the new address in all chats.
async fn handle_setup_change(
&self,
context: &Context,
timestamp: i64,
change: PeerstateChange,
) -> Result<()> {
if context.is_self_addr(&self.addr).await? {
// Do not try to search all the chats with self.
return Ok(());
}
let contact_id = context
.sql
.query_get_value(
"SELECT id FROM contacts WHERE addr=? COLLATE NOCASE;",
paramsv![self.addr],
)
.await?
.with_context(|| format!("contact with peerstate.addr {:?} not found", &self.addr))?;
let chats = Chatlist::try_load(context, 0, None, Some(contact_id)).await?;
for (chat_id, msg_id) in chats.iter() {
let msg = match &change {
PeerstateChange::FingerprintChange => {
stock_str::contact_setup_changed(context, self.addr.clone()).await
}
PeerstateChange::Aeap(new_addr) => {
let old_contact = Contact::load_from_db(context, contact_id).await?;
stock_str::aeap_addr_changed(
context,
old_contact.get_display_name(),
&self.addr,
new_addr,
)
.await
}
};
let timestamp_sort = if let Some(msg_id) = msg_id {
let lastmsg = Message::load_from_db(context, *msg_id).await?;
lastmsg.timestamp_sort
} else {
context
.sql
.query_get_value(
"SELECT created_timestamp FROM chats WHERE id=?;",
paramsv![chat_id],
)
.await?
.unwrap_or(0)
};
chat::add_info_msg_with_cmd(
context,
*chat_id,
&msg,
SystemMessage::Unknown,
timestamp_sort,
Some(timestamp),
None,
None,
)
.await?;
if let PeerstateChange::Aeap(new_addr) = &change {
let chat = Chat::load_from_db(context, *chat_id).await?;
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
chat::remove_from_chat_contacts_table(context, *chat_id, contact_id).await?;
let (new_contact_id, _) =
Contact::add_or_lookup(context, "", new_addr, Origin::IncomingUnknownFrom)
.await?;
if !is_contact_in_chat(context, *chat_id, new_contact_id).await? {
chat::add_to_chat_contacts_table(context, *chat_id, new_contact_id).await?;
}
context.emit_event(EventType::ChatModified(*chat_id));
}
}
}
Ok(())
}
/// Adds a warning to all the chats corresponding to peerstate if fingerprint has changed.
pub(crate) async fn handle_fingerprint_change(
&self,
context: &Context,
timestamp: i64,
) -> Result<()> {
if self.fingerprint_changed {
self.handle_setup_change(context, timestamp, PeerstateChange::FingerprintChange)
.await?;
}
Ok(())
}
}
/// Do an AEAP transition, if necessary.
/// AEAP stands for "Automatic Email Address Porting."
///
/// In `drafts/aeap_mvp.md` there is a "big picture" overview over AEAP.
pub async fn maybe_do_aeap_transition(
context: &Context,
info: &mut crate::e2ee::DecryptionInfo,
mime_parser: &crate::mimeparser::MimeMessage,
) -> Result<()> {
if let Some(peerstate) = &mut info.peerstate {
if let Some(from) = mime_parser.from.first() {
// If the from addr is different from the peerstate address we know,
// we may want to do an AEAP transition.
if !addr_cmp(&peerstate.addr, &from.addr)
// Check if it's a chat message; we do this to avoid
// some accidental transitions if someone writes from multiple
// addresses with an MUA.
&& mime_parser.has_chat_version()
// Check if the message is signed correctly.
// If it's not signed correctly, the whole autocrypt header will be mostly
// ignored anyway and the message shown as not encrypted, so we don't
// have to handle this case.
&& !mime_parser.signatures.is_empty()
// Check if the From: address was also in the signed part of the email.
// Without this check, an attacker could replay a message from Alice
// to Bob. Then Bob's device would do an AEAP transition from Alice's
// to the attacker's address, allowing for easier phishing.
&& mime_parser.from_is_signed
&& info.message_time > peerstate.last_seen
{
// Add an info messages to all chats with this contact
//
peerstate
.handle_setup_change(
context,
info.message_time,
PeerstateChange::Aeap(info.from.clone()),
)
.await?;
peerstate.addr = info.from.clone();
let header = info.autocrypt_header.as_ref().context(
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
)?;
peerstate.apply_header(header, info.message_time);
peerstate.to_save = Some(ToSave::All);
// We don't know whether a peerstate with this address already existed, or a
// new one should be created, so just try both create=false and create=true,
// and if this fails, create=true, one will succeed (this is a very cold path,
// so performance doesn't really matter).
peerstate.save_to_db(&context.sql, true).await?;
peerstate.save_to_db(&context.sql, false).await?;
}
}
}
Ok(())
}
enum PeerstateChange {
/// The contact's public key fingerprint changed, likely because
/// the contact uses a new device and didn't transfer their key.
FingerprintChange,
/// The contact changed their address to the given new address
/// (Automatic Email Address Porting).
Aeap(String),
}
/// Removes duplicate peerstates from `acpeerstates` database table.
@@ -588,11 +725,10 @@ mod tests {
// clear to_save, as that is not persissted
peerstate.to_save = None;
assert_eq!(peerstate, peerstate_new);
let peerstate_new2 =
Peerstate::from_fingerprint(&ctx.ctx, &ctx.ctx.sql, &pub_key.fingerprint())
.await
.expect("failed to load peerstate from db")
.expect("no peerstate found in the database");
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.fingerprint())
.await
.expect("failed to load peerstate from db")
.expect("no peerstate found in the database");
assert_eq!(peerstate, peerstate_new2);
}

View File

@@ -201,7 +201,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
};
// retrieve known state for this fingerprint
let peerstate = Peerstate::from_fingerprint(context, &context.sql, &fingerprint)
let peerstate = Peerstate::from_fingerprint(context, &fingerprint)
.await
.context("Can't load peerstate")?;

View File

@@ -633,9 +633,7 @@ async fn could_not_establish_secure_connection(
}
async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) -> Result<(), Error> {
if let Some(ref mut peerstate) =
Peerstate::from_fingerprint(context, &context.sql, fingerprint).await?
{
if let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, fingerprint).await? {
if peerstate.set_verified(
PeerstateKeyType::PublicKey,
fingerprint,

View File

@@ -332,6 +332,9 @@ pub enum StockMessage {
#[strum(props(fallback = "Not connected"))]
NotConnected = 121,
#[strum(props(fallback = "%1$s changed their address from %2$s to %3$s"))]
AeapAddrChanged = 122,
}
impl StockMessage {
@@ -375,6 +378,17 @@ trait StockStringMods: AsRef<str> + Sized {
.replacen("%2$@", replacement.as_ref(), 1)
}
/// Substitutes the third replacement value if one is present.
///
/// Be aware you probably should have also called [`StockStringMods::replace1`] and
/// [`StockStringMods::replace2`] if you are calling this.
fn replace3(&self, replacement: impl AsRef<str>) -> String {
self.as_ref()
.replacen("%3$s", replacement.as_ref(), 1)
.replacen("%3$d", replacement.as_ref(), 1)
.replacen("%3$@", replacement.as_ref(), 1)
}
/// Augments the message by saying it was performed by a user.
///
/// This looks up the display name of `contact` and uses the [`msg_action_by_me`] and
@@ -1076,6 +1090,20 @@ pub(crate) async fn broadcast_list(context: &Context) -> String {
translated(context, StockMessage::BroadcastList).await
}
/// Stock string: `%1$s changed their address from %2$s to %3$s`.
pub(crate) async fn aeap_addr_changed(
context: &Context,
contact_name: impl AsRef<str>,
old_addr: impl AsRef<str>,
new_addr: impl AsRef<str>,
) -> String {
translated(context, StockMessage::AeapAddrChanged)
.await
.replace1(contact_name)
.replace2(old_addr)
.replace3(new_addr)
}
impl Context {
/// Set the stock string for the [StockMessage].
///

View File

@@ -2,7 +2,6 @@
//!
//! This private module is only compiled for test runs.
#![allow(clippy::indexing_slicing)]
#![allow(dead_code)] // Can be removed once PR #3385 is merged
use std::collections::BTreeMap;
use std::ops::Deref;
use std::panic;
@@ -250,6 +249,7 @@ impl TestContext {
Self::builder().configure_fiona().build().await
}
#[allow(dead_code)]
/// Print current chat state.
pub async fn print_chats(&self) {
println!("\n========== Chats of {}: ==========", self.name());

1
src/tests.rs Normal file
View File

@@ -0,0 +1 @@
mod aeap;

360
src/tests/aeap.rs Normal file
View File

@@ -0,0 +1,360 @@
#![allow(clippy::indexing_slicing)]
use anyhow::Result;
use crate::chat;
use crate::chat::ChatId;
use crate::constants;
use crate::contact;
use crate::contact::Contact;
use crate::contact::ContactId;
use crate::message::Message;
use crate::peerstate;
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::stock_str;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_change_primary_self_addr() -> Result<()> {
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
tcm.send_recv_accept(&alice, &bob, "Hi").await;
let bob_alice_chat = bob.create_chat(&alice).await;
tcm.change_addr(&alice, "alice@someotherdomain.xyz").await;
tcm.section("Bob sends a message to Alice, encrypting to her previous key");
let sent = bob.send_text(bob_alice_chat.id, "hi back").await;
// Alice set up message forwarding so that she still receives
// the message with her new address
let alice_msg = alice.recv_msg(&sent).await;
assert_eq!(alice_msg.text, Some("hi back".to_string()));
assert_eq!(alice_msg.get_showpadlock(), true);
let alice_bob_chat = alice.create_chat(&bob).await;
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
tcm.section("Bob sends a message to Alice without In-Reply-To");
// Even if Bob sends a message to Alice without In-Reply-To,
// it's still assigned to the 1:1 chat with Bob and not to
// a group (without secondary addresses, an ad-hoc group
// would be created)
receive_imf(
&alice,
b"From: bob@example.net
To: alice@example.org
Chat-Version: 1.0
Message-ID: <456@example.com>
Message w/out In-Reply-To
",
false,
)
.await?;
let alice_msg = alice.get_last_msg().await;
assert_eq!(
alice_msg.text,
Some("Message w/out In-Reply-To".to_string())
);
assert_eq!(alice_msg.get_showpadlock(), false);
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
Ok(())
}
enum ChatForTransition {
OneToOne,
GroupChat,
VerifiedGroup,
}
use ChatForTransition::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_0() {
check_aeap_transition(OneToOne, false, false).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_1() {
check_aeap_transition(GroupChat, false, false).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_0_verified() {
check_aeap_transition(OneToOne, true, false).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_1_verified() {
check_aeap_transition(GroupChat, true, false).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_2_verified() {
check_aeap_transition(VerifiedGroup, true, false).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_0_bob_knew_new_addr() {
check_aeap_transition(OneToOne, false, true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_1_bob_knew_new_addr() {
check_aeap_transition(GroupChat, false, true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_0_verified_bob_knew_new_addr() {
check_aeap_transition(OneToOne, true, true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_1_verified_bob_knew_new_addr() {
check_aeap_transition(GroupChat, true, true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_2_verified_bob_knew_new_addr() {
check_aeap_transition(VerifiedGroup, true, true).await;
}
/// Happy path test for AEAP in various configurations.
/// - `chat_for_transition`: Which chat the transition message should be sent in
/// - `verified`: Whether Alice and Bob verified each other
/// - `bob_knew_new_addr`: Whether Bob already had a chat with Alice's new address
async fn check_aeap_transition(
chat_for_transition: ChatForTransition,
verified: bool,
bob_knew_new_addr: bool,
) {
// Alice's new address is "fiona@example.net" so that we can test
// the case where Bob already had contact with Alice's new address
const ALICE_NEW_ADDR: &str = "fiona@example.net";
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
if bob_knew_new_addr {
let fiona = tcm.fiona().await;
tcm.send_recv_accept(&fiona, &bob, "Hi").await;
tcm.send_recv_accept(&bob, &fiona, "Hi back").await;
}
tcm.send_recv_accept(&alice, &bob, "Hi").await;
tcm.send_recv_accept(&bob, &alice, "Hi back").await;
if verified {
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
}
let mut groups = vec![
chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 0")
.await
.unwrap(),
chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 1")
.await
.unwrap(),
];
if verified {
groups.push(
chat::create_group_chat(&bob, chat::ProtectionStatus::Protected, "Group 2")
.await
.unwrap(),
);
groups.push(
chat::create_group_chat(&bob, chat::ProtectionStatus::Protected, "Group 3")
.await
.unwrap(),
);
}
let old_contact = Contact::create(&bob, "Alice", "alice@example.org")
.await
.unwrap();
for group in &groups {
chat::add_contact_to_chat(&bob, *group, old_contact)
.await
.unwrap();
}
// Already add the new contact to one of the groups.
// We can then later check that the contact isn't in the group twice.
let already_new_contact = Contact::create(&bob, "Alice", "fiona@example.net")
.await
.unwrap();
chat::add_contact_to_chat(&bob, groups[0], already_new_contact)
.await
.unwrap();
// groups 0 and 2 stay unpromoted (i.e. local
// on Bob's device, Alice doesn't know about them)
tcm.section("Promoting group 1");
let sent = bob.send_text(groups[1], "group created").await;
let group1_alice = alice.recv_msg(&sent).await.chat_id;
let mut group3_alice = None;
if verified {
tcm.section("Promoting group 3");
let sent = bob.send_text(groups[3], "group created").await;
group3_alice = Some(alice.recv_msg(&sent).await.chat_id);
}
tcm.change_addr(&alice, ALICE_NEW_ADDR).await;
tcm.section("Alice sends another message to Bob, this time from her new addr");
// No matter which chat Alice sends to, the transition should be done in all groups
let chat_to_send = match chat_for_transition {
OneToOne => alice.create_chat(&bob).await.id,
GroupChat => group1_alice,
VerifiedGroup => group3_alice.expect("No verified group"),
};
let sent = alice
.send_text(chat_to_send, "Hello from my new addr!")
.await;
let recvd = bob.recv_msg(&sent).await;
let sent_timestamp = recvd.timestamp_sent;
assert_eq!(recvd.text.unwrap(), "Hello from my new addr!");
tcm.section("Check that the AEAP transition worked");
check_that_transition_worked(
&groups,
&alice,
"alice@example.org",
ALICE_NEW_ADDR,
"Alice",
&bob,
)
.await;
// Assert that the autocrypt header is also applied to the peerstate
// if the address changed
let bob_alice_peerstate = Peerstate::from_addr(&bob, ALICE_NEW_ADDR)
.await
.unwrap()
.unwrap();
assert_eq!(bob_alice_peerstate.last_seen, sent_timestamp);
assert_eq!(bob_alice_peerstate.last_seen_autocrypt, sent_timestamp);
tcm.section("Test switching back");
tcm.change_addr(&alice, "alice@example.org").await;
let sent = alice
.send_text(chat_to_send, "Hello from my old addr!")
.await;
let recvd = bob.recv_msg(&sent).await;
assert_eq!(recvd.text.unwrap(), "Hello from my old addr!");
check_that_transition_worked(
&groups,
&alice,
// Note that "alice@example.org" and ALICE_NEW_ADDR are switched now:
ALICE_NEW_ADDR,
"alice@example.org",
"Alice",
&bob,
)
.await;
}
async fn check_that_transition_worked(
groups: &[ChatId],
alice: &TestContext,
old_alice_addr: &str,
new_alice_addr: &str,
name: &str,
bob: &TestContext,
) {
let new_contact = Contact::lookup_id_by_addr(bob, new_alice_addr, contact::Origin::Unknown)
.await
.unwrap()
.unwrap();
for group in groups {
let members = chat::get_chat_contacts(bob, *group).await.unwrap();
// In all the groups, exactly Bob and Alice's new number are members.
// (and Alice's new number isn't in there twice)
assert_eq!(members.len(), 2);
assert!(members.contains(&new_contact));
assert!(members.contains(&ContactId::SELF));
let info_msg = get_last_info_msg(bob, *group).await;
let expected_text =
stock_str::aeap_addr_changed(bob, name, old_alice_addr, new_alice_addr).await;
assert_eq!(info_msg.text.unwrap(), expected_text);
assert_eq!(info_msg.from_id, ContactId::INFO);
let msg = format!("Sending to group {}", group);
let sent = bob.send_text(*group, &msg).await;
let recvd = alice.recv_msg(&sent).await;
assert_eq!(recvd.text.unwrap(), msg);
}
}
async fn mark_as_verified(this: &TestContext, other: &TestContext) {
let other_addr = other.get_primary_self_addr().await.unwrap();
let mut peerstate = peerstate::Peerstate::from_addr(this, &other_addr)
.await
.unwrap()
.unwrap();
peerstate.verified_key = peerstate.public_key.clone();
peerstate.verified_key_fingerprint = peerstate.public_key_fingerprint.clone();
peerstate.to_save = Some(peerstate::ToSave::All);
peerstate.save_to_db(&this.sql, false).await.unwrap();
}
async fn get_last_info_msg(t: &TestContext, chat_id: ChatId) -> Message {
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, constants::DC_GCM_INFO_ONLY)
.await
.unwrap();
let msg_id = if let chat::ChatItem::Message { msg_id } = msgs.last().unwrap() {
msg_id
} else {
panic!("Wrong item type");
};
Message::load_from_db(&t.ctx, *msg_id).await.unwrap()
}
/// Test that an attacker - here Fiona - can't replay a message sent by Alice
/// to make Bob think that there was a transition to Fiona's address.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_replay_attack() -> Result<()> {
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
tcm.send_recv_accept(&alice, &bob, "Hi").await;
tcm.send_recv_accept(&bob, &alice, "Hi back").await;
let group =
chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 0").await?;
let bob_alice_contact = Contact::create(&bob, "Alice", "alice@example.org").await?;
chat::add_contact_to_chat(&bob, group, bob_alice_contact).await?;
// Alice sends a message which Bob doesn't receive or something
// A real attack would rather re-use a message that was sent to a group
// and replace the Message-Id or so.
let chat = alice.create_chat(&bob).await;
let sent = alice.send_text(chat.id, "whoop whoop").await;
// Fiona gets the message, replaces the From addr...
let sent = sent
.payload()
.replace("From: <alice@example.org>", "From: <fiona@example.net>")
.replace("addr=alice@example.org;", "addr=fiona@example.net;");
sent.find("From: <fiona@example.net>").unwrap(); // Assert that it worked
sent.find("addr=fiona@example.net;").unwrap(); // Assert that it worked
tcm.section("Fiona replaced the From addr and forwards the message to Bob");
receive_imf(&bob, sent.as_bytes(), false).await?.unwrap();
// Check that no transition was done
assert!(chat::is_contact_in_chat(&bob, group, bob_alice_contact).await?);
let bob_fiona_contact = Contact::create(&bob, "", "fiona@example.net").await?;
assert!(!chat::is_contact_in_chat(&bob, group, bob_fiona_contact).await?);
Ok(())
}