mirror of
https://github.com/chatmail/core.git
synced 2026-04-24 17:06:28 +03:00
Prefer encryption for the peer if the message is encrypted or signed with the known key (#3844)
Note that if the message is encrypted, we don't check whether it's signed with an attached key currently, otherwise a massive refactoring of the code is needed because for encrypted messages a signature is checked and discarded first now.
This commit is contained in:
@@ -18,7 +18,10 @@ use crate::blob::BlobObject;
|
||||
use crate::constants::{DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN};
|
||||
use crate::contact::{addr_cmp, addr_normalize, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::decrypt::{prepare_decryption, try_decrypt, DecryptionInfo};
|
||||
use crate::decrypt::{
|
||||
keyring_from_peerstate, prepare_decryption, try_decrypt, validate_detached_signature,
|
||||
DecryptionInfo,
|
||||
};
|
||||
use crate::dehtml::dehtml;
|
||||
use crate::events::EventType;
|
||||
use crate::format_flowed::unformat_flowed;
|
||||
@@ -64,7 +67,8 @@ pub struct MimeMessage {
|
||||
/// If a message is not encrypted or the signature is not valid,
|
||||
/// this set is empty.
|
||||
pub signatures: HashSet<Fingerprint>,
|
||||
|
||||
/// Whether the message is encrypted in a domestic (not Autocrypt) sense
|
||||
pub encrypted: bool,
|
||||
/// The set of mail recipient addresses for which gossip headers were applied, regardless of
|
||||
/// whether they modified any peerstates.
|
||||
pub gossiped_addr: HashSet<String>,
|
||||
@@ -232,91 +236,95 @@ impl MimeMessage {
|
||||
hop_info += &decryption_info.dkim_results.to_string();
|
||||
|
||||
// `signatures` is non-empty exactly if the message was encrypted and correctly signed.
|
||||
let (mail, signatures, warn_empty_signature) =
|
||||
match try_decrypt(context, &mail, &decryption_info).await {
|
||||
Ok(Some((raw, signatures))) => {
|
||||
// Encrypted, but maybe unsigned message. Only if
|
||||
// `signatures` set is non-empty, it is a valid
|
||||
// autocrypt message.
|
||||
let (mail, signatures, encrypted) = match try_decrypt(context, &mail, &decryption_info)
|
||||
.await
|
||||
{
|
||||
Ok(Some((raw, signatures))) => {
|
||||
// Encrypted, but maybe unsigned message. Only if
|
||||
// `signatures` set is non-empty, it is a valid
|
||||
// autocrypt message.
|
||||
|
||||
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:");
|
||||
println!("{}", String::from_utf8_lossy(&mail_raw));
|
||||
}
|
||||
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:");
|
||||
println!("{}", String::from_utf8_lossy(&mail_raw));
|
||||
}
|
||||
|
||||
// Handle any gossip headers if the mail was encrypted. See section
|
||||
// "3.6 Key Gossip" of <https://autocrypt.org/autocrypt-spec-1.1.0.pdf>
|
||||
// but only if the mail was correctly signed:
|
||||
if !signatures.is_empty() {
|
||||
let gossip_headers =
|
||||
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
|
||||
gossiped_addr =
|
||||
update_gossip_peerstates(context, message_time, &mail, gossip_headers)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// let known protected headers from the decrypted
|
||||
// part override the unencrypted top-level
|
||||
|
||||
// Signature was checked for original From, so we
|
||||
// do not allow overriding it.
|
||||
let mut signed_from = None;
|
||||
|
||||
// We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe.
|
||||
// See <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
|
||||
headers.remove("subject");
|
||||
|
||||
MimeMessage::merge_headers(
|
||||
// Handle any gossip headers if the mail was encrypted. See section
|
||||
// "3.6 Key Gossip" of <https://autocrypt.org/autocrypt-spec-1.1.0.pdf>
|
||||
// but only if the mail was correctly signed:
|
||||
if !signatures.is_empty() {
|
||||
let gossip_headers = decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
|
||||
gossiped_addr = update_gossip_peerstates(
|
||||
context,
|
||||
&mut headers,
|
||||
&mut recipients,
|
||||
&mut signed_from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
&decrypted_mail.headers,
|
||||
);
|
||||
if let Some(signed_from) = signed_from {
|
||||
if addr_cmp(&signed_from.addr, &from.addr) {
|
||||
from_is_signed = true;
|
||||
} else {
|
||||
// There is a From: header in the encrypted &
|
||||
// signed part, but it doesn't match the outer one.
|
||||
// This _might_ be because the sender's mail server
|
||||
// replaced the sending address, e.g. in a mailing list.
|
||||
// Or it's because someone is doing some replay attack
|
||||
// - OTOH, I can't come up with an attack scenario
|
||||
// where this would be useful.
|
||||
warn!(
|
||||
context,
|
||||
"From header in signed part does't match the outer one"
|
||||
);
|
||||
}
|
||||
}
|
||||
message_time,
|
||||
&from.addr,
|
||||
&mail,
|
||||
gossip_headers,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
(Ok(decrypted_mail), signatures, true)
|
||||
}
|
||||
Ok(None) => {
|
||||
// Message was not encrypted.
|
||||
// If it is not a read receipt, degrade encryption.
|
||||
if let Some(peerstate) = &mut decryption_info.peerstate {
|
||||
if message_time > peerstate.last_seen_autocrypt
|
||||
&& mail.ctype.mimetype != "multipart/report"
|
||||
// Disallowing keychanges is disabled for now:
|
||||
// && decryption_info.dkim_results.allow_keychange
|
||||
{
|
||||
peerstate.degrade_encryption(message_time);
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
}
|
||||
// let known protected headers from the decrypted
|
||||
// part override the unencrypted top-level
|
||||
|
||||
// Signature was checked for original From, so we
|
||||
// do not allow overriding it.
|
||||
let mut signed_from = None;
|
||||
|
||||
// We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe.
|
||||
// See <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
|
||||
headers.remove("subject");
|
||||
|
||||
MimeMessage::merge_headers(
|
||||
context,
|
||||
&mut headers,
|
||||
&mut recipients,
|
||||
&mut signed_from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
&decrypted_mail.headers,
|
||||
);
|
||||
if let Some(signed_from) = signed_from {
|
||||
if addr_cmp(&signed_from.addr, &from.addr) {
|
||||
from_is_signed = true;
|
||||
} else {
|
||||
// There is a From: header in the encrypted &
|
||||
// signed part, but it doesn't match the outer one.
|
||||
// This _might_ be because the sender's mail server
|
||||
// replaced the sending address, e.g. in a mailing list.
|
||||
// Or it's because someone is doing some replay attack
|
||||
// - OTOH, I can't come up with an attack scenario
|
||||
// where this would be useful.
|
||||
warn!(
|
||||
context,
|
||||
"From header in signed part does't match the outer one",
|
||||
);
|
||||
}
|
||||
(Ok(mail), HashSet::new(), false)
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "decryption failed: {}", err);
|
||||
(Err(err), HashSet::new(), true)
|
||||
|
||||
(Ok(decrypted_mail), signatures, true)
|
||||
}
|
||||
Ok(None) => {
|
||||
// Message was not encrypted.
|
||||
// If it is not a read receipt, degrade encryption.
|
||||
if let Some(peerstate) = &mut decryption_info.peerstate {
|
||||
if message_time > peerstate.last_seen_autocrypt
|
||||
&& mail.ctype.mimetype != "multipart/report"
|
||||
// Disallowing keychanges is disabled for now:
|
||||
// && decryption_info.dkim_results.allow_keychange
|
||||
{
|
||||
peerstate.degrade_encryption(message_time);
|
||||
}
|
||||
}
|
||||
};
|
||||
(Ok(mail), HashSet::new(), false)
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "decryption failed: {}", err);
|
||||
(Err(err), HashSet::new(), true)
|
||||
}
|
||||
};
|
||||
|
||||
let mut parser = MimeMessage {
|
||||
parts: Vec::new(),
|
||||
@@ -331,6 +339,7 @@ impl MimeMessage {
|
||||
|
||||
// only non-empty if it was a valid autocrypt message
|
||||
signatures,
|
||||
encrypted,
|
||||
gossiped_addr,
|
||||
is_forwarded: false,
|
||||
mdn_reports: Vec::new(),
|
||||
@@ -385,7 +394,7 @@ impl MimeMessage {
|
||||
// part.error = Some("Seems like DKIM failed, this either is an attack or (more likely) a bug in Authentication-Results checking. Please tell us about this at https://support.delta.chat.".to_string());
|
||||
// }
|
||||
// }
|
||||
if warn_empty_signature && parser.signatures.is_empty() {
|
||||
if encrypted && parser.signatures.is_empty() {
|
||||
for part in parser.parts.iter_mut() {
|
||||
part.error = Some("No valid signature".to_string());
|
||||
}
|
||||
@@ -400,6 +409,13 @@ impl MimeMessage {
|
||||
peerstate
|
||||
.handle_fingerprint_change(context, message_time)
|
||||
.await?;
|
||||
// When peerstate is set to Mutual, it's saved immediately to not lose that fact in case
|
||||
// of an error. Otherwise we don't save peerstate until get here to reduce the number of
|
||||
// calls to save_to_db() and not to degrade encryption if a mail wasn't parsed
|
||||
// successfully.
|
||||
if peerstate.prefer_encrypt != EncryptPreference::Mutual {
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(parser)
|
||||
@@ -852,6 +868,26 @@ impl MimeMessage {
|
||||
.parse_mime_recursive(context, first, is_related)
|
||||
.await?;
|
||||
}
|
||||
if let Some(peerstate) = &mut self.decryption_info.peerstate {
|
||||
let keyring = keyring_from_peerstate(Some(peerstate));
|
||||
match validate_detached_signature(mail, &keyring) {
|
||||
Ok(Some((_, fprints))) => {
|
||||
if fprints.is_empty() {
|
||||
warn!(context, "signed message is not signed with a known key");
|
||||
} else if peerstate.prefer_encrypt != EncryptPreference::Mutual {
|
||||
info!(
|
||||
context,
|
||||
"message is signed with the known key, setting \
|
||||
prefer-encrypt=mutual for '{}'",
|
||||
peerstate.addr,
|
||||
);
|
||||
Self::upgrade_to_mutual_encryption(context, peerstate).await?;
|
||||
}
|
||||
}
|
||||
Ok(None) => warn!(context, "not a 'multipart/signed' part??"),
|
||||
Err(err) => warn!(context, "signed message validation failed: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
(mime::MULTIPART, "report") => {
|
||||
/* RFC 6522: the first part is for humans, the second for machines */
|
||||
@@ -929,6 +965,17 @@ impl MimeMessage {
|
||||
Ok(any_part_added)
|
||||
}
|
||||
|
||||
async fn upgrade_to_mutual_encryption(
|
||||
context: &Context,
|
||||
peerstate: &mut Peerstate,
|
||||
) -> Result<()> {
|
||||
if peerstate.public_key.is_none() {
|
||||
peerstate.public_key = peerstate.gossip_key.take();
|
||||
}
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await
|
||||
}
|
||||
|
||||
/// Returns true if any part was added, false otherwise.
|
||||
async fn add_single_part_if_known(
|
||||
&mut self,
|
||||
@@ -1106,7 +1153,13 @@ impl MimeMessage {
|
||||
if peerstate.prefer_encrypt != EncryptPreference::Mutual
|
||||
&& mime_type.type_() == mime::APPLICATION
|
||||
&& mime_type.subtype().as_str() == "pgp-keys"
|
||||
&& Self::try_set_peer_key_from_file_part(context, peerstate, decoded_data).await?
|
||||
&& Self::try_set_peer_key_from_file_part(
|
||||
context,
|
||||
peerstate,
|
||||
decoded_data,
|
||||
self.encrypted,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1196,6 +1249,7 @@ impl MimeMessage {
|
||||
context: &Context,
|
||||
peerstate: &mut Peerstate,
|
||||
decoded_data: &[u8],
|
||||
mail_is_encrypted: bool,
|
||||
) -> Result<bool> {
|
||||
let key = match str::from_utf8(decoded_data) {
|
||||
Err(err) => {
|
||||
@@ -1240,13 +1294,23 @@ impl MimeMessage {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"will use attached PGP key for peer '{}' with mutual encryption", peerstate.addr,
|
||||
);
|
||||
peerstate.public_key = Some(key);
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
if mail_is_encrypted {
|
||||
info!(
|
||||
context,
|
||||
"using attached PGP key for peer '{}' with prefer-encrypt=mutual as the mail is \
|
||||
encrypted",
|
||||
peerstate.addr,
|
||||
);
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"using attached PGP key for peer '{}'", peerstate.addr,
|
||||
);
|
||||
peerstate.prefer_encrypt = EncryptPreference::NoPreference;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -1624,6 +1688,7 @@ impl MimeMessage {
|
||||
async fn update_gossip_peerstates(
|
||||
context: &Context,
|
||||
message_time: i64,
|
||||
from: &str,
|
||||
mail: &mailparse::ParsedMail<'_>,
|
||||
gossip_headers: Vec<String>,
|
||||
) -> Result<HashSet<String>> {
|
||||
@@ -1641,7 +1706,7 @@ async fn update_gossip_peerstates(
|
||||
|
||||
if !get_recipients(&mail.headers)
|
||||
.iter()
|
||||
.any(|info| info.addr == header.addr.to_lowercase())
|
||||
.any(|info| addr_cmp(&info.addr, &header.addr))
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
@@ -1649,6 +1714,14 @@ async fn update_gossip_peerstates(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if addr_cmp(from, &header.addr) {
|
||||
// Non-standard, but anyway we can't update the cached peerstate here.
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring gossiped \"{}\" as it equals the From address", &header.addr,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let peerstate;
|
||||
if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? {
|
||||
|
||||
Reference in New Issue
Block a user