diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index d5c83a39d..ae2272dbb 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1220,6 +1220,18 @@ void dc_delete_chat (dc_context_t* context, uint32_t ch */ dc_array_t* dc_get_chat_contacts (dc_context_t* context, uint32_t chat_id); +/** + * Get encryption info for a chat. + * Get a multi-line encryption info, containing encryption preferences of all members. + * Can be used to find out why messages sent to group are not encrypted. + * + * @memberof dc_context_t + * @param context The context object. + * @param chat_id ID of the chat to get the encryption info for. + * @return Multi-line text, must be released using dc_str_unref() after usage. + */ +char* dc_get_chat_encrinfo (dc_context_t* context, uint32_t chat_id); + /** * Get the chat's ephemeral message timer. * The ephemeral message timer is set by dc_set_chat_ephemeral_timer() diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 8633986fb..62ca694cf 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -1322,6 +1322,29 @@ pub unsafe extern "C" fn dc_set_chat_mute_duration( }) } +#[no_mangle] +pub unsafe extern "C" fn dc_get_chat_encrinfo( + context: *mut dc_context_t, + chat_id: u32, +) -> *mut libc::c_char { + if context.is_null() { + eprintln!("ignoring careless call to dc_get_chat_encrinfo()"); + return "".strdup(); + } + let ctx = &*context; + + block_on(async move { + ChatId::new(chat_id) + .get_encryption_info(&ctx) + .await + .map(|s| s.strdup()) + .unwrap_or_else(|e| { + error!(&ctx, "{}", e); + ptr::null_mut() + }) + }) +} + #[no_mangle] pub unsafe extern "C" fn dc_get_chat_ephemeral_timer( context: *mut dc_context_t, diff --git a/python/src/deltachat/chat.py b/python/src/deltachat/chat.py index 4aa0b2d04..079389b30 100644 --- a/python/src/deltachat/chat.py +++ b/python/src/deltachat/chat.py @@ -167,6 +167,13 @@ class Chat(object): """ return lib.dc_chat_get_type(self._dc_chat) + def get_encryption_info(self): + """Return encryption info for this chat. + + :returns: a string with encryption preferences of all chat members""" + res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id) + return from_dc_charpointer(res) + def get_join_qr(self): """ get/create Join-Group QR Code as ascii-string. diff --git a/python/tests/test_account.py b/python/tests/test_account.py index 010411df3..9c476d7c6 100644 --- a/python/tests/test_account.py +++ b/python/tests/test_account.py @@ -1130,6 +1130,7 @@ class TestOnlineAccount: msg = ac1._evtracker.wait_next_incoming_message() assert msg.text == "first message" assert not msg.is_encrypted() + assert msg.chat.get_encryption_info() == f"{ac2.get_config('addr')} End-to-end encryption preferred." lp.sec("ac2 learns that ac3 prefers encryption") ac2.create_chat(ac3) @@ -1141,6 +1142,7 @@ class TestOnlineAccount: lp.sec("ac3 does not know that ac1 prefers encryption") ac1.create_chat(ac3) chat = ac3.create_chat(ac1) + assert chat.get_encryption_info() == f"{ac1.get_config('addr')} No encryption." msg = chat.send_text("not encrypted") msg = ac1._evtracker.wait_next_incoming_message() assert msg.text == "not encrypted" @@ -1149,6 +1151,8 @@ class TestOnlineAccount: lp.sec("ac1 creates a group chat with ac2") group_chat = ac1.create_group_chat("hello") group_chat.add_contact(ac2) + encryption_info = group_chat.get_encryption_info() + assert encryption_info == f"{ac2.get_config('addr')} End-to-end encryption preferred." msg = group_chat.send_text("hi") msg = ac2._evtracker.wait_next_incoming_message() @@ -1161,6 +1165,9 @@ class TestOnlineAccount: lp.sec("ac3 learns that ac1 prefers encryption") msg = ac3._evtracker.wait_next_incoming_message() + encryption_info = msg.chat.get_encryption_info().splitlines() + assert f"{ac1.get_config('addr')} End-to-end encryption preferred." in encryption_info + assert f"{ac2.get_config('addr')} End-to-end encryption preferred." in encryption_info msg = chat.send_text("encrypted") assert msg.is_encrypted() diff --git a/src/chat.rs b/src/chat.rs index d986a3fcf..daacc2736 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -12,6 +12,7 @@ use itertools::Itertools; use num_traits::FromPrimitive; use serde::{Deserialize, Serialize}; +use crate::aheader::EncryptPreference; use crate::blob::{BlobError, BlobObject}; use crate::chatlist::dc_get_archived_cnt; use crate::config::Config; @@ -35,6 +36,7 @@ use crate::job::{self, Action}; use crate::message::{self, InvalidMsgId, Message, MessageState, MsgId}; use crate::mimeparser::SystemMessage; use crate::param::{Param, Params}; +use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; use crate::sql; use crate::stock::StockMessage; @@ -637,6 +639,47 @@ impl ChatId { } } + /// Returns multi-line text summary of encryption preferences of all chat contacts. + /// + /// This can be used to find out if encryption is not available because + /// keys for some users are missing or simply because the majority of the users in a group + /// prefer plaintext emails. + /// + /// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`]. + pub async fn get_encryption_info(self, context: &Context) -> Result { + let mut ret = String::new(); + + for contact_id in get_chat_contacts(context, self) + .await + .iter() + .filter(|&contact_id| *contact_id > DC_CONTACT_ID_LAST_SPECIAL) + { + let contact = Contact::load_from_db(context, *contact_id).await?; + let addr = contact.get_addr(); + let peerstate = Peerstate::from_addr(context, addr).await?; + + let stock_message = peerstate + .filter(|peerstate| { + peerstate + .peek_key(PeerstateVerifiedStatus::Unverified) + .is_some() + }) + .map(|peerstate| match peerstate.prefer_encrypt { + EncryptPreference::Mutual => StockMessage::E2ePreferred, + EncryptPreference::NoPreference => StockMessage::E2eAvailable, + EncryptPreference::Reset => StockMessage::EncrNone, + }) + .unwrap_or(StockMessage::EncrNone); + + if !ret.is_empty() { + ret.push('\n') + } + ret += &format!("{} {}", addr, context.stock_str(stock_message).await); + } + + Ok(ret) + } + /// Bad evil escape hatch. /// /// Avoid using this, eventually types should be cleaned up enough