diff --git a/deltachat-jsonrpc/src/api/types/chat.rs b/deltachat-jsonrpc/src/api/types/chat.rs index e75892bb7..96388c27b 100644 --- a/deltachat-jsonrpc/src/api/types/chat.rs +++ b/deltachat-jsonrpc/src/api/types/chat.rs @@ -71,7 +71,9 @@ pub struct FullChat { fresh_message_counter: usize, // is_group - please check over chat.type in frontend instead is_contact_request: bool, - is_protection_broken: bool, // deprecated 2025-07 + /// Deprecated 2025-07. Chats protection cannot break any longer. + is_protection_broken: bool, + is_device_chat: bool, self_in_group: bool, is_muted: bool, @@ -216,7 +218,9 @@ pub struct BasicChat { is_self_talk: bool, color: String, is_contact_request: bool, + /// Deprecated 2025-07. Chats protection cannot break any longer. is_protection_broken: bool, + is_device_chat: bool, is_muted: bool, } diff --git a/src/chat.rs b/src/chat.rs index d0862a1d4..5e95afbec 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -94,14 +94,12 @@ pub enum ProtectionStatus { /// /// All members of the chat must be verified. Protected = 1, + // `2` was never used as a value. - /// The chat was protected, but now a new message came in - /// which was not encrypted / signed correctly. - /// The user has to confirm that this is OK. - /// - /// We only do this in 1:1 chats; in group chats, the chat just - /// stays protected. - ProtectionBroken = 3, // `2` was never used as a value. + // Chats don't break in Core v2 anymore. Chats with broken protection existing before the + // key-contacts migration are treated as `Unprotected`. + // + // ProtectionBroken = 3, } /// The reason why messages cannot be sent to the chat. @@ -118,10 +116,6 @@ pub(crate) enum CantSendReason { /// The chat is a contact request, it needs to be accepted before sending a message. ContactRequest, - /// Deprecated. The chat was protected, but now a new message came in - /// which was not encrypted / signed correctly. - ProtectionBroken, - /// Mailing list without known List-Post header. ReadOnlyMailingList, @@ -144,10 +138,6 @@ impl fmt::Display for CantSendReason { f, "contact request chat should be accepted before sending messages" ), - Self::ProtectionBroken => write!( - f, - "accept that the encryption isn't verified anymore before sending messages" - ), Self::ReadOnlyMailingList => { write!(f, "mailing list does not have a know post address") } @@ -479,16 +469,6 @@ impl ChatId { let chat = Chat::load_from_db(context, self).await?; match chat.typ { - Chattype::Single - if chat.blocked == Blocked::Not - && chat.protected == ProtectionStatus::ProtectionBroken => - { - // The protection was broken, then the user clicked 'Accept'/'OK', - // so, now we want to set the status to Unprotected again: - chat.id - .inner_set_protection(context, ProtectionStatus::Unprotected) - .await?; - } Chattype::Single | Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast => { // User has "created a chat" with all these contacts. // @@ -545,7 +525,7 @@ impl ChatId { | Chattype::InBroadcast => {} Chattype::Mailinglist => bail!("Cannot protect mailing lists"), }, - ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {} + ProtectionStatus::Unprotected => {} }; context @@ -588,7 +568,6 @@ impl ChatId { let cmd = match protect { ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled, ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled, - ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled, }; add_info_msg_with_cmd( context, @@ -1700,12 +1679,6 @@ impl Chat { return Ok(Some(reason)); } } - if self.is_protection_broken() { - let reason = ProtectionBroken; - if !skip_fn(&reason) { - return Ok(Some(reason)); - } - } if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() { let reason = ReadOnlyMailingList; if !skip_fn(&reason) { @@ -1935,25 +1908,9 @@ impl Chat { Ok(is_encrypted) } - /// Deprecated 2025-07. Returns true if the chat was protected, and then an incoming message broke this protection. - /// - /// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag, - /// otherwise it will return false for all chats. - /// - /// 1:1 chats are automatically set as protected when a contact is verified. - /// When a message comes in that is not encrypted / signed correctly, - /// the chat is automatically set as unprotected again. - /// `is_protection_broken()` will return true until `chat_id.accept()` is called. - /// - /// The UI should let the user confirm that this is OK with a message like - /// `Bob sent a message from another device. Tap to learn more` - /// and then call `chat_id.accept()`. + /// Deprecated 2025-07. Returns false. pub fn is_protection_broken(&self) -> bool { - match self.protected { - ProtectionStatus::Protected => false, - ProtectionStatus::Unprotected => false, - ProtectionStatus::ProtectionBroken => true, - } + false } /// Returns true if location streaming is enabled in the chat. @@ -2947,7 +2904,7 @@ async fn prepare_send_msg( let mut chat = Chat::load_from_db(context, chat_id).await?; let skip_fn = |reason: &CantSendReason| match reason { - CantSendReason::ProtectionBroken | CantSendReason::ContactRequest => { + CantSendReason::ContactRequest => { // Allow securejoin messages, they are supposed to repair the verification. // If the chat is a contact request, let the user accept it later. msg.param.get_cmd() == SystemMessage::SecurejoinMessage diff --git a/src/chatlist.rs b/src/chatlist.rs index 8da78cf40..423a30ea9 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -245,9 +245,6 @@ impl Chatlist { .collect::, _>>() .map_err(Into::into) }; - // Return ProtectionBroken chats also, as that may happen to a verified chat at any - // time. It may be confusing if a chat that is normally in the list disappears - // suddenly. The UI need to deal with that case anyway. context.sql.query_map( "SELECT c.id, c.type, c.param, m.id FROM chats c diff --git a/src/context.rs b/src/context.rs index 9e891d8e7..ca964deb1 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1087,7 +1087,6 @@ impl Context { #[derive(Default)] struct ChatNumbers { protected: u32, - protection_broken: u32, opportunistic_dc: u32, opportunistic_mua: u32, unencrypted_dc: u32, @@ -1123,7 +1122,6 @@ impl Context { // how many of the chats active in the last months are: // - protected - // - protection-broken // - opportunistic-encrypted and the contact uses Delta Chat // - opportunistic-encrypted and the contact uses a classical MUA // - unencrypted and the contact uses Delta Chat @@ -1166,8 +1164,6 @@ impl Context { if protected == ProtectionStatus::Protected { chats.protected += 1; - } else if protected == ProtectionStatus::ProtectionBroken { - chats.protection_broken += 1; } else if encrypted { if is_dc_message { chats.opportunistic_dc += 1; @@ -1185,7 +1181,6 @@ impl Context { ) .await?; res += &format!("chats_protected {}\n", chats.protected); - res += &format!("chats_protection_broken {}\n", chats.protection_broken); res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc); res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua); res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc); diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 811f76fc1..6bb55bc7b 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1461,19 +1461,16 @@ async fn do_chat_assignment( chat.typ == Chattype::Single, "Chat {chat_id} is not Single", ); - let mut new_protection = match verified_encryption { + let new_protection = match verified_encryption { VerifiedEncryption::Verified => ProtectionStatus::Protected, VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected, }; - if chat.protected != ProtectionStatus::Unprotected - && new_protection == ProtectionStatus::Unprotected - // `chat.protected` must be maintained regardless of the `Config::VerifiedOneOnOneChats`. - // That's why the config is checked here, and not above. - && context.get_config_bool(Config::VerifiedOneOnOneChats).await? - { - new_protection = ProtectionStatus::ProtectionBroken; - } + ensure_and_debug_assert!( + chat.protected == ProtectionStatus::Unprotected + || new_protection == ProtectionStatus::Protected, + "Chat {chat_id} can't downgrade to Unprotected", + ); if chat.protected != new_protection { // The message itself will be sorted under the device message since the device // message is `MessageState::InNoticed`, which means that all following diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 3db54183b..4a7c40e91 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1251,6 +1251,16 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); .await?; } + inc_and_check(&mut migration_version, 133)?; + if dbversion < migration_version { + // Make `ProtectionBroken` chats `Unprotected`. Chats can't break anymore. + sql.execute_migration( + "UPDATE chats SET protected=0 WHERE protected!=1", + migration_version, + ) + .await?; + } + let new_version = sql .get_raw_config_int(VERSION_CFG) .await? diff --git a/src/stock_str.rs b/src/stock_str.rs index fbb7bd507..261416ecb 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -1292,7 +1292,7 @@ impl Context { contact_id: Option, ) -> String { match protect { - ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => { + ProtectionStatus::Unprotected => { if let Some(contact_id) = contact_id { chat_protection_disabled(self, contact_id).await } else { diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index d5595e22c..84088b6ad 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -31,7 +31,7 @@ async fn test_verified_oneonone_chat_not_broken_by_device_change() { check_verified_oneonone_chat_protection_not_broken(false).await; } -async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_email: bool) { +async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email: bool) { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; let bob = tcm.bob().await; @@ -42,7 +42,7 @@ async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_ assert_verified(&alice, &bob, ProtectionStatus::Protected).await; assert_verified(&bob, &alice, ProtectionStatus::Protected).await; - if broken_by_classical_email { + if by_classical_email { tcm.section("Bob uses a classical MUA to send a message to Alice"); receive_imf( &alice, @@ -58,7 +58,6 @@ async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_ .await .unwrap() .unwrap(); - // Bob's contact is still verified, but the chat isn't marked as protected anymore let contact = alice.add_or_lookup_contact(&bob).await; assert_eq!(contact.is_verified(&alice).await.unwrap(), true); assert_verified(&alice, &bob, ProtectionStatus::Protected).await; @@ -199,7 +198,6 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> { let chat_id = tcm.execute_securejoin(bob, alice).await; let chat = Chat::load_from_db(bob, chat_id).await?; assert!(chat.is_protected()); - assert!(!chat.is_protection_broken()); Ok(()) } @@ -213,7 +211,6 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> { // A chat with an unknown contact should be created unprotected let chat = alice.create_chat(&bob).await; assert!(!chat.is_protected()); - assert!(!chat.is_protection_broken()); receive_imf( &alice, @@ -230,14 +227,12 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> { // Now Bob is a known contact, new chats should still be created unprotected let chat = alice.create_chat(&bob).await; assert!(!chat.is_protected()); - assert!(!chat.is_protection_broken()); tcm.send_recv(&bob, &alice, "hi").await; chat.id.delete(&alice).await.unwrap(); // Now we have a public key, new chats should still be created unprotected let chat = alice.create_chat(&bob).await; assert!(!chat.is_protected()); - assert!(!chat.is_protection_broken()); Ok(()) } @@ -525,7 +520,6 @@ async fn test_message_from_old_dc_setup() -> Result<()> { assert!(contact.is_verified(alice).await.unwrap()); let chat = alice.get_chat(bob).await; assert!(chat.is_protected()); - assert_eq!(chat.is_protection_broken(), false); Ok(()) } @@ -812,19 +806,15 @@ async fn test_verified_chat_editor_reordering() -> Result<()> { // ============== Helper Functions ============== async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) { - if protected != ProtectionStatus::ProtectionBroken { - let contact = this.add_or_lookup_contact(other).await; - assert_eq!(contact.is_verified(this).await.unwrap(), true); - } + let contact = this.add_or_lookup_contact(other).await; + assert_eq!(contact.is_verified(this).await.unwrap(), true); let chat = this.get_chat(other).await; - let (expect_protected, expect_broken) = match protected { - ProtectionStatus::Unprotected => (false, false), - ProtectionStatus::Protected => (true, false), - ProtectionStatus::ProtectionBroken => (false, true), - }; - assert_eq!(chat.is_protected(), expect_protected); - assert_eq!(chat.is_protection_broken(), expect_broken); + assert_eq!( + chat.is_protected(), + protected == ProtectionStatus::Protected + ); + assert_eq!(chat.is_protection_broken(), false); } async fn enable_verified_oneonone_chats(test_contexts: &[&TestContext]) {