Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
c0b86f5
Send v=3 in QR codes
Hocuri Jan 15, 2026
ad584d2
[wip, not compiling] Add render_symm_encrypted_securejoin_message()
Hocuri Jan 20, 2026
3b66611
[wip, not compiling] extract function group_headers_by_confidentiality()
Hocuri Jan 20, 2026
2025e9b
[wip, doesn't compile] refactor: exctract add_headers_to_encrypted_pa…
Hocuri Jan 20, 2026
2eba552
[wip, doesn't compile] fix: Set subject for securejoin message
Hocuri Jan 20, 2026
2566c5b
[wip, doesn't compile] Extract render_outer_message() functions
Hocuri Jan 20, 2026
1014bf1
It compiles
Hocuri Jan 20, 2026
c91b349
feat: Decrypt and answer on Alice's side (sending the message seems n…
Hocuri Jan 20, 2026
4c00bc1
Fix some bugs when sending vc-pubkey, decrypt vc-pubkey on Bob's side
Hocuri Jan 21, 2026
d979938
Handle vc-pubkey on Bob's side
Hocuri Jan 21, 2026
c9bd5d0
Adapt some tests
Hocuri Jan 21, 2026
c3d4c43
fix: Don't leak cryptographic identity by signing vc-request-pubkey
Hocuri Jan 21, 2026
7ff5cd6
fix: Don't limit number of auth tokens to 1
Hocuri Jan 21, 2026
297c6d8
modify test
Hocuri Jan 21, 2026
2358c9d
Update golden tests
Hocuri Jan 21, 2026
90b0f94
Make the remaining Rust tests pass
Hocuri Jan 21, 2026
aceb75b
Update src/mimeparser.rs
Hocuri Jan 22, 2026
8ee3c2a
Add failing test test_auth_token_is_synchronized()
Hocuri Jan 28, 2026
6b658a0
fix: Synchronize all AUTH tokens
Hocuri Jan 30, 2026
db7e667
Merge remote-tracking branch 'origin/main' into hoc/securejoin-v3
Hocuri Feb 10, 2026
20ef54b
bench: Also add auth tokens into benchmark
Hocuri Feb 10, 2026
50eb923
bench: Add black_box
Hocuri Feb 10, 2026
1b25853
bench: Remove benchmarks that only decrypt without parsing
Hocuri Feb 10, 2026
61ae743
perf: Only load shared secrets if the message actually is symm-encrypted
Hocuri Feb 10, 2026
7a81411
perf: Load BobState shared secrets first; refactor: extract fn load_s…
Hocuri Feb 10, 2026
a442077
Add explanatory performance comment
Hocuri Feb 10, 2026
cca6bd4
Remove TODO
Hocuri Feb 10, 2026
642ba1d
feat: Make it possible to omit the invitenumber and v=3 for Securejoi…
Hocuri Feb 10, 2026
b5ae93c
test: Test that securejoin v3 runs even without `v=3` when there is n…
Hocuri Feb 10, 2026
80db501
test: Test that Securejoin without v=3 and without INVITENUMBER also …
Hocuri Feb 10, 2026
81d9fa7
fix: For future-compatibility, don't rely on Invite for checking whet…
Hocuri Feb 10, 2026
e773468
fix: Don't save empty tokens
Hocuri Feb 10, 2026
0e359e9
Improve comment
Hocuri Feb 10, 2026
46c9fc3
feat: Remove Auto-Submitted: auto-replied header from securejoin mess…
Hocuri Feb 10, 2026
10933cc
test: Improve test
Hocuri Feb 10, 2026
fd61423
Some more comments improvements
Hocuri Feb 10, 2026
0d47a52
More comments and renamings
Hocuri Feb 10, 2026
3b0f798
Merge remote-tracking branch 'origin/main' into hoc/securejoin-v3
Hocuri Feb 10, 2026
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
117 changes: 30 additions & 87 deletions benches/decrypting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,47 @@
//! cargo bench --bench decrypting --features="internals"
//! ```
//!
//! or, if you want to only run e.g. the 'Decrypt a symmetrically encrypted message' benchmark:
//! or, if you want to only run e.g. the 'Decrypt and parse a symmetrically encrypted message' benchmark:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I removed the "Decrypt a symmetrically/public-key encrypted message" benchmarks, because I don't need them anymore, it seems unlikely they will be needed soon, and they made it hard to implement the performance improvement in 61ae743

//!
//! ```text
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt a symmetrically encrypted message'
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt and parse a symmetrically encrypted message'
//! ```
//!
//! You can also pass a substring.
//! So, you can run all 'Decrypt and parse' benchmarks with:
//! You can also pass a substring:
//!
//! ```text
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt and parse'
//! cargo bench --bench decrypting --features="internals" -- 'symmetrically'
//! ```
//!
//! 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 std::sync::LazyLock;

use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::internals_for_benches::create_broadcast_secret;
use deltachat::internals_for_benches::create_dummy_keypair;
use deltachat::internals_for_benches::save_broadcast_secret;
use deltachat::securejoin::get_securejoin_qr;
use deltachat::{
Events,
chat::ChatId,
config::Config,
context::Context,
internals_for_benches::key_from_asc,
internals_for_benches::parse_and_get_text,
internals_for_benches::store_self_keypair,
pgp::{KeyPair, SeipdVersion, decrypt, pk_encrypt, symm_encrypt_message},
stock_str::StockStrings,
Events, chat::ChatId, config::Config, context::Context, internals_for_benches::key_from_asc,
internals_for_benches::parse_and_get_text, internals_for_benches::store_self_keypair,
pgp::KeyPair, stock_str::StockStrings,
};
use rand::{Rng, rng};
use tempfile::tempdir;

const NUM_SECRETS: usize = 500;
static NUM_BROADCAST_SECRETS: LazyLock<usize> = LazyLock::new(|| {
std::env::var("NUM_BROADCAST_SECRETS")
.unwrap_or("500".to_string())
.parse()
.unwrap()
});
static NUM_AUTH_TOKENS: LazyLock<usize> = LazyLock::new(|| {
std::env::var("NUM_AUTH_TOKENS")
.unwrap_or("5000".to_string())
.parse()
.unwrap()
});

async fn create_context() -> Context {
let dir = tempdir().unwrap();
Expand All @@ -70,66 +74,6 @@ async fn create_context() -> 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(),
create_dummy_keypair("alice@example.org").unwrap().secret,
black_box(&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("alice@example.org").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())],
key_pair.secret.clone(),
true,
true,
SeipdVersion::V2,
)
.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())
// ===========================================================================================
Expand All @@ -139,7 +83,7 @@ fn criterion_benchmark(c: &mut Criterion) {

// "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();
secrets[*NUM_BROADCAST_SECRETS / 2] = "secret".to_string();

let context = rt.block_on(async {
let context = create_context().await;
Expand All @@ -148,6 +92,10 @@ fn criterion_benchmark(c: &mut Criterion) {
.await
.unwrap();
}
for _i in 0..*NUM_AUTH_TOKENS {
get_securejoin_qr(&context, None).await.unwrap();
}
println!("NUM_AUTH_TOKENS={}", *NUM_AUTH_TOKENS);
context
});

Expand All @@ -161,7 +109,7 @@ fn criterion_benchmark(c: &mut Criterion) {
)
.await
.unwrap();
assert_eq!(text, "Symmetrically encrypted message");
assert_eq!(black_box(text), "Symmetrically encrypted message");
}
});
});
Expand All @@ -176,7 +124,7 @@ fn criterion_benchmark(c: &mut Criterion) {
)
.await
.unwrap();
assert_eq!(text, "hi");
assert_eq!(black_box(text), "hi");
}
});
});
Expand All @@ -185,17 +133,12 @@ fn criterion_benchmark(c: &mut Criterion) {
}

