From d89a9e2f28533f74ea84b5ea2e23a27a24e989d7 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 3 Sep 2025 03:56:33 +0000 Subject: [PATCH] feat: withdraw all QR codes when one is withdrawn This is a preparation for expiring authentication tokens. If we make authentication token expire, we need to generate new authentication tokens each time QR code screen is opened in the UI, so authentication token is fresh. We however don't want to completely invalidate old authentication codes at the same time, e.g. they should still be valid for joining groups, just not result in a verification on the inviter side. Since a group now can have a lot of authentication tokens, it is easy to lose track of them without any way to remove them as they are not displayed anywhere in the UI. As a solution, we now remove all tokens corresponding to a group ID when one token is withdrawn, or all non-group tokens when a single non-group token is withdrawn. "Reset QR code" option already present in the UI which works by resetting current QR code will work without any UI changes, but will now result in invalidation of all previously created QR codes and invite links. --- src/qr.rs | 7 ++--- src/qr/qr_tests.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++- src/sync.rs | 15 +++++++--- src/token.rs | 11 +++---- 4 files changed, 92 insertions(+), 14 deletions(-) diff --git a/src/qr.rs b/src/qr.rs index 6453188033..553a2a9243 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -766,19 +766,18 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> { authcode, .. } => { - token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?; - token::delete(context, token::Namespace::Auth, &authcode).await?; + token::delete(context, "").await?; context .sync_qr_code_token_deletion(invitenumber, authcode) .await?; } Qr::WithdrawVerifyGroup { + grpid, invitenumber, authcode, .. } => { - token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?; - token::delete(context, token::Namespace::Auth, &authcode).await?; + token::delete(context, &grpid).await?; context .sync_qr_code_token_deletion(invitenumber, authcode) .await?; diff --git a/src/qr/qr_tests.rs b/src/qr/qr_tests.rs index 01cb0edbec..307cbbf5d4 100644 --- a/src/qr/qr_tests.rs +++ b/src/qr/qr_tests.rs @@ -2,7 +2,7 @@ use super::*; use crate::chat::{ProtectionStatus, create_group_chat}; use crate::config::Config; use crate::securejoin::get_securejoin_qr; -use crate::test_utils::{TestContext, TestContextManager}; +use crate::test_utils::{TestContext, TestContextManager, sync}; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decode_http() -> Result<()> { @@ -509,6 +509,77 @@ async fn test_withdraw_verifygroup() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_withdraw_multidevice() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let alice2 = &tcm.alice().await; + + alice.set_config_bool(Config::SyncMsgs, true).await?; + alice2.set_config_bool(Config::SyncMsgs, true).await?; + + // Alice creates two QR codes on the first device: + // group QR code and contact QR code. + let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; + let chat2_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group 2").await?; + let contact_qr = get_securejoin_qr(alice, None).await?; + let group_qr = get_securejoin_qr(alice, Some(chat_id)).await?; + let group2_qr = get_securejoin_qr(alice, Some(chat2_id)).await?; + + assert!(matches!( + check_qr(alice, &contact_qr).await?, + Qr::WithdrawVerifyContact { .. } + )); + assert!(matches!( + check_qr(alice, &group_qr).await?, + Qr::WithdrawVerifyGroup { .. } + )); + + // Sync group QR codes. + sync(alice, alice2).await; + assert!(matches!( + check_qr(alice2, &group_qr).await?, + Qr::WithdrawVerifyGroup { .. } + )); + assert!(matches!( + check_qr(alice2, &group2_qr).await?, + Qr::WithdrawVerifyGroup { .. } + )); + + // Alice creates a contact QR code on second device + // and withdraws it. + let contact_qr2 = get_securejoin_qr(alice2, None).await?; + set_config_from_qr(alice2, &contact_qr2).await?; + assert!(matches!( + check_qr(alice2, &contact_qr2).await?, + Qr::ReviveVerifyContact { .. } + )); + + // Alice also withdraws second group QR code on second device. + set_config_from_qr(alice2, &group2_qr).await?; + + // Sync messages are sent from Alice's second device to first device. + sync(alice2, alice).await; + + // Now first device has reset all contact QR codes + // and second group QR code, + // but first group QR code is still valid. + assert!(matches!( + check_qr(alice, &contact_qr2).await?, + Qr::ReviveVerifyContact { .. } + )); + assert!(matches!( + check_qr(alice, &group_qr).await?, + Qr::WithdrawVerifyGroup { .. } + )); + assert!(matches!( + check_qr(alice, &group2_qr).await?, + Qr::ReviveVerifyGroup { .. } + )); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decode_and_apply_dclogin() -> Result<()> { let ctx = TestContext::new().await; diff --git a/src/sync.rs b/src/sync.rs index b5a42130b8..6342e78cfa 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -296,8 +296,15 @@ impl Context { } async fn delete_qr_token(&self, token: &QrTokenData) -> Result<()> { - token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?; - token::delete(self, Namespace::Auth, &token.auth).await?; + self.sql + .execute( + "DELETE FROM tokens + WHERE foreign_key IN + (SELECT foreign_key FROM tokens + WHERE token=? OR token=?)", + (&token.invitenumber, &token.auth), + ) + .await?; Ok(()) } @@ -568,8 +575,8 @@ mod tests { .await? .is_none() ); - assert!(token::exists(&t, Namespace::InviteNumber, "yip-in").await?); - assert!(token::exists(&t, Namespace::Auth, "yip-auth").await?); + assert!(!token::exists(&t, Namespace::InviteNumber, "yip-in").await?); + assert!(!token::exists(&t, Namespace::Auth, "yip-auth").await?); assert!(!token::exists(&t, Namespace::Auth, "non-existent").await?); assert!(!token::exists(&t, Namespace::Auth, "directly deleted").await?); diff --git a/src/token.rs b/src/token.rs index a5bdfc0681..7846e1e59a 100644 --- a/src/token.rs +++ b/src/token.rs @@ -104,13 +104,14 @@ pub async fn auth_foreign_key(context: &Context, token: &str) -> Result Result<()> { +/// Resets all tokens corresponding to the `foreign_key`. +/// +/// `foreign_key` is a group ID to reset all group tokens +/// or empty string to reset all setup contact tokens. +pub async fn delete(context: &Context, foreign_key: &str) -> Result<()> { context .sql - .execute( - "DELETE FROM tokens WHERE namespc=? AND token=?;", - (namespace, token), - ) + .execute("DELETE FROM tokens WHERE foreign_key=?", (foreign_key,)) .await?; Ok(()) }