Skip to content

Commit 9f5e608

Browse files
committed
feat: track gossiping per (chat, fingerprint) pair
This change simplifies updating the gossip timestamps when we receive a message because we only need to know the keys received in Autocrypt-Gossip header and which chat the message is assigned to. We no longer need to iterate over the member list. This is a preparation for PGP contacts and member lists that contain key fingerprints rather than email addresses. This change also removes encryption preference from Autocrypt-Gossip header. It SHOULD NOT be gossiped according to the Autocrypt specification and we ignore encryption preference anyway since 1.157.0. test_gossip_optimization is removed because it relied on a per-chat gossip_timestamp.
1 parent 0b82b42 commit 9f5e608

File tree

8 files changed

+109
-173
lines changed

8 files changed

+109
-173
lines changed

python/tests/test_0_complex_or_slow.py

-8
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ def test_group_many_members_add_leave_remove(self, acfactory, lp):
1616
lp.sec("ac1: send message to new group chat")
1717
msg1 = chat.send_text("hello")
1818
assert msg1.is_encrypted()
19-
gossiped_timestamp = chat.get_summary()["gossiped_timestamp"]
20-
assert gossiped_timestamp > 0
2119

2220
assert chat.num_contacts() == 3 + 1
2321

@@ -46,19 +44,13 @@ def test_group_many_members_add_leave_remove(self, acfactory, lp):
4644
assert to_remove.addr in sysmsg.text
4745
assert sysmsg.chat.num_contacts() == 3
4846

49-
# Receiving message about removed contact does not reset gossip
50-
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
51-
5247
lp.sec("ac1: sending another message to the chat")
5348
chat.send_text("hello2")
5449
msg = ac2._evtracker.wait_next_incoming_message()
5550
assert msg.text == "hello2"
56-
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
5751

5852
lp.sec("ac1: adding fifth member to the chat")
5953
chat.add_contact(ac5)
60-
# Adding contact to chat resets gossiped_timestamp
61-
assert chat.get_summary()["gossiped_timestamp"] >= gossiped_timestamp
6254

6355
lp.sec("ac2: receiving system message about contact addition")
6456
sysmsg = ac2._evtracker.wait_next_incoming_message()

python/tests/test_1_online.py

-41
Original file line numberDiff line numberDiff line change
@@ -798,12 +798,6 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
798798
msg3.mark_seen()
799799
assert not list(ac1.get_fresh_messages())
800800

801-
# Test that we do not gossip peer keys in 1-to-1 chat,
802-
# as it makes no sense to gossip to peers their own keys.
803-
# Gossip is only sent in encrypted messages,
804-
# and we sent encrypted msg_back right above.
805-
assert chat2b.get_summary()["gossiped_timestamp"] == 0
806-
807801
lp.sec("create group chat with two members, one of which has no encrypt state")
808802
chat = ac1.create_group_chat("encryption test")
809803
chat.add_contact(ac2)
@@ -813,41 +807,6 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
813807
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
814808

815809

816-
def test_gossip_optimization(acfactory, lp):
817-
"""Test that gossip timestamp is updated when someone else sends gossip,
818-
so we don't have to send gossip ourselves.
819-
"""
820-
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
821-
822-
acfactory.introduce_each_other([ac1, ac2])
823-
acfactory.introduce_each_other([ac2, ac3])
824-
825-
lp.sec("ac1 creates a group chat with ac2")
826-
group_chat = ac1.create_group_chat("hello")
827-
group_chat.add_contact(ac2)
828-
msg = group_chat.send_text("hi")
829-
830-
# No Autocrypt gossip was sent yet.
831-
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
832-
assert gossiped_timestamp == 0
833-
834-
msg = ac2._evtracker.wait_next_incoming_message()
835-
assert msg.is_encrypted()
836-
assert msg.text == "hi"
837-
838-
lp.sec("ac2 adds ac3 to the group")
839-
msg.chat.add_contact(ac3)
840-
841-
lp.sec("ac1 receives message from ac2 and updates gossip timestamp")
842-
msg = ac1._evtracker.wait_next_incoming_message()
843-
assert msg.is_encrypted()
844-
845-
# ac1 has updated the gossip timestamp even though no gossip was sent by ac1.
846-
# ac1 does not need to send gossip because ac2 already did it.
847-
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
848-
assert gossiped_timestamp == int(msg.time_sent.timestamp())
849-
850-
851810
def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
852811
ac1, ac2 = acfactory.get_online_accounts(2)
853812

src/chat.rs

+1-40
Original file line numberDiff line numberDiff line change
@@ -1376,41 +1376,10 @@ impl ChatId {
13761376
}
13771377

