Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 55 additions & 75 deletions src/mimefactory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use deltachat_contact_tools::sanitize_bidi_characters;
use iroh_gossip::proto::TopicId;
use mail_builder::headers::HeaderType;
use mail_builder::headers::address::Address;
use mail_builder::headers::raw::Raw;
use mail_builder::headers::text::Text;
use mail_builder::mime::MimePart;
use tokio::fs;

Expand All @@ -32,7 +34,7 @@ use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::peer_channels::{create_iroh_header, get_iroh_topic_for_msg};
use crate::pgp::{SeipdVersion, addresses_from_public_key, pubkey_supports_seipdv2};
use crate::pgp::{SeipdVersion, addresses_from_public_key, pubkey_supports_seipdv2, relay_addrs};
use crate::simplify::escape_message_footer_marks;
use crate::stock_str;
use crate::tools::{IsNoneOrEmpty, create_outgoing_rfc724_mid, remove_subject_prefix, time};
Expand Down Expand Up @@ -405,6 +407,8 @@ pub(crate) fn render_queued_mail(
}
}

let sign_key = if should_sign { Some(secret_key) } else { None };

let message = match encryption {
Encryption::No => raw_message,
Encryption::Asymmetric { encryption_pubkeys } => {
Expand Down Expand Up @@ -432,7 +436,7 @@ pub(crate) fn render_queued_mail(
let encrypted = crate::pgp::pk_encrypt(
full_raw_message,
encryption_keyring,
secret_key.clone(),
sign_key,
should_compress,
seipd_version,
)?;
Expand All @@ -444,12 +448,6 @@ pub(crate) fn render_queued_mail(
let mut full_raw_message = inner_headers.clone();
full_raw_message.extend(raw_message);

let sign_key = if should_sign {
Some(secret_key.clone())
} else {
None
};

let encrypted = crate::pgp::symm_encrypt_message(
full_raw_message,
sign_key,
Expand Down Expand Up @@ -578,9 +576,7 @@ impl MimeFactory {

let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;

let relays =
addresses_from_public_key(&public_key).unwrap_or_else(|| vec![addr.clone()]);
recipients.extend(relays);
recipients.extend(relay_addrs(&public_key, &addr));
to.push((authname, addr.clone()));

Encryption::Asymmetric {
Expand Down Expand Up @@ -897,7 +893,7 @@ impl MimeFactory {
}
} else if contact.is_key_contact() {
let encryption_pubkeys = if let Some(key) = contact.public_key(context).await? {
recipients = addresses_from_public_key(&key).unwrap_or_else(|| vec![addr.clone()]);
recipients = relay_addrs(&key, &addr);
vec![(addr.clone(), key)]
} else {
Vec::new()
Expand Down Expand Up @@ -2439,6 +2435,42 @@ fn b_encode(value: &str) -> String {
)
}

/// Returns the headers to place into the encrypted part
/// of messages that are not part of a chat.
async fn non_chat_protected_headers(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All headers are protected in some way except for Chat-Is-Post-Message, so not sure what protected means here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed the comment.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the function should be renamed too, then? To non_chat_headers or headers_for_non_chat_msg?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually meant the function name, it is still _protected_. It's fine, i just don't know why it has _protected_ in the name.

context: &Context,
subject: &str,
) -> Result<Vec<(&'static str, HeaderType<'static>)>> {
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(time(), 0)
.unwrap()
.to_rfc2822();
let mut headers = vec![
("To", Address::new_list(vec![hidden_recipients()]).into()),
Comment thread
link2xt marked this conversation as resolved.
("Date", Raw::new(date).into()),
("Subject", Text::new(subject.to_string()).into()),
];
// Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
if context.get_config_bool(Config::Bot).await? {
headers.push(("Auto-Submitted", Raw::new("auto-generated").into()));
}
Ok(headers)
}

/// Renders `queued_mail` for SMTP with the own key pair and primary address.
async fn render_with_self_key(context: &Context, queued_mail: QueuedMail) -> Result<String> {
let public_key = key::load_self_public_key(context).await?;
let secret_key = key::load_self_secret_key(context).await?;
let from_addr = context.get_primary_self_addr().await?;
let rendered_mail = render_queued_mail(
queued_mail,
&public_key,
&secret_key,
from_addr,
RenderSideEffects::default(),
)?;
Ok(rendered_mail.message)
}

pub(crate) async fn render_symm_encrypted_securejoin_message(
context: &Context,
step: &str,
Expand All @@ -2451,81 +2483,29 @@ pub(crate) async fn render_symm_encrypted_securejoin_message(

let message: MimePart<'static> = MimePart::new("text/plain", "Secure-Join");

let mut headers = Vec::<(&'static str, HeaderType<'static>)>::new();

let to: Vec<Address<'static>> = vec![hidden_recipients()];
headers.push((
"To",
mail_builder::headers::address::Address::new_list(to.clone()).into(),
));

let timestamp = time();
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(timestamp, 0)
.unwrap()
.to_rfc2822();
headers.push(("Date", mail_builder::headers::raw::Raw::new(date).into()));

headers.push((
"Subject",
mail_builder::headers::text::Text::new("Secure-Join".to_string()).into(),
));

// Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
if context.get_config_bool(Config::Bot).await? {
headers.push((
"Auto-Submitted",
mail_builder::headers::raw::Raw::new("auto-generated".to_string()).into(),
));
}

headers.push((
"Secure-Join",
mail_builder::headers::raw::Raw::new(step.to_string()).into(),
));

headers.push((
"Secure-Join-Auth",
mail_builder::headers::text::Text::new(auth.to_string()).into(),
));
let mut headers = non_chat_protected_headers(context, "Secure-Join").await?;
headers.push(("Secure-Join", Raw::new(step.to_string()).into()));
headers.push(("Secure-Join-Auth", Text::new(auth.to_string()).into()));

let message = add_headers_to_encrypted_part(message, headers);

// Disable compression for SecureJoin to ensure
// there are no compression side channels
// leaking information about the tokens.
let should_compress = false;

// Only sign the message if we attach the pubkey.
let should_sign = should_attach_pubkey;

let raw_message = part_to_bytes(message);

let queued_mail = QueuedMail {
raw_message,
raw_message: part_to_bytes(message),
display_name: String::new(),
rfc724_mid: rfc724_mid.to_string(),
encryption: Encryption::Symmetric {
shared_secret: shared_secret.to_string(),
},
should_attach_pubkey,
should_sign,
should_compress,
// Only sign the message if we attach the pubkey.
should_sign: should_attach_pubkey,
// Disable compression for SecureJoin to ensure
// there are no compression side channels
// leaking information about the tokens.
should_compress: false,
};

let public_key = key::load_self_public_key(context).await?;
let secret_key = key::load_self_secret_key(context).await?;
let side_effects = RenderSideEffects::default();

let from_addr = context.get_primary_self_addr().await?;
let rendered_mail = render_queued_mail(
queued_mail,
&public_key,
&secret_key,
from_addr,
side_effects,
)?;

Ok(rendered_mail.message)
render_with_self_key(context, queued_mail).await
}

/// Renders MIME part into a vector of bytes.
Expand Down
2 changes: 1 addition & 1 deletion src/mimeparser/shared_secret_decryption_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async fn test_shared_secret_decryption_ext(

let encrypted_msg = pgp::symm_encrypt_message(
plain_text.as_bytes().to_vec(),
signer_key,
signer_key.as_ref(),
secret_for_encryption.to_string(),
true,
)?;
Expand Down
113 changes: 69 additions & 44 deletions src/pgp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,46 @@ pub enum SeipdVersion {
V2,
}

/// Encrypts `plain` text using `public_keys_for_encryption`
/// and signs it using `private_key_for_signing`.
/// Returns the subpackets for a signature over a message
/// encrypted to `public_keys_for_encryption`.
#[expect(clippy::arithmetic_side_effects)]
fn signature_subpackets(
private_key_for_signing: &SignedSecretKey,
public_keys_for_encryption: &[SignedPublicKey],
) -> Result<SubpacketConfig> {
let mut hashed = Vec::with_capacity(1 + public_keys_for_encryption.len() + 1);
hashed.push(Subpacket::critical(SubpacketData::SignatureCreationTime(
pgp::types::Timestamp::now(),
))?);
for key in public_keys_for_encryption {
let data = SubpacketData::IntendedRecipientFingerprint(key.fingerprint());
let subpkt = match private_key_for_signing.version() < KeyVersion::V6 {
true => Subpacket::regular(data)?,
false => Subpacket::critical(data)?,
};
hashed.push(subpkt);
}
hashed.push(Subpacket::regular(SubpacketData::IssuerFingerprint(
private_key_for_signing.fingerprint(),
))?);
let mut unhashed = vec![];
if private_key_for_signing.version() <= KeyVersion::V4 {
unhashed.push(Subpacket::regular(SubpacketData::IssuerKeyId(
private_key_for_signing.legacy_key_id(),
))?);
}
Ok(SubpacketConfig::UserDefined { hashed, unhashed })
}

/// Encrypts `plain` text using `public_keys_for_encryption`,
/// signing it with `private_key_for_signing` if there is one.
///
/// An unsigned message carries no intended recipient fingerprints,
/// so its recipients do not learn who else received it.
pub fn pk_encrypt(
plain: Vec<u8>,
public_keys_for_encryption: Vec<SignedPublicKey>,
private_key_for_signing: SignedSecretKey,
private_key_for_signing: Option<&SignedSecretKey>,
compress: bool,
seipd_version: SeipdVersion,
) -> Result<String> {
Expand All @@ -122,30 +155,6 @@ pub fn pk_encrypt(
let pkeys = public_keys_for_encryption
.iter()
.filter_map(select_pk_for_encryption);
let subpkts = {
let mut hashed = Vec::with_capacity(1 + public_keys_for_encryption.len() + 1);
hashed.push(Subpacket::critical(SubpacketData::SignatureCreationTime(
pgp::types::Timestamp::now(),
))?);
for key in &public_keys_for_encryption {
let data = SubpacketData::IntendedRecipientFingerprint(key.fingerprint());
let subpkt = match private_key_for_signing.version() < KeyVersion::V6 {
true => Subpacket::regular(data)?,
false => Subpacket::critical(data)?,
};
hashed.push(subpkt);
}
hashed.push(Subpacket::regular(SubpacketData::IssuerFingerprint(
private_key_for_signing.fingerprint(),
))?);
let mut unhashed = vec![];
if private_key_for_signing.version() <= KeyVersion::V4 {
unhashed.push(Subpacket::regular(SubpacketData::IssuerKeyId(
private_key_for_signing.legacy_key_id(),
))?);
}
SubpacketConfig::UserDefined { hashed, unhashed }
};

let msg = MessageBuilder::from_bytes("", plain);
let encoded_msg = match seipd_version {
Expand All @@ -156,13 +165,16 @@ pub fn pk_encrypt(
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
}

let hash_algorithm = private_key_for_signing.hash_alg();
msg.sign_with_subpackets(
&*private_key_for_signing,
Password::empty(),
hash_algorithm,
subpkts,
);
if let Some(secret_key) = private_key_for_signing {
let subpkts = signature_subpackets(secret_key, &public_keys_for_encryption)?;
let hash_algorithm = secret_key.hash_alg();
msg.sign_with_subpackets(
&**secret_key,
Password::empty(),
hash_algorithm,
subpkts,
);
}
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
Expand All @@ -181,13 +193,16 @@ pub fn pk_encrypt(
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
}

let hash_algorithm = private_key_for_signing.hash_alg();
msg.sign_with_subpackets(
&*private_key_for_signing,
Password::empty(),
hash_algorithm,
subpkts,
);
if let Some(secret_key) = private_key_for_signing {
let subpkts = signature_subpackets(secret_key, &public_keys_for_encryption)?;
let hash_algorithm = secret_key.hash_alg();
msg.sign_with_subpackets(
&**secret_key,
Password::empty(),
hash_algorithm,
subpkts,
);
}
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
Expand Down Expand Up @@ -254,7 +269,7 @@ pub fn pk_validate(
/// `shared secret` is the secret that will be used for symmetric encryption.
pub fn symm_encrypt_message(
plain: Vec<u8>,
private_key_for_signing: Option<SignedSecretKey>,
private_key_for_signing: Option<&SignedSecretKey>,
shared_secret: String,
compress: bool,
) -> Result<String> {
Expand All @@ -277,9 +292,13 @@ pub fn symm_encrypt_message(
);
msg.encrypt_with_password(&mut rng, s2k, &shared_secret)?;

if let Some(private_key_for_signing) = private_key_for_signing.as_deref() {
if let Some(private_key_for_signing) = private_key_for_signing {
let hash_algorithm = private_key_for_signing.hash_alg();
msg.sign(private_key_for_signing, Password::empty(), hash_algorithm);
msg.sign(
&**private_key_for_signing,
Password::empty(),
hash_algorithm,
);
}
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
Expand Down Expand Up @@ -452,6 +471,12 @@ pub(crate) fn addresses_from_public_key(public_key: &SignedPublicKey) -> Option<
None
}

/// Returns the addresses to reach the owner of `public_key`,
/// falling back to `addr` if the key carries no relay list.
pub(crate) fn relay_addrs(public_key: &SignedPublicKey, addr: &str) -> Vec<String> {
addresses_from_public_key(public_key).unwrap_or_else(|| vec![addr.to_string()])
Comment thread
link2xt marked this conversation as resolved.
Comment thread
link2xt marked this conversation as resolved.
}

/// Returns true if public key advertises SEIPDv2 feature.
pub(crate) fn pubkey_supports_seipdv2(public_key: &SignedPublicKey) -> bool {
// If any Direct Key Signature or any User ID signature has SEIPDv2 feature,
Expand Down
Loading