fn generate_secrets() -> Vec<String> {
let secrets: Vec<String> = (0..NUM_SECRETS)
let secrets: Vec<String> = (0..*NUM_BROADCAST_SECRETS)
.map(|_| create_broadcast_secret())
.collect();
println!("NUM_BROADCAST_SECRETS={}", *NUM_BROADCAST_SECRETS);
secrets
}

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

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
12 changes: 12 additions & 0 deletions deltachat-jsonrpc/src/api/types/qr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub enum QrObject {
invitenumber: String,
/// Authentication code.
authcode: String,
/// Whether the sender supports the new Securejoin v3 protocol
is_v3: bool,
},
/// Ask the user whether to join the group.
AskVerifyGroup {
Expand All @@ -34,6 +36,8 @@ pub enum QrObject {
invitenumber: String,
/// Authentication code.
authcode: String,
/// Whether the sender supports the new Securejoin v3 protocol
is_v3: bool,
},
/// Ask the user whether to join the broadcast channel.
AskJoinBroadcast {
Expand All @@ -54,6 +58,8 @@ pub enum QrObject {
invitenumber: String,
/// Authentication code.
authcode: String,
/// Whether the sender supports the new Securejoin v3 protocol
is_v3: bool,
},
/// Contact fingerprint is verified.
///
Expand Down Expand Up @@ -229,6 +235,7 @@ impl From<Qr> for QrObject {
fingerprint,
invitenumber,
authcode,
is_v3,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
Expand All @@ -237,6 +244,7 @@ impl From<Qr> for QrObject {
fingerprint,
invitenumber,
authcode,
is_v3,
}
}
Qr::AskVerifyGroup {
Expand All @@ -246,6 +254,7 @@ impl From<Qr> for QrObject {
fingerprint,
invitenumber,
authcode,
is_v3,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
Expand All @@ -256,6 +265,7 @@ impl From<Qr> for QrObject {
fingerprint,
invitenumber,
authcode,
is_v3,
}
}
Qr::AskJoinBroadcast {
Expand All @@ -265,6 +275,7 @@ impl From<Qr> for QrObject {
fingerprint,
authcode,
invitenumber,
is_v3,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
Expand All @@ -275,6 +286,7 @@ impl From<Qr> for QrObject {
fingerprint,
authcode,
invitenumber,
is_v3,
}
}
Qr::FprOk { contact_id } => {
Expand Down
10 changes: 5 additions & 5 deletions src/aheader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ pub struct Aheader {
pub public_key: SignedPublicKey,
pub prefer_encrypt: EncryptPreference,

// Whether `_verified` attribute is present.
//
// `_verified` attribute is an extension to `Autocrypt-Gossip`
// header that is used to tell that the sender
// marked this key as verified.
/// Whether `_verified` attribute is present.
///
/// `_verified` attribute is an extension to `Autocrypt-Gossip`
/// header that is used to tell that the sender
/// marked this key as verified.
pub verified: bool,
}

Expand Down
52 changes: 24 additions & 28 deletions src/chat/chat_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2731,27 +2731,24 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
join_securejoin(charlie, &qr).await.unwrap();

let request = charlie.pop_sent_msg().await;
assert_eq!(request.recipients, "alice@example.org charlie@example.net");
assert_eq!(request.recipients, "alice@example.org");
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The first two messages of the protocol aren't sent to self anymore, because in many cases, one's own other devices wouldn't be able to decrypt them, anyways.


alice.recv_msg_trash(&request).await;
}

tcm.section("Alice sends auth-required");
tcm.section("Alice sends vc-pubkey");
{
let auth_required = alice.pop_sent_msg().await;
assert_eq!(
auth_required.recipients,
"charlie@example.net alice@example.org"
);
let parsed = charlie.parse_msg(&auth_required).await;
assert!(parsed.get_header(HeaderDef::AutocryptGossip).is_some());
assert!(parsed.decoded_data_contains("charlie@example.net"));
let vc_pubkey = alice.pop_sent_msg().await;
assert_eq!(vc_pubkey.recipients, "charlie@example.net");
let parsed = charlie.parse_msg(&vc_pubkey).await;
assert!(parsed.get_header(HeaderDef::AutocryptGossip).is_none());
assert_eq!(parsed.decoded_data_contains("charlie@example.net"), false);
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);

let parsed_by_bob = bob.parse_msg(&auth_required).await;
let parsed_by_bob = bob.parse_msg(&vc_pubkey).await;
assert!(parsed_by_bob.decrypting_failed);

charlie.recv_msg_trash(&auth_required).await;
charlie.recv_msg_trash(&vc_pubkey).await;
}

tcm.section("Charlie sends request-with-auth");
Expand Down Expand Up @@ -2992,9 +2989,8 @@ async fn test_broadcast_recipients_sync1() -> Result<()> {
alice1.recv_msg_trash(&request).await;
alice2.recv_msg_trash(&request).await;

let auth_required = alice1.pop_sent_msg().await;
charlie.recv_msg_trash(&auth_required).await;
alice2.recv_msg_trash(&auth_required).await;
let vc_pubkey = alice1.pop_sent_msg().await;
charlie.recv_msg_trash(&vc_pubkey).await;

let request_with_auth = charlie.pop_sent_msg().await;
alice1.recv_msg_trash(&request_with_auth).await;
Expand Down Expand Up @@ -3299,14 +3295,17 @@ async fn test_broadcast_joining_golden() -> Result<()> {
.await;

let alice_bob_contact = alice.add_or_lookup_contact_no_key(bob).await;
let private_chat = ChatIdBlocked::lookup_by_contact(alice, alice_bob_contact.id)
.await?
.unwrap();
// The 1:1 chat with Bob should not be visible to the user:
assert_eq!(private_chat.blocked, Blocked::Yes);
assert!(
ChatIdBlocked::lookup_by_contact(alice, alice_bob_contact.id)
.await?
.is_none()
);
let private_chat_id =
ChatId::create_for_contact_with_blocked(alice, alice_bob_contact.id, Blocked::Not).await?;
alice
.golden_test_chat(
private_chat.id,
private_chat_id,
"test_broadcast_joining_golden_private_chat",
)
.await;
Expand Down Expand Up @@ -3583,16 +3582,13 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
join_securejoin(bob0, &qr).await.unwrap();

let request = bob0.pop_sent_msg().await;
assert_eq!(request.recipients, "alice@example.org bob@example.net");
assert_eq!(request.recipients, "alice@example.org");

alice.recv_msg_trash(&request).await;
let auth_required = alice.pop_sent_msg().await;
assert_eq!(
auth_required.recipients,
"bob@example.net alice@example.org"
);
let vc_pubkey = alice.pop_sent_msg().await;
assert_eq!(vc_pubkey.recipients, "bob@example.net");

bob0.recv_msg_trash(&auth_required).await;
bob0.recv_msg_trash(&vc_pubkey).await;
let request_with_auth = bob0.pop_sent_msg().await;
assert_eq!(
request_with_auth.recipients,
Expand All @@ -3608,7 +3604,7 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
assert_eq!(rcvd.param.get_cmd(), SystemMessage::MemberAddedToGroup);

tcm.section("Bob's second device also receives these messages");
bob1.recv_msg_trash(&auth_required).await;
bob1.recv_msg_trash(&vc_pubkey).await;
bob1.recv_msg_trash(&request_with_auth).await;
bob1.recv_msg(&member_added).await;

Expand Down
Loading