mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 21:46:35 +03:00
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:
@@ -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?;
|
||||
|
||||
@@ -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;
|
||||
|
||||
15
src/sync.rs
15
src/sync.rs
@@ -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?);
|
||||
|
||||
|
||||
11
src/token.rs
11
src/token.rs
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user