fix: Rerun the full securejoin protocol if the address was outdated (#8358)

Alternative to https://github.com/chatmail/core/pull/8355/, fixes
https://github.com/chatmail/core/issues/8329

This fixes the following bug:

Scanning QR for existing contact doesn't update relay list

Two chat partners suddenly lost connectivity to all of their relays. They added another relay, which is accessible, and set it as main. Of course, they still couldn't communicate, because there was no way to announce a new relay list to each other (clients know only about old relays so they try to send updates there).

That's why the friends used a different channel (e.g. met IRL) to send their QR codes once again.

Expected behavior: They would expect that clients update their relay lists according to the contact info inside the QR codes, and they can continue talking in Delta.

Actual behavior: But Delta Chat client doesn't update a list of announced relays on scanning a QR in case a contact already exists. It simply opens the existing chat without altering any contact info.

---------

Co-authored-by: holger krekel <holger@merlinux.eu>
This commit is contained in:
Hocuri
2026-06-25 18:59:08 +02:00
committed by GitHub
parent 09d5d0bddf
commit abb84efc37
7 changed files with 85 additions and 13 deletions

View File

@@ -39,7 +39,7 @@ mod vcard;
pub use vcard::{make_vcard, parse_vcard, VcardContact}; pub use vcard::{make_vcard, parse_vcard, VcardContact};
/// Valid contact address. /// Valid contact address.
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContactAddress(String); pub struct ContactAddress(String);
impl Deref for ContactAddress { impl Deref for ContactAddress {

View File

@@ -236,6 +236,7 @@ impl From<Qr> for QrObject {
invitenumber, invitenumber,
authcode, authcode,
is_v3, is_v3,
..
} => { } => {
let contact_id = contact_id.to_u32(); let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.human_readable(); let fingerprint = fingerprint.human_readable();
@@ -255,6 +256,7 @@ impl From<Qr> for QrObject {
invitenumber, invitenumber,
authcode, authcode,
is_v3, is_v3,
..
} => { } => {
let contact_id = contact_id.to_u32(); let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.human_readable(); let fingerprint = fingerprint.human_readable();
@@ -276,6 +278,7 @@ impl From<Qr> for QrObject {
authcode, authcode,
invitenumber, invitenumber,
is_v3, is_v3,
..
} => { } => {
let contact_id = contact_id.to_u32(); let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.human_readable(); let fingerprint = fingerprint.human_readable();

View File

@@ -704,3 +704,25 @@ def test_withdraw_securejoin_qr(acfactory):
and "Ignoring RequestWithAuth message because of invalid auth code." in event.msg and "Ignoring RequestWithAuth message because of invalid auth code." in event.msg
): ):
break break
def test_qr_scan_updates_new_relay_address(acfactory):
alice, bob = acfactory.get_online_accounts(2)
bob_alice_chat = bob.secure_join(alice.get_qr_code())
alice.wait_for_securejoin_inviter_success()
bob.wait_for_securejoin_joiner_success()
for ac in [alice, bob]:
old_addr = ac.get_config("configured_addr")
ac.add_transport_from_qr(acfactory.get_account_qr())
ac.set_config("configured_addr", ac.list_transports()[1]["addr"])
ac.delete_transport(old_addr)
bob.secure_join(alice.get_qr_code())
alice.wait_for_securejoin_inviter_success()
bob.wait_for_securejoin_joiner_success()
bob_alice_chat.send_text("hi")
snapshot = alice.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "hi"

View File

@@ -56,6 +56,9 @@ pub enum Qr {
/// Fingerprint of the contact key as scanned from the QR code. /// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint, fingerprint: Fingerprint,
/// The inviter's addresses.
addrs: Vec<String>,
/// Invite number. /// Invite number.
invitenumber: String, invitenumber: String,
@@ -80,6 +83,9 @@ pub enum Qr {
/// Fingerprint of the contact key as scanned from the QR code. /// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint, fingerprint: Fingerprint,
/// The inviter's addresses.
addrs: Vec<String>,
/// Invite number. /// Invite number.
invitenumber: String, invitenumber: String,
@@ -108,6 +114,9 @@ pub enum Qr {
/// Fingerprint of the contact's key as scanned from the QR code. /// Fingerprint of the contact's key as scanned from the QR code.
fingerprint: Fingerprint, fingerprint: Fingerprint,
/// The inviter's addresses.
addrs: Vec<String>,
/// Invite number. /// Invite number.
invitenumber: String, invitenumber: String,
/// Authentication code. /// Authentication code.
@@ -563,6 +572,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
grpid, grpid,
contact_id, contact_id,
fingerprint, fingerprint,
addrs: vec![addr.to_string()],
invitenumber, invitenumber,
authcode, authcode,
is_v3, is_v3,
@@ -599,6 +609,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
grpid, grpid,
contact_id, contact_id,
fingerprint, fingerprint,
addrs: vec![addr.to_string()],
invitenumber, invitenumber,
authcode, authcode,
is_v3, is_v3,
@@ -624,6 +635,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
Ok(Qr::AskVerifyContact { Ok(Qr::AskVerifyContact {
contact_id, contact_id,
fingerprint, fingerprint,
addrs: vec![addr.to_string()],
invitenumber, invitenumber,
authcode, authcode,
is_v3, is_v3,

View File

@@ -741,7 +741,7 @@ pub(crate) async fn handle_securejoin_handshake(
async fn insert_into_smtp( async fn insert_into_smtp(
context: &Context, context: &Context,
rfc724_mid: &str, rfc724_mid: &str,
recipient: &str, recipients: &str,
rendered_message: String, rendered_message: String,
msg_id: MsgId, msg_id: MsgId,
) -> Result<(), Error> { ) -> Result<(), Error> {
@@ -750,7 +750,7 @@ async fn insert_into_smtp(
.execute( .execute(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
VALUES (?1, ?2, ?3, ?4)", VALUES (?1, ?2, ?3, ?4)",
(&rfc724_mid, &recipient, &rendered_message, msg_id), (&rfc724_mid, &recipients, &rendered_message, msg_id),
) )
.await?; .await?;
Ok(()) Ok(())

View File

@@ -1,19 +1,21 @@
//! Bob's side of SecureJoin handling, the joiner-side. //! Bob's side of SecureJoin handling, the joiner-side.
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use pgp::composed::SignedPublicKey;
use super::HandshakeMessage; use super::HandshakeMessage;
use super::qrinvite::QrInvite; use super::qrinvite::QrInvite;
use crate::chat::{self, ChatId, is_contact_in_chat}; use crate::chat::{self, ChatId, is_contact_in_chat};
use crate::constants::{Blocked, Chattype}; use crate::constants::{Blocked, Chattype};
use crate::contact::{Contact, Origin}; use crate::contact::Origin;
use crate::context::Context; use crate::context::Context;
use crate::events::EventType; use crate::events::EventType;
use crate::key::self_fingerprint; use crate::key::{DcKey as _, self_fingerprint};
use crate::log::LogExt; use crate::log::LogExt;
use crate::message::{self, Message, MsgId, Viewtype}; use crate::message::{self, Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::{Param, Params}; use crate::param::{Param, Params};
use crate::pgp::addresses_from_public_key;
use crate::securejoin::{ use crate::securejoin::{
ContactId, encrypted_and_signed, insert_into_smtp, verify_sender_by_fingerprint, ContactId, encrypted_and_signed, insert_into_smtp, verify_sender_by_fingerprint,
}; };
@@ -58,14 +60,28 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
QrInvite::Broadcast { .. } => {} QrInvite::Broadcast { .. } => {}
} }
let has_key = context let public_key_bytes: Option<Vec<u8>> = context
.sql .sql
.exists( .query_get_value(
"SELECT COUNT(*) FROM public_keys WHERE fingerprint=?", "SELECT public_key FROM public_keys WHERE fingerprint=?",
(invite.fingerprint().hex(),), (invite.fingerprint().hex(),),
) )
.await?; .await?;
let key_contains_all_invite_addrs = if let Some(public_key_bytes) = public_key_bytes {
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
if let Some(addrs_in_key) = addresses_from_public_key(&public_key) {
invite.addrs().iter().all(|a| addrs_in_key.contains(a))
} else {
// This can happen if the inviter is using an old version of Delta Chat
// that doesn't put the relay list into the key.
// In this case, we never take the securejoin protocol shortcut, which is fine.
false
}
} else {
false
};
// Now start the protocol and initialise the state. // Now start the protocol and initialise the state.
{ {
// `joining_chat_id` is `Some` if group chat // `joining_chat_id` is `Some` if group chat
@@ -97,7 +113,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
progress: JoinerProgress::Succeeded.into_u16(), progress: JoinerProgress::Succeeded.into_u16(),
}); });
return Ok(joining_chat_id); return Ok(joining_chat_id);
} else if has_key } else if key_contains_all_invite_addrs
&& verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()) && verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id())
.await? .await?
{ {
@@ -154,7 +170,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
QrInvite::Contact { .. } => { QrInvite::Contact { .. } => {
// For setup-contact the BobState already ensured the 1:1 chat exists because it is // For setup-contact the BobState already ensured the 1:1 chat exists because it is
// used to send the handshake messages. // used to send the handshake messages.
if !has_key { if !key_contains_all_invite_addrs {
chat::add_info_msg_with_cmd( chat::add_info_msg_with_cmd(
context, context,
private_chat_id, private_chat_id,
@@ -310,8 +326,7 @@ pub(crate) async fn send_handshake_message(
if invite.is_v3() && matches!(step, BobHandshakeMsg::Request) { if invite.is_v3() && matches!(step, BobHandshakeMsg::Request) {
// Send a minimal symmetrically-encrypted vc-request-pubkey message // Send a minimal symmetrically-encrypted vc-request-pubkey message
let rfc724_mid = create_outgoing_rfc724_mid(); let rfc724_mid = create_outgoing_rfc724_mid();
let contact = Contact::get_by_id(context, invite.contact_id()).await?; let recipients = invite.addrs().join(" ");
let recipient = contact.get_addr();
let alice_fp = invite.fingerprint().hex(); let alice_fp = invite.fingerprint().hex();
let auth = invite.authcode(); let auth = invite.authcode();
let shared_secret = format!("securejoin/{alice_fp}/{auth}"); let shared_secret = format!("securejoin/{alice_fp}/{auth}");
@@ -327,7 +342,7 @@ pub(crate) async fn send_handshake_message(
.await?; .await?;
let msg_id = message::insert_tombstone(context, &rfc724_mid).await?; let msg_id = message::insert_tombstone(context, &rfc724_mid).await?;
insert_into_smtp(context, &rfc724_mid, recipient, rendered_message, msg_id).await?; insert_into_smtp(context, &rfc724_mid, &recipients, rendered_message, msg_id).await?;
context.scheduler.interrupt_smtp().await; context.scheduler.interrupt_smtp().await;
} else { } else {
let mut msg = Message { let mut msg = Message {

View File

@@ -18,6 +18,8 @@ pub enum QrInvite {
Contact { Contact {
contact_id: ContactId, contact_id: ContactId,
fingerprint: Fingerprint, fingerprint: Fingerprint,
#[serde(default)]
addrs: Vec<String>,
invitenumber: String, invitenumber: String,
authcode: String, authcode: String,
#[serde(default)] #[serde(default)]
@@ -26,6 +28,8 @@ pub enum QrInvite {
Group { Group {
contact_id: ContactId, contact_id: ContactId,
fingerprint: Fingerprint, fingerprint: Fingerprint,
#[serde(default)]
addrs: Vec<String>,
name: String, name: String,
grpid: String, grpid: String,
invitenumber: String, invitenumber: String,
@@ -36,6 +40,8 @@ pub enum QrInvite {
Broadcast { Broadcast {
contact_id: ContactId, contact_id: ContactId,
fingerprint: Fingerprint, fingerprint: Fingerprint,
#[serde(default)]
addrs: Vec<String>,
name: String, name: String,
grpid: String, grpid: String,
invitenumber: String, invitenumber: String,
@@ -92,6 +98,14 @@ impl QrInvite {
QrInvite::Broadcast { is_v3, .. } => is_v3, QrInvite::Broadcast { is_v3, .. } => is_v3,
} }
} }
pub(crate) fn addrs(&self) -> &Vec<String> {
match self {
QrInvite::Contact { addrs, .. } => addrs,
QrInvite::Group { addrs, .. } => addrs,
QrInvite::Broadcast { addrs, .. } => addrs,
}
}
} }
impl TryFrom<Qr> for QrInvite { impl TryFrom<Qr> for QrInvite {
@@ -102,12 +116,14 @@ impl TryFrom<Qr> for QrInvite {
Qr::AskVerifyContact { Qr::AskVerifyContact {
contact_id, contact_id,
fingerprint, fingerprint,
addrs,
invitenumber, invitenumber,
authcode, authcode,
is_v3, is_v3,
} => Ok(QrInvite::Contact { } => Ok(QrInvite::Contact {
contact_id, contact_id,
fingerprint, fingerprint,
addrs,
invitenumber, invitenumber,
authcode, authcode,
is_v3, is_v3,
@@ -117,12 +133,14 @@ impl TryFrom<Qr> for QrInvite {
grpid, grpid,
contact_id, contact_id,
fingerprint, fingerprint,
addrs,
invitenumber, invitenumber,
authcode, authcode,
is_v3, is_v3,
} => Ok(QrInvite::Group { } => Ok(QrInvite::Group {
contact_id, contact_id,
fingerprint, fingerprint,
addrs,
name: grpname, name: grpname,
grpid, grpid,
invitenumber, invitenumber,
@@ -134,6 +152,7 @@ impl TryFrom<Qr> for QrInvite {
grpid, grpid,
contact_id, contact_id,
fingerprint, fingerprint,
addrs,
authcode, authcode,
invitenumber, invitenumber,
is_v3, is_v3,
@@ -142,6 +161,7 @@ impl TryFrom<Qr> for QrInvite {
grpid, grpid,
contact_id, contact_id,
fingerprint, fingerprint,
addrs,
authcode, authcode,
invitenumber, invitenumber,
is_v3, is_v3,