Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
ec2056f
feat: Symmetric encryption. No decryption, no sharing of the secret, …
Hocuri Jul 7, 2025
47bf4da
WIP: Start with decryption, and a test for it. Next TODO: SQL table m…
Hocuri Jul 7, 2025
8653fdb
feat: Save the secret to encrypt and decrypt messages. Next: Send it …
Hocuri Jul 11, 2025
3781a35
feat: Add create_broadcast_shared_secret()
Hocuri Jul 21, 2025
382023d
sync broadcast secret for multidevice
Hocuri Jul 21, 2025
547f750
Make it compile
Hocuri Jul 21, 2025
789b923
feat: Store symmetric key non-redundantly in the database
Hocuri Jul 21, 2025
3389e93
feat: Add broadcast QR type (todo: documentation)
Hocuri Jul 21, 2025
5da7e45
Adapt the rest of the code to the new QR code type
Hocuri Jul 23, 2025
24561cd
test: Add test_send_avatar_in_securejoin
Hocuri Jul 25, 2025
326deab
Broadcast-securejoin is working!!
Hocuri Aug 1, 2025
9c747b4
fix: make test_broadcast work, return an error when trying to add man…
Hocuri Aug 1, 2025
b276eda
Make basic multi-device work on joiner side, fix test_only_minimal_da…
Hocuri Aug 1, 2025
6bb714a
fix: Make syncing of QR tokens work, make test_sync_broadcast pass
Hocuri Aug 1, 2025
cdd280a
make test_block_broadcast pass
Hocuri Aug 1, 2025
df2693f
test: Fix test_broadcast_multidev
Hocuri Aug 1, 2025
37f6da1
test: Fix one panic in test_broadcasts_name_and_avatar, but there is …
Hocuri Aug 1, 2025
7e191f6
fix: Make joining a channel work with multi-device, fix test_leave_br…
Hocuri Aug 1, 2025
504b2d6
test: fix test_leave_broadcast
Hocuri Aug 1, 2025
6012595
test: fix test_encrypt_decrypt_broadcast()
Hocuri Aug 4, 2025
9111014
fix: Actually send broadcast message to recipients, ALL TESTS PASS NO…
Hocuri Aug 4, 2025
548f5a4
Add TODO
Hocuri Aug 4, 2025
c4001cc
fix: Let Alice send vb-member-added so that the chat is immediately s…
Hocuri Aug 4, 2025
9474fbf
fix: Correct member-added info messages
Hocuri Aug 4, 2025
a14b53e
fix: Don't show a weird 'vb-request-with-auth' message when a subscri…
Hocuri Aug 4, 2025
13bbcbe
Add some print statements for debugging
Hocuri Aug 4, 2025
494ad63
feat: Increase secret size to 256 bits of entropy
Hocuri Aug 4, 2025
ad0e317
Remove unused and problematic ensure!
Hocuri Aug 4, 2025
d431f2e
Add benchmark for message decryption
Hocuri Aug 4, 2025
fca8948
Speed up message decryption by not iterating in the s2k algorithm
Hocuri Aug 4, 2025
72336eb
Add benchmark for message decryption
Hocuri Aug 5, 2025
410048a
Improve TODOs
Hocuri Aug 6, 2025
0978a46
WIP, untested: Sending side of transferring the secret in member-adde…
Hocuri Aug 6, 2025
e1abaeb
WIP, untested: Receiving side of passing broadcast secret in a message
Hocuri Aug 6, 2025
738f6c1
feat: Transfer the broadcast secret in an encrypted message rather th…
Hocuri Aug 7, 2025
db32f11
Don't include the broadcast's shared secret in the QR code
Hocuri Aug 7, 2025
1377a77
refactor: Use the same decode_name() function for the contact name, r…
Hocuri Aug 7, 2025
40e3c34
refactor: It's not actually necessary for Alice to remember how the m…
Hocuri Aug 7, 2025
58d0fd3
clippy
Hocuri Aug 7, 2025
5da6ca1
test: Improve test_send_avatar_in_securejoin()
Hocuri Aug 7, 2025
3d5e97e
No clippy warnings anymore!
Hocuri Aug 7, 2025
a858709
Use translatable message for broadcast-joining
Hocuri Aug 7, 2025
8d89dcc
Add golden test that only one member-added message is shown for Bob
Hocuri Aug 7, 2025
265ac4e
fix: Show only one member-added message for Bob
Hocuri Aug 7, 2025
378896e
docs: Fix wrong comment on msg_del_member_local()
Hocuri Aug 8, 2025
0acc34a
Notify a removed member that they were removed
Hocuri Aug 8, 2025
3cf7746
Remove unnecessary TODO
Hocuri Aug 8, 2025
792c05f
fix: Don't show a weird 'Secure-Join: vb-request-v2 message' in Alice…
Hocuri Aug 8, 2025
90d4856
comments/naming: Make sure that I consistently use shared_secret
Hocuri Aug 8, 2025
956519c
fix: Make sure that only the channel owner can write into the chat
Hocuri Aug 11, 2025
9dc590c
feat: Rename vb-request-v2 -> vb-request-with-auth
Hocuri Aug 11, 2025
479a563
feat: Make reacting to v2 invites generic over the type of the invite…
Hocuri Aug 11, 2025
2efbbcc
bench: Improve benchmark_decrypting.rs benchmark
Hocuri Aug 11, 2025
61e0d14
refactor: Remove small code duplication
Hocuri Aug 11, 2025
3a64869
resolve some small TODOs
Hocuri Aug 11, 2025
00ba559
Resolve some small TODOs
Hocuri Aug 11, 2025
40f4eea
feat: Sync Alice's verification on Bob's side
Hocuri Aug 12, 2025
9b49386
fix: Protect against DOS attacks via a message with many esks using e…
Hocuri Aug 15, 2025
f66f6f3
refactor: Rename to symm_encrypt_message()
Hocuri Aug 16, 2025
dc5237f
fix: Remove panic!() call
Hocuri Aug 16, 2025
a3d1e3b
Remove TODO
Hocuri Aug 16, 2025
f7844e9
fix: Don't show wrong system message on Bob's second device
Hocuri Aug 16, 2025
51a36d2
small refactoring
Hocuri Aug 16, 2025
19159c9
test: Rename alice0, alice1 to alice1, alice2 in test_sync_muted()
Hocuri Sep 1, 2025
019da70
test: When a golden test fails, print some extra info
Hocuri Sep 1, 2025
0c25646
test: Add golden test for Alice's side, too, in test_sync_broadcast
Hocuri Sep 1, 2025
4a9af2b
refactor: Remove superflous check for ChatGroupMemberAdded
Hocuri Sep 1, 2025
153ced7
Remove outdated TODO
Hocuri Sep 1, 2025
6e68eb1
Resolve identity-misbinding TODO
Hocuri Sep 1, 2025
286f913
refactor: No need for observe_securejoin_on_other_device() for secure…
Hocuri Sep 2, 2025
8eb5fc5
Adapt to things that changed when I rebased
Hocuri Sep 3, 2025
60e4899
test: Add python test test_qr_securejoin_broadcast, and fix some smal…
Hocuri Sep 3, 2025
01d9acb
test_qr_securejoin_broadcast(): Test a few more things
Hocuri Sep 4, 2025
b5a54aa
fix: Scaleup contact on securejoin, send more events, use correct cre…
Hocuri Sep 5, 2025
ae4b0fd
Adapt golden tests to the fact that 'Messages are end-to-end encrypte…
Hocuri Sep 5, 2025
302059c
clippy
Hocuri Sep 5, 2025
f8a46fe
test(python): Extend test_qr_securejoin_broadcast and make it less flaky
Hocuri Sep 8, 2025
18c84e8
Merge remote-tracking branch 'origin/main' into hoc/channels-encrypti…
Hocuri Sep 9, 2025
4c068e8
test: fix test_sync_broadcast()
Hocuri Sep 9, 2025
fc52c8d
Merge remote-tracking branch 'origin/main' into hoc/channels-encrypti…
Hocuri Sep 9, 2025
557702e
Remove outdated TODO
Hocuri Sep 9, 2025
3a8a6f6
Revert small superflous change
Hocuri Sep 9, 2025
dd11364
test: fix test_broadcast(): Broadcast channels are never unpromoted
Hocuri Sep 9, 2025
de10f31
test: Remove old test_broadcast, which tested manually adding a membe…
Hocuri Sep 9, 2025
d967bff
Revert `debug = 'full'`
Hocuri Sep 10, 2025
dca184f
refactor: simplify create_broadcast_ex()
Hocuri Sep 10, 2025
a5d9d43
refactor: small renames
Hocuri Sep 10, 2025
abd091d
Remove TODO
Hocuri Sep 10, 2025
6cd499e
Accept 9 lines of code duplication in exchange for lower code complexity
Hocuri Sep 10, 2025
23c04c2
security: Make sure that there is no trace of a member after they left
Hocuri Sep 10, 2025
ac98289
Remove outdated TODO
Hocuri Sep 10, 2025
632dd28
test: Add test_leave_broadcast, fix bugs I found along the way
Hocuri Sep 11, 2025
e8fff88
test: Improve test_leave_broadcast(), fix small bugs I found along th…
Hocuri Sep 11, 2025
cc54c68
Improve docs
Hocuri Sep 11, 2025
9914233
Remove unused function
Hocuri Sep 11, 2025
45bed57
test: Improve test_leave_broadcast a bit
Hocuri Sep 11, 2025
dfc969e
Try reverting a possibly-unnecessary change
Hocuri Sep 11, 2025
c5b5d80
test: simplify a bit
Hocuri Sep 11, 2025
640d810
Remove rarely-used function is_any_broadcast()
Hocuri Sep 11, 2025
8fda2de
Remove another unnecessary function
Hocuri Sep 11, 2025
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
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ name = "receive_emails"
required-features = ["internals"]
harness = false

