mirror of
https://github.com/chatmail/core.git
synced 2026-10-02 11:10:25 +03:00
feat: send Autocrypt pgp key in MDNs occassionally and when relaylist changes
This commit is contained in:
+67
-8
@@ -25,7 +25,7 @@ use crate::e2ee::EncryptHelper;
|
|||||||
use crate::ensure_and_debug_assert;
|
use crate::ensure_and_debug_assert;
|
||||||
use crate::ephemeral::Timer as EphemeralTimer;
|
use crate::ephemeral::Timer as EphemeralTimer;
|
||||||
use crate::headerdef::HeaderDef;
|
use crate::headerdef::HeaderDef;
|
||||||
use crate::key::{DcKey, SignedPublicKey, self_fingerprint};
|
use crate::key::{DcKey, SignedPublicKey, load_self_public_key, self_fingerprint};
|
||||||
use crate::location;
|
use crate::location;
|
||||||
use crate::log::warn;
|
use crate::log::warn;
|
||||||
use crate::message::{Message, MsgId, Viewtype};
|
use crate::message::{Message, MsgId, Viewtype};
|
||||||
@@ -619,11 +619,68 @@ impl MimeFactory {
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_skip_autocrypt(&self) -> bool {
|
/// Returns whether own Autocrypt key should be attached to this MDN
|
||||||
match &self.loaded {
|
/// and if so, records the attachment.
|
||||||
Loaded::Message { .. } => false,
|
///
|
||||||
Loaded::Mdn { .. } => true,
|
/// The key is attached to encrypted MDNs
|
||||||
|
/// once per `gossip_period` for each recipient
|
||||||
|
/// and immediately when own key gains a newer self-signature,
|
||||||
|
/// so that contacts we only read messages from
|
||||||
|
/// still learn our current key and relay list
|
||||||
|
/// and will likely re-gossip it to group chats.
|
||||||
|
async fn update_mdn_pubkey_attachment(&self, context: &Context) -> Result<bool> {
|
||||||
|
debug_assert!(
|
||||||
|
self.encryption_pubkeys
|
||||||
|
.as_deref()
|
||||||
|
.is_none_or(|keys| keys.len() <= 1),
|
||||||
|
"MDNs have at most one recipient key; own key is only added at encryption time"
|
||||||
|
);
|
||||||
|
let Some([(_, key)]) = self.encryption_pubkeys.as_deref() else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
let fingerprint = key.dc_fingerprint().hex();
|
||||||
|
let self_key_created = load_self_public_key(context)
|
||||||
|
.await?
|
||||||
|
.details
|
||||||
|
.direct_signatures
|
||||||
|
.iter()
|
||||||
|
.filter_map(|sig| sig.created())
|
||||||
|
.max()
|
||||||
|
.map_or(0, |created| i64::from(created.as_secs()));
|
||||||
|
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
|
||||||
|
let now = time();
|
||||||
|
let attached_timestamp: Option<i64> = context
|
||||||
|
.sql
|
||||||
|
.query_get_value(
|
||||||
|
"SELECT attached_timestamp FROM mdn_autocrypt_timestamp WHERE fingerprint=?",
|
||||||
|
(&fingerprint,),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Attach when our key gained a newer self-signature
|
||||||
|
// (e.g. relay addresses changed) or every `gossip_period`.
|
||||||
|
// If clocks are skewed, attach always.
|
||||||
|
let should_attach = attached_timestamp.is_none_or(|attached_timestamp| {
|
||||||
|
self_key_created > attached_timestamp
|
||||||
|
|| now >= attached_timestamp.saturating_add(gossip_period)
|
||||||
|
|| now < attached_timestamp
|
||||||
|
});
|
||||||
|
if should_attach {
|
||||||
|
// We don't track or care if the MDN fails to be send or received
|
||||||
|
// because attaching a potentially fresh key is only best-effort
|
||||||
|
// and we want to keep the attach-key mechanism simple and localized.
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO mdn_autocrypt_timestamp (fingerprint, attached_timestamp)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT (fingerprint)
|
||||||
|
DO UPDATE SET attached_timestamp=excluded.attached_timestamp",
|
||||||
|
(&fingerprint, now),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
Ok(should_attach)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_attach_profile_data(msg: &Message) -> bool {
|
fn should_attach_profile_data(msg: &Message) -> bool {
|
||||||
@@ -945,11 +1002,13 @@ impl MimeFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let grpimage = self.grpimage();
|
let grpimage = self.grpimage();
|
||||||
let skip_autocrypt = self.should_skip_autocrypt();
|
let should_attach_pubkey = match &self.loaded {
|
||||||
|
Loaded::Message { .. } => true,
|
||||||
|
Loaded::Mdn { .. } => self.update_mdn_pubkey_attachment(context).await?,
|
||||||
|
};
|
||||||
let encrypt_helper = EncryptHelper::new(context).await?;
|
let encrypt_helper = EncryptHelper::new(context).await?;
|
||||||
|
|
||||||
if !skip_autocrypt {
|
if should_attach_pubkey {
|
||||||
// unless determined otherwise we add the Autocrypt header
|
|
||||||
let aheader = encrypt_helper.get_aheader().to_string();
|
let aheader = encrypt_helper.get_aheader().to_string();
|
||||||
headers.push((
|
headers.push((
|
||||||
"Autocrypt",
|
"Autocrypt",
|
||||||
|
|||||||
@@ -327,6 +327,60 @@ async fn test_mdn_create_encrypted() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_mdn_autocrypt_throttle() -> Result<()> {
|
||||||
|
async fn mdn_has_aheader(
|
||||||
|
bob: &TestContext,
|
||||||
|
alice: &TestContext,
|
||||||
|
rcvd: &Message,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let mf = MimeFactory::from_mdn(bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?;
|
||||||
|
let rendered_msg = mf.render(bob).await?;
|
||||||
|
let mime = MimeMessage::from_bytes(alice, rendered_msg.message.as_bytes()).await?;
|
||||||
|
Ok(mime.autocrypt_fingerprint.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = tcm.alice().await;
|
||||||
|
let bob = tcm.bob().await;
|
||||||
|
bob.set_config_bool(Config::MdnsEnabled, true).await?;
|
||||||
|
|
||||||
|
let rcvd = tcm.send_recv_accept(&alice, &bob, "Heyho").await;
|
||||||
|
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
|
||||||
|
|
||||||
|
assert!(mdn_has_aheader(&bob, &alice, &rcvd).await?);
|
||||||
|
assert!(!mdn_has_aheader(&bob, &alice, &rcvd).await?);
|
||||||
|
|
||||||
|
// Own key change forces the header:
|
||||||
|
// a relay list change bumps the transports timestamp
|
||||||
|
// which becomes the key signature timestamp,
|
||||||
|
// so drop the cached self key to re-derive it.
|
||||||
|
SystemTime::shift(Duration::from_secs(100));
|
||||||
|
bob.sql
|
||||||
|
.execute("UPDATE transports SET add_timestamp=?", (time(),))
|
||||||
|
.await?;
|
||||||
|
*bob.self_public_key.lock().await = None;
|
||||||
|
assert!(mdn_has_aheader(&bob, &alice, &rcvd).await?);
|
||||||
|
assert!(!mdn_has_aheader(&bob, &alice, &rcvd).await?);
|
||||||
|
|
||||||
|
// A stored timestamp from the future is ignored
|
||||||
|
// and replaced by one from the current clock.
|
||||||
|
bob.sql
|
||||||
|
.execute(
|
||||||
|
"UPDATE mdn_autocrypt_timestamp SET attached_timestamp=?",
|
||||||
|
(time() + 1000,),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert!(mdn_has_aheader(&bob, &alice, &rcvd).await?);
|
||||||
|
assert!(!mdn_has_aheader(&bob, &alice, &rcvd).await?);
|
||||||
|
|
||||||
|
let gossip_period = bob.get_config_i64(Config::GossipPeriod).await?;
|
||||||
|
SystemTime::shift(Duration::from_secs(gossip_period.try_into()?));
|
||||||
|
assert!(mdn_has_aheader(&bob, &alice, &rcvd).await?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_subject_in_group() -> Result<()> {
|
async fn test_subject_in_group() -> Result<()> {
|
||||||
async fn send_msg_get_subject(
|
async fn send_msg_get_subject(
|
||||||
|
|||||||
@@ -2520,6 +2520,20 @@ UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed.
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inc_and_check(&mut migration_version, 160)?;
|
||||||
|
if dbversion < migration_version {
|
||||||
|
// Tracks when own key was last attached to an MDN
|
||||||
|
// so it is not attached to every MDN.
|
||||||
|
sql.execute_migration(
|
||||||
|
"CREATE TABLE mdn_autocrypt_timestamp (
|
||||||
|
fingerprint TEXT PRIMARY KEY NOT NULL, -- Upper-case fingerprint of the recipient key.
|
||||||
|
attached_timestamp INTEGER NOT NULL
|
||||||
|
) STRICT",
|
||||||
|
migration_version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
let new_version = sql
|
let new_version = sql
|
||||||
.get_raw_config_int(VERSION_CFG)
|
.get_raw_config_int(VERSION_CFG)
|
||||||
.await?
|
.await?
|
||||||
|
|||||||
Reference in New Issue
Block a user