diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 200eb895d4..f596d147f0 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -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 diff --git a/src/securejoin.rs b/src/securejoin.rs index 117c963bae..068bf0fa1e 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -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)?; @@ -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) @@ -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, diff --git a/src/test_utils.rs b/src/test_utils.rs index 28c4082c9f..098c294e00 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -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. /// 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 @@ -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(); @@ -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) { + 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; } diff --git a/test-data/golden/test_sync_broadcast_alice1 b/test-data/golden/test_sync_broadcast_alice1 index 7d450fb717..23c767db56 100644 --- a/test-data/golden/test_sync_broadcast_alice1 +++ b/test-data/golden/test_sync_broadcast_alice1 @@ -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] √ -------------------------------------------------------------------------------- diff --git a/test-data/golden/test_sync_broadcast_alice2 b/test-data/golden/test_sync_broadcast_alice2 index 6672cd38df..277f0f8e6e 100644 --- a/test-data/golden/test_sync_broadcast_alice2 +++ b/test-data/golden/test_sync_broadcast_alice2 @@ -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] √ --------------------------------------------------------------------------------