mirror of
https://github.com/chatmail/core.git
synced 2026-10-05 20:50:16 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a31f1441f8 |
@@ -430,6 +430,10 @@ pub enum SystemMessageType {
|
||||
CallEnded,
|
||||
MessagePinned,
|
||||
MessageUnpinned,
|
||||
|
||||
/// Keyupdate message informing contacts about the current key and relay list.
|
||||
/// Never shown in chats: receivers apply the key and then trash the message.
|
||||
Keyupdate,
|
||||
}
|
||||
|
||||
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
@@ -461,6 +465,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
SystemMessage::CallEnded => SystemMessageType::CallEnded,
|
||||
SystemMessage::MessagePinned => SystemMessageType::MessagePinned,
|
||||
SystemMessage::MessageUnpinned => SystemMessageType::MessageUnpinned,
|
||||
SystemMessage::Keyupdate => SystemMessageType::Keyupdate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,6 +366,9 @@ pub enum Config {
|
||||
/// Whether automatic relay management successfully added the desired number of relays
|
||||
AutomaticRelayManagementFinished,
|
||||
|
||||
/// Sorted, comma-separated relay list for which no keyupdate is due.
|
||||
KeyupdateBaseline,
|
||||
|
||||
/// Whether to avoid using IMAP IDLE even if the server supports it.
|
||||
///
|
||||
/// This is a developer option for testing "fake idle".
|
||||
|
||||
+12
-1
@@ -4,7 +4,7 @@ use std::collections::{BTreeMap, HashMap};
|
||||
use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64};
|
||||
use std::sync::{Arc, OnceLock, Weak};
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -332,6 +332,10 @@ pub struct InnerContext {
|
||||
/// `Connectivity` values for published relays, unordered. Used to compute the aggregate connectivity,
|
||||
/// see [`Context::get_connectivity()`].
|
||||
pub(crate) published_connectivities: parking_lot::Mutex<Vec<ConnectivityStore>>,
|
||||
|
||||
/// Timestamp after which the SMTP loop checks for a keyupdate to send, or 0 if none is due.
|
||||
/// Not persistent: losing it only delays the check.
|
||||
pub(crate) keyupdate_check_deadline: AtomicI64,
|
||||
}
|
||||
|
||||
/// The state of ongoing process.
|
||||
@@ -509,6 +513,7 @@ impl Context {
|
||||
self_fingerprint: OnceLock::new(),
|
||||
self_public_key: Mutex::new(None),
|
||||
published_connectivities: parking_lot::Mutex::new(Vec::new()),
|
||||
keyupdate_check_deadline: AtomicI64::new(0),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
@@ -1067,6 +1072,12 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"keyupdate_baseline",
|
||||
self.get_config(Config::KeyupdateBaseline)
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
let elapsed = time_elapsed(&self.creation_time);
|
||||
res.insert("uptime", duration_to_str(elapsed));
|
||||
|
||||
+37
-3
@@ -22,8 +22,8 @@ use crate::chat::ChatId;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::key::{Fingerprint, SignedPublicKey, load_self_secret_keyring};
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, load_self_secret_keyring, self_fingerprint};
|
||||
use crate::pgp::keyupdate_secret;
|
||||
use crate::token::Namespace;
|
||||
|
||||
/// Tries to decrypt the message,
|
||||
@@ -135,7 +135,7 @@ async fn decrypt_session_key_symmetrically(
|
||||
return Ok((plain_session_key, fingerprint));
|
||||
}
|
||||
|
||||
// Finally, try decrypting using own AUTH tokens
|
||||
// Then, try decrypting using own AUTH tokens
|
||||
// There can be a lot of AUTH tokens,
|
||||
// because a new one is generated every time a QR code is shown
|
||||
let res: Option<PlainSessionKey> = try_decrypt_with_auth_token(esk, conn, self_fp)?;
|
||||
@@ -143,6 +143,11 @@ async fn decrypt_session_key_symmetrically(
|
||||
return Ok((plain_session_key, None));
|
||||
}
|
||||
|
||||
// Finally, try decrypting using the keyupdate secrets of the key-contacts
|
||||
if let Some((psk, fingerprint)) = try_decrypt_with_keyupdate_secret(esk, conn)? {
|
||||
return Ok((psk, Some(fingerprint)));
|
||||
}
|
||||
|
||||
bail!("Could not find symmetric secret for session key")
|
||||
})
|
||||
.await
|
||||
@@ -167,6 +172,35 @@ fn try_decrypt_with_bobstate(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Tries to decrypt the session key with the keyupdate secrets derived
|
||||
/// from the keys of the unblocked key-contacts,
|
||||
/// so keyupdates from blocked contacts are not processed.
|
||||
///
|
||||
/// Only reached for keyupdates and undecryptable garbage,
|
||||
/// so any sender can trigger one certificate parse per key-contact.
|
||||
fn try_decrypt_with_keyupdate_secret(
|
||||
esk: &SymKeyEncryptedSessionKey,
|
||||
conn: &mut rusqlite::Connection,
|
||||
) -> Result<Option<(PlainSessionKey, String)>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT c.fingerprint, k.public_key
|
||||
FROM contacts c INNER JOIN public_keys k ON k.fingerprint=c.fingerprint
|
||||
WHERE c.id>9 AND c.blocked=0",
|
||||
)?;
|
||||
let mut rows = stmt.query(())?;
|
||||
while let Some(row) = rows.next()? {
|
||||
let fingerprint: String = row.get(0)?;
|
||||
let public_key_bytes: Vec<u8> = row.get(1)?;
|
||||
if let Ok(key) = SignedPublicKey::from_slice(&public_key_bytes)
|
||||
&& let Ok(secret) = keyupdate_secret(&key)
|
||||
&& let Ok(psk) = decrypt_session_key_with_password(esk, &Password::from(secret))
|
||||
{
|
||||
return Ok(Some((psk, fingerprint)));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn try_decrypt_with_broadcast_secret(
|
||||
esk: &SymKeyEncryptedSessionKey,
|
||||
conn: &mut rusqlite::Connection,
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
//! # Keyupdate messages.
|
||||
//!
|
||||
//! Keyupdates keep chats connected when relays are added or removed,
|
||||
//! by hand today, by automatic relay management later.
|
||||
//! Contacts learn our relay list only from messages carrying our key,
|
||||
//! so without keyupdates, the next message of a "mutually silent" contact
|
||||
//! might go to relays that we no longer read.
|
||||
//! Reliable keyupdates could also speed up recovery from wider-scale network degradation.
|
||||
//!
|
||||
//! When the published relay list changes,
|
||||
//! key-contacts are informed with a keyupdate message:
|
||||
//! one broadcast for all contacts, symmetrically encrypted with a secret
|
||||
//! derived from the own key (`pgp::keyupdate_secret`),
|
||||
//! carrying the re-signed key with its current relay list
|
||||
//! and the protected header `Chat-Content: key-update` ([`render_keyupdate_message`]).
|
||||
//! Receivers trial-decrypt it (`decrypt::try_decrypt_with_keyupdate_secret`),
|
||||
//! apply the key on the normal Autocrypt path
|
||||
//! and only then trash the message without updating the sender's `last_seen`.
|
||||
//! Replaying an old keyupdate cannot revert a relay list,
|
||||
//! because certificate merging keeps the newest direct key signature.
|
||||
//!
|
||||
//! Recipients are the unblocked key-contacts that are members of an accepted chat
|
||||
//! ([`keyupdate_recipients`]), leaving out (potentially many)
|
||||
//! subscribers of our own broadcast channels unless they are in a chat too.
|
||||
//!
|
||||
//! Incoming keyupdates are accepted from any unblocked key-contact
|
||||
//! without checking any further chat state (contact request etc.):
|
||||
//! a stale relay list is worth updating in any case,
|
||||
//! with certificate merging being the final cryptographic guardian.
|
||||
//!
|
||||
//! Sending policy:
|
||||
//!
|
||||
//! - A transport change only schedules a check ([`schedule_keyupdate_check`]),
|
||||
//! setting a deadline that each further change pushes,
|
||||
//! debouncing several changes into a single keyupdate message.
|
||||
//!
|
||||
//! - The SMTP loop sends once the deadline passed and its queue is drained,
|
||||
//! so real messages are never delayed and a keyupdate is only attempted
|
||||
//! when the loop is ready to send: never during send-failure backoff.
|
||||
//!
|
||||
//! - Sending is driven by a diff: a keyupdate is due only while the published
|
||||
//! relay list differs from the recorded [`Config::KeyupdateBaseline`].
|
||||
//! A migration seeds it, so upgrading alone pushes no keyupdate messages.
|
||||
//!
|
||||
//! - Only the device where the change originated sends keyupdates:
|
||||
//! devices applying a multi-device sync changing the relay list
|
||||
//! record the new list as baseline without sending
|
||||
//! ([`set_current_relays_as_keyupdate_baseline`]),
|
||||
//! which also prevents devices catching up on old sync messages
|
||||
//! from sending historical states.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::addr_normalize;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Chattype, DC_CHAT_ID_LAST_SPECIAL};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::message::insert_tombstone;
|
||||
use crate::mimefactory::render_keyupdate_message;
|
||||
use crate::pgp::relay_addrs;
|
||||
use crate::smtp::insert_into_smtp;
|
||||
use crate::tools::{create_outgoing_rfc724_mid, time};
|
||||
|
||||
/// Delay after a transport change, so that one automatic relay addition run yields one message.
|
||||
const KEYUPDATE_DEBOUNCE_SECONDS: i64 = 30;
|
||||
|
||||
/// Returns deduplicated relay addresses of the key-contacts to inform,
|
||||
/// see the module docs for the criteria.
|
||||
async fn keyupdate_recipients(context: &Context) -> Result<Vec<String>> {
|
||||
// Deliberately not narrowed further: selecting by 1:1 chats or by activity
|
||||
// may disclose to the relays which contacts are close ones,
|
||||
// and we want to ensure chat connectivity also between "mutually silent" contacts.
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT c.addr, k.public_key
|
||||
FROM contacts c
|
||||
LEFT JOIN public_keys k ON k.fingerprint=c.fingerprint
|
||||
WHERE c.id>? AND c.fingerprint<>'' AND c.blocked=0
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM chats_contacts cc
|
||||
INNER JOIN chats ch ON ch.id=cc.chat_id
|
||||
WHERE cc.contact_id=c.id AND cc.add_timestamp >= cc.remove_timestamp
|
||||
AND ch.id>? AND ch.type IN (?, ?, ?) AND ch.blocked=0
|
||||
)",
|
||||
(
|
||||
ContactId::LAST_SPECIAL,
|
||||
DC_CHAT_ID_LAST_SPECIAL,
|
||||
Chattype::Single,
|
||||
Chattype::Group,
|
||||
Chattype::InBroadcast,
|
||||
),
|
||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, Option<Vec<u8>>>(1)?)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut recipients = BTreeMap::new();
|
||||
for (addr, public_key) in rows {
|
||||
let public_key = public_key.and_then(|bytes| SignedPublicKey::from_slice(&bytes).ok());
|
||||
for relay in relay_addrs(public_key.as_ref(), &addr) {
|
||||
if !relay.is_empty() {
|
||||
recipients.entry(addr_normalize(&relay)).or_insert(relay);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(recipients.into_values().collect())
|
||||
}
|
||||
|
||||
/// Returns the published relay list in the format stored in [`Config::KeyupdateBaseline`].
|
||||
async fn published_relays_joined(context: &Context) -> Result<String> {
|
||||
let mut relays = context.get_published_self_addrs().await?;
|
||||
relays.sort();
|
||||
Ok(relays.join(","))
|
||||
}
|
||||
|
||||
/// Schedules a check for whether a keyupdate needs sending, after the debounce period.
|
||||
pub(crate) fn schedule_keyupdate_check(context: &Context) {
|
||||
context.keyupdate_check_deadline.store(
|
||||
time().saturating_add(KEYUPDATE_DEBOUNCE_SECONDS),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
|
||||
/// Records the currently published relay list as not needing a keyupdate, see the module docs.
|
||||
pub(crate) async fn set_current_relays_as_keyupdate_baseline(context: &Context) -> Result<()> {
|
||||
let current = published_relays_joined(context).await?;
|
||||
context
|
||||
.set_config_internal(Config::KeyupdateBaseline, Some(¤t))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Sends a keyupdate message if the published relay list differs from the recorded baseline.
|
||||
pub(crate) async fn maybe_send_keyupdate_message(context: &Context) -> Result<()> {
|
||||
let current = published_relays_joined(context).await?;
|
||||
let last = context.get_config(Config::KeyupdateBaseline).await?;
|
||||
if last.unwrap_or_default() == current {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let recipients = keyupdate_recipients(context).await?.join(" ");
|
||||
if !recipients.is_empty() {
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let rendered_message = render_keyupdate_message(context, &rfc724_mid).await?;
|
||||
let msg_id = insert_tombstone(context, &rfc724_mid).await?;
|
||||
insert_into_smtp(context, &rfc724_mid, &recipients, rendered_message, msg_id).await?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
|
||||
// Record only after queueing, so failed queueing is retried by a later check.
|
||||
context
|
||||
.set_config_internal(Config::KeyupdateBaseline, Some(¤t))
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod keyupdate_tests;
|
||||
@@ -0,0 +1,134 @@
|
||||
use super::*;
|
||||
use crate::contact::Contact;
|
||||
use crate::decrypt::get_encrypted_pgp_message_boxed;
|
||||
use crate::pgp::addresses_from_public_key;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::transport::send_sync_transports;
|
||||
use pgp::composed::{Esk, Message};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_keyupdate_recipients() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
let dom = &tcm.dom().await;
|
||||
let elena = &tcm.elena().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
alice.create_chat(bob).await;
|
||||
alice
|
||||
.create_group_with_members("Group", &[charlie, elena])
|
||||
.await;
|
||||
// The group chat stays accepted, so only the contact-level block excludes elena.
|
||||
Contact::block(alice, alice.add_or_lookup_contact_id(elena).await).await?;
|
||||
|
||||
alice
|
||||
.create_broadcast_with_subscribers("Channel", &[dom])
|
||||
.await;
|
||||
|
||||
// Unaccepted contact request.
|
||||
tcm.send_recv(fiona, alice, "hi").await;
|
||||
|
||||
let mut recipients = keyupdate_recipients(alice).await?;
|
||||
recipients.sort();
|
||||
assert_eq!(recipients, ["bob@example.net", "charlie@example.net"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_and_receive_keyupdate() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
alice.create_chat(bob).await;
|
||||
let alice_contact = bob.add_or_lookup_contact(alice).await;
|
||||
assert_eq!(alice_contact.last_seen(), 0);
|
||||
// Create Bob's chat before the transport change: creating it later
|
||||
// would re-import Alice's current key and hide a failing keyupdate.
|
||||
let bob_chat_id = bob.create_chat_id(alice).await;
|
||||
|
||||
alice.add_transport("alice@relay.example.net").await;
|
||||
maybe_send_keyupdate_message(alice).await?;
|
||||
let keyupdate = alice.pop_sent_msg().await;
|
||||
assert_eq!(keyupdate.recipients, "bob@example.net");
|
||||
assert!(keyupdate.payload.contains("Subject: [...]"));
|
||||
|
||||
// One SKESK and no PKESK: nothing leaks about the recipients.
|
||||
let mail = mailparse::parse_mail(keyupdate.payload.as_bytes())?;
|
||||
let msg = get_encrypted_pgp_message_boxed(&mail)?.unwrap();
|
||||
let Message::Encrypted { esk, .. } = &*msg else {
|
||||
panic!("Expected encrypted message");
|
||||
};
|
||||
assert!(matches!(esk[..], [Esk::SymKeyEncryptedSessionKey(_)]));
|
||||
|
||||
bob.recv_msg_trash(&keyupdate).await;
|
||||
let alice_key_at_bob = alice_contact.public_key(bob).await?.unwrap();
|
||||
let addrs = addresses_from_public_key(&alice_key_at_bob).unwrap();
|
||||
assert!(addrs.contains(&"alice@relay.example.net".to_string()));
|
||||
// No green "online" dot, unlike for a regular message, see `test_last_seen()`.
|
||||
let alice_contact = Contact::get_by_id(bob, alice_contact.id).await?;
|
||||
assert_eq!(alice_contact.last_seen(), 0);
|
||||
|
||||
let bob_message = bob.send_text(bob_chat_id, "hi").await;
|
||||
assert!(bob_message.recipients.contains("alice@relay.example.net"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_keyupdate_trigger_dedup() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// No contacts yet, so this only records the baseline.
|
||||
maybe_send_keyupdate_message(alice).await?;
|
||||
assert!(alice.pop_sent_msg_opt().await.is_none());
|
||||
|
||||
alice.create_chat(bob).await;
|
||||
maybe_send_keyupdate_message(alice).await?;
|
||||
assert!(alice.pop_sent_msg_opt().await.is_none());
|
||||
|
||||
alice.add_transport("alice@relay.example.net").await;
|
||||
send_sync_transports(alice).await?;
|
||||
let deadline = alice.keyupdate_check_deadline.load(Ordering::Relaxed);
|
||||
assert!(deadline > time());
|
||||
|
||||
// Sending is driven by the changed relay list, not by the deadline.
|
||||
maybe_send_keyupdate_message(alice).await?;
|
||||
assert!(alice.pop_sent_msg_opt().await.is_some());
|
||||
assert!(alice.pop_sent_msg_opt().await.is_none());
|
||||
maybe_send_keyupdate_message(alice).await?;
|
||||
assert!(alice.pop_sent_msg_opt().await.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_keyupdate_not_sent_by_synced_device() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
for a in [alice, alice2] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
a.set_config_bool(Config::BccSelf, true).await?;
|
||||
// Both devices need a recipient, otherwise silence proves nothing.
|
||||
a.create_chat(bob).await;
|
||||
}
|
||||
|
||||
alice.add_transport("alice@relay.example.net").await;
|
||||
send_sync_transports(alice).await?;
|
||||
alice.send_sync_msg().await?;
|
||||
alice2.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
||||
|
||||
maybe_send_keyupdate_message(alice2).await?;
|
||||
assert!(alice2.pop_sent_msg_opt().await.is_none());
|
||||
maybe_send_keyupdate_message(alice).await?;
|
||||
assert_eq!(alice.pop_sent_msg().await.recipients, "bob@example.net");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -72,6 +72,7 @@ pub mod ephemeral;
|
||||
mod imap;
|
||||
pub mod imex;
|
||||
pub mod key;
|
||||
mod keyupdate;
|
||||
pub mod location;
|
||||
pub mod login_param;
|
||||
pub mod message;
|
||||
|
||||
@@ -1059,6 +1059,7 @@ impl Message {
|
||||
| SystemMessage::CallEnded
|
||||
| SystemMessage::MessagePinned // UI should scroll to pinned message on tapping
|
||||
| SystemMessage::MessageUnpinned // UI should scroll to unpinned message on tapping
|
||||
| SystemMessage::Keyupdate
|
||||
| SystemMessage::Unknown => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
+89
-65
@@ -10,6 +10,8 @@ use deltachat_contact_tools::sanitize_bidi_characters;
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use mail_builder::headers::HeaderType;
|
||||
use mail_builder::headers::address::Address;
|
||||
use mail_builder::headers::raw::Raw;
|
||||
use mail_builder::headers::text::Text;
|
||||
use mail_builder::mime::MimePart;
|
||||
use tokio::fs;
|
||||
|
||||
@@ -32,7 +34,9 @@ use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::peer_channels::{create_iroh_header, get_iroh_topic_for_msg};
|
||||
use crate::pgp::{SeipdVersion, addresses_from_public_key, pubkey_supports_seipdv2};
|
||||
use crate::pgp::{
|
||||
SeipdVersion, addresses_from_public_key, keyupdate_secret, pubkey_supports_seipdv2, relay_addrs,
|
||||
};
|
||||
use crate::simplify::escape_message_footer_marks;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{IsNoneOrEmpty, create_outgoing_rfc724_mid, remove_subject_prefix, time};
|
||||
@@ -578,9 +582,7 @@ impl MimeFactory {
|
||||
|
||||
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
|
||||
|
||||
let relays =
|
||||
addresses_from_public_key(&public_key).unwrap_or_else(|| vec![addr.clone()]);
|
||||
recipients.extend(relays);
|
||||
recipients.extend(relay_addrs(Some(&public_key), &addr));
|
||||
to.push((authname, addr.clone()));
|
||||
|
||||
Encryption::Asymmetric {
|
||||
@@ -897,7 +899,7 @@ impl MimeFactory {
|
||||
}
|
||||
} else if contact.is_key_contact() {
|
||||
let encryption_pubkeys = if let Some(key) = contact.public_key(context).await? {
|
||||
recipients = addresses_from_public_key(&key).unwrap_or_else(|| vec![addr.clone()]);
|
||||
recipients = relay_addrs(Some(&key), &addr);
|
||||
vec![(addr.clone(), key)]
|
||||
} else {
|
||||
Vec::new()
|
||||
@@ -1798,6 +1800,7 @@ impl MimeFactory {
|
||||
SystemMessage::CallEnded => {}
|
||||
SystemMessage::MessagePinned => {}
|
||||
SystemMessage::MessageUnpinned => {}
|
||||
SystemMessage::Keyupdate => {}
|
||||
}
|
||||
|
||||
if command == SystemMessage::GroupDescriptionChanged
|
||||
@@ -2422,6 +2425,42 @@ fn b_encode(value: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the protected headers shared by symmetrically encrypted messages
|
||||
/// that are not part of a chat.
|
||||
async fn symm_encrypted_headers(
|
||||
context: &Context,
|
||||
subject: &str,
|
||||
) -> Result<Vec<(&'static str, HeaderType<'static>)>> {
|
||||
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(time(), 0)
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
let mut headers = vec![
|
||||
("To", Address::new_list(vec![hidden_recipients()]).into()),
|
||||
("Date", Raw::new(date).into()),
|
||||
("Subject", Text::new(subject.to_string()).into()),
|
||||
];
|
||||
// Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
|
||||
if context.get_config_bool(Config::Bot).await? {
|
||||
headers.push(("Auto-Submitted", Raw::new("auto-generated").into()));
|
||||
}
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
/// Renders `queued_mail` for SMTP with the own key pair and primary address.
|
||||
async fn render_with_self_key(context: &Context, queued_mail: QueuedMail) -> Result<String> {
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let secret_key = key::load_self_secret_key(context).await?;
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
let rendered_mail = render_queued_mail(
|
||||
queued_mail,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
from_addr,
|
||||
RenderSideEffects::default(),
|
||||
)?;
|
||||
Ok(rendered_mail.message)
|
||||
}
|
||||
|
||||
pub(crate) async fn render_symm_encrypted_securejoin_message(
|
||||
context: &Context,
|
||||
step: &str,
|
||||
@@ -2434,81 +2473,66 @@ pub(crate) async fn render_symm_encrypted_securejoin_message(
|
||||
|
||||
let message: MimePart<'static> = MimePart::new("text/plain", "Secure-Join");
|
||||
|
||||
let mut headers = Vec::<(&'static str, HeaderType<'static>)>::new();
|
||||
|
||||
let to: Vec<Address<'static>> = vec![hidden_recipients()];
|
||||
headers.push((
|
||||
"To",
|
||||
mail_builder::headers::address::Address::new_list(to.clone()).into(),
|
||||
));
|
||||
|
||||
let timestamp = time();
|
||||
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(timestamp, 0)
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
headers.push(("Date", mail_builder::headers::raw::Raw::new(date).into()));
|
||||
|
||||
headers.push((
|
||||
"Subject",
|
||||
mail_builder::headers::text::Text::new("Secure-Join".to_string()).into(),
|
||||
));
|
||||
|
||||
// Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
|
||||
if context.get_config_bool(Config::Bot).await? {
|
||||
headers.push((
|
||||
"Auto-Submitted",
|
||||
mail_builder::headers::raw::Raw::new("auto-generated".to_string()).into(),
|
||||
));
|
||||
}
|
||||
|
||||
headers.push((
|
||||
"Secure-Join",
|
||||
mail_builder::headers::raw::Raw::new(step.to_string()).into(),
|
||||
));
|
||||
|
||||
headers.push((
|
||||
"Secure-Join-Auth",
|
||||
mail_builder::headers::text::Text::new(auth.to_string()).into(),
|
||||
));
|
||||
let mut headers = symm_encrypted_headers(context, "Secure-Join").await?;
|
||||
headers.push(("Secure-Join", Raw::new(step.to_string()).into()));
|
||||
headers.push(("Secure-Join-Auth", Text::new(auth.to_string()).into()));
|
||||
|
||||
let message = add_headers_to_encrypted_part(message, headers);
|
||||
|
||||
// Disable compression for SecureJoin to ensure
|
||||
// there are no compression side channels
|
||||
// leaking information about the tokens.
|
||||
let should_compress = false;
|
||||
|
||||
// Only sign the message if we attach the pubkey.
|
||||
let should_sign = should_attach_pubkey;
|
||||
|
||||
let raw_message = part_to_bytes(message);
|
||||
|
||||
let queued_mail = QueuedMail {
|
||||
raw_message,
|
||||
raw_message: part_to_bytes(message),
|
||||
display_name: String::new(),
|
||||
rfc724_mid: rfc724_mid.to_string(),
|
||||
encryption: Encryption::Symmetric {
|
||||
shared_secret: shared_secret.to_string(),
|
||||
},
|
||||
should_attach_pubkey,
|
||||
should_sign,
|
||||
should_compress,
|
||||
// Only sign the message if we attach the pubkey.
|
||||
should_sign: should_attach_pubkey,
|
||||
// Disable compression for SecureJoin to ensure
|
||||
// there are no compression side channels
|
||||
// leaking information about the tokens.
|
||||
should_compress: false,
|
||||
};
|
||||
|
||||
render_with_self_key(context, queued_mail).await
|
||||
}
|
||||
|
||||
/// Renders a keyupdate message informing contacts
|
||||
/// about the current key and relay list, see [`crate::keyupdate`].
|
||||
pub(crate) async fn render_keyupdate_message(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
) -> Result<String> {
|
||||
let message: MimePart<'static> = MimePart::new(
|
||||
"text/plain",
|
||||
"This message updates the sender's encryption key and relay list.",
|
||||
);
|
||||
|
||||
let mut headers = symm_encrypted_headers(context, "Keyupdate").await?;
|
||||
headers.push(("Chat-Content", Raw::new("key-update").into()));
|
||||
|
||||
let message = add_headers_to_encrypted_part(message, headers);
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let secret_key = key::load_self_secret_key(context).await?;
|
||||
let side_effects = RenderSideEffects::default();
|
||||
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
let rendered_mail = render_queued_mail(
|
||||
queued_mail,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
from_addr,
|
||||
side_effects,
|
||||
)?;
|
||||
let queued_mail = QueuedMail {
|
||||
raw_message: part_to_bytes(message),
|
||||
display_name: String::new(),
|
||||
rfc724_mid: rfc724_mid.to_string(),
|
||||
encryption: Encryption::Symmetric {
|
||||
shared_secret: keyupdate_secret(&public_key)?,
|
||||
},
|
||||
// The attached key with its relay list notation is the actual payload.
|
||||
should_attach_pubkey: true,
|
||||
// Receivers drop symmetrically encrypted messages
|
||||
// that are not signed by the contact owning the secret.
|
||||
should_sign: true,
|
||||
// Compression normalizes the size: uncompressed, the length grows
|
||||
// linearly with the relay list and leaks its size to relays.
|
||||
should_compress: true,
|
||||
};
|
||||
|
||||
Ok(rendered_mail.message)
|
||||
render_with_self_key(context, queued_mail).await
|
||||
}
|
||||
|
||||
/// Renders MIME part into a vector of bytes.
|
||||
|
||||
@@ -269,6 +269,10 @@ pub enum SystemMessage {
|
||||
|
||||
/// Message unpinned. The unpinned message is referred in `In-Reply-To:` header.
|
||||
MessageUnpinned = 72,
|
||||
|
||||
/// Keyupdate message informing contacts about the current key and relay list.
|
||||
/// Receivers apply the key, then trash the message without updating `last_seen`.
|
||||
Keyupdate = 80,
|
||||
}
|
||||
|
||||
impl MimeMessage {
|
||||
@@ -751,6 +755,8 @@ impl MimeMessage {
|
||||
self.is_system_message = SystemMessage::MessagePinned;
|
||||
} else if value == "message-unpinned" {
|
||||
self.is_system_message = SystemMessage::MessageUnpinned;
|
||||
} else if value == "key-update" {
|
||||
self.is_system_message = SystemMessage::Keyupdate;
|
||||
}
|
||||
} else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
|
||||
self.is_system_message = SystemMessage::MemberRemovedFromGroup;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::*;
|
||||
use crate::chat::{create_broadcast, load_broadcast_secret};
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::key::{load_self_secret_key, self_fingerprint};
|
||||
use crate::contact::Contact;
|
||||
use crate::key::{load_self_public_key, load_self_secret_key, self_fingerprint};
|
||||
use crate::pgp;
|
||||
use crate::qr::{Qr, check_qr};
|
||||
use crate::receive_imf::receive_imf;
|
||||
@@ -248,7 +249,82 @@ async fn test_qr_code_happy_path() -> Result<()> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Control: Test that the behavior is the same when the shared secret is unknown
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_keyupdate_unsigned_rejected() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
bob.add_or_lookup_contact(alice).await;
|
||||
let secret = pgp::keyupdate_secret(&load_self_public_key(alice).await?)?;
|
||||
let alice_addr = alice.get_config(Config::Addr).await?.unwrap();
|
||||
|
||||
test_shared_secret_decryption_ext(
|
||||
bob,
|
||||
&alice_addr,
|
||||
&secret,
|
||||
None,
|
||||
Some("Unsigned message is not allowed to be encrypted with this shared secret"),
|
||||
)
|
||||
.await?;
|
||||
bob.assert_warn("Unsigned message is not allowed to be encrypted with this shared secret")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_keyupdate_wrong_signer_rejected() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await; // Attacker who also has Alice's key
|
||||
|
||||
bob.add_or_lookup_contact(alice).await;
|
||||
let secret = pgp::keyupdate_secret(&load_self_public_key(alice).await?)?;
|
||||
let charlie_addr = charlie.get_config(Config::Addr).await?.unwrap();
|
||||
|
||||
test_shared_secret_decryption_ext(
|
||||
bob,
|
||||
&charlie_addr,
|
||||
&secret,
|
||||
Some(charlie),
|
||||
Some("This sender is not allowed to encrypt with this secret key"),
|
||||
)
|
||||
.await?;
|
||||
bob.assert_warn("This sender is not allowed to encrypt with this secret key")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A blocked contact's keyupdate secret is never tried, so the message stays undecryptable.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_keyupdate_blocked_sender_rejected() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
Contact::block(bob, bob.add_or_lookup_contact_id(alice).await).await?;
|
||||
let secret = pgp::keyupdate_secret(&load_self_public_key(alice).await?)?;
|
||||
let alice_addr = alice.get_config(Config::Addr).await?.unwrap();
|
||||
|
||||
test_shared_secret_decryption_ext(
|
||||
bob,
|
||||
&alice_addr,
|
||||
&secret,
|
||||
Some(alice),
|
||||
Some("Could not find symmetric secret for session key"),
|
||||
)
|
||||
.await?;
|
||||
bob.assert_warn("Could not find symmetric secret for session key")
|
||||
.await;
|
||||
bob.assert_warn("unencrypted message").await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Control: Test that the behavior is the same when the shared secret is unknown.
|
||||
///
|
||||
/// This is also how an old client, or one that does not know the sender's key,
|
||||
/// treats a keyupdate message: the secret cannot be derived, so it is unknown.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unknown_secret() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
+48
-3
@@ -15,12 +15,13 @@ use pgp::crypto::ecc_curve::ECCCurve;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::packet::{Signature, Subpacket, SubpacketData};
|
||||
use pgp::ser::Serialize;
|
||||
use pgp::types::{
|
||||
CompressionAlgorithm, Imprint, KeyDetails, KeyVersion, Password, SignedUser, SigningKey as _,
|
||||
StringToKey,
|
||||
};
|
||||
use rand_old::{Rng as _, thread_rng};
|
||||
use sha2::Sha256;
|
||||
use sha2::{Digest as _, Sha256};
|
||||
|
||||
use crate::configure::MAX_RELAYS;
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
@@ -250,7 +251,7 @@ pub fn pk_validate(
|
||||
}
|
||||
|
||||
/// Symmetrically encrypt the message.
|
||||
/// This is used for broadcast channels and for version 2 of the Securejoin protocol.
|
||||
/// This is used for broadcast channels, Securejoin v3 and `keyupdate` messages.
|
||||
/// `shared secret` is the secret that will be used for symmetric encryption.
|
||||
pub fn symm_encrypt_message(
|
||||
plain: Vec<u8>,
|
||||
@@ -452,6 +453,36 @@ pub(crate) fn addresses_from_public_key(public_key: &SignedPublicKey) -> Option<
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the addresses to reach the owner of `public_key`,
|
||||
/// falling back to `addr` if there is no key
|
||||
/// (as for key-contacts created from a fingerprint alone)
|
||||
/// or if the key carries no relay list.
|
||||
pub(crate) fn relay_addrs(public_key: Option<&SignedPublicKey>, addr: &str) -> Vec<String> {
|
||||
public_key
|
||||
.and_then(addresses_from_public_key)
|
||||
.unwrap_or_else(|| vec![addr.to_string()])
|
||||
}
|
||||
|
||||
/// Returns the symmetric secret for keyupdate messages sent by the owner of `public_key`.
|
||||
///
|
||||
/// The secret is derived from the primary key packet,
|
||||
/// which is immutable and identical in every copy of the key ever handed out:
|
||||
/// this is safe to hash directly because rpgp serializes the packet body without the header,
|
||||
/// the only part that differs between the legacy and current packet format.
|
||||
/// So everyone who ever obtained the key can derive the secret and decrypt, forever.
|
||||
/// That is exactly the intended audience of keyupdates (key-contacts),
|
||||
/// but messages encrypted with this secret must only carry near-public payloads,
|
||||
/// i.e. the key itself and its relay list, never any traces of user content or metadata.
|
||||
///
|
||||
/// The `keyupdate` prefix domain-separates the digest from v6 fingerprints,
|
||||
/// which are also Sha256 hashes over the same key material and travel in invite links.
|
||||
pub(crate) fn keyupdate_secret(public_key: &SignedPublicKey) -> Result<String> {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(b"keyupdate");
|
||||
hasher.update(Serialize::to_bytes(&public_key.primary_key)?);
|
||||
Ok(format!("keyupdate/{}", hex::encode(hasher.finalize())))
|
||||
}
|
||||
|
||||
/// Returns true if public key advertises SEIPDv2 feature.
|
||||
pub(crate) fn pubkey_supports_seipdv2(public_key: &SignedPublicKey) -> bool {
|
||||
// If any Direct Key Signature or any User ID signature has SEIPDv2 feature,
|
||||
@@ -492,7 +523,7 @@ mod tests {
|
||||
decrypt,
|
||||
key::{load_self_public_key, self_fingerprint, store_self_keypair},
|
||||
mimefactory::{part_to_bytes, wrap_encrypted_part},
|
||||
test_utils::{TestContext, TestContextManager, alice_keypair, bob_keypair},
|
||||
test_utils::{TestContext, TestContextManager, alice_keypair, bob_keypair, pqc_keypair},
|
||||
token,
|
||||
};
|
||||
use pgp::composed::{Esk, Message};
|
||||
@@ -848,6 +879,20 @@ mod tests {
|
||||
assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keyupdate_secret() {
|
||||
// Pinned: deployed contacts derive the secret from their stored key copies.
|
||||
// One v4 and one v6 key: the v6 body additionally contains the key material length.
|
||||
assert_eq!(
|
||||
keyupdate_secret(&alice_keypair().to_public_key()).unwrap(),
|
||||
"keyupdate/51d75fab83de11ac2dfbaa4221d4bffe04773da64e3004616f7456f96ae7d6be"
|
||||
);
|
||||
assert_eq!(
|
||||
keyupdate_secret(&pqc_keypair().to_public_key()).unwrap(),
|
||||
"keyupdate/07989f6c74aa553bf1cbed500588fad969be6011ca3ac470781d343f9a2de408"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test PQC support.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_pqc() -> Result<()> {
|
||||
|
||||
+6
-1
@@ -740,7 +740,9 @@ pub(crate) async fn receive_imf_inner(
|
||||
msg
|
||||
};
|
||||
|
||||
if !from_id.is_special() {
|
||||
// A keyupdate is invisible, so refreshing `last_seen` would light up the
|
||||
// sender's green "online" dot with no visible user message sent.
|
||||
if !from_id.is_special() && mime_parser.is_system_message != SystemMessage::Keyupdate {
|
||||
contact::update_last_seen(context, from_id, mime_parser.timestamp_sent).await?;
|
||||
}
|
||||
|
||||
@@ -1153,6 +1155,9 @@ async fn decide_chat_assignment(
|
||||
} else if mime_parser.is_system_message == SystemMessage::MessageUnpinned {
|
||||
info!(context, "Message unpinned (TRASH).");
|
||||
true
|
||||
} else if mime_parser.is_system_message == SystemMessage::Keyupdate {
|
||||
info!(context, "Keyupdate message (TRASH).");
|
||||
true
|
||||
} else if let Some(ref decryption_error) = mime_parser.decryption_error
|
||||
&& !mime_parser.incoming
|
||||
{
|
||||
|
||||
+23
-1
@@ -21,6 +21,7 @@ use crate::download::{download_known_post_messages_without_pre_message, download
|
||||
use crate::ephemeral;
|
||||
use crate::events::EventType;
|
||||
use crate::imap::{Imap, session::Session};
|
||||
use crate::keyupdate::{maybe_send_keyupdate_message, schedule_keyupdate_check};
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::reaction::broadcast_reactions::maybe_broadcast_reactions;
|
||||
@@ -573,6 +574,9 @@ async fn smtp_loop(
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-arm the non-persistent deadline to catch changes lost to a restart.
|
||||
schedule_keyupdate_check(&ctx);
|
||||
|
||||
let mut timeout = None;
|
||||
loop {
|
||||
if let Err(err) = send_smtp_messages(&ctx, &mut connection).await {
|
||||
@@ -626,8 +630,26 @@ async fn smtp_loop(
|
||||
slept.saturating_add(rand::random_range((slept / 2)..=slept)),
|
||||
));
|
||||
} else {
|
||||
// Queue is drained: send a due keyupdate without delaying real messages.
|
||||
let check_deadline = ctx.keyupdate_check_deadline.load(Ordering::Relaxed);
|
||||
let wait = check_deadline.saturating_sub(time());
|
||||
if check_deadline != 0 && wait <= 0 {
|
||||
// Clear first so that a change arriving meanwhile sets a new deadline.
|
||||
ctx.keyupdate_check_deadline.store(0, Ordering::Relaxed);
|
||||
// A failed check is retried at the next loop start or transport change;
|
||||
// re-arming here would retry persistent failures every 30 seconds forever.
|
||||
maybe_send_keyupdate_message(&ctx).await.log_err(&ctx).ok();
|
||||
continue;
|
||||
}
|
||||
|
||||
info!(ctx, "SMTP has no messages to retry, waiting for interrupt.");
|
||||
idle_interrupt_receiver.recv().await.unwrap_or_default();
|
||||
let interrupt = async { idle_interrupt_receiver.recv().await.unwrap_or_default() };
|
||||
if check_deadline != 0 {
|
||||
let duration = std::time::Duration::from_secs(u64::try_from(wait).unwrap_or(1));
|
||||
tokio::time::timeout(duration, interrupt).await.ok();
|
||||
} else {
|
||||
interrupt.await;
|
||||
}
|
||||
};
|
||||
|
||||
info!(ctx, "SMTP fake idle interrupted.")
|
||||
|
||||
+2
-19
@@ -16,11 +16,12 @@ use crate::key;
|
||||
use crate::key::{DcKey, Fingerprint, load_self_public_key, self_fingerprint};
|
||||
use crate::log::LogExt as _;
|
||||
use crate::log::warn;
|
||||
use crate::message::{self, Message, MsgId, Viewtype};
|
||||
use crate::message::{self, Message, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
use crate::qr::check_qr;
|
||||
use crate::securejoin::bob::JoinerProgress;
|
||||
use crate::smtp::insert_into_smtp;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::{create_id, create_outgoing_rfc724_mid, time};
|
||||
use crate::{SecurejoinSource, mimefactory, stats};
|
||||
@@ -731,24 +732,6 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
}
|
||||
}
|
||||
|
||||
async fn insert_into_smtp(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
recipients: &str,
|
||||
rendered_message: String,
|
||||
msg_id: MsgId,
|
||||
) -> Result<(), Error> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
(&rfc724_mid, &recipients, &rendered_message, msg_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Observe self-sent Securejoin message.
|
||||
///
|
||||
/// In a multi-device-setup, there may be other devices that "see" the handshake messages.
|
||||
|
||||
@@ -16,9 +16,8 @@ use crate::message::{self, Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::addresses_from_public_key;
|
||||
use crate::securejoin::{
|
||||
ContactId, encrypted_and_signed, insert_into_smtp, verify_sender_by_fingerprint,
|
||||
};
|
||||
use crate::securejoin::{ContactId, encrypted_and_signed, verify_sender_by_fingerprint};
|
||||
use crate::smtp::insert_into_smtp;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::{create_outgoing_rfc724_mid, time};
|
||||
|
||||
+19
@@ -327,6 +327,25 @@ pub(crate) async fn smtp_send(
|
||||
status
|
||||
}
|
||||
|
||||
/// Inserts a rendered message into the `smtp` table for sending.
|
||||
pub(crate) async fn insert_into_smtp(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
recipients: &str,
|
||||
rendered_message: String,
|
||||
msg_id: MsgId,
|
||||
) -> Result<(), Error> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
(&rfc724_mid, &recipients, &rendered_message, msg_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends message identified by `smtp` table rowid over SMTP connection.
|
||||
///
|
||||
/// Removes row if the message should not be retried, otherwise increments retry count.
|
||||
|
||||
@@ -2610,6 +2610,28 @@ UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed.
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 164)?;
|
||||
if dbversion < migration_version {
|
||||
// Seed the keyupdate baseline so that upgrading alone
|
||||
// sends nothing, see `keyupdate.rs`.
|
||||
sql.execute_migration_transaction(
|
||||
|transaction| {
|
||||
let relays: Vec<String> = transaction
|
||||
.prepare("SELECT addr FROM transports WHERE is_published=1 ORDER BY addr")?
|
||||
.query_map((), |row| row.get(0))?
|
||||
.collect::<rusqlite::Result<_>>()?;
|
||||
transaction.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value)
|
||||
VALUES ('keyupdate_baseline', ?)",
|
||||
(relays.join(","),),
|
||||
)?;
|
||||
Ok(())
|
||||
},
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
@@ -29,6 +29,23 @@ async fn test_clear_config_cache() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_keyupdate_baseline_migration() -> Result<()> {
|
||||
let configured = STOP_MIGRATIONS_AT
|
||||
.scope(163, async move { TestContext::new_alice().await })
|
||||
.await;
|
||||
configured.sql.run_migrations(&configured).await?;
|
||||
let relays = configured.get_config(Config::KeyupdateBaseline).await?;
|
||||
assert_eq!(relays.as_deref(), Some("alice@example.org"));
|
||||
|
||||
// A fresh account is seeded with an empty baseline.
|
||||
let fresh = TestContext::new().await;
|
||||
let relays = fresh.get_config(Config::KeyupdateBaseline).await?;
|
||||
assert_eq!(relays.as_deref(), Some(""));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_key_contacts_migration_autocrypt() -> Result<()> {
|
||||
let t = STOP_MIGRATIONS_AT
|
||||
|
||||
+48
-12
@@ -25,7 +25,8 @@ use uuid::Uuid;
|
||||
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::chat::{
|
||||
self, Chat, ChatId, ChatIdBlocked, MessageListOptions, add_to_chat_contacts_table, create_group,
|
||||
self, Chat, ChatId, ChatIdBlocked, MessageListOptions, add_to_chat_contacts_table,
|
||||
create_broadcast, create_group,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
@@ -200,17 +201,7 @@ impl TestContextManager {
|
||||
test_context.name()
|
||||
));
|
||||
|
||||
// Insert a transport for the new address.
|
||||
test_context.sql
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
|
||||
(
|
||||
new_addr,
|
||||
serde_json::to_string(&EnteredLoginParam{addr: new_addr.to_string(), ..Default::default()}).unwrap(),
|
||||
format!(r#"{{"addr":"{new_addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic"}}"#)
|
||||
),
|
||||
).await.unwrap();
|
||||
|
||||
test_context.add_transport(new_addr).await;
|
||||
test_context.set_primary_self_addr(new_addr).await.unwrap();
|
||||
// ensure_secret_key_exists() is called during configure
|
||||
key::ensure_secret_key_exists(test_context).await.unwrap();
|
||||
@@ -578,6 +569,32 @@ impl TestContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a published transport for `addr` without any network activity.
|
||||
pub async fn add_transport(&self, addr: &str) {
|
||||
// A fresh `add_timestamp` makes the re-signed self key newer than the copies
|
||||
// contacts hold, so that certificate merging prefers the new relay list.
|
||||
self.sql
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO transports (addr, entered_param, configured_param, add_timestamp) VALUES (?, ?, ?, ?)",
|
||||
(
|
||||
addr,
|
||||
serde_json::to_string(&EnteredLoginParam {
|
||||
addr: addr.to_string(),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap(),
|
||||
format!(
|
||||
r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic"}}"#
|
||||
),
|
||||
time(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// Invalidate the cached self key so that it is regenerated with the new list.
|
||||
self.self_public_key.lock().await.take();
|
||||
}
|
||||
|
||||
/// Retrieves a sent message from the jobs table.
|
||||
///
|
||||
/// This retrieves and removes a message which has been scheduled to send from the jobs
|
||||
@@ -1154,6 +1171,25 @@ ORDER BY id"
|
||||
chat_id
|
||||
}
|
||||
|
||||
/// Creates a broadcast channel with `subscribers` added as member rows directly.
|
||||
/// Joining via securejoin instead would also create accepted 1:1 chats with them.
|
||||
pub async fn create_broadcast_with_subscribers(
|
||||
&self,
|
||||
name: &str,
|
||||
subscribers: &[&TestContext],
|
||||
) -> ChatId {
|
||||
let chat_id = create_broadcast(self, name.to_string()).await.unwrap();
|
||||
let mut to_add = vec![];
|
||||
for subscriber in subscribers {
|
||||
to_add.push(self.add_or_lookup_contact_id(subscriber).await);
|
||||
}
|
||||
add_to_chat_contacts_table(self, time(), chat_id, &to_add)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
chat_id
|
||||
}
|
||||
|
||||
/// Set the legacy `protected` column in the chats table to 1,
|
||||
/// because for now, only these chats that were once protected can be used
|
||||
/// to gossip verifications.
|
||||
|
||||
@@ -19,6 +19,7 @@ use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::ensure_and_debug_assert;
|
||||
use crate::events::EventType;
|
||||
use crate::keyupdate::{schedule_keyupdate_check, set_current_relays_as_keyupdate_baseline};
|
||||
use crate::login_param::EnteredLoginParam;
|
||||
use crate::net::load_connection_timestamp;
|
||||
use crate::provider::Socket;
|
||||
@@ -614,6 +615,8 @@ pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
|
||||
removed_transports,
|
||||
})
|
||||
.await?;
|
||||
// Set the deadline before interrupting, so the woken SMTP loop sees it.
|
||||
schedule_keyupdate_check(context);
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
|
||||
Ok(())
|
||||
@@ -678,6 +681,12 @@ pub(crate) async fn sync_transports(
|
||||
.restart_io_after_fetch
|
||||
.store(true, Ordering::Relaxed);
|
||||
context.emit_event(EventType::TransportsModified);
|
||||
|
||||
// Only the originating device sends a keyupdate;
|
||||
// a device ingesting a sync message just records the new baseline, see `keyupdate.rs`.
|
||||
// Acceptable gap: with concurrent changes on two devices,
|
||||
// contacts may only learn the merged list with the next relay change or regular message.
|
||||
set_current_relays_as_keyupdate_baseline(context).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user