feat: experimental header protection for Autocrypt

This change adds support for receiving
Autocrypt header in the protected part of encrypted message.

Autocrypt header is now also allowed in mailing lists.
Previously Autocrypt header was rejected when
List-Post header was present,
but the check for the address being equal to the From: address
is sufficient.

New experimental `protect_autocrypt` config is disabled
by default because Delta Chat with reception
support should be released first on all platforms.
This commit is contained in:
link2xt
2024-11-02 22:27:06 +00:00
committed by l
parent b96593ed10
commit faad576d10
10 changed files with 207 additions and 193 deletions

View File

@@ -506,6 +506,11 @@ char* dc_get_blobdir (const dc_context_t* context);
* to not mess up with non-delivery-reports or read-receipts. * to not mess up with non-delivery-reports or read-receipts.
* 0=no limit (default). * 0=no limit (default).
* Changes affect future messages only. * 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 * - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
* seconds. 2 days by default. * seconds. 2 days by default.
* This is not supposed to be changed by UIs and only used for testing. * This is not supposed to be changed by UIs and only used for testing.

View File

@@ -520,8 +520,13 @@ Authentication-Results: dkim=";
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap(); 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)] #[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 mut tcm = TestContextManager::new();
let alice = tcm.alice().await; let alice = tcm.alice().await;
let bob = tcm.bob().await; let bob = tcm.bob().await;
@@ -533,28 +538,18 @@ Authentication-Results: dkim=";
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n"); .insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
bob.recv_msg(&sent).await; bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").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()); 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 let mut sent = bob
.send_text(bob_alice_chat.id, "hellooo in the mailinglist again") .send_text(bob_alice_chat.id, "hellooo in the mailinglist again")
.await; .await;
assert!(sent.load_from_db().await.get_showpadlock()); 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 sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n"); .insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
let rcvd = alice.recv_msg(&sent).await; let rcvd = alice.recv_msg(&sent).await;
assert!(!rcvd.get_showpadlock()); assert!(rcvd.get_showpadlock());
assert_eq!(&rcvd.text, "hellooo in the mailinglist again"); assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
Ok(()) Ok(())

View File

@@ -396,6 +396,12 @@ pub enum Config {
/// Make all outgoing messages with Autocrypt header "multipart/signed". /// Make all outgoing messages with Autocrypt header "multipart/signed".
SignUnencrypted, 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. /// Let the core save all events to the database.
/// This value is used internally to remember the MsgId of the logging xdc /// This value is used internally to remember the MsgId of the logging xdc
#[strum(props(default = "0"))] #[strum(props(default = "0"))]

View File

@@ -990,6 +990,12 @@ impl Context {
.await? .await?
.to_string(), .to_string(),
); );
res.insert(
"protect_autocrypt",
self.get_config_int(Config::ProtectAutocrypt)
.await?
.to_string(),
);
res.insert( res.insert(
"debug_logging", "debug_logging",
self.get_config_int(Config::DebugLogging).await?.to_string(), self.get_config_int(Config::DebugLogging).await?.to_string(),

View File

@@ -1,125 +1,36 @@
//! End-to-end decryption support. //! End-to-end decryption support.
use std::collections::HashSet; use std::collections::HashSet;
use std::str::FromStr;
use anyhow::Result; use anyhow::Result;
use deltachat_contact_tools::addr_cmp; use deltachat_contact_tools::addr_cmp;
use mailparse::ParsedMail; use mailparse::ParsedMail;
use crate::aheader::Aheader; use crate::aheader::Aheader;
use crate::authres::handle_authres;
use crate::authres::{self, DkimResults};
use crate::context::Context; use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey}; use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::peerstate::Peerstate; use crate::peerstate::Peerstate;
use crate::pgp; use crate::pgp;
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message. /// 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 /// If successful and the message is encrypted, returns decrypted body.
/// signature fingerprints.
///
/// If the message is wrongly signed, HashSet will be empty.
pub fn try_decrypt( pub fn try_decrypt(
mail: &ParsedMail<'_>, mail: &ParsedMail<'_>,
private_keyring: &[SignedSecretKey], private_keyring: &[SignedSecretKey],
public_keyring_for_validate: &[SignedPublicKey], ) -> Result<Option<::pgp::composed::Message>> {
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let Some(encrypted_data_part) = get_encrypted_mime(mail) else { let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
return Ok(None); return Ok(None);
}; };
let data = encrypted_data_part.get_body_raw()?; let data = encrypted_data_part.get_body_raw()?;
let msg = pgp::pk_decrypt(data, private_keyring)?;
let (plain, ret_valid_signatures) = Ok(Some(msg))
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,
} }
/// Returns a reference to the encrypted payload of a message. /// 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) get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail)) .or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail)) .or_else(|| get_attachment_mime(mail))

