mirror of
https://github.com/chatmail/core.git
synced 2026-04-28 10:56:29 +03:00
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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user