diff --git a/src/aheader.rs b/src/aheader.rs index 3b69e09072..86941fbeb5 100644 --- a/src/aheader.rs +++ b/src/aheader.rs @@ -46,21 +46,13 @@ pub struct Aheader { pub addr: String, pub public_key: SignedPublicKey, pub prefer_encrypt: EncryptPreference, -} -impl Aheader { - /// Creates new autocrypt header - pub fn new( - addr: String, - public_key: SignedPublicKey, - prefer_encrypt: EncryptPreference, - ) -> Self { - Aheader { - addr, - public_key, - prefer_encrypt, - } - } + // Whether `_verified` attribute is present. + // + // `_verified` attribute is an extension to `Autocrypt-Gossip` + // header that is used to tell that the sender + // marked this key as verified. + pub verified: bool, } impl fmt::Display for Aheader { @@ -69,6 +61,9 @@ impl fmt::Display for Aheader { if self.prefer_encrypt == EncryptPreference::Mutual { write!(fmt, " prefer-encrypt=mutual;")?; } + if self.verified { + write!(fmt, " _verified=1;")?; + } // adds a whitespace every 78 characters, this allows // email crate to wrap the lines according to RFC 5322 @@ -123,6 +118,8 @@ impl FromStr for Aheader { .and_then(|raw| raw.parse().ok()) .unwrap_or_default(); + let verified = attributes.remove("_verified").is_some(); + // Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored // Autocrypt-Level0: unknown attribute, treat the header as invalid if attributes.keys().any(|k| !k.starts_with('_')) { @@ -133,6 +130,7 @@ impl FromStr for Aheader { addr, public_key, prefer_encrypt, + verified, }) } } @@ -150,6 +148,7 @@ mod tests { assert_eq!(h.addr, "me@mail.com"); assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual); + assert_eq!(h.verified, false); Ok(()) } @@ -243,11 +242,12 @@ mod tests { assert!( format!( "{}", - Aheader::new( - "test@example.com".to_string(), - SignedPublicKey::from_base64(RAWKEY).unwrap(), - EncryptPreference::Mutual - ) + Aheader { + addr: "test@example.com".to_string(), + public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(), + prefer_encrypt: EncryptPreference::Mutual, + verified: false + } ) .contains("prefer-encrypt=mutual;") ); @@ -258,11 +258,12 @@ mod tests { assert!( !format!( "{}", - Aheader::new( - "test@example.com".to_string(), - SignedPublicKey::from_base64(RAWKEY).unwrap(), - EncryptPreference::NoPreference - ) + Aheader { + addr: "test@example.com".to_string(), + public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(), + prefer_encrypt: EncryptPreference::NoPreference, + verified: false + } ) .contains("prefer-encrypt") ); @@ -271,13 +272,27 @@ mod tests { assert!( format!( "{}", - Aheader::new( - "TeSt@eXaMpLe.cOm".to_string(), - SignedPublicKey::from_base64(RAWKEY).unwrap(), - EncryptPreference::Mutual - ) + Aheader { + addr: "TeSt@eXaMpLe.cOm".to_string(), + public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(), + prefer_encrypt: EncryptPreference::Mutual, + verified: false + } ) .contains("test@example.com") ); + + assert!( + format!( + "{}", + Aheader { + addr: "test@example.com".to_string(), + public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(), + prefer_encrypt: EncryptPreference::NoPreference, + verified: true + } + ) + .contains("_verified") + ); } } diff --git a/src/e2ee.rs b/src/e2ee.rs index b683613470..1eabbb4e1c 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -31,9 +31,12 @@ impl EncryptHelper { } pub fn get_aheader(&self) -> Aheader { - let pk = self.public_key.clone(); - let addr = self.addr.to_string(); - Aheader::new(addr, pk, self.prefer_encrypt) + Aheader { + addr: self.addr.clone(), + public_key: self.public_key.clone(), + prefer_encrypt: self.prefer_encrypt, + verified: false, + } } /// Tries to encrypt the passed in `mail`. diff --git a/src/mimefactory.rs b/src/mimefactory.rs index a16420e524..3d940eb6e8 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1092,13 +1092,14 @@ impl MimeFactory { continue; } - let header = Aheader::new( - addr.clone(), - key.clone(), + let header = Aheader { + addr: addr.clone(), + public_key: key.clone(), // Autocrypt 1.1.0 specification says that // `prefer-encrypt` attribute SHOULD NOT be included. - EncryptPreference::NoPreference, - ) + prefer_encrypt: EncryptPreference::NoPreference, + verified: false, + } .to_string(); message = message.header( diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 467a82b336..f64ee0b418 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1,7 +1,7 @@ //! # MIME message parsing module. use std::cmp::min; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::path::Path; use std::str; use std::str::FromStr; @@ -36,6 +36,17 @@ use crate::tools::{ }; use crate::{chatlist_events, location, stock_str, tools}; +/// Public key extracted from `Autocrypt-Gossip` +/// header with associated information. +#[derive(Debug)] +pub struct GossipedKey { + /// Public key extracted from `keydata` attribute. + pub public_key: SignedPublicKey, + + /// True if `Autocrypt-Gossip` has a `_verified` attribute. + pub verified: bool, +} + /// A parsed MIME message. /// /// This represents the relevant information of a parsed MIME message @@ -85,7 +96,7 @@ pub(crate) struct MimeMessage { /// The addresses for which there was a gossip header /// and their respective gossiped keys. - pub gossiped_keys: HashMap, + pub gossiped_keys: BTreeMap, /// Fingerprint of the key in the Autocrypt header. /// @@ -1967,9 +1978,9 @@ async fn parse_gossip_headers( from: &str, recipients: &[SingleInfo], gossip_headers: Vec, -) -> Result> { +) -> Result> { // XXX split the parsing from the modification part - let mut gossiped_keys: HashMap = Default::default(); + let mut gossiped_keys: BTreeMap = Default::default(); for value in &gossip_headers { let header = match value.parse::() { @@ -2011,7 +2022,12 @@ async fn parse_gossip_headers( ) .await?; - gossiped_keys.insert(header.addr.to_lowercase(), header.public_key); + let gossiped_key = GossipedKey { + public_key: header.public_key, + + verified: header.verified, + }; + gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key); } Ok(gossiped_keys) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index c228a1b185..305e5ea5e3 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1,6 +1,6 @@ //! Internet Message Format reception pipeline. -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashSet}; use std::iter; use std::sync::LazyLock; @@ -28,14 +28,14 @@ use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table}; use crate::key::self_fingerprint_opt; -use crate::key::{DcKey, Fingerprint, SignedPublicKey}; +use crate::key::{DcKey, Fingerprint}; use crate::log::LogExt; use crate::log::{info, warn}; use crate::logged_debug_assert; use crate::message::{ self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists, }; -use crate::mimeparser::{AvatarAction, MimeMessage, SystemMessage, parse_message_ids}; +use crate::mimeparser::{AvatarAction, GossipedKey, MimeMessage, SystemMessage, parse_message_ids}; use crate::param::{Param, Params}; use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub}; use crate::reaction::{Reaction, set_msg_reaction}; @@ -835,7 +835,7 @@ pub(crate) async fn receive_imf_inner( context .sql .transaction(move |transaction| { - let fingerprint = gossiped_key.dc_fingerprint().hex(); + let fingerprint = gossiped_key.public_key.dc_fingerprint().hex(); transaction.execute( "INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp) VALUES (?, ?, ?) @@ -2917,7 +2917,7 @@ async fn apply_group_changes( // highest `add_timestamp` to disambiguate. // The result of the error is that info message // may contain display name of the wrong contact. - let fingerprint = key.dc_fingerprint().hex(); + let fingerprint = key.public_key.dc_fingerprint().hex(); if let Some(contact_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? { @@ -3659,10 +3659,28 @@ async fn mark_recipients_as_verified( to_ids: &[Option], mimeparser: &MimeMessage, ) -> Result<()> { + let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF); + for gossiped_key in mimeparser + .gossiped_keys + .values() + .filter(|gossiped_key| gossiped_key.verified) + { + let fingerprint = gossiped_key.public_key.dc_fingerprint().hex(); + let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else { + continue; + }; + + if to_id == ContactId::SELF || to_id == from_id { + continue; + } + + mark_contact_id_as_verified(context, to_id, verifier_id).await?; + ChatId::set_protection_for_contact(context, to_id, mimeparser.timestamp_sent).await?; + } + if mimeparser.get_header(HeaderDef::ChatVerified).is_none() { return Ok(()); } - let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF); for to_id in to_ids.iter().filter_map(|&x| x) { if to_id == ContactId::SELF || to_id == from_id { continue; @@ -3755,7 +3773,7 @@ async fn add_or_lookup_contacts_by_address_list( async fn add_or_lookup_key_contacts( context: &Context, address_list: &[SingleInfo], - gossiped_keys: &HashMap, + gossiped_keys: &BTreeMap, fingerprints: &[Fingerprint], origin: Origin, ) -> Result>> { @@ -3771,7 +3789,7 @@ async fn add_or_lookup_key_contacts( // Iterator has not ran out of fingerprints yet. fp.hex() } else if let Some(key) = gossiped_keys.get(addr) { - key.dc_fingerprint().hex() + key.public_key.dc_fingerprint().hex() } else if context.is_self_addr(addr).await? { contact_ids.push(Some(ContactId::SELF)); continue; diff --git a/src/securejoin.rs b/src/securejoin.rs index 7813c9de5e..888fe05565 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -272,7 +272,9 @@ pub(crate) async fn handle_securejoin_handshake( let mut self_found = false; let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint(); for (addr, key) in &mime_message.gossiped_keys { - if key.dc_fingerprint() == self_fingerprint && context.is_self_addr(addr).await? { + if key.public_key.dc_fingerprint() == self_fingerprint + && context.is_self_addr(addr).await? + { self_found = true; break; } @@ -542,7 +544,7 @@ pub(crate) async fn observe_securejoin_on_other_device( return Ok(HandshakeMessage::Ignore); }; - if key.dc_fingerprint() != contact_fingerprint { + if key.public_key.dc_fingerprint() != contact_fingerprint { // Fingerprint does not match, ignore. warn!(context, "Fingerprint does not match."); return Ok(HandshakeMessage::Ignore); diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index e245e7d8e5..981640640c 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -5,6 +5,7 @@ use crate::chat::{CantSendReason, remove_contact_from_chat}; use crate::chatlist::Chatlist; use crate::constants::Chattype; use crate::key::self_fingerprint; +use crate::mimeparser::GossipedKey; use crate::receive_imf::receive_imf; use crate::stock_str::{self, messages_e2e_encrypted}; use crate::test_utils::{ @@ -185,7 +186,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) { ); if case == SetupContactCase::WrongAliceGossip { - let wrong_pubkey = load_self_public_key(&bob).await.unwrap(); + let wrong_pubkey = GossipedKey { + public_key: load_self_public_key(&bob).await.unwrap(), + verified: false, + }; let alice_pubkey = msg .gossiped_keys .insert(alice_addr.to_string(), wrong_pubkey)