feat: Verified 1:1 chats (#4315)

Implement #4188

BREAKING CHANGE: Remove unused DC_STR_PROTECTION_(EN)ABLED* strings
BREAKING CHANGE: Remove unused dc_set_chat_protection()
This commit is contained in:
Hocuri
2023-07-09 14:06:45 +02:00
committed by GitHub
parent 243c035b03
commit 9cd000c4f2
21 changed files with 1037 additions and 455 deletions

View File

@@ -485,6 +485,13 @@ char* dc_get_blobdir (const dc_context_t* context);
* to not mess up with non-delivery-reports or read-receipts.
* 0=no limit (default).
* Changes affect future messages only.
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
* to 1 if it supports verified 1:1 chats.
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
* and when the key changes, an info message is posted into the chat.
* 0=Nothing else happens when the key changes.
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
* until `dc_accept_chat()` is called.
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
* The prefix should be followed by the system and maybe subsystem,
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
@@ -1470,24 +1477,6 @@ 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, e.g. 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.
*
@@ -3712,7 +3701,6 @@ int dc_chat_can_send (const dc_chat_t* chat);
* 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.
@@ -3721,6 +3709,26 @@ int dc_chat_can_send (const dc_chat_t* chat);
int dc_chat_is_protected (const dc_chat_t* chat);
/**
* Checks if the chat was protected, and then an incoming message broke this protection.
*
* This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
* otherwise it will return false for all chats.
*
* 1:1 chats are automatically set as protected when a contact is verified.
* When a message comes in that is not encrypted / signed correctly,
* the chat is automatically set as unprotected again.
* dc_chat_is_protection_broken() will return true until dc_accept_chat() is called.
*
* The UI should let the user confirm that this is OK with a message like
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat protection broken, 0=otherwise.
*/
int dc_chat_is_protection_broken (const dc_chat_t* chat);
/**
* Check if locations are sent to the chat
* at the time the object was created using dc_get_chat().
@@ -4315,7 +4323,7 @@ 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,
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection()
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(),
* or dc_add_contact_to_chat().
*
* These messages are typically shown in the center of the chat view,
@@ -6749,15 +6757,6 @@ void dc_event_unref(dc_event_t* event);
/// Used in error strings.
#define DC_STR_ERROR_NO_NETWORK 87
/// "Chat protection enabled."
///
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY.
#define DC_STR_PROTECTION_ENABLED 88
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY.
#define DC_STR_PROTECTION_DISABLED 89
/// "Reply"
///
/// Used in summaries.
@@ -7202,26 +7201,6 @@ void dc_event_unref(dc_event_t* event);
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
/// "You enabled chat protection."
///
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED_BY_YOU 158
/// "Chat protection enabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED_BY_OTHER 159
/// "You disabled chat protection."
#define DC_STR_PROTECTION_DISABLED_BY_YOU 160
/// "Chat protection disabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161
/// "Scan to set up second device for %1$s"
///
/// `%1$s` will be replaced by name and address of the account.
@@ -7232,6 +7211,16 @@ void dc_event_unref(dc_event_t* event);
/// Used as a device message after a successful backup transfer.
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
/// "Messages are guaranteed to be end-to-end encrypted from now on."
///
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_ENABLED 170
/// "%1$s sent a message from another device."
///
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_DISABLED 171
/**
* @}
*/

View File

@@ -1429,32 +1429,6 @@ 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,
@@ -3084,6 +3058,16 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i
ffi_chat.chat.is_protected() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_protection_broken()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_protection_broken() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {

View File

@@ -18,6 +18,7 @@ use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::mimeparser::SystemMessage;
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::reaction::send_reaction;
@@ -210,7 +211,17 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
} else {
"[FRESH]"
},
if msg.is_info() { "[INFO]" } else { "" },
if msg.is_info() {
if msg.get_info_type() == SystemMessage::ChatProtectionEnabled {
"[INFO 🛡️]"
} else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled {
"[INFO 🛡️❌]"
} else {
"[INFO]"
}
} else {
""
},
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",
@@ -395,8 +406,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unpin <chat-id>\n\
mute <chat-id> [<seconds>]\n\
unmute <chat-id>\n\
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\
accept <chat-id>\n\
decline <chat-id>\n\
@@ -1056,20 +1065,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
};
chat::set_muted(&context, chat_id, duration).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?;
}
"delchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);

View File

