Skip to content

PGP contacts #6796

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 236 commits into
base: main
Choose a base branch
from
Open

PGP contacts #6796

wants to merge 236 commits into from

Conversation

link2xt
Copy link
Collaborator

@link2xt link2xt commented Apr 12, 2025

JSON-RPC: reset_contact_encryption API is removed.
In deltachat-rpc-client API Contact.reset_encryption is removed.
"Group tracking plugin" in legacy Python API was removed because it relied on parsing email addresses from system messages with regexps.

Known multi-device (non-)issue: pinning a 1:1 chat from legacy client pins email chat via sync message.

Still missing, not all of this should be fixed before merging but at least CI should pass:

  • Return an error when API user tries to add email contact to encrypted chats.
  • Same for PGP-contacts, they should not be added to unencrypted chats.
  • Fix AEAP tests.
  • Chat-Group-Member-Fpr header.
  • Ignore Chat-Group-ID if the message is not encrypted+signed.
  • Complete Thunderbird tests.
  • Tests for 1:1 chat assignment. If we have multiple PGP-contacts with the same email address, can assign outgoing messages without Autocrypt-Gossip to the most recent one or using In-Reply-To or References. If there are no PGP-contacts, the message can go to email contact or trash. EDIT: we have tests for incomplete message assignment, they pass. This is not comprehensive and does not use Message-IDs and References, but tying chat assignment to contact lookup is complicated.
  • Migration. Started at [WIP] feat: Migration for PGP-contacts #6818
  • Do not list email-contacts in the contact list by default. For email contacts maybe add a flag.
  • Open issues for Thunderbird: test_prefer_encrypt_mutual_if_encrypted has TODOs for assigning to chat by issuer fingerprint. This should also work if the key is attached and no Autocrypt header is sent. Signed-only messages should probably go to PGP-chat without a padlock. Fixing this is out of scope of this PR. Signed-only messages to to email-contact. If Thunderbird is configured to send Autocrypt header, chatting should work, this is already tested.
  • SHA-256 fingerprints (API to get SHA2-256 fingerprints for any keys rpgp/rpgp#531, wip: "imprint" fn rpgp/rpgp#541). If we can, we should use SHA-256 fingerprints for v4 keys instead of having SHA-1 fingerprints as the primary key. Invite links with SHA-1 should still be supported. Adding a column can be done later and we will likely need two columns anyway with standard fingerprint and SHA-256 to support openpgp4fpr and invite links, but Chat-Group-Member-Fpr format should be fixed before release.

@link2xt link2xt mentioned this pull request Apr 12, 2025
@link2xt link2xt changed the title Link2xt/pgp contacts WIP: PGP contacts Apr 12, 2025
@link2xt link2xt force-pushed the link2xt/pgp-contacts branch 2 times, most recently from 7acd920 to 280e5dd Compare April 13, 2025 05:49
@link2xt link2xt force-pushed the link2xt/pgp-contacts branch from 7e5dab4 to 479879a Compare April 14, 2025 19:13
Copy link
Collaborator

@Hocuri Hocuri left a comment

Choose a reason for hiding this comment

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

Just 3 small things I was wondering while skimming over

/// This first creates a contact, but does not import the key,
/// so may create a PGP-contact with a fingerprint
/// but without the key.
pub async fn get_pgp_chat(&self, other: &TestContext) -> Chat {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since PGP chats are the more common type of chat now, I think it would make sense to call this function get_chat() and the other one get_email_chat() or get_unencrypted_chat() or similar. But it's not that important, e.g. if this would mean that a lot of test have to be changed.

@link2xt link2xt force-pushed the link2xt/pgp-contacts branch 2 times, most recently from ebe2a09 to cac8d20 Compare April 16, 2025 16:13
@adbenitez
Copy link
Collaborator

a way to know if a chat is an encrypted chat is missing, currently there is only "isPgpContact" that can be used to mark contacts/1:1 chat but for unencrypted groups/threads of classic email non-pgp contacts there is no API to recognize such chats and also put the "classic email" marker as for classic email contacts

@link2xt link2xt force-pushed the link2xt/pgp-contacts branch 2 times, most recently from 625f8f4 to c87c241 Compare April 21, 2025 21:24
@@ -187,6 +191,7 @@ impl BasicChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(context).await?,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This adds up to 2 additional database calls to loading BasicChat, though I can't think of a good way to avoid it, so maybe it's fine. Or maybe it should only be added to FullChat, idk.

) -> Result<(ContactId, Modifier)> {
Self::add_or_lookup_ex(context, name, addr, "", origin).await
}

/// Lookup a contact and create it if it does not exist yet.
/// The contact is identified by the email-address, a name and an "origin" can be given.
Copy link
Collaborator

Choose a reason for hiding this comment

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

The comment here should explain how fingerprint & addr parameters relate; the way I understand the code:

/// If `fingerprint` is passed, a PGP-contact with this fingerprint added / looked up.
/// Otherwise, an email-contact with `addr` is added / looked up.
/// A name and an "origin" can be given.

Comment on lines +850 to +858
if !fingerprint.is_empty() {
let fingerprint_self = load_self_public_key(context).await?.dc_fingerprint().hex();
if fingerprint == fingerprint_self {
return Ok((ContactId::SELF, sth_modified));
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Loading the self fingerprint everytime a contact is looked up, just to check whether it's ContactId::SELF, seems unnecessarily slow to me.

I wonder if we can simplify & speed up things by not using the kepairs table anymore, and instead storing the private and public key in configs. This would mean that it's not possible to have multiple self keys anymore, but I'm not sure there is anyone who is actually making use of the fact that DC theoretically supports using multiple self keys. It would also mean that we have to store the key as base64, not sure about the consequences there. An alternative would be to store the self fingerprint in a OnceCell, since we don't allow changing the primary key anyway.

But we can open an issue for this, and do it in a follow-up PR.

Comment on lines -713 to +801
to_ids: &[ContactId],
past_ids: &[ContactId],
to_ids: &[Option<ContactId>],
past_ids: &[Option<ContactId>],
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would be good to put a comment what a None element means.

@link2xt link2xt force-pushed the link2xt/pgp-contacts branch 2 times, most recently from 1cff5d4 to 907628e Compare May 2, 2025 17:13
@link2xt link2xt marked this pull request as ready for review May 2, 2025 17:53
Hocuri and others added 6 commits May 6, 2025 19:22
…ent addr

If a contact did AEAP in the past, then it can happen that there are multiple contacts with the same email address but different keys.
In this case, we should make sure that we don't send messages to a different email address all of a sudden,
and that there aren't two 1:1 chats with the same contact.

Therefore, if the PGP contact has a different email address than the
email contact, we keep the email contact in 1:1 chats.
@link2xt link2xt force-pushed the link2xt/pgp-contacts branch from dbade62 to b7284c0 Compare May 6, 2025 22:43
@link2xt link2xt changed the title WIP: PGP contacts PGP contacts May 7, 2025
Copy link
Collaborator

@Hocuri Hocuri left a comment

Choose a reason for hiding this comment

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

Another day, another set of review comments

@@ -474,7 +397,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
ac2_offl.start_io()
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
assert not msg_in.is_system_message()
assert msg_in.text.startswith("[The message was sent with non-verified encryption")
assert msg_in.text.startswith("[The message was sent by non-verified contact")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just a question: IIRC plan is to show the verified icon iff all group members are verified? In this case, we won't need this message anymore. But this can also come in a subsequent PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, I would like to get rid of this message, but this is out of scope for this PR which is already large.

Comment on lines +337 to +338
let to_ids: Vec<Option<ContactId>>;
let past_ids: Vec<Option<ContactId>>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

That's a lot of code to calculate the to and past ids, would be nice to extract a function here:

async fn get_to_and_past_contacts(
    context: &Context,
    mime_parser: &MimeMessage,
    incoming_origin: Origin,
) -> Result<(Vec<Option<ContactId>>, Vec<Option<ContactId>>)> {
    let chat_id = if let Some(grpid) = mime_parser.get_chat_group_id() {
    ...
        past_ids = add_or_lookup_contacts_by_address_list(
            context,
            &mime_parser.past_members,
            Origin::Hidden,
        )
        .await?;
    };
    Ok((to_ids, past_ids))
}
    let (to_ids, past_ids) =
        get_to_and_past_contacts(context, &mime_parser, incoming_origin).await?;

Comment on lines +702 to +706
/// * `is_partial_download`: the message is partially downloaded.
/// We only know the email address and not the contact fingerprint,
/// but try to assign the message to some PGP-contact.
/// If we get it wrong, the message will be placed into the correct
/// chat after downloading.
Copy link
Collaborator

Choose a reason for hiding this comment

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

What about unencrypted emails that are partially downloaded? They should be assigned to the email contact, not to some PGP contact with this email address. Maybe there is some code for this already and I just haven't seen it, otherwise we need a check for whether Content-Type is multipart/encrypted.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We don't know if partially downloaded email is encrypted or not, we don't even prefetch content-type. Messages are assigned to some PGP-contact. After downloading, they will go to email-contact.

Copy link
Collaborator

Choose a reason for hiding this comment

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

But this is not about prefetch but about partial downloads? And for partial downloads, we fetch the full headers, which includes Content-Type:

const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";

Co-authored-by: Hocuri <hocuri@gmx.de>
@link2xt
Copy link
Collaborator Author

link2xt commented May 7, 2025

There is a problem with 1:1 chat assignment. If there is no Autocrypt-Gossip on outgoing message, then to_id gets converted to email contact. If there is also a Chat-Verified header, receive_imf fails with an error like DeltaChat: [accId=1] src/imap.rs:1456: receive_imf error: Non-PGP contact Contact#10 cannot be verified.. and the message does not show up at all. So we should assign by References or In-Reply-To and 1:1 chat before converting to_ids.

@@ -2397,27 +2486,32 @@ 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?;
removed_id = lookup_pgp_contact_by_address(context, removed_addr, chat_id).await?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Consider the following case:

  • Alice's current key is member of a group. Her past key (on the same email address) is a past member of the group, since she reinstalled DC once without restoring a backup.
  • Bob removes Alice from the group.

This code looks like that might not work reliably? Because lookup_pgp_contact_by_address() also looks up past contacts, so that if Bob is unlucky, then this will try to remove Alice's old key again, which won't work of course, because Alice's old key already is a past member.

Or will old-Alice-key be removed past-members as soon as new-Alice-key is added, and everything is fine?

Comment on lines +5008 to +5010
SyncAction::Block => {
return contact::set_blocked(self, Nosync, contact_id, true).await
}
Copy link
Collaborator

@Hocuri Hocuri May 8, 2025

Choose a reason for hiding this comment

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

Just a non-actionable remark: It's a bit weird that this calls contact::set_blocked(.., true), and then a few lines below, chat_id.block_ex() is called, which again calls contact::set_blocked(.., true). But how (un)blocking a chat and (un)blocking a contact interacts is just super weird anyways. Not sure if anyone really uses the "block" feature, a spammer can just create a new account anyways, and if you want a chat to be out of your view you can archive&mute it. So, maybe it's fine that this feature is not too "polished".

Doing nothing here, and relying on chat_id.block.ex() to also block the contact, would be even weirder, so, I'm not proposing to change anything here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants