Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/chat/chat_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5099,6 +5099,80 @@ async fn test_broadcast_contacts_are_hidden() -> Result<()> {
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_blocked_bob_cant_join_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice1 = &tcm.alice().await;
let alice2 = &tcm.alice().await;
let bob = &tcm.bob().await;

for a in [alice1, alice2] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
// The observing device has Bob blocked from the early start.
let alice2_bob_id = alice2.add_or_lookup_contact_id(bob).await;
Contact::block(alice2, alice2_bob_id).await?;

let alice1_chat_id = create_group(alice1, "").await?;
sync(alice1, alice2).await;
let alice1_chat = Chat::load_from_db(alice1, alice1_chat_id).await?;
let (alice2_chat_id, _blocked) = get_chat_id_by_grpid(alice2, &alice1_chat.grpid)
.await?
.unwrap();
let qr = get_securejoin_qr(alice1, Some(alice1_chat_id)).await?;
sync(alice1, alice2).await;

tcm.exec_securejoin_qr_multi_device(bob, &[alice1, alice2], &qr)
.await;
let alice1_bob_id = alice1.add_or_lookup_contact_id(bob).await;
assert_eq!(get_chat_contacts(alice1, alice1_chat_id).await?.len(), 2);
// "vg-member-added" from alice1 adds bob for alice2 to provide membership consistency on
// devices.
assert_eq!(get_chat_contacts(alice2, alice2_chat_id).await?.len(), 2);
remove_contact_from_chat(alice1, alice1_chat_id, alice1_bob_id).await?;
bob.recv_msg(&alice1.pop_sent_msg().await).await;
tcm.exec_securejoin_qr(bob, alice1, &qr).await;
// Bob can join again if he isn't blocked.
assert_eq!(get_chat_contacts(alice1, alice1_chat_id).await?.len(), 2);
Contact::block(alice1, alice1_bob_id).await?;
remove_contact_from_chat(alice1, alice1_chat_id, alice1_bob_id).await?;
bob.recv_msg(&alice1.pop_sent_msg().await).await;
tcm.exec_securejoin_qr(bob, alice1, &qr).await;
let members = get_chat_contacts(alice1, alice1_chat_id).await?;
assert_eq!(members.len(), 1);
assert!(members.contains(&ContactId::SELF));
let past_members = get_past_chat_contacts(alice1, alice1_chat_id).await?;
assert_eq!(past_members.len(), 1);
assert!(past_members.contains(&alice1_bob_id));
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_blocked_bob_cant_create_11_chat_via_securejoin() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice1 = &tcm.alice().await;
let alice2 = &tcm.alice().await;
let bob = &tcm.bob().await;

for a in [alice1, alice2] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
// The observing device has Bob blocked.
let alice2_bob_id = alice2.add_or_lookup_contact_id(bob).await;
Contact::block(alice2, alice2_bob_id).await?;

let qr = get_securejoin_qr(alice1, None).await?;
sync(alice1, alice2).await;

let chat_cnt = get_chat_cnt(alice1).await?;
assert_eq!(get_chat_cnt(alice2).await?, chat_cnt);
tcm.exec_securejoin_qr_multi_device(bob, &[alice1, alice2], &qr)
.await;
assert_eq!(get_chat_cnt(alice1).await?, chat_cnt + 1);
assert_eq!(get_chat_cnt(alice2).await?, chat_cnt);
Ok(())
}

/// Tests sending JPEG image with .png extension.
///
/// This is a regression test, previously sending failed
Expand Down
22 changes: 22 additions & 0 deletions src/securejoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,18 @@ pub(crate) async fn handle_securejoin_handshake(
warn!(context, "Secure-join denied (bad auth).");
return Ok(HandshakeMessage::Ignore);
}
if Contact::lookup_id_by_addr_ex(
context,
&mime_message.from.addr,
Origin::Unknown,
Some(Blocked::Yes),
)
.await?
.is_some()
{
warn!(context, "Ignoring {step} message: {contact_id} is blocked.");
return Ok(HandshakeMessage::Ignore);
}

