feat: reduce unncessary gossipping of keys in group chats

- treat an Autocrypt header as a kind of Autocrypt-Gossip: addr=<self>
  letting it participate in the existing cooperative key gossipping mechanics.

- speed up processing of incoming autocrypt-gossip headers by using
  1 sql commit for N gossip headers instead of the current N commits.
This commit is contained in:
holger krekel
2026-07-27 21:54:42 +02:00
parent 6a07a2b222
commit 856ea29c47
3 changed files with 57 additions and 8 deletions
+18
View File
@@ -1027,6 +1027,24 @@ impl MimeMessage {
self.signature.is_some() self.signature.is_some()
} }
/// Returns the fingerprints of all keys distributed by this message:
/// - keys from Autocrypt-Gossip headers
/// - the key from the sender's Autocrypt header ("self-gossip")
///
/// Nothing is returned unless the message was correctly encrypted.
pub(crate) fn distributed_key_fingerprints(&self) -> Vec<String> {
let sender_fingerprint = if self.was_encrypted() {
self.autocrypt_fingerprint.clone()
} else {
None
};
self.gossiped_keys
.values()
.map(|gossiped_key| gossiped_key.public_key.dc_fingerprint().hex())
.chain(sender_fingerprint)
.collect()
}
/// Returns whether the email contains a `chat-version` header. /// Returns whether the email contains a `chat-version` header.
/// This indicates that the email is a DC-email. /// This indicates that the email is a DC-email.
pub(crate) fn has_chat_version(&self) -> bool { pub(crate) fn has_chat_version(&self) -> bool {
+10 -8
View File
@@ -755,24 +755,26 @@ pub(crate) async fn receive_imf_inner(
contact::update_last_seen(context, from_id, mime_parser.timestamp_sent).await?; contact::update_last_seen(context, from_id, mime_parser.timestamp_sent).await?;
} }
// Update gossiped timestamp for the chat if someone else or our other device sent // Update gossiped timestamp for the chat if someone else or our other device
// Autocrypt-Gossip header to avoid sending Autocrypt-Gossip ourselves // distributed keys to this chat, via Autocrypt-Gossip headers
// and waste traffic. // or the sender's own Autocrypt header which is a kind of self-gossip.
let chat_id = received_msg.chat_id; let chat_id = received_msg.chat_id;
if !chat_id.is_special() { if !chat_id.is_special() {
for gossiped_key in mime_parser.gossiped_keys.values() { let fingerprints = mime_parser.distributed_key_fingerprints();
if !fingerprints.is_empty() {
let timestamp_sent = mime_parser.timestamp_sent;
context context
.sql .sql
.transaction(move |transaction| { .transaction(move |transaction| {
let fingerprint = gossiped_key.public_key.dc_fingerprint().hex(); let mut stmt = transaction.prepare(
transaction.execute(
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp) "INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
VALUES (?, ?, ?) VALUES (?, ?, ?)
ON CONFLICT (chat_id, fingerprint) ON CONFLICT (chat_id, fingerprint)
DO UPDATE SET timestamp=MAX(timestamp, excluded.timestamp)", DO UPDATE SET timestamp=MAX(timestamp, excluded.timestamp)",
(chat_id, &fingerprint, mime_parser.timestamp_sent),
)?; )?;
for fingerprint in &fingerprints {
stmt.execute((chat_id, fingerprint, timestamp_sent))?;
}
Ok(()) Ok(())
}) })
.await?; .await?;
+29
View File
@@ -5503,6 +5503,35 @@ async fn test_group_introduction_no_gossip() -> Result<()> {
Ok(()) Ok(())
} }
/// Tests that the sender's own Autocrypt header counts like received gossip:
/// members do not re-gossip a key that its owner just distributed themselves.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_autocrypt_header_suppresses_gossip() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let alice_chat_id = alice
.create_group_with_members("Group", &[bob, fiona])
.await;
let sent = alice.send_text(alice_chat_id, "Hello group").await;
// Alice's first message gossips the other members' keys.
let msg = bob.recv_msg(&sent).await;
assert!(!bob.parse_msg(&sent).await.gossiped_keys.is_empty());
// Bob got Alice's key from her Autocrypt header
// and the other members' keys from her gossip,
// so Bob has nothing left to gossip.
let bob_chat_id = msg.chat_id;
bob_chat_id.accept(bob).await?;
let sent = bob.send_text(bob_chat_id, "Hello back").await;
assert!(fiona.parse_msg(&sent).await.gossiped_keys.is_empty());
Ok(())
}
/// Tests reception of an encrypted group message /// Tests reception of an encrypted group message
/// without Chat-Group-ID. /// without Chat-Group-ID.
/// ///