From 92c6dd483cc566b5de3cfc5bca25143520a16aaa Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 2 Nov 2024 20:45:47 +0000 Subject: [PATCH] api: add API to reset contact encryption --- deltachat-jsonrpc/src/api.rs | 9 ++ .../src/deltachat_rpc_client/contact.py | 4 + deltachat-rpc-client/tests/test_something.py | 1 + src/contact.rs | 92 +++++++++++++++++++ 4 files changed, 106 insertions(+) diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 92d4804e51..5fb0efed4c 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -1419,6 +1419,15 @@ impl CommandApi { Ok(()) } + /// Resets contact encryption. + async fn reset_contact_encryption(&self, account_id: u32, contact_id: u32) -> Result<()> { + let ctx = self.get_context(account_id).await?; + let contact_id = ContactId::new(contact_id); + + contact_id.reset_encryption(&ctx).await?; + Ok(()) + } + async fn change_contact_name( &self, account_id: u32, diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py index eefa474f10..81c4bba59d 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py @@ -36,6 +36,10 @@ def delete(self) -> None: """Delete contact.""" self._rpc.delete_contact(self.account.id, self.id) + def reset_encryption(self) -> None: + """Reset contact encryption.""" + self._rpc.reset_contact_encryption(self.account.id, self.id) + def set_name(self, name: str) -> None: """Change the name of this contact.""" self._rpc.change_contact_name(self.account.id, self.id, name) diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 7f1a153520..22f22a6a4a 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -246,6 +246,7 @@ def test_contact(acfactory) -> None: assert repr(alice_contact_bob) alice_contact_bob.block() alice_contact_bob.unblock() + alice_contact_bob.reset_encryption() alice_contact_bob.set_name("new name") alice_contact_bob.get_encryption_info() snapshot = alice_contact_bob.get_snapshot() diff --git a/src/contact.rs b/src/contact.rs index 4d8704f246..1fa2bea6be 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -143,6 +143,43 @@ impl ContactId { .await?; Ok(()) } + + /// Returns contact adress. + pub async fn addr(&self, context: &Context) -> Result { + let addr = context + .sql + .query_row("SELECT addr FROM contacts WHERE id=?", (self,), |row| { + let addr: String = row.get(0)?; + Ok(addr) + }) + .await?; + Ok(addr) + } + + /// Resets encryption with the contact. + /// + /// Effect is similar to receiving a message without Autocrypt header + /// from the contact, but this action is triggered manually by the user. + /// + /// For example, this will result in sending the next message + /// to 1:1 chat unencrypted, but will not remove existing verified keys. + pub async fn reset_encryption(self, context: &Context) -> Result<()> { + let now = time(); + + let addr = self.addr(context).await?; + if let Some(mut peerstate) = Peerstate::from_addr(context, &addr).await? { + peerstate.degrade_encryption(now); + peerstate.save_to_db(&context.sql).await?; + } + + // Reset 1:1 chat protection. + if let Some(chat_id) = ChatId::lookup_by_contact(context, self).await? { + chat_id + .set_protection(context, ProtectionStatus::Unprotected, now, Some(self)) + .await?; + } + Ok(()) + } } impl fmt::Display for ContactId { @@ -3152,4 +3189,59 @@ Until the false-positive is fixed: Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_reset_encryption() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let msg = tcm.send_recv_accept(alice, bob, "Hello!").await; + assert_eq!(msg.get_showpadlock(), false); + + let msg = tcm.send_recv(bob, alice, "Hi!").await; + assert_eq!(msg.get_showpadlock(), true); + let alice_bob_contact_id = msg.from_id; + + alice_bob_contact_id.reset_encryption(alice).await?; + + let msg = tcm.send_recv(alice, bob, "Unencrypted").await; + assert_eq!(msg.get_showpadlock(), false); + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_reset_verified_encryption() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + tcm.execute_securejoin(bob, alice).await; + + let msg = tcm.send_recv(bob, alice, "Encrypted").await; + assert_eq!(msg.get_showpadlock(), true); + + let alice_bob_chat_id = msg.chat_id; + let alice_bob_contact_id = msg.from_id; + alice_bob_contact_id.reset_encryption(alice).await?; + + // Check that the contact is still verified after resetting encryption. + let alice_bob_contact = Contact::get_by_id(alice, alice_bob_contact_id).await?; + assert_eq!(alice_bob_contact.is_verified(alice).await?, true); + + // 1:1 chat and profile is no longer verified. + assert_eq!(alice_bob_contact.is_profile_verified(alice).await?, false); + + let info_msg = alice.get_last_msg_in(alice_bob_chat_id).await; + assert_eq!( + info_msg.text, + "bob@example.net sent a message from another device." + ); + + let msg = tcm.send_recv(alice, bob, "Unencrypted").await; + assert_eq!(msg.get_showpadlock(), false); + + Ok(()) + } }