mirror of
https://github.com/chatmail/core.git
synced 2026-04-19 06:26:30 +03:00
1587 lines
57 KiB
Rust
1587 lines
57 KiB
Rust
use std::time::Duration;
|
|
|
|
use deltachat_contact_tools::EmailAddress;
|
|
use regex::Regex;
|
|
|
|
use super::*;
|
|
use crate::chat::{CantSendReason, add_contact_to_chat, remove_contact_from_chat};
|
|
use crate::chatlist::Chatlist;
|
|
use crate::constants::Chattype;
|
|
use crate::constants::DC_CHAT_ID_TRASH;
|
|
use crate::key::self_fingerprint;
|
|
use crate::mimeparser::{GossipedKey, SystemMessage};
|
|
use crate::qr::Qr;
|
|
use crate::receive_imf::receive_imf;
|
|
use crate::stock_str::{self, messages_e2e_encrypted};
|
|
use crate::test_utils::{
|
|
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
|
|
TimeShiftFalsePositiveNote, get_chat_msg, sync,
|
|
};
|
|
use crate::tools::SystemTime;
|
|
|
|
#[derive(PartialEq)]
|
|
enum SetupContactCase {
|
|
Normal,
|
|
WrongAliceGossip,
|
|
AliceIsBot,
|
|
AliceHasName,
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_setup_contact_basic() {
|
|
test_setup_contact_ex(SetupContactCase::Normal).await
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_setup_contact_wrong_alice_gossip() {
|
|
test_setup_contact_ex(SetupContactCase::WrongAliceGossip).await
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_setup_contact_alice_is_bot() {
|
|
test_setup_contact_ex(SetupContactCase::AliceIsBot).await
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_setup_contact_alice_has_name() {
|
|
test_setup_contact_ex(SetupContactCase::AliceHasName).await
|
|
}
|
|
|
|
async fn test_setup_contact_ex(case: SetupContactCase) {
|
|
let _n = TimeShiftFalsePositiveNote;
|
|
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = tcm.alice().await;
|
|
let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap();
|
|
if case == SetupContactCase::AliceHasName {
|
|
alice
|
|
.set_config(Config::Displayname, Some("Alice"))
|
|
.await
|
|
.unwrap();
|
|
}
|
|
let bob = tcm.bob().await;
|
|
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
|
|
.await
|
|
.unwrap();
|
|
let alice_auto_submitted_hdr: bool;
|
|
match case {
|
|
SetupContactCase::AliceIsBot => {
|
|
alice.set_config_bool(Config::Bot, true).await.unwrap();
|
|
alice_auto_submitted_hdr = true;
|
|
}
|
|
_ => alice_auto_submitted_hdr = false,
|
|
};
|
|
|
|
assert_eq!(
|
|
Chatlist::try_load(&alice, 0, None, None)
|
|
.await
|
|
.unwrap()
|
|
.len(),
|
|
0
|
|
);
|
|
assert_eq!(
|
|
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
|
0
|
|
);
|
|
|
|
tcm.section("Step 1: Generate QR-code, ChatId(0) indicates setup-contact");
|
|
let qr = get_securejoin_qr(&alice, None).await.unwrap();
|
|
// We want Bob to learn Alice's name from their messages, not from the QR code.
|
|
alice
|
|
.set_config(Config::Displayname, Some("Alice Exampleorg"))
|
|
.await
|
|
.unwrap();
|
|
|
|
tcm.section("Step 2: Bob scans QR-code, sends vc-request");
|
|
let bob_chat_id = join_securejoin(&bob.ctx, &qr).await.unwrap();
|
|
assert_eq!(
|
|
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
|
1
|
|
);
|
|
let bob_chat = Chat::load_from_db(&bob, bob_chat_id).await.unwrap();
|
|
assert_eq!(
|
|
bob_chat.why_cant_send(&bob).await.unwrap(),
|
|
Some(CantSendReason::MissingKey)
|
|
);
|
|
|
|
// Check Bob's info messages.
|
|
let msg_cnt = 2;
|
|
let mut i = 0..msg_cnt;
|
|
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
|
assert!(msg.is_info());
|
|
assert_eq!(msg.get_text(), messages_e2e_encrypted(&bob).await);
|
|
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
|
assert!(msg.is_info());
|
|
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
|
|
|
|
let contact_alice_id = bob.add_or_lookup_contact_no_key(&alice).await.id;
|
|
let sent = bob.pop_sent_msg().await;
|
|
assert!(!sent.payload.contains("Bob Examplenet"));
|
|
assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap());
|
|
let msg = alice.parse_msg(&sent).await;
|
|
assert!(msg.signature.is_none());
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
"vc-request-pubkey"
|
|
);
|
|
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
|
assert!(!msg.header_exists(HeaderDef::AutoSubmitted));
|
|
|
|
tcm.section("Step 3: Alice receives vc-request-pubkey, sends vc-pubkey");
|
|
alice.recv_msg_trash(&sent).await;
|
|
assert_eq!(
|
|
Chatlist::try_load(&alice, 0, None, None)
|
|
.await
|
|
.unwrap()
|
|
.len(),
|
|
0
|
|
);
|
|
|
|
let sent = alice.pop_sent_msg().await;
|
|
assert_eq!(sent.payload.contains("Auto-Submitted:"), false);
|
|
assert!(!sent.payload.contains("Alice Exampleorg"));
|
|
let msg = bob.parse_msg(&sent).await;
|
|
assert!(msg.was_encrypted());
|
|
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-pubkey");
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::AutoSubmitted),
|
|
alice_auto_submitted_hdr.then_some("auto-generated")
|
|
);
|
|
|
|
let bob_chat = bob.get_chat(&alice).await;
|
|
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
|
|
|
tcm.section("Step 4: Bob receives vc-auth-required, sends vc-request-with-auth");
|
|
bob.recv_msg_trash(&sent).await;
|
|
let bob_chat = bob.get_chat(&alice).await;
|
|
assert_eq!(bob_chat.why_cant_send(&bob).await.unwrap(), None);
|
|
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
|
|
|
// Check Bob emitted the JoinerProgress event.
|
|
let event = bob
|
|
.evtracker
|
|
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
|
.await;
|
|
match event {
|
|
EventType::SecurejoinJoinerProgress {
|
|
contact_id,
|
|
progress,
|
|
} => {
|
|
assert_eq!(contact_id, contact_alice_id);
|
|
assert_eq!(progress, 400);
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
|
|
// Check Bob sent the right message.
|
|
let sent = bob.pop_sent_msg().await;
|
|
assert!(!sent.payload.contains("Bob Examplenet"));
|
|
let mut msg = alice.parse_msg(&sent).await;
|
|
assert!(msg.was_encrypted());
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
"vc-request-with-auth"
|
|
);
|
|
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
|
let bob_fp = self_fingerprint(&bob).await.unwrap();
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
|
bob_fp
|
|
);
|
|
|
|
if case == SetupContactCase::WrongAliceGossip {
|
|
let wrong_pubkey = GossipedKey {
|
|
public_key: load_self_public_key(&bob).await.unwrap(),
|
|
verified: false,
|
|
};
|
|
let alice_pubkey = msg
|
|
.gossiped_keys
|
|
.insert(alice_addr.to_string(), wrong_pubkey)
|
|
.unwrap();
|
|
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
|
|
let handshake_msg = handle_securejoin_handshake(&alice, &mut msg, contact_bob.id)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
|
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false);
|
|
|
|
msg.gossiped_keys
|
|
.insert(alice_addr.to_string(), alice_pubkey)
|
|
.unwrap();
|
|
let handshake_msg = handle_securejoin_handshake(&alice, &mut msg, contact_bob.id)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
|
assert!(contact_bob.is_verified(&alice).await.unwrap());
|
|
return;
|
|
}
|
|
|
|
// Alice should not yet have Bob verified
|
|
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
|
|
let contact_bob_id = contact_bob.id;
|
|
assert_eq!(contact_bob.is_key_contact(), true);
|
|
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false);
|
|
assert_eq!(contact_bob.get_authname(), "");
|
|
|
|
tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm");
|
|
alice.recv_msg_trash(&sent).await;
|
|
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), true);
|
|
let contact_bob = Contact::get_by_id(&alice, contact_bob_id).await.unwrap();
|
|
assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
|
|
assert!(contact_bob.get_name().is_empty());
|
|
assert_eq!(contact_bob.is_bot(), false);
|
|
|
|
// exactly one one-to-one chat should be visible for both now
|
|
// (check this before calling alice.get_chat() explicitly below)
|
|
assert_eq!(
|
|
Chatlist::try_load(&alice, 0, None, None)
|
|
.await
|
|
.unwrap()
|
|
.len(),
|
|
1
|
|
);
|
|
assert_eq!(
|
|
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
|
1
|
|
);
|
|
|
|
// Check Alice got the verified message in her 1:1 chat.
|
|
{
|
|
let chat = alice.get_chat(&bob).await;
|
|
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
|
|
assert!(msg.is_info());
|
|
let expected_text = messages_e2e_encrypted(&alice).await;
|
|
assert_eq!(msg.get_text(), expected_text);
|
|
}
|
|
|
|
// Make sure Alice hasn't yet sent their name to Bob.
|
|
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
|
.await
|
|
.unwrap();
|
|
match case {
|
|
SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"),
|
|
_ => assert_eq!(contact_alice.get_authname(), ""),
|
|
};
|
|
|
|
// Check Alice sent the right message to Bob.
|
|
let sent = alice.pop_sent_msg().await;
|
|
assert_eq!(
|
|
sent.payload.contains("Auto-Submitted: auto-generated"),
|
|
false
|
|
);
|
|
assert!(!sent.payload.contains("Alice Exampleorg"));
|
|
let msg = bob.parse_msg(&sent).await;
|
|
assert!(msg.was_encrypted());
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
"vc-contact-confirm"
|
|
);
|
|
|
|
// Bob has verified Alice already.
|
|
//
|
|
// Alice may not have verified Bob yet.
|
|
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true);
|
|
|
|
// Step 7: Bob receives vc-contact-confirm
|
|
bob.recv_msg_trash(&sent).await;
|
|
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true);
|
|
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(contact_alice.get_authname(), "Alice Exampleorg");
|
|
assert!(contact_alice.get_name().is_empty());
|
|
assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot);
|
|
|
|
// The `SecurejoinWait` info message has been removed, but the e2ee notice remains.
|
|
let msg = get_chat_msg(&bob, bob_chat.get_id(), 0, 1).await;
|
|
assert!(msg.is_info());
|
|
assert_eq!(msg.get_text(), messages_e2e_encrypted(&bob).await);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_setup_contact_bad_qr() {
|
|
let bob = TestContext::new_bob().await;
|
|
let ret = join_securejoin(&bob.ctx, "not a qr code").await;
|
|
assert!(ret.is_err());
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
|
|
// Ensure Bob knows Alice_FP
|
|
let alice_contact_id = bob.add_or_lookup_contact_id(alice).await;
|
|
|
|
tcm.section("Step 1: Generate QR-code");
|
|
// `None` indicates setup-contact.
|
|
let qr = get_securejoin_qr(alice, None).await?;
|
|
|
|
tcm.section("Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request");
|
|
join_securejoin(bob, &qr).await.unwrap();
|
|
|
|
// Check Bob emitted the JoinerProgress event.
|
|
let event = bob
|
|
.evtracker
|
|
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
|
.await;
|
|
match event {
|
|
EventType::SecurejoinJoinerProgress {
|
|
contact_id,
|
|
progress,
|
|
} => {
|
|
assert_eq!(contact_id, alice_contact_id);
|
|
assert_eq!(progress, 400);
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
|
|
// Check Bob sent the right handshake message.
|
|
let sent = bob.pop_sent_msg().await;
|
|
let msg = alice.parse_msg(&sent).await;
|
|
assert!(msg.was_encrypted());
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
"vc-request-with-auth"
|
|
);
|
|
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
|
let bob_fp = load_self_public_key(bob).await?.dc_fingerprint();
|
|
assert_eq!(
|
|
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
|
bob_fp.hex()
|
|
);
|
|
|
|
// Alice should not yet have Bob verified
|
|
let contact_bob = alice.add_or_lookup_contact_no_key(bob).await;
|
|
assert_eq!(contact_bob.is_verified(alice).await?, false);
|
|
|
|
tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm");
|
|
alice.recv_msg_trash(&sent).await;
|
|
assert_eq!(contact_bob.is_verified(alice).await?, true);
|
|
|
|
// Check Alice signalled success via the SecurejoinInviterProgress event.
|
|
let event = alice
|
|
.evtracker
|
|
.get_matching(|evt| {
|
|
matches!(
|
|
evt,
|
|
EventType::SecurejoinInviterProgress { progress: 1000, .. }
|
|
)
|
|
})
|
|
.await;
|
|
match event {
|
|
EventType::SecurejoinInviterProgress {
|
|
contact_id,
|
|
chat_type,
|
|
progress,
|
|
..
|
|
} => {
|
|
assert_eq!(contact_id, contact_bob.id);
|
|
assert_eq!(chat_type, Chattype::Single);
|
|
assert_eq!(progress, 1000);
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
|
|
let sent = alice.pop_sent_msg().await;
|
|
let msg = bob.parse_msg(&sent).await;
|
|
assert!(msg.was_encrypted());
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
"vc-contact-confirm"
|
|
);
|
|
|
|
// Bob has verified Alice already.
|
|
let contact_alice = bob.add_or_lookup_contact_no_key(alice).await;
|
|
assert_eq!(contact_alice.is_verified(bob).await?, true);
|
|
|
|
// Alice confirms that Bob is now verified.
|
|
//
|
|
// This does not change anything for Bob.
|
|
tcm.section("Step 7: Bob receives vc-contact-confirm");
|
|
bob.recv_msg_trash(&sent).await;
|
|
assert_eq!(contact_alice.is_verified(bob).await?, true);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_setup_contact_concurrent_calls() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = tcm.alice().await;
|
|
let bob = tcm.bob().await;
|
|
|
|
// do a scan that is not working as claire is never responding
|
|
let qr_stale = "OPENPGP4FPR:1234567890123456789012345678901234567890#a=claire%40foo.de&n=&i=12345678901&s=23456789012";
|
|
let claire_id = join_securejoin(&bob, qr_stale).await?;
|
|
let chat = Chat::load_from_db(&bob, claire_id).await?;
|
|
assert!(!claire_id.is_special());
|
|
assert_eq!(chat.typ, Chattype::Single);
|
|
assert!(bob.pop_sent_msg().await.payload().contains("claire@foo.de"));
|
|
|
|
// subsequent scans shall abort existing ones or run concurrently -
|
|
// but they must not fail as otherwise the whole qr scanning becomes unusable until restart.
|
|
let qr = get_securejoin_qr(&alice, None).await?;
|
|
let alice_id = join_securejoin(&bob, &qr).await?;
|
|
let chat = Chat::load_from_db(&bob, alice_id).await?;
|
|
assert!(!alice_id.is_special());
|
|
assert_eq!(chat.typ, Chattype::Single);
|
|
assert_ne!(claire_id, alice_id);
|
|
assert_eq!(
|
|
bob.pop_sent_msg()
|
|
.await
|
|
.payload()
|
|
.contains("alice@example.org"),
|
|
false // Alice's address must not be sent in cleartext
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_secure_join_group_legacy() -> Result<()> {
|
|
test_secure_join_group_ex(false, false).await
|
|
}
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_secure_join_group_v3() -> Result<()> {
|
|
test_secure_join_group_ex(true, false).await
|
|
}
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_secure_join_group_v3_without_invite() -> Result<()> {
|
|
test_secure_join_group_ex(true, true).await
|
|
}
|
|
|
|
async fn test_secure_join_group_ex(v3: bool, remove_invite: bool) -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = tcm.alice().await;
|
|
let bob = tcm.bob().await;
|
|
|
|
// We start with empty chatlists.
|
|
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
|
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
|
|
|
let alice_chatid = chat::create_group(&alice, "the chat").await?;
|
|
|
|
tcm.section("Step 1: Generate QR-code, secure-join implied by chatid");
|
|
let mut qr = get_securejoin_qr(&alice, Some(alice_chatid)).await.unwrap();
|
|
manipulate_qr(v3, remove_invite, &mut qr);
|
|
|
|
tcm.section("Step 2: Bob scans QR-code, sends vg-request");
|
|
let bob_chatid = join_securejoin(&bob, &qr).await?;
|
|
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
|
|
|
let sent = bob.pop_sent_msg().await;
|
|
assert_eq!(
|
|
sent.recipient(),
|
|
EmailAddress::new("alice@example.org").unwrap()
|
|
);
|
|
let msg = alice.parse_msg(&sent).await;
|
|
assert!(msg.signature.is_none());
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
if v3 {
|
|
"vc-request-pubkey"
|
|
} else {
|
|
"vg-request"
|
|
}
|
|
);
|
|
assert_eq!(msg.get_header(HeaderDef::SecureJoinAuth).is_some(), v3);
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some(),
|
|
!v3
|
|
);
|
|
assert!(!msg.header_exists(HeaderDef::AutoSubmitted));
|
|
|
|
// Old Delta Chat core sent `Secure-Join-Group` header in `vg-request`,
|
|
// but it was only used by Alice in `vg-request-with-auth`.
|
|
// New Delta Chat versions do not use `Secure-Join-Group` header at all
|
|
// and it is deprecated.
|
|
// Now `Secure-Join-Group` header
|
|
// is only sent in `vg-request-with-auth` for compatibility.
|
|
assert!(!msg.header_exists(HeaderDef::SecureJoinGroup));
|
|
|
|
tcm.section("Step 3: Alice receives vc-request-pubkey and sends vc-pubkey, or receives vg-request and sends vg-auth-required");
|
|
alice.recv_msg_trash(&sent).await;
|
|
|
|
let sent = alice.pop_sent_msg().await;
|
|
let msg = bob.parse_msg(&sent).await;
|
|
assert!(msg.was_encrypted());
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
if v3 { "vc-pubkey" } else { "vg-auth-required" }
|
|
);
|
|
|
|
tcm.section("Step 4: Bob receives vc-pubkey or vg-auth-required, sends v*-request-with-auth");
|
|
bob.recv_msg_trash(&sent).await;
|
|
let sent = bob.pop_sent_msg().await;
|
|
|
|
// At this point Alice is still not part of the chat.
|
|
// The final step of Alice adding Bob to the chat
|
|
// may not work out and we don't want Bob
|
|
// to implicitly add Alice if he manages to join the group
|
|
// much later via another member.
|
|
assert_eq!(chat::get_chat_contacts(&bob, bob_chatid).await?.len(), 0);
|
|
|
|
let contact_alice_id = bob.add_or_lookup_contact_no_key(&alice).await.id;
|
|
|
|
// Check Bob emitted the JoinerProgress event.
|
|
let event = bob
|
|
.evtracker
|
|
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
|
.await;
|
|
match event {
|
|
EventType::SecurejoinJoinerProgress {
|
|
contact_id,
|
|
progress,
|
|
} => {
|
|
assert_eq!(contact_id, contact_alice_id);
|
|
assert_eq!(progress, 400);
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
|
|
// Check Bob sent the right handshake message.
|
|
let msg = alice.parse_msg(&sent).await;
|
|
assert!(msg.was_encrypted());
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
"vg-request-with-auth"
|
|
);
|
|
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
|
let bob_fp = self_fingerprint(&bob).await?;
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
|
bob_fp
|
|
);
|
|
|
|
// Alice should not yet have Bob verified
|
|
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
|
|
assert_eq!(contact_bob.is_verified(&alice).await?, false);
|
|
|
|
tcm.section("Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added");
|
|
alice.recv_msg_trash(&sent).await;
|
|
assert_eq!(contact_bob.is_verified(&alice).await?, true);
|
|
|
|
// Check Alice signalled success via the SecurejoinInviterProgress event.
|
|
let event = alice
|
|
.evtracker
|
|
.get_matching(|evt| {
|
|
matches!(
|
|
evt,
|
|
EventType::SecurejoinInviterProgress { progress: 1000, .. }
|
|
)
|
|
})
|
|
.await;
|
|
match event {
|
|
EventType::SecurejoinInviterProgress {
|
|
contact_id,
|
|
chat_type,
|
|
chat_id,
|
|
progress,
|
|
} => {
|
|
assert_eq!(contact_id, contact_bob.id);
|
|
assert_eq!(chat_type, Chattype::Group);
|
|
assert_eq!(chat_id, alice_chatid);
|
|
assert_eq!(progress, 1000);
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
|
|
let sent = alice.pop_sent_msg().await;
|
|
let msg = bob.parse_msg(&sent).await;
|
|
assert!(msg.was_encrypted());
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
"vg-member-added"
|
|
);
|
|
// Formally this message is auto-submitted, but as the member addition is a result of an
|
|
// explicit user action, the Auto-Submitted header shouldn't be present. Otherwise it would
|
|
// be strange to have it in "member-added" messages of verified groups only.
|
|
assert!(!msg.header_exists(HeaderDef::AutoSubmitted));
|
|
// This is a two-member group, but Alice must Autocrypt-gossip to her other devices.
|
|
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some());
|
|
|
|
{
|
|
// Now Alice's chat with Bob should still be hidden, the verified message should
|
|
// appear in the group chat.
|
|
if v3 {
|
|
assert!(
|
|
ChatIdBlocked::lookup_by_contact(&alice, contact_bob.id)
|
|
.await?
|
|
.is_none()
|
|
);
|
|
} else {
|
|
let chat = alice.get_chat(&bob).await;
|
|
assert_eq!(
|
|
chat.blocked,
|
|
Blocked::Yes,
|
|
"Alice's 1:1 chat with Bob is not hidden"
|
|
);
|
|
}
|
|
|
|
// There should be 2 messages in the chat:
|
|
// - The ChatProtectionEnabled message
|
|
// - You added member bob@example.net
|
|
let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await;
|
|
assert!(msg.is_info());
|
|
let expected_text = messages_e2e_encrypted(&alice).await;
|
|
assert_eq!(msg.get_text(), expected_text);
|
|
}
|
|
|
|
// Bob has verified Alice already.
|
|
//
|
|
// Alice may not have verified Bob yet.
|
|
let contact_alice = bob.add_or_lookup_contact_no_key(&alice).await;
|
|
assert_eq!(contact_alice.is_verified(&bob).await?, true);
|
|
|
|
tcm.section("Step 7: Bob receives vg-member-added");
|
|
bob.recv_msg(&sent).await;
|
|
{
|
|
// Bob has Alice verified, message shows up in the group chat.
|
|
assert_eq!(contact_alice.is_verified(&bob).await?, true);
|
|
let chat = bob.get_chat(&alice).await;
|
|
assert_eq!(
|
|
chat.blocked,
|
|
Blocked::Yes,
|
|
"Bob's 1:1 chat with Alice is not hidden"
|
|
);
|
|
for item in chat::get_chat_msgs(&bob.ctx, bob_chatid).await.unwrap() {
|
|
if let chat::ChatItem::Message { msg_id } = item {
|
|
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
|
|
let text = msg.get_text();
|
|
println!("msg {msg_id} text: {text}");
|
|
}
|
|
}
|
|
}
|
|
|
|
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
|
|
assert!(bob_chat.typ == Chattype::Group);
|
|
|
|
// At the end of the protocol
|
|
// Bob should have two members in the chat.
|
|
assert_eq!(chat::get_chat_contacts(&bob, bob_chatid).await?.len(), 2);
|
|
|
|
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
|
|
// The one-to-one chats are used internally for the hidden handshake messages,
|
|
// however, should not be visible in the UIs.
|
|
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
|
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
|
|
|
// If Bob then sends a direct message to alice, however, the one-to-one with Alice should appear.
|
|
let bobs_chat_with_alice = bob.create_chat(&alice).await;
|
|
let sent = bob.send_text(bobs_chat_with_alice.id, "Hello").await;
|
|
alice.recv_msg(&sent).await;
|
|
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 2);
|
|
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 2);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_secure_join_broadcast_legacy() -> Result<()> {
|
|
test_secure_join_broadcast_ex(false, false).await
|
|
}
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_secure_join_broadcast_v3() -> Result<()> {
|
|
test_secure_join_broadcast_ex(true, false).await
|
|
}
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_secure_join_broadcast_v3_without_invite() -> Result<()> {
|
|
test_secure_join_broadcast_ex(true, true).await
|
|
}
|
|
|
|
async fn test_secure_join_broadcast_ex(v3: bool, remove_invite: bool) -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
|
|
let alice_chat_id = chat::create_broadcast(alice, "Channel".to_string()).await?;
|
|
let mut qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
|
|
manipulate_qr(v3, remove_invite, &mut qr);
|
|
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
|
|
|
let sent = alice.send_text(alice_chat_id, "Hi channel").await;
|
|
assert!(sent.recipients.contains("bob@example.net"));
|
|
let rcvd = bob.recv_msg(&sent).await;
|
|
assert_eq!(rcvd.chat_id, bob_chat_id);
|
|
assert_eq!(rcvd.text, "Hi channel");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_setup_contact_compatibility_legacy() -> Result<()> {
|
|
test_setup_contact_compatibility_ex(false, false).await
|
|
}
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_setup_contact_compatibility_v3() -> Result<()> {
|
|
test_setup_contact_compatibility_ex(true, false).await
|
|
}
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_setup_contact_compatibility_v3_without_invite() -> Result<()> {
|
|
test_setup_contact_compatibility_ex(true, true).await
|
|
}
|
|
|
|
async fn test_setup_contact_compatibility_ex(v3: bool, remove_invite: bool) -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
alice.set_config(Config::Displayname, Some("Alice")).await?;
|
|
|
|
let mut qr = get_securejoin_qr(alice, None).await?;
|
|
manipulate_qr(v3, remove_invite, &mut qr);
|
|
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
|
|
|
let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?;
|
|
assert_eq!(bob_chat.name, "Alice");
|
|
assert!(bob_chat.can_send(bob).await?);
|
|
assert_eq!(bob_chat.typ, Chattype::Single);
|
|
assert_eq!(bob_chat.id, bob.get_chat(alice).await.id);
|
|
|
|
let alice_chat = alice.get_chat(bob).await;
|
|
assert_eq!(alice_chat.name, "bob@example.net");
|
|
assert!(alice_chat.can_send(alice).await?);
|
|
assert_eq!(alice_chat.typ, Chattype::Single);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn manipulate_qr(v3: bool, remove_invite: bool, qr: &mut String) {
|
|
if remove_invite {
|
|
// Remove the INVITENUBMER. It's not needed in Securejoin v3,
|
|
// but still included for backwards compatibility reasons.
|
|
// We want to be able to remove it in the future,
|
|
// therefore we test that things work without it.
|
|
let new_qr = Regex::new("&i=.*?&").unwrap().replace(qr, "&");
|
|
// Broadcast channels use `j` for the INVITENUMBER
|
|
let new_qr = Regex::new("&j=.*?&").unwrap().replace(&new_qr, "&");
|
|
assert!(new_qr != *qr);
|
|
*qr = new_qr.to_string();
|
|
}
|
|
// If `!v3`, force legacy securejoin to run by removing the &v=3 parameter.
|
|
// If `remove_invite`, we can also remove the v=3 parameter,
|
|
// because a QR with AUTH but no INVITE is obviously v3 QR code.
|
|
if !v3 || remove_invite {
|
|
let new_qr = Regex::new("&v=3").unwrap().replace(qr, "");
|
|
assert!(new_qr != *qr);
|
|
*qr = new_qr.to_string();
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_adhoc_group_no_qr() -> Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
|
|
let mime = br#"Subject: First thread
|
|
Message-ID: first@example.org
|
|
To: Alice <alice@example.org>, Bob <bob@example.net>
|
|
From: Claire <claire@example.org>
|
|
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
|
|
|
First thread."#;
|
|
|
|
receive_imf(&alice, mime, false).await?;
|
|
let msg = alice.get_last_msg().await;
|
|
let chat_id = msg.chat_id;
|
|
|
|
assert!(get_securejoin_qr(&alice, Some(chat_id)).await.is_err());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_unknown_sender() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = tcm.alice().await;
|
|
let bob = tcm.bob().await;
|
|
|
|
tcm.execute_securejoin(&alice, &bob).await;
|
|
|
|
let alice_chat_id = alice
|
|
.create_group_with_members("Group with Bob", &[&bob])
|
|
.await;
|
|
|
|
let sent = alice.send_text(alice_chat_id, "Hi!").await;
|
|
let bob_chat_id = bob.recv_msg(&sent).await.chat_id;
|
|
|
|
let sent = bob.send_text(bob_chat_id, "Hi hi!").await;
|
|
|
|
let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
|
|
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
|
alice.pop_sent_msg().await;
|
|
|
|
// The message from Bob is delivered late, Bob is already removed.
|
|
let msg = alice.recv_msg(&sent).await;
|
|
assert_eq!(msg.text, "Hi hi!");
|
|
assert_eq!(msg.get_override_sender_name().unwrap(), "bob@example.net");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that Bob gets Alice as verified
|
|
/// if `vc-contact-confirm` is lost.
|
|
/// Previously `vc-contact-confirm` was used
|
|
/// to confirm backward verification,
|
|
/// but backward verification is not tracked anymore.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_lost_contact_confirm() {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = tcm.alice().await;
|
|
let bob = tcm.bob().await;
|
|
|
|
let qr = get_securejoin_qr(&alice, None).await.unwrap();
|
|
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
|
|
|
// vc-request
|
|
let sent = bob.pop_sent_msg().await;
|
|
alice.recv_msg_trash(&sent).await;
|
|
|
|
// vc-auth-required
|
|
let sent = alice.pop_sent_msg().await;
|
|
bob.recv_msg_trash(&sent).await;
|
|
|
|
// vc-request-with-auth
|
|
let sent = bob.pop_sent_msg().await;
|
|
alice.recv_msg_trash(&sent).await;
|
|
|
|
// Alice has Bob verified now.
|
|
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
|
|
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), true);
|
|
|
|
// Alice sends vc-contact-confirm, but it gets lost.
|
|
let _sent_vc_contact_confirm = alice.pop_sent_msg().await;
|
|
|
|
// Bob has alice as verified too, even though vc-contact-confirm is lost.
|
|
let contact_alice = bob.add_or_lookup_contact_no_key(&alice).await;
|
|
assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), true);
|
|
}
|
|
|
|
/// Tests Bob joining two groups by scanning two QR codes
|
|
/// from the same Alice at the same time.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_parallel_securejoin() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
|
|
let alice_chat1_id = chat::create_group(alice, "First chat").await?;
|
|
let alice_chat2_id = chat::create_group(alice, "Second chat").await?;
|
|
|
|
let qr1 = get_securejoin_qr(alice, Some(alice_chat1_id)).await?;
|
|
let qr2 = get_securejoin_qr(alice, Some(alice_chat2_id)).await?;
|
|
|
|
// Bob scans both QR codes.
|
|
let bob_chat1_id = join_securejoin(bob, &qr1).await?;
|
|
let sent_vg_request1 = bob.pop_sent_msg().await;
|
|
|
|
let bob_chat2_id = join_securejoin(bob, &qr2).await?;
|
|
let sent_vg_request2 = bob.pop_sent_msg().await;
|
|
|
|
// Alice receives two `vg-request` messages
|
|
// and sends back two `vg-auth-required` messages.
|
|
alice.recv_msg_trash(&sent_vg_request1).await;
|
|
let sent_vg_auth_required1 = alice.pop_sent_msg().await;
|
|
|
|
alice.recv_msg_trash(&sent_vg_request2).await;
|
|
let _sent_vg_auth_required2 = alice.pop_sent_msg().await;
|
|
|
|
// Bob receives first `vg-auth-required` message.
|
|
// Bob has two securejoin processes started,
|
|
// so he should send two `vg-request-with-auth` messages.
|
|
bob.recv_msg_trash(&sent_vg_auth_required1).await;
|
|
|
|
// Bob sends `vg-request-with-auth` messages.
|
|
let sent_vg_request_with_auth2 = bob.pop_sent_msg().await;
|
|
let sent_vg_request_with_auth1 = bob.pop_sent_msg().await;
|
|
|
|
// Alice receives both `vg-request-with-auth` messages.
|
|
alice.recv_msg_trash(&sent_vg_request_with_auth1).await;
|
|
let sent_vg_member_added1 = alice.pop_sent_msg().await;
|
|
let joined_chat_id1 = bob.recv_msg(&sent_vg_member_added1).await.chat_id;
|
|
assert_eq!(joined_chat_id1, bob_chat1_id);
|
|
|
|
alice.recv_msg_trash(&sent_vg_request_with_auth2).await;
|
|
let sent_vg_member_added2 = alice.pop_sent_msg().await;
|
|
let joined_chat_id2 = bob.recv_msg(&sent_vg_member_added2).await.chat_id;
|
|
assert_eq!(joined_chat_id2, bob_chat2_id);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests Bob scanning setup contact QR codes of Alice and Fiona
|
|
/// concurrently.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_parallel_setup_contact_basic() -> Result<()> {
|
|
test_parallel_setup_contact(false).await
|
|
}
|
|
|
|
/// Tests Bob scanning setup contact QR codes of Alice and Fiona
|
|
/// concurrently, and then deleting the Fiona contact.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_parallel_setup_contact_bob_deletes_fiona() -> Result<()> {
|
|
test_parallel_setup_contact(true).await
|
|
}
|
|
|
|
async fn test_parallel_setup_contact(bob_deletes_fiona_contact: bool) -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
let fiona = &tcm.fiona().await;
|
|
|
|
// Bob scans Alice's QR code,
|
|
// but Alice is offline and takes a while to respond.
|
|
let alice_qr = get_securejoin_qr(alice, None).await?;
|
|
join_securejoin(bob, &alice_qr).await?;
|
|
let sent_alice_vc_request = bob.pop_sent_msg().await;
|
|
|
|
// Bob scans Fiona's QR code while SecureJoin
|
|
// process with Alice is not finished.
|
|
let fiona_qr = get_securejoin_qr(fiona, None).await?;
|
|
join_securejoin(bob, &fiona_qr).await?;
|
|
let sent_fiona_vc_request = bob.pop_sent_msg().await;
|
|
|
|
fiona.recv_msg_trash(&sent_fiona_vc_request).await;
|
|
let sent_fiona_vc_auth_required = fiona.pop_sent_msg().await;
|
|
|
|
let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).await;
|
|
if bob_deletes_fiona_contact {
|
|
bob.get_chat(fiona).await.id.delete(bob).await?;
|
|
Contact::delete(bob, bob_fiona_contact_id).await?;
|
|
|
|
bob.recv_msg_trash(&sent_fiona_vc_auth_required).await;
|
|
let sent = bob.pop_sent_msg_opt(Duration::ZERO).await;
|
|
assert!(sent.is_none());
|
|
} else {
|
|
bob.recv_msg_trash(&sent_fiona_vc_auth_required).await;
|
|
let sent_fiona_vc_request_with_auth = bob.pop_sent_msg().await;
|
|
|
|
fiona.recv_msg_trash(&sent_fiona_vc_request_with_auth).await;
|
|
let sent_fiona_vc_contact_confirm = fiona.pop_sent_msg().await;
|
|
|
|
bob.recv_msg_trash(&sent_fiona_vc_contact_confirm).await;
|
|
let bob_fiona_contact = Contact::get_by_id(bob, bob_fiona_contact_id).await.unwrap();
|
|
assert_eq!(bob_fiona_contact.is_verified(bob).await.unwrap(), true);
|
|
}
|
|
|
|
// Alice gets online and previously started SecureJoin process finishes.
|
|
alice.recv_msg_trash(&sent_alice_vc_request).await;
|
|
let sent_alice_vc_auth_required = alice.pop_sent_msg().await;
|
|
|
|
bob.recv_msg_trash(&sent_alice_vc_auth_required).await;
|
|
let sent_alice_vc_request_with_auth = bob.pop_sent_msg().await;
|
|
|
|
alice.recv_msg_trash(&sent_alice_vc_request_with_auth).await;
|
|
let sent_alice_vc_contact_confirm = alice.pop_sent_msg().await;
|
|
|
|
bob.recv_msg_trash(&sent_alice_vc_contact_confirm).await;
|
|
let bob_alice_contact_id = bob.add_or_lookup_contact_id(alice).await;
|
|
let bob_alice_contact = Contact::get_by_id(bob, bob_alice_contact_id).await.unwrap();
|
|
assert_eq!(bob_alice_contact.is_verified(bob).await.unwrap(), true);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_wrong_auth_token() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
|
|
// Bob should already have Alice's key
|
|
// so that he can directly send vc-request-with-auth
|
|
tcm.send_recv(alice, bob, "hi").await;
|
|
|
|
let alice_qr = get_securejoin_qr(alice, None).await?;
|
|
println!("{}", &alice_qr);
|
|
let invalid_alice_qr = alice_qr.replace("&s=", "&s=INVALIDAUTHTOKEN&someotherkey=");
|
|
|
|
join_securejoin(bob, &invalid_alice_qr).await?;
|
|
let sent = bob.pop_sent_msg().await;
|
|
|
|
let msg = alice.parse_msg(&sent).await;
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
"vc-request-with-auth"
|
|
);
|
|
|
|
alice.recv_msg_trash(&sent).await;
|
|
|
|
let alice_bob_contact = alice.add_or_lookup_contact(bob).await;
|
|
assert!(!alice_bob_contact.is_verified(alice).await?);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_send_avatar_in_securejoin() -> Result<()> {
|
|
async fn exec_securejoin_group(
|
|
tcm: &TestContextManager,
|
|
scanner: &TestContext,
|
|
scanned: &TestContext,
|
|
) {
|
|
let chat_id = chat::create_group(scanned, "group").await.unwrap();
|
|
let qr = get_securejoin_qr(scanned, Some(chat_id)).await.unwrap();
|
|
tcm.exec_securejoin_qr(scanner, scanned, &qr).await;
|
|
}
|
|
async fn exec_securejoin_broadcast(
|
|
tcm: &TestContextManager,
|
|
scanner: &TestContext,
|
|
scanned: &TestContext,
|
|
) {
|
|
let chat_id = chat::create_broadcast(scanned, "group".to_string())
|
|
.await
|
|
.unwrap();
|
|
let qr = get_securejoin_qr(scanned, Some(chat_id)).await.unwrap();
|
|
tcm.exec_securejoin_qr(scanner, scanned, &qr).await;
|
|
}
|
|
|
|
for round in 0..6 {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
|
|
let file = alice.dir.path().join("avatar.png");
|
|
tokio::fs::write(&file, AVATAR_64x64_BYTES).await?;
|
|
alice
|
|
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
|
|
.await?;
|
|
|
|
match round {
|
|
0 => {
|
|
tcm.execute_securejoin(alice, bob).await;
|
|
}
|
|
1 => {
|
|
tcm.execute_securejoin(bob, alice).await;
|
|
}
|
|
2 => {
|
|
exec_securejoin_group(&tcm, alice, bob).await;
|
|
}
|
|
3 => {
|
|
exec_securejoin_group(&tcm, bob, alice).await;
|
|
}
|
|
4 => {
|
|
exec_securejoin_broadcast(&tcm, alice, bob).await;
|
|
}
|
|
5 => {
|
|
exec_securejoin_broadcast(&tcm, bob, alice).await;
|
|
}
|
|
_ => panic!(),
|
|
}
|
|
|
|
let alice_on_bob = bob.add_or_lookup_contact_no_key(alice).await;
|
|
let avatar = alice_on_bob.get_profile_image(bob).await?.unwrap();
|
|
assert_eq!(
|
|
avatar.file_name().unwrap().to_str().unwrap(),
|
|
AVATAR_64x64_DEDUPLICATED
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that scanning a QR code week later
|
|
/// allows Bob to establish a contact with Alice,
|
|
/// but does not mark Bob as verified for Alice.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_expired_contact_auth_token() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
|
|
// Alice creates a QR code.
|
|
let qr = get_securejoin_qr(alice, None).await?;
|
|
|
|
// One week passes, QR code expires.
|
|
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
|
|
|
|
// Bob scans the QR code.
|
|
join_securejoin(bob, &qr).await?;
|
|
|
|
// vc-request
|
|
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
|
|
|
|
// vc-auth-requried
|
|
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
|
|
|
// vc-request-with-auth
|
|
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
|
|
|
|
// Bob should not be verified for Alice.
|
|
let contact_bob = alice.add_or_lookup_contact_no_key(bob).await;
|
|
assert_eq!(contact_bob.is_verified(alice).await.unwrap(), false);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_expired_group_auth_token() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
|
|
let alice_chat_id = chat::create_group(alice, "Group").await?;
|
|
|
|
// Alice creates a group QR code.
|
|
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
|
|
|
|
// One week passes, QR code expires.
|
|
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
|
|
|
|
// Bob scans the QR code.
|
|
join_securejoin(bob, &qr).await?;
|
|
|
|
// vg-request
|
|
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
|
|
|
|
// vg-auth-requried
|
|
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
|
|
|
// vg-request-with-auth
|
|
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
|
|
|
|
// vg-member-added
|
|
let bob_member_added_msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
|
assert!(bob_member_added_msg.is_info());
|
|
assert_eq!(
|
|
bob_member_added_msg.get_info_type(),
|
|
SystemMessage::MemberAddedToGroup
|
|
);
|
|
|
|
// Bob should not be verified for Alice.
|
|
let contact_bob = alice.add_or_lookup_contact_no_key(bob).await;
|
|
assert_eq!(contact_bob.is_verified(alice).await.unwrap(), false);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that old token is considered expired
|
|
/// even if sync message just arrived.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_expired_synced_auth_token() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let alice2 = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
|
|
alice.set_config_bool(Config::SyncMsgs, true).await?;
|
|
alice2.set_config_bool(Config::SyncMsgs, true).await?;
|
|
|
|
// Alice creates a QR code on the second device.
|
|
let qr = get_securejoin_qr(alice2, None).await?;
|
|
|
|
alice2.send_sync_msg().await.unwrap();
|
|
let sync_msg = alice2.pop_sent_msg().await;
|
|
|
|
// One week passes, QR code expires.
|
|
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
|
|
|
|
alice.recv_msg_trash(&sync_msg).await;
|
|
|
|
// Bob scans the QR code.
|
|
join_securejoin(bob, &qr).await?;
|
|
|
|
// vc-request
|
|
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
|
|
|
|
// vc-auth-requried
|
|
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
|
|
|
// vc-request-with-auth
|
|
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
|
|
|
|
// Bob should not be verified for Alice.
|
|
let contact_bob = alice.add_or_lookup_contact_no_key(bob).await;
|
|
assert_eq!(contact_bob.is_verified(alice).await.unwrap(), false);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that attempting to join already joined group does nothing.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_rejoin_group() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
|
|
let alice_chat_id = chat::create_group(alice, "the chat").await?;
|
|
|
|
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
|
|
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
|
|
|
// Bob gets two progress events.
|
|
for expected_progress in [400, 1000] {
|
|
let EventType::SecurejoinJoinerProgress { progress, .. } = bob
|
|
.evtracker
|
|
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
|
.await
|
|
else {
|
|
unreachable!();
|
|
};
|
|
assert_eq!(progress, expected_progress);
|
|
}
|
|
|
|
// Bob scans the QR code again.
|
|
join_securejoin(bob, &qr).await?;
|
|
|
|
// Bob immediately receives progress 1000 event.
|
|
let EventType::SecurejoinJoinerProgress { progress, .. } = bob
|
|
.evtracker
|
|
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
|
.await
|
|
else {
|
|
unreachable!();
|
|
};
|
|
assert_eq!(progress, 1000);
|
|
|
|
// Bob does not send any more messages by scanning the QR code.
|
|
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// To make invite links a little bit nicer, we put the readable part at the end and encode spaces by `+` instead of `%20`.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_get_securejoin_qr_name_is_last() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
alice
|
|
.set_config(Config::Displayname, Some("Alice Axe"))
|
|
.await?;
|
|
let qr = get_securejoin_qr(alice, None).await?;
|
|
assert!(qr.ends_with("Alice+Axe"));
|
|
|
|
let alice_chat_id = chat::create_group(alice, "The Chat").await?;
|
|
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
|
|
assert!(qr.ends_with("The+Chat"));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// QR codes should not get arbitrary big because of long names.
|
|
/// The truncation, however, should not let the url end with a `.`, which is a call for trouble in linkfiers.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_get_securejoin_qr_name_is_truncated() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
alice
|
|
.set_config(
|
|
Config::Displayname,
|
|
Some("Alice Axe Has A Very Long Family Name Really Indeed"),
|
|
)
|
|
.await?;
|
|
let qr = get_securejoin_qr(alice, None).await?;
|
|
assert!(qr.ends_with("Alice+Axe+Has+A+Very+Lon_"));
|
|
assert!(!qr.ends_with("."));
|
|
|
|
let alice_chat_id =
|
|
chat::create_group(alice, "The Chat With One Of The Longest Titles Around").await?;
|
|
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
|
|
assert!(qr.ends_with("The+Chat+With+One+Of+The_"));
|
|
assert!(!qr.ends_with("."));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Regression test for the bug
|
|
/// that resulted in an info message
|
|
/// about Bob addition to the group on Fiona's device.
|
|
///
|
|
/// There should be no info messages about implicit
|
|
/// member changes when we are added to the group.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_two_group_securejoins() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
let fiona = &tcm.fiona().await;
|
|
|
|
let group_id = chat::create_group(alice, "Group").await?;
|
|
|
|
let qr = get_securejoin_qr(alice, Some(group_id)).await?;
|
|
|
|
// Bob joins using QR code.
|
|
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
|
|
|
// Fiona joins using QR code.
|
|
tcm.exec_securejoin_qr(fiona, alice, &qr).await;
|
|
|
|
let fiona_chat_id = fiona.get_last_msg().await.chat_id;
|
|
fiona
|
|
.golden_test_chat(fiona_chat_id, "two_group_securejoins")
|
|
.await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that scanning an outdated QR code does not add the removed inviter back to the group.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_qr_no_implicit_inviter_addition() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
let charlie = &tcm.charlie().await;
|
|
|
|
// Alice creates a group with Bob.
|
|
let alice_chat_id = alice
|
|
.create_group_with_members("Group with Bob", &[bob])
|
|
.await;
|
|
let alice_qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
|
|
|
|
// Bob joins the group via QR code.
|
|
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &alice_qr).await;
|
|
|
|
// Bob creates a QR code for joining the group.
|
|
let bob_qr = get_securejoin_qr(bob, Some(bob_chat_id)).await?;
|
|
|
|
// Alice removes Bob from the group.
|
|
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
|
remove_contact_from_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
|
|
|
// Deliver the removal message to Bob.
|
|
let removal_msg = alice.pop_sent_msg().await;
|
|
bob.recv_msg(&removal_msg).await;
|
|
|
|
// Charlie scans Bob's outdated QR code.
|
|
let charlie_chat_id = join_securejoin(charlie, &bob_qr).await?;
|
|
|
|
// Charlie sends vg-request to Bob.
|
|
let sent = charlie.pop_sent_msg().await;
|
|
bob.recv_msg_trash(&sent).await;
|
|
|
|
// Bob sends vg-auth-required to Charlie.
|
|
let sent = bob.pop_sent_msg().await;
|
|
charlie.recv_msg_trash(&sent).await;
|
|
|
|
// Bob receives vg-request-with-auth, but cannot add Charlie
|
|
// because Bob himself is not in the group.
|
|
let sent = charlie.pop_sent_msg().await;
|
|
bob.recv_msg_trash(&sent).await;
|
|
|
|
// Charlie still has no contacts in the list.
|
|
let charlie_chat_contacts = chat::get_chat_contacts(charlie, charlie_chat_id).await?;
|
|
assert_eq!(charlie_chat_contacts.len(), 0);
|
|
|
|
// Alice adds Charlie to the group
|
|
let alice_charlie_contact_id = alice.add_or_lookup_contact_id(charlie).await;
|
|
add_contact_to_chat(alice, alice_chat_id, alice_charlie_contact_id).await?;
|
|
|
|
let sent = alice.pop_sent_msg().await;
|
|
assert_eq!(charlie.recv_msg(&sent).await.chat_id, charlie_chat_id);
|
|
|
|
// Charlie has two contacts in the list: Alice and self.
|
|
let charlie_chat_contacts = chat::get_chat_contacts(charlie, charlie_chat_id).await?;
|
|
assert_eq!(charlie_chat_contacts.len(), 2);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_user_deletes_chat_before_securejoin_completes() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
|
|
let qr = get_securejoin_qr(alice, None).await?;
|
|
let bob_chat_id = join_securejoin(bob, &qr).await?;
|
|
|
|
let bob_alice_chat = bob.get_chat(alice).await;
|
|
// It's not possible yet to send to the chat, because Bob doesn't have Alice's key:
|
|
assert_eq!(bob_alice_chat.can_send(bob).await?, false);
|
|
assert_eq!(bob_alice_chat.id, bob_chat_id);
|
|
|
|
let request = bob.pop_sent_msg().await;
|
|
|
|
bob_chat_id.delete(bob).await?;
|
|
|
|
alice.recv_msg_trash(&request).await;
|
|
let auth_required = alice.pop_sent_msg().await;
|
|
|
|
bob.recv_msg_trash(&auth_required).await;
|
|
|
|
// The chat with Alice should be recreated,
|
|
// and it should be sendable now:
|
|
assert!(bob.get_chat(alice).await.can_send(bob).await?);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that vc-request is processed even if the server
|
|
/// encrypts incoming unencrypted messages with OpenPGP.
|
|
///
|
|
/// For an example of software that encrypts incoming messages
|
|
/// with OpenPGP, see <https://lacre.io/>.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_vc_request_encrypted_at_rest() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
|
|
let qr = get_securejoin_qr(alice, None).await?;
|
|
|
|
let Qr::AskVerifyContact { invitenumber, .. } = check_qr(bob, &qr).await? else {
|
|
panic!("Failed to parse QR code");
|
|
};
|
|
|
|
// Encrypted payload is this message encrypted to Alice's key:
|
|
/*
|
|
Content-Type: multipart/mixed;
|
|
boundary="1888168c99b6020b_ed9b3571859a92d7_eeabe50050fc923b"
|
|
|
|
--1888168c99b6020b_ed9b3571859a92d7_eeabe50050fc923b
|
|
Content-Type: text/plain; charset="utf-8"
|
|
Message-ID: <4f108a03-6c03-4f41-a6fb-fcd05b94c21f@localhost>
|
|
Content-Transfer-Encoding: 7bit
|
|
|
|
Secure-Join: vc-request
|
|
--1888168c99b6020b_ed9b3571859a92d7_eeabe50050fc923b--
|
|
*/
|
|
|
|
let payload = format!("To: <alice@example.org>
|
|
Subject: [...]
|
|
Date: Tue, 6 Jan 2026 08:20:47 +0000
|
|
References: <4f108a03-6c03-4f41-a6fb-fcd05b94c21f@localhost>
|
|
Chat-Version: 1.0
|
|
Autocrypt: addr=bob@example.net; prefer-encrypt=mutual; keydata=xsBNBF4wx1cBCADOwLS/xCd8iKDWUsyTfVzWby+ZGKPpamPTvdj0GFgnf0B1EBaA5//PjA
|
|
zbK5iKio6QNEmZagzJPkXPByJcAIRUm0T16tqDtCvxm+H93YEXpHi/XWOeJw9kohATSqUtsRO0pFJe
|
|
DvPiMTmQrEmHYoWDSQBfCrowZdvnMAlbJ9JjYOngcMeTxc0jxmPs5s17yFC+1OWu4fwWCyUM3wy1Jz
|
|
dKTcDWryrSkvmgFdUqJ7pJDk1HFTt+x9tvQlK3un9BXiRwv0u0zDSuI8eDH/dRLA4UL9Pq6vmJmBam
|
|
e1BPsE1PA7VzeTSJR2ooJXMT6o2AmH8PPUfRkv3OiWuh7LM5FSpHABEBAAHNETxib2JAZXhhbXBsZS
|
|
5uZXQ+wsCOBBMBCAA4BQJpXW0wFiEEzMtaqfbhFByUMWXx2xixjLz3BIcCGwMCHgAECwkIBwYVCAkK
|
|
CwIDFgIBAScCGQEACgkQ2xixjLz3BIcexwgAsOauz2xZ6QhVBQNX3Hg8OoYTn60w6b4XUOpI4UtqAT
|
|
bAmPr5VPhCUa5vzI19QCSekLZIVxUSl6C+7YF/mU8yJldKPgq1Kpr8tbnISNbUDpvuGz7NQdK0TEwx
|
|
RkPDw+ilqY7v2Rhhg0Sogi23Yrbyqvs72oorDjia6dBA7VLixpW1O9h/MfQ6WiXJcHSavoLqk2hFW1
|
|
y/AsVmArGiuT+kzRJWhTzojZLjQLHTKQii/AOQ3MhkJycyayI/igSEDWWwCcuqL8SF+PUJWGyU5PxU
|
|
2hTHDBsVItLLsqseM3WrwJRkBBdsdsR8+moHVyXJV9Gfl/4UFQCgltdEwWISgNTbAc7ATQReMMdXAQ
|
|
gAogeBLbIjaeJII3W2pxsu+SEusQkJVykbGYDtqyXV+XBZisY4GE0kTawK5mqSh+rDqquCxDgYWBRT
|
|
nGZwEKohnj2NG75pjfyVPhYMUdJt7+Ya1oiFvZlgrrOj1btjevq53yFtQolMN+X2oS8mlf9jSzIyPC
|
|
eDxJk1N1gxaAATg3ByAyB1Td0wDdFPp48ni8qzfyGZeYicvJlx74YnOaja2lnI/y+I9LsmmqiOgI8H
|
|
cbmH1od5qSnVjhcpBoTEA15YLIEkSE3C00Q5USlDS3EVg/IOu3FXnLl7v0hQ/jXyv88eycfpSfFcbM
|
|
Hot9VtJ4TIPIoSX7DQ+uU2SXJKiZNkVQARAQABwsB2BBgBCAAgBQJpXW0wAhsMFiEEzMtaqfbhFByU
|
|
MWXx2xixjLz3BIcACgkQ2xixjLz3BIcxvwf9G33yLtvekpNGeRbWkjRXSa083k/4CZpkF0Fb5led1O
|
|
hjRjw9KQdZj68qn1vVrDxygnYdHwZWy8WbwEkK+l3gyJ2v2D/Wsj+sHjQ1YMtPnx+EsRFWjKsolYfN
|
|
gJZ+IfMTasip8uJNxN5LIvI/svn43mzgeLUudddekmr/2eekhXfYz5covje3yf+u4rYDWQ78TLpJL5
|
|
bxTwiiUyV1ZIO1G2F4RNjv8wZHsGD3DasQck+OCV7jQNuyojcmq6J22lXN0clisI51mHzwkLAnI+Yh
|
|
jRj3nqwVSCxsmgfejap53JGHEL0CEy6KvepLqjFf8BiK4kwRBmI7/YWOyY2xBWdTew==
|
|
Secure-Join: vc-request
|
|
Secure-Join-Invitenumber: {invitenumber}
|
|
MIME-Version: 1.0
|
|
From: <bob@example.net>
|
|
Message-ID: <4f108a03-6c03-4f41-a6fb-fcd05b94c21f@localhost>
|
|
Content-Type: multipart/encrypted;
|
|
protocol=\"application/pgp-encrypted\";
|
|
boundary=\"1767687657/562150/131174/example.org\"
|
|
|
|
This is a MIME-encapsulated message.
|
|
|
|
--1767687657/562150/131174/example.org
|
|
Content-Type: application/pgp-encrypted
|
|
Content-Disposition: attachment
|
|
|
|
Version: 1
|
|
|
|
--1767687657/562150/131174/example.org
|
|
Content-Type: application/octet-stream
|
|
Content-Disposition: inline; filename=\"msg.asc\"
|
|
|
|
-----BEGIN PGP MESSAGE-----
|
|
|
|
wV4D5tq63hTeebASAQdAGKP3GqsznPOYBXOo8FYf5x8r0ZaJZUGkXIR+Oi32H1Mw
|
|
LdVEu0jcXmZ1fGZ7LLPtvfwllB2W9Bhiy/lCZZs+c98VYzSWaT0DHmp23WvTsBZn
|
|
0sDeAS0n2I6xPwYhUjqL8G1a9xdyvZRgHYUXhiARrXzx7G+aX8/KFjk6e2OZxiVy
|
|
PSHKh61T9wPYzPMlToPsI0GjXAzOgI9I3XQ8xPqihiUpnU6BSg+Tj26m9ZqdJe8v
|
|
aVAHxAu7aqz7eljp2H336Idgfvf1lXdA6gMA8HhHI729Ws/Mc8lcgefM9kzZWEIN
|
|
FOBYmf4JCdTghESpEJ5i+pF28ChqZ3+8lGzNmT66c2SQRTclwxFa4SS19D4mxkHq
|
|
iOMqj7LZ8R6IhnNVRD1V4mVapc7vFVZy8ViyZlmhJF0TBR/TEXlit6vtSkDKFiR9
|
|
Tb5zuFQVAwhun3i1tG3/Ii1ReflZNnteVlL1J1XnptOJ70vVUeO3qRqkbkiH8Mdm
|
|
Asvy24WKkz9jmEgdt0Hj8VUHtcOL5xmtsr0ZUCLUriylpzDh8E+0LTl2/DOgH+mI
|
|
D/gFLCzFHjI0jfXyLTGCYOPYAnvqJukLBT/X7JbCAI/62aq0K28Lp6beJhFs+bIz
|
|
gU6dGXsFMe/RpRHrIAkMAaM5xkxMDRuRJDxiUdS/X+Y8
|
|
=QrdS
|
|
-----END PGP MESSAGE-----
|
|
|
|
--1767687657/562150/131174/example.org--
|
|
");
|
|
|
|
// Alice receives vc-request, but it is encrypted
|
|
// by the server.
|
|
let received = receive_imf(alice, payload.as_bytes(), false)
|
|
.await?
|
|
.unwrap();
|
|
assert_eq!(received.chat_id, DC_CHAT_ID_TRASH);
|
|
|
|
// Test that Alice sends vc-auth-required after processing vc-request.
|
|
let sent = alice.pop_sent_msg().await;
|
|
let msg = bob.parse_msg(&sent).await;
|
|
assert_eq!(
|
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
"vc-auth-required"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_auth_token_is_synchronized() -> Result<()> {
|
|
let mut tcm = TestContextManager::new();
|
|
let alice1 = &tcm.alice().await;
|
|
let alice2 = &tcm.alice().await;
|
|
let bob = &tcm.bob().await;
|
|
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
|
|
|
alice1.set_config_bool(Config::SyncMsgs, true).await?;
|
|
alice2.set_config_bool(Config::SyncMsgs, true).await?;
|
|
|
|
// This creates first auth token:
|
|
let qr1 = get_securejoin_qr(alice1, None).await?;
|
|
|
|
// This creates another auth token; both of them need to be synchronized
|
|
let qr2 = get_securejoin_qr(alice1, None).await?;
|
|
sync(alice1, alice2).await;
|
|
|
|
// Note that Bob will throw away the AUTH token after sending `vc-request-with-auth`.
|
|
// Therefore, he will fail to decrypt the answer from Alice's second device,
|
|
// which leads to a "decryption failed: missing key" message in the logs.
|
|
// This is fine.
|
|
tcm.exec_securejoin_qr_multi_device(bob, &[alice1, alice2], &qr2)
|
|
.await;
|
|
|
|
let contacts = Contact::get_all(alice2, 0, Some("Bob")).await?;
|
|
assert_eq!(contacts[0], alice2.add_or_lookup_contact_id(bob).await);
|
|
assert_eq!(contacts.len(), 1);
|
|
|
|
let chatlist = Chatlist::try_load(alice2, 0, Some("Bob"), None).await?;
|
|
assert_eq!(chatlist.get_chat_id(0)?, alice2.get_chat(bob).await.id);
|
|
assert_eq!(chatlist.len(), 1);
|
|
|
|
for qr in [qr1, qr2] {
|
|
let qr = check_qr(bob, &qr).await?;
|
|
let qr = QrInvite::try_from(qr)?;
|
|
assert!(token::exists(alice2, Namespace::InviteNumber, qr.invitenumber()).await?);
|
|
assert!(token::exists(alice2, Namespace::Auth, qr.authcode()).await?);
|
|
}
|
|
|
|
// Check that alice2 only saves the invite number once:
|
|
let invite_count: u32 = alice2
|
|
.sql
|
|
.query_get_value(
|
|
"SELECT COUNT(*) FROM tokens WHERE namespc=?;",
|
|
(Namespace::InviteNumber,),
|
|
)
|
|
.await?
|
|
.unwrap();
|
|
assert_eq!(invite_count, 1);
|
|
|
|
// ...but knows two AUTH tokens:
|
|
let auth_count: u32 = alice2
|
|
.sql
|
|
.query_get_value(
|
|
"SELECT COUNT(*) FROM tokens WHERE namespc=?;",
|
|
(Namespace::Auth,),
|
|
)
|
|
.await?
|
|
.unwrap();
|
|
assert_eq!(auth_count, 2);
|
|
|
|
Ok(())
|
|
}
|