Merge "protected groups" branch into master

This commit is contained in:
Alexander Krotov
2020-10-13 18:44:31 +03:00
15 changed files with 433 additions and 116 deletions

View File

@@ -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); 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. * 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 * @memberof dc_context_t
* @param context The context object. * @param context The context object.
* @param verified If set to 1 the function creates a secure verified group. * @param protect If set to 1 the function creates group with protection initially enabled.
* Only secure-verified members are allowed in these groups * Only verified members are allowed in these groups
* and end-to-end-encryption is always enabled. * and end-to-end-encryption is always enabled.
* @param name The name of the group chat to create. * @param name The name of the group chat to create.
* The name may be changed later using dc_set_chat_name(). * 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() * 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. * @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), * 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. * 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. * 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 context The context object.
* @param chat_id If set to a group-chat-id, * @param chat_id If set to a group-chat-id,
* the Verified-Group-Invite protocol is offered in the QR code; * 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. * If set to 0, the Setup-Contact protocol is offered in the QR code.
* See https://countermitm.readthedocs.io/en/latest/new.html * See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols. * 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. * When the protocol has finished, an info-message is added to that chat.
* - If the given QR code starts the Verified-Group-Invite protocol, * - If the given QR code starts the Verified-Group-Invite protocol,
* the function waits until the protocol has finished. * 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. * and can be created only when the contacts have verified each other.
* *
* See https://countermitm.readthedocs.io/en/latest/new.html * 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(). * to dc_check_qr().
* @return Chat-id of the joined chat, the UI may redirect to the this chat. * @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. * 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. * 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_verified() and dc_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. * 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); 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_UNDEFINED 0
#define DC_CHAT_TYPE_SINGLE 100 #define DC_CHAT_TYPE_SINGLE 100
#define DC_CHAT_TYPE_GROUP 120 #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 * - DC_CHAT_TYPE_GROUP (120) - a group chat, chats_contacts contain all group
* members, incl. DC_CONTACT_ID_SELF * 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 * @memberof dc_chat_t
* @param chat The chat object. * @param chat The chat object.
* @return Chat type. * @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 * Check if a chat is protected.
* and encryption is alwasy enabled. Verified chats are created using * Protected chats contain only verified members and encryption is always enabled.
* dc_create_group_chat() by setting the 'verified' parameter to true. * 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 * @memberof dc_chat_t
* @param chat The chat object. * @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 * 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 * 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(). * or dc_add_contact_to_chat().
* *
* These messages are typically shown in the center of the chat view, * 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); 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 * Check if a message is still in creation. A message is in creation between
* the calls to dc_prepare_msg() and dc_send_msg(). * 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_BAD_TIME_MSG_BODY 85
#define DC_STR_UPDATE_REMINDER_MSG_BODY 86 #define DC_STR_UPDATE_REMINDER_MSG_BODY 86
#define DC_STR_ERROR_NO_NETWORK 87 #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
/* /*
* @} * @}

View File

@@ -25,7 +25,7 @@ use async_std::task::{block_on, spawn};
use num_traits::{FromPrimitive, ToPrimitive}; use num_traits::{FromPrimitive, ToPrimitive};
use deltachat::accounts::Accounts; 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::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, Origin}; use deltachat::contact::{Contact, Origin};
use deltachat::context::Context; 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] #[no_mangle]
pub unsafe extern "C" fn dc_set_chat_visibility( pub unsafe extern "C" fn dc_set_chat_visibility(
context: *mut dc_context_t, 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] #[no_mangle]
pub unsafe extern "C" fn dc_create_group_chat( pub unsafe extern "C" fn dc_create_group_chat(
context: *mut dc_context_t, context: *mut dc_context_t,
verified: libc::c_int, protect: libc::c_int,
name: *const libc::c_char, name: *const libc::c_char,
) -> u32 { ) -> u32 {
if context.is_null() || name.is_null() { if context.is_null() || name.is_null() {
@@ -1178,14 +1204,15 @@ pub unsafe extern "C" fn dc_create_group_chat(
return 0; return 0;
} }
let ctx = &*context; 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 s
} else { } else {
warn!(ctx, "bad protect-value for dc_create_group_chat()");
return 0; return 0;
}; };
block_on(async move { 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 .await
.log_err(ctx, "Failed to create group chat") .log_err(ctx, "Failed to create group chat")
.map(|id| id.to_u32()) .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] #[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() { if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_verified()"); eprintln!("ignoring careless call to dc_chat_is_protected()");
return 0; return 0;
} }
let ffi_chat = &*chat; let ffi_chat = &*chat;
ffi_chat.chat.is_verified() as libc::c_int ffi_chat.chat.is_protected() as libc::c_int
} }
#[no_mangle] #[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() 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] #[no_mangle]
pub unsafe extern "C" fn dc_msg_is_increation(msg: *mut dc_msg_t) -> libc::c_int { pub unsafe extern "C" fn dc_msg_is_increation(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() { if msg.is_null() {

View File

@@ -4,7 +4,7 @@ use std::str::FromStr;
use anyhow::{bail, ensure}; use anyhow::{bail, ensure};
use async_std::path::Path; 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::chatlist::*;
use deltachat::constants::*; use deltachat::constants::*;
use deltachat::contact::*; use deltachat::contact::*;
@@ -357,7 +357,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
createchat <contact-id>\n\ createchat <contact-id>\n\
createchatbymsg <msg-id>\n\ createchatbymsg <msg-id>\n\
creategroup <name>\n\ creategroup <name>\n\
createverified <name>\n\ createprotected <name>\n\
addmember <contact-id>\n\ addmember <contact-id>\n\
removemember <contact-id>\n\ removemember <contact-id>\n\
groupname <name>\n\ groupname <name>\n\
@@ -379,6 +379,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unarchive <chat-id>\n\ unarchive <chat-id>\n\
pin <chat-id>\n\ pin <chat-id>\n\
unpin <chat-id>\n\ unpin <chat-id>\n\
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\ delchat <chat-id>\n\
===========================Message commands==\n\ ===========================Message commands==\n\
listmsgs <query>\n\ listmsgs <query>\n\
@@ -523,7 +525,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
for i in (0..cnt).rev() { for i in (0..cnt).rev() {
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?; let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
println!( println!(
"{}#{}: {} [{} fresh] {}", "{}#{}: {} [{} fresh] {}{}",
chat_prefix(&chat), chat_prefix(&chat),
chat.get_id(), chat.get_id(),
chat.get_name(), chat.get_name(),
@@ -533,6 +535,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ChatVisibility::Archived => "📦", ChatVisibility::Archived => "📦",
ChatVisibility::Pinned => "📌", ChatVisibility::Pinned => "📌",
}, },
if chat.is_protected() { "🛡️" } else { "" },
); );
let lot = chatlist.get_summary(&context, i, Some(&chat)).await; let lot = chatlist.get_summary(&context, i, Some(&chat)).await;
let statestr = if chat.visibility == ChatVisibility::Archived { 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()) format!("{} member(s)", members.len())
}; };
println!( println!(
"{}#{}: {} [{}]{}{}", "{}#{}: {} [{}]{}{} {}",
chat_prefix(sel_chat), chat_prefix(sel_chat),
sel_chat.get_id(), sel_chat.get_id(),
sel_chat.get_name(), sel_chat.get_name(),
@@ -624,6 +627,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}, },
_ => "".to_string(), _ => "".to_string(),
}, },
if sel_chat.is_protected() {
"🛡️"
} else {
""
},
); );
log_msglist(&context, &msglist).await?; log_msglist(&context, &msglist).await?;
if let Some(draft) = sel_chat.get_id().get_draft(&context).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" => { "creategroup" => {
ensure!(!arg1.is_empty(), "Argument <name> missing."); ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id = 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); println!("Group#{} created successfully.", chat_id);
} }
"createverified" => { "createprotected" => {
ensure!(!arg1.is_empty(), "Argument <name> missing."); ensure!(!arg1.is_empty(), "Argument <name> 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" => { "addmember" => {
ensure!(sel_chat.is_some(), "No chat selected"); 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, "archive" => ChatVisibility::Archived,
"unarchive" | "unpin" => ChatVisibility::Normal, "unarchive" | "unpin" => ChatVisibility::Normal,
"pin" => ChatVisibility::Pinned, "pin" => ChatVisibility::Pinned,
_ => panic!("Unexpected command (This should never happen)"), _ => unreachable!("arg0={:?}", arg0),
},
)
.await?;
}
"protect" | "unprotect" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> 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?; .await?;

View File

@@ -57,10 +57,7 @@ class Chat(object):
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat. :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 ( return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
const.DC_CHAT_TYPE_GROUP,
const.DC_CHAT_TYPE_VERIFIED_GROUP
)
def is_deaddrop(self): def is_deaddrop(self):
""" return true if this chat is a deaddrop chat. """ 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) return not lib.dc_chat_is_unpromoted(self._dc_chat)
def is_verified(self): def is_protected(self):
""" return True if this chat is a verified group. """ 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): def get_name(self):
""" return name of this chat. """ return name of this chat.

View File

@@ -1343,7 +1343,7 @@ class TestOnlineAccount:
ac1, ac2 = acfactory.get_two_online_accounts() ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create verified-group QR, ac2 scans and joins") lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello", verified=True) chat1 = ac1.create_group_chat("hello", verified=True)
assert chat1.is_verified() assert chat1.is_protected()
qr = chat1.get_join_qr() qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol") lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr) chat2 = ac2.qr_join_chat(qr)
@@ -1362,7 +1362,7 @@ class TestOnlineAccount:
lp.sec("ac2: read message and check it's verified chat") lp.sec("ac2: read message and check it's verified chat")
msg = ac2._evtracker.wait_next_incoming_message() msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello" assert msg.text == "hello"
assert msg.chat.is_verified() assert msg.chat.is_protected()
assert msg.is_encrypted() assert msg.is_encrypted()
lp.sec("ac2: send message and let ac1 read it") lp.sec("ac2: send message and let ac1 read it")

View File

@@ -1,5 +1,6 @@
//! # Chat module //! # Chat module
use deltachat_derive::{FromSql, ToSql};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::time::{Duration, SystemTime}; 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. /// Chat ID, including reserved IDs.
/// ///
/// Some chat IDs are reserved to identify special chat types. This /// Some chat IDs are reserved to identify special chat types. This
@@ -146,6 +174,107 @@ impl ChatId {
self.set_blocked(context, Blocked::Not).await; 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. /// Archives or unarchives a chat.
pub async fn set_visibility( pub async fn set_visibility(
self, self,
@@ -538,6 +667,7 @@ pub struct Chat {
pub param: Params, pub param: Params,
is_sending_locations: bool, is_sending_locations: bool,
pub mute_duration: MuteDuration, pub mute_duration: MuteDuration,
protected: ProtectionStatus,
} }
impl Chat { impl Chat {
@@ -547,7 +677,7 @@ impl Chat {
.sql .sql
.query_row( .query_row(
"SELECT c.type, c.name, c.grpid, c.param, c.archived, "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 FROM chats c
WHERE c.id=?;", WHERE c.id=?;",
paramsv![chat_id], paramsv![chat_id],
@@ -562,6 +692,7 @@ impl Chat {
blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(), blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(),
is_sending_locations: row.get(6)?, is_sending_locations: row.get(6)?,
mute_duration: row.get(7)?, mute_duration: row.get(7)?,
protected: row.get(8)?,
}; };
Ok(c) Ok(c)
}, },
@@ -727,9 +858,9 @@ impl Chat {
!self.is_unpromoted() !self.is_unpromoted()
} }
/// Returns true if chat is a verified group chat. /// Returns true if chat protection is enabled.
pub fn is_verified(&self) -> bool { pub fn is_protected(&self) -> bool {
self.typ == Chattype::VerifiedGroup self.protected == ProtectionStatus::Protected
} }
/// Returns true if location streaming is enabled in the chat. /// Returns true if location streaming is enabled in the chat.
@@ -756,15 +887,12 @@ impl Chat {
let mut to_id = 0; let mut to_id = 0;
let mut location_id = 0; let mut location_id = 0;
if !(self.typ == Chattype::Single if !(self.typ == Chattype::Single || self.typ == Chattype::Group) {
|| self.typ == Chattype::Group
|| self.typ == Chattype::VerifiedGroup)
{
error!(context, "Cannot send to chat type #{}.", self.typ,); error!(context, "Cannot send to chat type #{}.", self.typ,);
bail!("Cannot set 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 && !is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await
{ {
emit_event!( emit_event!(
@@ -781,7 +909,7 @@ impl Chat {
let new_rfc724_mid = { let new_rfc724_mid = {
let grpid = match self.typ { let grpid = match self.typ {
Chattype::Group | Chattype::VerifiedGroup => Some(self.grpid.as_str()), Chattype::Group => Some(self.grpid.as_str()),
_ => None, _ => None,
}; };
dc_create_outgoing_rfc724_mid(grpid, &from) dc_create_outgoing_rfc724_mid(grpid, &from)
@@ -805,7 +933,7 @@ impl Chat {
); );
bail!("Cannot set message, contact for {} not found.", self.id); 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 && self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
{ {
msg.param.set_int(Param::AttachGroupImage, 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 /// On the C API this number is one of the
/// `DC_CHAT_TYPE_UNDEFINED`, `DC_CHAT_TYPE_SINGLE`, /// `DC_CHAT_TYPE_UNDEFINED`, `DC_CHAT_TYPE_SINGLE`,
/// `DC_CHAT_TYPE_GROUP` or `DC_CHAT_TYPE_VERIFIED_GROUP` /// or `DC_CHAT_TYPE_GROUP`
/// constants. /// constants.
#[serde(rename = "type")] #[serde(rename = "type")]
pub type_: u32, pub type_: u32,
@@ -1865,7 +1993,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Vec<u32> {
pub async fn create_group_chat( pub async fn create_group_chat(
context: &Context, context: &Context,
verified: VerifiedStatus, protect: ProtectionStatus,
chat_name: impl AsRef<str>, chat_name: impl AsRef<str>,
) -> Result<ChatId, Error> { ) -> Result<ChatId, Error> {
let chat_name = improve_single_line_input(chat_name); let chat_name = improve_single_line_input(chat_name);
@@ -1879,11 +2007,7 @@ pub async fn create_group_chat(
context.sql.execute( context.sql.execute(
"INSERT INTO chats (type, name, grpid, param, created_timestamp) VALUES(?, ?, ?, \'U=1\', ?);", "INSERT INTO chats (type, name, grpid, param, created_timestamp) VALUES(?, ?, ?, \'U=1\', ?);",
paramsv![ paramsv![
if verified != VerifiedStatus::Unverified { Chattype::Group,
Chattype::VerifiedGroup
} else {
Chattype::Group
},
chat_name, chat_name,
grpid, grpid,
time(), time(),
@@ -1907,6 +2031,10 @@ pub async fn create_group_chat(
chat_id: ChatId::new(0), chat_id: ChatId::new(0),
}); });
if protect == ProtectionStatus::Protected {
chat_id.set_protection(context, protect).await?;
}
Ok(chat_id) Ok(chat_id)
} }
@@ -2032,12 +2160,12 @@ pub(crate) async fn add_contact_to_chat_ex(
} }
} else { } else {
// else continue and send status mail // else continue and send status mail
if chat.typ == Chattype::VerifiedGroup if chat.is_protected()
&& contact.is_verified(context).await != VerifiedStatus::BidirectVerified && contact.is_verified(context).await != VerifiedStatus::BidirectVerified
{ {
error!( error!(
context, context,
"Only bidirectional verified contacts can be added to verified groups." "Only bidirectional verified contacts can be added to protected chats."
); );
return Ok(false); return Ok(false);
} }
@@ -2075,7 +2203,7 @@ async fn real_group_exists(context: &Context, chat_id: ChatId) -> bool {
context context
.sql .sql
.exists( .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], paramsv![chat_id],
) )
.await .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( pub(crate) async fn get_chat_id_by_grpid(
context: &Context, context: &Context,
grpid: impl AsRef<str>, grpid: impl AsRef<str>,
@@ -2609,14 +2737,16 @@ pub(crate) async fn get_chat_id_by_grpid(
context context
.sql .sql
.query_row( .query_row(
"SELECT id, blocked, type FROM chats WHERE grpid=?;", "SELECT id, blocked, protected FROM chats WHERE grpid=?;",
paramsv![grpid.as_ref()], paramsv![grpid.as_ref()],
|row| { |row| {
let chat_id = row.get::<_, ChatId>(0)?; let chat_id = row.get::<_, ChatId>(0)?;
let b = row.get::<_, Option<Blocked>>(1)?.unwrap_or_default(); let b = row.get::<_, Option<Blocked>>(1)?.unwrap_or_default();
let v = row.get::<_, Option<Chattype>>(2)?.unwrap_or_default(); let p = row
Ok((chat_id, v == Chattype::VerifiedGroup, b)) .get::<_, Option<ProtectionStatus>>(2)?
.unwrap_or_default();
Ok((chat_id, p == ProtectionStatus::Protected, b))
}, },
) )
.await .await
@@ -2898,7 +3028,7 @@ mod tests {
async fn test_add_contact_to_chat_ex_add_self() { async fn test_add_contact_to_chat_ex_add_self() {
// Adding self to a contact should succeed, even though it's pointless. // Adding self to a contact should succeed, even though it's pointless.
let t = TestContext::new().await; 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 .await
.unwrap(); .unwrap();
let added = add_contact_to_chat_ex(&t.ctx, chat_id, DC_CONTACT_ID_SELF, false) let added = add_contact_to_chat_ex(&t.ctx, chat_id, DC_CONTACT_ID_SELF, false)
@@ -3284,7 +3414,7 @@ mod tests {
.await .await
.unwrap(); .unwrap();
async_std::task::sleep(std::time::Duration::from_millis(1000)).await; 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 .await
.unwrap(); .unwrap();
@@ -3329,7 +3459,7 @@ mod tests {
#[async_std::test] #[async_std::test]
async fn test_set_chat_name() { async fn test_set_chat_name() {
let t = TestContext::new().await; 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 .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -3372,7 +3502,7 @@ mod tests {
#[async_std::test] #[async_std::test]
async fn test_shall_attach_selfavatar() { async fn test_shall_attach_selfavatar() {
let t = TestContext::new().await; 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 .await
.unwrap(); .unwrap();
assert!(!shall_attach_selfavatar(&t.ctx, chat_id).await.unwrap()); assert!(!shall_attach_selfavatar(&t.ctx, chat_id).await.unwrap());
@@ -3396,7 +3526,7 @@ mod tests {
#[async_std::test] #[async_std::test]
async fn test_set_mute_duration() { async fn test_set_mute_duration() {
let t = TestContext::new().await; 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 .await
.unwrap(); .unwrap();
// Initial // Initial

View File

@@ -362,9 +362,7 @@ impl Chatlist {
let mut lastcontact = None; let mut lastcontact = None;
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await { let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await {
if lastmsg.from_id != DC_CONTACT_ID_SELF if lastmsg.from_id != DC_CONTACT_ID_SELF && chat.typ == Chattype::Group {
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{
lastcontact = Contact::load_from_db(context, lastmsg.from_id).await.ok(); lastcontact = Contact::load_from_db(context, lastmsg.from_id).await.ok();
} }
@@ -440,13 +438,13 @@ mod tests {
#[async_std::test] #[async_std::test]
async fn test_try_load() { async fn test_try_load() {
let t = TestContext::new().await; 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 .await
.unwrap(); .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 .await
.unwrap(); .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 .await
.unwrap(); .unwrap();
@@ -489,7 +487,7 @@ mod tests {
async fn test_sort_self_talk_up_on_forward() { async fn test_sort_self_talk_up_on_forward() {
let t = TestContext::new().await; let t = TestContext::new().await;
t.ctx.update_device_chats().await.unwrap(); 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 .await
.unwrap(); .unwrap();
@@ -546,7 +544,7 @@ mod tests {
#[async_std::test] #[async_std::test]
async fn test_get_summary_unwrap() { async fn test_get_summary_unwrap() {
let t = TestContext::new().await; 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 .await
.unwrap(); .unwrap();

View File

@@ -145,7 +145,6 @@ pub enum Chattype {
Undefined = 0, Undefined = 0,
Single = 100, Single = 100,
Group = 120, Group = 120,
VerifiedGroup = 130,
} }
impl Default for Chattype { impl Default for Chattype {

View File

@@ -4,7 +4,7 @@ use sha2::{Digest, Sha256};
use mailparse::SingleInfo; use mailparse::SingleInfo;
use crate::chat::{self, Chat, ChatId}; use crate::chat::{self, Chat, ChatId, ProtectionStatus};
use crate::config::Config; use crate::config::Config;
use crate::constants::*; use crate::constants::*;
use crate::contact::*; use crate::contact::*;
@@ -714,6 +714,19 @@ async fn add_parts(
ephemeral_timer = EphemeralTimer::Disabled; 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, // correct message_timestamp, it should not be used before,
// however, we cannot do this earlier as we need from_id to be set // however, we cannot do this earlier as we need from_id to be set
let in_fresh = state == MessageState::InFresh; 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())) || X_MrAddToGrp.is_some() && addr_cmp(&self_addr, X_MrAddToGrp.as_ref().unwrap()))
{ {
// group does not exist but should be created // 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) = if let Err(err) =
check_verified_properties(context, mime_parser, from_id as u32, to_ids).await 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); let s = format!("{}. See 'Info' for more details", err);
mime_parser.repl_msg_by_error(&s); mime_parser.repl_msg_by_error(&s);
} }
VerifiedStatus::Verified ProtectionStatus::Protected
} else { } else {
VerifiedStatus::Unverified ProtectionStatus::Unprotected
}; };
if !allow_creation { if !allow_creation {
@@ -1222,11 +1235,17 @@ async fn create_or_lookup_group(
&grpid, &grpid,
grpname.as_ref().unwrap(), grpname.as_ref().unwrap(),
create_blocked, create_blocked,
create_verified, create_protected,
) )
.await; .await;
chat_id_blocked = create_blocked; chat_id_blocked = create_blocked;
recreate_member_list = true; 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 // 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 { if let Some(avatar_action) = &mime_parser.group_avatar {
info!(context, "group-avatar change for {}", chat_id); info!(context, "group-avatar change for {}", chat_id);
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await { 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, &grpid,
grpname, grpname,
create_blocked, create_blocked,
VerifiedStatus::Unverified, ProtectionStatus::Unprotected,
) )
.await; .await;
for &member_id in &member_ids { for &member_id in &member_ids {
@@ -1480,20 +1507,17 @@ async fn create_group_record(
grpid: impl AsRef<str>, grpid: impl AsRef<str>,
grpname: impl AsRef<str>, grpname: impl AsRef<str>,
create_blocked: Blocked, create_blocked: Blocked,
create_verified: VerifiedStatus, create_protected: ProtectionStatus,
) -> ChatId { ) -> ChatId {
if context.sql.execute( 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![ paramsv![
if VerifiedStatus::Unverified != create_verified { Chattype::Group,
Chattype::VerifiedGroup
} else {
Chattype::Group
},
grpname.as_ref(), grpname.as_ref(),
grpid.as_ref(), grpid.as_ref(),
create_blocked, create_blocked,
time(), time(),
create_protected,
], ],
).await ).await
.is_err() .is_err()
@@ -1645,6 +1669,11 @@ async fn check_verified_properties(
ensure!(mimeparser.was_encrypted(), "This message is not encrypted."); 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 // ensure, the contact is verified
// and the message is signed with a verified key of the sender. // 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 // 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 { if !is_verified {
bail!( bail!(
"{} is not a member of this verified group", "{} is not a member of this protected chat",
to_addr.to_string() to_addr.to_string()
); );
} }
@@ -2182,7 +2211,7 @@ mod tests {
assert!(one2one.get_visibility() == ChatVisibility::Archived); assert!(one2one.get_visibility() == ChatVisibility::Archived);
// create a group with bob, archive group // 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 .await
.unwrap(); .unwrap();
chat::add_contact_to_chat(&t.ctx, group_id, bob_id).await; chat::add_contact_to_chat(&t.ctx, group_id, bob_id).await;

View File

@@ -544,9 +544,7 @@ impl Message {
return ret; return ret;
}; };
let contact = if self.from_id != DC_CONTACT_ID_SELF as u32 let contact = if self.from_id != DC_CONTACT_ID_SELF as u32 && chat.typ == Chattype::Group {
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{
Contact::get_by_id(context, self.from_id).await.ok() Contact::get_by_id(context, self.from_id).await.ok()
} else { } else {
None None
@@ -591,6 +589,10 @@ impl Message {
|| cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage || cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
} }
pub fn get_info_type(&self) -> SystemMessage {
self.param.get_cmd()
}
pub fn is_system_message(&self) -> bool { pub fn is_system_message(&self) -> bool {
let cmd = self.param.get_cmd(); let cmd = self.param.get_cmd();
cmd != SystemMessage::Unknown cmd != SystemMessage::Unknown
@@ -976,7 +978,7 @@ impl Lot {
); );
self.text1_meaning = Meaning::Text1Self; 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() { if msg.is_info() || contact.is_none() {
self.text1 = None; self.text1 = None;
self.text1_meaning = Meaning::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 { if let Ok((msg_id, chat_id, chat_type)) = res {
set_msg_failed(context, msg_id, error).await; 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 { if let Some(failed_recipient) = &failed.failed_recipient {
let contact_id = let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown).await; Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown).await;

View File

@@ -233,7 +233,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
fn is_e2ee_guaranteed(&self) -> bool { fn is_e2ee_guaranteed(&self) -> bool {
match &self.loaded { match &self.loaded {
Loaded::Message { chat } => { Loaded::Message { chat } => {
if chat.typ == Chattype::VerifiedGroup { if chat.is_protected() {
return true; return true;
} }
@@ -255,7 +255,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
fn min_verified(&self) -> PeerstateVerifiedStatus { fn min_verified(&self) -> PeerstateVerifiedStatus {
match &self.loaded { match &self.loaded {
Loaded::Message { chat } => { Loaded::Message { chat } => {
if chat.typ == Chattype::VerifiedGroup { if chat.is_protected() {
PeerstateVerifiedStatus::BidirectVerified PeerstateVerifiedStatus::BidirectVerified
} else { } else {
PeerstateVerifiedStatus::Unverified PeerstateVerifiedStatus::Unverified
@@ -268,7 +268,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
fn should_force_plaintext(&self) -> bool { fn should_force_plaintext(&self) -> bool {
match &self.loaded { match &self.loaded {
Loaded::Message { chat } => { Loaded::Message { chat } => {
if chat.typ == Chattype::VerifiedGroup { if chat.is_protected() {
false false
} else { } else {
self.msg self.msg
@@ -345,7 +345,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
.stock_str(StockMessage::AcSetupMsgSubject) .stock_str(StockMessage::AcSetupMsgSubject)
.await .await
.into_owned() .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() { let re = if self.in_reply_to.is_empty() {
"" ""
} else { } else {
@@ -708,11 +708,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let mut placeholdertext = None; let mut placeholdertext = None;
let mut meta_part = 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())); 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())); protected_headers.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone()));
let encoded = encode_words(&chat.name); 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(),
));
}
_ => {} _ => {}
} }

View File

@@ -86,6 +86,10 @@ pub enum SystemMessage {
/// Chat ephemeral message timer is changed. /// Chat ephemeral message timer is changed.
EphemeralTimerChanged = 10, EphemeralTimerChanged = 10,
// Chat protection state changed
ChatProtectionEnabled = 11,
ChatProtectionDisabled = 12,
} }
impl Default for SystemMessage { impl Default for SystemMessage {
@@ -123,6 +127,7 @@ impl MimeMessage {
// remove headers that are allowed _only_ in the encrypted part // remove headers that are allowed _only_ in the encrypted part
headers.remove("secure-join-fingerprint"); headers.remove("secure-join-fingerprint");
headers.remove("chat-verified");
// Memory location for a possible decrypted message. // Memory location for a possible decrypted message.
let mail_raw; let mail_raw;
@@ -253,6 +258,10 @@ impl MimeMessage {
self.is_system_message = SystemMessage::LocationStreamingEnabled; self.is_system_message = SystemMessage::LocationStreamingEnabled;
} else if value == "ephemeral-timer-changed" { } else if value == "ephemeral-timer-changed" {
self.is_system_message = SystemMessage::EphemeralTimerChanged; 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(()) Ok(())
@@ -848,6 +857,7 @@ impl MimeMessage {
} }
pub fn repl_msg_by_error(&mut self, error_msg: impl AsRef<str>) { pub fn repl_msg_by_error(&mut self, error_msg: impl AsRef<str>) {
self.is_system_message = SystemMessage::Unknown;
if let Some(part) = self.parts.first_mut() { if let Some(part) = self.parts.first_mut() {
part.typ = Viewtype::Text; part.typ = Viewtype::Text;
part.msg = format!("[{}]", error_msg.as_ref()); part.msg = format!("[{}]", error_msg.as_ref());

View File

@@ -348,7 +348,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
let bob = context.bob.read().await; let bob = context.bob.read().await;
let grpid = bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(); let grpid = bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap();
match chat::get_chat_id_by_grpid(context, grpid).await { 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) => { Err(err) => {
if start.elapsed() > Duration::from_secs(7) { if start.elapsed() > Duration::from_secs(7) {
return Err(JoinError::MissingChat(err)); return Err(JoinError::MissingChat(err));
@@ -791,19 +791,19 @@ pub(crate) async fn handle_securejoin_handshake(
let vg_expect_encrypted = if join_vg { let vg_expect_encrypted = if join_vg {
let group_id = get_qr_attr!(context, text2).to_string(); 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 // false since the group is created by receive_imf by
// the very handshake message we're handling now. But // the very handshake message we're handling now. But
// only after we have returned. It does not impact // only after we have returned. It does not impact
// the security invariants of secure-join however. // 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 .await
.unwrap_or((ChatId::new(0), false, Blocked::Not)); .unwrap_or((ChatId::new(0), false, Blocked::Not));
// when joining a non-verified group // when joining a non-verified group
// the vg-member-added message may be unencrypted // the vg-member-added message may be unencrypted
// when not all group members have keys or prefer encryption. // when not all group members have keys or prefer encryption.
// So only expect encryption if this is a verified group // So only expect encryption if this is a verified group
is_verified_group is_protected_group
} else { } else {
// setup contact is always encrypted // setup contact is always encrypted
true true
@@ -1102,6 +1102,7 @@ mod tests {
use super::*; use super::*;
use crate::chat; use crate::chat;
use crate::chat::ProtectionStatus;
use crate::peerstate::Peerstate; use crate::peerstate::Peerstate;
use crate::test_utils::TestContext; use crate::test_utils::TestContext;
@@ -1328,7 +1329,7 @@ mod tests {
let alice = TestContext::new_alice().await; let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().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 .await
.unwrap(); .unwrap();
@@ -1427,6 +1428,6 @@ mod tests {
let bob_chatid = joiner.await; let bob_chatid = joiner.await;
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await.unwrap(); let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await.unwrap();
assert!(bob_chat.is_verified()); assert!(bob_chat.is_protected());
} }
} }

View File

@@ -1368,6 +1368,20 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);
.await?; .await?;
sql.set_raw_config_int(context, "dbversion", 68).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 // (2) updates that require high-level objects
// (the structure is complete now and all objects are usable) // (the structure is complete now and all objects are usable)

View File

@@ -7,6 +7,7 @@ use strum_macros::EnumProperty;
use crate::blob::BlobObject; use crate::blob::BlobObject;
use crate::chat; use crate::chat;
use crate::chat::ProtectionStatus;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF}; use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::contact::*; use crate::contact::*;
use crate::context::Context; use crate::context::Context;
@@ -233,6 +234,12 @@ pub enum StockMessage {
fallback = "Could not find your mail server.\n\nPlease check your internet connection." fallback = "Could not find your mail server.\n\nPlease check your internet connection."
))] ))]
ErrorNoNetwork = 87, 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> { 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. // 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 // this is worthwhile as this function is typically called