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.
This commit is contained in:
link2xt
2025-09-03 03:56:33 +00:00
committed by l
parent ab8aedf06e
commit 307a2eb6ec
4 changed files with 92 additions and 14 deletions

View File

@@ -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?;

View File

@@ -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;

View File

@@ -292,8 +292,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(())
}
@@ -564,8 +571,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?);

View File

@@ -104,13 +104,14 @@ pub async fn auth_foreign_key(context: &Context, token: &str) -> Result<Option<S
.await
}
pub async fn delete(context: &Context, namespace: Namespace, token: &str) -> 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(())
}