[[bench]]
name = "benchmark_decrypting"
required-features = ["internals"]
harness = false

[[bench]]
name = "get_chat_msgs"
harness = false
Expand Down
199 changes: 199 additions & 0 deletions benches/benchmark_decrypting.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//! Benchmarks for message decryption,
//! comparing decryption of symmetrically-encrypted messages
//! to decryption of asymmetrically-encrypted messages.
//!
//! Call with
//!
//! ```text
//! cargo bench --bench benchmark_decrypting --features="internals"
//! ```
//!
//! or, if you want to only run e.g. the 'Decrypt a symmetrically encrypted message' benchmark:
//!
//! ```text
//! cargo bench --bench benchmark_decrypting --features="internals" -- 'Decrypt a symmetrically encrypted message'
//! ```
//!
//! You can also pass a substring.
//! So, you can run all 'Decrypt and parse' benchmarks with:
//!
//! ```text
//! cargo bench --bench benchmark_decrypting --features="internals" -- 'Decrypt and parse'
//! ```
//!
//! Symmetric decryption has to try out all known secrets,
//! You can benchmark this by adapting the `NUM_SECRETS` variable.

use std::hint::black_box;

use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::internals_for_benchmarks::create_broadcast_shared_secret;
use deltachat::internals_for_benchmarks::create_dummy_keypair;
use deltachat::internals_for_benchmarks::save_broadcast_shared_secret;
use deltachat::{
Events,
chat::ChatId,
config::Config,
context::Context,
internals_for_benchmarks::key_from_asc,
internals_for_benchmarks::parse_and_get_text,
internals_for_benchmarks::store_self_keypair,
pgp::{KeyPair, decrypt, pk_encrypt, symm_encrypt_message},
stock_str::StockStrings,
};
use rand::{Rng, thread_rng};
use tempfile::tempdir;

