mirror of
https://github.com/chatmail/core.git
synced 2026-04-05 23:22:11 +03:00
Compare commits
12 Commits
v1.149.0
...
sk/contact
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c284794ac2 | ||
|
|
e494226120 | ||
|
|
276db00312 | ||
|
|
5fde02f9fd | ||
|
|
3b2f18f926 | ||
|
|
c9cf2b7f2e | ||
|
|
800edc6fce | ||
|
|
4e5e9f6006 | ||
|
|
d9d694ead0 | ||
|
|
faad576d10 | ||
|
|
b96593ed10 | ||
|
|
d2324a8fc4 |
Binary file not shown.
@@ -506,6 +506,11 @@ 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.
|
||||
* - `protect_autocrypt` = Enable Header Protection for Autocrypt header.
|
||||
* This is an experimental option not compatible to other MUAs
|
||||
* and older Delta Chat versions.
|
||||
* 1 = enable.
|
||||
* 0 = disable (default).
|
||||
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||
* seconds. 2 days by default.
|
||||
* This is not supposed to be changed by UIs and only used for testing.
|
||||
|
||||
@@ -45,7 +45,7 @@ pub mod types;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
use types::account::Account;
|
||||
use types::chat::FullChat;
|
||||
use types::chat::{EncryptionInfo, FullChat};
|
||||
use types::contact::{ContactObject, VcardContact};
|
||||
use types::events::Event;
|
||||
use types::http::HttpResponse;
|
||||
@@ -708,6 +708,19 @@ impl CommandApi {
|
||||
ChatId::new(chat_id).get_encryption_info(&ctx).await
|
||||
}
|
||||
|
||||
/// Get encryption info for a chat.
|
||||
async fn get_chat_encryption_info_json(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
) -> Result<EncryptionInfo> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ChatId::new(chat_id)
|
||||
.get_encryption_info_json(&ctx)
|
||||
.await?
|
||||
.into())
|
||||
}
|
||||
|
||||
/// Get QR code text that will offer a [SecureJoin](https://securejoin.delta.chat/) invitation.
|
||||
///
|
||||
/// If `chat_id` is a group chat ID, SecureJoin QR code for the group is returned.
|
||||
|
||||
@@ -9,6 +9,7 @@ use deltachat::context::Context;
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
use yerpc::JsonSchema;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
use super::contact::ContactObject;
|
||||
@@ -239,3 +240,23 @@ impl JSONRPCChatVisibility {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, JsonSchema, TypeDef, Serialize, Deserialize)]
|
||||
pub struct EncryptionInfo {
|
||||
/// Addresses with End-to-end encryption preferred.
|
||||
pub mutual: Vec<String>,
|
||||
/// Addresses with End-to-end encryption available.
|
||||
pub no_preference: Vec<String>,
|
||||
/// Addresses with no encryption.
|
||||
pub reset: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<chat::EncryptionInfo> for EncryptionInfo {
|
||||
fn from(encryption_info: chat::EncryptionInfo) -> Self {
|
||||
EncryptionInfo {
|
||||
mutual: encryption_info.mutual,
|
||||
no_preference: encryption_info.no_preference,
|
||||
reset: encryption_info.reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ If you want to debug iroh at rust-trace/log level set
|
||||
RUST_LOG=iroh_net=trace,iroh_gossip=trace
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
@@ -25,9 +25,7 @@ def path_to_webxdc(request):
|
||||
|
||||
|
||||
def log(msg):
|
||||
print()
|
||||
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
|
||||
print()
|
||||
logging.info(msg)
|
||||
|
||||
|
||||
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
|
||||
|
||||
@@ -57,8 +57,8 @@ def test_acfactory(acfactory) -> None:
|
||||
if event.progress == 1000: # Success
|
||||
break
|
||||
else:
|
||||
print(event)
|
||||
print("Successful configuration")
|
||||
logging.info(event)
|
||||
logging.info("Successful configuration")
|
||||
|
||||
|
||||
def test_configure_starttls(acfactory) -> None:
|
||||
|
||||
@@ -520,8 +520,13 @@ Authentication-Results: dkim=";
|
||||
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
|
||||
}
|
||||
|
||||
// Test that Autocrypt works with mailing list.
|
||||
//
|
||||
// Previous versions of Delta Chat ignored Autocrypt based on the List-Post header.
|
||||
// This is not needed: comparing of the From address to Autocrypt header address is enough.
|
||||
// If the mailing list is not rewriting the From header, Autocrypt should be applied.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_autocrypt_in_mailinglist_ignored() -> Result<()> {
|
||||
async fn test_autocrypt_in_mailinglist_not_ignored() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
@@ -533,28 +538,18 @@ Authentication-Results: dkim=";
|
||||
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
|
||||
bob.recv_msg(&sent).await;
|
||||
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
|
||||
assert!(peerstate.is_none());
|
||||
|
||||
// Do the same without the mailing list header, this time the peerstate should be accepted
|
||||
let sent = alice
|
||||
.send_text(alice_bob_chat.id, "hellooo without mailing list")
|
||||
.await;
|
||||
bob.recv_msg(&sent).await;
|
||||
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
|
||||
assert!(peerstate.is_some());
|
||||
|
||||
// This also means that Bob can now write encrypted to Alice:
|
||||
// Bob can now write encrypted to Alice:
|
||||
let mut sent = bob
|
||||
.send_text(bob_alice_chat.id, "hellooo in the mailinglist again")
|
||||
.await;
|
||||
assert!(sent.load_from_db().await.get_showpadlock());
|
||||
|
||||
// But if Bob writes to a mailing list, Alice doesn't show a padlock
|
||||
// since she can't verify the signature without accepting Bob's key:
|
||||
sent.payload
|
||||
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
|
||||
let rcvd = alice.recv_msg(&sent).await;
|
||||
assert!(!rcvd.get_showpadlock());
|
||||
assert!(rcvd.get_showpadlock());
|
||||
assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
|
||||
|
||||
Ok(())
|
||||
|
||||
92
src/chat.rs
92
src/chat.rs
@@ -105,6 +105,17 @@ pub enum ProtectionStatus {
|
||||
ProtectionBroken = 3, // `2` was never used as a value.
|
||||
}
|
||||
|
||||
/// Encryption info for a single chat.
|
||||
#[derive(Debug)]
|
||||
pub struct EncryptionInfo {
|
||||
/// Addresses with End-to-end encryption preferred.
|
||||
pub mutual: Vec<String>,
|
||||
/// Addresses with End-to-end encryption available.
|
||||
pub no_preference: Vec<String>,
|
||||
/// Addresses with no encryption.
|
||||
pub reset: Vec<String>,
|
||||
}
|
||||
|
||||
/// The reason why messages cannot be sent to the chat.
|
||||
///
|
||||
/// The reason is mainly for logging and displaying in debug REPL, thus not translated.
|
||||
@@ -1283,9 +1294,39 @@ impl ChatId {
|
||||
///
|
||||
/// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`].
|
||||
pub async fn get_encryption_info(self, context: &Context) -> Result<String> {
|
||||
let mut ret_mutual = String::new();
|
||||
let mut ret_nopreference = String::new();
|
||||
let mut ret_reset = String::new();
|
||||
let encr_info = self.get_encryption_info_json(context).await?;
|
||||
|
||||
let mut ret = String::new();
|
||||
if !encr_info.reset.is_empty() {
|
||||
ret += &stock_str::encr_none(context).await;
|
||||
ret.push_str(":\n");
|
||||
ret += &encr_info.reset.join("\n");
|
||||
}
|
||||
if !encr_info.no_preference.is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push_str("\n\n");
|
||||
}
|
||||
ret += &stock_str::e2e_available(context).await;
|
||||
ret.push_str(":\n");
|
||||
ret += &encr_info.no_preference.join("\n");
|
||||
}
|
||||
if !encr_info.mutual.is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push_str("\n\n");
|
||||
}
|
||||
ret += &stock_str::e2e_preferred(context).await;
|
||||
ret.push_str(":\n");
|
||||
ret += &encr_info.mutual.join("\n");
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Returns encryption preferences of all chat contacts.
|
||||
pub async fn get_encryption_info_json(self, context: &Context) -> Result<EncryptionInfo> {
|
||||
let mut mutual = vec![];
|
||||
let mut no_preference = vec![];
|
||||
let mut reset = vec![];
|
||||
|
||||
for contact_id in get_chat_contacts(context, self)
|
||||
.await?
|
||||
@@ -1293,46 +1334,24 @@ impl ChatId {
|
||||
.filter(|&contact_id| !contact_id.is_special())
|
||||
{
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
let addr = contact.get_addr();
|
||||
let peerstate = Peerstate::from_addr(context, addr).await?;
|
||||
let addr = contact.get_addr().to_string();
|
||||
let peerstate = Peerstate::from_addr(context, &addr).await?;
|
||||
|
||||
match peerstate
|
||||
.filter(|peerstate| peerstate.peek_key(false).is_some())
|
||||
.map(|peerstate| peerstate.prefer_encrypt)
|
||||
{
|
||||
Some(EncryptPreference::Mutual) => ret_mutual += &format!("{addr}\n"),
|
||||
Some(EncryptPreference::NoPreference) => ret_nopreference += &format!("{addr}\n"),
|
||||
Some(EncryptPreference::Reset) | None => ret_reset += &format!("{addr}\n"),
|
||||
Some(EncryptPreference::Mutual) => mutual.push(addr),
|
||||
Some(EncryptPreference::NoPreference) => no_preference.push(addr),
|
||||
Some(EncryptPreference::Reset) | None => reset.push(addr),
|
||||
};
|
||||
}
|
||||
|
||||
let mut ret = String::new();
|
||||
if !ret_reset.is_empty() {
|
||||
ret += &stock_str::encr_none(context).await;
|
||||
ret.push(':');
|
||||
ret.push('\n');
|
||||
ret += &ret_reset;
|
||||
}
|
||||
if !ret_nopreference.is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push('\n');
|
||||
}
|
||||
ret += &stock_str::e2e_available(context).await;
|
||||
ret.push(':');
|
||||
ret.push('\n');
|
||||
ret += &ret_nopreference;
|
||||
}
|
||||
if !ret_mutual.is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push('\n');
|
||||
}
|
||||
ret += &stock_str::e2e_preferred(context).await;
|
||||
ret.push(':');
|
||||
ret.push('\n');
|
||||
ret += &ret_mutual;
|
||||
}
|
||||
|
||||
Ok(ret.trim().to_string())
|
||||
Ok(EncryptionInfo {
|
||||
mutual,
|
||||
no_preference,
|
||||
reset,
|
||||
})
|
||||
}
|
||||
|
||||
/// Bad evil escape hatch.
|
||||
@@ -4505,6 +4524,7 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
/// Adds an informational message to chat.
|
||||
///
|
||||
/// For example, it can be a message showing that a member was added to a group.
|
||||
/// Doesn't fail if the chat doesn't exist.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn add_info_msg_with_cmd(
|
||||
context: &Context,
|
||||
@@ -5370,7 +5390,7 @@ mod tests {
|
||||
|
||||
// Eventually, first removal message arrives.
|
||||
// This has no effect.
|
||||
bob.recv_msg(&remove1).await;
|
||||
bob.recv_msg_trash(&remove1).await;
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -396,6 +396,12 @@ pub enum Config {
|
||||
/// Make all outgoing messages with Autocrypt header "multipart/signed".
|
||||
SignUnencrypted,
|
||||
|
||||
/// Enable header protection for `Autocrypt` header.
|
||||
///
|
||||
/// This is an experimental setting not compatible to other MUAs
|
||||
/// and older Delta Chat versions (core version <= v1.149.0).
|
||||
ProtectAutocrypt,
|
||||
|
||||
/// Let the core save all events to the database.
|
||||
/// This value is used internally to remember the MsgId of the logging xdc
|
||||
#[strum(props(default = "0"))]
|
||||
|
||||
@@ -990,6 +990,12 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"protect_autocrypt",
|
||||
self.get_config_int(Config::ProtectAutocrypt)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"debug_logging",
|
||||
self.get_config_int(Config::DebugLogging).await?.to_string(),
|
||||
|
||||
@@ -1,125 +1,36 @@
|
||||
//! End-to-end decryption support.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
use mailparse::ParsedMail;
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::authres::handle_authres;
|
||||
use crate::authres::{self, DkimResults};
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::pgp;
|
||||
|
||||
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message.
|
||||
///
|
||||
/// If successful and the message is encrypted, returns decrypted body and a set of valid
|
||||
/// signature fingerprints.
|
||||
///
|
||||
/// If the message is wrongly signed, HashSet will be empty.
|
||||
/// If successful and the message is encrypted, returns decrypted body.
|
||||
pub fn try_decrypt(
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: &[SignedSecretKey],
|
||||
public_keyring_for_validate: &[SignedPublicKey],
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
) -> Result<Option<::pgp::composed::Message>> {
|
||||
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let data = encrypted_data_part.get_body_raw()?;
|
||||
let msg = pgp::pk_decrypt(data, private_keyring)?;
|
||||
|
||||
let (plain, ret_valid_signatures) =
|
||||
pgp::pk_decrypt(data, private_keyring, public_keyring_for_validate)?;
|
||||
Ok(Some((plain, ret_valid_signatures)))
|
||||
}
|
||||
|
||||
pub(crate) async fn prepare_decryption(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
from: &str,
|
||||
message_time: i64,
|
||||
) -> Result<DecryptionInfo> {
|
||||
if mail.headers.get_header(HeaderDef::ListPost).is_some() {
|
||||
if mail.headers.get_header(HeaderDef::Autocrypt).is_some() {
|
||||
info!(
|
||||
context,
|
||||
"Ignoring autocrypt header since this is a mailing list message. \
|
||||
NOTE: For privacy reasons, the mailing list software should remove Autocrypt headers."
|
||||
);
|
||||
}
|
||||
return Ok(DecryptionInfo {
|
||||
from: from.to_string(),
|
||||
autocrypt_header: None,
|
||||
peerstate: None,
|
||||
message_time,
|
||||
dkim_results: DkimResults { dkim_passed: false },
|
||||
});
|
||||
}
|
||||
|
||||
let autocrypt_header = if context.is_self_addr(from).await? {
|
||||
None
|
||||
} else if let Some(aheader_value) = mail.headers.get_header_value(HeaderDef::Autocrypt) {
|
||||
match Aheader::from_str(&aheader_value) {
|
||||
Ok(header) if addr_cmp(&header.addr, from) => Some(header),
|
||||
Ok(header) => {
|
||||
warn!(
|
||||
context,
|
||||
"Autocrypt header address {:?} is not {:?}.", header.addr, from
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let dkim_results = handle_authres(context, mail, from).await?;
|
||||
let allow_aeap = get_encrypted_mime(mail).is_some();
|
||||
let peerstate = get_autocrypt_peerstate(
|
||||
context,
|
||||
from,
|
||||
autocrypt_header.as_ref(),
|
||||
message_time,
|
||||
allow_aeap,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(DecryptionInfo {
|
||||
from: from.to_string(),
|
||||
autocrypt_header,
|
||||
peerstate,
|
||||
message_time,
|
||||
dkim_results,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DecryptionInfo {
|
||||
/// The From address. This is the address from the unnencrypted, outer
|
||||
/// From header.
|
||||
pub from: String,
|
||||
pub autocrypt_header: Option<Aheader>,
|
||||
/// The peerstate that will be used to validate the signatures
|
||||
pub peerstate: Option<Peerstate>,
|
||||
/// The timestamp when the message was sent.
|
||||
/// If this is older than the peerstate's last_seen, this probably
|
||||
/// means out-of-order message arrival, We don't modify the
|
||||
/// peerstate in this case.
|
||||
pub message_time: i64,
|
||||
pub(crate) dkim_results: authres::DkimResults,
|
||||
Ok(Some(msg))
|
||||
}
|
||||
|
||||
/// Returns a reference to the encrypted payload of a message.
|
||||
fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
|
||||
pub(crate) fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
|
||||
get_autocrypt_mime(mail)
|
||||
.or_else(|| get_mixed_up_mime(mail))
|
||||
.or_else(|| get_attachment_mime(mail))
|
||||
|
||||
@@ -144,12 +144,12 @@ impl HtmlMsgParser {
|
||||
self.plain = Some(PlainText {
|
||||
text: decoded_data,
|
||||
flowed: if let Some(format) = mail.ctype.params.get("format") {
|
||||
format.as_str().to_ascii_lowercase() == "flowed"
|
||||
format.as_str().eq_ignore_ascii_case("flowed")
|
||||
} else {
|
||||
false
|
||||
},
|
||||
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
|
||||
delsp.as_str().to_ascii_lowercase() == "yes"
|
||||
delsp.as_str().eq_ignore_ascii_case("yes")
|
||||
} else {
|
||||
false
|
||||
},
|
||||
@@ -283,7 +283,6 @@ mod tests {
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
</head><body>
|
||||
This message does not have Content-Type nor Subject.<br/>
|
||||
<br/>
|
||||
</body></html>
|
||||
"#
|
||||
);
|
||||
@@ -302,7 +301,6 @@ This message does not have Content-Type nor Subject.<br/>
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
</head><body>
|
||||
message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
|
||||
<br/>
|
||||
</body></html>
|
||||
"#
|
||||
);
|
||||
@@ -325,7 +323,6 @@ This line ends with a space and will be merged with the next one due to format=f
|
||||
<br/>
|
||||
This line does not end with a space<br/>
|
||||
and will be wrapped as usual.<br/>
|
||||
<br/>
|
||||
</body></html>
|
||||
"#
|
||||
);
|
||||
@@ -347,7 +344,6 @@ mime-modified should not be set set as there is no html and no special stuff;<br
|
||||
although not being a delta-message.<br/>
|
||||
test some special html-characters as < > and & but also " and ' :)<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
</body></html>
|
||||
"#
|
||||
);
|
||||
|
||||
53
src/imap.rs
53
src/imap.rs
@@ -89,7 +89,7 @@ pub(crate) struct Imap {
|
||||
|
||||
oauth2: bool,
|
||||
|
||||
login_failed_once: bool,
|
||||
authentication_failed_once: bool,
|
||||
|
||||
pub(crate) connectivity: ConnectivityStore,
|
||||
|
||||
@@ -254,7 +254,7 @@ impl Imap {
|
||||
proxy_config,
|
||||
strict_tls,
|
||||
oauth2,
|
||||
login_failed_once: false,
|
||||
authentication_failed_once: false,
|
||||
connectivity: Default::default(),
|
||||
conn_last_try: UNIX_EPOCH,
|
||||
conn_backoff_ms: 0,
|
||||
@@ -402,7 +402,7 @@ impl Imap {
|
||||
let mut lock = context.server_id.write().await;
|
||||
lock.clone_from(&session.capabilities.server_id);
|
||||
|
||||
self.login_failed_once = false;
|
||||
self.authentication_failed_once = false;
|
||||
context.emit_event(EventType::ImapConnected(format!(
|
||||
"IMAP-LOGIN as {}",
|
||||
lp.user
|
||||
@@ -416,35 +416,38 @@ impl Imap {
|
||||
let imap_user = lp.user.to_owned();
|
||||
let message = stock_str::cannot_login(context, &imap_user).await;
|
||||
|
||||
let err_str = err.to_string();
|
||||
warn!(context, "IMAP failed to login: {err:#}.");
|
||||
first_error.get_or_insert(format_err!("{message} ({err:#})"));
|
||||
|
||||
// If it looks like the password is wrong, send a notification:
|
||||
let _lock = context.wrong_pw_warning_mutex.lock().await;
|
||||
if !configuring
|
||||
&& self.login_failed_once
|
||||
&& err_str.to_lowercase().contains("authentication")
|
||||
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
|
||||
{
|
||||
let mut msg = Message::new_text(message);
|
||||
if let Err(e) = chat::add_device_msg_with_importance(
|
||||
context,
|
||||
None,
|
||||
Some(&mut msg),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
if err.to_string().to_lowercase().contains("authentication") {
|
||||
if self.authentication_failed_once
|
||||
&& !configuring
|
||||
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
|
||||
{
|
||||
warn!(context, "Failed to add device message: {e:#}.");
|
||||
let mut msg = Message::new_text(message);
|
||||
if let Err(e) = chat::add_device_msg_with_importance(
|
||||
context,
|
||||
None,
|
||||
Some(&mut msg),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(context, "Failed to add device message: {e:#}.");
|
||||
} else {
|
||||
context
|
||||
.set_config_internal(Config::NotifyAboutWrongPw, None)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
} else {
|
||||
context
|
||||
.set_config_internal(Config::NotifyAboutWrongPw, None)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
self.authentication_failed_once = true;
|
||||
}
|
||||
} else {
|
||||
self.login_failed_once = true;
|
||||
self.authentication_failed_once = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1913,7 +1916,7 @@ async fn needs_move_to_mvbox(
|
||||
&& has_chat_version
|
||||
&& headers
|
||||
.get_header_value(HeaderDef::AutoSubmitted)
|
||||
.filter(|val| val.to_ascii_lowercase() == "auto-generated")
|
||||
.filter(|val| val.eq_ignore_ascii_case("auto-generated"))
|
||||
.is_some()
|
||||
{
|
||||
if let Some(from) = mimeparser::get_from(headers) {
|
||||
|
||||
@@ -743,7 +743,9 @@ impl MimeFactory {
|
||||
hidden_headers.push(header);
|
||||
} else if header_name == "chat-user-avatar" {
|
||||
hidden_headers.push(header);
|
||||
} else if header_name == "autocrypt" {
|
||||
} else if header_name == "autocrypt"
|
||||
&& !context.get_config_bool(Config::ProtectAutocrypt).await?
|
||||
{
|
||||
unprotected_headers.push(header.clone());
|
||||
} else if header_name == "from" {
|
||||
// Unencrypted securejoin messages should _not_ include the display name:
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::cmp::min;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
use std::str;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use deltachat_contact_tools::{addr_cmp, addr_normalize, sanitize_bidi_characters};
|
||||
@@ -14,6 +15,7 @@ use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, Si
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::authres::handle_authres;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{add_info_msg, ChatId};
|
||||
use crate::config::Config;
|
||||
@@ -21,8 +23,8 @@ use crate::constants::{self, Chattype};
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::decrypt::{
|
||||
keyring_from_peerstate, prepare_decryption, try_decrypt, validate_detached_signature,
|
||||
DecryptionInfo,
|
||||
get_autocrypt_peerstate, get_encrypted_mime, keyring_from_peerstate, try_decrypt,
|
||||
validate_detached_signature,
|
||||
};
|
||||
use crate::dehtml::dehtml;
|
||||
use crate::events::EventType;
|
||||
@@ -71,7 +73,8 @@ pub(crate) struct MimeMessage {
|
||||
/// messages to this address to post them to the list.
|
||||
pub list_post: Option<String>,
|
||||
pub chat_disposition_notification_to: Option<SingleInfo>,
|
||||
pub decryption_info: DecryptionInfo,
|
||||
pub autocrypt_header: Option<Aheader>,
|
||||
pub peerstate: Option<Peerstate>,
|
||||
pub decrypting_failed: bool,
|
||||
|
||||
/// Set of valid signature fingerprints if a message is an
|
||||
@@ -301,42 +304,101 @@ impl MimeMessage {
|
||||
let mut from = from.context("No from in message")?;
|
||||
let private_keyring = load_self_secret_keyring(context).await?;
|
||||
|
||||
let mut decryption_info =
|
||||
prepare_decryption(context, &mail, &from.addr, timestamp_sent).await?;
|
||||
let allow_aeap = get_encrypted_mime(&mail).is_some();
|
||||
|
||||
let dkim_results = handle_authres(context, &mail, &from.addr).await?;
|
||||
|
||||
// Memory location for a possible decrypted message.
|
||||
let mut mail_raw = Vec::new();
|
||||
let mut gossiped_keys = Default::default();
|
||||
let mut from_is_signed = false;
|
||||
hop_info += "\n\n";
|
||||
hop_info += &decryption_info.dkim_results.to_string();
|
||||
hop_info += &dkim_results.to_string();
|
||||
|
||||
let incoming = !context.is_self_addr(&from.addr).await?;
|
||||
let public_keyring = match decryption_info.peerstate.is_none() && !incoming {
|
||||
true => key::load_self_public_keyring(context).await?,
|
||||
false => keyring_from_peerstate(decryption_info.peerstate.as_ref()),
|
||||
};
|
||||
let (mail, mut signatures, encrypted) = match tokio::task::block_in_place(|| {
|
||||
try_decrypt(&mail, &private_keyring, &public_keyring)
|
||||
}) {
|
||||
Ok(Some((raw, signatures))) => {
|
||||
mail_raw = raw;
|
||||
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(
|
||||
context,
|
||||
"decrypted message mime-body:\n{}",
|
||||
String::from_utf8_lossy(&mail_raw),
|
||||
);
|
||||
|
||||
let mut aheader_value: Option<String> = mail.headers.get_header_value(HeaderDef::Autocrypt);
|
||||
|
||||
let mail_raw; // Memory location for a possible decrypted message.
|
||||
let decrypted_msg; // Decrypted signed OpenPGP message.
|
||||
|
||||
let (mail, encrypted) =
|
||||
match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring)) {
|
||||
Ok(Some(msg)) => {
|
||||
mail_raw = msg.get_content()?.unwrap_or_default();
|
||||
|
||||
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(
|
||||
context,
|
||||
"decrypted message mime-body:\n{}",
|
||||
String::from_utf8_lossy(&mail_raw),
|
||||
);
|
||||
}
|
||||
|
||||
decrypted_msg = Some(msg);
|
||||
if let Some(protected_aheader_value) = decrypted_mail
|
||||
.headers
|
||||
.get_header_value(HeaderDef::Autocrypt)
|
||||
{
|
||||
aheader_value = Some(protected_aheader_value);
|
||||
}
|
||||
|
||||
(Ok(decrypted_mail), true)
|
||||
}
|
||||
Ok(None) => {
|
||||
mail_raw = Vec::new();
|
||||
decrypted_msg = None;
|
||||
(Ok(mail), false)
|
||||
}
|
||||
Err(err) => {
|
||||
mail_raw = Vec::new();
|
||||
decrypted_msg = None;
|
||||
warn!(context, "decryption failed: {:#}", err);
|
||||
(Err(err), false)
|
||||
}
|
||||
};
|
||||
|
||||
let autocrypt_header = if !incoming {
|
||||
None
|
||||
} else if let Some(aheader_value) = aheader_value {
|
||||
match Aheader::from_str(&aheader_value) {
|
||||
Ok(header) if addr_cmp(&header.addr, &from.addr) => Some(header),
|
||||
Ok(header) => {
|
||||
warn!(
|
||||
context,
|
||||
"Autocrypt header address {:?} is not {:?}.", header.addr, from.addr
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
|
||||
None
|
||||
}
|
||||
(Ok(decrypted_mail), signatures, true)
|
||||
}
|
||||
Ok(None) => (Ok(mail), HashSet::new(), false),
|
||||
Err(err) => {
|
||||
warn!(context, "decryption failed: {:#}", err);
|
||||
(Err(err), HashSet::new(), false)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// The peerstate that will be used to validate the signatures.
|
||||
let mut peerstate = get_autocrypt_peerstate(
|
||||
context,
|
||||
&from.addr,
|
||||
autocrypt_header.as_ref(),
|
||||
timestamp_sent,
|
||||
allow_aeap,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let public_keyring = match peerstate.is_none() && !incoming {
|
||||
true => key::load_self_public_keyring(context).await?,
|
||||
false => keyring_from_peerstate(peerstate.as_ref()),
|
||||
};
|
||||
|
||||
let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
|
||||
crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)?
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
|
||||
let mail = mail.as_ref().map(|mail| {
|
||||
let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
|
||||
.unwrap_or((mail, Default::default()));
|
||||
@@ -422,7 +484,7 @@ impl MimeMessage {
|
||||
Self::remove_secured_headers(&mut headers);
|
||||
|
||||
// If it is not a read receipt, degrade encryption.
|
||||
if let (Some(peerstate), Ok(mail)) = (&mut decryption_info.peerstate, mail) {
|
||||
if let (Some(peerstate), Ok(mail)) = (&mut peerstate, mail) {
|
||||
if timestamp_sent > peerstate.last_seen_autocrypt
|
||||
&& mail.ctype.mimetype != "multipart/report"
|
||||
{
|
||||
@@ -433,7 +495,7 @@ impl MimeMessage {
|
||||
if !encrypted {
|
||||
signatures.clear();
|
||||
}
|
||||
if let Some(peerstate) = &mut decryption_info.peerstate {
|
||||
if let Some(peerstate) = &mut peerstate {
|
||||
if peerstate.prefer_encrypt != EncryptPreference::Mutual && !signatures.is_empty() {
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
@@ -449,7 +511,8 @@ impl MimeMessage {
|
||||
from_is_signed,
|
||||
incoming,
|
||||
chat_disposition_notification_to,
|
||||
decryption_info,
|
||||
autocrypt_header,
|
||||
peerstate,
|
||||
decrypting_failed: mail.is_err(),
|
||||
|
||||
// only non-empty if it was a valid autocrypt message
|
||||
@@ -1158,7 +1221,7 @@ impl MimeMessage {
|
||||
|
||||
let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
|
||||
{
|
||||
format.as_str().to_ascii_lowercase() == "flowed"
|
||||
format.as_str().eq_ignore_ascii_case("flowed")
|
||||
} else {
|
||||
false
|
||||
};
|
||||
@@ -1168,7 +1231,7 @@ impl MimeMessage {
|
||||
&& is_format_flowed
|
||||
{
|
||||
let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
|
||||
delsp.as_str().to_ascii_lowercase() == "yes"
|
||||
delsp.as_str().eq_ignore_ascii_case("yes")
|
||||
} else {
|
||||
false
|
||||
};
|
||||
@@ -1231,7 +1294,7 @@ impl MimeMessage {
|
||||
if decoded_data.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(peerstate) = &mut self.decryption_info.peerstate {
|
||||
if let Some(peerstate) = &mut self.peerstate {
|
||||
if peerstate.prefer_encrypt != EncryptPreference::Mutual
|
||||
&& mime_type.type_() == mime::APPLICATION
|
||||
&& mime_type.subtype().as_str() == "pgp-keys"
|
||||
@@ -3645,6 +3708,28 @@ On 2020-10-25, Bob wrote:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that sender status (signature) does not appear
|
||||
/// in HTML view of a long message.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_large_message_no_signature() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
alice
|
||||
.set_config(Config::Selfstatus, Some("Some signature"))
|
||||
.await?;
|
||||
let chat = alice.create_chat(bob).await;
|
||||
let txt = "Hello!\n".repeat(500);
|
||||
let sent = alice.send_text(chat.id, &txt).await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
|
||||
assert_eq!(msg.has_html(), true);
|
||||
let html = msg.id.get_html(bob).await?.unwrap();
|
||||
assert_eq!(html.contains("Some signature"), false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_x_microsoft_original_message_id() {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -4012,12 +4097,8 @@ Content-Disposition: reaction\n\
|
||||
|
||||
// We do allow the time to be in the future a bit (because of unsynchronized clocks),
|
||||
// but only 60 seconds:
|
||||
assert!(mime_message.decryption_info.message_time <= time() + 60);
|
||||
assert!(mime_message.decryption_info.message_time >= beginning_time + 60);
|
||||
assert_eq!(
|
||||
mime_message.decryption_info.message_time,
|
||||
mime_message.timestamp_sent
|
||||
);
|
||||
assert!(mime_message.timestamp_sent <= time() + 60);
|
||||
assert!(mime_message.timestamp_sent >= beginning_time + 60);
|
||||
assert!(mime_message.timestamp_rcvd <= time());
|
||||
|
||||
Ok(())
|
||||
@@ -4088,4 +4169,24 @@ Content-Type: text/plain; charset=utf-8
|
||||
"alice@example.org"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_protect_autocrypt() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
alice
|
||||
.set_config_bool(Config::ProtectAutocrypt, true)
|
||||
.await?;
|
||||
bob.set_config_bool(Config::ProtectAutocrypt, true).await?;
|
||||
|
||||
let msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
|
||||
assert_eq!(msg.get_showpadlock(), false);
|
||||
|
||||
let msg = tcm.send_recv(bob, alice, "Hi!").await;
|
||||
assert_eq!(msg.get_showpadlock(), true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use async_native_tls::TlsStream;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::time::timeout;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::time;
|
||||
|
||||
@@ -128,7 +128,7 @@ pub(crate) async fn connect_tls_inner(
|
||||
host: &str,
|
||||
strict_tls: bool,
|
||||
alpn: &[&str],
|
||||
) -> Result<TlsStream<Pin<Box<TimeoutStream<TcpStream>>>>> {
|
||||
) -> Result<impl SessionStream> {
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
let tls_stream = wrap_tls(strict_tls, host, alpn, tcp_stream).await?;
|
||||
Ok(tls_stream)
|
||||
|
||||
@@ -2,45 +2,39 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_native_tls::{Certificate, Protocol, TlsConnector, TlsStream};
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
|
||||
// this certificate is missing on older android devices (eg. lg with android6 from 2017)
|
||||
// certificate downloaded from https://letsencrypt.org/certificates/
|
||||
static LETSENCRYPT_ROOT: Lazy<Certificate> = Lazy::new(|| {
|
||||
Certificate::from_der(include_bytes!(
|
||||
"../../assets/root-certificates/letsencrypt/isrgrootx1.der"
|
||||
))
|
||||
.unwrap()
|
||||
});
|
||||
use crate::net::session::SessionStream;
|
||||
|
||||
pub async fn wrap_tls<T: AsyncRead + AsyncWrite + Unpin>(
|
||||
pub async fn wrap_tls(
|
||||
strict_tls: bool,
|
||||
hostname: &str,
|
||||
alpn: &[&str],
|
||||
stream: T,
|
||||
) -> Result<TlsStream<T>> {
|
||||
let tls_builder = TlsConnector::new()
|
||||
.min_protocol_version(Some(Protocol::Tlsv12))
|
||||
.request_alpns(alpn)
|
||||
.add_root_certificate(LETSENCRYPT_ROOT.clone());
|
||||
let tls = if strict_tls {
|
||||
tls_builder
|
||||
stream: impl SessionStream + 'static,
|
||||
) -> Result<impl SessionStream> {
|
||||
if strict_tls {
|
||||
let tls_stream = wrap_rustls(hostname, alpn, stream).await?;
|
||||
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
|
||||
Ok(boxed_stream)
|
||||
} else {
|
||||
tls_builder
|
||||
// We use native_tls because it accepts 1024-bit RSA keys.
|
||||
// Rustls does not support them even if
|
||||
// certificate checks are disabled: <https://github.com/rustls/rustls/issues/234>.
|
||||
let tls = async_native_tls::TlsConnector::new()
|
||||
.min_protocol_version(Some(async_native_tls::Protocol::Tlsv12))
|
||||
.request_alpns(alpn)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.danger_accept_invalid_certs(true)
|
||||
};
|
||||
let tls_stream = tls.connect(hostname, stream).await?;
|
||||
Ok(tls_stream)
|
||||
.danger_accept_invalid_certs(true);
|
||||
let tls_stream = tls.connect(hostname, stream).await?;
|
||||
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
|
||||
Ok(boxed_stream)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wrap_rustls<T: AsyncRead + AsyncWrite + Unpin>(
|
||||
pub async fn wrap_rustls(
|
||||
hostname: &str,
|
||||
alpn: &[&str],
|
||||
stream: T,
|
||||
) -> Result<tokio_rustls::client::TlsStream<T>> {
|
||||
stream: impl SessionStream,
|
||||
) -> Result<impl SessionStream> {
|
||||
let mut root_cert_store = rustls::RootCertStore::empty();
|
||||
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
|
||||
|
||||
@@ -766,8 +766,7 @@ pub(crate) async fn maybe_do_aeap_transition(
|
||||
context: &Context,
|
||||
mime_parser: &mut crate::mimeparser::MimeMessage,
|
||||
) -> Result<()> {
|
||||
let info = &mime_parser.decryption_info;
|
||||
let Some(peerstate) = &info.peerstate else {
|
||||
let Some(peerstate) = &mime_parser.peerstate else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -815,13 +814,13 @@ pub(crate) async fn maybe_do_aeap_transition(
|
||||
|
||||
// DC avoids sending messages with the same timestamp, that's why messages
|
||||
// with equal timestamps are ignored here unlike in `Peerstate::apply_header()`.
|
||||
if info.message_time <= peerstate.last_seen {
|
||||
if mime_parser.timestamp_sent <= peerstate.last_seen {
|
||||
info!(
|
||||
context,
|
||||
"Not doing AEAP from {} to {} because {} < {}.",
|
||||
&peerstate.addr,
|
||||
&mime_parser.from.addr,
|
||||
info.message_time,
|
||||
mime_parser.timestamp_sent,
|
||||
peerstate.last_seen
|
||||
);
|
||||
return Ok(());
|
||||
@@ -832,24 +831,23 @@ pub(crate) async fn maybe_do_aeap_transition(
|
||||
"Doing AEAP transition from {} to {}.", &peerstate.addr, &mime_parser.from.addr
|
||||
);
|
||||
|
||||
let info = &mut mime_parser.decryption_info;
|
||||
let peerstate = info.peerstate.as_mut().context("no peerstate??")?;
|
||||
let peerstate = mime_parser.peerstate.as_mut().context("no peerstate??")?;
|
||||
// Add info messages to chats with this (verified) contact
|
||||
//
|
||||
peerstate
|
||||
.handle_setup_change(
|
||||
context,
|
||||
info.message_time,
|
||||
PeerstateChange::Aeap(info.from.clone()),
|
||||
mime_parser.timestamp_sent,
|
||||
PeerstateChange::Aeap(mime_parser.from.addr.clone()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let old_addr = mem::take(&mut peerstate.addr);
|
||||
peerstate.addr.clone_from(&info.from);
|
||||
let header = info.autocrypt_header.as_ref().context(
|
||||
peerstate.addr.clone_from(&mime_parser.from.addr);
|
||||
let header = mime_parser.autocrypt_header.as_ref().context(
|
||||
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
|
||||
)?;
|
||||
peerstate.apply_header(context, header, info.message_time);
|
||||
peerstate.apply_header(context, header, mime_parser.timestamp_sent);
|
||||
|
||||
peerstate
|
||||
.save_to_db_ex(&context.sql, Some(&old_addr))
|
||||
|
||||
67
src/pgp.rs
67
src/pgp.rs
@@ -297,34 +297,34 @@ pub fn pk_calc_signature(
|
||||
///
|
||||
/// Receiver private keys are provided in
|
||||
/// `private_keys_for_decryption`.
|
||||
///
|
||||
/// Returns decrypted message and fingerprints
|
||||
/// of all keys from the `public_keys_for_validation` keyring that
|
||||
/// have valid signatures there.
|
||||
#[allow(clippy::implicit_hasher)]
|
||||
pub fn pk_decrypt(
|
||||
ctext: Vec<u8>,
|
||||
private_keys_for_decryption: &[SignedSecretKey],
|
||||
public_keys_for_validation: &[SignedPublicKey],
|
||||
) -> Result<(Vec<u8>, HashSet<Fingerprint>)> {
|
||||
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
|
||||
|
||||
) -> Result<pgp::composed::Message> {
|
||||
let cursor = Cursor::new(ctext);
|
||||
let (msg, _) = Message::from_armor_single(cursor)?;
|
||||
let (msg, _headers) = Message::from_armor_single(cursor)?;
|
||||
|
||||
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect();
|
||||
|
||||
let (msg, _) = msg.decrypt(|| "".into(), &skeys[..])?;
|
||||
let (msg, _key_ids) = msg.decrypt(|| "".into(), &skeys[..])?;
|
||||
|
||||
// get_content() will decompress the message if needed,
|
||||
// but this avoids decompressing it again to check signatures
|
||||
let msg = msg.decompress()?;
|
||||
|
||||
let content = match msg.get_content()? {
|
||||
Some(content) => content,
|
||||
None => bail!("The decrypted message is empty"),
|
||||
};
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Returns fingerprints
|
||||
/// of all keys from the `public_keys_for_validation` keyring that
|
||||
/// have valid signatures there.
|
||||
///
|
||||
/// If the message is wrongly signed, HashSet will be empty.
|
||||
pub fn valid_signature_fingerprints(
|
||||
msg: &pgp::composed::Message,
|
||||
public_keys_for_validation: &[SignedPublicKey],
|
||||
) -> Result<HashSet<Fingerprint>> {
|
||||
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
|
||||
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
|
||||
for pkey in public_keys_for_validation {
|
||||
if signed_msg.verify(&pkey.primary_key).is_ok() {
|
||||
@@ -333,7 +333,7 @@ pub fn pk_decrypt(
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((content, ret_signature_fingerprints))
|
||||
Ok(ret_signature_fingerprints)
|
||||
}
|
||||
|
||||
/// Validates detached signature.
|
||||
@@ -407,6 +407,18 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::{alice_keypair, bob_keypair};
|
||||
|
||||
fn pk_decrypt_and_validate(
|
||||
ctext: Vec<u8>,
|
||||
private_keys_for_decryption: &[SignedSecretKey],
|
||||
public_keys_for_validation: &[SignedPublicKey],
|
||||
) -> Result<(pgp::composed::Message, HashSet<Fingerprint>)> {
|
||||
let msg = pk_decrypt(ctext, private_keys_for_decryption)?;
|
||||
let ret_signature_fingerprints =
|
||||
valid_signature_fingerprints(&msg, public_keys_for_validation)?;
|
||||
|
||||
Ok((msg, ret_signature_fingerprints))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_armored_data_1() {
|
||||
let (typ, _headers, base64) = split_armored_data(
|
||||
@@ -534,34 +546,35 @@ mod tests {
|
||||
// Check decrypting as Alice
|
||||
let decrypt_keyring = vec![KEYS.alice_secret.clone()];
|
||||
let sig_check_keyring = vec![KEYS.alice_public.clone()];
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
let (msg, valid_signatures) = pk_decrypt_and_validate(
|
||||
ctext_signed().await.as_bytes().to_vec(),
|
||||
&decrypt_keyring,
|
||||
&sig_check_keyring,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 1);
|
||||
|
||||
// Check decrypting as Bob
|
||||
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
|
||||
let sig_check_keyring = vec![KEYS.alice_public.clone()];
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
let (msg, valid_signatures) = pk_decrypt_and_validate(
|
||||
ctext_signed().await.as_bytes().to_vec(),
|
||||
&decrypt_keyring,
|
||||
&sig_check_keyring,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decrypt_no_sig_check() {
|
||||
let keyring = vec![KEYS.alice_secret.clone()];
|
||||
let (plain, valid_signatures) =
|
||||
pk_decrypt(ctext_signed().await.as_bytes().to_vec(), &keyring, &[]).unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
let (msg, valid_signatures) =
|
||||
pk_decrypt_and_validate(ctext_signed().await.as_bytes().to_vec(), &keyring, &[])
|
||||
.unwrap();
|
||||
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 0);
|
||||
}
|
||||
|
||||
@@ -570,26 +583,26 @@ mod tests {
|
||||
// The validation does not have the public key of the signer.
|
||||
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
|
||||
let sig_check_keyring = vec![KEYS.bob_public.clone()];
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
let (msg, valid_signatures) = pk_decrypt_and_validate(
|
||||
ctext_signed().await.as_bytes().to_vec(),
|
||||
&decrypt_keyring,
|
||||
&sig_check_keyring,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decrypt_unsigned() {
|
||||
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
let (msg, valid_signatures) = pk_decrypt_and_validate(
|
||||
ctext_unsigned().await.as_bytes().to_vec(),
|
||||
&decrypt_keyring,
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::simplify::split_lines;
|
||||
use crate::simplify::remove_message_footer;
|
||||
|
||||
/// Plaintext message body together with format=flowed attributes.
|
||||
#[derive(Debug)]
|
||||
@@ -32,7 +32,8 @@ impl PlainText {
|
||||
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
|
||||
});
|
||||
|
||||
let lines = split_lines(&self.text);
|
||||
let lines: Vec<&str> = self.text.lines().collect();
|
||||
let (lines, _footer) = remove_message_footer(&lines);
|
||||
|
||||
let mut ret = r#"<!DOCTYPE html>
|
||||
<html><head>
|
||||
@@ -136,7 +137,28 @@ line 1<br/>
|
||||
line 2<br/>
|
||||
line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a> and <a href="http://link-end-of-line.com/file?foo=bar%20">http://link-end-of-line.com/file?foo=bar%20</a><br/>
|
||||
<a href="http://link-at-start-of-line.org">http://link-at-start-of-line.org</a><br/>
|
||||
<br/>
|
||||
</body></html>
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plain_remove_signature() {
|
||||
let html = PlainText {
|
||||
text: "Foo\nbar\n-- \nSignature here".to_string(),
|
||||
flowed: false,
|
||||
delsp: false,
|
||||
}
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r#"<!DOCTYPE html>
|
||||
<html><head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
</head><body>
|
||||
Foo<br/>
|
||||
bar<br/>
|
||||
</body></html>
|
||||
"#
|
||||
);
|
||||
|
||||
@@ -201,7 +201,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
};
|
||||
|
||||
crate::peerstate::maybe_do_aeap_transition(context, &mut mime_parser).await?;
|
||||
if let Some(peerstate) = &mime_parser.decryption_info.peerstate {
|
||||
if let Some(peerstate) = &mime_parser.peerstate {
|
||||
peerstate
|
||||
.handle_fingerprint_change(context, mime_parser.timestamp_sent)
|
||||
.await?;
|
||||
@@ -356,8 +356,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
|
||||
// Peerstate could be updated by handling the Securejoin handshake.
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
mime_parser.decryption_info.peerstate =
|
||||
Peerstate::from_addr(context, contact.get_addr()).await?;
|
||||
mime_parser.peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
|
||||
} else {
|
||||
let to_id = to_ids.first().copied().unwrap_or_default();
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
@@ -393,7 +392,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
if verified_encryption == VerifiedEncryption::Verified
|
||||
&& mime_parser.get_header(HeaderDef::ChatVerified).is_some()
|
||||
{
|
||||
if let Some(peerstate) = &mut mime_parser.decryption_info.peerstate {
|
||||
if let Some(peerstate) = &mut mime_parser.peerstate {
|
||||
// NOTE: it might be better to remember ID of the key
|
||||
// that we used to decrypt the message, but
|
||||
// it is unlikely that default key ever changes
|
||||
@@ -1006,7 +1005,7 @@ async fn add_parts(
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if let Some(peerstate) = &mime_parser.decryption_info.peerstate {
|
||||
if let Some(peerstate) = &mime_parser.peerstate {
|
||||
restore_protection = new_protection != ProtectionStatus::Protected
|
||||
&& peerstate.prefer_encrypt == EncryptPreference::Mutual
|
||||
// Check that the contact still has the Autocrypt key same as the
|
||||
@@ -1424,7 +1423,11 @@ async fn add_parts(
|
||||
if let Some(msg) = group_changes_msgs.1 {
|
||||
match &better_msg {
|
||||
None => better_msg = Some(msg),
|
||||
Some(_) => group_changes_msgs.0.push(msg),
|
||||
Some(_) => {
|
||||
if !msg.is_empty() {
|
||||
group_changes_msgs.0.push(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1507,6 +1510,9 @@ async fn add_parts(
|
||||
|
||||
let mut txt_raw = "".to_string();
|
||||
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
|
||||
if better_msg.is_empty() && is_partial_download.is_none() {
|
||||
chat_id = DC_CHAT_ID_TRASH;
|
||||
}
|
||||
(better_msg, Viewtype::Text)
|
||||
} else {
|
||||
(&part.msg, part.typ)
|
||||
@@ -2078,8 +2084,11 @@ async fn create_group(
|
||||
|
||||
/// Apply group member list, name, avatar and protection status changes from the MIME message.
|
||||
///
|
||||
/// Optionally returns better message to replace the original system message.
|
||||
/// is_partial_download: whether the message is not fully downloaded.
|
||||
/// Returns `Vec` of group changes messages and, optionally, a better message to replace the
|
||||
/// original system message. If the better message is empty, the original system message should be
|
||||
/// just omitted.
|
||||
///
|
||||
/// * `is_partial_download` - whether the message is not fully downloaded.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn apply_group_changes(
|
||||
context: &Context,
|
||||
@@ -2181,39 +2190,47 @@ async fn apply_group_changes(
|
||||
|
||||
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
|
||||
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
|
||||
|
||||
better_msg = if removed_id == Some(from_id) {
|
||||
Some(stock_str::msg_group_left_local(context, from_id).await)
|
||||
} else {
|
||||
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
|
||||
};
|
||||
|
||||
if removed_id.is_some() {
|
||||
if !allow_member_list_changes {
|
||||
info!(
|
||||
context,
|
||||
"Ignoring removal of {removed_addr:?} from {chat_id}."
|
||||
);
|
||||
if let Some(id) = removed_id {
|
||||
if allow_member_list_changes && chat_contacts.contains(&id) {
|
||||
better_msg = if id == from_id {
|
||||
Some(stock_str::msg_group_left_local(context, from_id).await)
|
||||
} else {
|
||||
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Removed {removed_addr:?} has no contact id.")
|
||||
}
|
||||
better_msg.get_or_insert_with(Default::default);
|
||||
if !allow_member_list_changes {
|
||||
info!(
|
||||
context,
|
||||
"Ignoring removal of {removed_addr:?} from {chat_id}."
|
||||
);
|
||||
}
|
||||
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
|
||||
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
|
||||
|
||||
if allow_member_list_changes {
|
||||
if !recreate_member_list {
|
||||
if let Some(contact_id) =
|
||||
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
|
||||
{
|
||||
let is_new_member;
|
||||
if let Some(contact_id) =
|
||||
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
|
||||
{
|
||||
if !recreate_member_list {
|
||||
added_id = Some(contact_id);
|
||||
} else {
|
||||
warn!(context, "Added {added_addr:?} has no contact id.")
|
||||
}
|
||||
is_new_member = !chat_contacts.contains(&contact_id);
|
||||
} else {
|
||||
warn!(context, "Added {added_addr:?} has no contact id.");
|
||||
is_new_member = false;
|
||||
}
|
||||
|
||||
if is_new_member || self_added {
|
||||
better_msg =
|
||||
Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
|
||||
}
|
||||
} else {
|
||||
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
|
||||
}
|
||||
better_msg.get_or_insert_with(Default::default);
|
||||
} else if let Some(old_name) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||
.map(|s| s.trim())
|
||||
@@ -2662,7 +2679,7 @@ async fn update_verified_keys(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Some(peerstate) = &mut mimeparser.decryption_info.peerstate else {
|
||||
let Some(peerstate) = &mut mimeparser.peerstate else {
|
||||
// No peerstate means no verified keys.
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -2735,7 +2752,7 @@ async fn has_verified_encryption(
|
||||
// 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 Some(peerstate) = &mimeparser.decryption_info.peerstate else {
|
||||
let Some(peerstate) = &mimeparser.peerstate else {
|
||||
return Ok(NotVerified(
|
||||
"No peerstate, the contact isn't verified".to_string(),
|
||||
));
|
||||
|
||||
@@ -4185,9 +4185,8 @@ async fn test_recreate_contact_list_on_missing_message() -> Result<()> {
|
||||
// readd fiona
|
||||
add_contact_to_chat(&alice, chat_id, alice_fiona).await?;
|
||||
|
||||
alice.recv_msg(&remove_msg).await;
|
||||
|
||||
// delayed removal of fiona shouldn't remove her
|
||||
alice.recv_msg_trash(&remove_msg).await;
|
||||
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 4);
|
||||
|
||||
Ok(())
|
||||
@@ -4947,6 +4946,32 @@ async fn test_unarchive_on_member_removal() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_op_member_added_is_trash() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "foos", &[bob])
|
||||
.await;
|
||||
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let msg = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&msg).await;
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
|
||||
let fiona_id = Contact::create(alice, "", "fiona@example.net").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
|
||||
let msg = alice.pop_sent_msg().await;
|
||||
|
||||
let fiona_id = Contact::create(bob, "", "fiona@example.net").await?;
|
||||
add_contact_to_chat(bob, bob_chat_id, fiona_id).await?;
|
||||
bob.recv_msg_trash(&msg).await;
|
||||
let contacts = get_chat_contacts(bob, bob_chat_id).await?;
|
||||
assert_eq!(contacts.len(), 3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forged_from() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
@@ -135,7 +135,7 @@ impl SchedulerState {
|
||||
/// If in the meantime [`SchedulerState::start`] or [`SchedulerState::stop`] is called
|
||||
/// resume will do the right thing and restore the scheduler to the state requested by
|
||||
/// the last call.
|
||||
pub(crate) async fn pause<'a>(&'_ self, context: Context) -> Result<IoPausedGuard> {
|
||||
pub(crate) async fn pause(&'_ self, context: Context) -> Result<IoPausedGuard> {
|
||||
{
|
||||
let mut inner = self.inner.write().await;
|
||||
match *inner {
|
||||
|
||||
@@ -21,7 +21,9 @@ pub fn escape_message_footer_marks(text: &str) -> String {
|
||||
/// `footer_lines` is set to `Some` if the footer was actually removed from `lines`
|
||||
/// (which is equal to the input array otherwise).
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_message_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<&'a [&'a str]>) {
|
||||
pub(crate) fn remove_message_footer<'a>(
|
||||
lines: &'a [&str],
|
||||
) -> (&'a [&'a str], Option<&'a [&'a str]>) {
|
||||
let mut nearly_standard_footer = None;
|
||||
for (ix, &line) in lines.iter().enumerate() {
|
||||
match line {
|
||||
|
||||
@@ -357,9 +357,9 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
.await
|
||||
.context("failed to update retries count")?;
|
||||
|
||||
let (body, recipients, msg_id, retries) = context
|
||||
let Some((body, recipients, msg_id, retries)) = context
|
||||
.sql
|
||||
.query_row(
|
||||
.query_row_optional(
|
||||
"SELECT mime, recipients, msg_id, retries FROM smtp WHERE id=?",
|
||||
(rowid,),
|
||||
|row| {
|
||||
@@ -370,7 +370,10 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
Ok((mime, recipients, msg_id, retries))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
if retries > 6 {
|
||||
if let Some(mut msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
message::set_msg_failed(context, &mut msg, "Number of retries exceeded the limit.")
|
||||
|
||||
Reference in New Issue
Block a user