Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions src/securejoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ use crate::qr::check_qr;
use crate::securejoin::bob::JoinerProgress;
use crate::sync::Sync::*;
use crate::token;
use crate::tools::create_id;
use crate::tools::time;

mod bob;
mod qrinvite;
Expand Down Expand Up @@ -75,10 +77,21 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
let sync_token = token::lookup(context, Namespace::InviteNumber, grpid)
.await?
.is_none();
// invitenumber will be used to allow starting the handshake,
// auth will be used to verify the fingerprint
// Invite number is used to request the inviter key.
let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, grpid).await?;
let auth = token::lookup_or_new(context, Namespace::Auth, grpid).await?;

// Auth token is used to verify the key-contact
// if the token is not old
// and add the contact to the group
// if there is an associated group ID.
//
// We always generate a new auth token
// because auth tokens "expire"
// and can only be used to join groups
// without verification afterwards.
let auth = create_id();
token::save(context, Namespace::Auth, grpid, &auth).await?;

let self_addr = context.get_primary_self_addr().await?;
let self_name = context
.get_config(Config::Displayname)
Expand Down Expand Up @@ -364,7 +377,19 @@ pub(crate) async fn handle_securejoin_handshake(
);
return Ok(HandshakeMessage::Ignore);
};
let Some(grpid) = token::auth_foreign_key(context, auth).await? else {
let Some((grpid, timestamp)) = context
.sql
.query_row_optional(
"SELECT foreign_key, timestamp FROM tokens WHERE namespc=? AND token=?",
(Namespace::Auth, auth),
|row| {
let foreign_key: String = row.get(0)?;
let timestamp: i64 = row.get(1)?;
Ok((foreign_key, timestamp))
},
)
.await?
else {
warn!(
context,
"Ignoring {step} message because of invalid auth code."
Expand All @@ -382,14 +407,23 @@ pub(crate) async fn handle_securejoin_handshake(
}
};

if !verify_sender_by_fingerprint(context, &fingerprint, contact_id).await? {
let sender_contact = Contact::get_by_id(context, contact_id).await?;
let sender_is_verified = sender_contact
.fingerprint()
.is_some_and(|fp| fp == fingerprint);
if !sender_is_verified {
warn!(
context,
"Ignoring {step} message because of fingerprint mismatch."
);
return Ok(HandshakeMessage::Ignore);
}
info!(context, "Fingerprint verified via Auth code.",);

// Mark the contact as verified if auth code is 600 second old.
if time() < timestamp + 600 {
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
}
contact_id.regossip_keys(context).await?;
ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
// for setup-contact, make Alice's one-to-one chat with Bob visible
Expand Down
37 changes: 37 additions & 0 deletions src/securejoin/securejoin_tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Duration;

use deltachat_contact_tools::EmailAddress;

use super::*;
Expand All @@ -7,6 +9,7 @@ use crate::constants::Chattype;
use crate::key::self_fingerprint;
use crate::mimeparser::GossipedKey;
use crate::receive_imf::receive_imf;
use crate::tools::SystemTime;
use crate::stock_str::{self, messages_e2e_encrypted};
use crate::test_utils::{
TestContext, TestContextManager, TimeShiftFalsePositiveNote, get_chat_msg,
Expand Down Expand Up @@ -800,3 +803,37 @@ async fn test_wrong_auth_token() -> Result<()> {

Ok(())
}

/// Tests that scanning a QR code week later
/// allows Bob to establish a contact with Alice,
/// but does not mark Bob as verified for Alice.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_expired_auth_token() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;

// Alice creates a QR code.
let alice_qr = get_securejoin_qr(alice, None).await?;

// One week passes, QR code expires.
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));

// Bob scans the QR code.
join_securejoin(bob, &alice_qr).await?;

// vc-request
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;

// vc-auth-requried
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;

// vc-request-with-auth
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;

// Bob should not be verified for Alice.
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false);

Ok(())
}
18 changes: 0 additions & 18 deletions src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,24 +86,6 @@ pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> Res
Ok(exists)
}

/// Looks up foreign key by auth token.
///
/// Returns None if auth token is not valid.
/// Returns an empty string if the token corresponds to "setup contact" rather than group join.
pub async fn auth_foreign_key(context: &Context, token: &str) -> Result<Option<String>> {
context
.sql
.query_row_optional(
"SELECT foreign_key FROM tokens WHERE namespc=? AND token=?",
(Namespace::Auth, token),
|row| {
let foreign_key: String = row.get(0)?;
Ok(foreign_key)
},
)
.await
}

pub async fn delete(context: &Context, namespace: Namespace, token: &str) -> Result<()> {
context
.sql
Expand Down
Loading