const NUM_SECRETS: usize = 500;

async fn create_context() -> Context {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let context = Context::new(dbfile.as_path(), 100, Events::new(), StockStrings::new())
.await
.unwrap();

context
.set_config(Config::ConfiguredAddr, Some("[email protected]"))
.await
.unwrap();
let secret = key_from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap();
let public = secret.signed_public_key();
let key_pair = KeyPair { public, secret };
store_self_keypair(&context, &key_pair)
.await
.expect("Failed to save key");

context
}

fn criterion_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("Decrypt");

// ===========================================================================================
// Benchmarks for decryption only, without any other parsing
// ===========================================================================================

group.sample_size(10);

group.bench_function("Decrypt a symmetrically encrypted message", |b| {
let plain = generate_plaintext();
let secrets = generate_secrets();
let encrypted = tokio::runtime::Runtime::new().unwrap().block_on(async {
let secret = secrets[NUM_SECRETS / 2].clone();
symm_encrypt_message(
plain.clone(),
black_box(&secret),
create_dummy_keypair("[email protected]").unwrap().secret,
true,
)
.await
.unwrap()
});

b.iter(|| {
let mut msg =
decrypt(encrypted.clone().into_bytes(), &[], black_box(&secrets)).unwrap();
let decrypted = msg.as_data_vec().unwrap();

assert_eq!(black_box(decrypted), plain);
});
});

