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);
/**
* 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
/*
* @}

View File

@@ -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() {

View File

@@ -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 <contact-id>\n\
createchatbymsg <msg-id>\n\
creategroup <name>\n\
createverified <name>\n\
createprotected <name>\n\
addmember <contact-id>\n\
removemember <contact-id>\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\
pin <chat-id>\n\
unpin <chat-id>\n\
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\
===========================Message commands==\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() {
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 <name> 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 <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" => {
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 <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?;

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.
"""
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.

View File

@@ -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")

View File

@@ -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<u32> {
pub async fn create_group_chat(
context: &Context,
verified: VerifiedStatus,
protect: ProtectionStatus,
chat_name: impl AsRef<str>,
) -> Result<ChatId, Error> {
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<str>,
@@ -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<Blocked>>(1)?.unwrap_or_default();
let v = row.get::<_, Option<Chattype>>(2)?.unwrap_or_default();
Ok((chat_id, v == Chattype::VerifiedGroup, b))
let p = row
.get::<_, Option<ProtectionStatus>>(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

View File

@@ -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();

View File

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

View File

@@ -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<str>,
grpname: impl AsRef<str>,
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;

View File

@@ -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;

View File

@@ -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(),
));
}
_ => {}
}

View File

@@ -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<str>) {
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());

View File

@@ -348,7 +348,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
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());
}
}

View File

@@ -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)

View File

@@ -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