feat: key-contacts

This change introduces a new type of contacts
identified by their public key fingerprint
rather than an e-mail address.

Encrypted chats now stay encrypted
and unencrypted chats stay unencrypted.
For example, 1:1 chats with key-contacts
are encrypted and 1:1 chats with address-contacts
are unencrypted.
Groups that have a group ID are encrypted
and can only contain key-contacts
while groups that don't have a group ID ("adhoc groups")
are unencrypted and can only contain address-contacts.

JSON-RPC API `reset_contact_encryption` is removed.
Python API `Contact.reset_encryption` is removed.
"Group tracking plugin" in legacy Python API was removed because it
relied on parsing email addresses from system messages with regexps.

Co-authored-by: Hocuri <hocuri@gmx.de>
Co-authored-by: iequidoo <dgreshilov@gmail.com>
Co-authored-by: B. Petersen <r10s@b44t.com>
This commit is contained in:
link2xt
2025-06-26 14:07:39 +00:00
parent 7ac04d0204
commit 416131b4a2
84 changed files with 4735 additions and 6338 deletions

View File

@@ -1,14 +1,19 @@
//! "AEAP" means "Automatic Email Address Porting"
//! and was the predecessor of key-contacts
//! (i.e. identifying contacts via the fingerprint,
//! while allowing the email address to change).
//!
//! These tests still pass because key-contacts
//! allows messaging to continue after an email address change,
//! just as AEAP did. Some other tests had to be removed.
use anyhow::Result;
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
use crate::contact;
use crate::contact::Contact;
use crate::contact::ContactId;
use crate::contact::{Contact, ContactId};
use crate::message::Message;
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::securejoin::get_securejoin_qr;
use crate::stock_str;
use crate::test_utils::mark_as_verified;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
@@ -35,30 +40,6 @@ async fn test_change_primary_self_addr() -> Result<()> {
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, "Message w/out In-Reply-To");
assert_eq!(alice_msg.get_showpadlock(), false);
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
Ok(())
}
@@ -71,115 +52,67 @@ use ChatForTransition::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_0() {
check_aeap_transition(OneToOne, false, false).await;
check_aeap_transition(OneToOne, false).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_1() {
check_aeap_transition(GroupChat, false, false).await;
check_aeap_transition(GroupChat, false).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_0_verified() {
check_aeap_transition(OneToOne, true, false).await;
check_aeap_transition(OneToOne, true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_1_verified() {
check_aeap_transition(GroupChat, true, false).await;
check_aeap_transition(GroupChat, true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_2_verified() {
check_aeap_transition(VerifiedGroup, true, false).await;
check_aeap_transition(VerifiedGroup, true).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.
/// Happy path test for AEAP.
/// - `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";
async fn check_aeap_transition(chat_for_transition: ChatForTransition, verified: bool) {
const ALICE_NEW_ADDR: &str = "alice2@example.net";
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().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(&bob, &fiona, "Hi back").await;
}
tcm.send_recv_accept(&alice, &bob, "Hi").await;
tcm.send_recv(&bob, &alice, "Hi back").await;
tcm.send_recv_accept(alice, bob, "Hi").await;
tcm.send_recv(bob, alice, "Hi back").await;
if verified {
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
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")
chat::create_group_chat(bob, chat::ProtectionStatus::Unprotected, "Group 0")
.await
.unwrap(),
chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 1")
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")
chat::create_group_chat(bob, chat::ProtectionStatus::Protected, "Group 2")
.await
.unwrap(),
);
groups.push(
chat::create_group_chat(&bob, chat::ProtectionStatus::Protected, "Group 3")
chat::create_group_chat(bob, chat::ProtectionStatus::Protected, "Group 3")
.await
.unwrap(),
);
}
let old_contact = Contact::create(&bob, "Alice", "alice@example.org")
.await
.unwrap();
let alice_contact = bob.add_or_lookup_contact_id(alice).await;
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", ALICE_NEW_ADDR)
.await
.unwrap();
if verified {
chat::add_contact_to_chat(&bob, groups[2], already_new_contact)
chat::add_contact_to_chat(bob, *group, alice_contact)
.await
.unwrap();
}
@@ -197,12 +130,12 @@ async fn check_aeap_transition(
group3_alice = Some(alice.recv_msg(&sent).await.chat_id);
}
tcm.change_addr(&alice, ALICE_NEW_ADDR).await;
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,
OneToOne => alice.create_chat(bob).await.id,
GroupChat => group1_alice,
VerifiedGroup => group3_alice.expect("No verified group"),
};
@@ -210,147 +143,50 @@ async fn check_aeap_transition(
.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, "Hello from my new addr!");
tcm.section("Check that the AEAP transition worked");
check_that_transition_worked(
&groups[2..],
&alice,
"alice@example.org",
ALICE_NEW_ADDR,
"Alice",
&bob,
)
.await;
check_no_transition_done(&groups[0..2], "alice@example.org", &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);
check_that_transition_worked(bob, &groups, alice_contact, ALICE_NEW_ADDR).await;
tcm.section("Test switching back");
tcm.change_addr(&alice, "alice@example.org").await;
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, "Hello from my old addr!");
check_that_transition_worked(
&groups[2..],
&alice,
// Note that "alice@example.org" and ALICE_NEW_ADDR are switched now:
ALICE_NEW_ADDR,
"alice@example.org",
"Alice",
&bob,
)
.await;
check_that_transition_worked(bob, &groups, alice_contact, "alice@example.org").await;
}
async fn check_that_transition_worked(
groups: &[ChatId],
alice: &TestContext,
old_alice_addr: &str,
new_alice_addr: &str,
name: &str,
bob: &TestContext,
groups: &[ChatId],
alice_contact_id: ContactId,
alice_addr: &str,
) {
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)
// In all the groups, exactly Bob and Alice are members.
assert_eq!(
members.len(),
2,
"Group {} has members {:?}, but should have members {:?} and {:?}",
group,
&members,
new_contact,
alice_contact_id,
ContactId::SELF
);
assert!(
members.contains(&new_contact),
"Group {group} lacks {new_contact}"
members.contains(&alice_contact_id),
"Group {group} lacks {alice_contact_id}"
);
assert!(members.contains(&ContactId::SELF));
let info_msg = get_last_info_msg(bob, *group).await.unwrap();
let expected_text =
stock_str::aeap_addr_changed(bob, name, old_alice_addr, new_alice_addr).await;
assert_eq!(info_msg.text, 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, msg);
}
}
async fn check_no_transition_done(groups: &[ChatId], old_alice_addr: &str, bob: &TestContext) {
let old_contact = Contact::lookup_id_by_addr(bob, old_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 _old_ number are members.
assert_eq!(
members.len(),
2,
"Group {} has members {:?}, but should have members {:?} and {:?}",
group,
&members,
old_contact,
ContactId::SELF
);
assert!(members.contains(&old_contact));
assert!(members.contains(&ContactId::SELF));
let last_info_msg = get_last_info_msg(bob, *group).await;
assert!(
last_info_msg.is_none(),
"{last_info_msg:?} shouldn't be there (or it's an unrelated info msg)"
);
let sent = bob.send_text(*group, "hi").await;
let msg = Message::load_from_db(bob, sent.sender_msg_id)
.await
.unwrap();
assert_eq!(msg.get_showpadlock(), true);
}
}
async fn get_last_info_msg(t: &TestContext, chat_id: ChatId) -> Option<Message> {
let msgs = chat::get_chat_msgs_ex(
&t.ctx,
chat_id,
chat::MessageListOptions {
info_only: true,
add_daymarker: false,
},
)
.await
.unwrap();
let msg_id = if let chat::ChatItem::Message { msg_id } = msgs.last()? {
msg_id
} else {
return None;
};
Some(Message::load_from_db(&t.ctx, *msg_id).await.unwrap())
// Test that the email address of Alice is updated.
let alice_contact = Contact::get_by_id(bob, alice_contact_id).await.unwrap();
assert_eq!(alice_contact.get_addr(), alice_addr);
}
/// Test that an attacker - here Fiona - can't replay a message sent by Alice
@@ -360,6 +196,7 @@ async fn test_aeap_replay_attack() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
tcm.send_recv_accept(&alice, &bob, "Hi").await;
tcm.send_recv(&bob, &alice, "Hi back").await;
@@ -367,7 +204,8 @@ async fn test_aeap_replay_attack() -> Result<()> {
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?;
let bob_alice_contact = bob.add_or_lookup_contact_id(&alice).await;
let bob_fiona_contact = bob.add_or_lookup_contact_id(&fiona).await;
chat::add_contact_to_chat(&bob, group, bob_alice_contact).await?;
// Alice sends a message which Bob doesn't receive or something
@@ -389,17 +227,22 @@ async fn test_aeap_replay_attack() -> Result<()> {
// 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(())
}
/// Tests that writing to a contact is possible
/// after address change.
///
/// This test is redundant after introduction
/// of key-contacts, but is kept to avoid deleting the tests.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_write_to_alice_after_aeap() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_grp_id = chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
let qr = get_securejoin_qr(alice, Some(alice_grp_id)).await?;
tcm.exec_securejoin_qr(bob, alice, &qr).await;
@@ -415,15 +258,13 @@ async fn test_write_to_alice_after_aeap() -> Result<()> {
let sent = alice.send_text(alice_grp_id, "Hello!").await;
bob.recv_msg(&sent).await;
assert!(!bob_alice_contact.is_verified(bob).await?);
assert!(bob_alice_contact.is_verified(bob).await?);
let bob_alice_chat = Chat::load_from_db(bob, bob_alice_chat.id).await?;
assert!(bob_alice_chat.is_protected());
let mut msg = Message::new_text("hi".to_string());
assert!(chat::send_msg(bob, bob_alice_chat.id, &mut msg)
.await
.is_err());
chat::send_msg(bob, bob_alice_chat.id, &mut msg).await?;
// But encrypted communication is still possible in unprotected groups with old Alice.
// Encrypted communication is also possible in unprotected groups with Alice.
let sent = bob
.send_text(bob_unprotected_grp_id, "Alice, how is your address change?")
.await;

View File

@@ -5,10 +5,10 @@ use crate::chat::resend_msgs;
use crate::chat::{
self, add_contact_to_chat, remove_contact_from_chat, send_msg, Chat, ProtectionStatus,
};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::{Chattype, DC_GCL_FOR_FORWARDING};
use crate::contact::{Contact, ContactId, Origin};
use crate::constants::Chattype;
use crate::contact::{Contact, ContactId};
use crate::key::{load_self_public_key, DcKey};
use crate::message::{Message, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
@@ -20,16 +20,16 @@ use crate::tools::SystemTime;
use crate::{e2ee, message};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_oneonone_chat_broken_by_classical() {
check_verified_oneonone_chat(true).await;
async fn test_verified_oneonone_chat_not_broken_by_classical() {
check_verified_oneonone_chat_protection_not_broken(true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_oneonone_chat_broken_by_device_change() {
check_verified_oneonone_chat(false).await;
async fn test_verified_oneonone_chat_not_broken_by_device_change() {
check_verified_oneonone_chat_protection_not_broken(false).await;
}
async fn check_verified_oneonone_chat(broken_by_classical_email: bool) {
async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_email: bool) {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
@@ -59,19 +59,19 @@ async fn check_verified_oneonone_chat(broken_by_classical_email: bool) {
// Bob's contact is still verified, but the chat isn't marked as protected anymore
let contact = alice.add_or_lookup_contact(&bob).await;
assert_eq!(contact.is_verified(&alice).await.unwrap(), true);
assert_verified(&alice, &bob, ProtectionStatus::ProtectionBroken).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
} else {
tcm.section("Bob sets up another Delta Chat device");
let bob2 = TestContext::new().await;
let bob2 = tcm.unconfigured().await;
bob2.set_name("bob2");
bob2.configure_addr("bob@example.net").await;
SystemTime::shift(std::time::Duration::from_secs(3600));
tcm.send_recv(&bob2, &alice, "Using another device now")
.await;
let contact = alice.add_or_lookup_contact(&bob).await;
let contact = alice.add_or_lookup_contact(&bob2).await;
assert_eq!(contact.is_verified(&alice).await.unwrap(), false);
assert_verified(&alice, &bob, ProtectionStatus::ProtectionBroken).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
}
tcm.section("Bob sends another message from DC");
@@ -157,44 +157,42 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
tcm.send_recv(&fiona_new, &alice, "I have a new device")
.await;
// The chat should be and stay unprotected
// Alice gets a new unprotected chat with new Fiona contact.
{
let chat = alice.get_chat(&fiona_new).await;
assert!(!chat.is_protected());
assert!(chat.is_protection_broken());
let msg1 = get_chat_msg(&alice, chat.id, 0, 3).await;
assert_eq!(msg1.get_info_type(), SystemMessage::ChatProtectionEnabled);
let msg2 = get_chat_msg(&alice, chat.id, 1, 3).await;
assert_eq!(msg2.get_info_type(), SystemMessage::ChatProtectionDisabled);
let msg2 = get_chat_msg(&alice, chat.id, 2, 3).await;
assert_eq!(msg2.text, "I have a new device");
let msg = get_chat_msg(&alice, chat.id, 0, 1).await;
assert_eq!(msg.text, "I have a new device");
// After recreating the chat, it should still be unprotected
chat.id.delete(&alice).await?;
let chat = alice.create_chat(&fiona_new).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_missing_peerstate_reexecute_securejoin() -> Result<()> {
async fn test_missing_key_reexecute_securejoin() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice_addr = alice.get_config(Config::Addr).await?.unwrap();
let bob = &tcm.bob().await;
enable_verified_oneonone_chats(&[alice, bob]).await;
let chat_id = tcm.execute_securejoin(bob, alice).await;
let chat = Chat::load_from_db(bob, chat_id).await?;
assert!(chat.is_protected());
bob.sql
.execute("DELETE FROM acpeerstates WHERE addr=?", (&alice_addr,))
.execute(
"DELETE FROM public_keys WHERE fingerprint=?",
(&load_self_public_key(alice)
.await
.unwrap()
.dc_fingerprint()
.hex(),),
)
.await?;
let chat_id = tcm.execute_securejoin(bob, alice).await;
let chat = Chat::load_from_db(bob, chat_id).await?;
@@ -242,6 +240,10 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> {
Ok(())
}
/// Tests that receiving unencrypted message
/// does not disable protection of 1:1 chat.
///
/// Instead, an email-chat is created.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_degrade_verified_oneonone_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -265,211 +267,16 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> {
)
.await?;
let contact_id = Contact::lookup_id_by_addr(&alice, "bob@example.net", Origin::Hidden)
.await?
.unwrap();
let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 3).await;
let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 1).await;
let enabled = stock_str::chat_protection_enabled(&alice).await;
assert_eq!(msg0.text, enabled);
assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled);
let msg1 = get_chat_msg(&alice, alice_chat.id, 1, 3).await;
let disabled = stock_str::chat_protection_disabled(&alice, contact_id).await;
assert_eq!(msg1.text, disabled);
assert_eq!(msg1.param.get_cmd(), SystemMessage::ChatProtectionDisabled);
let msg2 = get_chat_msg(&alice, alice_chat.id, 2, 3).await;
assert_eq!(msg2.text, "hello".to_string());
assert!(!msg2.is_system_message());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_oneonone_chat_enable_disable() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
// Alice & Bob verify each other
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
let chat = alice.create_chat(&bob).await;
assert!(chat.is_protected());
for alice_accepts_breakage in [true, false] {
SystemTime::shift(std::time::Duration::from_secs(300));
// Bob uses Thunderbird to send a message
receive_imf(
&alice,
format!(
"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2{alice_accepts_breakage}@example.org>\n\
\n\
Message from Thunderbird\n"
)
.as_bytes(),
false,
)
.await?;
let chat = alice.get_chat(&bob).await;
assert!(!chat.is_protected());
assert!(chat.is_protection_broken());
if alice_accepts_breakage {
tcm.section("Alice clicks 'Accept' on the input-bar-dialog");
chat.id.accept(&alice).await?;
let chat = alice.get_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
}
// Bob sends a message from DC again
tcm.send_recv(&bob, &alice, "Hello from DC").await;
let chat = alice.get_chat(&bob).await;
assert!(chat.is_protected());
assert!(!chat.is_protection_broken());
}
alice
.golden_test_chat(chat.id, "test_verified_oneonone_chat_enable_disable")
.await;
Ok(())
}
/// Messages with old timestamps are difficult for verified chats:
/// - They must not be sorted over a protection-changed info message.
/// That's what `test_old_message_2` tests
/// - If they change the protection, then they must not be sorted over existing other messages,
/// because then the protection-changed info message would also be above these existing messages.
/// That's what `test_old_message_3` tests.
///
/// `test_old_message_1` tests the case where both the old and the new message
/// change verification
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_old_message_1() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
let chat = alice.create_chat(&bob).await; // This creates a protection-changed info message
assert!(chat.is_protected());
// This creates protection-changed info message #2;
// even though the date is old, info message and email must be sorted below the original info message.
receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2-3@example.org>\n\
Date: Sat, 07 Dec 2019 19:00:27 +0000\n\
\n\
Message from Thunderbird\n",
true,
)
.await?;
alice.golden_test_chat(chat.id, "test_old_message_1").await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_old_message_2() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
// This creates protection-changed info message #1:
let chat = alice.create_chat(&bob).await;
assert!(chat.is_protected());
let protection_msg = alice.get_last_msg().await;
assert_eq!(
protection_msg.param.get_cmd(),
SystemMessage::ChatProtectionEnabled
);
// This creates protection-changed info message #2 with `timestamp_sort` greater by 1.
let first_email = receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2-3@example.org>\n\
Date: Sun, 08 Dec 2019 19:00:27 +0000\n\
\n\
Somewhat old message\n",
false,
)
.await?
.unwrap();
// Both messages will get the same timestamp, so this one will be sorted under the previous one
// even though it has an older timestamp.
let second_email = receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <2319-2-3@example.org>\n\
Date: Sat, 07 Dec 2019 19:00:27 +0000\n\
\n\
Even older message, that must NOT be shown before the info message\n",
true,
)
.await?
.unwrap();
assert_eq!(first_email.sort_timestamp, second_email.sort_timestamp);
assert_eq!(
first_email.sort_timestamp,
protection_msg.timestamp_sort + 1
);
alice.golden_test_chat(chat.id, "test_old_message_2").await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_old_message_3() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
tcm.send_recv_accept(&bob, &alice, "Heyho from my verified device!")
.await;
// This unverified message must not be sorted over the message sent in the previous line:
receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2-3@example.org>\n\
Date: Sat, 07 Dec 2019 19:00:27 +0000\n\
\n\
Old, unverified message\n",
true,
)
.await?;
alice
.golden_test_chat(alice.get_chat(&bob).await.id, "test_old_message_3")
.await;
let email_chat = alice.get_email_chat(&bob).await;
assert!(!email_chat.is_encrypted(&alice).await?);
let email_msg = get_chat_msg(&alice, email_chat.id, 0, 1).await;
assert_eq!(email_msg.text, "hello".to_string());
assert!(!email_msg.is_system_message());
Ok(())
}
@@ -602,13 +409,35 @@ async fn test_outgoing_mua_msg() -> Result<()> {
.unwrap();
tcm.send_recv(&alice, &bob, "Sending with DC again").await;
// Unencrypted message from MUA gets into a separate chat.
// PGP chat gets all encrypted messages.
alice
.golden_test_chat(sent.chat_id, "test_outgoing_mua_msg")
.await;
alice
.golden_test_chat(alice.get_chat(&bob).await.id, "test_outgoing_mua_msg_pgp")
.await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_outgoing_encrypted_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
enable_verified_oneonone_chats(&[alice]).await;
mark_as_verified(alice, bob).await;
let chat_id = alice.create_chat(bob).await.id;
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
receive_imf(alice, raw, false).await?;
alice
.golden_test_chat(chat_id, "test_outgoing_encrypted_msg")
.await;
Ok(())
}
/// If Bob answers unencrypted from another address with a classical MUA,
/// the message is under some circumstances still assigned to the original
/// chat (see lookup_chat_by_reply()); this is meant to make aliases
@@ -652,96 +481,23 @@ async fn test_reply() -> Result<()> {
let unencrypted_msg = Message::load_from_db(&alice, unencrypted_msg.msg_ids[0]).await?;
assert_eq!(unencrypted_msg.text, "Weird reply");
if verified {
assert_ne!(unencrypted_msg.chat_id, encrypted_msg.chat_id);
} else {
assert_eq!(unencrypted_msg.chat_id, encrypted_msg.chat_id);
}
assert_ne!(unencrypted_msg.chat_id, encrypted_msg.chat_id);
}
Ok(())
}
/// Regression test for the following bug:
///
/// - Scan your chat partner's QR Code
/// - They change devices
/// - They send you a message
/// - Without accepting the encryption downgrade, scan your chat partner's QR Code again
///
/// -> The re-verification fails.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_break_protection_then_verify_again() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
// Cave: Bob can't write a message to Alice here.
// If he did, alice would increase his peerstate's last_seen timestamp.
// Then, after Bob reinstalls DC, alice's `if message_time > last_seen*`
// checks would return false (there are many checks of this form in peerstate.rs).
// Therefore, during the securejoin, Alice wouldn't accept the new key
// and reject the securejoin.
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
alice.create_chat(&bob).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
let chats = Chatlist::try_load(&alice, DC_GCL_FOR_FORWARDING, None, None).await?;
assert_eq!(chats.len(), 1);
tcm.section("Bob reinstalls DC");
drop(bob);
let bob_new = tcm.unconfigured().await;
enable_verified_oneonone_chats(&[&bob_new]).await;
bob_new.configure_addr("bob@example.net").await;
e2ee::ensure_secret_key_exists(&bob_new).await?;
tcm.send_recv(&bob_new, &alice, "I have a new device").await;
let contact = alice.add_or_lookup_contact(&bob_new).await;
assert_eq!(
contact.is_verified(&alice).await.unwrap(),
// Bob sent a message with a new key, so he most likely doesn't have
// the old key anymore. This means that Alice's device should show
// him as unverified:
false
);
let chat = alice.get_chat(&bob_new).await;
assert_eq!(chat.is_protected(), false);
assert_eq!(chat.is_protection_broken(), true);
let chats = Chatlist::try_load(&alice, DC_GCL_FOR_FORWARDING, None, None).await?;
assert_eq!(chats.len(), 1);
{
let alice_bob_chat = alice.get_chat(&bob_new).await;
assert!(!alice_bob_chat.can_send(&alice).await?);
// Alice's UI should still be able to save a draft, which Alice started to type right when she got Bob's message:
let mut msg = Message::new_text("Draftttt".to_string());
alice_bob_chat.id.set_draft(&alice, Some(&mut msg)).await?;
assert_eq!(
alice_bob_chat.id.get_draft(&alice).await?.unwrap().text,
"Draftttt"
);
}
tcm.execute_securejoin(&alice, &bob_new).await;
assert_verified(&alice, &bob_new, ProtectionStatus::Protected).await;
Ok(())
}
/// Tests that message from old DC setup does not break
/// new verified chat.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_message_from_old_dc_setup() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob_old = &tcm.unconfigured().await;
enable_verified_oneonone_chats(&[alice, bob_old]).await;
mark_as_verified(bob_old, alice).await;
bob_old.configure_addr("bob@example.net").await;
mark_as_verified(bob_old, alice).await;
let chat = bob_old.create_chat(alice).await;
let sent_old = bob_old
.send_text(chat.id, "Soon i'll have a new device")
@@ -759,22 +515,15 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
assert_verified(alice, bob, ProtectionStatus::Protected).await;
let msg = alice.recv_msg(&sent_old).await;
assert!(!msg.get_showpadlock());
assert!(msg.get_showpadlock());
let contact = alice.add_or_lookup_contact(bob).await;
// The outdated Bob's Autocrypt header isn't applied, so the verification preserves.
// The outdated Bob's Autocrypt header isn't applied
// and the message goes to another chat, so the verification preserves.
assert!(contact.is_verified(alice).await.unwrap());
let chat = alice.get_chat(bob).await;
assert!(chat.is_protected());
assert_eq!(chat.is_protection_broken(), false);
let protection_msg = alice.get_last_msg().await;
assert_eq!(
protection_msg.param.get_cmd(),
SystemMessage::ChatProtectionEnabled
);
assert!(protection_msg.timestamp_sort >= msg.timestamp_rcvd);
alice
.golden_test_chat(msg.chat_id, "verified_chats_message_from_old_dc_setup")
.await;
Ok(())
}
@@ -811,43 +560,6 @@ async fn test_verify_then_verify_again() -> Result<()> {
Ok(())
}
/// Regression test:
/// - Verify a contact
/// - The contact stops using DC and sends a message from a classical MUA instead
/// - Delete the 1:1 chat
/// - Create a 1:1 chat
/// - Check that the created chat is not marked as protected
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_oneonone_chat_with_former_verified_contact() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice]).await;
mark_as_verified(&alice, &bob).await;
receive_imf(
&alice,
b"Subject: Message from bob\r\n\
From: <bob@example.net>\r\n\
To: <alice@example.org>\r\n\
Date: Mon, 12 Dec 2022 14:33:39 +0000\r\n\
Message-ID: <abcd@example.net>\r\n\
\r\n\
Heyho!\r\n",
false,
)
.await
.unwrap()
.unwrap();
alice.create_chat(&bob).await;
assert_verified(&alice, &bob, ProtectionStatus::Unprotected).await;
Ok(())
}
/// Tests that on the second device of a protected group creator the first message is
/// `SystemMessage::ChatProtectionEnabled` and the second one is the message populating the group.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -893,7 +605,7 @@ async fn test_verified_member_added_reordering() -> Result<()> {
let fiona = &tcm.fiona().await;
enable_verified_oneonone_chats(&[alice, bob, fiona]).await;
let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
// Bob and Fiona scan Alice's QR code.
tcm.execute_securejoin(bob, alice).await;