group.bench_function("Decrypt a public-key encrypted message", |b| {
let plain = generate_plaintext();
let key_pair = create_dummy_keypair("[email protected]").unwrap();
let secrets = generate_secrets();
let encrypted = tokio::runtime::Runtime::new().unwrap().block_on(async {
pk_encrypt(
plain.clone(),
vec![black_box(key_pair.public.clone())],
Some(key_pair.secret.clone()),
true,
)
.await
.unwrap()
});

b.iter(|| {
let mut msg = decrypt(
encrypted.clone().into_bytes(),
std::slice::from_ref(&key_pair.secret),
black_box(&secrets),
)
.unwrap();
let decrypted = msg.as_data_vec().unwrap();

assert_eq!(black_box(decrypted), plain);
});
});

// ===========================================================================================
// Benchmarks for the whole parsing pipeline, incl. decryption (but excl. receive_imf())
// ===========================================================================================

let rt = tokio::runtime::Runtime::new().unwrap();
let mut secrets = generate_secrets();

// "secret" is the shared secret that was used to encrypt text_symmetrically_encrypted.eml.
// Put it into the middle of our secrets:
secrets[NUM_SECRETS / 2] = "secret".to_string();

let context = rt.block_on(async {
let context = create_context().await;
for (i, secret) in secrets.iter().enumerate() {
save_broadcast_shared_secret(&context, ChatId::new(10 + i as u32), secret)
.await
.unwrap();
}
context
});

group.bench_function("Decrypt and parse a symmetrically encrypted message", |b| {
b.to_async(&rt).iter(|| {
let ctx = context.clone();
async move {
let text = parse_and_get_text(
&ctx,
include_bytes!("../test-data/message/text_symmetrically_encrypted.eml"),
)
.await
.unwrap();
assert_eq!(text, "Symmetrically encrypted message");
}
});
});

group.bench_function("Decrypt and parse a public-key encrypted message", |b| {
b.to_async(&rt).iter(|| {
let ctx = context.clone();
async move {
let text = parse_and_get_text(
&ctx,
include_bytes!("../test-data/message/text_from_alice_encrypted.eml"),
)
.await
.unwrap();
assert_eq!(text, "hi");
}
});
});

group.finish();
}

fn generate_secrets() -> Vec<String> {
let secrets: Vec<String> = (0..NUM_SECRETS)
.map(|_| create_broadcast_shared_secret())
.collect();
secrets
}