@@ -699,23 +699,6 @@ export class Context extends EventEmitter {
)
}
/**
*
* @param chatId
* @param protect
* @returns success boolean
*/
setChatProtection(chatId: number, protect: boolean) {
debug(`setChatProtection ${chatId} ${protect}`)
return Boolean(
binding.dcn_set_chat_protection(
this.dcn_context,
Number(chatId),
protect ? 1 : 0
)
)
}
getChatEphemeralTimer(chatId: number): number {
debug(`getChatEphemeralTimer ${chatId}`)
return binding.dcn_get_chat_ephemeral_timer(

View File

@@ -1399,18 +1399,6 @@ NAPI_METHOD(dcn_set_chat_name) {
NAPI_RETURN_INT32(result);
}
NAPI_METHOD(dcn_set_chat_protection) {
NAPI_ARGV(3);
NAPI_DCN_CONTEXT();
NAPI_ARGV_UINT32(chat_id, 1);
NAPI_ARGV_INT32(protect, 1);
int result = dc_set_chat_protection(dcn_context->dc_context,
chat_id,
protect);
NAPI_RETURN_INT32(result);
}
NAPI_METHOD(dcn_get_chat_ephemeral_timer) {
NAPI_ARGV(2);
NAPI_DCN_CONTEXT();
@@ -3491,7 +3479,6 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_send_msg);
NAPI_EXPORT_FUNCTION(dcn_send_videochat_invitation);
NAPI_EXPORT_FUNCTION(dcn_set_chat_name);
NAPI_EXPORT_FUNCTION(dcn_set_chat_protection);
NAPI_EXPORT_FUNCTION(dcn_get_chat_ephemeral_timer);
NAPI_EXPORT_FUNCTION(dcn_set_chat_ephemeral_timer);
NAPI_EXPORT_FUNCTION(dcn_set_chat_profile_image);

View File

@@ -86,6 +86,14 @@ pub enum ProtectionStatus {
///
/// All members of the chat must be verified.
Protected = 1,
/// The chat was protected, but now a new message came in
/// which was not encrypted / signed correctly.
/// The user has to confirm that this is OK.
///
/// We only do this in 1:1 chats; in group chats, the chat just
/// stays protected.
ProtectionBroken = 3, // `2` was never used as a value.
}
/// The reason why messages cannot be sent to the chat.
@@ -102,6 +110,10 @@ pub(crate) enum CantSendReason {
/// The chat is a contact request, it needs to be accepted before sending a message.
ContactRequest,
/// The chat was protected, but now a new message came in
/// which was not encrypted / signed correctly.
ProtectionBroken,
/// Mailing list without known List-Post header.
ReadOnlyMailingList,
@@ -118,6 +130,10 @@ impl fmt::Display for CantSendReason {
f,
"contact request chat should be accepted before sending messages"
),
Self::ProtectionBroken => write!(
f,
"accept that the encryption isn't verified anymore before sending messages"
),
Self::ReadOnlyMailingList => {
write!(f, "mailing list does not have a know post address")
}
@@ -270,6 +286,7 @@ impl ChatId {
param: Option<String>,
) -> Result<Self> {
let grpname = strip_rtlo_characters(grpname);
let smeared_time = create_smeared_timestamp(context);
let row_id =
context.sql.insert(
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);",
@@ -278,13 +295,20 @@ impl ChatId {
&grpname,
grpid,
create_blocked,
create_smeared_timestamp(context),
smeared_time,
create_protected,
param.unwrap_or_default(),
),
).await?;
let chat_id = ChatId::new(u32::try_from(row_id)?);
if create_protected == ProtectionStatus::Protected {
chat_id
.add_protection_msg(context, ProtectionStatus::Protected, None, smeared_time)
.await?;
}
info!(
context,
"Created group/mailinglist '{}' grpid={} as {}, blocked={}.",
@@ -374,6 +398,13 @@ impl ChatId {
match chat.typ {
Chattype::Undefined => bail!("Can't accept chat of undefined chattype"),
Chattype::Single if chat.protected == ProtectionStatus::ProtectionBroken => {
// The chat was in the 'Request' state because the protection was broken.
// The user clicked 'Accept', so, now we want to set the status to Unprotected again:
chat.id
.inner_set_protection(context, ProtectionStatus::Unprotected)
.await?;
}
Chattype::Single | Chattype::Group | Chattype::Broadcast => {
// User has "created a chat" with all these contacts.
//
@@ -400,20 +431,19 @@ impl ChatId {
/// Sets protection without sending a message.
///
/// Used when a message arrives indicating that someone else has
/// changed the protection value for a chat.
/// Returns whether the protection status was actually modified.
pub(crate) async fn inner_set_protection(
self,
context: &Context,
protect: ProtectionStatus,
) -> Result<()> {
ensure!(!self.is_special(), "Invalid chat-id.");
) -> Result<bool> {
ensure!(!self.is_special(), "Invalid chat-id {self}.");
let chat = Chat::load_from_db(context, self).await?;
if protect == chat.protected {
info!(context, "Protection status unchanged for {}.", self);
return Ok(());
return Ok(false);
}
match protect {
@@ -430,7 +460,7 @@ impl ChatId {
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
Chattype::Undefined => bail!("Undefined group type"),
},
ProtectionStatus::Unprotected => {}
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
};
context
@@ -443,68 +473,58 @@ impl ChatId {
// make sure, the receivers will get all keys
self.reset_gossiped_timestamp(context).await?;
Ok(())
Ok(true)
}
/// Send protected status message to the chat.
/// Adds an info message to the chat, telling the user that the protection status changed.
///
/// 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.
/// Params:
///
/// If `promote` is false this means, the message must not be sent out
/// and only a local info message should be added to the chat.
/// This is used when protection is enabled implicitly or when a chat is not yet promoted.
/// * `contact_id`: In a 1:1 chat, pass the chat partner's contact id.
/// * `timestamp_sort` is used as the timestamp of the added message
/// and should be the timestamp of the change happening.
pub(crate) async fn add_protection_msg(
self,
context: &Context,
protect: ProtectionStatus,
promote: bool,
from_id: ContactId,
contact_id: Option<ContactId>,
timestamp_sort: i64,
) -> Result<()> {
let text = context.stock_protection_msg(protect, from_id).await;
let text = context.stock_protection_msg(protect, contact_id).await;
let cmd = match protect {
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
};
if promote {
let mut msg = Message {
viewtype: Viewtype::Text,
text,
..Default::default()
};
msg.param.set_cmd(cmd);
send_msg(context, self, &mut msg).await?;
} else {
add_info_msg_with_cmd(
context,
self,
&text,
cmd,
create_smeared_timestamp(context),
None,
None,
None,
)
.await?;
}
add_info_msg_with_cmd(context, self, &text, cmd, timestamp_sort, None, None, None).await?;
Ok(())
}
/// Sets protection and sends or adds a message.
pub async fn set_protection(self, context: &Context, protect: ProtectionStatus) -> Result<()> {
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);
///
/// `timestamp_sort` is used as the timestamp of the added message
/// and should be the timestamp of the change happening.
pub(crate) async fn set_protection(
self,
context: &Context,
protect: ProtectionStatus,
timestamp_sort: i64,
contact_id: Option<ContactId>,
) -> Result<()> {
match self.inner_set_protection(context, protect).await {
Ok(protection_status_modified) => {
if protection_status_modified {
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
.await?;
}
Ok(())
}
Err(e) => {
error!(context, "Cannot set protection: {e:#}."); // make error user-visible
Err(e)
}
}
self.add_protection_msg(context, protect, chat.is_promoted(), ContactId::SELF)
.await
}
/// Archives or unarchives a chat.
@@ -1141,7 +1161,7 @@ pub struct Chat {
pub grpid: String,
/// Whether the chat is blocked, unblocked or a contact request.
pub(crate) blocked: Blocked,
pub blocked: Blocked,
/// Additional chat parameters stored in the database.
pub param: Params,
@@ -1153,7 +1173,7 @@ pub struct Chat {
pub mute_duration: MuteDuration,
/// If the chat is protected (verified).
protected: ProtectionStatus,
pub(crate) protected: ProtectionStatus,
}
impl Chat {
@@ -1247,6 +1267,8 @@ impl Chat {
Some(DeviceChat)
} else if self.is_contact_request() {
Some(ContactRequest)
} else if self.is_protection_broken() {
Some(ProtectionBroken)
} else if self.is_mailing_list() && self.param.get(Param::ListPost).is_none_or_empty() {
Some(ReadOnlyMailingList)
} else if !self.is_self_in_chat(context).await? {
@@ -1410,6 +1432,27 @@ impl Chat {
self.protected == ProtectionStatus::Protected
}
/// Returns true if the chat was protected, and then an incoming message broke this protection.
///
/// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
/// otherwise it will return false for all chats.
///
/// 1:1 chats are automatically set as protected when a contact is verified.
/// When a message comes in that is not encrypted / signed correctly,
/// the chat is automatically set as unprotected again.
/// `is_protection_broken()` will return true until `chat_id.accept()` is called.
///
/// The UI should let the user confirm that this is OK with a message like
/// `Bob sent a message from another device. Tap to learn more`
/// and then call `chat_id.accept()`.
pub fn is_protection_broken(&self) -> bool {
match self.protected {
ProtectionStatus::Protected => false,
ProtectionStatus::Unprotected => false,
ProtectionStatus::ProtectionBroken => true,
}
}
/// Returns true if location streaming is enabled in the chat.
pub fn is_sending_locations(&self) -> bool {
self.is_sending_locations
@@ -1440,15 +1483,6 @@ impl Chat {
let mut to_id = 0;
let mut location_id = 0;
if let Some(reason) = self.why_cant_send(context).await? {
if self.typ == Chattype::Group && reason == CantSendReason::NotAMember {
context.emit_event(EventType::ErrorSelfNotInGroup(
"Cannot send message; self not in group.".into(),
));
}
bail!("Cannot send message to {}: {}", self.id, reason);
}
let from = context.get_primary_self_addr().await?;
let new_rfc724_mid = {
let grpid = match self.typ {
@@ -1964,19 +1998,28 @@ impl ChatIdBlocked {
_ => (),
}
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
let protected = peerstate.map_or(false, |p| p.is_using_verified_key());
let smeared_time = create_smeared_timestamp(context);
let chat_id = context
.sql
.transaction(move |transaction| {
transaction.execute(
"INSERT INTO chats
(type, name, param, blocked, created_timestamp)
VALUES(?, ?, ?, ?, ?)",
(type, name, param, blocked, created_timestamp, protected)
VALUES(?, ?, ?, ?, ?, ?)",
(
Chattype::Single,
chat_name,
params.to_string(),
create_blocked as u8,
create_smeared_timestamp(context),
smeared_time,
if protected {
ProtectionStatus::Protected
} else {
ProtectionStatus::Unprotected
},
),
)?;
let chat_id = ChatId::new(
@@ -1997,6 +2040,17 @@ impl ChatIdBlocked {
})
.await?;
if protected {
chat_id
.add_protection_msg(
context,
ProtectionStatus::Protected,
Some(contact_id),
smeared_time,
)
.await?;
}
match contact_id {
ContactId::SELF => update_saved_messages_icon(context).await?,
ContactId::DEVICE => update_device_icon(context).await?,
@@ -2100,7 +2154,13 @@ async fn prepare_msg_common(
// Check if the chat can be sent to.
if let Some(reason) = chat.why_cant_send(context).await? {
bail!("cannot send to {}: {}", chat_id, reason);
if reason == CantSendReason::ProtectionBroken
&& msg.param.get_cmd() == SystemMessage::SecurejoinMessage
{
// Send out the message, the securejoin message is supposed to repair the verification
} else {
bail!("cannot send to {chat_id}: {reason}");
}
}
// check current MessageState for drafts (to keep msg_id) ...
@@ -2850,18 +2910,14 @@ pub async fn create_group_chat(
let grpid = create_id();
let timestamp = create_smeared_timestamp(context);
let row_id = context
.sql
.insert(
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
(
Chattype::Group,
chat_name,
grpid,
create_smeared_timestamp(context),
),
(Chattype::Group, chat_name, grpid, timestamp),
)
.await?;
@@ -2873,9 +2929,9 @@ pub async fn create_group_chat(
context.emit_msgs_changed_without_ids();
if protect == ProtectionStatus::Protected {
// this part is to stay compatible to verified groups,
// in some future, we will drop the "protect"-flag from create_group_chat()
chat_id.inner_set_protection(context, protect).await?;
chat_id
.set_protection(context, protect, timestamp, None)
.await?;
}
Ok(chat_id)
@@ -5131,72 +5187,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_protection() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config_bool(Config::BccSelf, false).await?;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(chat.is_unpromoted());
// enable protection on unpromoted chat, the info-message is added via add_info_msg()
chat_id
.set_protection(&t, ProtectionStatus::Protected)
.await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(chat.is_protected());
assert!(chat.is_unpromoted());
let msgs = get_chat_msgs(&t, chat_id).await?;
assert_eq!(msgs.len(), 1);
let msg = t.get_last_msg_in(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
assert_eq!(msg.get_state(), MessageState::InNoticed);
// disable protection again, still unpromoted
chat_id
.set_protection(&t, ProtectionStatus::Unprotected)
.await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(chat.is_unpromoted());
let msg = t.get_last_msg_in(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionDisabled);
assert_eq!(msg.get_state(), MessageState::InNoticed);
// send a message, this switches to promoted state
send_text_msg(&t, chat_id, "hi!".to_string()).await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(!chat.is_unpromoted());
let msgs = get_chat_msgs(&t, chat_id).await?;
assert_eq!(msgs.len(), 3);
// enable protection on promoted chat, the info-message is sent via send_msg() this time
chat_id
.set_protection(&t, ProtectionStatus::Protected)
.await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(chat.is_protected());
assert!(!chat.is_unpromoted());
let msg = t.get_last_msg_in(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lookup_by_contact_id() {
let ctx = TestContext::new_alice().await;

View File

@@ -311,6 +311,16 @@ pub enum Config {
/// Last message processed by the bot.
LastMsgId,
/// Feature flag for verified 1:1 chats; the UI should set it
/// to 1 if it supports verified 1:1 chats.
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
/// and when the key changes, an info message is posted into the chat.
/// 0=Nothing else happens when the key changes.
/// 1=After the key changed, `can_send()` returns false and `is_protection_broken()` returns true
/// until `chat_id.accept()` is called.
#[strum(props(default = "0"))]
VerifiedOneOnOneChats,
}
impl Context {

View File

@@ -755,6 +755,12 @@ impl Context {
"last_msg_id",
self.get_config_int(Config::LastMsgId).await?.to_string(),
);
res.insert(
"verified_one_on_one_chats",
self.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
.to_string(),
);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));

View File

@@ -8,7 +8,6 @@
missing_debug_implementations,
missing_docs,
clippy::all,
clippy::indexing_slicing,
clippy::wildcard_imports,
clippy::needless_borrow,
clippy::cast_lossless,
@@ -17,6 +16,7 @@
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,

View File

@@ -896,7 +896,17 @@ impl<'a> MimeFactory<'a> {
let mut placeholdertext = None;
let mut meta_part = None;
if chat.is_protected() {
let send_verified_headers = match chat.typ {
Chattype::Undefined => bail!("Undefined chat type"),
// In single chats, the protection status isn't necessarily the same for both sides,
// so we don't send the Chat-Verified header:
Chattype::Single => false,
Chattype::Group => true,
// Mailinglists and broadcast lists can actually never be verified:
Chattype::Mailinglist => false,
Chattype::Broadcast => false,
};
if chat.is_protected() && send_verified_headers {
headers
.protected
.push(Header::new("Chat-Verified".to_string(), "1".to_string()));

View File

@@ -392,6 +392,31 @@ impl Peerstate {
}
}
/// Returns a reference to the contact's public key fingerprint.
///
/// Similar to [`Self::peek_key`], but returns the fingerprint instead of the key.
fn peek_key_fingerprint(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Fingerprint> {
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key_fingerprint.as_ref(),
PeerstateVerifiedStatus::Unverified => self
.public_key_fingerprint
.as_ref()
.or(self.gossip_key_fingerprint.as_ref()),
}
}
/// Returns true if the key used for opportunistic encryption in the 1:1 chat
/// is the same as the verified key.
///
/// Note that verified groups always use the verified key no matter if the
/// opportunistic key matches or not.
pub(crate) fn is_using_verified_key(&self) -> bool {
let verified = self.peek_key_fingerprint(PeerstateVerifiedStatus::BidirectVerified);
verified.is_some()
&& verified == self.peek_key_fingerprint(PeerstateVerifiedStatus::Unverified)
}
/// Set this peerstate to verified
/// Make sure to call `self.save_to_db` to save these changes
/// Params:

View File

@@ -4,7 +4,7 @@ use std::cmp::min;
use std::collections::HashSet;
use std::convert::TryFrom;
use anyhow::{bail, ensure, Context as _, Result};
use anyhow::{Context as _, Result};
use mailparse::{parse_mail, SingleInfo};
use num_traits::FromPrimitive;
use once_cell::sync::Lazy;
@@ -14,7 +14,7 @@ use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
use crate::contact::{
may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin, VerifiedStatus,
may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin,
};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc_inner;
@@ -593,12 +593,17 @@ async fn add_parts(
// In lookup_chat_by_reply() and create_or_lookup_group(), it can happen that the message is put into a chat
// but the From-address is not a member of this chat.
if let Some(chat_id) = chat_id {
if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
let chat = Chat::load_from_db(context, chat_id).await?;
if let Some(group_chat_id) = chat_id {
if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? {
let chat = Chat::load_from_db(context, group_chat_id).await?;
if chat.is_protected() {
let s = stock_str::unknown_sender_for_chat(context).await;
mime_parser.repl_msg_by_error(&s);
if chat.typ == Chattype::Single {
// Just assign the message to the 1:1 chat with the actual sender instead.
chat_id = None;
} else {
let s = stock_str::unknown_sender_for_chat(context).await;
mime_parser.repl_msg_by_error(&s);
}
} else {
// In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
// to the sender's name, indicating to the user that he/she is not part of the group.
@@ -614,7 +619,7 @@ async fn add_parts(
context,
mime_parser,
sent_timestamp,
chat_id,
group_chat_id,
from_id,
to_ids,
)
@@ -717,6 +722,44 @@ async fn add_parts(
);
}
}
// The next block checks if the message was sent with verified encryption
// and sets the protection of the 1:1 chat accordingly.
if is_partial_download.is_none()
&& mime_parser.get_header(HeaderDef::SecureJoin).is_none()
&& !is_mdn
{
let mut new_protection = match has_verified_encryption(
context,
mime_parser,
from_id,
to_ids,
Chattype::Single,
)
.await?
{
VerifiedEncryption::Verified => ProtectionStatus::Protected,
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
};
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.protected != new_protection {
if new_protection == ProtectionStatus::Unprotected
&& context
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
{
new_protection = ProtectionStatus::ProtectionBroken;
}
let sort_timestamp =
calc_sort_timestamp(context, sent_timestamp, chat_id, true).await?;
chat_id
.set_protection(context, new_protection, sort_timestamp, Some(from_id))
.await?;
}
}
}
}
@@ -983,42 +1026,14 @@ async fn add_parts(
// if a chat is protected and the message is fully downloaded, check additional properties
if !chat_id.is_special() && is_partial_download.is_none() {
let chat = Chat::load_from_db(context, chat_id).await?;
let new_status = match mime_parser.is_system_message {
SystemMessage::ChatProtectionEnabled => Some(ProtectionStatus::Protected),
SystemMessage::ChatProtectionDisabled => Some(ProtectionStatus::Unprotected),
_ => None,
};
if chat.is_protected() || new_status.is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await
if chat.is_protected() {
if let VerifiedEncryption::NotVerified(err) =
has_verified_encryption(context, mime_parser, from_id, to_ids, chat.typ).await?
{
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.repl_msg_by_error(&s);
} else {
// change chat protection only when verification check passes
if let Some(new_status) = new_status {
if chat_id
.update_timestamp(
context,
Param::ProtectionSettingsTimestamp,
sent_timestamp,
)
.await?
{
if let Err(e) = chat_id.inner_set_protection(context, new_status).await {
chat::add_info_msg(
context,
chat_id,
&format!("Cannot set protection: {e}"),
sort_timestamp,
)
.await?;
// do not return an error as this would result in retrying the message
}
}
better_msg = Some(context.stock_protection_msg(new_status, from_id).await);
}
}
}
}
@@ -1520,7 +1535,9 @@ async fn create_or_lookup_group(
}
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await {
if let VerifiedEncryption::NotVerified(err) =
has_verified_encryption(context, mime_parser, from_id, to_ids, Chattype::Group).await?
{
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.repl_msg_by_error(&s);
@@ -1768,20 +1785,6 @@ async fn apply_group_changes(
}
}
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await {
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.repl_msg_by_error(&s);
}
if !chat.is_protected() {
chat_id
.inner_set_protection(context, ProtectionStatus::Protected)
.await?;
}
}
// Recreate the member list.
if recreate_member_list {
if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
@@ -2118,49 +2121,53 @@ async fn create_adhoc_group(
Ok(Some(new_chat_id))
}
async fn check_verified_properties(
enum VerifiedEncryption {
Verified,
NotVerified(String), // The string contains the reason why it's not verified
}
/// Checks whether the message is allowed to appear in a protected chat.
///
/// This means that it is encrypted, signed with a verified key,
/// and if it's a group, all the recipients are verified.
async fn has_verified_encryption(
context: &Context,
mimeparser: &MimeMessage,
from_id: ContactId,
to_ids: &[ContactId],
) -> Result<()> {
let contact = Contact::get_by_id(context, from_id).await?;
chat_type: Chattype,
) -> Result<VerifiedEncryption> {
use VerifiedEncryption::*;
ensure!(mimeparser.was_encrypted(), "This message is not encrypted");
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
// we do not fail here currently, this would exclude (a) non-deltas
// and (b) deltas with different protection views across multiple devices.
// for group creation or protection enabled/disabled, however, Chat-Verified is respected.
warn!(
context,
"{} did not mark message as protected.",
contact.get_addr()
);
if from_id == ContactId::SELF && chat_type == Chattype::Single {
// For outgoing emails in the 1:1 chat, we have an exception that
// they are allowed to be unencrypted:
// 1. They can't be an attack (they are outgoing, not incoming)
// 2. Probably the unencryptedness is just a temporary state, after all
// the user obviously still uses DC
// -> Showing info messages everytime would be a lot of noise
// 3. The info messages that are shown to the user ("Your chat partner
// likely reinstalled DC" or similar) would be wrong.
return Ok(Verified);
}
if !mimeparser.was_encrypted() {
return Ok(NotVerified("This message is not encrypted".to_string()));
};
// 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
// and results in group-splits otherwise.
if from_id != ContactId::SELF {
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
let Some(peerstate) = &mimeparser.decryption_info.peerstate else {
return Ok(NotVerified("No peerstate, the contact isn't verified".to_string()));
};
if peerstate.is_none()
|| contact.is_verified_ex(context, peerstate.as_ref()).await?
!= VerifiedStatus::BidirectVerified
{
bail!(
"Sender of this message is not verified: {}",
contact.get_addr()
);
}
if let Some(peerstate) = peerstate {
ensure!(
peerstate.has_verified_key(&mimeparser.signatures),
"The message was sent with non-verified encryption"
);
if !peerstate.has_verified_key(&mimeparser.signatures) {
return Ok(NotVerified(
"The message was sent with non-verified encryption".to_string(),
));
}
}
@@ -2172,7 +2179,7 @@ async fn check_verified_properties(
.collect::<Vec<ContactId>>();
if to_ids.is_empty() {
return Ok(());
return Ok(Verified);
}
let rows = context
@@ -2196,10 +2203,12 @@ async fn check_verified_properties(
)
.await?;
let contact = Contact::get_by_id(context, from_id).await?;
for (to_addr, mut is_verified) in rows {
info!(
context,
"check_verified_properties: {:?} self={:?}.",
"has_verified_encryption: {:?} self={:?}.",
to_addr,
context.is_self_addr(&to_addr).await
);
@@ -2233,13 +2242,13 @@ async fn check_verified_properties(
}
}
if !is_verified {
bail!(
return Ok(NotVerified(format!(
"{} is not a member of this protected chat",
to_addr.to_string()
);
to_addr
)));
}
}
Ok(())
Ok(Verified)
}
/// Returns the last message referenced from `References` header if it is in the database.

View File

@@ -6,7 +6,7 @@ use anyhow::{bail, Context as _, Error, Result};
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked};
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
@@ -701,6 +701,14 @@ async fn secure_connection_established(
let contact = Contact::get_by_id(context, contact_id).await?;
let msg = stock_str::contact_verified(context, &contact).await;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
time(),
Some(contact_id),
)
.await?;
context.emit_event(EventType::ChatModified(chat_id));
Ok(())
}
@@ -783,6 +791,8 @@ mod tests {
use crate::contact::VerifiedStatus;
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::stock_str::chat_protection_enabled;
use crate::test_utils::get_chat_msg;
use crate::test_utils::{TestContext, TestContextManager};
use crate::tools::EmailAddress;
@@ -921,7 +931,7 @@ mod tests {
// Check Alice got the verified message in her 1:1 chat.
{
let chat = alice.create_chat(&bob).await;
let msg_id = chat::get_chat_msgs(&alice.ctx, chat.get_id())
let msg_ids: Vec<_> = chat::get_chat_msgs(&alice.ctx, chat.get_id())
.await
.unwrap()
.into_iter()
@@ -929,11 +939,17 @@ mod tests {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.max()
.expect("No messages in Alice's 1:1 chat");
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
assert!(msg.get_text().contains("bob@example.net verified"));
.collect();
assert_eq!(msg_ids.len(), 2);
let msg0 = Message::load_from_db(&alice.ctx, msg_ids[0]).await.unwrap();
assert!(msg0.is_info());
assert!(msg0.get_text().contains("bob@example.net verified"));
let msg1 = Message::load_from_db(&alice.ctx, msg_ids[1]).await.unwrap();
assert!(msg1.is_info());
let expected_text = chat_protection_enabled(&alice).await;
assert_eq!(msg1.get_text(), expected_text);
}
// Check Alice sent the right message to Bob.
@@ -969,7 +985,7 @@ mod tests {
// Check Bob got the verified message in his 1:1 chat.
{
let chat = bob.create_chat(&alice).await;
let msg_id = chat::get_chat_msgs(&bob.ctx, chat.get_id())
let msg_ids: Vec<_> = chat::get_chat_msgs(&bob.ctx, chat.get_id())
.await
.unwrap()
.into_iter()
@@ -977,11 +993,16 @@ mod tests {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.max()
.expect("No messages in Bob's 1:1 chat");
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
assert!(msg.get_text().contains("alice@example.org verified"));
.collect();
let msg0 = Message::load_from_db(&bob.ctx, msg_ids[0]).await.unwrap();
assert!(msg0.is_info());
assert!(msg0.get_text().contains("alice@example.org verified"));
let msg1 = Message::load_from_db(&bob.ctx, msg_ids[1]).await.unwrap();
assert!(msg1.is_info());
let expected_text = chat_protection_enabled(&bob).await;
assert_eq!(msg1.get_text(), expected_text);
}
// Check Bob sent the final message
@@ -1278,17 +1299,11 @@ mod tests {
Blocked::Yes,
"Alice's 1:1 chat with Bob is not hidden"
);
let msg_id = chat::get_chat_msgs(&alice.ctx, alice_chatid)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.min()
.expect("No messages in Alice's group chat");
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
// There should be 3 messages in the chat:
// - The ChatProtectionEnabled message
// - bob@example.net verified
// - You added member bob@example.net
let msg = get_chat_msg(&alice, alice_chatid, 1, 3).await;
assert!(msg.is_info());
assert!(msg.get_text().contains("bob@example.net verified"));
}

View File

@@ -222,6 +222,14 @@ impl BobState {
let msg = stock_str::contact_verified(context, &contact).await;
let chat_id = self.joining_chat_id(context).await?;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
time(),
Some(contact.id),
)
.await?;
context.emit_event(EventType::ChatModified(chat_id));
Ok(())
}

View File

@@ -393,18 +393,6 @@ pub enum StockMessage {
#[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
MsgEphemeralTimerWeeksBy = 157,
#[strum(props(fallback = "You enabled chat protection."))]
YouEnabledProtection = 158,
#[strum(props(fallback = "Chat protection enabled by %1$s."))]
ProtectionEnabledBy = 159,
#[strum(props(fallback = "You disabled chat protection."))]
YouDisabledProtection = 160,
#[strum(props(fallback = "Chat protection disabled by %1$s."))]
ProtectionDisabledBy = 161,
#[strum(props(fallback = "Scan to set up second device for %1$s"))]
BackupTransferQr = 162,
@@ -419,6 +407,12 @@ pub enum StockMessage {
#[strum(props(fallback = "I left the group."))]
MsgILeftGroup = 166,
#[strum(props(fallback = "Messages are guaranteed to be end-to-end encrypted from now on."))]
ChatProtectionEnabled = 170,
#[strum(props(fallback = "%1$s sent a message from another device."))]
ChatProtectionDisabled = 171,
}
impl StockMessage {
@@ -515,13 +509,21 @@ trait StockStringMods: AsRef<str> + Sized {
}
impl ContactId {
/// Get contact name for stock string.
async fn get_stock_name(self, context: &Context) -> String {
/// Get contact name and address for stock string, e.g. `Bob (bob@example.net)`
async fn get_stock_name_n_addr(self, context: &Context) -> String {
Contact::get_by_id(context, self)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_else(|_| self.to_string())
}
/// Get contact name, e.g. `Bob`, or `bob@exmple.net` if no name is set.
async fn get_stock_name(self, context: &Context) -> String {
Contact::get_by_id(context, self)
.await
.map(|contact| contact.get_display_name().to_string())
.unwrap_or_else(|_| self.to_string())
}
}
impl StockStringMods for String {}
@@ -583,7 +585,7 @@ pub(crate) async fn msg_grp_name(
.await
.replace1(from_group)
.replace2(to_group)
.replace3(&by_contact.get_stock_name(context).await)
.replace3(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -593,7 +595,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
} else {
translated(context, StockMessage::MsgGrpImgChangedBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -640,7 +642,7 @@ pub(crate) async fn msg_add_member_local(
translated(context, StockMessage::MsgAddMemberBy)
.await
.replace1(whom)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -687,7 +689,7 @@ pub(crate) async fn msg_del_member_local(
translated(context, StockMessage::MsgDelMemberBy)
.await
.replace1(whom)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -703,7 +705,7 @@ pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactI
} else {
translated(context, StockMessage::MsgGroupLeftBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -756,7 +758,7 @@ pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId
} else {
translated(context, StockMessage::MsgGrpImgDeletedBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -782,13 +784,9 @@ pub(crate) async fn secure_join_started(
/// Stock string: `%1$s replied, waiting for being added to the group…`.
pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
translated(context, StockMessage::SecureJoinReplies)
.await
.replace1(contact.get_display_name())
} else {
format!("secure_join_replies: unknown contact {contact_id}")
}
translated(context, StockMessage::SecureJoinReplies)
.await
.replace1(&contact_id.get_stock_name(context).await)
}
/// Stock string: `Scan to chat with %1$s`.
@@ -881,7 +879,7 @@ pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactI
} else {
translated(context, StockMessage::MsgLocationEnabledBy)
.await
.replace1(&contact.get_stock_name(context).await)
.replace1(&contact.get_stock_name_n_addr(context).await)
}
}
@@ -950,7 +948,7 @@ pub(crate) async fn msg_ephemeral_timer_disabled(
} else {
translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -968,7 +966,7 @@ pub(crate) async fn msg_ephemeral_timer_enabled(
translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
.await
.replace1(timer)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -979,7 +977,7 @@ pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: Co
} else {
translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -990,7 +988,7 @@ pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: Cont
} else {
translated(context, StockMessage::MsgEphemeralTimerHourBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -1001,7 +999,7 @@ pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: Conta
} else {
translated(context, StockMessage::MsgEphemeralTimerDayBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -1012,7 +1010,7 @@ pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: Cont
} else {
translated(context, StockMessage::MsgEphemeralTimerWeekBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -1053,26 +1051,16 @@ pub(crate) async fn error_no_network(context: &Context) -> String {
translated(context, StockMessage::ErrorNoNetwork).await
}
/// Stock string: `Chat protection enabled.`.
pub(crate) async fn protection_enabled(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::YouEnabledProtection).await
} else {
translated(context, StockMessage::ProtectionEnabledBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
/// Stock string: `Messages are guaranteed to be end-to-end encrypted from now on.`
pub(crate) async fn chat_protection_enabled(context: &Context) -> String {
translated(context, StockMessage::ChatProtectionEnabled).await
}
/// Stock string: `Chat protection disabled.`.
pub(crate) async fn protection_disabled(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::YouDisabledProtection).await
} else {
translated(context, StockMessage::ProtectionDisabledBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
/// Stock string: `%1$s sent a message from another device.`
pub(crate) async fn chat_protection_disabled(context: &Context, contact_id: ContactId) -> String {
translated(context, StockMessage::ChatProtectionDisabled)
.await
.replace1(&contact_id.get_stock_name(context).await)
}
/// Stock string: `Reply`.
@@ -1104,7 +1092,7 @@ pub(crate) async fn msg_ephemeral_timer_minutes(
translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
.await
.replace1(minutes)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -1122,7 +1110,7 @@ pub(crate) async fn msg_ephemeral_timer_hours(
translated(context, StockMessage::MsgEphemeralTimerHoursBy)
.await
.replace1(hours)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -1140,7 +1128,7 @@ pub(crate) async fn msg_ephemeral_timer_days(
translated(context, StockMessage::MsgEphemeralTimerDaysBy)
.await
.replace1(days)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -1158,7 +1146,7 @@ pub(crate) async fn msg_ephemeral_timer_weeks(
translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
.await
.replace1(weeks)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -1332,11 +1320,19 @@ impl Context {
pub(crate) async fn stock_protection_msg(
&self,
protect: ProtectionStatus,
from_id: ContactId,
contact_id: Option<ContactId>,
) -> String {
match protect {
ProtectionStatus::Unprotected => protection_enabled(self, from_id).await,
ProtectionStatus::Protected => protection_disabled(self, from_id).await,
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {
if let Some(contact_id) = contact_id {
chat_protection_disabled(self, contact_id).await
} else {
// In a group chat, it's not possible to downgrade verification.
// In a 1:1 chat, the `contact_id` always has to be provided.
"[Error] No contact_id given".to_string()
}
}
ProtectionStatus::Protected => chat_protection_enabled(self).await,
}
}

View File

@@ -31,11 +31,14 @@ use crate::constants::Chattype;
use crate::constants::{DC_GCL_NO_SPECIALS, DC_MSG_ID_DAYMARKER};
use crate::contact::{Contact, ContactAddress, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::e2ee::EncryptHelper;
use crate::events::{Event, EventType, Events};
use crate::key::{self, DcKey, KeyPair, KeyPairUse};
use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype};
use crate::mimeparser::MimeMessage;
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::securejoin::{get_securejoin_qr, join_securejoin};
use crate::stock_str::StockStrings;
use crate::tools::EmailAddress;
@@ -108,9 +111,15 @@ impl TestContextManager {
/// - Let one TestContext send a message
/// - Let the other TestContext receive it and accept the chat
/// - Assert that the message arrived
pub async fn send_recv_accept(&self, from: &TestContext, to: &TestContext, msg: &str) {
pub async fn send_recv_accept(
&self,
from: &TestContext,
to: &TestContext,
msg: &str,
) -> Message {
let received_msg = self.send_recv(from, to, msg).await;
received_msg.chat_id.accept(to).await.unwrap();
received_msg
}
/// - Let one TestContext send a message
@@ -152,6 +161,27 @@ impl TestContextManager {
new_addr
);
}
pub async fn execute_securejoin(&self, scanner: &TestContext, scanned: &TestContext) {
self.section(&format!(
"{} scans {}'s QR code",
scanner.name(),
scanned.name()
));
let qr = get_securejoin_qr(&scanned.ctx, None).await.unwrap();
join_securejoin(&scanner.ctx, &qr).await.unwrap();
loop {
if let Some(sent) = scanner.pop_sent_msg_opt(Duration::ZERO).await {
scanned.recv_msg(&sent).await;
} else if let Some(sent) = scanned.pop_sent_msg_opt(Duration::ZERO).await {
scanner.recv_msg(&sent).await;
} else {
break;
}
}
}
}
#[derive(Debug, Clone, Default)]
@@ -636,7 +666,7 @@ impl TestContext {
// We're using `unwrap_or_default()` here so that if the file doesn't exist,
// it can be created using `write` below.
let expected = fs::read(&filename).await.unwrap_or_default();
let expected = String::from_utf8(expected).unwrap();
let expected = String::from_utf8(expected).unwrap().replace("\r\n", "\n");
if (std::env::var("UPDATE_GOLDEN_TESTS") == Ok("1".to_string())) && actual != expected {
fs::write(&filename, &actual)
.await
@@ -1008,6 +1038,26 @@ fn print_logevent(logevent: &LogEvent) {
}
}
/// Saves the other account's public key as verified.
pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) {
let mut peerstate = Peerstate::from_header(
&EncryptHelper::new(other).await.unwrap().get_aheader(),
// We have to give 0 as the time, not the current time:
// The time is going to be saved in peerstate.last_seen.
// The code in `peerstate.rs` then compares `if message_time > self.last_seen`,
// and many similar checks in peerstate.rs, and doesn't allow changes otherwise.
// Giving the current time would mean that message_time == peerstate.last_seen,
// so changes would not be allowed.
// This might lead to flaky tests.
0,
);
peerstate.verified_key = peerstate.public_key.clone();
peerstate.verified_key_fingerprint = peerstate.public_key_fingerprint.clone();
peerstate.save_to_db(&this.sql).await.unwrap();
}
/// Pretty-print an event to stdout
///
/// Done during tests this is captured by `cargo test` and associated with the test itself.
@@ -1114,7 +1164,17 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str
} else {
"[FRESH]"
},
if msg.is_info() { "[INFO]" } else { "" },
if msg.is_info() {
if msg.get_info_type() == SystemMessage::ChatProtectionEnabled {
"[INFO 🛡️]"
} else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled {
"[INFO 🛡️❌]"
} else {
"[INFO]"
}
} else {
""
},
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",

View File

@@ -1 +1,2 @@
mod aeap;
mod verified_chats;

View File

@@ -8,10 +8,10 @@ use crate::contact;
use crate::contact::Contact;
use crate::contact::ContactId;
use crate::message::Message;
use crate::peerstate;
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::stock_str;
use crate::test_utils::mark_as_verified;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
@@ -327,19 +327,6 @@ async fn check_no_transition_done(groups: &[ChatId], old_alice_addr: &str, bob:
}
}
async fn mark_as_verified(this: &TestContext, other: &TestContext) {
let other_addr = other.get_primary_self_addr().await.unwrap();
let mut peerstate = peerstate::Peerstate::from_addr(this, &other_addr)
.await
.unwrap()
.unwrap();
peerstate.verified_key = peerstate.public_key.clone();
peerstate.verified_key_fingerprint = peerstate.public_key_fingerprint.clone();
peerstate.save_to_db(&this.sql).await.unwrap();
}
async fn get_last_info_msg(t: &TestContext, chat_id: ChatId) -> Option<Message> {
let msgs = chat::get_chat_msgs_ex(
&t.ctx,

508
src/tests/verified_chats.rs Normal file
View File

@@ -0,0 +1,508 @@
use anyhow::Result;
use pretty_assertions::assert_eq;
use crate::chat::{Chat, ProtectionStatus};
use crate::config::Config;
use crate::contact::VerifiedStatus;
use crate::contact::{Contact, Origin};
use crate::message::Message;
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf;
use crate::stock_str;
use crate::test_utils::{get_chat_msg, mark_as_verified, TestContext, TestContextManager};
use crate::{e2ee, message};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_oneonone_chat_broken_by_classical() {
check_verified_oneonone_chat(true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_oneonone_chat_broken_by_device_change() {
check_verified_oneonone_chat(false).await;
}
async fn check_verified_oneonone_chat(broken_by_classical_email: bool) {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
tcm.execute_securejoin(&alice, &bob).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&bob, &alice, ProtectionStatus::Protected).await;
if broken_by_classical_email {
tcm.section("Bob uses a classical MUA to send a message to Alice");
receive_imf(
&alice,
b"Subject: Re: Message from alice\r\n\
From: <bob@example.net>\r\n\
To: <alice@example.org>\r\n\
Date: Mon, 12 Dec 2022 14:33:39 +0000\r\n\
Message-ID: <abcd@example.net>\r\n\
\r\n\
Heyho!\r\n",
false,
)
.await
.unwrap()
.unwrap();
} else {
tcm.section("Bob sets up another Delta Chat device");
let bob2 = TestContext::new().await;
enable_verified_oneonone_chats(&[&bob2]).await;
bob2.set_name("bob2");
bob2.configure_addr("bob@example.net").await;
tcm.send_recv(&bob2, &alice, "Using another device now")
.await;
}
// Bob's contact is still verified, but the chat isn't marked as protected anymore
assert_verified(&alice, &bob, ProtectionStatus::ProtectionBroken).await;
tcm.section("Bob sends another message from DC");
tcm.send_recv(&bob, &alice, "Using DC again").await;
let contact = alice.add_or_lookup_contact(&bob).await;
assert_eq!(
contact.is_verified(&alice.ctx).await.unwrap(),
VerifiedStatus::BidirectVerified
);
// Bob's chat is marked as verified again
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_verified_oneonone_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
enable_verified_oneonone_chats(&[&alice, &bob, &fiona]).await;
tcm.execute_securejoin(&alice, &bob).await;
tcm.execute_securejoin(&bob, &fiona).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&bob, &alice, ProtectionStatus::Protected).await;
assert_verified(&bob, &fiona, ProtectionStatus::Protected).await;
assert_verified(&fiona, &bob, ProtectionStatus::Protected).await;
let group_id = bob
.create_group_with_members(
ProtectionStatus::Protected,
"Group with everyone",
&[&alice, &fiona],
)
.await;
assert_eq!(
get_chat_msg(&bob, group_id, 0, 1).await.get_info_type(),
SystemMessage::ChatProtectionEnabled
);
{
let sent = bob.send_text(group_id, "Heyho").await;
alice.recv_msg(&sent).await;
let msg = fiona.recv_msg(&sent).await;
assert_eq!(
get_chat_msg(&fiona, msg.chat_id, 0, 2)
.await
.get_info_type(),
SystemMessage::ChatProtectionEnabled
);
}
// Alice and Fiona should now be verified because of gossip
let alice_fiona_contact = alice.add_or_lookup_contact(&fiona).await;
assert_eq!(
alice_fiona_contact.is_verified(&alice).await.unwrap(),
VerifiedStatus::BidirectVerified
);
// As soon as Alice creates a chat with Fiona, it should directly be protected
{
let chat = alice.create_chat(&fiona).await;
assert!(chat.is_protected());
let msg = alice.get_last_msg().await;
let expected_text = stock_str::chat_protection_enabled(&alice).await;
assert_eq!(msg.text, expected_text);
}
// Fiona should also see the chat as protected
{
let rcvd = tcm.send_recv(&alice, &fiona, "Hi Fiona").await;
let alice_fiona_id = rcvd.chat_id;
let chat = Chat::load_from_db(&fiona, alice_fiona_id).await?;
assert!(chat.is_protected());
let msg0 = get_chat_msg(&fiona, chat.id, 0, 2).await;
let expected_text = stock_str::chat_protection_enabled(&fiona).await;
assert_eq!(msg0.text, expected_text);
}
tcm.section("Fiona reinstalls DC");
drop(fiona);
let fiona_new = tcm.unconfigured().await;
enable_verified_oneonone_chats(&[&fiona_new]).await;
fiona_new.configure_addr("fiona@example.net").await;
e2ee::ensure_secret_key_exists(&fiona_new).await?;
tcm.send_recv(&fiona_new, &alice, "I have a new device")
.await;
// The chat should be and stay unprotected
{
let chat = alice.get_chat(&fiona_new).await.unwrap();
assert!(!chat.is_protected());
assert!(chat.is_protection_broken());
// After recreating the chat, it should still be unprotected
chat.id.delete(&alice).await?;
let chat = alice.create_chat(&fiona_new).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_unverified_oneonone_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
// A chat with an unknown contact should be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2@example.org>\n\
\n\
hello\n",
false,
)
.await?;
chat.id.delete(&alice).await.unwrap();
// Now Bob is a known contact, new chats should still be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
tcm.send_recv(&bob, &alice, "hi").await;
chat.id.delete(&alice).await.unwrap();
// Now we have a public key, new chats should still be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_degrade_verified_oneonone_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
let alice_chat = alice.create_chat(&bob).await;
assert!(alice_chat.is_protected());
receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2@example.org>\n\
\n\
hello\n",
false,
)
.await?;
let contact_id = Contact::lookup_id_by_addr(&alice, "bob@example.net", Origin::Hidden)
.await?
.unwrap();
let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 3).await;
let enabled = stock_str::chat_protection_enabled(&alice).await;
assert_eq!(msg0.text, enabled);
assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled);
let msg1 = get_chat_msg(&alice, alice_chat.id, 1, 3).await;
let disabled = stock_str::chat_protection_disabled(&alice, contact_id).await;
assert_eq!(msg1.text, disabled);
assert_eq!(msg1.param.get_cmd(), SystemMessage::ChatProtectionDisabled);
let msg2 = get_chat_msg(&alice, alice_chat.id, 2, 3).await;
assert_eq!(msg2.text, "hello".to_string());
assert!(!msg2.is_system_message());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_oneonone_chat_enable_disable() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
// Alice & Bob verify each other
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
let chat = alice.create_chat(&bob).await;
assert!(chat.is_protected());
for alice_accepts_breakage in [true, false] {
// Bob uses Thunderbird to send a message
receive_imf(
&alice,
format!(
"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2{alice_accepts_breakage}@example.org>\n\
\n\
Message from Thunderbird\n"
)
.as_bytes(),
false,
)
.await?;
let chat = alice.get_chat(&bob).await.unwrap();
assert!(!chat.is_protected());
assert!(chat.is_protection_broken());
if alice_accepts_breakage {
tcm.section("Alice clicks 'Accept' on the input-bar-dialog");
chat.id.accept(&alice).await?;
let chat = alice.get_chat(&bob).await.unwrap();
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
}
// Bob sends a message from DC again
tcm.send_recv(&bob, &alice, "Hello from DC").await;
let chat = alice.get_chat(&bob).await.unwrap();
assert!(chat.is_protected());
assert!(!chat.is_protection_broken());
}
alice
.golden_test_chat(chat.id, "test_verified_oneonone_chat_enable_disable")
.await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdn_doesnt_disable_verification() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
bob.set_config_bool(Config::MdnsEnabled, true).await?;
// Alice & Bob verify each other
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
let rcvd = tcm.send_recv_accept(&alice, &bob, "Heyho").await;
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
let mimefactory = MimeFactory::from_mdn(&bob, &rcvd, vec![]).await?;
let rendered_msg = mimefactory.render(&bob).await?;
let body = rendered_msg.message;
receive_imf(&alice, body.as_bytes(), false).await.unwrap();
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_outgoing_mua_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
tcm.send_recv_accept(&bob, &alice, "Heyho from DC").await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
let sent = receive_imf(
&alice,
b"From: alice@example.org\n\
To: bob@example.net\n\
\n\
One classical MUA message",
false,
)
.await?
.unwrap();
tcm.send_recv_accept(&alice, &bob, "Sending with DC again")
.await;
alice
.golden_test_chat(sent.chat_id, "test_outgoing_mua_msg")
.await;
Ok(())
}
/// If Bob answers unencrypted from another address with a classical MUA,
/// the message is under some circumstances still assigned to the original
/// chat (see lookup_chat_by_reply()); this is meant to make aliases
/// work nicely.
/// However, if the original chat is verified, the unencrypted message
/// must NOT be assigned to it (it would be replaced by an error
/// message in the verified chat, so, this would just be a usability issue,
/// not a security issue).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reply() -> Result<()> {
for verified in [false, true] {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
if verified {
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
}
tcm.send_recv_accept(&bob, &alice, "Heyho from DC").await;
let encrypted_msg = tcm.send_recv_accept(&alice, &bob, "Heyho back").await;
let unencrypted_msg = receive_imf(
&alice,
format!(
"From: bob@someotherdomain.org\n\
To: some-alias-forwarding-to-alice@example.org\n\
In-Reply-To: {}\n\
\n\
Weird reply",
encrypted_msg.rfc724_mid
)
.as_bytes(),
false,
)
.await?
.unwrap();
let unencrypted_msg = Message::load_from_db(&alice, unencrypted_msg.msg_ids[0]).await?;
assert_eq!(unencrypted_msg.text, "Weird reply");
if verified {
assert_ne!(unencrypted_msg.chat_id, encrypted_msg.chat_id);
} else {
assert_eq!(unencrypted_msg.chat_id, encrypted_msg.chat_id);
}
}
Ok(())
}
/// Regression test for the following bug:
///
/// - Scan your chat partner's QR Code
/// - They change devices
/// - They send you a message
/// - Without accepting the encryption downgrade, scan your chat partner's QR Code again
///
/// -> The re-verification fails.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_break_protection_then_verify_again() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
// Cave: Bob can't write a message to Alice here.
// If he did, alice would increase his peerstate's last_seen timestamp.
// Then, after Bob reinstalls DC, alice's `if message_time > last_seen*`
// checks would return false (there are many checks of this form in peerstate.rs).
// Therefore, during the securejoin, Alice wouldn't accept the new key
// and reject the securejoin.
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
alice
.create_chat(&bob)
.await
.id
.inner_set_protection(&alice, ProtectionStatus::Protected)
.await?;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
tcm.section("Bob reinstalls DC");
drop(bob);
let bob_new = tcm.unconfigured().await;
enable_verified_oneonone_chats(&[&bob_new]).await;
bob_new.configure_addr("bob@example.net").await;
e2ee::ensure_secret_key_exists(&bob_new).await?;
tcm.send_recv(&bob_new, &alice, "I have a new device").await;
assert_verified(&alice, &bob_new, ProtectionStatus::ProtectionBroken).await;
assert!(
!alice
.get_chat(&bob_new)
.await
.unwrap()
.can_send(&alice)
.await?
);
tcm.execute_securejoin(&alice, &bob_new).await;
assert_verified(&alice, &bob_new, ProtectionStatus::Protected).await;
Ok(())
}
// ============== Helper Functions ==============
async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) {
let contact = this.add_or_lookup_contact(other).await;
assert_eq!(
contact.is_verified(this).await.unwrap(),
VerifiedStatus::BidirectVerified
);
let chat = this.get_chat(other).await.unwrap();
let (expect_protected, expect_broken) = match protected {
ProtectionStatus::Unprotected => (false, false),
ProtectionStatus::Protected => (true, false),
ProtectionStatus::ProtectionBroken => (false, true),
};
assert_eq!(chat.is_protected(), expect_protected);
assert_eq!(chat.is_protection_broken(), expect_broken);
}
async fn enable_verified_oneonone_chats(test_contexts: &[&TestContext]) {
for t in test_contexts {
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
.await
.unwrap()
}
}

View File

@@ -0,0 +1,7 @@
Single#Chat#10: bob@example.net [bob@example.net] 🛡️
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#11🔒: (Contact#Contact#10): Heyho from DC [FRESH]
Msg#12: Me (Contact#Contact#Self): One classical MUA message √
Msg#13🔒: Me (Contact#Contact#Self): Sending with DC again √
--------------------------------------------------------------------------------

View File

@@ -0,0 +1,12 @@
Single#Chat#10: Bob [bob@example.net] 🛡️
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#11: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌]
Msg#12: (Contact#Contact#10): Message from Thunderbird [FRESH]
Msg#13: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#14🔒: (Contact#Contact#10): Hello from DC [FRESH]
Msg#15: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌]
Msg#16: (Contact#Contact#10): Message from Thunderbird [FRESH]
Msg#17: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#18🔒: (Contact#Contact#10): Hello from DC [FRESH]
--------------------------------------------------------------------------------