mirror of
https://github.com/chatmail/core.git
synced 2026-04-27 18:36:30 +03:00
Add AEAP transition (#3385)
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
206
src/e2ee.rs
206
src/e2ee.rs
@@ -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
|
||||
|
||||
@@ -108,3 +108,5 @@ pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_utils;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
268
src/peerstate.rs
268
src/peerstate.rs
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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].
|
||||
///
|
||||
|
||||
@@ -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
1
src/tests.rs
Normal file
@@ -0,0 +1 @@
|
||||
mod aeap;
|
||||
360
src/tests/aeap.rs
Normal file
360
src/tests/aeap.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user