diff --git a/deltachat-rpc-client/tests/test_multitransport.py b/deltachat-rpc-client/tests/test_multitransport.py index 4ccba8474..ae43c65ed 100644 --- a/deltachat-rpc-client/tests/test_multitransport.py +++ b/deltachat-rpc-client/tests/test_multitransport.py @@ -315,11 +315,10 @@ def test_transport_limit(acfactory) -> None: account.add_transport_from_qr(qr) -def test_message_info_imap_urls(acfactory, log) -> None: +def test_message_info_imap_urls(acfactory) -> None: """Test that message info contains IMAP URLs of where the message was received.""" alice, bob = acfactory.get_online_accounts(2) - log.section("Alice adds ac1 clone removes second transport") qr = acfactory.get_account_qr() for i in range(3): alice.add_transport_from_qr(qr) @@ -327,9 +326,6 @@ def test_message_info_imap_urls(acfactory, log) -> None: for _ in range(i + 1): alice.bring_online() - new_alice_addr = alice.list_transports()[2]["addr"] - alice.set_config("configured_addr", new_alice_addr) - # Enable multi-device mode so messages are not deleted immediately. alice.set_config("bcc_self", "1") @@ -337,12 +333,51 @@ def test_message_info_imap_urls(acfactory, log) -> None: # This is where he will send the message. bob_chat = bob.create_chat(alice) - # Alice changes the transport again. - alice.set_config("configured_addr", alice.list_transports()[3]["addr"]) + # Alice switches to another transport and removes the rest of the transports. + new_alice_addr = alice.list_transports()[1]["addr"] + alice.set_config("configured_addr", new_alice_addr) + removed_addrs = [] + for transport in alice.list_transports(): + if transport["addr"] != new_alice_addr: + alice.delete_transport(transport["addr"]) + removed_addrs.append(transport["addr"]) + alice.stop_io() + alice.start_io() bob_chat.send_text("Hello!") msg = alice.wait_for_incoming_msg() - for alice_transport in alice.list_transports(): - addr = alice_transport["addr"] - assert (addr == new_alice_addr) == (addr in msg.get_info()) + msg_info = msg.get_info() + assert new_alice_addr in msg_info + for removed_addr in removed_addrs: + assert removed_addr not in msg_info + assert f"{new_alice_addr}/INBOX" in msg_info + + +def test_remove_primary_transport(acfactory) -> None: + """Test that after removing the primary relay, Alice can still receive messages.""" + alice, bob = acfactory.get_online_accounts(2) + qr = acfactory.get_account_qr() + + alice.add_transport_from_qr(qr) + alice.bring_online() + + bob_chat = bob.create_chat(alice) + alice.create_chat(bob) + + # Alice changes the transport. + [transport1, transport2] = alice.list_transports() + alice.set_config("configured_addr", transport2["addr"]) + + bob_chat.send_text("Hello!") + msg1 = alice.wait_for_incoming_msg().get_snapshot() + assert msg1.text == "Hello!" + + # Alice deletes the first transport. + alice.delete_transport(transport1["addr"]) + alice.stop_io() + alice.start_io() + + bob_chat.send_text("Hello again!") + msg2 = alice.wait_for_incoming_msg().get_snapshot() + assert msg2.text == "Hello again!" diff --git a/src/chat.rs b/src/chat.rs index e402a3753..2dd153570 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -42,6 +42,7 @@ use crate::message::{self, Message, MessageState, MsgId, Viewtype}; use crate::mimefactory::{MimeFactory, RenderedEmail}; use crate::mimeparser::SystemMessage; use crate::param::{Param, Params}; +use crate::pgp::addresses_from_public_key; use crate::receive_imf::ReceivedMsg; use crate::smtp::{self, send_msg_to_smtp}; use crate::stock_str; @@ -1174,8 +1175,13 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=? let fingerprint = contact .fingerprint() .context("Contact does not have a fingerprint in encrypted chat")?; - if contact.public_key(context).await?.is_some() { - ret += &format!("\n{addr}\n{fingerprint}\n"); + if let Some(public_key) = contact.public_key(context).await? { + if let Some(relay_addrs) = addresses_from_public_key(&public_key) { + let relays = relay_addrs.join(","); + ret += &format!("\n{addr}({relays})\n{fingerprint}\n"); + } else { + ret += &format!("\n{addr}\n{fingerprint}\n"); + } } else { ret += &format!("\n{addr}\n(key missing)\n{fingerprint}\n"); } diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 32df45b08..0a53709b6 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -3856,6 +3856,7 @@ async fn test_only_broadcast_owner_can_send_2() -> Result<()> { tcm.section("Now, Alice's fingerprint changes"); alice.sql.execute("DELETE FROM keypairs", ()).await?; + *alice.self_public_key.lock().await = None; alice .sql .execute("DELETE FROM config WHERE keyname='key_id'", ()) @@ -4046,7 +4047,7 @@ async fn test_chat_get_encryption_info() -> Result<()> { chat_id.get_encryption_info(alice).await?, "Messages are end-to-end encrypted.\n\ \n\ - bob@example.net\n\ + bob@example.net(bob@example.net)\n\ CCCB 5AA9 F6E1 141C 9431\n\ 65F1 DB18 B18C BCF7 0487" ); @@ -4056,11 +4057,11 @@ async fn test_chat_get_encryption_info() -> Result<()> { chat_id.get_encryption_info(alice).await?, "Messages are end-to-end encrypted.\n\ \n\ - fiona@example.net\n\ + fiona@example.net(fiona@example.net)\n\ C8BA 50BF 4AC1 2FAF 38D7\n\ F657 DDFC 8E9F 3C79 9195\n\ \n\ - bob@example.net\n\ + bob@example.net(bob@example.net)\n\ CCCB 5AA9 F6E1 141C 9431\n\ 65F1 DB18 B18C BCF7 0487" ); diff --git a/src/contact.rs b/src/contact.rs index ba2d26180..8eea7cee7 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -35,6 +35,7 @@ use crate::log::{LogExt, warn}; use crate::message::MessageState; use crate::mimeparser::AvatarAction; use crate::param::{Param, Params}; +use crate::pgp::{addresses_from_public_key, merge_openpgp_certificates}; use crate::sync::{self, Sync::*}; use crate::tools::{SystemTime, duration_to_str, get_abs_path, normalize_text, time, to_lowercase}; use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str}; @@ -314,6 +315,67 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result Result<()> { + public_key + .verify_bindings() + .context("Attempt to import broken public key")?; + + let fingerprint = public_key.dc_fingerprint().hex(); + + let merged_public_key; + let merged_public_key_ref = if let Some(public_key_bytes) = context + .sql + .query_row_optional( + "SELECT public_key + FROM public_keys + WHERE fingerprint=?", + (&fingerprint,), + |row| { + let bytes: Vec = row.get(0)?; + Ok(bytes) + }, + ) + .await? + { + let old_public_key = SignedPublicKey::from_slice(&public_key_bytes)?; + merged_public_key = merge_openpgp_certificates(public_key.clone(), old_public_key) + .context("Failed to merge public keys")?; + &merged_public_key + } else { + public_key + }; + + let inserted = context + .sql + .execute( + "INSERT INTO public_keys (fingerprint, public_key) + VALUES (?, ?) + ON CONFLICT (fingerprint) + DO UPDATE SET public_key=excluded.public_key + WHERE public_key!=excluded.public_key", + (&fingerprint, merged_public_key_ref.to_bytes()), + ) + .await?; + if inserted > 0 { + info!( + context, + "Saved key with fingerprint {fingerprint} from the Autocrypt header" + ); + } + + Ok(()) +} + /// Imports contacts from the given vCard. /// /// Returns the ids of successfully processed contacts in the order they appear in `vcard`, @@ -352,23 +414,14 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu .ok() }); - let fingerprint; - if let Some(public_key) = key { - fingerprint = public_key.dc_fingerprint().hex(); - - context - .sql - .execute( - "INSERT INTO public_keys (fingerprint, public_key) - VALUES (?, ?) - ON CONFLICT (fingerprint) - DO NOTHING", - (&fingerprint, public_key.to_bytes()), - ) - .await?; + let fingerprint = if let Some(public_key) = key { + import_public_key(context, &public_key) + .await + .context("Failed to import public key from vCard")?; + public_key.dc_fingerprint().hex() } else { - fingerprint = String::new(); - } + String::new() + }; let (id, modified) = match Contact::add_or_lookup_ex(context, &contact.authname, &addr, &fingerprint, origin) @@ -1384,6 +1437,16 @@ WHERE addr=? ); } + if let Some(public_key) = contact.public_key(context).await? + && let Some(relay_addrs) = addresses_from_public_key(&public_key) + { + ret += "\n\nRelays:"; + for relay in &relay_addrs { + ret += "\n"; + ret += relay; + } + } + Ok(ret) } diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index fd3aa66b9..6b6dae38a 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -841,7 +841,10 @@ Me (alice@example.org): bob@example.net (bob@example.net): CCCB 5AA9 F6E1 141C 9431 -65F1 DB18 B18C BCF7 0487" +65F1 DB18 B18C BCF7 0487 + +Relays: +bob@example.net" ); let contact = Contact::get_by_id(alice, contact_bob_id).await?; assert!(contact.e2ee_avail(alice).await?); diff --git a/src/context.rs b/src/context.rs index a7070d464..adcb3b2a7 100644 --- a/src/context.rs +++ b/src/context.rs @@ -10,6 +10,7 @@ use std::time::Duration; use anyhow::{Result, bail, ensure}; use async_channel::{self as channel, Receiver, Sender}; +use pgp::composed::SignedPublicKey; use ratelimit::Ratelimit; use tokio::sync::{Mutex, Notify, RwLock}; @@ -233,8 +234,6 @@ pub struct InnerContext { /// This is a global mutex-like state for operations which should be modal in the /// clients. running_state: RwLock, - /// Mutex to avoid generating the key for the user more than once. - pub(crate) generating_key_mutex: Mutex<()>, /// Mutex to enforce only a single running oauth2 is running. pub(crate) oauth2_mutex: Mutex<()>, /// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messages being sent. @@ -317,6 +316,13 @@ pub struct InnerContext { /// the standard library's OnceLock is enough, and it's a lot smaller in memory. pub(crate) self_fingerprint: OnceLock, + /// OpenPGP certificate aka Transferrable Public Key. + /// + /// It is generated on first use from the secret key stored in the database. + /// + /// Mutex is also held while generating the key to avoid generating the key twice. + pub(crate) self_public_key: Mutex>, + /// `Connectivity` values for mailboxes, unordered. Used to compute the aggregate connectivity, /// see [`Context::get_connectivity()`]. pub(crate) connectivities: parking_lot::Mutex>, @@ -486,7 +492,6 @@ impl Context { running_state: RwLock::new(Default::default()), sql: Sql::new(dbfile), smeared_timestamp: SmearedTimestamp::new(), - generating_key_mutex: Mutex::new(()), oauth2_mutex: Mutex::new(()), wrong_pw_warning_mutex: Mutex::new(()), housekeeping_mutex: Mutex::new(()), @@ -508,6 +513,7 @@ impl Context { tls_session_store: TlsSessionStore::new(), iroh: Arc::new(RwLock::new(None)), self_fingerprint: OnceLock::new(), + self_public_key: Mutex::new(None), connectivities: parking_lot::Mutex::new(Vec::new()), pre_encrypt_mime_hook: None.into(), }; diff --git a/src/key.rs b/src/key.rs index 54d20b6a7..101c03518 100644 --- a/src/key.rs +++ b/src/key.rs @@ -7,10 +7,18 @@ use std::io::Cursor; use anyhow::{Context as _, Result, bail, ensure}; use base64::Engine as _; use deltachat_contact_tools::EmailAddress; -use pgp::composed::Deserializable; +use pgp::composed::{Deserializable, SignedKeyDetails}; pub use pgp::composed::{SignedPublicKey, SignedSecretKey}; +use pgp::crypto::aead::AeadAlgorithm; +use pgp::crypto::hash::HashAlgorithm; +use pgp::crypto::sym::SymmetricKeyAlgorithm; +use pgp::packet::{ + Features, KeyFlags, Notation, PacketTrait as _, SignatureConfig, SignatureType, Subpacket, + SubpacketData, +}; use pgp::ser::Serialize; -use pgp::types::KeyDetails; +use pgp::types::{CompressionAlgorithm, KeyDetails, KeyVersion}; +use rand_old::thread_rng; use tokio::runtime::Handle; use crate::context::Context; @@ -114,10 +122,149 @@ pub trait DcKey: Serialize + Deserializable + Clone { fn is_private() -> bool; } +/// Converts secret key to public key. +pub(crate) fn secret_key_to_public_key( + context: &Context, + mut signed_secret_key: SignedSecretKey, + timestamp: u32, + addr: &str, + relay_addrs: &str, +) -> Result { + info!(context, "Converting secret key to public key."); + let timestamp = pgp::types::Timestamp::from_secs(timestamp); + + // Subpackets that we want to share between DKS and User ID signature. + let common_subpackets = || -> Result> { + let keyflags = { + let mut keyflags = KeyFlags::default(); + keyflags.set_certify(true); + keyflags.set_sign(true); + keyflags + }; + let features = { + let mut features = Features::default(); + features.set_seipd_v1(true); + features.set_seipd_v2(true); + features + }; + + Ok(vec![ + Subpacket::regular(SubpacketData::SignatureCreationTime(timestamp))?, + Subpacket::regular(SubpacketData::IssuerFingerprint( + signed_secret_key.fingerprint(), + ))?, + Subpacket::regular(SubpacketData::KeyFlags(keyflags))?, + Subpacket::regular(SubpacketData::Features(features))?, + Subpacket::regular(SubpacketData::PreferredSymmetricAlgorithms(smallvec![ + SymmetricKeyAlgorithm::AES256, + SymmetricKeyAlgorithm::AES192, + SymmetricKeyAlgorithm::AES128 + ]))?, + Subpacket::regular(SubpacketData::PreferredHashAlgorithms(smallvec![ + HashAlgorithm::Sha256, + HashAlgorithm::Sha384, + HashAlgorithm::Sha512, + HashAlgorithm::Sha224, + ]))?, + Subpacket::regular(SubpacketData::PreferredCompressionAlgorithms(smallvec![ + CompressionAlgorithm::ZLIB, + CompressionAlgorithm::ZIP, + ]))?, + Subpacket::regular(SubpacketData::PreferredAeadAlgorithms(smallvec![( + SymmetricKeyAlgorithm::AES256, + AeadAlgorithm::Ocb + )]))?, + Subpacket::regular(SubpacketData::IsPrimary(true))?, + ]) + }; + + // RFC 4880 required that Transferrable Public Key (aka OpenPGP Certificate) + // contains at least one User ID: + // + // RFC 9580 does not require User ID even for V4 certificates anymore: + // + // + // We do not use and do not expect User ID in any keys, + // but nevertheless include User ID in V4 keys for compatibility with clients that follow RFC 4880. + // RFC 9580 also recommends including User ID into V4 keys: + // + // + // We do not support keys older than V4 and are not going + // to include User ID in newer V6 keys as all clients that support V6 + // should support keys without User ID. + let users = if signed_secret_key.version() == KeyVersion::V4 { + let user_id = format!("<{addr}>"); + + let mut rng = thread_rng(); + // Self-signature is a "positive certification", + // see . + let mut user_id_signature_config = SignatureConfig::from_key( + &mut rng, + &signed_secret_key.primary_key, + SignatureType::CertPositive, + )?; + user_id_signature_config.hashed_subpackets = common_subpackets()?; + user_id_signature_config.unhashed_subpackets = vec![Subpacket::regular( + SubpacketData::IssuerKeyId(signed_secret_key.legacy_key_id()), + )?]; + let user_id_packet = + pgp::packet::UserId::from_str(pgp::types::PacketHeaderVersion::New, &user_id)?; + let signature = user_id_signature_config.sign_certification( + &signed_secret_key.primary_key, + &signed_secret_key.primary_key.public_key(), + &pgp::types::Password::empty(), + user_id_packet.tag(), + &user_id_packet, + )?; + vec![user_id_packet.into_signed(signature)] + } else { + vec![] + }; + + let direct_signatures = { + let mut rng = thread_rng(); + let mut direct_key_signature_config = SignatureConfig::from_key( + &mut rng, + &signed_secret_key.primary_key, + SignatureType::Key, + )?; + direct_key_signature_config.hashed_subpackets = common_subpackets()?; + let notation = Notation { + readable: true, + name: "relays@chatmail.at".into(), + value: relay_addrs.to_string().into(), + }; + direct_key_signature_config + .hashed_subpackets + .push(Subpacket::regular(SubpacketData::Notation(notation))?); + let direct_key_signature = direct_key_signature_config.sign_key( + &signed_secret_key.primary_key, + &pgp::types::Password::empty(), + signed_secret_key.primary_key.public_key(), + )?; + vec![direct_key_signature] + }; + + signed_secret_key.details = SignedKeyDetails { + revocation_signatures: vec![], + direct_signatures, + users, + user_attributes: vec![], + }; + + Ok(signed_secret_key.to_public_key()) +} + /// Attempts to load own public key. /// -/// Returns `None` if no key is generated yet. +/// Returns `None` if no secret key is generated yet. pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result> { + let mut lock = context.self_public_key.lock().await; + + if let Some(ref public_key) = *lock { + return Ok(Some(public_key.clone())); + } + let Some(secret_key_bytes) = context .sql .query_row_optional( @@ -135,7 +282,25 @@ pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result