fn generate_plaintext() -> Vec<u8> {
let mut plain: Vec<u8> = vec![0; 500];
thread_rng().fill(&mut plain[..]);
plain
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
6 changes: 6 additions & 0 deletions deltachat-ffi/src/lot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ impl Lot {
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { .. } => None,
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
Qr::AskJoinBroadcast { broadcast_name, .. } => Some(Cow::Borrowed(broadcast_name)),
Qr::FprOk { .. } => None,
Qr::FprMismatch { .. } => None,
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
Expand Down Expand Up @@ -99,6 +100,7 @@ impl Lot {
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
Qr::AskJoinBroadcast { .. } => LotState::QrAskJoinBroadcast,
Qr::FprOk { .. } => LotState::QrFprOk,
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
Expand Down Expand Up @@ -126,6 +128,7 @@ impl Lot {
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::AskVerifyGroup { .. } => Default::default(),
Qr::AskJoinBroadcast { .. } => Default::default(),
Qr::FprOk { contact_id } => contact_id.to_u32(),
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
Qr::FprWithoutAddr { .. } => Default::default(),
Expand Down Expand Up @@ -169,6 +172,9 @@ pub enum LotState {
/// text1=groupname
QrAskVerifyGroup = 202,

/// text1=broadcast_name
QrAskJoinBroadcast = 204,

/// id=contact
QrFprOk = 210,

Expand Down
2 changes: 1 addition & 1 deletion deltachat-jsonrpc/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,7 @@ impl CommandApi {
.await
}

/// Create a new **broadcast channel**
/// Create a new, outgoing **broadcast channel**
/// (called "Channel" in the UI).
///
/// Broadcast channels are similar to groups on the sending device,
Expand Down
34 changes: 34 additions & 0 deletions deltachat-jsonrpc/src/api/types/qr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ pub enum QrObject {
/// Authentication code.
authcode: String,
},
/// Ask the user whether to join the broadcast channel.
AskJoinBroadcast {
/// Chat name.
broadcast_name: String,
/// A string of random characters,
/// uniquely identifying this broadcast channel in the database.
/// Called `grpid` for historic reasons:
/// The id of multi-user chats is always called `grpid` in the database
/// because groups were once the only multi-user chats.
grpid: String,
/// ID of the contact who owns the channel and created the QR code.
contact_id: u32,
/// Fingerprint of the contact's key as scanned from the QR code.
fingerprint: String,

authcode: String,
},
/// Contact fingerprint is verified.
///
/// Ask the user if they want to start chatting.
Expand Down Expand Up @@ -207,6 +224,23 @@ impl From<Qr> for QrObject {
authcode,
}
}
Qr::AskJoinBroadcast {
broadcast_name,
grpid,
contact_id,
fingerprint,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskJoinBroadcast {
broadcast_name,
grpid,
contact_id,
fingerprint,
authcode,
}
}
Qr::FprOk { contact_id } => {
let contact_id = contact_id.to_u32();
QrObject::FprOk { contact_id }
Expand Down
2 changes: 1 addition & 1 deletion deltachat-rpc-client/src/deltachat_rpc_client/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ def create_group(self, name: str, protect: bool = False) -> Chat:
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))

def create_broadcast(self, name: str) -> Chat:
"""Create a new **broadcast channel**
"""Create a new, outgoing **broadcast channel**
(called "Channel" in the UI).

Broadcast channels are similar to groups on the sending device,
Expand Down
11 changes: 11 additions & 0 deletions deltachat-rpc-client/src/deltachat_rpc_client/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ def wait_until_delivered(self) -> None:
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
break

def resend(self) -> None:
"""Resend messages and make information available for newly added chat members.
Resending sends out the original message, however, recipients and webxdc-status may differ.
Clients that already have the original message can still ignore the resent message as
they have tracked the state by dedicated updates.

Some messages cannot be resent, eg. info-messages, drafts, already pending messages,
or messages that are not sent by SELF.
"""
self._rpc.resend_messages(self.account.id, [self.id])

@futuremethod
def send_webxdc_realtime_advertisement(self):
"""Send an advertisement to join the realtime channel."""
Expand Down
Loading
Loading