Compare commits

...

1 Commits

Author SHA1 Message Date
iequidoo
6e6b202c85 fix: Add encrypted ad-hoc groups
Allow encrypted ad-hoc groups in addition to unencrypted:
- Create an encrypted ad-hoc group from an encrypted multi-participant non-chat message. It's not
  completely secure because we don't know the exact keys the message is encrypted to and we use an
  heuristic based on `contacts.last_ssen`, but it's more secure than creating an unencrypted group
  and adds an interoperable group encryption in some way.
- Assign encrypted non-chat messages to groups with matching name and members. This is useful when
  e.g. the user was removed from a group and the re-added by a non-chat message; in this case we
  can't rely on assigning by References. Another case is reordered messages. This is safe because
  such messages don't modify the group members, so the next outgoing message is encrypted to the
  same keys.

This doesn't change the existing ad-hoc groups db migration because probably it's too late. No tests
are broken, `create_group_ex()` preserves behavior and encryption status never changes once the
group is created, so UIs should continue to work.

Known issues:
- UI's "QR Invite Code" button doesn't work, an error explaining the reason is shown. It can't work
  w/o a Group Id. But it's better than just hiding the button w/o any explanation.
- When adding/removing members, non-chat contacts receive weird system messages. OTOH currently
  that's not possible at all, so it's not a regression and the problem exists anyway for Delta Chat
  groups with non-chat contacts.
2025-08-16 09:31:29 -03:00
2 changed files with 59 additions and 33 deletions

View File

@@ -1885,10 +1885,11 @@ impl Chat {
pub async fn is_encrypted(&self, context: &Context) -> Result<bool> {
let is_encrypted = self.is_protected()
|| match self.typ {
Chattype::Single => {
match context
Chattype::Group if !self.grpid.is_empty() => true,
Chattype::Group | Chattype::Single => {
let contacts = context
.sql
.query_row_optional(
.query_map(
"SELECT cc.contact_id, c.fingerprint<>''
FROM chats_contacts cc LEFT JOIN contacts c
ON c.id=cc.contact_id
@@ -1900,16 +1901,13 @@ impl Chat {
let is_key: bool = row.get(1)?;
Ok((id, is_key))
},
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?
{
Some((id, is_key)) => is_key || id == ContactId::DEVICE,
None => true,
}
}
Chattype::Group => {
// Do not encrypt ad-hoc groups.
!self.grpid.is_empty()
.await?;
(self.typ != Chattype::Group || contacts.iter().any(|&(_, is_key)| is_key))
&& contacts.iter().all(|&(id, is_key)| {
is_key || matches!(id, ContactId::SELF | ContactId::DEVICE)
})
}
Chattype::Mailinglist => false,
Chattype::OutBroadcast | Chattype::InBroadcast => true,

View File

@@ -114,7 +114,8 @@ enum ChatAssignment {
/// Group chat without a Group ID.
///
/// This is not encrypted.
/// This is either encrypted or unencrypted. Encryption never downgrades, but it can upgrade if
/// the last address-contact is removed from the group.
AdHocGroup,
/// Assign the message to existing chat
@@ -372,25 +373,52 @@ async fn get_to_and_past_contact_ids(
}
}
ChatAssignment::AdHocGroup => {
to_ids = add_or_lookup_contacts_by_address_list(
context,
&mime_parser.recipients,
if !mime_parser.incoming {
Origin::OutgoingTo
} else if incoming_origin.is_known() {
Origin::IncomingTo
} else {
Origin::IncomingUnknownTo
},
)
.await?;
let key_to_ids = match mime_parser.was_encrypted() {
false => Vec::new(),
true => {
let ids = lookup_key_contacts_by_address_list(
context,
&mime_parser.recipients,
to_member_fingerprints,
None,
)
.await?;
match ids.contains(&None) {
false => ids,
true => Vec::new(),
}
}
};
if !key_to_ids.is_empty() {
to_ids = key_to_ids;
past_ids = lookup_key_contacts_by_address_list(
context,
&mime_parser.past_members,
past_member_fingerprints,
None,
)
.await?;
} else {
to_ids = add_or_lookup_contacts_by_address_list(
context,
&mime_parser.recipients,
if !mime_parser.incoming {
Origin::OutgoingTo
} else if incoming_origin.is_known() {
Origin::IncomingTo
} else {
Origin::IncomingUnknownTo
},
)
.await?;
past_ids = add_or_lookup_contacts_by_address_list(
context,
&mime_parser.past_members,
Origin::Hidden,
)
.await?;
past_ids = add_or_lookup_contacts_by_address_list(
context,
&mime_parser.past_members,
Origin::Hidden,
)
.await?;
}
}
ChatAssignment::OneOneChat => {
let pgp_to_ids = add_or_lookup_key_contacts(
@@ -2535,7 +2563,7 @@ async fn lookup_or_create_adhoc_group(
.query_row(
"SELECT c.id, c.blocked
FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
WHERE m.hidden=0 AND c.grpid='' AND c.name=?
WHERE m.hidden=0 AND (? OR c.grpid='') AND c.name=?
AND (SELECT COUNT(*) FROM chats_contacts
WHERE chat_id=c.id
AND add_timestamp >= remove_timestamp)=?
@@ -2544,7 +2572,7 @@ async fn lookup_or_create_adhoc_group(
AND contact_id NOT IN (SELECT id FROM temp.contacts)
AND add_timestamp >= remove_timestamp)=0
ORDER BY m.timestamp DESC",
(&grpname, contact_ids.len()),
(mime_parser.was_encrypted(), &grpname, contact_ids.len()),
|row| {
let id: ChatId = row.get(0)?;
let blocked: Blocked = row.get(1)?;