View File

@@ -743,7 +743,9 @@ impl MimeFactory {
hidden_headers.push(header); hidden_headers.push(header);
} else if header_name == "chat-user-avatar" { } else if header_name == "chat-user-avatar" {
hidden_headers.push(header); 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()); unprotected_headers.push(header.clone());
} else if header_name == "from" { } else if header_name == "from" {
// Unencrypted securejoin messages should _not_ include the display name: // Unencrypted securejoin messages should _not_ include the display name:

View File

@@ -4,6 +4,7 @@ use std::cmp::min;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::Path; use std::path::Path;
use std::str; use std::str;
use std::str::FromStr;
use anyhow::{bail, Context as _, Result}; use anyhow::{bail, Context as _, Result};
use deltachat_contact_tools::{addr_cmp, addr_normalize, sanitize_bidi_characters}; 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 rand::distributions::{Alphanumeric, DistString};
use crate::aheader::{Aheader, EncryptPreference}; use crate::aheader::{Aheader, EncryptPreference};
use crate::authres::handle_authres;
use crate::blob::BlobObject; use crate::blob::BlobObject;
use crate::chat::{add_info_msg, ChatId}; use crate::chat::{add_info_msg, ChatId};
use crate::config::Config; use crate::config::Config;
@@ -21,8 +23,8 @@ use crate::constants::{self, Chattype};
use crate::contact::{Contact, ContactId, Origin}; use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context; use crate::context::Context;
use crate::decrypt::{ use crate::decrypt::{
keyring_from_peerstate, prepare_decryption, try_decrypt, validate_detached_signature, get_autocrypt_peerstate, get_encrypted_mime, keyring_from_peerstate, try_decrypt,
DecryptionInfo, validate_detached_signature,
}; };
use crate::dehtml::dehtml; use crate::dehtml::dehtml;
use crate::events::EventType; use crate::events::EventType;
@@ -71,7 +73,8 @@ pub(crate) struct MimeMessage {
/// messages to this address to post them to the list. /// messages to this address to post them to the list.
pub list_post: Option<String>, pub list_post: Option<String>,
pub chat_disposition_notification_to: Option<SingleInfo>, pub chat_disposition_notification_to: Option<SingleInfo>,
pub decryption_info: DecryptionInfo, pub autocrypt_header: Option<Aheader>,
pub peerstate: Option<Peerstate>,
pub decrypting_failed: bool, pub decrypting_failed: bool,
/// Set of valid signature fingerprints if a message is an /// 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 mut from = from.context("No from in message")?;
let private_keyring = load_self_secret_keyring(context).await?; let private_keyring = load_self_secret_keyring(context).await?;
let mut decryption_info = let allow_aeap = get_encrypted_mime(&mail).is_some();
prepare_decryption(context, &mail, &from.addr, timestamp_sent).await?;
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 gossiped_keys = Default::default();
let mut from_is_signed = false; let mut from_is_signed = false;
hop_info += "\n\n"; 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 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?, let mut aheader_value: Option<String> = mail.headers.get_header_value(HeaderDef::Autocrypt);
false => keyring_from_peerstate(decryption_info.peerstate.as_ref()),
}; let mail_raw; // Memory location for a possible decrypted message.
let (mail, mut signatures, encrypted) = match tokio::task::block_in_place(|| { let decrypted_msg; // Decrypted signed OpenPGP message.
try_decrypt(&mail, &private_keyring, &public_keyring)
}) { let (mail, encrypted) =
Ok(Some((raw, signatures))) => { match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring)) {
mail_raw = raw; Ok(Some(msg)) => {
let decrypted_mail = mailparse::parse_mail(&mail_raw)?; mail_raw = msg.get_content()?.unwrap_or_default();
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!( let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
context, if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
"decrypted message mime-body:\n{}", info!(
String::from_utf8_lossy(&mail_raw), 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 mail = mail.as_ref().map(|mail| {
let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring) let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
.unwrap_or((mail, Default::default())); .unwrap_or((mail, Default::default()));
@@ -422,7 +484,7 @@ impl MimeMessage {
Self::remove_secured_headers(&mut headers); Self::remove_secured_headers(&mut headers);
// If it is not a read receipt, degrade encryption. // 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 if timestamp_sent > peerstate.last_seen_autocrypt
&& mail.ctype.mimetype != "multipart/report" && mail.ctype.mimetype != "multipart/report"
{ {
@@ -433,7 +495,7 @@ impl MimeMessage {
if !encrypted { if !encrypted {
signatures.clear(); 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() { if peerstate.prefer_encrypt != EncryptPreference::Mutual && !signatures.is_empty() {
peerstate.prefer_encrypt = EncryptPreference::Mutual; peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.save_to_db(&context.sql).await?; peerstate.save_to_db(&context.sql).await?;
@@ -449,7 +511,8 @@ impl MimeMessage {
from_is_signed, from_is_signed,
incoming, incoming,
chat_disposition_notification_to, chat_disposition_notification_to,
decryption_info, autocrypt_header,
peerstate,
decrypting_failed: mail.is_err(), decrypting_failed: mail.is_err(),
// only non-empty if it was a valid autocrypt message // only non-empty if it was a valid autocrypt message
@@ -1231,7 +1294,7 @@ impl MimeMessage {
if decoded_data.is_empty() { if decoded_data.is_empty() {
return Ok(()); return Ok(());
} }
if let Some(peerstate) = &mut self.decryption_info.peerstate { if let Some(peerstate) = &mut self.peerstate {
if peerstate.prefer_encrypt != EncryptPreference::Mutual if peerstate.prefer_encrypt != EncryptPreference::Mutual
&& mime_type.type_() == mime::APPLICATION && mime_type.type_() == mime::APPLICATION
&& mime_type.subtype().as_str() == "pgp-keys" && mime_type.subtype().as_str() == "pgp-keys"
@@ -4012,12 +4075,8 @@ Content-Disposition: reaction\n\
// We do allow the time to be in the future a bit (because of unsynchronized clocks), // We do allow the time to be in the future a bit (because of unsynchronized clocks),
// but only 60 seconds: // but only 60 seconds:
assert!(mime_message.decryption_info.message_time <= time() + 60); assert!(mime_message.timestamp_sent <= time() + 60);
assert!(mime_message.decryption_info.message_time >= beginning_time + 60); assert!(mime_message.timestamp_sent >= beginning_time + 60);
assert_eq!(
mime_message.decryption_info.message_time,
mime_message.timestamp_sent
);
assert!(mime_message.timestamp_rcvd <= time()); assert!(mime_message.timestamp_rcvd <= time());
Ok(()) Ok(())
@@ -4088,4 +4147,24 @@ Content-Type: text/plain; charset=utf-8
"alice@example.org" "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(())
}
} }

View File

@@ -766,8 +766,7 @@ pub(crate) async fn maybe_do_aeap_transition(
context: &Context, context: &Context,
mime_parser: &mut crate::mimeparser::MimeMessage, mime_parser: &mut crate::mimeparser::MimeMessage,
) -> Result<()> { ) -> Result<()> {
let info = &mime_parser.decryption_info; let Some(peerstate) = &mime_parser.peerstate else {
let Some(peerstate) = &info.peerstate else {
return Ok(()); 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 // DC avoids sending messages with the same timestamp, that's why messages
// with equal timestamps are ignored here unlike in `Peerstate::apply_header()`. // 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!( info!(
context, context,
"Not doing AEAP from {} to {} because {} < {}.", "Not doing AEAP from {} to {} because {} < {}.",
&peerstate.addr, &peerstate.addr,
&mime_parser.from.addr, &mime_parser.from.addr,
info.message_time, mime_parser.timestamp_sent,
peerstate.last_seen peerstate.last_seen
); );
return Ok(()); 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 "Doing AEAP transition from {} to {}.", &peerstate.addr, &mime_parser.from.addr
); );
let info = &mut mime_parser.decryption_info; let peerstate = mime_parser.peerstate.as_mut().context("no peerstate??")?;
let peerstate = info.peerstate.as_mut().context("no peerstate??")?;
// Add info messages to chats with this (verified) contact // Add info messages to chats with this (verified) contact
// //
peerstate peerstate
.handle_setup_change( .handle_setup_change(
context, context,
info.message_time, mime_parser.timestamp_sent,
PeerstateChange::Aeap(info.from.clone()), PeerstateChange::Aeap(mime_parser.from.addr.clone()),
) )
.await?; .await?;
let old_addr = mem::take(&mut peerstate.addr); let old_addr = mem::take(&mut peerstate.addr);
peerstate.addr.clone_from(&info.from); peerstate.addr.clone_from(&mime_parser.from.addr);
let header = info.autocrypt_header.as_ref().context( let header = mime_parser.autocrypt_header.as_ref().context(
"Internal error: Tried to do an AEAP transition without an autocrypt header??", "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 peerstate
.save_to_db_ex(&context.sql, Some(&old_addr)) .save_to_db_ex(&context.sql, Some(&old_addr))

View File

@@ -297,34 +297,34 @@ pub fn pk_calc_signature(
/// ///
/// Receiver private keys are provided in /// Receiver private keys are provided in
/// `private_keys_for_decryption`. /// `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( pub fn pk_decrypt(
ctext: Vec<u8>, ctext: Vec<u8>,
private_keys_for_decryption: &[SignedSecretKey], private_keys_for_decryption: &[SignedSecretKey],
public_keys_for_validation: &[SignedPublicKey], ) -> Result<pgp::composed::Message> {
) -> Result<(Vec<u8>, HashSet<Fingerprint>)> {
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
let cursor = Cursor::new(ctext); 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 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, // get_content() will decompress the message if needed,
// but this avoids decompressing it again to check signatures // but this avoids decompressing it again to check signatures
let msg = msg.decompress()?; let msg = msg.decompress()?;
let content = match msg.get_content()? { Ok(msg)
Some(content) => content, }
None => bail!("The decrypted message is empty"),
};
/// 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 { if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
for pkey in public_keys_for_validation { for pkey in public_keys_for_validation {
if signed_msg.verify(&pkey.primary_key).is_ok() { 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. /// Validates detached signature.
@@ -407,6 +407,18 @@ mod tests {
use super::*; use super::*;
use crate::test_utils::{alice_keypair, bob_keypair}; 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] #[test]
fn test_split_armored_data_1() { fn test_split_armored_data_1() {
let (typ, _headers, base64) = split_armored_data( let (typ, _headers, base64) = split_armored_data(
@@ -534,34 +546,35 @@ mod tests {
// Check decrypting as Alice // Check decrypting as Alice
let decrypt_keyring = vec![KEYS.alice_secret.clone()]; let decrypt_keyring = vec![KEYS.alice_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.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(), ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring, &decrypt_keyring,
&sig_check_keyring, &sig_check_keyring,
) )
.unwrap(); .unwrap();
assert_eq!(plain, CLEARTEXT); assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 1); assert_eq!(valid_signatures.len(), 1);
// Check decrypting as Bob // Check decrypting as Bob
let decrypt_keyring = vec![KEYS.bob_secret.clone()]; let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.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(), ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring, &decrypt_keyring,
&sig_check_keyring, &sig_check_keyring,
) )
.unwrap(); .unwrap();
assert_eq!(plain, CLEARTEXT); assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 1); assert_eq!(valid_signatures.len(), 1);
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_no_sig_check() { async fn test_decrypt_no_sig_check() {
let keyring = vec![KEYS.alice_secret.clone()]; let keyring = vec![KEYS.alice_secret.clone()];
let (plain, valid_signatures) = let (msg, valid_signatures) =
pk_decrypt(ctext_signed().await.as_bytes().to_vec(), &keyring, &[]).unwrap(); pk_decrypt_and_validate(ctext_signed().await.as_bytes().to_vec(), &keyring, &[])
assert_eq!(plain, CLEARTEXT); .unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 0); assert_eq!(valid_signatures.len(), 0);
} }
@@ -570,26 +583,26 @@ mod tests {
// The validation does not have the public key of the signer. // The validation does not have the public key of the signer.
let decrypt_keyring = vec![KEYS.bob_secret.clone()]; let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let sig_check_keyring = vec![KEYS.bob_public.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(), ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring, &decrypt_keyring,
&sig_check_keyring, &sig_check_keyring,
) )
.unwrap(); .unwrap();
assert_eq!(plain, CLEARTEXT); assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 0); assert_eq!(valid_signatures.len(), 0);
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_unsigned() { async fn test_decrypt_unsigned() {
let decrypt_keyring = vec![KEYS.bob_secret.clone()]; 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(), ctext_unsigned().await.as_bytes().to_vec(),
&decrypt_keyring, &decrypt_keyring,
&[], &[],
) )
.unwrap(); .unwrap();
assert_eq!(plain, CLEARTEXT); assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 0); assert_eq!(valid_signatures.len(), 0);
} }
} }

View File

@@ -201,7 +201,7 @@ pub(crate) async fn receive_imf_inner(
}; };
crate::peerstate::maybe_do_aeap_transition(context, &mut mime_parser).await?; 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 peerstate
.handle_fingerprint_change(context, mime_parser.timestamp_sent) .handle_fingerprint_change(context, mime_parser.timestamp_sent)
.await?; .await?;
@@ -356,8 +356,7 @@ pub(crate) async fn receive_imf_inner(
// Peerstate could be updated by handling the Securejoin handshake. // Peerstate could be updated by handling the Securejoin handshake.
let contact = Contact::get_by_id(context, from_id).await?; let contact = Contact::get_by_id(context, from_id).await?;
mime_parser.decryption_info.peerstate = mime_parser.peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
Peerstate::from_addr(context, contact.get_addr()).await?;
} else { } else {
let to_id = to_ids.first().copied().unwrap_or_default(); let to_id = to_ids.first().copied().unwrap_or_default();
// handshake may mark contacts as verified and must be processed before chats are created // 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 if verified_encryption == VerifiedEncryption::Verified
&& mime_parser.get_header(HeaderDef::ChatVerified).is_some() && 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 // NOTE: it might be better to remember ID of the key
// that we used to decrypt the message, but // that we used to decrypt the message, but
// it is unlikely that default key ever changes // it is unlikely that default key ever changes
@@ -1006,7 +1005,7 @@ async fn add_parts(
) )
.await?; .await?;
} }
if let Some(peerstate) = &mime_parser.decryption_info.peerstate { if let Some(peerstate) = &mime_parser.peerstate {
restore_protection = new_protection != ProtectionStatus::Protected restore_protection = new_protection != ProtectionStatus::Protected
&& peerstate.prefer_encrypt == EncryptPreference::Mutual && peerstate.prefer_encrypt == EncryptPreference::Mutual
// Check that the contact still has the Autocrypt key same as the // Check that the contact still has the Autocrypt key same as the
@@ -2662,7 +2661,7 @@ async fn update_verified_keys(
return Ok(None); 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. // No peerstate means no verified keys.
return Ok(None); return Ok(None);
}; };
@@ -2735,7 +2734,7 @@ async fn has_verified_encryption(
// this check is skipped for SELF as there is no proper SELF-peerstate // this check is skipped for SELF as there is no proper SELF-peerstate
// and results in group-splits otherwise. // and results in group-splits otherwise.
if from_id != ContactId::SELF { if from_id != ContactId::SELF {
let Some(peerstate) = &mimeparser.decryption_info.peerstate else { let Some(peerstate) = &mimeparser.peerstate else {
return Ok(NotVerified( return Ok(NotVerified(
"No peerstate, the contact isn't verified".to_string(), "No peerstate, the contact isn't verified".to_string(),
)); ));