13781378
pub(crate) async fn reset_gossiped_timestamp(self, context: &Context) -> Result<()> {
1379-
self.set_gossiped_timestamp(context, 0).await
1380-
}
1381-
1382-
/// Get timestamp of the last gossip sent in the chat.
1383-
/// Zero return value means that gossip was never sent.
1384-
pub async fn get_gossiped_timestamp(self, context: &Context) -> Result<i64> {
1385-
let timestamp: Option<i64> = context
1386-
.sql
1387-
.query_get_value("SELECT gossiped_timestamp FROM chats WHERE id=?;", (self,))
1388-
.await?;
1389-
Ok(timestamp.unwrap_or_default())
1390-
}
1391-
1392-
pub(crate) async fn set_gossiped_timestamp(
1393-
self,
1394-
context: &Context,
1395-
timestamp: i64,
1396-
) -> Result<()> {
1397-
ensure!(
1398-
!self.is_special(),
1399-
"can not set gossiped timestamp for special chats"
1400-
);
1401-
info!(
1402-
context,
1403-
"Set gossiped_timestamp for chat {} to {}.", self, timestamp,
1404-
);
1405-
14061379
context
14071380
.sql
1408-
.execute(
1409-
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
1410-
(timestamp, self),
1411-
)
1381+
.execute("DELETE FROM gossip_timestamp WHERE chat_id=?", (self,))
14121382
.await?;
1413-
14141383
Ok(())
14151384
}
14161385

