-
-
Notifications
You must be signed in to change notification settings - Fork 119
feat!: QR codes and symmetric encryption for broadcast channels #7268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
9694ba3
10091a7
62d7441
a434e6e
7f3d0cc
17fdac2
8b3b597
915bad9
d755f61
e001067
c602586
7d92fcd
faa149a
5a91887
41b4953
6114a6a
27d1d35
89f1e52
7fcbf39
e00db35
a3a932a
7bd1ced
07a6062
e6bb254
24cbcbe
691887f
e74f312
fcc3363
f336add
24d5335
a836775
8a4044e
1d671d2
cafc2c7
efc323f
ab1844d
9776cd7
745d38c
1029707
205ca0f
4d615d6
9f87a53
c101f2a
93b8763
ea82fe9
0e0c534
9e9ed67
501b5a7
9e0ff87
717d6bb
0a69a55
7fdee26
1a85ad7
05cacc0
9048977
bd12139
c3be22f
bc116bd
d9e38c6
c71f698
dd3cffd
7be7ba0
1d3ae6d
327e0cc
af20e3e
ca986cc
644d6e5
1bbd3ec
3723cd1
c1b53b1
c0aa31c
2409431
28d1ca9
36a8f20
8b300b8
788765f
47b8a2d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2627,6 +2627,121 @@ async fn test_can_send_group() -> Result<()> { | |
| Ok(()) | ||
| } | ||
|
|
||
| /// Tests that in a broadcast channel, | ||
| /// the recipients can't see the identity of their fellow recipients. | ||
| #[tokio::test(flavor = "multi_thread", worker_threads = 2)] | ||
| async fn test_broadcast_members_cant_see_each_other() -> Result<()> { | ||
| fn contains(parsed: &MimeMessage, s: &str) -> bool { | ||
iequidoo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| assert_eq!(parsed.decrypting_failed, false); | ||
| let decoded_str = str::from_utf8(&parsed.decoded_data).unwrap(); | ||
| decoded_str.contains(s) | ||
| } | ||
|
|
||
| let mut tcm = TestContextManager::new(); | ||
| let alice = &tcm.alice().await; | ||
| let bob = &tcm.bob().await; | ||
| let charlie = &tcm.charlie().await; | ||
|
|
||
| tcm.section("Alice creates a channel, Bob joins."); | ||
| let alice_broadcast_id = create_broadcast(alice, "Channel".to_string()).await?; | ||
| let qr = get_securejoin_qr(alice, Some(alice_broadcast_id)) | ||
| .await | ||
| .unwrap(); | ||
| tcm.exec_securejoin_qr(bob, alice, &qr).await; | ||
|
|
||
| tcm.section("Charlie scans the QR code and sends request."); | ||
| { | ||
| join_securejoin(charlie, &qr).await.unwrap(); | ||
|
|
||
| let request = charlie.pop_sent_msg().await; | ||
| assert_eq!(request.recipients, "[email protected] [email protected]"); | ||
|
|
||
| alice.recv_msg_trash(&request).await; | ||
| } | ||
|
|
||
| tcm.section("Alice sends auth-required"); | ||
| { | ||
| let auth_required = alice.pop_sent_msg().await; | ||
| assert_eq!( | ||
| auth_required.recipients, | ||
| "[email protected] [email protected]" | ||
| ); | ||
| let parsed = charlie.parse_msg(&auth_required).await; | ||
| assert!(parsed.header_exists(HeaderDef::AutocryptGossip)); | ||
Hocuri marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| assert!(contains(&parsed, "[email protected]")); | ||
| assert_eq!(contains(&parsed, "[email protected]"), false); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. None of these is great, and I do miss pytest in these moments, but sure, I can replace it
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, no, clippy will complain: ...and signifying just by a
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since in most other tests, we do the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This means that later clippy will maybe complain about
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can deal with this problem if and when it arrives. |
||
|
|
||
| let parsed_by_bob = bob.parse_msg(&auth_required).await; | ||
| assert!(parsed_by_bob.decrypting_failed); | ||
|
|
||
| charlie.recv_msg_trash(&auth_required).await; | ||
| } | ||
|
|
||
| tcm.section("Charlie sends request-with-auth"); | ||
| { | ||
| let request_with_auth = charlie.pop_sent_msg().await; | ||
| assert_eq!( | ||
| request_with_auth.recipients, | ||
| "[email protected] [email protected]" | ||
| ); | ||
|
|
||
| alice.recv_msg_trash(&request_with_auth).await; | ||
| } | ||
|
|
||
| tcm.section("Alice adds member"); | ||
| { | ||
| let member_added = alice.pop_sent_msg().await; | ||
| assert_eq!( | ||
| member_added.recipients, | ||
| "[email protected] [email protected]" | ||
| ); | ||
| let parsed = charlie.parse_msg(&member_added).await; | ||
| assert!(parsed.header_exists(HeaderDef::AutocryptGossip)); | ||
| assert!(contains(&parsed, "[email protected]")); | ||
| assert_eq!(contains(&parsed, "[email protected]"), false); | ||
|
|
||
| let parsed_by_bob = bob.parse_msg(&member_added).await; | ||
| assert!(parsed_by_bob.decrypting_failed); | ||
|
|
||
| let rcvd = charlie.recv_msg(&member_added).await; | ||
| assert_eq!(rcvd.param.get_cmd(), SystemMessage::MemberAddedToGroup); | ||
| } | ||
|
|
||
| tcm.section("Alice sends into the channel."); | ||
| { | ||
| let hi_msg = alice.send_text(alice_broadcast_id, "hi").await; | ||
| let parsed = charlie.parse_msg(&hi_msg).await; | ||
| assert_eq!(parsed.header_exists(HeaderDef::AutocryptGossip), false); | ||
| assert_eq!(contains(&parsed, "[email protected]"), false); | ||
| assert_eq!(contains(&parsed, "[email protected]"), false); | ||
|
|
||
| let parsed_by_bob = bob.parse_msg(&hi_msg).await; | ||
| assert_eq!(parsed_by_bob.decrypting_failed, false); | ||
| } | ||
|
|
||
| tcm.section("Alice removes Charlie. Bob must not see it."); | ||
| { | ||
| let alice_charlie_contact = alice.add_or_lookup_contact_id(charlie).await; | ||
| remove_contact_from_chat(alice, alice_broadcast_id, alice_charlie_contact).await?; | ||
| let member_removed = alice.pop_sent_msg().await; | ||
| assert_eq!( | ||
| member_removed.recipients, | ||
| "[email protected] [email protected]" | ||
| ); | ||
| let parsed = charlie.parse_msg(&member_removed).await; | ||
| assert!(contains(&parsed, "[email protected]")); | ||
| assert_eq!(contains(&parsed, "[email protected]"), false); | ||
|
|
||
| let parsed_by_bob = bob.parse_msg(&member_removed).await; | ||
| assert!(parsed_by_bob.decrypting_failed); | ||
|
|
||
| let rcvd = charlie.recv_msg(&member_removed).await; | ||
| assert_eq!(rcvd.param.get_cmd(), SystemMessage::MemberRemovedFromGroup); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| #[tokio::test(flavor = "multi_thread", worker_threads = 2)] | ||
| async fn test_broadcast_change_name() -> Result<()> { | ||
| let mut tcm = TestContextManager::new(); | ||
|
|
@@ -3174,10 +3289,7 @@ async fn test_leave_broadcast_multidevice() -> Result<()> { | |
|
|
||
| alice.recv_msg_trash(&request_with_auth).await; | ||
| let member_added = alice.pop_sent_msg().await; | ||
| assert_eq!( | ||
| request_with_auth.recipients, | ||
| "[email protected] [email protected]" | ||
| ); | ||
| assert_eq!(member_added.recipients, "[email protected] [email protected]"); | ||
|
|
||
| tcm.section("Bob receives the member-added message answer, and processes it"); | ||
| let rcvd = bob0.recv_msg(&member_added).await; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ | |
| use std::collections::{BTreeSet, HashSet}; | ||
| use std::io::Cursor; | ||
|
|
||
| use anyhow::{Context as _, Result, bail}; | ||
| use anyhow::{Context as _, Result, bail, format_err}; | ||
| use base64::Engine as _; | ||
| use data_encoding::BASE32_NOPAD; | ||
| use deltachat_contact_tools::sanitize_bidi_characters; | ||
|
|
@@ -290,6 +290,14 @@ impl MimeFactory { | |
| for row in rows { | ||
| let (authname, addr, fingerprint, id, add_timestamp, remove_timestamp, public_key_bytes_opt) = row?; | ||
|
|
||
| // In a broadcast channel, only send member-added/removed messages | ||
| // to the affected member: | ||
| if let Some(fp) = should_only_send_to_one_recipient(&msg, &chat){ | ||
Hocuri marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if fp? != fingerprint { | ||
| continue; | ||
| } | ||
| } | ||
|
|
||
| let public_key_opt = if let Some(public_key_bytes) = &public_key_bytes_opt { | ||
| Some(SignedPublicKey::from_slice(public_key_bytes)?) | ||
| } else { | ||
|
|
@@ -415,20 +423,6 @@ impl MimeFactory { | |
| req_mdn = true; | ||
| } | ||
|
|
||
| // If undisclosed_recipients, and this is a member-added/removed message, | ||
| // only send to the added/removed member | ||
| if undisclosed_recipients | ||
| && matches!( | ||
| msg.param.get_cmd(), | ||
| SystemMessage::MemberRemovedFromGroup | SystemMessage::MemberAddedToGroup | ||
| ) | ||
| { | ||
| let Some(member) = msg.param.get(Param::Arg) else { | ||
| bail!("Missing removed/added member"); | ||
| }; | ||
| recipients.retain(|addr| addr == member); | ||
| } | ||
|
|
||
| encryption_pubkeys = if !is_encrypted { | ||
| None | ||
| } else if should_encrypt_symmetrically(&msg, &chat) { | ||
|
|
@@ -1975,12 +1969,7 @@ fn hidden_recipients() -> Address<'static> { | |
| } | ||
|
|
||
| fn should_encrypt_with_broadcast_secret(msg: &Message, chat: &Chat) -> bool { | ||
| chat.typ == Chattype::OutBroadcast | ||
| // We encrypt securejoin messages asymmetrically | ||
| && msg.param.get_cmd() != SystemMessage::SecurejoinMessage | ||
| // The member-added message in a broadcast must be asymmetrically encrypted, | ||
| // because the newly-added member doesn't know the broadcast shared secret yet: | ||
| && msg.param.get_cmd() != SystemMessage::MemberAddedToGroup | ||
| chat.typ == Chattype::OutBroadcast && should_only_send_to_one_recipient(msg, chat).is_none() | ||
| } | ||
|
|
||
| fn should_hide_recipients(msg: &Message, chat: &Chat) -> bool { | ||
|
|
@@ -1991,6 +1980,25 @@ fn should_encrypt_symmetrically(msg: &Message, chat: &Chat) -> bool { | |
| should_encrypt_with_broadcast_secret(msg, chat) | ||
| } | ||
|
|
||
| /// Some messages sent into outgoing broadcast channels (member-added/member-removed) | ||
| /// should only go to a single recipient, | ||
| /// rather than all recipients. | ||
| /// This function returns the fingerprint of the recipient the message should be sent to. | ||
| fn should_only_send_to_one_recipient<'a>(msg: &'a Message, chat: &Chat) -> Option<Result<&'a str>> { | ||
|
||
| if chat.typ == Chattype::OutBroadcast | ||
| && matches!( | ||
| msg.param.get_cmd(), | ||
| SystemMessage::MemberRemovedFromGroup | SystemMessage::MemberAddedToGroup | ||
| ) | ||
| { | ||
| let Some(fp) = msg.param.get(Param::Arg4) else { | ||
| return Some(Err(format_err!("Missing removed/added member"))); | ||
| }; | ||
| return Some(Ok(fp)); | ||
| } | ||
| None | ||
| } | ||
|
|
||
| async fn build_body_file(context: &Context, msg: &Message) -> Result<MimePart<'static>> { | ||
| let file_name = msg.get_filename().context("msg has no file")?; | ||
| let blob = msg | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.