From 0562e23ee0fffe67b4187669c3e3938fb9ff667a Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 8 Sep 2023 07:00:18 +0000 Subject: [PATCH 01/12] chore(cargo): bump webpki from 0.22.0 to 0.22.1 --- Cargo.lock | 4 ++-- deny.toml | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e9b02504..d94847778 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5448,9 +5448,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e" dependencies = [ "ring", "untrusted", diff --git a/deny.toml b/deny.toml index ffc43ebb1..d475bbbdb 100644 --- a/deny.toml +++ b/deny.toml @@ -3,11 +3,6 @@ unmaintained = "allow" ignore = [ "RUSTSEC-2020-0071", "RUSTSEC-2022-0093", - - # Exponential CPU time usage for TLS certificate processing in webpki. - # It is only used for backup transfer, so does not affect IMAP and SMTP connections. - # Waiting for `iroh` to update dependencies. - "RUSTSEC-2023-0052", ] [bans] From 2939de013b8821350c8422a9d26050be092da2b0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 8 Sep 2023 17:54:05 +0000 Subject: [PATCH 02/12] api(jsonrpc): return only chat IDs for similar chats This is already the way `get_chatlist_entries` works. `get_similar_chatlist_entries` is renamed into `get_similar_chat_ids` because return values are not entries anymore. --- deltachat-jsonrpc/src/api/mod.rs | 25 ++++++++------------ deltachat-jsonrpc/src/api/types/chat_list.rs | 5 +--- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index beb5a3c7e..0b9816390 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -39,7 +39,6 @@ pub mod types; use num_traits::FromPrimitive; use types::account::Account; use types::chat::FullChat; -use types::chat_list::ChatListEntry; use types::contact::ContactObject; use types::events::Event; use types::http::HttpResponse; @@ -568,22 +567,18 @@ impl CommandApi { } /// Returns chats similar to the given one. - async fn get_similar_chatlist_entries( - &self, - account_id: u32, - chat_id: u32, - ) -> Result> { + /// + /// Experimental API, subject to change without notice. + async fn get_similar_chat_ids(&self, account_id: u32, chat_id: u32) -> Result> { let ctx = self.get_context(account_id).await?; let chat_id = ChatId::new(chat_id); - let list = chat_id.get_similar_chatlist(&ctx).await?; - let mut l: Vec = Vec::with_capacity(list.len()); - for i in 0..list.len() { - l.push(ChatListEntry( - list.get_chat_id(i)?.to_u32(), - list.get_msg_id(i)?.unwrap_or_default().to_u32(), - )); - } - Ok(l) + let list = chat_id + .get_similar_chat_ids(&ctx) + .await? + .into_iter() + .map(|(chat_id, _metric)| chat_id.to_u32()) + .collect(); + Ok(list) } async fn get_chatlist_items_by_entries( diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs index 2c15165d9..fac1fb039 100644 --- a/deltachat-jsonrpc/src/api/types/chat_list.rs +++ b/deltachat-jsonrpc/src/api/types/chat_list.rs @@ -8,15 +8,12 @@ use deltachat::{ chatlist::Chatlist, }; use num_traits::cast::ToPrimitive; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use typescript_type_def::TypeDef; use super::color_int_to_hex_string; use super::message::MessageViewtype; -#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)] -pub struct ChatListEntry(pub u32, pub u32); - #[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(tag = "type")] pub enum ChatListItemFetchResult { From 212fbc125cb6fb100d31a9b66442370171daab48 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Tue, 5 Sep 2023 21:38:41 -0300 Subject: [PATCH 03/12] fix: ChatId::parent_query(): Don't filter out OutPending and OutFailed messages The new message for which `parent_query()` is done may assume that it will be received in a context affected by those messages, e.g. they could add new members to a group and the new message will contain them in "To:". Anyway recipients must be prepared to orphaned references. --- src/chat.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 9baa7fdb1..306985cbd 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1040,7 +1040,7 @@ impl ChatId { let sql = &context.sql; let query = format!( "SELECT {fields} \ - FROM msgs WHERE chat_id=? AND state NOT IN (?, ?, ?, ?) AND NOT hidden \ + FROM msgs WHERE chat_id=? AND state NOT IN (?, ?) AND NOT hidden \ ORDER BY timestamp DESC, id DESC \ LIMIT 1;" ); @@ -1051,8 +1051,11 @@ impl ChatId { self, MessageState::OutPreparing, MessageState::OutDraft, - MessageState::OutPending, - MessageState::OutFailed, + // We don't filter `OutPending` and `OutFailed` messages because the new message + // for which `parent_query()` is done may assume that it will be received in a + // context affected by those messages, e.g. they could add new members to a + // group and the new message will contain them in "To:". Anyway recipients must + // be prepared to orphaned references. ), f, ) From e12e026bd80ea7d468e35f200011a65afc75f3ad Mon Sep 17 00:00:00 2001 From: iequidoo Date: Thu, 31 Aug 2023 16:04:34 -0300 Subject: [PATCH 04/12] fix: Switch to original Hocuri's group membership consistency algo (#3782)(#4624) - If we don't know the parent (=In-Reply-To) message, then completely recreate the group member list (i.e. use the member list of the incoming message) (because we assume that we missed some messages & have a wrong group state). - If the message has a "Chat-Group-Member-Removed: member@example.com" header, then remove this member. - If the message has a "Chat-Group-Member-Added: member@example.com" header, then add this member. That means: - Remove checks for the presense of `ContactId::SELF` in the group. Thus all recipients of a message take the same decision about group membership changes, no matter if they are in the group currently. This fixes a situation when a recipient thinks it's not a member because it missed a message about its addition before. NOTE: But always recreate membership list if SELF has been added. The older versions of DC don't always set "In-Reply-To" to the latest message they sent, but to the latest delivered message (so it's a race), so we need this heuristic currently. - Recreate the group member list if we don't know the parent (=In-Reply-To) message, even if the sender isn't in the group as per our view, because we missed some messages and our view may be stale. --- src/receive_imf.rs | 103 +++++++++++++++++---------------------- src/receive_imf/tests.rs | 60 +++++++++++++++++++++++ 2 files changed, 104 insertions(+), 59 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index b575529d1..755301ae5 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1654,40 +1654,27 @@ async fn apply_group_changes( false }; - // Whether to allow any changes to the member list at all. - let allow_member_list_changes = if chat::is_contact_in_chat(context, chat_id, ContactId::SELF) - .await? - || self_added - || !mime_parser.has_chat_version() - { - // Reject old group changes. - chat_id - .update_timestamp(context, Param::MemberListTimestamp, sent_timestamp) - .await? - } else { - // Member list changes are not allowed if we're not in the group - // and are not explicitly added. - // This message comes from a Delta Chat that restored an old backup - // or the message is a MUA reply to an old message. - false - }; + // Whether to allow any changes to the member list at all. Just reject old group changes. + let allow_member_list_changes = chat_id + .update_timestamp(context, Param::MemberListTimestamp, sent_timestamp) + .await?; + + let is_from_in_chat = chat::is_contact_in_chat(context, chat_id, from_id).await?; // Whether to rebuild the member list from scratch. - let recreate_member_list = if allow_member_list_changes { + let recreate_member_list = allow_member_list_changes && { // Recreate member list if the message comes from a MUA as these messages do _not_ set add/remove headers. - // Always recreate membership list if self has been added. - if !mime_parser.has_chat_version() || self_added { - true - } else { - match mime_parser.get_header(HeaderDef::InReplyTo) { + (is_from_in_chat && !mime_parser.has_chat_version()) + // Always recreate membership list if SELF has been added. The older versions of DC + // don't always set "In-Reply-To" to the latest message they sent, but to the latest + // delivered message (so it's a race), so we have this heuristic here. + || self_added + || match mime_parser.get_header(HeaderDef::InReplyTo) { // If we don't know the referenced message, we missed some messages. // Maybe they added/removed members, so we need to recreate our member list. Some(reply_to) => rfc724_mid_exists(context, reply_to).await?.is_none(), None => false, } - } - } else { - false }; if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) { @@ -1794,43 +1781,41 @@ async fn apply_group_changes( // Recreate the member list. if recreate_member_list { - if !chat::is_contact_in_chat(context, chat_id, from_id).await? { + if !is_from_in_chat { warn!( context, - "Contact {from_id} attempts to modify group chat {chat_id} member list without being a member." + "Contact {from_id} modifies group chat {chat_id} member list possibly not being a member." ); - } else { - // Only delete old contacts if the sender is not a classical MUA user: - // Classical MUA users usually don't intend to remove users from an email - // thread, so if they removed a recipient then it was probably by accident. - if mime_parser.has_chat_version() { - context - .sql - .execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,)) - .await?; - } - - let mut members_to_add = HashSet::new(); - members_to_add.extend(to_ids); - members_to_add.insert(ContactId::SELF); - - if !from_id.is_special() { - members_to_add.insert(from_id); - } - - if let Some(removed_id) = removed_id { - members_to_add.remove(&removed_id); - } - - info!( - context, - "Recreating chat {chat_id} with members {members_to_add:?}." - ); - - chat::add_to_chat_contacts_table(context, chat_id, &Vec::from_iter(members_to_add)) - .await?; - send_event_chat_modified = true; } + // Only delete old contacts if the sender is not a classical MUA user: + // Classical MUA users usually don't intend to remove users from an email + // thread, so if they removed a recipient then it was probably by accident. + if mime_parser.has_chat_version() { + context + .sql + .execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,)) + .await?; + } + + let mut members_to_add = HashSet::new(); + members_to_add.extend(to_ids); + members_to_add.insert(ContactId::SELF); + + if !from_id.is_special() { + members_to_add.insert(from_id); + } + + if let Some(removed_id) = removed_id { + members_to_add.remove(&removed_id); + } + + info!( + context, + "Recreating chat {chat_id} with members {members_to_add:?}." + ); + + chat::add_to_chat_contacts_table(context, chat_id, &Vec::from_iter(members_to_add)).await?; + send_event_chat_modified = true; } if let Some(avatar_action) = &mime_parser.group_avatar { diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs index c610961e1..dc9f91696 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -8,6 +8,7 @@ use crate::chat::{ }; use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility}; use crate::chatlist::Chatlist; +use crate::config::Config; use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS}; use crate::imap::prefetch_should_download; use crate::message::Message; @@ -3611,3 +3612,62 @@ async fn test_mua_can_readd() -> Result<()> { assert!(is_contact_in_chat(&alice, alice_chat.id, ContactId::SELF).await?); Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + add_contact_to_chat( + &alice, + alice_chat_id, + Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?, + ) + .await?; + send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; + alice.pop_sent_msg().await; + remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?; + let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id; + + // Bob missed the message adding them, but must recreate the member list. + assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1); + assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_recreate_member_list_on_missing_add_of_others() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + add_contact_to_chat( + &alice, + alice_chat_id, + Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?, + ) + .await?; + send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; + let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id; + + let fiona = TestContext::new_fiona().await; + add_contact_to_chat( + &alice, + alice_chat_id, + Contact::create( + &alice, + "fiona", + &fiona.get_config(Config::Addr).await?.unwrap(), + ) + .await?, + ) + .await?; + let fiona_chat_id = fiona.recv_msg(&alice.pop_sent_msg().await).await.chat_id; + fiona_chat_id.accept(&fiona).await?; + + send_text_msg(&fiona, fiona_chat_id, "hi".to_string()).await?; + bob.recv_msg(&fiona.pop_sent_msg().await).await; + + // Bob missed the message adding fiona, but must recreate the member list. + assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 3); + Ok(()) +} From 38b31aa88dcfa3632022ecf0835a9b386f2b1674 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 8 Sep 2023 01:49:23 +0000 Subject: [PATCH 05/12] fix: do not block new group chats if 1:1 chat is blocked 1:1 chat may be blocked while the contact is not if 1:1 chat was created as a result of scanning a verified group join QR code with the contact as the inviter. In this case 1:1 chat is blocked to hide it while the contact is unblocked. --- python/tests/test_1_online.py | 30 ++++++++++++++++++++++++++++++ src/receive_imf.rs | 33 ++++++++++++++++++++++----------- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 0ca45abbf..c94e4a904 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -1679,6 +1679,36 @@ def test_qr_join_chat(acfactory, lp): ac1._evtracker.wait_securejoin_inviter_progress(1000) +def test_qr_new_group_unblocked(acfactory, lp): + """Regression test for a bug intoduced in core v1.113.0. + ac2 scans a verified group QR code created by ac1. + This results in creation of a blocked 1:1 chat with ac1 on ac2, + but ac1 contact is not blocked on ac2. + Then ac1 creates a group, adds ac2 there and promotes it by sending a message. + ac2 should receive a message and create a contact request for the group. + Due to a bug previously ac2 created a blocked group. + """ + + ac1, ac2 = acfactory.get_online_accounts(2) + ac1_chat = ac1.create_group_chat("Group for joining", verified=True) + qr = ac1_chat.get_join_qr() + ac2.qr_join_chat(qr) + + ac1._evtracker.wait_securejoin_inviter_progress(1000) + + ac1_new_chat = ac1.create_group_chat("Another group") + ac1_new_chat.add_contact(ac2) + ac1_new_chat.send_text("Hello!") + + # Receive "Member added" message. + ac2._evtracker.wait_next_incoming_message() + + # Receive "Hello!" message. + ac2_msg = ac2._evtracker.wait_next_incoming_message() + assert ac2_msg.text == "Hello!" + assert ac2_msg.chat.is_contact_request() + + def test_qr_email_capitalization(acfactory, lp): """Regression test for a bug that resulted in failure to propagate verification via gossip in a verified group diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 755301ae5..2179356d7 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -546,19 +546,30 @@ async fn add_parts( // signals whether the current user is a bot let is_bot = context.get_config_bool(Config::Bot).await?; - let create_blocked = match test_normal_chat { - Some(ChatIdBlocked { - id: _, - blocked: Blocked::Request, - }) if is_bot => Blocked::Not, - Some(ChatIdBlocked { id: _, blocked }) => blocked, - None => { - if is_bot { - Blocked::Not - } else { - Blocked::Request + let create_blocked_default = if is_bot { + Blocked::Not + } else { + Blocked::Request + }; + let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat { + match blocked { + Blocked::Request => create_blocked_default, + Blocked::Not => Blocked::Not, + Blocked::Yes => { + if Contact::is_blocked_load(context, from_id).await? { + // User has blocked the contact. + // Block the group contact created as well. + Blocked::Yes + } else { + // 1:1 chat is blocked, but the contact is not. + // This happens when 1:1 chat is hidden + // during scanning of a group invitation code. + Blocked::Request + } } } + } else { + create_blocked_default }; if chat_id.is_none() { From 7a359f631863e3173f39fbac870fbc02b15c482c Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 8 Sep 2023 15:50:00 +0000 Subject: [PATCH 06/12] build(python): add link to mastodon into projects.urls Such links are displayed on PyPI with mastodon icon. --- python/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/python/pyproject.toml b/python/pyproject.toml index 024f62dc0..5e0bee124 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -34,6 +34,7 @@ dynamic = [ "Home" = "https://github.com/deltachat/deltachat-core-rust/" "Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues" "Documentation" = "https://py.delta.chat/" +"Mastodon" = "https://chaos.social/@delta" [project.entry-points.pytest11] "deltachat.testplugin" = "deltachat.testplugin" From 9bd7ab728035f44e6155e5254bd51f38d9493c97 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Sun, 10 Sep 2023 21:20:26 -0300 Subject: [PATCH 07/12] fix: apply_group_changes(): Forbid membership changes from possible non-members (#3782) It can be not good for membership consistency if we missed a message adding a member, but improves security because nobody can add themselves to a group from now on. --- src/receive_imf.rs | 30 +++++++++++++++++------------- src/receive_imf/tests.rs | 14 +++++++++++--- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 2179356d7..2e51cbaa1 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1665,17 +1665,19 @@ async fn apply_group_changes( false }; - // Whether to allow any changes to the member list at all. Just reject old group changes. - let allow_member_list_changes = chat_id - .update_timestamp(context, Param::MemberListTimestamp, sent_timestamp) - .await?; + let is_from_in_chat = !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await? + || chat::is_contact_in_chat(context, chat_id, from_id).await?; - let is_from_in_chat = chat::is_contact_in_chat(context, chat_id, from_id).await?; + // Reject group membership changes from non-members and old changes. + let allow_member_list_changes = is_from_in_chat + && chat_id + .update_timestamp(context, Param::MemberListTimestamp, sent_timestamp) + .await?; // Whether to rebuild the member list from scratch. - let recreate_member_list = allow_member_list_changes && { + let recreate_member_list = { // Recreate member list if the message comes from a MUA as these messages do _not_ set add/remove headers. - (is_from_in_chat && !mime_parser.has_chat_version()) + !mime_parser.has_chat_version() // Always recreate membership list if SELF has been added. The older versions of DC // don't always set "In-Reply-To" to the latest message they sent, but to the latest // delivered message (so it's a race), so we have this heuristic here. @@ -1686,6 +1688,14 @@ async fn apply_group_changes( Some(reply_to) => rfc724_mid_exists(context, reply_to).await?.is_none(), None => false, } + } && { + if !allow_member_list_changes { + info!( + context, + "Ignoring a try to recreate member list of {chat_id} by {from_id}.", + ); + } + allow_member_list_changes }; if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) { @@ -1792,12 +1802,6 @@ async fn apply_group_changes( // Recreate the member list. if recreate_member_list { - if !is_from_in_chat { - warn!( - context, - "Contact {from_id} modifies group chat {chat_id} member list possibly not being a member." - ); - } // Only delete old contacts if the sender is not a classical MUA user: // Classical MUA users usually don't intend to remove users from an email // thread, so if they removed a recipient then it was probably by accident. diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs index dc9f91696..051707627 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -3636,7 +3636,7 @@ async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_recreate_member_list_on_missing_add_of_others() -> Result<()> { +async fn test_keep_member_list_if_possibly_nomember() -> Result<()> { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; @@ -3667,7 +3667,15 @@ async fn test_recreate_member_list_on_missing_add_of_others() -> Result<()> { send_text_msg(&fiona, fiona_chat_id, "hi".to_string()).await?; bob.recv_msg(&fiona.pop_sent_msg().await).await; - // Bob missed the message adding fiona, but must recreate the member list. - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 3); + // Bob missed the message adding fiona, but mustn't recreate the member list. + assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); + assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?); + let bob_alice_contact = Contact::create( + &bob, + "alice", + &alice.get_config(Config::Addr).await?.unwrap(), + ) + .await?; + assert!(is_contact_in_chat(&bob, bob_chat_id, bob_alice_contact).await?); Ok(()) } From 68c95dee17459eee8c9b34185fdaf19b53c790a0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 11 Sep 2023 17:22:25 +0000 Subject: [PATCH 08/12] refactor(pgp): add constants for encryption algorithm and hash These constants are current defaults in `pgp` crate, this change would prevent accidental change due to rPGP upgrade and make it easier to change in a single place. --- src/pgp.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/pgp.rs b/src/pgp.rs index a4ae1f014..0113cd5b9 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -30,6 +30,12 @@ pub(crate) const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt"; #[allow(missing_docs)] pub const HEADER_SETUPCODE: &str = "passphrase-begin"; +/// Preferred symmetric encryption algorithm. +const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128; + +/// Preferred cryptographic hash. +const HASH_ALGORITHM: HashAlgorithm = HashAlgorithm::SHA2_256; + /// A wrapper for rPGP public key types #[derive(Debug)] enum SignedPublicKeyOrSubkey<'a> { @@ -249,11 +255,13 @@ pub async fn pk_encrypt( // TODO: measure time let encrypted_msg = if let Some(ref skey) = private_key_for_signing { lit_msg - .sign(skey, || "".into(), Default::default()) + .sign(skey, || "".into(), HASH_ALGORITHM) .and_then(|msg| msg.compress(CompressionAlgorithm::ZLIB)) - .and_then(|msg| msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs)) + .and_then(|msg| { + msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs) + }) } else { - lit_msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs) + lit_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs) }; let msg = encrypted_msg?; @@ -272,7 +280,7 @@ pub fn pk_calc_signature( let msg = Message::new_literal_bytes("", plain).sign( private_key_for_signing, || "".into(), - Default::default(), + HASH_ALGORITHM, )?; let signature = msg.into_signature().to_armored_string(None)?; Ok(signature) @@ -369,7 +377,7 @@ pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result { let mut rng = thread_rng(); let s2k = StringToKey::new_default(&mut rng); let msg = - lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase)?; + lit_msg.encrypt_with_password(&mut rng, s2k, SYMMETRIC_KEY_ALGORITHM, || passphrase)?; let encoded_msg = msg.to_armored_string(None)?; From 49cc5fb673124efc5911eddca9181403774f5184 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 11 Sep 2023 22:11:53 +0000 Subject: [PATCH 09/12] feat: add RSA-4096 key generation support --- deltachat-ffi/deltachat.h | 5 ++++- node/constants.js | 1 + node/lib/constants.ts | 1 + src/constants.rs | 8 ++++++++ src/pgp.rs | 1 + 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index ff76c3d7c..e4d268262 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -443,7 +443,9 @@ char* dc_get_blobdir (const dc_context_t* context); * DC_KEY_GEN_RSA2048 (1)= * generate RSA 2048 keypair * DC_KEY_GEN_ED25519 (2)= - * generate Ed25519 keypair + * generate Curve25519 keypair + * DC_KEY_GEN_RSA4096 (3)= + * generate RSA 4096 keypair * - `save_mime_headers` = 1=save mime headers * and make dc_get_mime_headers() work for subsequent calls, * 0=do not save mime headers (default) @@ -6293,6 +6295,7 @@ void dc_event_unref(dc_event_t* event); #define DC_KEY_GEN_DEFAULT 0 #define DC_KEY_GEN_RSA2048 1 #define DC_KEY_GEN_ED25519 2 +#define DC_KEY_GEN_RSA4096 3 /** diff --git a/node/constants.js b/node/constants.js index d55d92782..dcbe52daf 100644 --- a/node/constants.js +++ b/node/constants.js @@ -90,6 +90,7 @@ module.exports = { DC_KEY_GEN_DEFAULT: 0, DC_KEY_GEN_ED25519: 2, DC_KEY_GEN_RSA2048: 1, + DC_KEY_GEN_RSA4096: 3, DC_LP_AUTH_NORMAL: 4, DC_LP_AUTH_OAUTH2: 2, DC_MEDIA_QUALITY_BALANCED: 0, diff --git a/node/lib/constants.ts b/node/lib/constants.ts index cacbd570d..f97e44426 100644 --- a/node/lib/constants.ts +++ b/node/lib/constants.ts @@ -90,6 +90,7 @@ export enum C { DC_KEY_GEN_DEFAULT = 0, DC_KEY_GEN_ED25519 = 2, DC_KEY_GEN_RSA2048 = 1, + DC_KEY_GEN_RSA4096 = 3, DC_LP_AUTH_NORMAL = 4, DC_LP_AUTH_OAUTH2 = 2, DC_MEDIA_QUALITY_BALANCED = 0, diff --git a/src/constants.rs b/src/constants.rs index c3eb33f1d..957a09ef3 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -62,8 +62,15 @@ pub enum MediaQuality { pub enum KeyGenType { #[default] Default = 0, + + /// 2048-bit RSA. Rsa2048 = 1, + + /// [Ed25519](https://ed25519.cr.yp.to/) signature and X25519 encryption. Ed25519 = 2, + + /// 4096-bit RSA. + Rsa4096 = 3, } /// Video chat URL type. @@ -231,6 +238,7 @@ mod tests { assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap()); assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap()); assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap()); + assert_eq!(KeyGenType::Rsa4096, KeyGenType::from_i32(3).unwrap()); } #[test] diff --git a/src/pgp.rs b/src/pgp.rs index 0113cd5b9..d4a5a072b 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -142,6 +142,7 @@ pub struct KeyPair { pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Result { let (secret_key_type, public_key_type) = match keygen_type { KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)), + KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)), KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH), }; From 67043177a91d54bb986619a1460e43b97fe58d26 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 11 Sep 2023 21:35:54 +0000 Subject: [PATCH 10/12] fix: reopen all connections on database passpharse change Previously only one connection, the one used to change the key, was working after passphrase change. With this fix the whole pool of connections is recreated on passphrase change, so there is no need to reopen the database manually. --- src/context.rs | 29 +++++++++++++++++++++++++++++ src/sql.rs | 36 +++++++++++++++++++++++++++++------- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/context.rs b/src/context.rs index ba96035fb..78c0ed47f 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1459,6 +1459,35 @@ mod tests { Ok(()) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_context_change_passphrase() -> Result<()> { + let dir = tempdir()?; + let dbfile = dir.path().join("db.sqlite"); + + let id = 1; + let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new()) + .await + .context("failed to create context")?; + assert_eq!(context.open("foo".to_string()).await?, true); + assert_eq!(context.is_open().await, true); + + context + .set_config(Config::Addr, Some("alice@example.org")) + .await?; + + context + .change_passphrase("bar".to_string()) + .await + .context("Failed to change passphrase")?; + + assert_eq!( + context.get_config(Config::Addr).await?.unwrap(), + "alice@example.org" + ); + + Ok(()) + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ongoing() -> Result<()> { let context = TestContext::new().await; diff --git a/src/sql.rs b/src/sql.rs index ab780a452..ceb38067e 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -310,12 +310,17 @@ impl Sql { /// It is impossible to turn encrypted database into unencrypted /// and vice versa this way, use import/export for this. pub async fn change_passphrase(&self, passphrase: String) -> Result<()> { - self.call_write(move |conn| { - conn.pragma_update(None, "rekey", passphrase) - .context("failed to set PRAGMA rekey")?; - Ok(()) - }) - .await + let mut lock = self.pool.write().await; + + let pool = lock.take().context("SQL connection pool is not open")?; + let conn = pool.get().await?; + conn.pragma_update(None, "rekey", passphrase.clone()) + .context("failed to set PRAGMA rekey")?; + drop(pool); + + *lock = Some(Self::new_pool(&self.dbfile, passphrase.to_string())?); + + Ok(()) } /// Locks the write transactions mutex in order to make sure that there never are @@ -1265,7 +1270,7 @@ mod tests { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_change_passphrase() -> Result<()> { + async fn test_sql_change_passphrase() -> Result<()> { use tempfile::tempdir; // The context is used only for logging. @@ -1289,6 +1294,23 @@ mod tests { sql.change_passphrase("bar".to_string()) .await .context("failed to change passphrase")?; + + // Test that at least two connections are still working. + // This ensures that not only the connection which changed the password is working, + // but other connections as well. + { + let lock = sql.pool.read().await; + let pool = lock.as_ref().unwrap(); + let conn1 = pool.get().await?; + let conn2 = pool.get().await?; + conn1 + .query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(())) + .unwrap(); + conn2 + .query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(())) + .unwrap(); + } + sql.close().await; let sql = Sql::new(dbfile); From ed781af52ca7aec899b9d8990909c4409c392bf0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 12 Sep 2023 16:36:46 +0000 Subject: [PATCH 11/12] chore(cargo): update to OpenSSL 3.0 OpenSSL 1.1.1 has reached End of Life: https://www.openssl.org/blog/blog/2023/09/11/eol-111/ --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d94847778..e7238b76e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3078,11 +3078,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.55" +version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.3.3", "cfg-if", "foreign-types", "libc", @@ -3110,18 +3110,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.26.0+1.1.1u" +version = "300.1.3+3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37" +checksum = "cd2c101a165fff9935e34def4669595ab1c7847943c42be86e21503e482be107" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.90" +version = "0.9.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" dependencies = [ "cc", "libc", From f02299c06cad087397ae5f18d02cc805ebcff6c3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 12 Sep 2023 17:33:22 +0000 Subject: [PATCH 12/12] chore(release): prepare for 1.122.0 --- CHANGELOG.md | 29 +++++++++++++++++++++++ Cargo.lock | 10 ++++---- Cargo.toml | 2 +- deltachat-ffi/Cargo.toml | 2 +- deltachat-jsonrpc/Cargo.toml | 2 +- deltachat-jsonrpc/typescript/package.json | 2 +- deltachat-repl/Cargo.toml | 2 +- deltachat-rpc-server/Cargo.toml | 2 +- package.json | 2 +- release-date.in | 2 +- 10 files changed, 42 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4c1e029d..329f52089 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## [1.122.0] - 2023-09-12 + +### API-Changes + +- jsonrpc: Return only chat IDs for similar chats. + +### Fixes + +- Reopen all connections on database passpharse change. +- Do not block new group chats if 1:1 chat is blocked. +- Improve group membership consistency algorithm ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782))([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)). +- Forbid membership changes from possible non-members ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782)). +- `ChatId::parent_query()`: Don't filter out OutPending and OutFailed messages. + +### Build system + +- Update to OpenSSL 3.0. +- Bump webpki from 0.22.0 to 0.22.1. +- python: Add link to Mastodon into projects.urls. + +### Features / Changes + +- Add RSA-4096 key generation support. + +### Refactor + +- pgp: Add constants for encryption algorithm and hash. + ## [1.121.0] - 2023-09-06 ### API-Changes @@ -2787,3 +2815,4 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed [1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1 [1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0 [1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0 +[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0 diff --git a/Cargo.lock b/Cargo.lock index e7238b76e..46d76958b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1103,7 +1103,7 @@ dependencies = [ [[package]] name = "deltachat" -version = "1.121.0" +version = "1.122.0" dependencies = [ "ansi_term", "anyhow", @@ -1179,7 +1179,7 @@ dependencies = [ [[package]] name = "deltachat-jsonrpc" -version = "1.121.0" +version = "1.122.0" dependencies = [ "anyhow", "async-channel", @@ -1203,7 +1203,7 @@ dependencies = [ [[package]] name = "deltachat-repl" -version = "1.121.0" +version = "1.122.0" dependencies = [ "ansi_term", "anyhow", @@ -1218,7 +1218,7 @@ dependencies = [ [[package]] name = "deltachat-rpc-server" -version = "1.121.0" +version = "1.122.0" dependencies = [ "anyhow", "deltachat", @@ -1243,7 +1243,7 @@ dependencies = [ [[package]] name = "deltachat_ffi" -version = "1.121.0" +version = "1.122.0" dependencies = [ "anyhow", "deltachat", diff --git a/Cargo.toml b/Cargo.toml index 1f0e90284..6fa94c796 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat" -version = "1.121.0" +version = "1.122.0" edition = "2021" license = "MPL-2.0" rust-version = "1.65" diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index 5bafbc735..0611e8b79 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat_ffi" -version = "1.121.0" +version = "1.122.0" description = "Deltachat FFI" edition = "2018" readme = "README.md" diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index 2c69a8f6c..d97ba800c 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-jsonrpc" -version = "1.121.0" +version = "1.122.0" description = "DeltaChat JSON-RPC API" edition = "2021" default-run = "deltachat-jsonrpc-server" diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index 3f3fd3d16..51473e565 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -55,5 +55,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "1.121.0" + "version": "1.122.0" } diff --git a/deltachat-repl/Cargo.toml b/deltachat-repl/Cargo.toml index beb8e8db1..b6f90b6d4 100644 --- a/deltachat-repl/Cargo.toml +++ b/deltachat-repl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-repl" -version = "1.121.0" +version = "1.122.0" license = "MPL-2.0" edition = "2021" diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index 172922a15..5407286ca 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-rpc-server" -version = "1.121.0" +version = "1.122.0" description = "DeltaChat JSON-RPC server" edition = "2021" readme = "README.md" diff --git a/package.json b/package.json index 779fe9c0e..a2cd51844 100644 --- a/package.json +++ b/package.json @@ -60,5 +60,5 @@ "test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit" }, "types": "node/dist/index.d.ts", - "version": "1.121.0" + "version": "1.122.0" } diff --git a/release-date.in b/release-date.in index 1cef16d38..05f0fc2f9 100644 --- a/release-date.in +++ b/release-date.in @@ -1 +1 @@ -2023-09-06 \ No newline at end of file +2023-09-12 \ No newline at end of file