diff --git a/CHANGELOG.md b/CHANGELOG.md index 787d93d2f..8edd9f065 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### API-Changes - jsonrpc: add python API for webxdc updates #3872 +- Add ffi functions to retrieve `verified by` information #3786 ### Fixes - Do not add an error if the message is encrypted but not signed #3860 @@ -26,8 +27,6 @@ - Only send the message about ephemeral timer change if the chat is promoted #3847 - Use relative paths in `accounts.toml` #3838 -### API-Changes - ### Fixes - Set read/write timeouts for IMAP over SOCKS5 #3833 - Treat attached PGP keys as peer keys with mutual encryption preference #3832 diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 95e4cf45f..4270d0003 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -4735,6 +4735,37 @@ int dc_contact_is_blocked (const dc_contact_t* contact); int dc_contact_is_verified (dc_contact_t* contact); + +/** + * Return the address that verified a contact + * + * The UI may use this in addition to a checkmark showing the verification status + * + * @memberof dc_contact_t + * @param contact The contact object. + * @return + * A string containing the verifiers address. If it is the same address as the contact itself, + * we verified the contact ourself. If it is an empty string, we don't have verifier + * information or the contact is not verified. + */ +char* dc_contact_get_verifier_addr (dc_contact_t* contact); + + +/** + * Return the `ContactId` that verified a contact + * + * The UI may use this in addition to a checkmark showing the verification status + * + * @memberof dc_contact_t + * @param contact The contact object. + * @return + * The `ContactId` of the verifiers address. If it is the same address as the contact itself, + * we verified the contact ourself. If it is 0, we don't have verifier information or + * the contact is not verified. + */ +int dc_contact_get_verifier_id (dc_contact_t* contact); + + /** * @class dc_provider_t * diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index a3b772c28..51a5d3353 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3963,6 +3963,40 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l .unwrap_or_default() as libc::c_int } +#[no_mangle] +pub unsafe extern "C" fn dc_contact_get_verifier_addr( + contact: *mut dc_contact_t, +) -> *mut libc::c_char { + if contact.is_null() { + eprintln!("ignoring careless call to dc_contact_get_verifier_addr()"); + return "".strdup(); + } + let ffi_contact = &*contact; + let ctx = &*ffi_contact.context; + block_on(Contact::get_verifier_addr( + ctx, + &ffi_contact.contact.get_id(), + )) + .log_err(ctx, "failed to get verifier for contact") + .unwrap_or_default() + .strdup() +} + +#[no_mangle] +pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> libc::c_int { + if contact.is_null() { + eprintln!("ignoring careless call to dc_contact_get_verifier_id()"); + return 0 as libc::c_int; + } + let ffi_contact = &*contact; + let ctx = &*ffi_contact.context; + let contact_id = block_on(Contact::get_verifier_id(ctx, &ffi_contact.contact.get_id())) + .log_err(ctx, "failed to get verifier") + .unwrap_or_default() + .unwrap_or_default(); + + contact_id.to_u32() as libc::c_int +} // dc_lot_t pub type dc_lot_t = lot::Lot; diff --git a/python/src/deltachat/contact.py b/python/src/deltachat/contact.py index 69fa809b6..75c44b8e5 100644 --- a/python/src/deltachat/contact.py +++ b/python/src/deltachat/contact.py @@ -75,6 +75,10 @@ class Contact(object): """Return True if the contact is verified.""" return lib.dc_contact_is_verified(self._dc_contact) + def get_verifier(self, contact): + """Return the address of the contact that verified the contact""" + return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact)) + def get_profile_image(self) -> Optional[str]: """Get contact profile image. diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index 335d07590..034839ee5 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -123,6 +123,7 @@ class TestGroupStressTests: def test_qr_verified_group_and_chatting(acfactory, lp): ac1, ac2, ac3 = acfactory.get_online_accounts(3) + ac1_addr = ac1.get_self_contact().addr lp.sec("ac1: create verified-group QR, ac2 scans and joins") chat1 = ac1.create_group_chat("hello", verified=True) assert chat1.is_protected() @@ -141,12 +142,17 @@ def test_qr_verified_group_and_chatting(acfactory, lp): msg_out = chat1.send_text("hello") assert msg_out.is_encrypted() - lp.sec("ac2: read message and check it's verified chat") + lp.sec("ac2: read message and check that it's a verified chat") msg = ac2._evtracker.wait_next_incoming_message() assert msg.text == "hello" assert msg.chat.is_protected() assert msg.is_encrypted() + lp.sec("ac2: Check that ac2 verified ac1") + # If we verified the contact ourselves then verifier addr == contact addr + ac2_ac1_contact = ac2.get_contacts()[0] + assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr + lp.sec("ac2: send message and let ac1 read it") chat2.send_text("world") msg = ac1._evtracker.wait_next_incoming_message() @@ -168,6 +174,12 @@ def test_qr_verified_group_and_chatting(acfactory, lp): assert msg.is_system_message() assert not msg.error + lp.sec("ac2: Check that ac1 verified ac3 for ac2") + ac2_ac1_contact = ac2.get_contacts()[0] + assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr + ac2_ac3_contact = ac2.get_contacts()[1] + assert ac2.get_self_contact().get_verifier(ac2_ac3_contact) == ac1_addr + lp.sec("ac2: send message and let ac3 read it") chat2.send_text("hi") # Skip system message about added member diff --git a/src/chat.rs b/src/chat.rs index 22818dda0..8be73e1ef 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3562,6 +3562,7 @@ mod tests { use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS}; use crate::contact::Contact; use crate::receive_imf::receive_imf; + use crate::test_utils::TestContext; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/src/contact.rs b/src/contact.rs index 17b74987d..baf74b4ac 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1138,6 +1138,31 @@ impl Contact { Ok(VerifiedStatus::Unverified) } + /// Return the address that verified the given contact + pub async fn get_verifier_addr( + context: &Context, + contact_id: &ContactId, + ) -> Result> { + let contact = Contact::load_from_db(context, *contact_id).await?; + + Ok(Peerstate::from_addr(context, contact.get_addr()) + .await? + .and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned()))) + } + + pub async fn get_verifier_id( + context: &Context, + contact_id: &ContactId, + ) -> Result> { + let verifier_addr = Contact::get_verifier_addr(context, contact_id).await?; + if let Some(addr) = verifier_addr { + Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?) + } else { + Ok(None) + } + } + + /// Return the ContactId that verified the given contact pub async fn get_real_cnt(context: &Context) -> Result { if !context.sql.is_open().await { return Ok(0); @@ -2300,7 +2325,6 @@ bob@example.net: CCCB 5AA9 F6E1 141C 9431 65F1 DB18 B18C BCF7 0487" ); - Ok(()) } diff --git a/src/e2ee.rs b/src/e2ee.rs index 311894a04..62f699fdf 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -297,6 +297,7 @@ Sent with my Delta Chat Messenger: https://delta.chat"; verified_key: Some(pub_key.clone()), verified_key_fingerprint: Some(pub_key.fingerprint()), fingerprint_changed: false, + verifier: None, }; vec![(Some(peerstate), addr)] } diff --git a/src/lib.rs b/src/lib.rs index 771783ba6..a672ec7c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,10 +21,7 @@ clippy::bool_assert_comparison, clippy::manual_split_once, clippy::format_push_string, - clippy::bool_to_int_with_if, - // This lint can be re-enabled once we don't target - // Rust 1.56 anymore: - clippy::collapsible_str_replace + clippy::bool_to_int_with_if )] #[macro_use] diff --git a/src/location.rs b/src/location.rs index c6845898a..e381039ac 100644 --- a/src/location.rs +++ b/src/location.rs @@ -100,11 +100,7 @@ impl Kml { if self.tag.contains(KmlTag::WHEN) || self.tag.contains(KmlTag::COORDINATES) { let val = event.unescape_and_decode(reader).unwrap_or_default(); - let val = val - .replace('\n', "") - .replace('\r', "") - .replace('\t', "") - .replace(' ', ""); + let val = val.replace(['\n', '\r', '\t', ' '], ""); if self.tag.contains(KmlTag::WHEN) && val.len() >= 19 { // YYYY-MM-DDTHH:MM:SSZ diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 5d43db545..843053930 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1635,6 +1635,8 @@ impl MimeMessage { } /// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates. +/// Params: +/// from: The address which sent the message currently beeing parsed /// /// Returns the set of mail recipient addresses for which valid gossip headers were found. async fn update_gossip_peerstates( diff --git a/src/peerstate.rs b/src/peerstate.rs index ab6d527a3..2699bf6eb 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -48,6 +48,8 @@ pub struct Peerstate { pub verified_key: Option, pub verified_key_fingerprint: Option, pub fingerprint_changed: bool, + /// The address that verified this contact + pub verifier: Option, } impl PartialEq for Peerstate { @@ -103,9 +105,11 @@ impl Peerstate { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, } } + /// Create peerstate from gossip pub fn from_gossip(gossip_header: &Aheader, message_time: i64) -> Self { Peerstate { addr: gossip_header.addr.clone(), @@ -119,7 +123,6 @@ impl Peerstate { // learn encryption preferences of other members immediately and don't send unencrypted // messages to a group where everyone prefers encryption. prefer_encrypt: gossip_header.prefer_encrypt, - public_key: None, public_key_fingerprint: None, gossip_key: Some(gossip_header.public_key.clone()), @@ -128,13 +131,14 @@ impl Peerstate { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, } } pub async fn from_addr(context: &Context, addr: &str) -> Result> { let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ - verified_key, verified_key_fingerprint \ + verified_key, verified_key_fingerprint, verifier \ FROM acpeerstates \ WHERE addr=? COLLATE NOCASE LIMIT 1;"; Self::from_stmt(context, query, paramsv![addr]).await @@ -146,7 +150,7 @@ impl Peerstate { ) -> Result> { let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ - verified_key, verified_key_fingerprint \ + verified_key, verified_key_fingerprint, verifier \ FROM acpeerstates \ WHERE public_key_fingerprint=? \ OR gossip_key_fingerprint=? \ @@ -162,7 +166,7 @@ impl Peerstate { ) -> Result> { let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ - verified_key, verified_key_fingerprint \ + verified_key, verified_key_fingerprint, verifier \ FROM acpeerstates \ WHERE verified_key_fingerprint=? \ OR addr=? COLLATE NOCASE \ @@ -219,6 +223,7 @@ impl Peerstate { .transpose() .unwrap_or_default(), fingerprint_changed: false, + verifier: row.get("verifier")?, }; Ok(res) @@ -358,11 +363,19 @@ impl Peerstate { } } + /// Set this peerstate to verified + /// Make sure to call `self.save_to_db` to save these changes + /// Params: + /// verifier: + /// The address which verifies the given contact + /// If we are verifying the contact, use that contacts address + /// Returns whether the value of the key has changed pub fn set_verified( &mut self, which_key: PeerstateKeyType, fingerprint: &Fingerprint, verified: PeerstateVerifiedStatus, + verifier: String, ) -> bool { if verified == PeerstateVerifiedStatus::BidirectVerified { match which_key { @@ -372,6 +385,7 @@ impl Peerstate { { self.verified_key = self.public_key.clone(); self.verified_key_fingerprint = self.public_key_fingerprint.clone(); + self.verifier = Some(verifier); true } else { false @@ -383,6 +397,7 @@ impl Peerstate { { self.verified_key = self.gossip_key.clone(); self.verified_key_fingerprint = self.gossip_key_fingerprint.clone(); + self.verifier = Some(verifier); true } else { false @@ -407,8 +422,9 @@ impl Peerstate { gossip_key_fingerprint, verified_key, verified_key_fingerprint, - addr) - VALUES (?,?,?,?,?,?,?,?,?,?,?) + addr, + verifier) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT (addr) DO UPDATE SET last_seen = excluded.last_seen, @@ -420,7 +436,8 @@ impl Peerstate { public_key_fingerprint = excluded.public_key_fingerprint, gossip_key_fingerprint = excluded.gossip_key_fingerprint, verified_key = excluded.verified_key, - verified_key_fingerprint = excluded.verified_key_fingerprint", + verified_key_fingerprint = excluded.verified_key_fingerprint, + verifier = excluded.verifier", paramsv![ self.last_seen, self.last_seen_autocrypt, @@ -433,6 +450,7 @@ impl Peerstate { self.verified_key.as_ref().map(|k| k.to_bytes()), self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()), self.addr, + self.verifier, ], ) .await?; @@ -447,6 +465,11 @@ impl Peerstate { } } + /// Returns the address that verified the contact + pub fn get_verifier(&self) -> Option<&str> { + self.verifier.as_deref() + } + /// Add an info message to all the chats with this contact, informing about /// a [`PeerstateChange`]. /// @@ -672,6 +695,7 @@ mod tests { verified_key: Some(pub_key.clone()), verified_key_fingerprint: Some(pub_key.fingerprint()), fingerprint_changed: false, + verifier: None, }; assert!( @@ -711,6 +735,7 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; assert!( @@ -743,6 +768,7 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; assert!( @@ -805,6 +831,7 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; peerstate.apply_header(&header, 100); diff --git a/src/qr.rs b/src/qr.rs index 6a735216e..2813c762d 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -896,6 +896,7 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; assert!( peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(), diff --git a/src/receive_imf.rs b/src/receive_imf.rs index d6a10b496..41bb20ff9 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2138,13 +2138,14 @@ async fn check_verified_properties( || peerstate.verified_key_fingerprint != peerstate.public_key_fingerprint && peerstate.verified_key_fingerprint != peerstate.gossip_key_fingerprint { - info!(context, "{} has verified {}.", contact.get_addr(), to_addr,); + info!(context, "{} has verified {}.", contact.get_addr(), to_addr); let fp = peerstate.gossip_key_fingerprint.clone(); if let Some(fp) = fp { peerstate.set_verified( PeerstateKeyType::GossipKey, &fp, PeerstateVerifiedStatus::BidirectVerified, + contact.get_addr().to_owned(), ); peerstate.save_to_db(&context.sql).await?; is_verified = true; diff --git a/src/securejoin.rs b/src/securejoin.rs index 9663a30a8..77d743c17 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -411,7 +411,14 @@ pub(crate) async fn handle_securejoin_handshake( .await?; return Ok(HandshakeMessage::Ignore); } - if mark_peer_as_verified(context, &fingerprint).await.is_err() { + let contact_addr = Contact::load_from_db(context, contact_id) + .await? + .get_addr() + .to_owned(); + if mark_peer_as_verified(context, &fingerprint, contact_addr) + .await + .is_err() + { could_not_establish_secure_connection( context, contact_id, @@ -531,7 +538,7 @@ pub(crate) async fn handle_securejoin_handshake( /// /// - if we see the self-sent-message vg-member-added/vc-contact-confirm, /// we know that we're an inviter-observer. -/// the inviting device has marked a peer as verified on vg-request-with-auth/vc-request-with-auth +/// The inviting device has marked a peer as verified on vg-request-with-auth/vc-request-with-auth /// before sending vg-member-added/vc-contact-confirm - so, if we observe vg-member-added/vc-contact-confirm, /// we can mark the peer as verified as well. /// @@ -586,7 +593,17 @@ pub(crate) async fn observe_securejoin_on_other_device( return Ok(HandshakeMessage::Ignore); } }; - if mark_peer_as_verified(context, &fingerprint).await.is_err() { + if mark_peer_as_verified( + context, + &fingerprint, + Contact::load_from_db(context, contact_id) + .await? + .get_addr() + .to_owned(), + ) + .await + .is_err() + { could_not_establish_secure_connection( context, contact_id, @@ -634,12 +651,17 @@ async fn could_not_establish_secure_connection( Ok(()) } -async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) -> Result<(), Error> { +async fn mark_peer_as_verified( + context: &Context, + fingerprint: &Fingerprint, + verifier: String, +) -> Result<(), Error> { if let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, fingerprint).await? { if peerstate.set_verified( PeerstateKeyType::PublicKey, fingerprint, PeerstateVerifiedStatus::BidirectVerified, + verifier, ) { peerstate.prefer_encrypt = EncryptPreference::Mutual; peerstate.save_to_db(&context.sql).await.unwrap_or_default(); @@ -931,6 +953,7 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; peerstate.save_to_db(&bob.ctx.sql).await?; diff --git a/src/securejoin/bobstate.rs b/src/securejoin/bobstate.rs index f5e340622..e1145a006 100644 --- a/src/securejoin/bobstate.rs +++ b/src/securejoin/bobstate.rs @@ -326,7 +326,7 @@ impl BobState { /// /// This deviates from the protocol by also sending a confirmation message in response /// to the *vc-contact-confirm* message. This has no specific value to the protocol and - /// is only done out of symmerty with *vg-member-added* handling. + /// is only done out of symmetry with *vg-member-added* handling. async fn step_contact_confirm( &mut self, context: &Context, @@ -366,7 +366,12 @@ impl BobState { "Contact confirm message not encrypted", ))); } - mark_peer_as_verified(context, self.invite.fingerprint()).await?; + mark_peer_as_verified( + context, + self.invite.fingerprint(), + mime_message.from.addr.to_string(), + ) + .await?; Contact::scaleup_origin_by_id(context, self.invite.contact_id(), Origin::SecurejoinJoined) .await?; context.emit_event(EventType::ContactsChanged(None)); diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 6324f01fb..fde27051c 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -664,6 +664,13 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid); 95 ).await?; } + if dbversion < 96 { + sql.execute_migration( + "ALTER TABLE acpeerstates ADD COLUMN verifier TEXT DEFAULT '';", + 96, + ) + .await?; + } let new_version = sql .get_raw_config_int(VERSION_CFG) diff --git a/src/test_utils.rs b/src/test_utils.rs index 43318edfc..ecf219d5c 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -244,6 +244,7 @@ impl TestContext { /// /// This is a shortcut which automatically calls [`TestContext::configure_alice`] after /// creating the context. + /// alice-email: alice@example.org pub async fn new_alice() -> Self { Self::builder().configure_alice().build().await } diff --git a/src/tools.rs b/src/tools.rs index 187a38ae4..b766dc73e 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -593,11 +593,7 @@ impl rusqlite::types::ToSql for EmailAddress { /// Makes sure that a user input that is not supposed to contain newlines does not contain newlines. pub(crate) fn improve_single_line_input(input: &str) -> String { - input - .replace('\n', " ") - .replace('\r', " ") - .trim() - .to_string() + input.replace(['\n', '\r'], " ").trim().to_string() } pub(crate) trait IsNoneOrEmpty {