Skip to content
Merged
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
22 changes: 22 additions & 0 deletions deltachat-rpc-client/tests/test_cross_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,28 @@ def test_qr_setup_contact(acf, alice_and_remote_bob, version) -> None:
alice2.wait_for_securejoin_inviter_success()


@pytest.mark.parametrize("version", ["2.24.0"])
def test_qr_setup_contact_multitransport(acf, alice_and_remote_bob, version) -> None:
"""Test other-core Bob profile can do securejoin with Alice on current core, with multiple transports."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
relay_qr = acf.get_account_qr()
alice.add_transport_from_qr(relay_qr)
alice.add_transport_from_qr(relay_qr)

qr_code = alice.get_qr_code()
remote_eval(f"bob.secure_join({qr_code!r})")
alice.wait_for_securejoin_inviter_success()

# Test that Alice verified Bob's profile.
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified

remote_eval("bob.wait_for_securejoin_joiner_success()")

# Test that Bob verified Alice's profile.
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")


def test_send_and_receive_message(alice_and_remote_bob) -> None:
"""Test other-core Bob profile can send a message to Alice on current core."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.23.0")
Expand Down
32 changes: 32 additions & 0 deletions deltachat-rpc-client/tests/test_multitransport.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import urllib.parse

import pytest

from deltachat_rpc_client import EventType
Expand Down Expand Up @@ -319,3 +321,33 @@ def test_remove_primary_transport(acf, log) -> None:
assert msg2.text == "Hello again!"
assert msg2.chat.get_basic_snapshot().chat_type == ChatType.SINGLE
assert msg2.chat == alice.create_chat(bob)


def test_qr_works_after_removing_primary_transport(acf, log) -> None:
log.section("Alice setups an account and adds two additional relays")
alice = acf.new_configured_account()
relay_qr = acf.get_account_qr()
alice.add_transport_from_qr(relay_qr)
alice.add_transport_from_qr(relay_qr)

first_addr = alice.list_transports()[0]["addr"]
second_addr = alice.list_transports()[1]["addr"]
third_addr = alice.list_transports()[2]["addr"]

log.section("Alice creates a QR code")
chat_qr = alice.get_qr_code()
chat_qr_unquoted = urllib.parse.unquote(chat_qr)
assert f"&a={first_addr}" in chat_qr_unquoted
assert f"&r={third_addr},{second_addr}" in chat_qr_unquoted

log.section("Alice removes first and second transport")
alice.set_config("configured_addr", third_addr)
alice.delete_transport(first_addr)
alice.delete_transport(second_addr)

log.section("Bob scans the QR code, which still works")
alice.bring_online()
bob = acf.get_online_account()
bob.secure_join(chat_qr)
alice.wait_for_securejoin_inviter_success()
bob.wait_for_securejoin_joiner_success()
31 changes: 21 additions & 10 deletions src/qr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,8 @@ pub fn format_backup(qr: &Qr) -> Result<String> {
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&b=BROADCAST_NAME&x=BROADCAST_ID&j=INVITENUMBER&s=AUTH`
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR`
///
/// with optional `&r=ADDRS` param.
async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
let payload = qr
.get(OPENPGP4FPR_SCHEME.len()..)
Expand Down Expand Up @@ -492,10 +494,17 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
})
.collect();

let addr = if let Some(addr) = param.get("a") {
Some(normalize_address(addr)?)
} else {
None
let addrs = {
let mut addrs = Vec::new();
if let Some(primary_addr) = param.get("a") {
addrs.push(normalize_address(primary_addr)?);
};
if let Some(secondary_addrs_raw) = param.get("r") {
for secondary_address in secondary_addrs_raw.split(',') {
addrs.push(normalize_address(secondary_address)?)
}
}
addrs
};

let name = decode_name(&param, "n")?.unwrap_or_default();
Expand Down Expand Up @@ -528,7 +537,9 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
invitenumber = Some("".to_string());
}

if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
if let (Some(addr), Some(invitenumber), Some(authcode)) =
(addrs.first(), invitenumber, authcode)
{
let addr = ContactAddress::new(addr)?;
let (contact_id, _) = Contact::add_or_lookup_ext(
context,
Expand Down Expand Up @@ -571,7 +582,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
grpid,
contact_id,
fingerprint,
addrs: vec![addr.to_string()],
addrs,
invitenumber,
authcode,
is_v3,
Expand Down Expand Up @@ -608,7 +619,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
grpid,
contact_id,
fingerprint,
addrs: vec![addr.to_string()],
addrs,
invitenumber,
authcode,
is_v3,
Expand All @@ -634,16 +645,16 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
Ok(Qr::AskVerifyContact {
contact_id,
fingerprint,
addrs: vec![addr.to_string()],
addrs,
invitenumber,
authcode,
is_v3,
})
}
} else if let Some(addr) = addr {
} else if let Some(addr) = addrs.first() {
let fingerprint = fingerprint.hex();
let (contact_id, _) =
Contact::add_or_lookup_ext(context, "", &addr, &fingerprint, Origin::UnhandledQrScan)
Contact::add_or_lookup_ext(context, "", addr, &fingerprint, Origin::UnhandledQrScan)
.await?;
let contact = Contact::get_by_id(context, contact_id).await?;

Expand Down
18 changes: 15 additions & 3 deletions src/securejoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,18 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
let self_addr = context.get_primary_self_addr().await?;
let self_addr_urlencoded = utf8_percent_encode(&self_addr, DISALLOWED_CHARACTERS).to_string();

let r_param = context
.get_published_secondary_self_addrs()
.await?
.into_iter()
.reduce(|acc, addr| {
format!(
"{acc},{}",
utf8_percent_encode(&addr, DISALLOWED_CHARACTERS)
)
})
.map_or(String::default(), |addrs| format!("&r={addrs}"));

let self_name = context
.get_config(Config::Displayname)
.await?
Expand All @@ -165,11 +177,11 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
if chat.typ == Chattype::OutBroadcast {
// For historic reansons, broadcasts currently use j instead of i for the invitenumber.
format!(
"https://i.delta.chat/#{fingerprint}&v=3&x={grpid}&j={invitenumber}&s={auth}&a={self_addr_urlencoded}&n={self_name_urlencoded}&b={chat_name_urlencoded}",
"https://i.delta.chat/#{fingerprint}&v=3&x={grpid}&j={invitenumber}&s={auth}&a={self_addr_urlencoded}{r_param}&n={self_name_urlencoded}&b={chat_name_urlencoded}",
)
} else {
format!(
"https://i.delta.chat/#{fingerprint}&v=3&x={grpid}&i={invitenumber}&s={auth}&a={self_addr_urlencoded}&n={self_name_urlencoded}&g={chat_name_urlencoded}",
"https://i.delta.chat/#{fingerprint}&v=3&x={grpid}&i={invitenumber}&s={auth}&a={self_addr_urlencoded}{r_param}&n={self_name_urlencoded}&g={chat_name_urlencoded}",
)
}
} else {
Expand All @@ -182,7 +194,7 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
context.scheduler.interrupt_smtp().await;

format!(
"https://i.delta.chat/#{fingerprint}&v=3&i={invitenumber}&s={auth}&a={self_addr_urlencoded}&n={self_name_urlencoded}",
"https://i.delta.chat/#{fingerprint}&v=3&i={invitenumber}&s={auth}&a={self_addr_urlencoded}{r_param}&n={self_name_urlencoded}",
)
};

Expand Down