let rfc724_mid = create_outgoing_rfc724_mid();
let addr = ContactAddress::new(&mime_message.from.addr)?;
Expand Down Expand Up @@ -640,6 +652,10 @@ pub(crate) async fn handle_securejoin_handshake(
if time() < timestamp + VERIFICATION_TIMEOUT_SECONDS {
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
}
if sender_contact.blocked {
warn!(context, "Ignoring {step} message: {contact_id} is blocked.");
return Ok(HandshakeMessage::Ignore);
}
contact_id.regossip_keys(context).await?;
// for setup-contact, make Alice's one-to-one chat with Bob visible
// (secure-join-information are shown in the group chat)
Expand Down Expand Up @@ -811,6 +827,12 @@ pub(crate) async fn observe_securejoin_on_other_device(
}

mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
if contact.blocked && step != SecureJoinStep::MemberAdded {
// Contact might be blocked after another device had issued the message. Still, to avoid
// membership inconsistency on devices, don't ignore "vg-member-added".
warn!(context, "Observing {step}: {contact_id} is blocked.");
return Ok(HandshakeMessage::Ignore);
}

if matches!(
step,
Expand Down
11 changes: 11 additions & 0 deletions src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ impl TestContextManager {

/// Executes SecureJoin initiated by `joiner`
/// scanning `qr` generated by one of the `inviters` devices.
/// `inviters` devices must have the same primary address.
Comment thread
link2xt marked this conversation as resolved.
/// All of the `inviters` devices will get the messages and send replies.
///
/// The [`ChatId`] of the created chat is returned, for a SetupContact QR this is the 1:1
Expand All @@ -283,8 +284,10 @@ impl TestContextManager {
qr: &str,
) -> ChatId {
assert!(joiner.pop_sent_msg_opt(Duration::ZERO).await.is_none());
let inviter_addr = inviters[0].get_primary_self_addr().await.unwrap();
for inviter in inviters {
assert!(inviter.pop_sent_msg_opt(Duration::ZERO).await.is_none());
assert_eq!(inviter.get_primary_self_addr().await.unwrap(), inviter_addr);
}

let chat_id = join_securejoin(&joiner.ctx, qr).await.unwrap();
Expand All @@ -300,6 +303,14 @@ impl TestContextManager {
}
for inviter in inviters {
if let Some(sent) = inviter.pop_sent_msg_ex(rev_order, Duration::ZERO).await {
if sent.recipients.split(' ').any(|addr| addr == inviter_addr) {

@iequidoo iequidoo Jun 12, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, without this condition test_blocked_bob_cant_create_11_chat_via_securejoin doesn't work:

========== Chats of alice2: ==========
Single#Chat#1001: Saved messages [KEY alice@example.org] Icon: 969142cb84015bc135767bc2370934a.png
--------------------------------------------------------------------------------
Msg#1003🔒: Me (Contact#Contact#Self): Secure-Join – Secure-Join  √
--------------------------------------------------------------------------------

The self-chat with this weird message is created from a vc-pubkey message which isn't self-sent normally. Maybe should be fixed because if the server copies sent messages to INBOX for some reason, this will happen. But not in this PR.

for observer in inviters {
// `imap::prefetch_should_download()` returns false on the sender side.
if observer.get_id() != inviter.get_id() {
observer.recv_msg_opt(&sent).await;
}
}
}
joiner.recv_msg_opt(&sent).await;
something_sent = true;
}
Expand Down
4 changes: 2 additions & 2 deletions test-data/golden/test_sync_broadcast_alice1
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ OutBroadcast#Chat#1001: Channel [0 member(s)]
--------------------------------------------------------------------------------
Msg#1001: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#1006🔒: Me (Contact#Contact#Self): Member bob@example.net added. [INFO] √
Msg#1008🔒: Me (Contact#Contact#Self): hi √
Msg#1009🔒: Me (Contact#Contact#Self): You removed member bob@example.net. [INFO] √
Msg#1009🔒: Me (Contact#Contact#Self): hi √
Msg#1010🔒: Me (Contact#Contact#Self): You removed member bob@example.net. [INFO] √
--------------------------------------------------------------------------------
4 changes: 2 additions & 2 deletions test-data/golden/test_sync_broadcast_alice2
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ OutBroadcast#Chat#1001: Channel [0 member(s)]
--------------------------------------------------------------------------------
Msg#1002: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#1006🔒: Me (Contact#Contact#Self): Member bob@example.net added. [INFO] √
Msg#1008🔒: Me (Contact#Contact#Self): hi √
Msg#1009🔒: Me (Contact#Contact#Self): You removed member bob@example.net. [INFO] √
Msg#1009🔒: Me (Contact#Contact#Self): hi √
Msg#1010🔒: Me (Contact#Contact#Self): You removed member bob@example.net. [INFO] √
--------------------------------------------------------------------------------
Loading