diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index a520e3c13..2856b1213 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1170,6 +1170,24 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3); +/** + * Enable or disable protection against active attacks. + * To enable protection, it is needed that all members are verified; + * if this condition is met, end-to-end-encryption is always enabled + * and only the verified keys are used. + * + * Sends out #DC_EVENT_CHAT_MODIFIED on changes + * and #DC_EVENT_MSGS_CHANGED if a status message was sent. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id The ID of the chat to change the protection for. + * @param protect 1=protect chat, 0=unprotect chat + * @return 1=success, 0=error, eg. some members may be unverified + */ +int dc_set_chat_protection (dc_context_t* context, uint32_t chat_id, int protect); + + /** * Set chat visibility to pinned, archived or normal. * @@ -1299,15 +1317,15 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch * * @memberof dc_context_t * @param context The context object. - * @param verified If set to 1 the function creates a secure verified group. - * Only secure-verified members are allowed in these groups + * @param protect If set to 1 the function creates group with protection initially enabled. + * Only verified members are allowed in these groups * and end-to-end-encryption is always enabled. * @param name The name of the group chat to create. * The name may be changed later using dc_set_chat_name(). * To find out the name of a group later, see dc_chat_get_name() * @return The chat ID of the new group chat, 0 on errors. */ -uint32_t dc_create_group_chat (dc_context_t* context, int verified, const char* name); +uint32_t dc_create_group_chat (dc_context_t* context, int protect, const char* name); /** @@ -1329,7 +1347,7 @@ int dc_is_contact_in_chat (dc_context_t* context, uint32_t ch * If the group is already _promoted_ (any message was sent to the group), * all group members are informed by a special status message that is sent automatically by this function. * - * If the group is a verified group, only verified contacts can be added to the group. + * If the group has group protection enabled, only verified contacts can be added to the group. * * Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent. * @@ -1973,7 +1991,7 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char* * @param context The context object. * @param chat_id If set to a group-chat-id, * the Verified-Group-Invite protocol is offered in the QR code; - * works for verified groups as well as for normal groups. + * works for protected groups as well as for normal groups. * If set to 0, the Setup-Contact protocol is offered in the QR code. * See https://countermitm.readthedocs.io/en/latest/new.html * for details about both protocols. @@ -2003,7 +2021,7 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch * When the protocol has finished, an info-message is added to that chat. * - If the given QR code starts the Verified-Group-Invite protocol, * the function waits until the protocol has finished. - * This is because the verified group is not opportunistic + * This is because the protected group is not opportunistic * and can be created only when the contacts have verified each other. * * See https://countermitm.readthedocs.io/en/latest/new.html @@ -2015,8 +2033,8 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch * to dc_check_qr(). * @return Chat-id of the joined chat, the UI may redirect to the this chat. * If the out-of-band verification failed or was aborted, 0 is returned. - * A returned chat-id does not guarantee that the chat or the belonging contact is verified. - * If needed, this be checked with dc_chat_is_verified() and dc_contact_is_verified(), + * A returned chat-id does not guarantee that the chat is protected or the belonging contact is verified. + * If needed, this be checked with dc_chat_is_protected() and dc_contact_is_verified(), * however, in practise, the UI will just listen to #DC_EVENT_CONTACTS_CHANGED unconditionally. */ uint32_t dc_join_securejoin (dc_context_t* context, const char* qr); @@ -2769,7 +2787,6 @@ char* dc_chat_get_info_json (dc_context_t* context, size_t chat #define DC_CHAT_TYPE_UNDEFINED 0 #define DC_CHAT_TYPE_SINGLE 100 #define DC_CHAT_TYPE_GROUP 120 -#define DC_CHAT_TYPE_VERIFIED_GROUP 130 /** @@ -2810,9 +2827,6 @@ uint32_t dc_chat_get_id (const dc_chat_t* chat); * - DC_CHAT_TYPE_GROUP (120) - a group chat, chats_contacts contain all group * members, incl. DC_CONTACT_ID_SELF * - * - DC_CHAT_TYPE_VERIFIED_GROUP (130) - a verified group chat. In verified groups, - * all members are verified and encryption is always active and cannot be disabled. - * * @memberof dc_chat_t * @param chat The chat object. * @return Chat type. @@ -2942,15 +2956,16 @@ int dc_chat_can_send (const dc_chat_t* chat); /** - * Check if a chat is verified. Verified chats contain only verified members - * and encryption is alwasy enabled. Verified chats are created using - * dc_create_group_chat() by setting the 'verified' parameter to true. + * Check if a chat is protected. + * Protected chats contain only verified members and encryption is always enabled. + * Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1. + * The status can be changed using dc_set_chat_protection(). * * @memberof dc_chat_t * @param chat The chat object. - * @return 1=chat verified, 0=chat is not verified + * @return 1=chat protected, 0=chat is not protected */ -int dc_chat_is_verified (const dc_chat_t* chat); +int dc_chat_is_protected (const dc_chat_t* chat); /** @@ -3444,7 +3459,8 @@ int dc_msg_is_forwarded (const dc_msg_t* msg); /** * Check if the message is an informational message, created by the * device or by another users. Such messages are not "typed" by the user but - * created due to other actions, eg. dc_set_chat_name(), dc_set_chat_profile_image() + * created due to other actions, + * eg. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection() * or dc_add_contact_to_chat(). * * These messages are typically shown in the center of the chat view, @@ -3460,6 +3476,32 @@ int dc_msg_is_forwarded (const dc_msg_t* msg); int dc_msg_is_info (const dc_msg_t* msg); +/** + * Get the type of an informational message. + * If dc_msg_is_info() returns 1, this function returns the type of the informational message. + * UIs can display eg. an icon based upon the type. + * + * Currently, the following types are defined: + * - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected" + * - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected" + * + * Even when you display an icon, + * you should still display the text of the informational message using dc_msg_get_text() + * + * @memberof dc_msg_t + * @param msg The message object. + * @return One of the DC_INFO* constants. + * 0 or other values indicate unspecified types + * or that the message is not an info-message. + */ +int dc_msg_get_info_type (const dc_msg_t* msg); + + +// DC_INFO* uses the same values as SystemMessage in rust-land +#define DC_INFO_PROTECTION_ENABLED 11 +#define DC_INFO_PROTECTION_DISABLED 12 + + /** * Check if a message is still in creation. A message is in creation between * the calls to dc_prepare_msg() and dc_send_msg(). @@ -4957,8 +4999,10 @@ void dc_event_unref(dc_event_t* event); #define DC_STR_BAD_TIME_MSG_BODY 85 #define DC_STR_UPDATE_REMINDER_MSG_BODY 86 #define DC_STR_ERROR_NO_NETWORK 87 +#define DC_STR_PROTECTION_ENABLED 88 +#define DC_STR_PROTECTION_DISABLED 89 -#define DC_STR_COUNT 87 +#define DC_STR_COUNT 89 /* * @} diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 52d779baa..d9c617af1 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -25,7 +25,7 @@ use async_std::task::{block_on, spawn}; use num_traits::{FromPrimitive, ToPrimitive}; use deltachat::accounts::Accounts; -use deltachat::chat::{ChatId, ChatVisibility, MuteDuration}; +use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus}; use deltachat::constants::DC_MSG_ID_LAST_SPECIAL; use deltachat::contact::{Contact, Origin}; use deltachat::context::Context; @@ -1057,6 +1057,32 @@ pub unsafe extern "C" fn dc_get_next_media( }) } +#[no_mangle] +pub unsafe extern "C" fn dc_set_chat_protection( + context: *mut dc_context_t, + chat_id: u32, + protect: libc::c_int, +) -> libc::c_int { + if context.is_null() { + eprintln!("ignoring careless call to dc_set_chat_protection()"); + return 0; + } + let ctx = &*context; + let protect = if let Some(s) = ProtectionStatus::from_i32(protect) { + s + } else { + warn!(ctx, "bad protect-value for dc_set_chat_protection()"); + return 0; + }; + + block_on(async move { + match ChatId::new(chat_id).set_protection(&ctx, protect).await { + Ok(()) => 1, + Err(_) => 0, + } + }) +} + #[no_mangle] pub unsafe extern "C" fn dc_set_chat_visibility( context: *mut dc_context_t, @@ -1170,7 +1196,7 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) - #[no_mangle] pub unsafe extern "C" fn dc_create_group_chat( context: *mut dc_context_t, - verified: libc::c_int, + protect: libc::c_int, name: *const libc::c_char, ) -> u32 { if context.is_null() || name.is_null() { @@ -1178,14 +1204,15 @@ pub unsafe extern "C" fn dc_create_group_chat( return 0; } let ctx = &*context; - let verified = if let Some(s) = contact::VerifiedStatus::from_i32(verified) { + let protect = if let Some(s) = ProtectionStatus::from_i32(protect) { s } else { + warn!(ctx, "bad protect-value for dc_create_group_chat()"); return 0; }; block_on(async move { - chat::create_group_chat(&ctx, verified, to_string_lossy(name)) + chat::create_group_chat(&ctx, protect, to_string_lossy(name)) .await .log_err(ctx, "Failed to create group chat") .map(|id| id.to_u32()) @@ -2389,13 +2416,13 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int { } #[no_mangle] -pub unsafe extern "C" fn dc_chat_is_verified(chat: *mut dc_chat_t) -> libc::c_int { +pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_int { if chat.is_null() { - eprintln!("ignoring careless call to dc_chat_is_verified()"); + eprintln!("ignoring careless call to dc_chat_is_protected()"); return 0; } let ffi_chat = &*chat; - ffi_chat.chat.is_verified() as libc::c_int + ffi_chat.chat.is_protected() as libc::c_int } #[no_mangle] @@ -2817,6 +2844,16 @@ pub unsafe extern "C" fn dc_msg_is_info(msg: *mut dc_msg_t) -> libc::c_int { ffi_msg.message.is_info().into() } +#[no_mangle] +pub unsafe extern "C" fn dc_msg_get_info_type(msg: *mut dc_msg_t) -> libc::c_int { + if msg.is_null() { + eprintln!("ignoring careless call to dc_msg_get_info_type()"); + return 0; + } + let ffi_msg = &*msg; + ffi_msg.message.get_info_type() as libc::c_int +} + #[no_mangle] pub unsafe extern "C" fn dc_msg_is_increation(msg: *mut dc_msg_t) -> libc::c_int { if msg.is_null() { diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index b4f172e22..e949a34e2 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use anyhow::{bail, ensure}; use async_std::path::Path; -use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility}; +use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, ProtectionStatus}; use deltachat::chatlist::*; use deltachat::constants::*; use deltachat::contact::*; @@ -357,7 +357,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu createchat \n\ createchatbymsg \n\ creategroup \n\ - createverified \n\ + createprotected \n\ addmember \n\ removemember \n\ groupname \n\ @@ -379,6 +379,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu unarchive \n\ pin \n\ unpin \n\ + protect \n\ + unprotect \n\ delchat \n\ ===========================Message commands==\n\ listmsgs \n\ @@ -523,7 +525,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu for i in (0..cnt).rev() { let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?; println!( - "{}#{}: {} [{} fresh] {}", + "{}#{}: {} [{} fresh] {}{}", chat_prefix(&chat), chat.get_id(), chat.get_name(), @@ -533,6 +535,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu ChatVisibility::Archived => "📦", ChatVisibility::Pinned => "📌", }, + if chat.is_protected() { "🛡️" } else { "" }, ); let lot = chatlist.get_summary(&context, i, Some(&chat)).await; let statestr = if chat.visibility == ChatVisibility::Archived { @@ -607,7 +610,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu format!("{} member(s)", members.len()) }; println!( - "{}#{}: {} [{}]{}{}", + "{}#{}: {} [{}]{}{} {}", chat_prefix(sel_chat), sel_chat.get_id(), sel_chat.get_name(), @@ -624,6 +627,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu }, _ => "".to_string(), }, + if sel_chat.is_protected() { + "🛡️" + } else { + "" + }, ); log_msglist(&context, &msglist).await?; if let Some(draft) = sel_chat.get_id().get_draft(&context).await? { @@ -654,15 +662,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu "creategroup" => { ensure!(!arg1.is_empty(), "Argument missing."); let chat_id = - chat::create_group_chat(&context, VerifiedStatus::Unverified, arg1).await?; + chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?; println!("Group#{} created successfully.", chat_id); } - "createverified" => { + "createprotected" => { ensure!(!arg1.is_empty(), "Argument missing."); - let chat_id = chat::create_group_chat(&context, VerifiedStatus::Verified, arg1).await?; + let chat_id = + chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?; - println!("VerifiedGroup#{} created successfully.", chat_id); + println!("Group#{} created and protected successfully.", chat_id); } "addmember" => { ensure!(sel_chat.is_some(), "No chat selected"); @@ -906,7 +915,21 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu "archive" => ChatVisibility::Archived, "unarchive" | "unpin" => ChatVisibility::Normal, "pin" => ChatVisibility::Pinned, - _ => panic!("Unexpected command (This should never happen)"), + _ => unreachable!("arg0={:?}", arg0), + }, + ) + .await?; + } + "protect" | "unprotect" => { + ensure!(!arg1.is_empty(), "Argument missing."); + let chat_id = ChatId::new(arg1.parse()?); + chat_id + .set_protection( + &context, + match arg0 { + "protect" => ProtectionStatus::Protected, + "unprotect" => ProtectionStatus::Unprotected, + _ => unreachable!("arg0={:?}", arg0), }, ) .await?; diff --git a/python/src/deltachat/chat.py b/python/src/deltachat/chat.py index 60860d5a3..bd3890b70 100644 --- a/python/src/deltachat/chat.py +++ b/python/src/deltachat/chat.py @@ -57,10 +57,7 @@ class Chat(object): :returns: True if chat is a group-chat, false if it's a contact 1:1 chat. """ - return lib.dc_chat_get_type(self._dc_chat) in ( - const.DC_CHAT_TYPE_GROUP, - const.DC_CHAT_TYPE_VERIFIED_GROUP - ) + return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP def is_deaddrop(self): """ return true if this chat is a deaddrop chat. @@ -85,12 +82,12 @@ class Chat(object): """ return not lib.dc_chat_is_unpromoted(self._dc_chat) - def is_verified(self): - """ return True if this chat is a verified group. + def is_protected(self): + """ return True if this chat is a protected chat. - :returns: True if chat is verified, False otherwise. + :returns: True if chat is protected, False otherwise. """ - return lib.dc_chat_is_verified(self._dc_chat) + return lib.dc_chat_is_protected(self._dc_chat) def get_name(self): """ return name of this chat. diff --git a/python/tests/test_account.py b/python/tests/test_account.py index 543317b1e..e65f27ced 100644 --- a/python/tests/test_account.py +++ b/python/tests/test_account.py @@ -1343,7 +1343,7 @@ class TestOnlineAccount: ac1, ac2 = acfactory.get_two_online_accounts() lp.sec("ac1: create verified-group QR, ac2 scans and joins") chat1 = ac1.create_group_chat("hello", verified=True) - assert chat1.is_verified() + assert chat1.is_protected() qr = chat1.get_join_qr() lp.sec("ac2: start QR-code based join-group protocol") chat2 = ac2.qr_join_chat(qr) @@ -1362,7 +1362,7 @@ class TestOnlineAccount: lp.sec("ac2: read message and check it's verified chat") msg = ac2._evtracker.wait_next_incoming_message() assert msg.text == "hello" - assert msg.chat.is_verified() + assert msg.chat.is_protected() assert msg.is_encrypted() lp.sec("ac2: send message and let ac1 read it") diff --git a/src/chat.rs b/src/chat.rs index 42f5e585b..17353eb7a 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,5 +1,6 @@ //! # Chat module +use deltachat_derive::{FromSql, ToSql}; use std::convert::TryFrom; use std::time::{Duration, SystemTime}; @@ -45,6 +46,33 @@ pub enum ChatItem { }, } +#[derive( + Debug, + Display, + Clone, + Copy, + PartialEq, + Eq, + FromPrimitive, + ToPrimitive, + FromSql, + ToSql, + IntoStaticStr, + Serialize, + Deserialize, +)] +#[repr(u32)] +pub enum ProtectionStatus { + Unprotected = 0, + Protected = 1, +} + +impl Default for ProtectionStatus { + fn default() -> Self { + ProtectionStatus::Unprotected + } +} + /// Chat ID, including reserved IDs. /// /// Some chat IDs are reserved to identify special chat types. This @@ -146,6 +174,107 @@ impl ChatId { self.set_blocked(context, Blocked::Not).await; } + /// Sets protection without sending a message. + /// + /// Used when a message arrives indicating that someone else has + /// changed the protection value for a chat. + pub(crate) async fn inner_set_protection( + self, + context: &Context, + protect: ProtectionStatus, + ) -> Result<(), Error> { + ensure!(!self.is_special(), "Invalid chat-id."); + + let chat = Chat::load_from_db(context, self).await?; + + if protect == chat.protected { + info!(context, "Protection status unchanged for {}.", self); + return Ok(()); + } + + match protect { + ProtectionStatus::Protected => match chat.typ { + Chattype::Single | Chattype::Group => { + let contact_ids = get_chat_contacts(context, self).await; + for contact_id in contact_ids.into_iter() { + let contact = Contact::get_by_id(context, contact_id).await?; + if contact.is_verified(context).await != VerifiedStatus::BidirectVerified { + bail!("{} is not verified.", contact.get_display_name()); + } + } + } + Chattype::Undefined => bail!("Undefined group type"), + }, + ProtectionStatus::Unprotected => {} + }; + + context + .sql + .execute( + "UPDATE chats SET protected=? WHERE id=?;", + paramsv![protect, self], + ) + .await?; + + context.emit_event(EventType::ChatModified(self)); + + // make sure, the receivers will get all keys + reset_gossiped_timestamp(context, self).await?; + + Ok(()) + } + + /// Send protected status message to the chat. + /// + /// This sends the message with the protected status change to the chat, + /// notifying the user on this device as well as the other users in the chat. + /// If `promoted` is false this means the chat only exists on this device so far + /// and does not need to be sent out. + /// In this case an local info message is added to the chat. + pub(crate) async fn add_protection_msg( + self, + context: &Context, + protect: ProtectionStatus, + promoted: bool, + from_id: u32, + ) -> Result<(), Error> { + let msg_text = context.stock_protection_msg(protect, from_id).await; + + if promoted { + let mut msg = Message::default(); + msg.viewtype = Viewtype::Text; + msg.text = Some(msg_text); + msg.param.set_cmd(match protect { + ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled, + ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled, + }); + send_msg(context, self, &mut msg).await?; + } else { + add_info_msg(context, self, msg_text).await; + } + + Ok(()) + } + + /// Sets protection and sends or adds a message. + pub async fn set_protection( + self, + context: &Context, + protect: ProtectionStatus, + ) -> Result<(), Error> { + ensure!(!self.is_special(), "set protection: invalid chat-id."); + + let chat = Chat::load_from_db(context, self).await?; + + if let Err(e) = self.inner_set_protection(context, protect).await { + error!(context, "Cannot set protection: {}", e); // make error user-visible + return Err(e); + } + + self.add_protection_msg(context, protect, chat.is_promoted(), DC_CONTACT_ID_SELF) + .await + } + /// Archives or unarchives a chat. pub async fn set_visibility( self, @@ -538,6 +667,7 @@ pub struct Chat { pub param: Params, is_sending_locations: bool, pub mute_duration: MuteDuration, + protected: ProtectionStatus, } impl Chat { @@ -547,7 +677,7 @@ impl Chat { .sql .query_row( "SELECT c.type, c.name, c.grpid, c.param, c.archived, - c.blocked, c.locations_send_until, c.muted_until + c.blocked, c.locations_send_until, c.muted_until, c.protected FROM chats c WHERE c.id=?;", paramsv![chat_id], @@ -562,6 +692,7 @@ impl Chat { blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(), is_sending_locations: row.get(6)?, mute_duration: row.get(7)?, + protected: row.get(8)?, }; Ok(c) }, @@ -727,9 +858,9 @@ impl Chat { !self.is_unpromoted() } - /// Returns true if chat is a verified group chat. - pub fn is_verified(&self) -> bool { - self.typ == Chattype::VerifiedGroup + /// Returns true if chat protection is enabled. + pub fn is_protected(&self) -> bool { + self.protected == ProtectionStatus::Protected } /// Returns true if location streaming is enabled in the chat. @@ -756,15 +887,12 @@ impl Chat { let mut to_id = 0; let mut location_id = 0; - if !(self.typ == Chattype::Single - || self.typ == Chattype::Group - || self.typ == Chattype::VerifiedGroup) - { + if !(self.typ == Chattype::Single || self.typ == Chattype::Group) { error!(context, "Cannot send to chat type #{}.", self.typ,); bail!("Cannot set to chat type #{}", self.typ); } - if (self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup) + if self.typ == Chattype::Group && !is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await { emit_event!( @@ -781,7 +909,7 @@ impl Chat { let new_rfc724_mid = { let grpid = match self.typ { - Chattype::Group | Chattype::VerifiedGroup => Some(self.grpid.as_str()), + Chattype::Group => Some(self.grpid.as_str()), _ => None, }; dc_create_outgoing_rfc724_mid(grpid, &from) @@ -805,7 +933,7 @@ impl Chat { ); bail!("Cannot set message, contact for {} not found.", self.id); } - } else if (self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup) + } else if self.typ == Chattype::Group && self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 { msg.param.set_int(Param::AttachGroupImage, 1); @@ -1000,7 +1128,7 @@ pub struct ChatInfo { /// /// On the C API this number is one of the /// `DC_CHAT_TYPE_UNDEFINED`, `DC_CHAT_TYPE_SINGLE`, - /// `DC_CHAT_TYPE_GROUP` or `DC_CHAT_TYPE_VERIFIED_GROUP` + /// or `DC_CHAT_TYPE_GROUP` /// constants. #[serde(rename = "type")] pub type_: u32, @@ -1865,7 +1993,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Vec { pub async fn create_group_chat( context: &Context, - verified: VerifiedStatus, + protect: ProtectionStatus, chat_name: impl AsRef, ) -> Result { let chat_name = improve_single_line_input(chat_name); @@ -1879,11 +2007,7 @@ pub async fn create_group_chat( context.sql.execute( "INSERT INTO chats (type, name, grpid, param, created_timestamp) VALUES(?, ?, ?, \'U=1\', ?);", paramsv![ - if verified != VerifiedStatus::Unverified { - Chattype::VerifiedGroup - } else { - Chattype::Group - }, + Chattype::Group, chat_name, grpid, time(), @@ -1907,6 +2031,10 @@ pub async fn create_group_chat( chat_id: ChatId::new(0), }); + if protect == ProtectionStatus::Protected { + chat_id.set_protection(context, protect).await?; + } + Ok(chat_id) } @@ -2032,12 +2160,12 @@ pub(crate) async fn add_contact_to_chat_ex( } } else { // else continue and send status mail - if chat.typ == Chattype::VerifiedGroup + if chat.is_protected() && contact.is_verified(context).await != VerifiedStatus::BidirectVerified { error!( context, - "Only bidirectional verified contacts can be added to verified groups." + "Only bidirectional verified contacts can be added to protected chats." ); return Ok(false); } @@ -2075,7 +2203,7 @@ async fn real_group_exists(context: &Context, chat_id: ChatId) -> bool { context .sql .exists( - "SELECT id FROM chats WHERE id=? AND (type=120 OR type=130);", + "SELECT id FROM chats WHERE id=? AND type=120;", paramsv![chat_id], ) .await @@ -2601,7 +2729,7 @@ pub(crate) async fn get_chat_cnt(context: &Context) -> usize { } } -/// Returns a tuple of `(chatid, is_verified, blocked)`. +/// Returns a tuple of `(chatid, is_protected, blocked)`. pub(crate) async fn get_chat_id_by_grpid( context: &Context, grpid: impl AsRef, @@ -2609,14 +2737,16 @@ pub(crate) async fn get_chat_id_by_grpid( context .sql .query_row( - "SELECT id, blocked, type FROM chats WHERE grpid=?;", + "SELECT id, blocked, protected FROM chats WHERE grpid=?;", paramsv![grpid.as_ref()], |row| { let chat_id = row.get::<_, ChatId>(0)?; let b = row.get::<_, Option>(1)?.unwrap_or_default(); - let v = row.get::<_, Option>(2)?.unwrap_or_default(); - Ok((chat_id, v == Chattype::VerifiedGroup, b)) + let p = row + .get::<_, Option>(2)? + .unwrap_or_default(); + Ok((chat_id, p == ProtectionStatus::Protected, b)) }, ) .await @@ -2898,7 +3028,7 @@ mod tests { async fn test_add_contact_to_chat_ex_add_self() { // Adding self to a contact should succeed, even though it's pointless. let t = TestContext::new().await; - let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo") + let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo") .await .unwrap(); let added = add_contact_to_chat_ex(&t.ctx, chat_id, DC_CONTACT_ID_SELF, false) @@ -3284,7 +3414,7 @@ mod tests { .await .unwrap(); async_std::task::sleep(std::time::Duration::from_millis(1000)).await; - let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo") + let chat_id3 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo") .await .unwrap(); @@ -3329,7 +3459,7 @@ mod tests { #[async_std::test] async fn test_set_chat_name() { let t = TestContext::new().await; - let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo") + let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo") .await .unwrap(); assert_eq!( @@ -3372,7 +3502,7 @@ mod tests { #[async_std::test] async fn test_shall_attach_selfavatar() { let t = TestContext::new().await; - let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo") + let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo") .await .unwrap(); assert!(!shall_attach_selfavatar(&t.ctx, chat_id).await.unwrap()); @@ -3396,7 +3526,7 @@ mod tests { #[async_std::test] async fn test_set_mute_duration() { let t = TestContext::new().await; - let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo") + let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo") .await .unwrap(); // Initial diff --git a/src/chatlist.rs b/src/chatlist.rs index fbee5e1ed..7b05169a1 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -362,9 +362,7 @@ impl Chatlist { let mut lastcontact = None; let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await { - if lastmsg.from_id != DC_CONTACT_ID_SELF - && (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup) - { + if lastmsg.from_id != DC_CONTACT_ID_SELF && chat.typ == Chattype::Group { lastcontact = Contact::load_from_db(context, lastmsg.from_id).await.ok(); } @@ -440,13 +438,13 @@ mod tests { #[async_std::test] async fn test_try_load() { let t = TestContext::new().await; - let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat") + let chat_id1 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat") .await .unwrap(); - let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat") + let chat_id2 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "b chat") .await .unwrap(); - let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat") + let chat_id3 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "c chat") .await .unwrap(); @@ -489,7 +487,7 @@ mod tests { async fn test_sort_self_talk_up_on_forward() { let t = TestContext::new().await; t.ctx.update_device_chats().await.unwrap(); - create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat") + create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat") .await .unwrap(); @@ -546,7 +544,7 @@ mod tests { #[async_std::test] async fn test_get_summary_unwrap() { let t = TestContext::new().await; - let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat") + let chat_id1 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat") .await .unwrap(); diff --git a/src/constants.rs b/src/constants.rs index b8a393520..c1b8b4b8e 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -145,7 +145,6 @@ pub enum Chattype { Undefined = 0, Single = 100, Group = 120, - VerifiedGroup = 130, } impl Default for Chattype { diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index fc092bf39..f10acf939 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -4,7 +4,7 @@ use sha2::{Digest, Sha256}; use mailparse::SingleInfo; -use crate::chat::{self, Chat, ChatId}; +use crate::chat::{self, Chat, ChatId, ProtectionStatus}; use crate::config::Config; use crate::constants::*; use crate::contact::*; @@ -714,6 +714,19 @@ async fn add_parts( ephemeral_timer = EphemeralTimer::Disabled; } + // change chat protection + if let Some(new_status) = match mime_parser.is_system_message { + SystemMessage::ChatProtectionEnabled => Some(ProtectionStatus::Protected), + SystemMessage::ChatProtectionDisabled => Some(ProtectionStatus::Unprotected), + _ => None, + } { + chat_id.inner_set_protection(context, new_status).await?; + set_better_msg( + mime_parser, + context.stock_protection_msg(new_status, from_id).await, + ); + } + // correct message_timestamp, it should not be used before, // however, we cannot do this earlier as we need from_id to be set let in_fresh = state == MessageState::InFresh; @@ -1199,7 +1212,7 @@ async fn create_or_lookup_group( || X_MrAddToGrp.is_some() && addr_cmp(&self_addr, X_MrAddToGrp.as_ref().unwrap())) { // group does not exist but should be created - let create_verified = if mime_parser.get(HeaderDef::ChatVerified).is_some() { + let create_protected = if mime_parser.get(HeaderDef::ChatVerified).is_some() { if let Err(err) = check_verified_properties(context, mime_parser, from_id as u32, to_ids).await { @@ -1207,9 +1220,9 @@ async fn create_or_lookup_group( let s = format!("{}. See 'Info' for more details", err); mime_parser.repl_msg_by_error(&s); } - VerifiedStatus::Verified + ProtectionStatus::Protected } else { - VerifiedStatus::Unverified + ProtectionStatus::Unprotected }; if !allow_creation { @@ -1222,11 +1235,17 @@ async fn create_or_lookup_group( &grpid, grpname.as_ref().unwrap(), create_blocked, - create_verified, + create_protected, ) .await; chat_id_blocked = create_blocked; recreate_member_list = true; + + if create_protected == ProtectionStatus::Protected { + chat_id + .add_protection_msg(context, ProtectionStatus::Protected, false, from_id) + .await?; + } } // again, check chat_id @@ -1278,7 +1297,15 @@ async fn create_or_lookup_group( } } } + } else if mime_parser.is_system_message == SystemMessage::ChatProtectionEnabled { + recreate_member_list = true; + if let Err(e) = check_verified_properties(context, mime_parser, from_id, to_ids).await { + warn!(context, "checking verified properties failed: {}", e); + let s = format!("{}. See 'Info' for more details", e); + mime_parser.repl_msg_by_error(s); + } } + if let Some(avatar_action) = &mime_parser.group_avatar { info!(context, "group-avatar change for {}", chat_id); if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await { @@ -1463,7 +1490,7 @@ async fn create_or_lookup_adhoc_group( &grpid, grpname, create_blocked, - VerifiedStatus::Unverified, + ProtectionStatus::Unprotected, ) .await; for &member_id in &member_ids { @@ -1480,20 +1507,17 @@ async fn create_group_record( grpid: impl AsRef, grpname: impl AsRef, create_blocked: Blocked, - create_verified: VerifiedStatus, + create_protected: ProtectionStatus, ) -> ChatId { if context.sql.execute( - "INSERT INTO chats (type, name, grpid, blocked, created_timestamp) VALUES(?, ?, ?, ?, ?);", + "INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected) VALUES(?, ?, ?, ?, ?, ?);", paramsv![ - if VerifiedStatus::Unverified != create_verified { - Chattype::VerifiedGroup - } else { - Chattype::Group - }, + Chattype::Group, grpname.as_ref(), grpid.as_ref(), create_blocked, time(), + create_protected, ], ).await .is_err() @@ -1645,6 +1669,11 @@ async fn check_verified_properties( ensure!(mimeparser.was_encrypted(), "This message is not encrypted."); + ensure!( + mimeparser.get(HeaderDef::ChatVerified).is_some(), + "Sender did not mark the message as protected." + ); + // ensure, the contact is verified // and the message is signed with a verified key of the sender. // this check is skipped for SELF as there is no proper SELF-peerstate @@ -1734,7 +1763,7 @@ async fn check_verified_properties( } if !is_verified { bail!( - "{} is not a member of this verified group", + "{} is not a member of this protected chat", to_addr.to_string() ); } @@ -2182,7 +2211,7 @@ mod tests { assert!(one2one.get_visibility() == ChatVisibility::Archived); // create a group with bob, archive group - let group_id = chat::create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo") + let group_id = chat::create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo") .await .unwrap(); chat::add_contact_to_chat(&t.ctx, group_id, bob_id).await; diff --git a/src/message.rs b/src/message.rs index 967e8efdd..577aa193f 100644 --- a/src/message.rs +++ b/src/message.rs @@ -544,9 +544,7 @@ impl Message { return ret; }; - let contact = if self.from_id != DC_CONTACT_ID_SELF as u32 - && (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup) - { + let contact = if self.from_id != DC_CONTACT_ID_SELF as u32 && chat.typ == Chattype::Group { Contact::get_by_id(context, self.from_id).await.ok() } else { None @@ -591,6 +589,10 @@ impl Message { || cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage } + pub fn get_info_type(&self) -> SystemMessage { + self.param.get_cmd() + } + pub fn is_system_message(&self) -> bool { let cmd = self.param.get_cmd(); cmd != SystemMessage::Unknown @@ -976,7 +978,7 @@ impl Lot { ); self.text1_meaning = Meaning::Text1Self; } - } else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup { + } else if chat.typ == Chattype::Group { if msg.is_info() || contact.is_none() { self.text1 = None; self.text1_meaning = Meaning::None; @@ -1661,7 +1663,7 @@ pub(crate) async fn handle_ndn( if let Ok((msg_id, chat_id, chat_type)) = res { set_msg_failed(context, msg_id, error).await; - if chat_type == Chattype::Group || chat_type == Chattype::VerifiedGroup { + if chat_type == Chattype::Group { if let Some(failed_recipient) = &failed.failed_recipient { let contact_id = Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown).await; diff --git a/src/mimefactory.rs b/src/mimefactory.rs index c143ff066..823e1e739 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -233,7 +233,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { fn is_e2ee_guaranteed(&self) -> bool { match &self.loaded { Loaded::Message { chat } => { - if chat.typ == Chattype::VerifiedGroup { + if chat.is_protected() { return true; } @@ -255,7 +255,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { fn min_verified(&self) -> PeerstateVerifiedStatus { match &self.loaded { Loaded::Message { chat } => { - if chat.typ == Chattype::VerifiedGroup { + if chat.is_protected() { PeerstateVerifiedStatus::BidirectVerified } else { PeerstateVerifiedStatus::Unverified @@ -268,7 +268,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { fn should_force_plaintext(&self) -> bool { match &self.loaded { Loaded::Message { chat } => { - if chat.typ == Chattype::VerifiedGroup { + if chat.is_protected() { false } else { self.msg @@ -345,7 +345,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { .stock_str(StockMessage::AcSetupMsgSubject) .await .into_owned() - } else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup { + } else if chat.typ == Chattype::Group { let re = if self.in_reply_to.is_empty() { "" } else { @@ -708,11 +708,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> { let mut placeholdertext = None; let mut meta_part = None; - if chat.typ == Chattype::VerifiedGroup { + if chat.is_protected() { protected_headers.push(Header::new("Chat-Verified".to_string(), "1".to_string())); } - if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup { + if chat.typ == Chattype::Group { protected_headers.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone())); let encoded = encode_words(&chat.name); @@ -846,6 +846,18 @@ impl<'a, 'b> MimeFactory<'a, 'b> { }; } } + SystemMessage::ChatProtectionEnabled => { + protected_headers.push(Header::new( + "Chat-Content".to_string(), + "protection-enabled".to_string(), + )); + } + SystemMessage::ChatProtectionDisabled => { + protected_headers.push(Header::new( + "Chat-Content".to_string(), + "protection-disabled".to_string(), + )); + } _ => {} } diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 933fc11b0..eddbf6f07 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -86,6 +86,10 @@ pub enum SystemMessage { /// Chat ephemeral message timer is changed. EphemeralTimerChanged = 10, + + // Chat protection state changed + ChatProtectionEnabled = 11, + ChatProtectionDisabled = 12, } impl Default for SystemMessage { @@ -123,6 +127,7 @@ impl MimeMessage { // remove headers that are allowed _only_ in the encrypted part headers.remove("secure-join-fingerprint"); + headers.remove("chat-verified"); // Memory location for a possible decrypted message. let mail_raw; @@ -253,6 +258,10 @@ impl MimeMessage { self.is_system_message = SystemMessage::LocationStreamingEnabled; } else if value == "ephemeral-timer-changed" { self.is_system_message = SystemMessage::EphemeralTimerChanged; + } else if value == "protection-enabled" { + self.is_system_message = SystemMessage::ChatProtectionEnabled; + } else if value == "protection-disabled" { + self.is_system_message = SystemMessage::ChatProtectionDisabled; } } Ok(()) @@ -848,6 +857,7 @@ impl MimeMessage { } pub fn repl_msg_by_error(&mut self, error_msg: impl AsRef) { + self.is_system_message = SystemMessage::Unknown; if let Some(part) = self.parts.first_mut() { part.typ = Viewtype::Text; part.msg = format!("[{}]", error_msg.as_ref()); diff --git a/src/securejoin.rs b/src/securejoin.rs index 1994fc7ad..02abc14c5 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -348,7 +348,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result { let bob = context.bob.read().await; let grpid = bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(); match chat::get_chat_id_by_grpid(context, grpid).await { - Ok((chatid, _is_verified, _blocked)) => break chatid, + Ok((chatid, _is_protected, _blocked)) => break chatid, Err(err) => { if start.elapsed() > Duration::from_secs(7) { return Err(JoinError::MissingChat(err)); @@ -791,19 +791,19 @@ pub(crate) async fn handle_securejoin_handshake( let vg_expect_encrypted = if join_vg { let group_id = get_qr_attr!(context, text2).to_string(); - // This is buggy, is_verified_group will always be + // This is buggy, is_protected_group will always be // false since the group is created by receive_imf by // the very handshake message we're handling now. But // only after we have returned. It does not impact // the security invariants of secure-join however. - let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, &group_id) + let (_, is_protected_group, _) = chat::get_chat_id_by_grpid(context, &group_id) .await .unwrap_or((ChatId::new(0), false, Blocked::Not)); // when joining a non-verified group // the vg-member-added message may be unencrypted // when not all group members have keys or prefer encryption. // So only expect encryption if this is a verified group - is_verified_group + is_protected_group } else { // setup contact is always encrypted true @@ -1102,6 +1102,7 @@ mod tests { use super::*; use crate::chat; + use crate::chat::ProtectionStatus; use crate::peerstate::Peerstate; use crate::test_utils::TestContext; @@ -1328,7 +1329,7 @@ mod tests { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; - let chatid = chat::create_group_chat(&alice.ctx, VerifiedStatus::Verified, "the chat") + let chatid = chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat") .await .unwrap(); @@ -1427,6 +1428,6 @@ mod tests { let bob_chatid = joiner.await; let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await.unwrap(); - assert!(bob_chat.is_verified()); + assert!(bob_chat.is_protected()); } } diff --git a/src/sql.rs b/src/sql.rs index 98fc7d21e..8006022d8 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -1368,6 +1368,20 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label); .await?; sql.set_raw_config_int(context, "dbversion", 68).await?; } + if dbversion < 69 { + info!(context, "[migration] v69"); + sql.execute( + "ALTER TABLE chats ADD COLUMN protected INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + sql.execute( + "UPDATE chats SET protected=1, type=120 WHERE type=130;", // 120=group, 130=old verified group + paramsv![], + ) + .await?; + sql.set_raw_config_int(context, "dbversion", 69).await?; + } // (2) updates that require high-level objects // (the structure is complete now and all objects are usable) diff --git a/src/stock.rs b/src/stock.rs index 5fee21f42..7ebcb40c3 100644 --- a/src/stock.rs +++ b/src/stock.rs @@ -7,6 +7,7 @@ use strum_macros::EnumProperty; use crate::blob::BlobObject; use crate::chat; +use crate::chat::ProtectionStatus; use crate::constants::{Viewtype, DC_CONTACT_ID_SELF}; use crate::contact::*; use crate::context::Context; @@ -233,6 +234,12 @@ pub enum StockMessage { fallback = "Could not find your mail server.\n\nPlease check your internet connection." ))] ErrorNoNetwork = 87, + + #[strum(props(fallback = "Chat protection enabled."))] + ProtectionEnabled = 88, + + #[strum(props(fallback = "Chat protection disabled."))] + ProtectionDisabled = 89, } /* @@ -398,6 +405,20 @@ impl Context { } } + /// Returns a stock message saying that protection status has changed. + pub async fn stock_protection_msg(&self, protect: ProtectionStatus, from_id: u32) -> String { + self.stock_system_msg( + match protect { + ProtectionStatus::Protected => StockMessage::ProtectionEnabled, + ProtectionStatus::Unprotected => StockMessage::ProtectionDisabled, + }, + "", + "", + from_id, + ) + .await + } + pub async fn update_device_chats(&self) -> Result<(), Error> { // check for the LAST added device message - if it is present, we can skip message creation. // this is worthwhile as this function is typically called