@@ -1917,7 +1886,6 @@ impl Chat {
19171886
name: self.name.clone(),
19181887
archived: self.visibility == ChatVisibility::Archived,
19191888
param: self.param.to_string(),
1920-
gossiped_timestamp: self.id.get_gossiped_timestamp(context).await?,
19211889
is_sending_locations: self.is_sending_locations,
19221890
color: self.get_color(context).await?,
19231891
profile_image: self
@@ -2460,9 +2428,6 @@ pub struct ChatInfo {
24602428
/// This is the string-serialised version of `Params` currently.
24612429
pub param: String,
24622430

2463-
/// Last time this client sent autocrypt gossip headers to this chat.
2464-
pub gossiped_timestamp: i64,
2465-
24662431
/// Whether this chat is currently sending location-stream messages.
24672432
pub is_sending_locations: bool,
24682433

@@ -3101,10 +3066,6 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
31013066

31023067
let now = smeared_time(context);
31033068

3104-
if rendered_msg.is_gossiped {
3105-
msg.chat_id.set_gossiped_timestamp(context, now).await?;
3106-
}
3107-
31083069
if rendered_msg.last_added_location_id.is_some() {
31093070
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
31103071
error!(context, "Failed to set kml sent_timestamp: {err:#}.");

src/mimefactory.rs

+72-47
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use mail_builder::headers::HeaderType;
1313
use mail_builder::mime::MimePart;
1414
use tokio::fs;
1515

16+
use crate::aheader::{Aheader, EncryptPreference};
1617
use crate::blob::BlobObject;
1718
use crate::chat::{self, Chat};
1819
use crate::config::Config;
@@ -22,6 +23,7 @@ use crate::contact::{Contact, ContactId, Origin};
2223
use crate::context::Context;
2324
use crate::e2ee::EncryptHelper;
2425
use crate::ephemeral::Timer as EphemeralTimer;
26+
use crate::key::DcKey;
2527
use crate::location;
2628
use crate::message::{self, Message, MsgId, Viewtype};
2729
use crate::mimeparser::{is_hidden, SystemMessage};
@@ -131,7 +133,6 @@ pub struct RenderedEmail {
131133
pub message: String,
132134
// pub envelope: Envelope,
133135
pub is_encrypted: bool,
134-
pub is_gossiped: bool,
135136
pub last_added_location_id: Option<u32>,
136137

137138
/// A comma-separated string of sync-IDs that are used by the rendered email and must be deleted
@@ -433,41 +434,6 @@ impl MimeFactory {
433434
}
434435
}
435436

436-
async fn should_do_gossip(&self, context: &Context, multiple_recipients: bool) -> Result<bool> {
437-
match &self.loaded {
438-
Loaded::Message { chat, msg } => {
439-
if chat.typ == Chattype::Broadcast {
440-
// Never send Autocrypt-Gossip in broadcast lists
441-
// as it discloses recipient email addresses.
442-
return Ok(false);
443-
}
444-
445-
let cmd = msg.param.get_cmd();
446-
if cmd == SystemMessage::MemberAddedToGroup
447-
|| cmd == SystemMessage::SecurejoinMessage
448-
{
449-
Ok(true)
450-
} else if multiple_recipients {
451-
// beside key- and member-changes, force a periodic re-gossip.
452-
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
453-
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
454-
// `gossip_period == 0` is a special case for testing,
455-
// enabling gossip in every message.
456-
// Otherwise "smeared timestamps" may result in the condition
457-
// to fail even if the clock is monotonic.
458-
if gossip_period == 0 || time() >= gossiped_timestamp + gossip_period {
459-
Ok(true)
460-
} else {
461-
Ok(false)
462-
}
463-
} else {
464-
Ok(false)
465-
}
466-
}
467-
Loaded::Mdn { .. } => Ok(false),
468-
}
469-
}
470-
471437
fn should_attach_profile_data(msg: &Message) -> bool {
472438
msg.param.get_cmd() != SystemMessage::SecurejoinMessage || {
473439
let step = msg.param.get(Param::Arg).unwrap_or_default();
@@ -787,8 +753,6 @@ impl MimeFactory {
787753
}
788754
}
789755

790-
let mut is_gossiped = false;
791-
792756
let peerstates = self.peerstates_for_recipients(context).await?;
793757
let is_encrypted = !self.should_force_plaintext()
794758
&& (e2ee_guaranteed || encrypt_helper.should_encrypt(context, &peerstates).await?);
@@ -956,16 +920,78 @@ impl MimeFactory {
956920
// Add gossip headers in chats with multiple recipients
957921
let multiple_recipients =
958922
peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
959-
if self.should_do_gossip(context, multiple_recipients).await? {
960-
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
961-
if let Some(header) = peerstate.render_gossip_header(verified) {
962-
message = message.header(
963-
"Autocrypt-Gossip",
964-
mail_builder::headers::raw::Raw::new(header),
965-
);
966-
is_gossiped = true;
923+
924+
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
925+
let now = time();
926+
927+
match &self.loaded {
928+
Loaded::Message { chat, msg } => {
929+
if chat.typ != Chattype::Broadcast {
930+
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
931+
let Some(key) = peerstate.peek_key(verified) else {
932+
continue;
933+
};
934+
935+
let fingerprint = key.dc_fingerprint().hex();
936+
let cmd = msg.param.get_cmd();
937+
let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup
938+
|| cmd == SystemMessage::SecurejoinMessage
939+
|| multiple_recipients && {
940+
let gossiped_timestamp: Option<i64> = context
941+
.sql
942+
.query_get_value(
943+
"SELECT timestamp
944+
FROM gossip_timestamp
945+
WHERE chat_id=? AND fingerprint=?",
946+
(chat.id, &fingerprint),
947+
)
948+
.await?;
949+
950+
// `gossip_period == 0` is a special case for testing,
951+
// enabling gossip in every message.
952+
//
953+
// If current time is in the past compared to
954+
// `gossiped_timestamp`, we also gossip because
955+
// either the `gossiped_timestamp` or clock is wrong.
956+
gossip_period == 0
957+
|| gossiped_timestamp
958+
.is_none_or(|ts| now >= ts + gossip_period || now < ts)
959+
};
960+
961+
if !should_do_gossip {
962+
continue;
963+
}
964+
965+
let header = Aheader::new(
966+
peerstate.addr.clone(),
967+
key.clone(),
968+
// Autocrypt 1.1.0 specification says that
969+
// `prefer-encrypt` attribute SHOULD NOT be included.
970+
EncryptPreference::NoPreference,
971+
)
972+
.to_string();
973+
974+
message = message.header(
975+
"Autocrypt-Gossip",
976+
mail_builder::headers::raw::Raw::new(header),
977+
);
978+
979+
context
980+
.sql
981+
.execute(
982+
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
983+
VALUES (?, ?, ?)
984+
ON CONFLICT (chat_id, fingerprint)
985+
DO UPDATE SET timestamp=excluded.timestamp",
986+
(chat.id, &fingerprint, now),
987+
)
988+
.await?;
989+
}
967990
}
968991
}
992+
Loaded::Mdn { .. } => {
993+
// Never gossip in MDNs.
994+
}
969995
}
970996

971997
// Set the appropriate Content-Type for the inner message.
@@ -1109,7 +1135,6 @@ impl MimeFactory {
11091135
message,
11101136
// envelope: Envelope::new,
11111137
is_encrypted,
1112-
is_gossiped,
11131138
last_added_location_id,
11141139
sync_ids_to_delete: self.sync_ids_to_delete,
11151140
rfc724_mid,

src/peerstate.rs

-22
Original file line numberDiff line numberDiff line change
@@ -405,28 +405,6 @@ impl Peerstate {
405405
};
406406
}
407407

408-
/// Returns the contents of the `Autocrypt-Gossip` header for outgoing messages.
409-
pub fn render_gossip_header(&self, verified: bool) -> Option<String> {
410-
if let Some(key) = self.peek_key(verified) {
411-
let header = Aheader::new(
412-
self.addr.clone(),
413-
key.clone(), // TODO: avoid cloning
414-
// Autocrypt 1.1.0 specification says that
415-
// `prefer-encrypt` attribute SHOULD NOT be included,
416-
// but we include it anyway to propagate encryption
417-
// preference to new members in group chats.
418-
if self.last_seen_autocrypt > 0 {
419-
self.prefer_encrypt
420-
} else {
421-
EncryptPreference::NoPreference
422-
},
423-
);
424-
Some(header.to_string())
425-
} else {
426-
None
427-
}
428-
}
429-
430408
/// Converts the peerstate into the contact public key.
431409
///
432410
/// Similar to [`Self::peek_key`], but consumes the peerstate and returns owned key.

0 commit comments

Comments
 (0)