feat!: stop tracking contact verification

Since V2 a contact is its key, there is no address-to-key binding left to verify.

The JSONRPC and FFI APIs are unchanged and report nothing as verified.

BREAKING CHANGE: the statistics JSON sent to the self-reporting-bot on Android changes: Contacts have `encrypted` instead of `verified` and lose `transitive_chain` properties and message stats have `encrypted` instead of `verified` and `unverified_encrypted`, and securejoin invites lose `already_verified`. The collecting bot stores incoming reports verbatim but analysis will have to make sense of older and newer reports.
This commit is contained in:
holger krekel
2026-09-16 16:09:09 +02:00
parent 373f1840a6
commit d8912a98ac
34 changed files with 203 additions and 1553 deletions
+1 -2
View File
@@ -173,7 +173,7 @@ const DB_COMMANDS: [&str; 10] = [
"housekeeping", "housekeeping",
]; ];
const CHAT_COMMANDS: [&str; 39] = [ const CHAT_COMMANDS: [&str; 38] = [
"listchats", "listchats",
"listarchived", "listarchived",
"start-realtime", "start-realtime",
@@ -182,7 +182,6 @@ const CHAT_COMMANDS: [&str; 39] = [
"createchat", "createchat",
"creategroup", "creategroup",
"createbroadcast", "createbroadcast",
"createprotected",
"addmember", "addmember",
"removemember", "removemember",
"groupname", "groupname",
@@ -25,13 +25,12 @@ def test_qr_setup_contact(acf, alice_and_remote_bob, version) -> None:
remote_eval(f"bob.secure_join({qr_code!r})") remote_eval(f"bob.secure_join({qr_code!r})")
alice.wait_for_securejoin_inviter_success() alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot() alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified assert alice_contact_bob_snapshot.e2ee_avail
remote_eval("bob.wait_for_securejoin_joiner_success()") remote_eval("bob.wait_for_securejoin_joiner_success()")
# Test that Bob verified Alice's profile. # The old core still marks Alice as verified, so the handshake is unchanged on the wire.
assert remote_eval("bob_contact_alice.get_snapshot().is_verified") assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
# Test that Bob can also scan a QR code # Test that Bob can also scan a QR code
@@ -56,13 +55,12 @@ def test_qr_setup_contact_multitransport(acf, alice_and_remote_bob, version) ->
remote_eval(f"bob.secure_join({qr_code!r})") remote_eval(f"bob.secure_join({qr_code!r})")
alice.wait_for_securejoin_inviter_success() alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot() alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified assert alice_contact_bob_snapshot.e2ee_avail
remote_eval("bob.wait_for_securejoin_joiner_success()") remote_eval("bob.wait_for_securejoin_joiner_success()")
# Test that Bob verified Alice's profile. # The old core still marks Alice as verified, so the handshake is unchanged on the wire.
assert remote_eval("bob_contact_alice.get_snapshot().is_verified") assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
+35 -125
View File
@@ -4,10 +4,9 @@ import pytest
from deltachat_rpc_client import Chat, EventType, SpecialContactId from deltachat_rpc_client import Chat, EventType, SpecialContactId
from deltachat_rpc_client.const import ChatType from deltachat_rpc_client.const import ChatType
from deltachat_rpc_client.rpc import JsonRpcError
def test_qr_setup_contact(acf, tmp_path) -> None: def test_qr_setup_contact(acf) -> None:
alice, bob = acf.get_online_accounts(2) alice, bob = acf.get_online_accounts(2)
qr_code = alice.get_qr_code() qr_code = alice.get_qr_code()
@@ -15,33 +14,15 @@ def test_qr_setup_contact(acf, tmp_path) -> None:
alice.wait_for_securejoin_inviter_success() alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.create_contact(bob) alice_contact_bob = alice.create_contact(bob)
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot() alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified assert alice_contact_bob_snapshot.e2ee_avail
bob.wait_for_securejoin_joiner_success() bob.wait_for_securejoin_joiner_success()
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.create_contact(alice) bob_contact_alice = bob.create_contact(alice)
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot() bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified assert bob_contact_alice_snapshot.e2ee_avail
# Test that if Bob imports a key,
# backwards verification is not lost
# because default key is not changed.
logging.info("Bob 2 is created")
bob2 = acf.new_configured_account()
bob2.export_self_keys(tmp_path)
logging.info("Bob tries to import a key")
# Importing a second key is not allowed.
with pytest.raises(JsonRpcError):
bob.import_self_keys(tmp_path)
assert bob.get_config("key_id") == "1"
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
def test_qr_setup_contact_svg(acf) -> None: def test_qr_setup_contact_svg(acf) -> None:
@@ -81,26 +62,24 @@ def test_qr_securejoin(acf):
ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED) ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
bob.wait_for_securejoin_joiner_success() bob.wait_for_securejoin_joiner_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.create_contact(bob) alice_contact_bob = alice.create_contact(bob)
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot() alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified assert alice_contact_bob_snapshot.e2ee_avail
snapshot = bob.wait_for_incoming_msg().get_snapshot() snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "You were added by {}.".format(alice.get_config("addr")) assert snapshot.text == "You were added by {}.".format(alice.get_config("addr"))
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.create_contact(alice) bob_contact_alice = bob.create_contact(alice)
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot() bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified assert bob_contact_alice_snapshot.e2ee_avail
# Start second Alice device. # Start second Alice device.
# Alice observes securejoin protocol and verifies Bob on second device. # Alice observes the securejoin protocol on the second device.
alice2.start_io() alice2.start_io()
alice2.wait_for_securejoin_inviter_success() alice2.wait_for_securejoin_inviter_success()
alice2_contact_bob = alice2.create_contact(bob) alice2_contact_bob = alice2.create_contact(bob)
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot() alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
assert alice2_contact_bob_snapshot.is_verified assert alice2_contact_bob_snapshot.e2ee_avail
# The QR code token is synced, so alice2 must be able to handle join requests. # The QR code token is synced, so alice2 must be able to handle join requests.
logging.info("Fiona joins the group via alice2") logging.info("Fiona joins the group via alice2")
@@ -151,9 +130,9 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
assert snapshot2.chat_id == chat.id assert snapshot2.chat_id == chat.id
def check_account(ac, contact, inviter_side, please_wait_info_msg=False): def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
# Check that the chat partner is verified. # Check that the chat partner's key is known.
contact_snapshot = contact.get_snapshot() contact_snapshot = contact.get_snapshot()
assert contact_snapshot.is_verified assert contact_snapshot.e2ee_avail
chat = get_broadcast(ac) chat = get_broadcast(ac)
chat_msgs = chat.get_messages() chat_msgs = chat.get_messages()
@@ -350,8 +329,8 @@ def test_setup_contact_resetup(acf) -> None:
bob.wait_for_securejoin_joiner_success() bob.wait_for_securejoin_joiner_success()
def test_verified_group_member_added_recovery(acf) -> None: def test_group_member_added_recovery(acf) -> None:
"""Tests verified group recovery by reverifying then removing and adding a member back.""" """Tests group recovery after a member resets its key."""
ac1, ac2, ac3 = acf.get_online_accounts(3) ac1, ac2, ac3 = acf.get_online_accounts(3)
logging.info("ac1 creates a group") logging.info("ac1 creates a group")
@@ -362,11 +341,7 @@ def test_verified_group_member_added_recovery(acf) -> None:
ac2.secure_join(qr_code) ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success() ac2.wait_for_securejoin_joiner_success()
# ac1 has ac2 directly verified. logging.info("ac3 joins the group")
ac1_contact_ac2 = ac1.create_contact(ac2)
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code) ac3_chat = ac3.secure_join(qr_code)
ac3.wait_for_securejoin_joiner_success() ac3.wait_for_securejoin_joiner_success()
ac3.wait_for_incoming_msg_event() # Member added ac3.wait_for_incoming_msg_event() # Member added
@@ -376,7 +351,7 @@ def test_verified_group_member_added_recovery(acf) -> None:
logging.info("ac2 logs in on a new device") logging.info("ac2 logs in on a new device")
ac2 = acf.resetup_account(ac2) ac2 = acf.resetup_account(ac2)
logging.info("ac2 reverifies with ac3") logging.info("ac2 scans ac3's QR code again")
qr_code = ac3.get_qr_code() qr_code = ac3.get_qr_code()
ac2.secure_join(qr_code) ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success() ac2.wait_for_securejoin_joiner_success()
@@ -416,14 +391,6 @@ def test_verified_group_member_added_recovery(acf) -> None:
snapshot = ac1.wait_for_incoming_msg().get_snapshot() snapshot = ac1.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Works again!" assert snapshot.text == "Works again!"
ac1_contact_ac2 = ac1.create_contact(ac2)
ac1_contact_ac3 = ac1.create_contact(ac3)
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not ac1_contact_ac2_snapshot.is_verified
assert ac1_contact_ac2_snapshot.verifier_id != ac1_contact_ac3.id
def test_qr_join_chat_with_pending_bobstate_issue4894(acf): def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
"""Regression test for """Regression test for
@@ -431,13 +398,13 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
""" """
ac1, ac2, ac3, ac4 = acf.get_online_accounts(4) ac1, ac2, ac3, ac4 = acf.get_online_accounts(4)
logging.info("ac3: verify with ac2") logging.info("ac3: set up contact with ac2")
qr_code = ac2.get_qr_code() qr_code = ac2.get_qr_code()
ac3.secure_join(qr_code) ac3.secure_join(qr_code)
ac2.wait_for_securejoin_inviter_success() ac2.wait_for_securejoin_inviter_success()
# in order for ac2 to have pending bobstate with a verified group # in order for ac2 to have pending bobstate with a group
# we first create a fully joined verified group, and then start # we first create a fully joined group, and then start
# joining a second time but interrupt it, to create pending bob state # joining a second time but interrupt it, to create pending bob state
logging.info("ac1: create a group that ac2 fully joins") logging.info("ac1: create a group that ac2 fully joins")
@@ -446,7 +413,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
ac2.secure_join(qr_code) ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success() ac1.wait_for_securejoin_inviter_success()
# ensure ac1 can write and ac2 receives messages in verified chat # ensure ac1 can write and ac2 receives messages in the chat
ch1.send_text("ac1 says hello") ch1.send_text("ac1 says hello")
while 1: while 1:
snapshot = ac2.wait_for_incoming_msg().get_snapshot() snapshot = ac2.wait_for_incoming_msg().get_snapshot()
@@ -459,11 +426,11 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
ac1.remove() ac1.remove()
logging.info("ac2 now has pending bobstate but ac1 is shutoff") logging.info("ac2 now has pending bobstate but ac1 is shutoff")
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed # we meanwhile expect the ac3/ac2 setup-contact started in the beginning to have completed
assert ac3.create_contact(ac2).get_snapshot().is_verified assert ac3.create_contact(ac2).get_snapshot().e2ee_avail
assert ac2.create_contact(ac3).get_snapshot().is_verified assert ac2.create_contact(ac3).get_snapshot().e2ee_avail
logging.info("ac3: create a verified group VG with ac2") logging.info("ac3: create a group VG with ac2")
vg = ac3.create_group("ac3-created") vg = ac3.create_group("ac3-created")
vg.add_contact(ac3.create_contact(ac2)) vg.add_contact(ac3.create_contact(ac2))
@@ -486,7 +453,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
def test_qr_new_group_unblocked(acf): def test_qr_new_group_unblocked(acf):
"""Regression test for a bug introduced in core v1.113.0. """Regression test for a bug introduced in core v1.113.0.
ac2 scans a verified group QR code created by ac1. ac2 scans a group QR code created by ac1.
This results in creation of a blocked single chat with ac1 on ac2, This results in creation of a blocked single chat with ac1 on ac2,
but ac1 contact is not blocked on ac2. but ac1 contact is not blocked on ac2.
Then ac1 creates a group, adds ac2 there and promotes it by sending a message. Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
@@ -513,13 +480,13 @@ def test_qr_new_group_unblocked(acf):
@pytest.mark.skip(reason="AEAP is disabled for now") @pytest.mark.skip(reason="AEAP is disabled for now")
def test_aeap_flow_verified(acf): def test_aeap_flow(acf):
"""Test that a new address is added to a contact when it changes its address.""" """Test that a new address is added to a contact when it changes its address."""
ac1, ac2 = acf.get_online_accounts(2) ac1, ac2 = acf.get_online_accounts(2)
addr, password = acf.get_credentials() addr, password = acf.get_credentials()
logging.info("ac1: create verified-group QR, ac2 scans and joins") logging.info("ac1: create group QR, ac2 scans and joins")
chat = ac1.create_group("hello") chat = ac1.create_group("hello")
qr_code = chat.get_qr_code() qr_code = chat.get_qr_code()
logging.info("ac2: start QR-code based join-group protocol") logging.info("ac2: start QR-code based join-group protocol")
@@ -555,65 +522,15 @@ def test_aeap_flow_verified(acf):
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()] assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
def test_gossip_verification(acf) -> None:
alice, bob, carol = acf.get_online_accounts(3)
# Bob verifies Alice.
qr_code = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
# Bob verifies Carol.
qr_code = carol.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
bob_contact_alice = bob.create_contact(alice, "Alice")
bob_contact_carol = bob.create_contact(carol, "Carol")
carol_contact_alice = carol.create_contact(alice, "Alice")
logging.info("Bob creates an Autocrypt group")
bob_group_chat = bob.create_group("Autocrypt Group")
bob_group_chat.add_contact(bob_contact_alice)
bob_group_chat.add_contact(bob_contact_carol)
bob_group_chat.send_message(text="Hello Autocrypt group")
snapshot = carol.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello Autocrypt group"
assert snapshot.show_padlock
# Group propagates verification using Autocrypt-Gossip header.
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not carol_contact_alice_snapshot.is_verified
logging.info("Bob creates a Securejoin group")
bob_group_chat = bob.create_group("Securejoin Group")
bob_group_chat.add_contact(bob_contact_alice)
bob_group_chat.add_contact(bob_contact_carol)
bob_group_chat.send_message(text="Hello Securejoin group")
snapshot = carol.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello Securejoin group"
assert snapshot.show_padlock
# Securejoin propagates verification.
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not carol_contact_alice_snapshot.is_verified
def test_securejoin_after_contact_resetup(acf) -> None: def test_securejoin_after_contact_resetup(acf) -> None:
""" """
Regression test for a bug that prevented joining verified group with a QR code Regression test for a bug that prevented joining a group with a QR code
if the group is already created and contains if the group already contains a contact with an inconsistent key state
a contact with inconsistent (Autocrypt and verified keys exist but don't match) key state. while a securejoin with that contact is still pending.
""" """
ac1, ac2, ac3 = acf.get_online_accounts(3) ac1, ac2, ac3 = acf.get_online_accounts(3)
# ac3 creates protected group with ac1. # ac3 creates a group with ac1.
ac3_chat = ac3.create_group("Group") ac3_chat = ac3.create_group("Group")
# ac1 joins ac3 group. # ac1 joins ac3 group.
@@ -626,28 +543,24 @@ def test_securejoin_after_contact_resetup(acf) -> None:
assert snapshot.text == "You were added by {}.".format(ac3.get_config("addr")) assert snapshot.text == "You were added by {}.".format(ac3.get_config("addr"))
ac1_qr_code = snapshot.chat.get_qr_code() ac1_qr_code = snapshot.chat.get_qr_code()
# ac2 verifies ac1 # ac2 sets up contact with ac1
qr_code = ac1.get_qr_code() qr_code = ac1.get_qr_code()
ac2.secure_join(qr_code) ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success() ac2.wait_for_securejoin_joiner_success()
# ac1 is verified for ac2.
ac2_contact_ac1 = ac2.create_contact(ac1, "") ac2_contact_ac1 = ac2.create_contact(ac1, "")
assert ac2_contact_ac1.get_snapshot().is_verified assert ac2_contact_ac1.get_snapshot().e2ee_avail
# ac1 resetups the account. # ac1 resetups the account.
ac1 = acf.resetup_account(ac1) ac1 = acf.resetup_account(ac1)
ac2_contact_ac1 = ac2.create_contact(ac1, "")
assert not ac2_contact_ac1.get_snapshot().is_verified
# ac1 goes offline. # ac1 goes offline.
ac1.remove() ac1.remove()
# Scanning a QR code results in creating an unprotected group with an inviter. # Scanning a QR code creates a group with the inviter, here ac1,
# In this case inviter is ac1 which has an inconsistent key state. # which has an inconsistent key state. Normally the securejoin protocol
# Normally inviter becomes verified as a result of Securejoin protocol # would complete and "Member added" would arrive,
# and then the group chat becomes verified when "Member added" is received, # but ac1 is offline so it never finishes.
# but in this case ac1 is offline and this Securejoin process will never finish.
logging.info("ac2 scans ac1 QR code, this is not expected to finish") logging.info("ac2 scans ac1 QR code, this is not expected to finish")
ac2.secure_join(ac1_qr_code) ac2.secure_join(ac1_qr_code)
@@ -664,16 +577,13 @@ def test_securejoin_after_contact_resetup(acf) -> None:
ac2_chat = snapshot.chat ac2_chat = snapshot.chat
assert len(ac2_chat.get_contacts()) == 3 assert len(ac2_chat.get_contacts()) == 3
# ac1 is still "not verified" for ac2 due to inconsistent state.
assert not ac2_contact_ac1.get_snapshot().is_verified
def test_withdraw_securejoin_qr(acf): def test_withdraw_securejoin_qr(acf):
alice, bob = acf.get_online_accounts(2) alice, bob = acf.get_online_accounts(2)
logging.info("Alice creates a group") logging.info("Alice creates a group")
alice_chat = alice.create_group("Group") alice_chat = alice.create_group("Group")
logging.info("Bob joins verified group") logging.info("Bob joins the group")
qr_code = alice_chat.get_qr_code() qr_code = alice_chat.get_qr_code()
bob_chat = bob.secure_join(qr_code) bob_chat = bob.secure_join(qr_code)
+5 -8
View File
@@ -72,8 +72,7 @@ CREATE TABLE contacts (
-- empty string for "address-contacts". -- empty string for "address-contacts".
fingerprint TEXT NOT NULL DEFAULT '', fingerprint TEXT NOT NULL DEFAULT '',
-- ID of the contact that has "introduced" us to this contact -- Unused. Was the ID of the contact that introduced this contact's key.
-- by sharing the key with a verified attribute or in a "verified" chat.
verifier INTEGER NOT NULL DEFAULT 0 verifier INTEGER NOT NULL DEFAULT 0
); );
CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE); CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE);
@@ -131,9 +130,7 @@ CREATE TABLE chats (
-- 0 means the timer is disabled. -- 0 means the timer is disabled.
ephemeral_timer INTEGER, ephemeral_timer INTEGER,
-- Deprecated, but still used to send Chat-Verified headers -- Unused. Was 1 for protected chats.
-- for existing protected chats.
-- All new chats are created as "not protected".
protected INTEGER DEFAULT 0, protected INTEGER DEFAULT 0,
gossiped_timestamp INTEGER DEFAULT 0, -- deprecated 2025-04-08, replaced with gossip_timestamp table gossiped_timestamp INTEGER DEFAULT 0, -- deprecated 2025-04-08, replaced with gossip_timestamp table
@@ -730,13 +727,13 @@ CREATE TABLE stats_securejoin_uipaths(
) STRICT; ) STRICT;
CREATE TABLE stats_securejoin_invites( CREATE TABLE stats_securejoin_invites(
already_existed INTEGER NOT NULL, already_existed INTEGER NOT NULL,
already_verified INTEGER NOT NULL, already_verified INTEGER NOT NULL, -- unused, always 0
type TEXT NOT NULL type TEXT NOT NULL
) STRICT; ) STRICT;
CREATE TABLE stats_msgs( CREATE TABLE stats_msgs(
chattype INTEGER PRIMARY KEY, chattype INTEGER PRIMARY KEY,
verified INTEGER NOT NULL DEFAULT 0, verified INTEGER NOT NULL DEFAULT 0, -- unused, always 0
unverified_encrypted INTEGER NOT NULL DEFAULT 0, unverified_encrypted INTEGER NOT NULL DEFAULT 0, -- counts all encrypted messages
unencrypted INTEGER NOT NULL DEFAULT 0, unencrypted INTEGER NOT NULL DEFAULT 0,
only_to_self INTEGER NOT NULL DEFAULT 0, only_to_self INTEGER NOT NULL DEFAULT 0,
last_counted_msg_id INTEGER NOT NULL DEFAULT 0 last_counted_msg_id INTEGER NOT NULL DEFAULT 0
+11 -27
View File
@@ -1,7 +1,5 @@
import time import time
import deltachat as dc
class TestGroupStressTests: class TestGroupStressTests:
def test_group_many_members_add_leave_remove(self, acfactory, lp): def test_group_many_members_add_leave_remove(self, acfactory, lp):
@@ -63,9 +61,9 @@ class TestGroupStressTests:
assert msg.is_encrypted() assert msg.is_encrypted()
def test_qr_verified_group_and_chatting(acfactory, lp): def test_qr_group_join_and_chatting(acfactory, lp):
ac1, ac2, ac3 = acfactory.get_online_accounts(3) ac1, ac2, ac3 = acfactory.get_online_accounts(3)
lp.sec("ac1: create verified-group QR, ac2 scans and joins") lp.sec("ac1: create group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello") chat1 = ac1.create_group_chat("hello")
qr = chat1.get_join_qr() qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol") lp.sec("ac2: start QR-code based join-group protocol")
@@ -86,15 +84,11 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
msg_out = chat1.send_text("hello") msg_out = chat1.send_text("hello")
assert msg_out.is_encrypted() assert msg_out.is_encrypted()
lp.sec("ac2: read message and check that it's a verified chat") lp.sec("ac2: read message and check that it is encrypted")
msg = ac2._evtracker.wait_next_incoming_message() msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello" assert msg.text == "hello"
assert msg.is_encrypted() assert msg.is_encrypted()
lp.sec("ac2: Check that ac2 verified ac1")
ac2_ac1_contact = ac2.get_contacts()[0]
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
lp.sec("ac2: send message and let ac1 read it") lp.sec("ac2: send message and let ac1 read it")
chat2.send_text("world") chat2.send_text("world")
msg = ac1._evtracker.wait_next_incoming_message() msg = ac1._evtracker.wait_next_incoming_message()
@@ -109,23 +103,13 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert ch.id >= 10 assert ch.id >= 10
ac1._evtracker.wait_securejoin_inviter_progress(1000) ac1._evtracker.wait_securejoin_inviter_progress(1000)
lp.sec("ac1: add ac3 to verified group") lp.sec("ac1: add ac3 to the group")
chat1.add_contact(ac3) chat1.add_contact(ac3)
msg = ac2._evtracker.wait_next_incoming_message() msg = ac2._evtracker.wait_next_incoming_message()
assert msg.is_encrypted() assert msg.is_encrypted()
assert msg.is_system_message() assert msg.is_system_message()
assert not msg.error assert not msg.error
lp.sec("ac2: Check that ac1 verified ac3 for ac2")
ac2_ac1_contact = ac2.get_contacts()[0]
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
for ac2_contact in chat2.get_contacts():
if ac2_contact == ac2_ac1_contact or ac2_contact.id == dc.const.DC_CONTACT_ID_SELF:
continue
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert ac2.get_self_contact().get_verifier(ac2_contact) is None
lp.sec("ac2: send message and let ac3 read it") lp.sec("ac2: send message and let ac3 read it")
chat2.send_text("hi") chat2.send_text("hi")
# System message about the added member. # System message about the added member.
@@ -195,10 +179,10 @@ def test_ephemeral_timer(acfactory, lp):
assert chat1.get_ephemeral_timer() == 0 assert chat1.get_ephemeral_timer() == 0
def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp): def test_see_new_member_after_going_online(acfactory, tmp_path, lp):
"""The test for the bug #3836: """The test for the bug #3836:
- Alice has two devices, the second is offline. - Alice has two devices, the second is offline.
- Alice creates a verified group and sends a QR invitation to Bob. - Alice creates a group and sends a QR invitation to Bob.
- Bob joins the group and sends a message there. Alice sees it. - Bob joins the group and sends a message there. Alice sees it.
- Alice's second devices goes online, but doesn't see Bob in the group. - Alice's second devices goes online, but doesn't see Bob in the group.
""" """
@@ -215,7 +199,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
ac1_offl.import_self_keys(str(dir)) ac1_offl.import_self_keys(str(dir))
ac1_offl.stop_io() ac1_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins") lp.sec("ac1: create group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello") chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr() qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol") lp.sec("ac2: start QR-code based join-group protocol")
@@ -242,12 +226,12 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
assert msg_in.get_sender_contact().addr == ac2_addr assert msg_in.get_sender_contact().addr == ac2_addr
def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp): def test_use_new_group_after_going_online(acfactory, data, tmp_path, lp):
"""Another test for the bug #3836: """Another test for the bug #3836:
- Bob has two devices, the second is offline. - Bob has two devices, the second is offline.
- Alice creates a verified group and sends a QR invitation to Bob. - Alice creates a group and sends a QR invitation to Bob.
- Bob joins the group. - Bob joins the group.
- Bob's second devices goes online, but sees a contact request instead of the verified group. - Bob's second devices goes online, but sees a contact request instead of the group.
- The "member added" message is not a system message but a plain text message. - The "member added" message is not a system message but a plain text message.
- Bob's second device doesn't display the Alice's avatar (bug #5354). - Bob's second device doesn't display the Alice's avatar (bug #5354).
- Sending a message fails as the key is missing -- message info says "proper enc-key for <Alice> - Sending a message fails as the key is missing -- message info says "proper enc-key for <Alice>
@@ -269,7 +253,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
avatar_path = data.get_path("d.png") avatar_path = data.get_path("d.png")
ac1.set_avatar(avatar_path) ac1.set_avatar(avatar_path)
lp.sec("ac1: create verified-group QR, ac2 scans and joins") lp.sec("ac1: create group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello") chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr() qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol") lp.sec("ac2: start QR-code based join-group protocol")
+2 -9
View File
@@ -796,9 +796,8 @@ def test_send_and_receive_image(acfactory, lp, data):
def test_qr_email_capitalization(acfactory, lp): def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug """Tests joining a group via QR code
that resulted in failure to propagate verification when the database already contains a contact with a different email address capitalization.
when the database already contained the contact with a different email address capitalization.
""" """
ac1, ac2, ac3 = acfactory.get_online_accounts(3) ac1, ac2, ac3 = acfactory.get_online_accounts(3)
@@ -822,13 +821,7 @@ def test_qr_email_capitalization(acfactory, lp):
ac2.qr_join_chat(qr) ac2.qr_join_chat(qr)
ac1._evtracker.wait_next_incoming_message() ac1._evtracker.wait_next_incoming_message()
# ac1 should see both ac3 and ac2 as verified.
assert len(ac1_chat.get_contacts()) == 3 assert len(ac1_chat.get_contacts()) == 3
# Until we reset verifications and then send the _verified header,
# the verification of ac2 is not gossiped here:
for contact in ac1_chat.get_contacts():
is_ac2 = contact.addr == ac2.get_config("addr")
assert contact.is_verified() != is_ac2
def test_set_get_contact_avatar(acfactory, data, lp): def test_set_get_contact_avatar(acfactory, data, lp):
-18
View File
@@ -596,24 +596,6 @@ and e.g. simply search for the line starting with `EMAIL`
in order to get the email address. in order to get the email address.
# Verifications
Keys obtained using [SecureJoin](https://securejoin.readthedocs.io) protocol
and corresponding contacts
are considered "verified".
As an extension to `Autocrypt-Gossip` header,
chatmail clients can add `_verified=1` attribute
(underscore marks the attribute as non-critical)
to indicate that they have the gossiped key
and the corresponding contact marked as verified.
When receiving such `Autocrypt-Gossip` header
in a message signed by a verified key,
chatmail clients mark the gossiped key
as indirectly verified.
# Miscellaneous # Miscellaneous
Messengers SHOULD use the header `In-Reply-To` as usual. Messengers SHOULD use the header `In-Reply-To` as usual.
-34
View File
@@ -43,13 +43,6 @@ pub struct Aheader {
pub addr: String, pub addr: String,
pub public_key: SignedPublicKey, pub public_key: SignedPublicKey,
pub prefer_encrypt: EncryptPreference, pub prefer_encrypt: EncryptPreference,
/// Whether `_verified` attribute is present.
///
/// `_verified` attribute is an extension to `Autocrypt-Gossip`
/// header that is used to tell that the sender
/// marked this key as verified.
pub verified: bool,
} }
impl fmt::Display for Aheader { impl fmt::Display for Aheader {
@@ -58,12 +51,6 @@ impl fmt::Display for Aheader {
if self.prefer_encrypt == EncryptPreference::Mutual { if self.prefer_encrypt == EncryptPreference::Mutual {
write!(fmt, " prefer-encrypt=mutual;")?; write!(fmt, " prefer-encrypt=mutual;")?;
} }
// TODO After we reset all existing verifications,
// we want to start sending the _verified attribute
// if self.verified {
// write!(fmt, " _verified=1;")?;
// }
// adds a whitespace every 78 characters, this allows // adds a whitespace every 78 characters, this allows
// email crate to wrap the lines according to RFC 5322 // email crate to wrap the lines according to RFC 5322
// (which may insert a linebreak before every whitespace) // (which may insert a linebreak before every whitespace)
@@ -114,8 +101,6 @@ impl Aheader {
.and_then(|raw| EncryptPreference::new(&raw).ok()) .and_then(|raw| EncryptPreference::new(&raw).ok())
.unwrap_or_default(); .unwrap_or_default();
let verified = attributes.remove("_verified").is_some();
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored // Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
// Autocrypt-Level0: unknown attribute, treat the header as invalid // Autocrypt-Level0: unknown attribute, treat the header as invalid
if attributes.keys().any(|k| !k.starts_with('_')) { if attributes.keys().any(|k| !k.starts_with('_')) {
@@ -126,7 +111,6 @@ impl Aheader {
addr, addr,
public_key, public_key,
prefer_encrypt, prefer_encrypt,
verified,
}) })
} }
} }
@@ -145,7 +129,6 @@ mod tests {
assert_eq!(h.addr, "me@mail.com"); assert_eq!(h.addr, "me@mail.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual); assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
assert_eq!(h.verified, false);
Ok(()) Ok(())
} }
@@ -243,7 +226,6 @@ mod tests {
addr: "test@example.com".to_string(), addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(), public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::Mutual, prefer_encrypt: EncryptPreference::Mutual,
verified: false
} }
) )
.contains("prefer-encrypt=mutual;") .contains("prefer-encrypt=mutual;")
@@ -259,7 +241,6 @@ mod tests {
addr: "test@example.com".to_string(), addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(), public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::NoPreference, prefer_encrypt: EncryptPreference::NoPreference,
verified: false
} }
) )
.contains("prefer-encrypt") .contains("prefer-encrypt")
@@ -273,24 +254,9 @@ mod tests {
addr: "TeSt@eXaMpLe.cOm".to_string(), addr: "TeSt@eXaMpLe.cOm".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(), public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::Mutual, prefer_encrypt: EncryptPreference::Mutual,
verified: false
} }
) )
.contains("test@example.com") .contains("test@example.com")
); );
// We don't send the _verified header yet:
assert!(
!format!(
"{}",
Aheader {
addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::NoPreference,
verified: true
}
)
.contains("_verified")
);
} }
} }
+1 -1
View File
@@ -2683,7 +2683,7 @@ async fn prepare_send_msg(
let skip_fn = |reason: &CantSendReason| match reason { let skip_fn = |reason: &CantSendReason| match reason {
CantSendReason::ContactRequest => { CantSendReason::ContactRequest => {
// Allow securejoin messages, they are supposed to repair the verification. // Allow securejoin messages.
// If the chat is a contact request, let the user accept it later. // If the chat is a contact request, let the user accept it later.
msg.param.get_cmd() == SystemMessage::SecurejoinMessage msg.param.get_cmd() == SystemMessage::SecurejoinMessage
} }
+4 -28
View File
@@ -3759,25 +3759,6 @@ async fn test_broadcast_joining_golden() -> Result<()> {
) )
.await; .await;
assert_eq!(
alice_bob_contact
.get_verifier_id(alice)
.await?
.unwrap()
.unwrap(),
ContactId::SELF
);
let bob_alice_contact = bob.add_or_lookup_contact_no_key(alice).await;
assert_eq!(
bob_alice_contact
.get_verifier_id(bob)
.await?
.unwrap()
.unwrap(),
ContactId::SELF
);
Ok(()) Ok(())
} }
@@ -4060,10 +4041,9 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
bob1.recv_msg(&member_added).await; bob1.recv_msg(&member_added).await;
// The single chat should not be visible to the user on any of the devices. // The single chat should not be visible to the user on any of the devices.
// The contact should be marked as verified. check_single_chat_is_hidden(alice, bob0).await;
check_single_chat_is_hidden_and_contact_is_verified(alice, bob0).await; check_single_chat_is_hidden(bob0, alice).await;
check_single_chat_is_hidden_and_contact_is_verified(bob0, alice).await; check_single_chat_is_hidden(bob1, alice).await;
check_single_chat_is_hidden_and_contact_is_verified(bob1, alice).await;
tcm.section("Alice sends first message to broadcast."); tcm.section("Alice sends first message to broadcast.");
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await; let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
@@ -4090,10 +4070,7 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
Ok(()) Ok(())
} }
async fn check_single_chat_is_hidden_and_contact_is_verified( async fn check_single_chat_is_hidden(t: &TestContext, contact: &TestContext) {
t: &TestContext,
contact: &TestContext,
) {
let contact = t.add_or_lookup_contact_no_key(contact).await; let contact = t.add_or_lookup_contact_no_key(contact).await;
if let Some(single_chat) = ChatIdBlocked::lookup_by_contact(t, contact.id) if let Some(single_chat) = ChatIdBlocked::lookup_by_contact(t, contact.id)
.await .await
@@ -4101,7 +4078,6 @@ async fn check_single_chat_is_hidden_and_contact_is_verified(
{ {
assert_eq!(single_chat.blocked, Blocked::Yes); assert_eq!(single_chat.blocked, Blocked::Yes);
} }
assert!(contact.is_verified(t).await.unwrap());
} }
/// Test that only the owner of the broadcast channel /// Test that only the owner of the broadcast channel
+1 -65
View File
@@ -38,9 +38,7 @@ use crate::param::{Param, Params};
use crate::pgp::{addresses_from_public_key, merge_openpgp_certificates}; use crate::pgp::{addresses_from_public_key, merge_openpgp_certificates};
use crate::sync::{self, Sync::*}; use crate::sync::{self, Sync::*};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, normalize_text, time, to_lowercase}; use crate::tools::{SystemTime, duration_to_str, get_abs_path, normalize_text, time, to_lowercase};
use crate::{ use crate::{chat, chatlist_events, ensure_and_debug_assert, stock_str};
chat, chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_ne, stock_str,
};
/// Time during which a contact is considered as seen recently. /// Time during which a contact is considered as seen recently.
const SEEN_RECENTLY_SECONDS: i64 = 600; const SEEN_RECENTLY_SECONDS: i64 = 600;
@@ -1997,68 +1995,6 @@ pub(crate) async fn update_last_seen(
Ok(()) Ok(())
} }
/// Marks contact `contact_id` as verified by `verifier_id`.
///
/// `verifier_id == None` means that the verifier is unknown.
pub(crate) async fn mark_contact_id_as_verified(
context: &Context,
contact_id: ContactId,
verifier_id: Option<ContactId>,
) -> Result<()> {
ensure_and_debug_assert_ne!(contact_id, ContactId::SELF,);
ensure_and_debug_assert_ne!(
Some(contact_id),
verifier_id,
"Contact cannot be verified by self",
);
let by_self = verifier_id == Some(ContactId::SELF);
let mut verifier_id = verifier_id.unwrap_or(contact_id);
context
.sql
.transaction(|transaction| {
let contact_fingerprint: String = transaction.query_row(
"SELECT fingerprint FROM contacts WHERE id=?",
(contact_id,),
|row| row.get(0),
)?;
if contact_fingerprint.is_empty() {
bail!("Non-key-contact {contact_id} cannot be verified");
}
if verifier_id != ContactId::SELF {
let (verifier_fingerprint, verifier_verifier_id): (String, ContactId) = transaction
.query_row(
"SELECT fingerprint, verifier FROM contacts WHERE id=?",
(verifier_id,),
|row| Ok((row.get(0)?, row.get(1)?)),
)?;
if verifier_fingerprint.is_empty() {
bail!(
"Contact {contact_id} cannot be verified by non-key-contact {verifier_id}"
);
}
ensure!(
verifier_id == contact_id || verifier_verifier_id != ContactId::UNDEFINED,
"Contact {contact_id} cannot be verified by unverified contact {verifier_id}",
);
if verifier_verifier_id == verifier_id {
// Avoid introducing incorrect reverse chains: if the verifier itself has an
// unknown verifier, it may be `contact_id` actually (directly or indirectly) on
// the other device (which is needed for getting "verified by unknown contact"
// in the first place).
verifier_id = contact_id;
}
}
transaction.execute(
"UPDATE contacts SET verifier=?1
WHERE id=?2 AND (verifier=0 OR verifier=id OR ?3)",
(verifier_id, contact_id, by_self),
)?;
Ok(())
})
.await?;
Ok(())
}
fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) { fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) {
*ret += &format!("\n\n{name} ({addr}):\n{fingerprint}"); *ret += &format!("\n\n{name} ({addr}):\n{fingerprint}");
} }
+1 -24
View File
@@ -1171,27 +1171,6 @@ async fn test_lookup_id_by_addr_recent_accepted() -> Result<()> {
test_lookup_id_by_addr_recent_ext(accept_unencrypted_chat).await test_lookup_id_by_addr_recent_ext(accept_unencrypted_chat).await
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_by_none() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
let contact = Contact::get_by_id(&alice, contact_id).await?;
assert!(contact.get_verifier_id(&alice).await?.is_none());
// Receive a message from Bob to save the public key.
let chat = bob.create_chat(&alice).await;
let sent_msg = bob.send_text(chat.id, "moin").await;
alice.recv_msg(&sent_msg).await;
let contact = Contact::get_by_id(&alice, contact_id).await?;
assert!(contact.get_verifier_id(&alice).await?.is_none());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_create() -> Result<()> { async fn test_sync_create() -> Result<()> {
let alice0 = &TestContext::new_alice().await; let alice0 = &TestContext::new_alice().await;
@@ -1391,13 +1370,11 @@ async fn test_import_vcard_key_change() -> Result<()> {
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_self_is_verified() -> Result<()> { async fn test_self_is_key_contact() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
let alice = tcm.alice().await; let alice = tcm.alice().await;
let contact = Contact::get_by_id(&alice, ContactId::SELF).await?; let contact = Contact::get_by_id(&alice, ContactId::SELF).await?;
assert_eq!(contact.is_verified(&alice).await?, true);
assert!(contact.get_verifier_id(&alice).await?.is_none());
assert!(contact.is_key_contact()); assert!(contact.is_key_contact());
Ok(()) Ok(())
+1 -3
View File
@@ -214,7 +214,6 @@ pub enum EventType {
}, },
/// Chat changed. The name or the image of a chat group was changed or members were added or removed. /// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat() /// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
/// and dc_remove_contact_from_chat(). /// and dc_remove_contact_from_chat().
/// ///
@@ -302,8 +301,7 @@ pub enum EventType {
contact_id: ContactId, contact_id: ContactId,
/// Progress as: /// Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself." /// 400=vg-/vc-request-with-auth sent, typically shown as "introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
/// 1000=vg-member-added/vc-contact-confirm received /// 1000=vg-member-added/vc-contact-confirm received
progress: u16, progress: u16,
}, },
-1
View File
@@ -63,7 +63,6 @@ pub enum HeaderDef {
ChatGroupDescription, ChatGroupDescription,
ChatGroupDescriptionChanged, ChatGroupDescriptionChanged,
ChatGroupDescriptionTimestamp, ChatGroupDescriptionTimestamp,
ChatVerified,
ChatGroupAvatar, ChatGroupAvatar,
ChatUserAvatar, ChatUserAvatar,
ChatVoiceMessage, ChatVoiceMessage,
-50
View File
@@ -436,7 +436,6 @@ pub(crate) fn render_queued_mail(
addr: from_addr, addr: from_addr,
public_key: public_key.clone(), public_key: public_key.clone(),
prefer_encrypt: EncryptPreference::Mutual, prefer_encrypt: EncryptPreference::Mutual,
verified: false,
}; };
let autocrypt_header = mail_builder::headers::raw::Raw::new(aheader.to_string()); let autocrypt_header = mail_builder::headers::raw::Raw::new(aheader.to_string());
add_header( add_header(
@@ -1563,17 +1562,6 @@ impl MimeFactory {
.is_none_or(|ts| now >= ts + gossip_period || now < ts) .is_none_or(|ts| now >= ts + gossip_period || now < ts)
}; };
let verifier_id: Option<u32> = context
.sql
.query_get_value(
"SELECT verifier FROM contacts WHERE fingerprint=?",
(&fingerprint,),
)
.await?;
let is_verified =
verifier_id.is_some_and(|verifier_id| verifier_id != 0);
if !should_do_gossip { if !should_do_gossip {
continue; continue;
} }
@@ -1584,7 +1572,6 @@ impl MimeFactory {
// Autocrypt 1.1.0 specification says that // Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included. // `prefer-encrypt` attribute SHOULD NOT be included.
prefer_encrypt: EncryptPreference::NoPreference, prefer_encrypt: EncryptPreference::NoPreference,
verified: is_verified,
} }
.to_string(); .to_string();
@@ -1728,43 +1715,6 @@ impl MimeFactory {
let command = msg.param.get_cmd(); let command = msg.param.get_cmd();
let mut placeholdertext = None; let mut placeholdertext = None;
let send_verified_headers = match chat.typ {
Chattype::Single => true,
Chattype::Group => true,
// Mailinglists and broadcast channels can actually never be verified:
Chattype::Mailinglist => false,
Chattype::OutBroadcast | Chattype::InBroadcast => false,
};
if send_verified_headers {
let was_protected: bool = context
.sql
.query_get_value("SELECT protected FROM chats WHERE id=?", (chat.id,))
.await?
.unwrap_or_default();
if was_protected {
let unverified_member_exists = context
.sql
.exists(
"SELECT COUNT(*)
FROM contacts, chats_contacts
WHERE chats_contacts.contact_id=contacts.id AND chats_contacts.chat_id=?
AND contacts.id>9
AND contacts.verifier=0",
(chat.id,),
)
.await?;
if !unverified_member_exists {
headers.push((
"Chat-Verified",
mail_builder::headers::raw::Raw::new("1").into(),
));
}
}
}
if chat.typ == Chattype::Group { if chat.typ == Chattype::Group {
// Send group ID unless it is an ad hoc group that has no ID. // Send group ID unless it is an ad hoc group that has no ID.
if !chat.grpid.is_empty() { if !chat.grpid.is_empty() {
+5 -21
View File
@@ -34,17 +34,6 @@ use crate::sync::SyncItems;
use crate::tools::{get_filemeta, parse_receive_headers, time, truncate_msg_text, validate_id}; use crate::tools::{get_filemeta, parse_receive_headers, time, truncate_msg_text, validate_id};
use crate::{chatlist_events, location, tools}; use crate::{chatlist_events, location, tools};
/// Public key extracted from `Autocrypt-Gossip`
/// header with associated information.
#[derive(Debug)]
pub struct GossipedKey {
/// Public key extracted from `keydata` attribute.
pub public_key: SignedPublicKey,
/// True if `Autocrypt-Gossip` has a `_verified` attribute.
pub verified: bool,
}
/// A parsed MIME message. /// A parsed MIME message.
/// ///
/// This represents the relevant information of a parsed MIME message /// This represents the relevant information of a parsed MIME message
@@ -99,7 +88,7 @@ pub(crate) struct MimeMessage {
/// The addresses for which there was a gossip header /// The addresses for which there was a gossip header
/// and their respective gossiped keys. /// and their respective gossiped keys.
pub gossiped_keys: BTreeMap<String, GossipedKey>, pub gossiped_keys: BTreeMap<String, SignedPublicKey>,
/// Fingerprint of the key in the Autocrypt header. /// Fingerprint of the key in the Autocrypt header.
/// ///
@@ -1048,7 +1037,7 @@ impl MimeMessage {
}; };
self.gossiped_keys self.gossiped_keys
.values() .values()
.map(|gossiped_key| gossiped_key.public_key.dc_fingerprint().hex()) .map(|gossiped_key| gossiped_key.dc_fingerprint().hex())
.chain(sender_fingerprint) .chain(sender_fingerprint)
.collect() .collect()
} }
@@ -2118,9 +2107,9 @@ fn remove_header(
async fn parse_gossip_headers( async fn parse_gossip_headers(
context: &Context, context: &Context,
gossip_headers: Vec<String>, gossip_headers: Vec<String>,
) -> Result<BTreeMap<String, GossipedKey>> { ) -> Result<BTreeMap<String, SignedPublicKey>> {
// XXX split the parsing from the modification part // XXX split the parsing from the modification part
let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default(); let mut gossiped_keys: BTreeMap<String, SignedPublicKey> = Default::default();
for value in &gossip_headers { for value in &gossip_headers {
let header = match Aheader::from_str(value) { let header = match Aheader::from_str(value) {
@@ -2135,12 +2124,7 @@ async fn parse_gossip_headers(
.await .await
.context("Failed to import Autocrypt-Gossip key")?; .context("Failed to import Autocrypt-Gossip key")?;
let gossiped_key = GossipedKey { gossiped_keys.insert(header.addr.to_lowercase(), header.public_key);
public_key: header.public_key,
verified: header.verified,
};
gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key);
} }
Ok(gossiped_keys) Ok(gossiped_keys)
+2 -2
View File
@@ -45,7 +45,7 @@ pub(crate) const DCBACKUP_VERSION: i32 = 5;
/// Scanned QR code. /// Scanned QR code.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Qr { pub enum Qr {
/// Ask the user whether to verify the contact. /// Ask the user whether to start chatting with the contact.
/// ///
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`]. /// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
AskVerifyContact { AskVerifyContact {
@@ -125,7 +125,7 @@ pub enum Qr {
is_v3: bool, is_v3: bool,
}, },
/// Contact fingerprint is verified. /// Contact fingerprint matches.
/// ///
/// Ask the user if they want to start chatting. /// Ask the user if they want to start chatting.
FprOk { FprOk {
+4 -4
View File
@@ -85,15 +85,15 @@ pub fn create_qr_svg(qrcode_content: &str) -> Result<String> {
Ok(svg) Ok(svg)
} }
/// Returns SVG of the QR code to join the group or verify contact. /// Returns SVG of the QR code to join the group or set up a contact.
/// ///
/// If `chat_id` is `None`, returns verification QR code. /// If `chat_id` is `None`, returns setup contact QR code.
/// Otherwise, returns secure join QR code. /// Otherwise, returns secure join QR code.
pub async fn get_securejoin_qr_svg(context: &Context, chat_id: Option<ChatId>) -> Result<String> { pub async fn get_securejoin_qr_svg(context: &Context, chat_id: Option<ChatId>) -> Result<String> {
if let Some(chat_id) = chat_id { if let Some(chat_id) = chat_id {
generate_join_group_qr_code(context, chat_id).await generate_join_group_qr_code(context, chat_id).await
} else { } else {
generate_verification_qr(context).await generate_setup_contact_qr(context).await
} }
} }
@@ -127,7 +127,7 @@ async fn generate_join_group_qr_code(context: &Context, chat_id: ChatId) -> Resu
) )
} }
async fn generate_verification_qr(context: &Context) -> Result<String> { async fn generate_setup_contact_qr(context: &Context) -> Result<String> {
let (avatar, displayname, addr, color) = self_info(context).await?; let (avatar, displayname, addr, color) = self_info(context).await?;
inner_generate_secure_join_qr_code( inner_generate_secure_join_qr_code(
+7 -97
View File
@@ -19,7 +19,7 @@ use crate::chat::{
}; };
use crate::config::Config; use crate::config::Config;
use crate::constants::{Blocked, Chattype, EDITED_PREFIX}; use crate::constants::{Blocked, Chattype, EDITED_PREFIX};
use crate::contact::{self, Contact, ContactId, Origin, mark_contact_id_as_verified}; use crate::contact::{self, Contact, ContactId, Origin};
use crate::context::Context; use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc_inner; use crate::debug_logging::maybe_set_logging_xdc_inner;
use crate::download::{DownloadState, msg_is_downloaded_for}; use crate::download::{DownloadState, msg_is_downloaded_for};
@@ -27,7 +27,7 @@ use crate::ephemeral::{Timer as EphemeralTimer, stock_ephemeral_timer_changed};
use crate::events::EventType; use crate::events::EventType;
use crate::headerdef::HeaderDef; use crate::headerdef::HeaderDef;
use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table}; use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table};
use crate::key::{DcKey, Fingerprint}; use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::key::{ use crate::key::{
load_self_public_key, load_self_public_key_opt, self_fingerprint, self_fingerprint_opt, load_self_public_key, load_self_public_key_opt, self_fingerprint, self_fingerprint_opt,
}; };
@@ -36,7 +36,7 @@ use crate::message::{
self, Message, MessageState, MsgId, Viewtype, insert_tombstone, rfc724_mid_exists, self, Message, MessageState, MsgId, Viewtype, insert_tombstone, rfc724_mid_exists,
}; };
use crate::mimeparser::{ use crate::mimeparser::{
AvatarAction, GossipedKey, MimeMessage, PreMessageMode, SystemMessage, parse_message_ids, AvatarAction, MimeMessage, PreMessageMode, SystemMessage, parse_message_ids,
}; };
use crate::param::{Param, Params}; use crate::param::{Param, Params};
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub, iroh_topic_from_str}; use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub, iroh_topic_from_str};
@@ -654,7 +654,6 @@ pub(crate) async fn receive_imf_inner(
) )
})? })?
} else if let Some(to_id) = to_ids.first().copied().flatten() { } else if let Some(to_id) = to_ids.first().copied().flatten() {
// handshake may mark contacts as verified and must be processed before chats are created
observe_securejoin_on_other_device(context, &mime_parser, to_id) observe_securejoin_on_other_device(context, &mime_parser, to_id)
.await .await
.with_context(|| { .with_context(|| {
@@ -687,12 +686,6 @@ pub(crate) async fn receive_imf_inner(
received_msg = None; received_msg = None;
} }
let verified_encryption = has_verified_encryption(context, &mime_parser, from_id).await?;
if verified_encryption == VerifiedEncryption::Verified {
mark_recipients_as_verified(context, from_id, &mime_parser).await?;
}
let is_old_contact_request; let is_old_contact_request;
let received_msg = if let Some(received_msg) = received_msg { let received_msg = if let Some(received_msg) = received_msg {
is_old_contact_request = false; is_old_contact_request = false;
@@ -2992,7 +2985,7 @@ struct GroupChangesInfo {
extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>, extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
} }
/// Apply group member list, name, avatar and protection status changes from the MIME message. /// Apply group member list, name and avatar changes from the MIME message.
/// ///
/// Returns [GroupChangesInfo]. /// Returns [GroupChangesInfo].
/// ///
@@ -3060,7 +3053,7 @@ async fn apply_group_changes(
// just like we look at ChatGroupMemberRemovedFpr. // just like we look at ChatGroupMemberRemovedFpr.
// The result of the error is that info message // The result of the error is that info message
// may contain display name of the wrong contact. // may contain display name of the wrong contact.
let fingerprint = key.public_key.dc_fingerprint().hex(); let fingerprint = key.dc_fingerprint().hex();
if let Some(contact_id) = if let Some(contact_id) =
lookup_key_contact_by_fingerprint(context, &fingerprint).await? lookup_key_contact_by_fingerprint(context, &fingerprint).await?
{ {
@@ -3967,89 +3960,6 @@ async fn create_adhoc_group(
Ok(Some((new_chat_id, create_blocked))) Ok(Some((new_chat_id, create_blocked)))
} }
#[derive(Debug, PartialEq, Eq)]
enum VerifiedEncryption {
Verified,
NotVerified(String), // The string contains the reason why it's not verified
}
/// Checks whether the message is allowed to appear in a protected chat.
///
/// This means that it is encrypted and signed with a verified key.
async fn has_verified_encryption(
context: &Context,
mimeparser: &MimeMessage,
from_id: ContactId,
) -> Result<VerifiedEncryption> {
use VerifiedEncryption::*;
if !mimeparser.was_encrypted() {
return Ok(NotVerified("This message is not encrypted".to_string()));
};
if from_id == ContactId::SELF {
return Ok(Verified);
}
let from_contact = Contact::get_by_id(context, from_id).await?;
let Some(fingerprint) = from_contact.fingerprint() else {
return Ok(NotVerified(
"The message was sent without encryption".to_string(),
));
};
if from_contact.get_verifier_id(context).await?.is_none() {
return Ok(NotVerified(
"The message was sent by non-verified contact".to_string(),
));
}
let signed_with_verified_key = mimeparser
.signature
.as_ref()
.is_some_and(|(signature, _)| *signature == fingerprint);
if signed_with_verified_key {
Ok(Verified)
} else {
Ok(NotVerified(
"The message was sent with non-verified encryption".to_string(),
))
}
}
async fn mark_recipients_as_verified(
context: &Context,
from_id: ContactId,
mimeparser: &MimeMessage,
) -> Result<()> {
let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
// We don't yet send the _verified property in autocrypt headers.
// Until we do, we instead accept the Chat-Verified header as indication all contacts are verified.
// TODO: Ignore ChatVerified header once we reset existing verifications.
let chat_verified = mimeparser.get_header(HeaderDef::ChatVerified).is_some();
for gossiped_key in mimeparser
.gossiped_keys
.values()
.filter(|gossiped_key| gossiped_key.verified || chat_verified)
{
let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
continue;
};
if to_id == ContactId::SELF || to_id == from_id {
continue;
}
mark_contact_id_as_verified(context, to_id, verifier_id).await?;
}
Ok(())
}
/// Returns the last message referenced from `References` header if it is in the database. /// Returns the last message referenced from `References` header if it is in the database.
/// ///
/// For Delta Chat messages it is the last message in the chat of the sender. /// For Delta Chat messages it is the last message in the chat of the sender.
@@ -4117,7 +4027,7 @@ async fn add_or_lookup_contacts_by_address_list(
async fn add_or_lookup_key_contacts( async fn add_or_lookup_key_contacts(
context: &Context, context: &Context,
address_list: &[SingleInfo], address_list: &[SingleInfo],
gossiped_keys: &BTreeMap<String, GossipedKey>, gossiped_keys: &BTreeMap<String, SignedPublicKey>,
fingerprints: &[Fingerprint], fingerprints: &[Fingerprint],
origin: Origin, origin: Origin,
) -> Result<Vec<Option<ContactId>>> { ) -> Result<Vec<Option<ContactId>>> {
@@ -4134,7 +4044,7 @@ async fn add_or_lookup_key_contacts(
// Iterator has not ran out of fingerprints yet. // Iterator has not ran out of fingerprints yet.
fp.hex() fp.hex()
} else if let Some(key) = gossiped_keys.get(addr) { } else if let Some(key) = gossiped_keys.get(addr) {
key.public_key.dc_fingerprint().hex() key.dc_fingerprint().hex()
} else if context.is_self_addr(addr).await? { } else if context.is_self_addr(addr).await? {
contact_ids.push(Some(ContactId::SELF)); contact_ids.push(Some(ContactId::SELF));
continue; continue;
+6 -111
View File
@@ -19,9 +19,7 @@ use crate::mimefactory;
use crate::securejoin::get_securejoin_qr; use crate::securejoin::get_securejoin_qr;
use crate::smtp; use crate::smtp;
use crate::test_utils; use crate::test_utils;
use crate::test_utils::{ use crate::test_utils::{TestContext, TestContextManager, alice_keypair, get_chat_msg};
TestContext, TestContextManager, alice_keypair, get_chat_msg, mark_as_verified,
};
use crate::tools::{SystemTime, time}; use crate::tools::{SystemTime, time};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -3166,13 +3164,11 @@ async fn test_auto_accept_group_for_bots() -> Result<()> {
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_auto_accept_protected_group_for_bots() -> Result<()> { async fn test_auto_accept_encrypted_group_for_bots() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await; let alice = &tcm.alice().await;
let bob = &tcm.bob().await; let bob = &tcm.bob().await;
bob.set_config(Config::Bot, Some("1")).await.unwrap(); bob.set_config(Config::Bot, Some("1")).await.unwrap();
mark_as_verified(alice, bob).await;
mark_as_verified(bob, alice).await;
let group_id = alice.create_group_with_members("Group", &[bob]).await; let group_id = alice.create_group_with_members("Group", &[bob]).await;
let sent = alice.send_text(group_id, "Hello!").await; let sent = alice.send_text(group_id, "Hello!").await;
let msg = bob.recv_msg(&sent).await; let msg = bob.recv_msg(&sent).await;
@@ -3182,21 +3178,13 @@ async fn test_auto_accept_protected_group_for_bots() -> Result<()> {
} }
/// Regression test for a bug where receive_imf() failed /// Regression test for a bug where receive_imf() failed
/// if the sender of a verification-gossiping message /// if the sender of a gossiping message
/// also put itself into the To header. /// also put itself into the To header.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verification_gossip() -> Result<()> { async fn test_gossip_sender_in_to_header() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await; let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
mark_as_verified(alice, bob).await;
mark_as_verified(bob, alice).await;
// This is message sent by Alice with verified encryption
// that gossips Fiona's verification,
// and for some reason, Alice also put herself into the To: header.
let imf_raw = let imf_raw =
include_bytes!("../../test-data/message/verification-gossip-also-sent-to-from.eml"); include_bytes!("../../test-data/message/verification-gossip-also-sent-to-from.eml");
@@ -3204,12 +3192,6 @@ async fn test_verification_gossip() -> Result<()> {
let msg = receive_imf(bob, imf_raw, false).await?.unwrap(); let msg = receive_imf(bob, imf_raw, false).await?.unwrap();
let msg = Message::load_from_db(bob, msg.msg_ids[0]).await?; let msg = Message::load_from_db(bob, msg.msg_ids[0]).await?;
assert_eq!(msg.text, "Hello!"); assert_eq!(msg.text, "Hello!");
assert!(
bob.add_or_lookup_contact(fiona)
.await
.is_verified(bob)
.await?
);
Ok(()) Ok(())
} }
@@ -4628,11 +4610,10 @@ async fn test_pre_msg_group_consistency() -> Result<()> {
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protected_group_add_remove_member_missing_key() -> Result<()> { async fn test_group_add_remove_member_missing_key() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await; let alice = &tcm.alice().await;
let bob = &tcm.bob().await; let bob = &tcm.bob().await;
mark_as_verified(alice, bob).await;
let group_id = create_group(alice, "Group").await?; let group_id = create_group(alice, "Group").await?;
let alice_bob_id = alice.add_or_lookup_contact(bob).await.id; let alice_bob_id = alice.add_or_lookup_contact(bob).await.id;
add_contact_to_chat(alice, group_id, alice_bob_id).await?; add_contact_to_chat(alice, group_id, alice_bob_id).await?;
@@ -4640,7 +4621,6 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
alice.sql.execute("DELETE FROM public_keys", ()).await?; alice.sql.execute("DELETE FROM public_keys", ()).await?;
let fiona = &tcm.fiona().await; let fiona = &tcm.fiona().await;
mark_as_verified(alice, fiona).await;
let alice_fiona_id = alice.add_or_lookup_contact(fiona).await.id; let alice_fiona_id = alice.add_or_lookup_contact(fiona).await.id;
add_contact_to_chat(alice, group_id, alice_fiona_id).await?; add_contact_to_chat(alice, group_id, alice_fiona_id).await?;
@@ -5133,7 +5113,7 @@ async fn test_rename_chat_after_creating_invite() -> Result<()> {
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unverified_member_msg() -> Result<()> { async fn test_member_msg_downloaded() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await; let alice = &tcm.alice().await;
let bob = &tcm.bob().await; let bob = &tcm.bob().await;
@@ -5148,97 +5128,12 @@ async fn test_unverified_member_msg() -> Result<()> {
let fiona_chat_id = fiona.get_last_msg().await.chat_id; let fiona_chat_id = fiona.get_last_msg().await.chat_id;
let fiona_sent_msg = fiona.send_text(fiona_chat_id, "Hi").await; let fiona_sent_msg = fiona.send_text(fiona_chat_id, "Hi").await;
// The message is by non-verified member,
// but the checks have been removed
// and the message should be downloaded as usual.
let bob_msg = bob.recv_msg(&fiona_sent_msg).await; let bob_msg = bob.recv_msg(&fiona_sent_msg).await;
assert_eq!(bob_msg.download_state, DownloadState::Done); assert_eq!(bob_msg.download_state, DownloadState::Done);
assert_eq!(bob_msg.text, "Hi"); assert_eq!(bob_msg.text, "Hi");
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dont_reverify_by_self_on_outgoing_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let a0 = &tcm.alice().await;
let a1 = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let bob_chat_id = chat::create_group(bob, "Group").await?;
bob.set_chat_protected(bob_chat_id).await;
let qr = get_securejoin_qr(bob, Some(bob_chat_id)).await?;
tcm.exec_securejoin_qr(fiona, bob, &qr).await;
tcm.exec_securejoin_qr(a0, bob, &qr).await;
tcm.exec_securejoin_qr(a1, bob, &qr).await;
// Shift time by one week to trigger gossip.
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
let a0_chat_id = a0.get_last_msg().await.chat_id;
let a0_sent_msg = a0.send_text(a0_chat_id, "Hi").await;
a1.recv_msg(&a0_sent_msg).await;
let a1_bob_id = a1.add_or_lookup_contact_id(bob).await;
let a1_fiona = a1.add_or_lookup_contact(fiona).await;
assert_eq!(
a1_fiona.get_verifier_id(a1).await?.unwrap().unwrap(),
a1_bob_id
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dont_verify_by_verified_by_unknown() -> Result<()> {
let mut tcm = TestContextManager::new();
let a0 = &tcm.alice().await;
let a1 = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let bob_chat_id = chat::create_group(bob, "Group").await?;
bob.set_chat_protected(bob_chat_id).await;
let qr = get_securejoin_qr(bob, Some(bob_chat_id)).await?;
tcm.exec_securejoin_qr(a0, bob, &qr).await;
let qr = get_securejoin_qr(bob, None).await?;
tcm.exec_securejoin_qr(fiona, bob, &qr).await;
// Bob verifies Fiona for Alice#0.
let bob_fiona_id = bob.add_or_lookup_contact_id(fiona).await;
add_contact_to_chat(bob, bob_chat_id, bob_fiona_id).await?;
let sent_msg = bob.pop_sent_msg().await;
a0.recv_msg(&sent_msg).await;
fiona.recv_msg(&sent_msg).await;
let a0_bob = a0.add_or_lookup_contact(bob).await;
let a0_fiona = a0.add_or_lookup_contact(fiona).await;
assert_eq!(a0_fiona.get_verifier_id(a0).await?, Some(Some(a0_bob.id)));
let chat_id = a0.create_group_with_members("group", &[fiona]).await;
a0.set_chat_protected(chat_id).await;
a1.recv_msg(&a0.send_text(chat_id, "Hi").await).await;
let a1_fiona = a1.add_or_lookup_contact(fiona).await;
assert_eq!(a1_fiona.get_verifier_id(a1).await?, Some(None));
let some_time_to_regossip = Duration::from_secs(20 * 24 * 3600);
SystemTime::shift(some_time_to_regossip);
let fiona_chat_id = fiona.get_last_msg().await.chat_id;
fiona.set_chat_protected(fiona_chat_id).await;
a1.recv_msg(&fiona.send_text(fiona_chat_id, "Hi").await)
.await;
let a1_bob = a1.add_or_lookup_contact(bob).await;
// There was a bug that Bob is verified by Fiona on Alice's other device.
assert_eq!(a1_bob.get_verifier_id(a1).await?, Some(None));
SystemTime::shift(some_time_to_regossip);
tcm.execute_securejoin(a1, fiona).await;
a1.recv_msg(&fiona.send_text(fiona_chat_id, "Hi").await)
.await;
// But now Bob's verifier id must be updated because Fiona is verified by a known verifier
// (moreover, directly), so Alice has reverse verification chains on her devices.
assert_eq!(a1_bob.get_verifier_id(a1).await?, Some(Some(a1_fiona.id)));
Ok(())
}
/// Tests that second device assigns outgoing encrypted messages /// Tests that second device assigns outgoing encrypted messages
/// to a single chat with key-contact even if the key of the contact is unknown. /// to a single chat with key-contact even if the key of the contact is unknown.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+20 -50
View File
@@ -7,7 +7,6 @@ use percent_encoding::{AsciiSet, utf8_percent_encode};
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, get_chat_id_by_grpid, load_broadcast_secret}; use crate::chat::{self, Chat, ChatId, ChatIdBlocked, get_chat_id_by_grpid, load_broadcast_secret};
use crate::config::Config; use crate::config::Config;
use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT}; use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT};
use crate::contact::mark_contact_id_as_verified;
use crate::contact::{Contact, ContactId, Origin}; use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context; use crate::context::Context;
use crate::events::EventType; use crate::events::EventType;
@@ -34,15 +33,6 @@ pub(crate) use qrinvite::QrInvite;
use crate::token::Namespace; use crate::token::Namespace;
/// Only new QR codes cause a verification on Alice's side.
/// When a QR code is too old, it is assumed that there was no direct QR scan,
/// and that the QR code was potentially published on a website,
/// so, Alice doesn't mark Bob as verified.
// TODO For backwards compatibility reasons, this is still using a rather large value.
// Set this to a lower value (e.g. 10 minutes)
// when Delta Chat v2.22.0 is sufficiently rolled out
const VERIFICATION_TIMEOUT_SECONDS: i64 = 7 * 24 * 3600;
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_'); const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_');
fn inviter_progress( fn inviter_progress(
@@ -86,7 +76,7 @@ fn shorten_name(name: &str, length: usize) -> String {
pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Result<String> { pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Result<String> {
/*======================================================= /*=======================================================
==== Alice - the inviter side ==== ==== Alice - the inviter side ====
==== Step 1 in "Setup verified contact" protocol ==== ==== Step 1 in the Setup-Contact protocol ====
=======================================================*/ =======================================================*/
key::ensure_secret_key_exists(context).await.ok(); key::ensure_secret_key_exists(context).await.ok();
@@ -123,15 +113,9 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
// Invite number is used to request the inviter key. // Invite number is used to request the inviter key.
let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, grpid).await?; let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, grpid).await?;
// Auth token is used to verify the key-contact // Auth token authenticates the joiner
// if the token is not old // and adds the contact to the group
// and add the contact to the group
// if there is an associated group ID. // 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(); let auth = create_id();
token::save(context, Namespace::Auth, grpid, &auth, time()).await?; token::save(context, Namespace::Auth, grpid, &auth, time()).await?;
@@ -255,7 +239,7 @@ pub async fn join_securejoin_with_ux_info(
async fn securejoin(context: &Context, qr: &str) -> Result<ChatId> { async fn securejoin(context: &Context, qr: &str) -> Result<ChatId> {
/*======================================================== /*========================================================
==== Bob - the joiner's side ===== ==== Bob - the joiner's side =====
==== Step 2 in "Setup verified contact" protocol ===== ==== Step 2 in the Setup-Contact protocol =====
========================================================*/ ========================================================*/
info!(context, "Requesting secure-join ...",); info!(context, "Requesting secure-join ...",);
@@ -303,8 +287,7 @@ async fn info_chat_id(context: &Context, contact_id: ContactId) -> Result<ChatId
Ok(chat_id_blocked.id) Ok(chat_id_blocked.id)
} }
/// Checks fingerprint and marks the contact as verified /// Checks whether the contact's fingerprint matches.
/// if fingerprint matches.
async fn verify_sender_by_fingerprint( async fn verify_sender_by_fingerprint(
context: &Context, context: &Context,
fingerprint: &Fingerprint, fingerprint: &Fingerprint,
@@ -313,11 +296,7 @@ async fn verify_sender_by_fingerprint(
let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? else { let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? else {
return Ok(false); return Ok(false);
}; };
let is_verified = contact.fingerprint().is_some_and(|fp| &fp == fingerprint); Ok(contact.fingerprint().is_some_and(|fp| &fp == fingerprint))
if is_verified {
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
}
Ok(is_verified)
} }
/// What to do with a Secure-Join handshake message after it was handled. /// What to do with a Secure-Join handshake message after it was handled.
@@ -421,7 +400,6 @@ pub(crate) fn get_secure_join_step(mime_message: &MimeMessage) -> Option<SecureJ
/// ///
/// When `handle_securejoin_handshake()` is called, the message is not yet filed in the /// When `handle_securejoin_handshake()` is called, the message is not yet filed in the
/// database; this is done by `receive_imf()` later on as needed. /// database; this is done by `receive_imf()` later on as needed.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn handle_securejoin_handshake( pub(crate) async fn handle_securejoin_handshake(
context: &Context, context: &Context,
mime_message: &mut MimeMessage, mime_message: &mut MimeMessage,
@@ -457,7 +435,7 @@ pub(crate) async fn handle_securejoin_handshake(
let mut self_found = false; let mut self_found = false;
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint(); let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
for key in mime_message.gossiped_keys.values() { for key in mime_message.gossiped_keys.values() {
if key.public_key.dc_fingerprint() == self_fingerprint { if key.dc_fingerprint() == self_fingerprint {
self_found = true; self_found = true;
break; break;
} }
@@ -474,7 +452,7 @@ pub(crate) async fn handle_securejoin_handshake(
SecureJoinStep::Request { ref invitenumber } => { SecureJoinStep::Request { ref invitenumber } => {
/*======================================================= /*=======================================================
==== Alice - the inviter side ==== ==== Alice - the inviter side ====
==== Step 3 in "Setup verified contact" protocol ==== ==== Step 3 in the Setup-Contact protocol ====
=======================================================*/ =======================================================*/
// this message may be unencrypted (Bob, the joiner and the sender, might not have Alice's key yet) // this message may be unencrypted (Bob, the joiner and the sender, might not have Alice's key yet)
@@ -515,7 +493,7 @@ pub(crate) async fn handle_securejoin_handshake(
SecureJoinStep::AuthRequired => { SecureJoinStep::AuthRequired => {
/*======================================================== /*========================================================
==== Bob - the joiner's side ===== ==== Bob - the joiner's side =====
==== Step 4 in "Setup verified contact" protocol ===== ==== Step 4 in the Setup-Contact protocol =====
========================================================*/ ========================================================*/
bob::handle_auth_required_or_pubkey(context, mime_message).await bob::handle_auth_required_or_pubkey(context, mime_message).await
} }
@@ -585,8 +563,8 @@ pub(crate) async fn handle_securejoin_handshake(
SecureJoinStep::RequestWithAuth => { SecureJoinStep::RequestWithAuth => {
/*========================================================== /*==========================================================
==== Alice - the inviter side ==== ==== Alice - the inviter side ====
==== Steps 5+6 in "Setup verified contact" protocol ==== ==== Steps 5+6 in the Setup-Contact protocol ====
==== Step 6 in "Out-of-band verified groups" protocol ==== ==== Step 6 in the Join-Group protocol ====
==========================================================*/ ==========================================================*/
// verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob // verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob
@@ -613,15 +591,14 @@ pub(crate) async fn handle_securejoin_handshake(
); );
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
}; };
let Some((grpid, timestamp)) = context let Some(grpid) = context
.sql .sql
.query_row_optional( .query_row_optional(
"SELECT foreign_key, timestamp FROM tokens WHERE namespc=? AND token=?", "SELECT foreign_key FROM tokens WHERE namespc=? AND token=?",
(Namespace::Auth, auth), (Namespace::Auth, auth),
|row| { |row| {
let foreign_key: String = row.get(0)?; let foreign_key: String = row.get(0)?;
let timestamp: i64 = row.get(1)?; Ok(foreign_key)
Ok((foreign_key, timestamp))
}, },
) )
.await? .await?
@@ -654,12 +631,8 @@ pub(crate) async fn handle_securejoin_handshake(
); );
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
info!(context, "Fingerprint verified via Auth code.",); info!(context, "Auth code and fingerprint match.",);
// Mark the contact as verified if auth code is less than VERIFICATION_TIMEOUT_SECONDS seconds old.
if time() < timestamp + VERIFICATION_TIMEOUT_SECONDS {
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
}
if sender_contact.blocked { if sender_contact.blocked {
warn!(context, "Ignoring {step} message: {contact_id} is blocked."); warn!(context, "Ignoring {step} message: {contact_id} is blocked.");
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
@@ -692,7 +665,7 @@ pub(crate) async fn handle_securejoin_handshake(
Ok(HandshakeMessage::Done) Ok(HandshakeMessage::Done)
} else { } else {
let chat_id = info_chat_id(context, contact_id).await?; let chat_id = info_chat_id(context, contact_id).await?;
// Setup verified contact. // Setup contact.
send_alice_handshake_msg(context, contact_id, "vc-contact-confirm") send_alice_handshake_msg(context, contact_id, "vc-contact-confirm")
.await .await
.context("failed sending vc-contact-confirm message")?; .context("failed sending vc-contact-confirm message")?;
@@ -703,7 +676,7 @@ pub(crate) async fn handle_securejoin_handshake(
} }
/*======================================================= /*=======================================================
==== Bob - the joiner's side ==== ==== Bob - the joiner's side ====
==== Step 7 in "Setup verified contact" protocol ==== ==== Step 7 in the Setup-Contact protocol ====
=======================================================*/ =======================================================*/
SecureJoinStep::ContactConfirm => { SecureJoinStep::ContactConfirm => {
context.emit_event(EventType::SecurejoinJoinerProgress { context.emit_event(EventType::SecurejoinJoinerProgress {
@@ -753,16 +726,14 @@ pub(crate) async fn handle_securejoin_handshake(
/// we can make some conclusions of it. /// we can make some conclusions of it.
/// ///
/// If we see self-sent {vc,vg}-request-with-auth, /// If we see self-sent {vc,vg}-request-with-auth,
/// we know that we are Bob (joiner-observer) /// we know that we are Bob (joiner-observer),
/// that just marked peer (Alice) as verified
/// either after receiving {vc,vg}-auth-required /// either after receiving {vc,vg}-auth-required
/// or immediately after scanning the QR-code /// or immediately after scanning the QR-code
/// if the key was already known. /// if the key was already known.
/// ///
/// If we see self-sent vc-contact-confirm or vg-member-added message, /// If we see self-sent vc-contact-confirm or vg-member-added message,
/// we know that we are Alice (inviter-observer) /// we know that we are Alice (inviter-observer)
/// that just marked peer (Bob) as verified /// responding to a correct vc-request-with-auth message.
/// in response to correct vc-request-with-auth message.
pub(crate) async fn observe_securejoin_on_other_device( pub(crate) async fn observe_securejoin_on_other_device(
context: &Context, context: &Context,
mime_message: &MimeMessage, mime_message: &MimeMessage,
@@ -810,13 +781,12 @@ pub(crate) async fn observe_securejoin_on_other_device(
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
}; };
if key.public_key.dc_fingerprint() != contact_fingerprint { if key.dc_fingerprint() != contact_fingerprint {
// Fingerprint does not match, ignore. // Fingerprint does not match, ignore.
warn!(context, "Fingerprint does not match."); warn!(context, "Fingerprint does not match.");
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
if contact.blocked && step != SecureJoinStep::MemberAdded { if contact.blocked && step != SecureJoinStep::MemberAdded {
// Contact might be blocked after another device had issued the message. Still, to avoid // Contact might be blocked after another device had issued the message. Still, to avoid
// membership inconsistency on devices, don't ignore "vg-member-added". // membership inconsistency on devices, don't ignore "vg-member-added".
+4 -5
View File
@@ -106,7 +106,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
// If QR code is a group invite // If QR code is a group invite
// and we are already in the chat, // and we are already in the chat,
// nothing needs to be done. // nothing needs to be done.
// Even if Alice is not verified, we don't send anything.
context.emit_event(EventType::SecurejoinJoinerProgress { context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id: invite.contact_id(), contact_id: invite.contact_id(),
progress: JoinerProgress::Succeeded.into_u16(), progress: JoinerProgress::Succeeded.into_u16(),
@@ -270,7 +269,7 @@ pub(super) async fn handle_auth_required_or_pubkey(
continue; continue;
} }
info!(context, "Fingerprint verified.",); info!(context, "Fingerprint matches.",);
let chat_id = private_chat_id(context, &invite).await?; let chat_id = private_chat_id(context, &invite).await?;
delete_securejoin_wait_msg(context, chat_id) delete_securejoin_wait_msg(context, chat_id)
.await .await
@@ -449,8 +448,8 @@ async fn private_chat_id(context: &Context, invite: &QrInvite) -> Result<ChatId>
/// ///
/// This is the chat in which you want to notify the user as well. /// This is the chat in which you want to notify the user as well.
/// ///
/// When joining a group this is the [`ChatId`] of the group chat, when verifying a /// When joining a group this is the [`ChatId`] of the group chat,
/// contact this is the [`ChatId`] of the single chat. /// when setting up a contact this is the [`ChatId`] of the single chat.
/// The group chat will be created if it does not yet exist. /// The group chat will be created if it does not yet exist.
async fn joining_chat_id( async fn joining_chat_id(
context: &Context, context: &Context,
@@ -496,7 +495,7 @@ async fn joining_chat_id(
pub(crate) enum JoinerProgress { pub(crate) enum JoinerProgress {
/// vg-vc-request-with-auth sent. /// vg-vc-request-with-auth sent.
/// ///
/// Typically shows as "alice@addr verified, introducing myself." /// Typically shows as "introducing myself."
RequestWithAuthSent, RequestWithAuthSent,
/// Completed securejoin. /// Completed securejoin.
Succeeded, Succeeded,
+25 -165
View File
@@ -1,5 +1,3 @@
use std::time::Duration;
use deltachat_contact_tools::EmailAddress; use deltachat_contact_tools::EmailAddress;
use regex::Regex; use regex::Regex;
@@ -8,7 +6,6 @@ use crate::chat::{CantSendReason, ChatId, add_contact_to_chat, remove_contact_fr
use crate::chatlist::Chatlist; use crate::chatlist::Chatlist;
use crate::constants::Chattype; use crate::constants::Chattype;
use crate::key::self_fingerprint; use crate::key::self_fingerprint;
use crate::mimeparser::{GossipedKey, SystemMessage};
use crate::qr::Qr; use crate::qr::Qr;
use crate::receive_imf::receive_imf; use crate::receive_imf::receive_imf;
use crate::stock_str::{self, messages_e2ee_info_msg}; use crate::stock_str::{self, messages_e2ee_info_msg};
@@ -16,7 +13,6 @@ use crate::test_utils::{
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager, AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
TimeShiftFalsePositiveNote, get_chat_msg, sync, TimeShiftFalsePositiveNote, get_chat_msg, sync,
}; };
use crate::tools::SystemTime;
#[derive(PartialEq)] #[derive(PartialEq)]
enum SetupContactCase { enum SetupContactCase {
@@ -193,10 +189,7 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
); );
if case == SetupContactCase::WrongAliceGossip { if case == SetupContactCase::WrongAliceGossip {
let wrong_pubkey = GossipedKey { let wrong_pubkey = load_self_public_key(&bob).await.unwrap();
public_key: load_self_public_key(&bob).await.unwrap(),
verified: false,
};
let alice_pubkey = msg let alice_pubkey = msg
.gossiped_keys .gossiped_keys
.insert(alice_addr.to_string(), wrong_pubkey) .insert(alice_addr.to_string(), wrong_pubkey)
@@ -206,7 +199,6 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
.await .await
.unwrap(); .unwrap();
assert_eq!(handshake_msg, HandshakeMessage::Ignore); assert_eq!(handshake_msg, HandshakeMessage::Ignore);
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false);
msg.gossiped_keys msg.gossiped_keys
.insert(alice_addr.to_string(), alice_pubkey) .insert(alice_addr.to_string(), alice_pubkey)
@@ -215,20 +207,17 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
.await .await
.unwrap(); .unwrap();
assert_eq!(handshake_msg, HandshakeMessage::Ignore); assert_eq!(handshake_msg, HandshakeMessage::Ignore);
assert!(contact_bob.is_verified(&alice).await.unwrap());
return (alice, bob); return (alice, bob);
} }
// Alice should not yet have Bob verified
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await; let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
let contact_bob_id = contact_bob.id; let contact_bob_id = contact_bob.id;
assert_eq!(contact_bob.is_key_contact(), true); assert_eq!(contact_bob.is_key_contact(), true);
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false);
assert_eq!(contact_bob.get_authname(), ""); assert_eq!(contact_bob.get_authname(), "");
tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm"); tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm");
alice.recv_msg_trash(&sent).await; alice.recv_msg_trash(&sent).await;
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), true); assert_eq!(contact_bob.e2ee_avail(&alice).await.unwrap(), true);
let contact_bob = Contact::get_by_id(&alice, contact_bob_id).await.unwrap(); let contact_bob = Contact::get_by_id(&alice, contact_bob_id).await.unwrap();
assert_eq!(contact_bob.get_authname(), "Bob Examplenet"); assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
assert!(contact_bob.get_name().is_empty()); assert!(contact_bob.get_name().is_empty());
@@ -248,7 +237,7 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
1 1
); );
// Check Alice got the verified message in her single chat. // Check Alice got the message in her single chat.
{ {
let chat = alice.get_chat(&bob).await; let chat = alice.get_chat(&bob).await;
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await; let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
@@ -280,14 +269,14 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
"vc-contact-confirm" "vc-contact-confirm"
); );
// Bob has verified Alice already. // Bob has Alice's key already.
// //
// Alice may not have verified Bob yet. // Alice may not have Bob's key yet.
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true); assert_eq!(contact_alice.e2ee_avail(&bob.ctx).await.unwrap(), true);
// Step 7: Bob receives vc-contact-confirm // Step 7: Bob receives vc-contact-confirm
bob.recv_msg_trash(&sent).await; bob.recv_msg_trash(&sent).await;
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true); assert_eq!(contact_alice.e2ee_avail(&bob.ctx).await.unwrap(), true);
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id) let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
.await .await
.unwrap(); .unwrap();
@@ -359,13 +348,11 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> {
bob_fp.hex() bob_fp.hex()
); );
// Alice should not yet have Bob verified
let contact_bob = alice.add_or_lookup_contact_no_key(bob).await; let contact_bob = alice.add_or_lookup_contact_no_key(bob).await;
assert_eq!(contact_bob.is_verified(alice).await?, false);
tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm"); tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm");
alice.recv_msg_trash(&sent).await; alice.recv_msg_trash(&sent).await;
assert_eq!(contact_bob.is_verified(alice).await?, true); assert_eq!(contact_bob.e2ee_avail(alice).await?, true);
// Check Alice signalled success via the SecurejoinInviterProgress event. // Check Alice signalled success via the SecurejoinInviterProgress event.
let event = alice let event = alice
@@ -399,16 +386,14 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> {
"vc-contact-confirm" "vc-contact-confirm"
); );
// Bob has verified Alice already. // Bob has Alice's key already.
let contact_alice = bob.add_or_lookup_contact_no_key(alice).await; let contact_alice = bob.add_or_lookup_contact_no_key(alice).await;
assert_eq!(contact_alice.is_verified(bob).await?, true); assert_eq!(contact_alice.e2ee_avail(bob).await?, true);
// Alice confirms that Bob is now verified.
//
// This does not change anything for Bob. // This does not change anything for Bob.
tcm.section("Step 7: Bob receives vc-contact-confirm"); tcm.section("Step 7: Bob receives vc-contact-confirm");
bob.recv_msg_trash(&sent).await; bob.recv_msg_trash(&sent).await;
assert_eq!(contact_alice.is_verified(bob).await?, true); assert_eq!(contact_alice.e2ee_avail(bob).await?, true);
Ok(()) Ok(())
} }
@@ -567,13 +552,11 @@ async fn test_secure_join_group_ext(v3: bool, remove_invite: bool) -> Result<()>
bob_fp bob_fp
); );
// Alice should not yet have Bob verified
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await; let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
assert_eq!(contact_bob.is_verified(&alice).await?, false);
tcm.section("Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added"); tcm.section("Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added");
alice.recv_msg_trash(&sent).await; alice.recv_msg_trash(&sent).await;
assert_eq!(contact_bob.is_verified(&alice).await?, true); assert_eq!(contact_bob.e2ee_avail(&alice).await?, true);
// Check Alice signalled success via the SecurejoinInviterProgress event. // Check Alice signalled success via the SecurejoinInviterProgress event.
let event = alice let event = alice
@@ -615,7 +598,7 @@ async fn test_secure_join_group_ext(v3: bool, remove_invite: bool) -> Result<()>
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some()); assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some());
{ {
// Now Alice's chat with Bob should still be hidden, the verified message should // Now Alice's chat with Bob should still be hidden, the member-added message should
// appear in the group chat. // appear in the group chat.
if v3 { if v3 {
assert!( assert!(
@@ -641,17 +624,17 @@ async fn test_secure_join_group_ext(v3: bool, remove_invite: bool) -> Result<()>
assert_eq!(msg.get_text(), expected_text); assert_eq!(msg.get_text(), expected_text);
} }
// Bob has verified Alice already. // Bob has Alice's key already.
// //
// Alice may not have verified Bob yet. // Alice may not have Bob's key yet.
let contact_alice = bob.add_or_lookup_contact_no_key(&alice).await; let contact_alice = bob.add_or_lookup_contact_no_key(&alice).await;
assert_eq!(contact_alice.is_verified(&bob).await?, true); assert_eq!(contact_alice.e2ee_avail(&bob).await?, true);
tcm.section("Step 7: Bob receives vg-member-added"); tcm.section("Step 7: Bob receives vg-member-added");
bob.recv_msg(&sent).await; bob.recv_msg(&sent).await;
{ {
// Bob has Alice verified, message shows up in the group chat. // The message shows up in the group chat.
assert_eq!(contact_alice.is_verified(&bob).await?, true); assert_eq!(contact_alice.e2ee_avail(&bob).await?, true);
let chat = bob.get_chat(&alice).await; let chat = bob.get_chat(&alice).await;
assert_eq!( assert_eq!(
chat.blocked, chat.blocked,
@@ -832,11 +815,7 @@ async fn test_unknown_sender() -> Result<()> {
Ok(()) Ok(())
} }
/// Tests that Bob gets Alice as verified /// Tests that Bob gets Alice's key if `vc-contact-confirm` is lost.
/// if `vc-contact-confirm` is lost.
/// Previously `vc-contact-confirm` was used
/// to confirm backward verification,
/// but backward verification is not tracked anymore.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lost_contact_confirm() { async fn test_lost_contact_confirm() {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
@@ -858,16 +837,16 @@ async fn test_lost_contact_confirm() {
let sent = bob.pop_sent_msg().await; let sent = bob.pop_sent_msg().await;
alice.recv_msg_trash(&sent).await; alice.recv_msg_trash(&sent).await;
// Alice has Bob verified now. // Alice has Bob's key now.
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await; let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), true); assert_eq!(contact_bob.e2ee_avail(&alice).await.unwrap(), true);
// Alice sends vc-contact-confirm, but it gets lost. // Alice sends vc-contact-confirm, but it gets lost.
let _sent_vc_contact_confirm = alice.pop_sent_msg().await; let _sent_vc_contact_confirm = alice.pop_sent_msg().await;
// Bob has alice as verified too, even though vc-contact-confirm is lost. // Bob has Alice's key too, even though vc-contact-confirm is lost.
let contact_alice = bob.add_or_lookup_contact_no_key(&alice).await; let contact_alice = bob.add_or_lookup_contact_no_key(&alice).await;
assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), true); assert_eq!(contact_alice.e2ee_avail(&bob).await.unwrap(), true);
} }
/// Tests Bob joining two groups by scanning two QR codes /// Tests Bob joining two groups by scanning two QR codes
@@ -974,7 +953,7 @@ async fn test_parallel_setup_contact(bob_deletes_fiona_contact: bool) -> Result<
bob.recv_msg_trash(&sent_fiona_vc_contact_confirm).await; bob.recv_msg_trash(&sent_fiona_vc_contact_confirm).await;
let bob_fiona_contact = Contact::get_by_id(bob, bob_fiona_contact_id).await.unwrap(); let bob_fiona_contact = Contact::get_by_id(bob, bob_fiona_contact_id).await.unwrap();
assert_eq!(bob_fiona_contact.is_verified(bob).await.unwrap(), true); assert_eq!(bob_fiona_contact.e2ee_avail(bob).await.unwrap(), true);
} }
// Alice gets online and previously started SecureJoin process finishes. // Alice gets online and previously started SecureJoin process finishes.
@@ -990,7 +969,7 @@ async fn test_parallel_setup_contact(bob_deletes_fiona_contact: bool) -> Result<
bob.recv_msg_trash(&sent_alice_vc_contact_confirm).await; bob.recv_msg_trash(&sent_alice_vc_contact_confirm).await;
let bob_alice_contact_id = bob.add_or_lookup_contact_id(alice).await; let bob_alice_contact_id = bob.add_or_lookup_contact_id(alice).await;
let bob_alice_contact = Contact::get_by_id(bob, bob_alice_contact_id).await.unwrap(); let bob_alice_contact = Contact::get_by_id(bob, bob_alice_contact_id).await.unwrap();
assert_eq!(bob_alice_contact.is_verified(bob).await.unwrap(), true); assert_eq!(bob_alice_contact.e2ee_avail(bob).await.unwrap(), true);
bob.assert_warn("Message does not match expected fingerprint") bob.assert_warn("Message does not match expected fingerprint")
.await; .await;
@@ -1027,8 +1006,6 @@ async fn test_wrong_auth_token() -> Result<()> {
alice.recv_msg_trash(&sent).await; alice.recv_msg_trash(&sent).await;
let alice_bob_contact = alice.add_or_lookup_contact(bob).await;
assert!(!alice_bob_contact.is_verified(alice).await?);
alice.assert_warn("invalid auth code").await; alice.assert_warn("invalid auth code").await;
Ok(()) Ok(())
} }
@@ -1100,123 +1077,6 @@ async fn test_send_avatar_in_securejoin() -> Result<()> {
Ok(()) 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_contact_auth_token() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
// Alice creates a QR code.
let 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, &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(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_expired_group_auth_token() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = chat::create_group(alice, "Group").await?;
// Alice creates a group QR code.
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
// One week passes, QR code expires.
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
// Bob scans the QR code.
join_securejoin(bob, &qr).await?;
// vg-request
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
// vg-auth-requried
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
// vg-request-with-auth
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
// vg-member-added
let bob_member_added_msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert!(bob_member_added_msg.is_info());
assert_eq!(
bob_member_added_msg.get_info_type(),
SystemMessage::MemberAddedToGroup
);
// 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(())
}
/// Tests that old token is considered expired
/// even if sync message just arrived.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_expired_synced_auth_token() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice2 = &tcm.alice().await;
let bob = &tcm.bob().await;
alice.set_config_bool(Config::SyncMsgs, true).await?;
alice2.set_config_bool(Config::SyncMsgs, true).await?;
// Alice creates a QR code on the second device.
let qr = get_securejoin_qr(alice2, None).await?;
alice2.send_sync_msg().await.unwrap();
let sync_msg = alice2.pop_sent_msg().await;
// One week passes, QR code expires.
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
alice.recv_msg_trash(&sync_msg).await;
// Bob scans the QR code.
join_securejoin(bob, &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(())
}
/// Tests that attempting to join already joined group does nothing. /// Tests that attempting to join already joined group does nothing.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_rejoin_group() -> Result<()> { async fn test_rejoin_group() -> Result<()> {
-4
View File
@@ -110,7 +110,6 @@ async fn test_key_contacts_migration_autocrypt() -> Result<()> {
pgp_bob.fingerprint().unwrap(), pgp_bob.fingerprint().unwrap(),
pgp_bob.public_key(&t).await?.unwrap().dc_fingerprint() pgp_bob.public_key(&t).await?.unwrap().dc_fingerprint()
); );
assert_eq!(pgp_bob.get_verifier_id(&t).await?, None);
// Hidden address-contact can't be looked up by name. // Hidden address-contact can't be looked up by name.
assert!( assert!(
@@ -149,7 +148,6 @@ async fn test_key_contacts_migration_email1() -> Result<()> {
assert_eq!(email_bob.origin, Origin::OutgoingTo); assert_eq!(email_bob.origin, Origin::OutgoingTo);
assert_eq!(email_bob.e2ee_avail(&t).await?, false); assert_eq!(email_bob.e2ee_avail(&t).await?, false);
assert_eq!(email_bob.fingerprint(), None); assert_eq!(email_bob.fingerprint(), None);
assert_eq!(email_bob.get_verifier_id(&t).await?, None);
Ok(()) Ok(())
} }
@@ -183,7 +181,6 @@ async fn test_key_contacts_migration_email2() -> Result<()> {
assert_eq!(email_bob.origin, Origin::OutgoingTo); assert_eq!(email_bob.origin, Origin::OutgoingTo);
assert_eq!(email_bob.e2ee_avail(&t).await?, false); assert_eq!(email_bob.e2ee_avail(&t).await?, false);
assert_eq!(email_bob.fingerprint(), None); assert_eq!(email_bob.fingerprint(), None);
assert_eq!(email_bob.get_verifier_id(&t).await?, None);
Ok(()) Ok(())
} }
@@ -227,7 +224,6 @@ async fn test_key_contacts_migration_verified() -> Result<()> {
pgp_bob.fingerprint().unwrap(), pgp_bob.fingerprint().unwrap(),
pgp_bob.public_key(&t).await?.unwrap().dc_fingerprint() pgp_bob.public_key(&t).await?.unwrap().dc_fingerprint()
); );
assert_eq!(pgp_bob.get_verifier_id(&t).await?, Some(None));
Ok(()) Ok(())
} }
+24 -146
View File
@@ -3,7 +3,7 @@
//! If this is enabled, a JSON file with some anonymous statistics //! If this is enabled, a JSON file with some anonymous statistics
//! will be sent to a bot once a week. //! will be sent to a bot once a week.
use std::collections::{BTreeMap, BTreeSet}; use std::collections::BTreeMap;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use deltachat_derive::FromSql; use deltachat_derive::FromSql;
@@ -16,7 +16,7 @@ use serde::Serialize;
use crate::chat::{self, ChatId, MuteDuration}; use crate::chat::{self, ChatId, MuteDuration};
use crate::config::Config; use crate::config::Config;
use crate::constants::{Chattype, DC_VERSION_STR}; use crate::constants::{Chattype, DC_VERSION_STR};
use crate::contact::{Contact, ContactId, Origin, import_vcard, mark_contact_id_as_verified}; use crate::contact::{Contact, ContactId, Origin, import_vcard};
use crate::context::Context; use crate::context::Context;
use crate::key::{DcKey, load_self_public_key}; use crate::key::{DcKey, load_self_public_key};
use crate::log::LogExt; use crate::log::LogExt;
@@ -52,21 +52,12 @@ struct Statistics {
sending_disabled_timestamps: Vec<i64>, sending_disabled_timestamps: Vec<i64>,
} }
#[derive(Serialize, PartialEq)]
enum VerifiedStatus {
Direct,
Transitive,
TransitiveViaBot,
Opportunistic,
Unencrypted,
}
#[derive(Serialize)] #[derive(Serialize)]
struct ContactStat { struct ContactStat {
#[serde(skip_serializing)] #[serde(skip_serializing)]
id: ContactId, id: ContactId,
verified: VerifiedStatus, encrypted: bool,
// If one of the boolean properties is false, // If one of the boolean properties is false,
// we leave them away. // we leave them away.
@@ -79,9 +70,6 @@ struct ContactStat {
last_seen: u64, last_seen: u64,
#[serde(skip_serializing_if = "Option::is_none")]
transitive_chain: Option<u32>,
/// Whether the contact was established after stats-sending was enabled /// Whether the contact was established after stats-sending was enabled
#[serde(skip_serializing_if = "is_false")] #[serde(skip_serializing_if = "is_false")]
new: bool, new: bool,
@@ -93,8 +81,7 @@ fn is_false(b: &bool) -> bool {
#[derive(Serialize, Default)] #[derive(Serialize, Default)]
struct MessageStats { struct MessageStats {
verified: u32, encrypted: u32,
unverified_encrypted: u32,
unencrypted: u32, unencrypted: u32,
only_to_self: u32, only_to_self: u32,
} }
@@ -159,14 +146,10 @@ struct JoinedInvite {
/// Whether the contact already existed before. /// Whether the contact already existed before.
/// If this is false, then a contact was newly created. /// If this is false, then a contact was newly created.
already_existed: bool, already_existed: bool,
/// If a contact already existed,
/// this tells us whether the contact was verified already.
already_verified: bool,
/// The type of the invite: /// The type of the invite:
/// "contact" for single chat invites that setup a verified contact, /// "contact" for single chat invites,
/// "group" for invites that invite to a group, /// "group" for invites that invite to a group,
/// "broadcast" for invites that invite to a broadcast channel. /// "broadcast" for invites that invite to a broadcast channel.
/// The invite also performs the contact verification 'along the way'.
typ: String, typ: String,
} }
@@ -423,7 +406,6 @@ async fn get_stats_chat_id(context: &Context) -> Result<ChatId, anyhow::Error> {
.await? .await?
.first() .first()
.context("Statistics bot vCard does not contain a contact")?; .context("Statistics bot vCard does not contain a contact")?;
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
let chat_id = if let Some(res) = ChatId::lookup_by_contact(context, contact_id).await? { let chat_id = if let Some(res) = ChatId::lookup_by_contact(context, contact_id).await? {
// Already exists, no need to create. // Already exists, no need to create.
@@ -438,84 +420,30 @@ async fn get_stats_chat_id(context: &Context) -> Result<ChatId, anyhow::Error> {
} }
async fn get_contact_stats(context: &Context, last_old_contact: u32) -> Result<Vec<ContactStat>> { async fn get_contact_stats(context: &Context, last_old_contact: u32) -> Result<Vec<ContactStat>> {
let mut verified_by_map: BTreeMap<ContactId, ContactId> = BTreeMap::new();
let mut bot_ids: BTreeSet<ContactId> = BTreeSet::new();
let mut contacts = context let mut contacts = context
.sql .sql
.query_map_vec( .query_map_vec(
"SELECT id, fingerprint<>'', verifier, last_seen, is_bot FROM contacts c "SELECT id, fingerprint<>'', last_seen, is_bot FROM contacts c
WHERE id>9 AND origin>? AND addr<>?", WHERE id>9 AND origin>? AND addr<>?",
(Origin::Hidden, STATISTICS_BOT_EMAIL), (Origin::Hidden, STATISTICS_BOT_EMAIL),
|row| { |row| {
let id = row.get(0)?; let id = row.get(0)?;
let is_encrypted: bool = row.get(1)?; let encrypted: bool = row.get(1)?;
let verifier: ContactId = row.get(2)?; let last_seen: u64 = row.get(2)?;
let last_seen: u64 = row.get(3)?; let bot: bool = row.get(3)?;
let bot: bool = row.get(4)?;
let verified = match (is_encrypted, verifier) {
(true, ContactId::SELF) => VerifiedStatus::Direct,
(true, ContactId::UNDEFINED) => VerifiedStatus::Opportunistic,
(true, _) => VerifiedStatus::Transitive, // TransitiveViaBot will be filled later
(false, _) => VerifiedStatus::Unencrypted,
};
if verifier != ContactId::UNDEFINED {
verified_by_map.insert(id, verifier);
}
if bot {
bot_ids.insert(id);
}
Ok(ContactStat { Ok(ContactStat {
id, id,
verified, encrypted,
bot, bot,
single_chat: false, // will be filled later single_chat: false, // will be filled later
last_seen, last_seen,
transitive_chain: None, // will be filled later
new: id.to_u32() > last_old_contact, new: id.to_u32() > last_old_contact,
}) })
}, },
) )
.await?; .await?;
// Fill TransitiveViaBot and transitive_chain
for contact in &mut contacts {
if contact.verified == VerifiedStatus::Transitive {
let mut transitive_chain: u32 = 0;
let mut has_bot = false;
let mut current_verifier_id = contact.id;
while current_verifier_id != ContactId::SELF && transitive_chain < 100 {
current_verifier_id = match verified_by_map.get(&current_verifier_id) {
Some(id) => *id,
None => {
// The chain ends here, probably because some verification was done
// before we started recording verifiers.
// It's unclear how long the chain really is.
transitive_chain = 0;
break;
}
};
if bot_ids.contains(&current_verifier_id) {
has_bot = true;
}
transitive_chain = transitive_chain.saturating_add(1);
}
if transitive_chain > 0 {
contact.transitive_chain = Some(transitive_chain);
}
if has_bot {
contact.verified = VerifiedStatus::TransitiveViaBot;
}
}
}
// Fill single_chat // Fill single_chat
for contact in &mut contacts { for contact in &mut contacts {
let single_chat = context let single_chat = context
@@ -537,18 +465,16 @@ async fn get_message_stats(context: &Context) -> Result<BTreeMap<Chattype, Messa
let mut map: BTreeMap<Chattype, MessageStats> = context let mut map: BTreeMap<Chattype, MessageStats> = context
.sql .sql
.query_map_collect( .query_map_collect(
"SELECT chattype, verified, unverified_encrypted, unencrypted, only_to_self "SELECT chattype, unverified_encrypted, unencrypted, only_to_self
FROM stats_msgs", FROM stats_msgs",
(), (),
|row| { |row| {
let chattype: Chattype = row.get(0)?; let chattype: Chattype = row.get(0)?;
let verified: u32 = row.get(1)?; let encrypted: u32 = row.get(1)?;
let unverified_encrypted: u32 = row.get(2)?; let unencrypted: u32 = row.get(2)?;
let unencrypted: u32 = row.get(3)?; let only_to_self: u32 = row.get(3)?;
let only_to_self: u32 = row.get(4)?;
let message_stats = MessageStats { let message_stats = MessageStats {
verified, encrypted,
unverified_encrypted,
unencrypted, unencrypted,
only_to_self, only_to_self,
}; };
@@ -620,32 +546,6 @@ async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Re
(), (),
)?; )?;
// This table will hold all verified chats,
// i.e. all chats that only contain verified contacts.
t.execute(
"CREATE TEMP TABLE temp.verified_chats (
id INTEGER PRIMARY KEY
) STRICT",
(),
)?;
// Verified chats are chats that are not empty,
// and do not contain any unverified contacts
t.execute(
"INSERT INTO temp.verified_chats
SELECT id FROM chats
WHERE id>9
AND id NOT IN (SELECT id FROM temp.empty_chats)
AND NOT EXISTS(
SELECT *
FROM contacts, chats_contacts
WHERE chats_contacts.contact_id=contacts.id AND chats_contacts.chat_id=chats.id
AND contacts.id>9
AND contacts.verifier=0
)",
(),
)?;
// This table will hold all single chats. // This table will hold all single chats.
t.execute( t.execute(
"CREATE TEMP TABLE temp.chat_with_correct_type ( "CREATE TEMP TABLE temp.chat_with_correct_type (
@@ -672,21 +572,11 @@ async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Re
.to_string(); .to_string();
let params = (last_counted_msg_id, ContactId::SELF, stats_bot_chat_id); let params = (last_counted_msg_id, ContactId::SELF, stats_bot_chat_id);
let verified: u32 = t.query_row( let encrypted: u32 = t.query_row(
&format!(
"SELECT COUNT(*) FROM msgs
WHERE chat_id IN temp.verified_chats
AND {general_requirements}"
),
params,
|row| row.get(0),
)?;
let unverified_encrypted: u32 = t.query_row(
&format!( &format!(
// (param GLOB '*\nc=1*' OR param GLOB 'c=1*') matches all messages that are end-to-end encrypted // (param GLOB '*\nc=1*' OR param GLOB 'c=1*') matches all messages that are end-to-end encrypted
"SELECT COUNT(*) FROM msgs "SELECT COUNT(*) FROM msgs
WHERE chat_id NOT IN temp.verified_chats AND chat_id NOT IN temp.empty_chats WHERE chat_id NOT IN temp.empty_chats
AND (param GLOB '*\nc=1*' OR param GLOB 'c=1*') AND (param GLOB '*\nc=1*' OR param GLOB 'c=1*')
AND {general_requirements}" AND {general_requirements}"
), ),
@@ -697,7 +587,7 @@ async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Re
let unencrypted: u32 = t.query_row( let unencrypted: u32 = t.query_row(
&format!( &format!(
"SELECT COUNT(*) FROM msgs "SELECT COUNT(*) FROM msgs
WHERE chat_id NOT IN temp.verified_chats AND chat_id NOT IN temp.empty_chats WHERE chat_id NOT IN temp.empty_chats
AND NOT (param GLOB '*\nc=1*' OR param GLOB 'c=1*') AND NOT (param GLOB '*\nc=1*' OR param GLOB 'c=1*')
AND {general_requirements}" AND {general_requirements}"
), ),
@@ -715,7 +605,6 @@ async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Re
|row| row.get(0), |row| row.get(0),
)?; )?;
t.execute("DROP TABLE temp.verified_chats", ())?;
t.execute("DROP TABLE temp.empty_chats", ())?; t.execute("DROP TABLE temp.empty_chats", ())?;
t.execute("DROP TABLE temp.chat_with_correct_type", ())?; t.execute("DROP TABLE temp.chat_with_correct_type", ())?;
@@ -726,18 +615,11 @@ async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Re
)?; )?;
t.execute( t.execute(
"UPDATE stats_msgs SET "UPDATE stats_msgs SET
verified=verified+?,
unverified_encrypted=unverified_encrypted+?, unverified_encrypted=unverified_encrypted+?,
unencrypted=unencrypted+?, unencrypted=unencrypted+?,
only_to_self=only_to_self+? only_to_self=only_to_self+?
WHERE chattype=?", WHERE chattype=?",
( (encrypted, unencrypted, only_to_self, chattype),
verified,
unverified_encrypted,
unencrypted,
only_to_self,
chattype,
),
)?; )?;
Ok(()) Ok(())
@@ -851,9 +733,6 @@ pub(crate) async fn count_securejoin_invite(context: &Context, invite: &QrInvite
// then its origin is UnhandledSecurejoinQrScan. // then its origin is UnhandledSecurejoinQrScan.
let already_existed = contact.origin > Origin::UnhandledSecurejoinQrScan; let already_existed = contact.origin > Origin::UnhandledSecurejoinQrScan;
// Check whether the contact was verified already before the QR scan.
let already_verified = contact.is_verified(context).await?;
let typ = match invite { let typ = match invite {
QrInvite::Contact { .. } => "contact", QrInvite::Contact { .. } => "contact",
QrInvite::Group { .. } => "group", QrInvite::Group { .. } => "group",
@@ -863,9 +742,10 @@ pub(crate) async fn count_securejoin_invite(context: &Context, invite: &QrInvite
context context
.sql .sql
.execute( .execute(
// already_verified is unused but NOT NULL without a default.
"INSERT INTO stats_securejoin_invites (already_existed, already_verified, type) "INSERT INTO stats_securejoin_invites (already_existed, already_verified, type)
VALUES (?, ?, ?)", VALUES (?, 0, ?)",
(already_existed, already_verified, typ), (already_existed, typ),
) )
.await?; .await?;
@@ -876,16 +756,14 @@ async fn get_securejoin_invite_stats(context: &Context) -> Result<Vec<JoinedInvi
context context
.sql .sql
.query_map_vec( .query_map_vec(
"SELECT already_existed, already_verified, type FROM stats_securejoin_invites", "SELECT already_existed, type FROM stats_securejoin_invites",
(), (),
|row| { |row| {
let already_existed: bool = row.get(0)?; let already_existed: bool = row.get(0)?;
let already_verified: bool = row.get(1)?; let typ: String = row.get(1)?;
let typ: String = row.get(2)?;
Ok(JoinedInvite { Ok(JoinedInvite {
already_existed, already_existed,
already_verified,
typ, typ,
}) })
}, },
+9 -26
View File
@@ -30,9 +30,6 @@ async fn test_maybe_send_stats() -> Result<()> {
assert!(chat.is_encrypted(alice).await?); assert!(chat.is_encrypted(alice).await?);
let contacts = get_chat_contacts(alice, chat_id).await?; let contacts = get_chat_contacts(alice, chat_id).await?;
assert_eq!(contacts.len(), 1); assert_eq!(contacts.len(), 1);
let contact = Contact::get_by_id(alice, contacts[0]).await?;
assert!(contact.is_verified(alice).await?);
let msg = get_chat_msg(alice, chat_id, 1, 2).await; let msg = get_chat_msg(alice, chat_id, 1, 2).await;
assert_eq!(msg.get_filename().unwrap(), "statistics.txt"); assert_eq!(msg.get_filename().unwrap(), "statistics.txt");
@@ -113,10 +110,9 @@ async fn test_stats_one_contact() -> Result<()> {
contact_info.get("direct_chat").unwrap(), contact_info.get("direct_chat").unwrap(),
&serde_json::Value::Bool(true) &serde_json::Value::Bool(true)
); );
assert!(contact_info.get("transitive_chain").is_none(),);
assert_eq!( assert_eq!(
contact_info.get("verified").unwrap(), contact_info.get("encrypted").unwrap(),
&serde_json::Value::String("Opportunistic".to_string()) &serde_json::Value::Bool(true)
); );
assert_eq!( assert_eq!(
contact_info.get("new").unwrap(), contact_info.get("new").unwrap(),
@@ -168,25 +164,16 @@ async fn test_message_stats() -> Result<()> {
check_stats(&update_get_stats(alice).await, &expected); check_stats(&update_get_stats(alice).await, &expected);
alice.send_text(encrypted_chat.id, "foo").await; alice.send_text(encrypted_chat.id, "foo").await;
expected expected.get_mut(&Chattype::Single).unwrap().encrypted += 1;
.get_mut(&Chattype::Single)
.unwrap()
.unverified_encrypted += 1;
check_stats(&update_get_stats(alice).await, &expected); check_stats(&update_get_stats(alice).await, &expected);
alice.send_text(encrypted_chat.id, "foo").await; alice.send_text(encrypted_chat.id, "foo").await;
expected expected.get_mut(&Chattype::Single).unwrap().encrypted += 1;
.get_mut(&Chattype::Single)
.unwrap()
.unverified_encrypted += 1;
check_stats(&update_get_stats(alice).await, &expected); check_stats(&update_get_stats(alice).await, &expected);
let group = alice.create_group_with_members("Pizza", &[bob]).await; let group = alice.create_group_with_members("Pizza", &[bob]).await;
alice.send_text(group, "foo").await; alice.send_text(group, "foo").await;
expected expected.get_mut(&Chattype::Group).unwrap().encrypted += 1;
.get_mut(&Chattype::Group)
.unwrap()
.unverified_encrypted += 1;
check_stats(&update_get_stats(alice).await, &expected); check_stats(&update_get_stats(alice).await, &expected);
tcm.execute_securejoin(alice, bob).await; tcm.execute_securejoin(alice, bob).await;
@@ -208,7 +195,7 @@ async fn test_message_stats() -> Result<()> {
let group = alice.create_group_with_members("Pizza 2", &[bob]).await; let group = alice.create_group_with_members("Pizza 2", &[bob]).await;
alice.send_text(group, "foo").await; alice.send_text(group, "foo").await;
expected.get_mut(&Chattype::Group).unwrap().verified += 1; expected.get_mut(&Chattype::Group).unwrap().encrypted += 1;
check_stats(&update_get_stats(alice).await, &expected); check_stats(&update_get_stats(alice).await, &expected);
let empty_broadcast = create_broadcast(alice, "Channel".to_string()).await?; let empty_broadcast = create_broadcast(alice, "Channel".to_string()).await?;
@@ -245,13 +232,13 @@ async fn test_message_stats() -> Result<()> {
SystemTime::shift(Duration::from_secs(8 * 24 * 3600)); SystemTime::shift(Duration::from_secs(8 * 24 * 3600));
tcm.send_recv(alice, bob, "Hi").await; tcm.send_recv(alice, bob, "Hi").await;
expected.get_mut(&Chattype::Single).unwrap().verified += 1; expected.get_mut(&Chattype::Single).unwrap().encrypted += 1;
update_message_stats(alice).await?; update_message_stats(alice).await?;
update_message_stats(alice).await?; update_message_stats(alice).await?;
tcm.send_recv(alice, bob, "Hi").await; tcm.send_recv(alice, bob, "Hi").await;
expected.get_mut(&Chattype::Single).unwrap().verified += 1; expected.get_mut(&Chattype::Single).unwrap().encrypted += 1;
tcm.send_recv(alice, bob, "Hi").await; tcm.send_recv(alice, bob, "Hi").await;
expected.get_mut(&Chattype::Single).unwrap().verified += 1; expected.get_mut(&Chattype::Single).unwrap().encrypted += 1;
check_stats(&send_and_read_stats(alice).await, &expected); check_stats(&send_and_read_stats(alice).await, &expected);
alice.assert_warn("Missing securejoin source").await; alice.assert_warn("Missing securejoin source").await;
@@ -428,7 +415,6 @@ async fn test_stats_securejoin_invites() -> Result<()> {
tcm.exec_securejoin_qr(alice, bob, &qr).await; tcm.exec_securejoin_qr(alice, bob, &qr).await;
expected.push(JoinedInvite { expected.push(JoinedInvite {
already_existed: false, already_existed: false,
already_verified: false,
typ: "contact".to_string(), typ: "contact".to_string(),
}); });
check_stats(alice, &expected).await; check_stats(alice, &expected).await;
@@ -437,7 +423,6 @@ async fn test_stats_securejoin_invites() -> Result<()> {
tcm.exec_securejoin_qr(alice, bob, &qr).await; tcm.exec_securejoin_qr(alice, bob, &qr).await;
expected.push(JoinedInvite { expected.push(JoinedInvite {
already_existed: true, already_existed: true,
already_verified: true,
typ: "contact".to_string(), typ: "contact".to_string(),
}); });
check_stats(alice, &expected).await; check_stats(alice, &expected).await;
@@ -449,7 +434,6 @@ async fn test_stats_securejoin_invites() -> Result<()> {
tcm.exec_securejoin_qr(alice, bob, &qr).await; tcm.exec_securejoin_qr(alice, bob, &qr).await;
expected.push(JoinedInvite { expected.push(JoinedInvite {
already_existed: true, already_existed: true,
already_verified: true,
typ: "group".to_string(), typ: "group".to_string(),
}); });
check_stats(alice, &expected).await; check_stats(alice, &expected).await;
@@ -463,7 +447,6 @@ async fn test_stats_securejoin_invites() -> Result<()> {
tcm.exec_securejoin_qr(alice, bob, &qr).await; tcm.exec_securejoin_qr(alice, bob, &qr).await;
expected.push(JoinedInvite { expected.push(JoinedInvite {
already_existed: true, already_existed: true,
already_verified: false,
typ: "group".to_string(), typ: "group".to_string(),
}); });
check_stats(alice, &expected).await; check_stats(alice, &expected).await;
+1 -7
View File
@@ -69,6 +69,7 @@ pub enum StockMessage {
#[strum(props(fallback = "Fingerprints"))] #[strum(props(fallback = "Fingerprints"))]
FingerPrints = 30, FingerPrints = 30,
/// Unused. Was used in status messages.
#[strum(props(fallback = "%1$s verified."))] #[strum(props(fallback = "%1$s verified."))]
ContactVerified = 35, ContactVerified = 35,
@@ -873,13 +874,6 @@ pub(crate) fn secure_join_broadcast_qr_description(context: &Context, chat: &Cha
translated(context, StockMessage::SecureJoinBrodcastQRDescription).replace1(chat.get_name()) translated(context, StockMessage::SecureJoinBrodcastQRDescription).replace1(chat.get_name())
} }
/// Stock string: `%1$s verified.`.
#[allow(dead_code)]
pub(crate) fn contact_verified(context: &Context, contact: &Contact) -> String {
let addr = contact.get_display_name();
translated(context, StockMessage::ContactVerified).replace1(addr)
}
/// Stock string: `Archived chats`. /// Stock string: `Archived chats`.
pub(crate) fn archived_chats(context: &Context) -> String { pub(crate) fn archived_chats(context: &Context) -> String {
translated(context, StockMessage::ArchivedChats) translated(context, StockMessage::ArchivedChats)
+4 -5
View File
@@ -48,12 +48,11 @@ async fn test_stock_str() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_stock_string_repl_str() { async fn test_stock_string_repl_str() {
let t = TestContext::new().await; let t = TestContext::new().await;
let contact_id = Contact::create(&t.ctx, "Someone", "someone@example.org")
.await
.unwrap();
let contact = Contact::get_by_id(&t.ctx, contact_id).await.unwrap();
// uses %1$s substitution // uses %1$s substitution
assert_eq!(contact_verified(&t, &contact), "Someone verified."); assert_eq!(
subject_for_new_contact(&t, "Someone"),
"Message from Someone"
);
// We have no string using %1$d to test... // We have no string using %1$d to test...
} }
+1 -45
View File
@@ -30,9 +30,7 @@ use crate::chat::{
use crate::chatlist::Chatlist; use crate::chatlist::Chatlist;
use crate::config::Config; use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_NO_SPECIALS}; use crate::constants::{Blocked, Chattype, DC_GCL_NO_SPECIALS};
use crate::contact::{ use crate::contact::{Contact, ContactId, Modifier, Origin, import_vcard, make_vcard};
Contact, ContactId, Modifier, Origin, import_vcard, make_vcard, mark_contact_id_as_verified,
};
use crate::context::Context; use crate::context::Context;
use crate::events::{Event, EventEmitter, EventType, Events}; use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{self, DcKey, self_fingerprint}; use crate::key::{self, DcKey, self_fingerprint};
@@ -942,27 +940,6 @@ ORDER BY id"
contact_id contact_id
} }
/// Returns a single [`Chat`] with another account address-contact.
/// Panics if it doesn't exist.
/// May return a blocked chat.
///
/// This first creates a contact using the configured details on the other account, then
/// gets the single chat with this contact.
pub async fn get_email_chat(&self, other: &TestContext) -> Chat {
let contact = self.add_or_lookup_address_contact(other).await;
let chat_id = ChatIdBlocked::lookup_by_contact(&self.ctx, contact.id)
.await
.unwrap()
.map(|chat_id_blocked| chat_id_blocked.id)
.expect(
"There is no chat with this contact. \
Hint: Use create_email_chat() instead of get_email_chat() if this is expected.",
);
Chat::load_from_db(&self.ctx, chat_id).await.unwrap()
}
/// Returns a single [`Chat`] with another account key-contact. /// Returns a single [`Chat`] with another account key-contact.
/// Panics if the chat does not exist. /// Panics if the chat does not exist.
/// ///
@@ -1207,18 +1184,6 @@ ORDER BY id"
chat_id chat_id
} }
/// Set the legacy `protected` column in the chats table to 1,
/// because for now, only these chats that were once protected can be used
/// to gossip verifications.
// TODO remove the next statement
// when we send the _verified header for all verified contacts
pub(crate) async fn set_chat_protected(self: &TestContext, chat_id: chat::ChatId) {
self.sql
.execute("UPDATE chats SET protected=1 WHERE id=?", (chat_id,))
.await
.unwrap();
}
/// Allow reception of unencrypted messages. /// Allow reception of unencrypted messages.
pub async fn allow_unencrypted(&self) -> Result<()> { pub async fn allow_unencrypted(&self) -> Result<()> {
self.set_config_bool(Config::ForceEncryption, false).await?; self.set_config_bool(Config::ForceEncryption, false).await?;
@@ -1263,7 +1228,6 @@ pub async fn encrypt_raw_message(
addr: context.get_primary_self_addr().await?, addr: context.get_primary_self_addr().await?,
public_key: public_key.clone(), public_key: public_key.clone(),
prefer_encrypt: EncryptPreference::Mutual, prefer_encrypt: EncryptPreference::Mutual,
verified: false,
}; };
let mut encryption_keyring = vec![public_key.clone()]; let mut encryption_keyring = vec![public_key.clone()];
@@ -1762,14 +1726,6 @@ pub(crate) async fn get_chat_msg(
Message::load_from_db(&t.ctx, msg_id).await.unwrap() Message::load_from_db(&t.ctx, msg_id).await.unwrap()
} }
/// Saves the other account's public key as verified
pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) {
let contact_id = this.add_or_lookup_contact_id(other).await;
mark_contact_id_as_verified(this, contact_id, Some(ContactId::SELF))
.await
.unwrap();
}
/// Pops a sync message from alice0 and receives it on alice1. Should be used after an action on /// Pops a sync message from alice0 and receives it on alice1. Should be used after an action on
/// alice0's side that implies sending a sync message. /// alice0's side that implies sending a sync message.
pub(crate) async fn sync(alice0: &TestContext, alice1: &TestContext) { pub(crate) async fn sync(alice0: &TestContext, alice1: &TestContext) {
+5 -40
View File
@@ -16,7 +16,6 @@ use crate::receive_imf::receive_imf;
use crate::securejoin::get_securejoin_qr; use crate::securejoin::get_securejoin_qr;
use crate::test_utils::TestContext; use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager; use crate::test_utils::TestContextManager;
use crate::test_utils::mark_as_verified;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_change_primary_self_addr() -> Result<()> { async fn test_change_primary_self_addr() -> Result<()> {
@@ -46,35 +45,21 @@ async fn test_change_primary_self_addr() -> Result<()> {
enum ChatForTransition { enum ChatForTransition {
Single, Single,
GroupChat, GroupChat,
VerifiedGroup,
} }
use ChatForTransition::*; use ChatForTransition::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_0() { async fn test_aeap_transition_0() {
check_aeap_transition(Single, false).await; check_aeap_transition(Single).await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_1() { async fn test_aeap_transition_1() {
check_aeap_transition(GroupChat, false).await; check_aeap_transition(GroupChat).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_0_verified() {
check_aeap_transition(Single, true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_1_verified() {
check_aeap_transition(GroupChat, true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_aeap_transition_2_verified() {
check_aeap_transition(VerifiedGroup, true).await;
} }
/// Happy path test for AEAP. /// Happy path test for AEAP.
/// - `chat_for_transition`: Which chat the transition message should be sent in /// - `chat_for_transition`: Which chat the transition message should be sent in
/// - `verified`: Whether Alice and Bob verified each other async fn check_aeap_transition(chat_for_transition: ChatForTransition) {
async fn check_aeap_transition(chat_for_transition: ChatForTransition, verified: bool) {
const ALICE_NEW_ADDR: &str = "alice2@example.net"; const ALICE_NEW_ADDR: &str = "alice2@example.net";
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
@@ -84,19 +69,10 @@ async fn check_aeap_transition(chat_for_transition: ChatForTransition, verified:
tcm.send_recv_accept(alice, bob, "Hi").await; tcm.send_recv_accept(alice, bob, "Hi").await;
tcm.send_recv(bob, alice, "Hi back").await; tcm.send_recv(bob, alice, "Hi back").await;
if verified { let groups = vec![
mark_as_verified(alice, bob).await;
mark_as_verified(bob, alice).await;
}
let mut groups = vec![
chat::create_group(bob, "Group 0").await.unwrap(), chat::create_group(bob, "Group 0").await.unwrap(),
chat::create_group(bob, "Group 1").await.unwrap(), chat::create_group(bob, "Group 1").await.unwrap(),
]; ];
if verified {
groups.push(chat::create_group(bob, "Group 2").await.unwrap());
groups.push(chat::create_group(bob, "Group 3").await.unwrap());
}
let alice_contact = bob.add_or_lookup_contact_id(alice).await; let alice_contact = bob.add_or_lookup_contact_id(alice).await;
for group in &groups { for group in &groups {
@@ -111,13 +87,6 @@ async fn check_aeap_transition(chat_for_transition: ChatForTransition, verified:
let sent = bob.send_text(groups[1], "group created").await; let sent = bob.send_text(groups[1], "group created").await;
let group1_alice = alice.recv_msg(&sent).await.chat_id; let group1_alice = alice.recv_msg(&sent).await.chat_id;
let mut group3_alice = None;
if verified {
tcm.section("Promoting group 3");
let sent = bob.send_text(groups[3], "group created").await;
group3_alice = Some(alice.recv_msg(&sent).await.chat_id);
}
tcm.change_addr(alice, ALICE_NEW_ADDR).await; tcm.change_addr(alice, ALICE_NEW_ADDR).await;
tcm.section("Alice sends another message to Bob, this time from her new addr"); tcm.section("Alice sends another message to Bob, this time from her new addr");
@@ -125,7 +94,6 @@ async fn check_aeap_transition(chat_for_transition: ChatForTransition, verified:
let chat_to_send = match chat_for_transition { let chat_to_send = match chat_for_transition {
Single => alice.create_chat(bob).await.id, Single => alice.create_chat(bob).await.id,
GroupChat => group1_alice, GroupChat => group1_alice,
VerifiedGroup => group3_alice.expect("No verified group"),
}; };
let sent = alice let sent = alice
.send_text(chat_to_send, "Hello from my new addr!") .send_text(chat_to_send, "Hello from my new addr!")
@@ -240,16 +208,13 @@ async fn test_write_to_alice_after_aeap() -> Result<()> {
let alice_grp_id = chat::create_group(alice, "Group").await?; let alice_grp_id = chat::create_group(alice, "Group").await?;
let qr = get_securejoin_qr(alice, Some(alice_grp_id)).await?; let qr = get_securejoin_qr(alice, Some(alice_grp_id)).await?;
tcm.exec_securejoin_qr(bob, alice, &qr).await; tcm.exec_securejoin_qr(bob, alice, &qr).await;
let bob_alice_contact = bob.add_or_lookup_contact(alice).await; let _bob_alice_contact = bob.add_or_lookup_contact(alice).await;
assert!(bob_alice_contact.is_verified(bob).await?);
let bob_alice_chat = bob.create_chat(alice).await; let bob_alice_chat = bob.create_chat(alice).await;
let bob_unprotected_grp_id = bob.create_group_with_members("Group", &[alice]).await; let bob_unprotected_grp_id = bob.create_group_with_members("Group", &[alice]).await;
tcm.change_addr(alice, "alice@someotherdomain.xyz").await; tcm.change_addr(alice, "alice@someotherdomain.xyz").await;
let sent = alice.send_text(alice_grp_id, "Hello!").await; let sent = alice.send_text(alice_grp_id, "Hello!").await;
bob.recv_msg(&sent).await; bob.recv_msg(&sent).await;
assert!(bob_alice_contact.is_verified(bob).await?);
let bob_alice_chat = Chat::load_from_db(bob, bob_alice_chat.id).await?; let bob_alice_chat = Chat::load_from_db(bob, bob_alice_chat.id).await?;
let mut msg = Message::new_text("hi".to_string()); let mut msg = Message::new_text("hi".to_string());
chat::send_msg(bob, bob_alice_chat.id, &mut msg).await?; chat::send_msg(bob, bob_alice_chat.id, &mut msg).await?;
+18 -422
View File
@@ -6,144 +6,14 @@ use crate::chat::{self, Chat, add_contact_to_chat, remove_contact_from_chat, sen
use crate::config::Config; use crate::config::Config;
use crate::constants::Chattype; use crate::constants::Chattype;
use crate::contact::{Contact, ContactId}; use crate::contact::{Contact, ContactId};
use crate::key;
use crate::key::self_fingerprint; use crate::key::self_fingerprint;
use crate::message;
use crate::message::{Message, Viewtype}; use crate::message::{Message, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage; use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf; use crate::receive_imf::receive_imf;
use crate::securejoin::{get_securejoin_qr, join_securejoin}; use crate::securejoin::{get_securejoin_qr, join_securejoin};
use crate::stock_str; use crate::test_utils::{TestContextManager, get_chat_msg};
use crate::test_utils;
use crate::test_utils::{
E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified,
};
use crate::tools::SystemTime; use crate::tools::SystemTime;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_single_chat_not_broken_by_classical() {
check_verified_single_chat_protection_not_broken(true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_single_chat_not_broken_by_device_change() {
check_verified_single_chat_protection_not_broken(false).await;
}
async fn check_verified_single_chat_protection_not_broken(by_classical_email: bool) {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
tcm.execute_securejoin(&alice, &bob).await;
assert_verified(&alice, &bob).await;
assert_verified(&bob, &alice).await;
if by_classical_email {
tcm.section("Bob uses a classical MUA to send a message to Alice");
receive_imf(
&alice,
b"Subject: Re: Message from alice\r\n\
From: <bob@example.net>\r\n\
To: <alice@example.org>\r\n\
Date: Mon, 12 Dec 3000 14:33:39 +0000\r\n\
Message-ID: <abcd@example.net>\r\n\
\r\n\
Heyho!\r\n",
false,
)
.await
.unwrap()
.unwrap();
alice.assert_warn("unencrypted message").await;
let contact = alice.add_or_lookup_contact(&bob).await;
assert_eq!(contact.is_verified(&alice).await.unwrap(), true);
assert_verified(&alice, &bob).await;
} else {
tcm.section("Bob sets up another Delta Chat device");
let bob2 = tcm.unconfigured().await;
bob2.set_name("bob2");
bob2.configure_addr("bob@example.net").await;
SystemTime::shift(std::time::Duration::from_secs(3600));
tcm.send_recv(&bob2, &alice, "Using another device now")
.await;
let contact = alice.add_or_lookup_contact(&bob2).await;
assert_eq!(contact.is_verified(&alice).await.unwrap(), false);
assert_verified(&alice, &bob).await;
}
tcm.section("Bob sends another message from DC");
SystemTime::shift(std::time::Duration::from_secs(3600));
tcm.send_recv(&bob, &alice, "Using DC again").await;
// Bob's chat is marked as verified again
assert_verified(&alice, &bob).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_verified_single_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
tcm.execute_securejoin(&alice, &bob).await;
tcm.execute_securejoin(&bob, &fiona).await;
assert_verified(&alice, &bob).await;
assert_verified(&bob, &alice).await;
assert_verified(&bob, &fiona).await;
assert_verified(&fiona, &bob).await;
let group_id = bob
.create_group_with_members("Group with everyone", &[&alice, &fiona])
.await;
bob.set_chat_protected(group_id).await;
assert_eq!(
get_chat_msg(&bob, group_id, 0, 1).await.get_info_type(),
SystemMessage::ChatE2ee
);
{
let sent = bob.send_text(group_id, "Heyho").await;
alice.recv_msg(&sent).await;
let msg = fiona.recv_msg(&sent).await;
assert_eq!(
get_chat_msg(&fiona, msg.chat_id, 0, 2)
.await
.get_info_type(),
SystemMessage::ChatE2ee
);
}
// Alice and Fiona should now be verified because of gossip
let alice_fiona_contact = alice.add_or_lookup_contact(&fiona).await;
assert!(alice_fiona_contact.is_verified(&alice).await.unwrap(),);
tcm.section("Fiona reinstalls DC");
drop(fiona);
let fiona_new = tcm.unconfigured().await;
fiona_new.configure_addr("fiona@example.net").await;
key::ensure_secret_key_exists(&fiona_new).await?;
tcm.send_recv(&fiona_new, &alice, "I have a new device")
.await;
// Alice gets a new chat with new Fiona contact.
{
let chat = alice.get_chat(&fiona_new).await;
let msg = get_chat_msg(&alice, chat.id, 1, E2EE_INFO_MSGS + 1).await;
assert_eq!(msg.text, "I have a new device");
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_missing_key_reexecute_securejoin() -> Result<()> { async fn test_missing_key_reexecute_securejoin() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
@@ -168,108 +38,6 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> {
Ok(()) Ok(())
} }
/// Tests that receiving unencrypted message
/// does not disable protection of single chat.
///
/// Instead, an email-chat is created.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_degrade_verified_single_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
alice.allow_unencrypted().await?;
mark_as_verified(alice, bob).await;
let alice_chat = alice.create_chat(bob).await;
receive_imf(
alice,
b"From: Bob <bob@example.net>\r\n\
To: alice@example.org\r\n\
Message-ID: <1234-2@example.net>\r\n\
\r\n\
hello\r\n",
false,
)
.await?;
let msg0 = get_chat_msg(alice, alice_chat.id, 0, 1).await;
let enabled = stock_str::messages_e2ee_info_msg(alice);
assert_eq!(msg0.text, enabled);
assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatE2ee);
let email_chat = alice.get_email_chat(bob).await;
assert!(!email_chat.is_encrypted(alice).await?);
let email_msg = get_chat_msg(alice, email_chat.id, 0, 1).await;
assert_eq!(email_msg.text, "hello".to_string());
assert!(!email_msg.is_system_message());
Ok(())
}
/// Alice is offline for some time. mvbox_move is 0.
/// When she comes online, first her inbox is synced and then her mvbox.
/// This test tests that the messages are still in the right order.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_old_message_4() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let msg_incoming = test_utils::receive_encrypted_imf(
alice,
bob,
b"From: Bob <bob@example.net>\r\n\
To: alice@example.org\r\n\
Message-ID: <1234-2-3@example.org>\r\n\
Date: Sun, 08 Dec 2019 19:00:27 +0000\r\n\
\r\n\
Thanks, Alice!\r\n",
)
.await?;
let msg_sent = test_utils::receive_encrypted_imf(
alice,
alice,
b"From: alice@example.org\r\n\
To: Bob <bob@example.net>\r\n\
Message-ID: <1234-2-4@example.org>\r\n\
Date: Sat, 07 Dec 2019 19:00:27 +0000\r\n\
\r\n\
Happy birthday, Bob!\r\n",
)
.await?;
// The "Happy birthday" message should be shown first, and then the "Thanks" message
assert!(msg_sent.sort_timestamp < msg_incoming.sort_timestamp);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdn_doesnt_disable_verification() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
bob.set_config_bool(Config::MdnsEnabled, true).await?;
// Alice & Bob verify each other
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
let rcvd = tcm.send_recv_accept(&alice, &bob, "Heyho").await;
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
let mimefactory = MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?;
let rendered_msg = mimefactory.render(&bob).await?;
let body = rendered_msg.message;
receive_imf(&alice, body.as_bytes(), false).await.unwrap();
assert_verified(&alice, &bob).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_outgoing_mua_msg() -> Result<()> { async fn test_outgoing_mua_msg() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
@@ -277,11 +45,7 @@ async fn test_outgoing_mua_msg() -> Result<()> {
let bob = &tcm.bob().await; let bob = &tcm.bob().await;
alice.allow_unencrypted().await?; alice.allow_unencrypted().await?;
mark_as_verified(alice, bob).await;
mark_as_verified(bob, alice).await;
tcm.send_recv_accept(bob, alice, "Heyho from DC").await; tcm.send_recv_accept(bob, alice, "Heyho from DC").await;
assert_verified(alice, bob).await;
let sent = receive_imf( let sent = receive_imf(
alice, alice,
@@ -313,7 +77,6 @@ async fn test_outgoing_encrypted_msg() -> Result<()> {
let alice = &tcm.alice().await; let alice = &tcm.alice().await;
let bob = &tcm.bob().await; let bob = &tcm.bob().await;
mark_as_verified(alice, bob).await;
let chat_id = alice.create_chat(bob).await.id; let chat_id = alice.create_chat(bob).await.id;
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml"); let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
receive_imf(alice, raw, false).await?; receive_imf(alice, raw, false).await?;
@@ -327,23 +90,15 @@ async fn test_outgoing_encrypted_msg() -> Result<()> {
/// the message is under some circumstances still assigned to the original /// the message is under some circumstances still assigned to the original
/// chat (see lookup_chat_by_reply()); this is meant to make aliases /// chat (see lookup_chat_by_reply()); this is meant to make aliases
/// work nicely. /// work nicely.
/// However, if the original chat is verified, the unencrypted message /// However, the unencrypted message must NOT be assigned to an encrypted chat.
/// must NOT be assigned to it (it would be replaced by an error
/// message in the verified chat, so, this would just be a usability issue,
/// not a security issue).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reply() -> Result<()> { async fn test_reply() -> Result<()> {
for verified in [false, true] { {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
let alice = tcm.alice().await; let alice = tcm.alice().await;
let bob = tcm.bob().await; let bob = tcm.bob().await;
alice.allow_unencrypted().await?; alice.allow_unencrypted().await?;
if verified {
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
}
tcm.send_recv_accept(&bob, &alice, "Heyho from DC").await; tcm.send_recv_accept(&bob, &alice, "Heyho from DC").await;
let encrypted_msg = tcm.send_recv(&alice, &bob, "Heyho back").await; let encrypted_msg = tcm.send_recv(&alice, &bob, "Heyho back").await;
@@ -372,8 +127,7 @@ async fn test_reply() -> Result<()> {
Ok(()) Ok(())
} }
/// Tests that message from old DC setup does not break /// Tests that a message from an old DC setup does not break the new chat.
/// new verified chat.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_message_from_old_dc_setup() -> Result<()> { async fn test_message_from_old_dc_setup() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
@@ -381,7 +135,6 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
let bob_old = &tcm.unconfigured().await; let bob_old = &tcm.unconfigured().await;
bob_old.configure_addr("bob@example.net").await; bob_old.configure_addr("bob@example.net").await;
mark_as_verified(bob_old, alice).await;
let chat = bob_old.create_chat(alice).await; let chat = bob_old.create_chat(alice).await;
let sent_old = bob_old let sent_old = bob_old
.send_text(chat.id, "Soon i'll have a new device") .send_text(chat.id, "Soon i'll have a new device")
@@ -391,55 +144,20 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
tcm.section("Bob reinstalls DC"); tcm.section("Bob reinstalls DC");
let bob = &tcm.bob().await; let bob = &tcm.bob().await;
mark_as_verified(alice, bob).await;
mark_as_verified(bob, alice).await;
tcm.send_recv(bob, alice, "Now i have it!").await; tcm.send_recv(bob, alice, "Now i have it!").await;
assert_verified(alice, bob).await;
let msg = alice.recv_msg(&sent_old).await; let msg = alice.recv_msg(&sent_old).await;
assert!(msg.get_showpadlock()); assert!(msg.get_showpadlock());
let contact = alice.add_or_lookup_contact(bob).await; let contact = alice.add_or_lookup_contact(bob).await;
// The outdated Bob's Autocrypt header isn't applied // The outdated Bob's Autocrypt header isn't applied,
// and the message goes to another chat, so the verification preserves. // so the message goes to another chat.
assert!(contact.is_verified(alice).await.unwrap()); assert_ne!(contact.id, msg.from_id);
Ok(()) Ok(())
} }
/// Regression test for the following bug: /// Tests that on a second device the e2ee info message
/// /// sorts before the older message that created the group.
/// - Scan your chat partner's QR Code
/// - They change devices
/// - Scan their QR code again
///
/// -> The re-verification fails.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verify_then_verify_again() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
alice.create_chat(&bob).await;
assert_verified(&alice, &bob).await;
tcm.section("Bob reinstalls DC");
drop(bob);
let bob_new = tcm.unconfigured().await;
bob_new.configure_addr("bob@example.net").await;
key::ensure_secret_key_exists(&bob_new).await?;
tcm.execute_securejoin(&bob_new, &alice).await;
assert_verified(&alice, &bob_new).await;
Ok(())
}
/// Tests that on the second device of a group creator the first message is
/// `SystemMessage::ChatE2ee` and the second one is the message populating the group.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_grp_multidev() -> Result<()> { async fn test_create_grp_multidev() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
@@ -473,7 +191,7 @@ async fn test_create_grp_multidev() -> Result<()> {
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_member_added_reordering() -> Result<()> { async fn test_member_added_reordering() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await; let alice = &tcm.alice().await;
let bob = &tcm.bob().await; let bob = &tcm.bob().await;
@@ -502,7 +220,7 @@ async fn test_verified_member_added_reordering() -> Result<()> {
let bob_sent_message = bob.send_text(bob_chat_id, "Hi").await; let bob_sent_message = bob.send_text(bob_chat_id, "Hi").await;
// Fiona receives message from Bob before receiving // Fiona receives message from Bob before receiving
// "Member added" message, so unverified group is created. // the "Member added" message, so she cannot send yet.
let fiona_received_message = fiona.recv_msg(&bob_sent_message).await; let fiona_received_message = fiona.recv_msg(&bob_sent_message).await;
let fiona_chat = Chat::load_from_db(fiona, fiona_received_message.chat_id).await?; let fiona_chat = Chat::load_from_db(fiona, fiona_received_message.chat_id).await?;
assert!(!fiona_chat.can_send(fiona).await?); assert!(!fiona_chat.can_send(fiona).await?);
@@ -518,16 +236,12 @@ async fn test_verified_member_added_reordering() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_unencrypted_name_if_encrypted() -> Result<()> { async fn test_no_unencrypted_name_if_encrypted() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
for verified in [false, true] { {
let alice = tcm.alice().await; let alice = tcm.alice().await;
let bob = tcm.bob().await; let bob = tcm.bob().await;
bob.set_config(Config::Displayname, Some("Bob Smith")) bob.set_config(Config::Displayname, Some("Bob Smith"))
.await?; .await?;
if verified { tcm.send_recv_accept(&alice, &bob, "hi").await;
mark_as_verified(&bob, &alice).await;
} else {
tcm.send_recv_accept(&alice, &bob, "hi").await;
}
let chat_id = bob.create_chat(&alice).await.id; let chat_id = bob.create_chat(&alice).await.id;
let msg = &bob.send_text(chat_id, "hi").await; let msg = &bob.send_text(chat_id, "hi").await;
@@ -544,7 +258,7 @@ async fn test_no_unencrypted_name_if_encrypted() -> Result<()> {
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_lost_member_added() -> Result<()> { async fn test_lost_member_added() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await; let alice = &tcm.alice().await;
let bob = &tcm.bob().await; let bob = &tcm.bob().await;
@@ -609,10 +323,9 @@ async fn test_verified_lost_member_added() -> Result<()> {
Ok(()) Ok(())
} }
/// Tests handling of resent .xdc arriving before "Member added" /// Tests handling of resent .xdc arriving before "Member added".
/// in a verified group
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_chat_editor_reordering() -> Result<()> { async fn test_chat_editor_reordering() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await; let alice = &tcm.alice().await;
let bob = &tcm.bob().await; let bob = &tcm.bob().await;
@@ -620,7 +333,7 @@ async fn test_verified_chat_editor_reordering() -> Result<()> {
tcm.execute_securejoin(alice, bob).await; tcm.execute_securejoin(alice, bob).await;
tcm.section("Alice creates a protected group with Bob"); tcm.section("Alice creates a group with Bob");
let alice_chat_id = alice.create_group_with_members("Group", &[bob]).await; let alice_chat_id = alice.create_group_with_members("Group", &[bob]).await;
let alice_sent = alice.send_text(alice_chat_id, "Hi!").await; let alice_sent = alice.send_text(alice_chat_id, "Hi!").await;
let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id; let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id;
@@ -667,9 +380,6 @@ async fn test_verified_chat_editor_reordering() -> Result<()> {
tcm.section("Charlie receives resent webxdc before member added"); tcm.section("Charlie receives resent webxdc before member added");
let charlie_received_xdc = charlie.recv_msg(&bob.pop_sent_msg().await).await; let charlie_received_xdc = charlie.recv_msg(&bob.pop_sent_msg().await).await;
// The message should not be replaced with
// "The message was sent with non-verified encryption." text
// just because it was reordered.
assert_eq!(charlie_received_xdc.viewtype, Viewtype::Webxdc); assert_eq!(charlie_received_xdc.viewtype, Viewtype::Webxdc);
tcm.section("Charlie receives member added message"); tcm.section("Charlie receives member added message");
@@ -677,122 +387,8 @@ async fn test_verified_chat_editor_reordering() -> Result<()> {
charlie charlie
.golden_test_chat( .golden_test_chat(
charlie_received_xdc.chat_id, charlie_received_xdc.chat_id,
"verified_chats_editor_reordering", "encrypted_chats_editor_reordering",
) )
.await; .await;
Ok(()) Ok(())
} }
/// Tests that already verified contact
/// does not get a new "verifier"
/// via gossip.
///
/// Directly verifying is still possible.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_reverification() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let fiona = &tcm.fiona().await;
tcm.execute_securejoin(alice, bob).await;
tcm.execute_securejoin(alice, charlie).await;
tcm.execute_securejoin(alice, fiona).await;
tcm.section("Alice creates a protected group with Bob, Charlie and Fiona");
let alice_chat_id = alice
.create_group_with_members("Group", &[bob, charlie, fiona])
.await;
alice.set_chat_protected(alice_chat_id).await;
let alice_sent = alice.send_text(alice_chat_id, "Hi!").await;
let bob_rcvd_msg = bob.recv_msg(&alice_sent).await;
let bob_alice_id = bob_rcvd_msg.from_id;
// Charlie is verified by Alice for Bob.
let bob_charlie_contact = bob.add_or_lookup_contact(charlie).await;
assert_eq!(
bob_charlie_contact
.get_verifier_id(bob)
.await?
.unwrap()
.unwrap(),
bob_alice_id
);
let fiona_rcvd_msg = fiona.recv_msg(&alice_sent).await;
let fiona_chat_id = fiona_rcvd_msg.chat_id;
let fiona_sent = fiona.send_text(fiona_chat_id, "Post by Fiona").await;
bob.recv_msg(&fiona_sent).await;
// Charlie should still be verified by Alice, not by Fiona.
let bob_charlie_contact = bob.add_or_lookup_contact(charlie).await;
assert_eq!(
bob_charlie_contact
.get_verifier_id(bob)
.await?
.unwrap()
.unwrap(),
bob_alice_id
);
// Bob can still verify Charlie directly.
tcm.execute_securejoin(bob, charlie).await;
let bob_charlie_contact = bob.add_or_lookup_contact(charlie).await;
assert_eq!(
bob_charlie_contact
.get_verifier_id(bob)
.await?
.unwrap()
.unwrap(),
ContactId::SELF
);
Ok(())
}
/// Tests that if our second device observes
/// us gossiping a verification,
/// it is not treated as direct verification.
///
/// Direct verifications should only happen
/// as a result of SecureJoin.
/// If we see our second device gossiping
/// a verification of some contact,
/// it may be indirect verification,
/// so we should mark the contact as verified,
/// but with unknown verifier.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_single_verification_via_bcc() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice2 = &tcm.alice().await;
let bob = &tcm.bob().await;
mark_as_verified(alice, bob).await;
let alice_chat_id = alice.create_chat_id(bob).await;
alice.set_chat_protected(alice_chat_id).await;
let alice_sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
alice2.recv_msg(&alice_sent_msg).await;
// Alice 2 observes Alice 1 gossiping verification for Bob.
// Alice 2 does not know if Alice 1 has verified Bob directly though.
let alice2_bob_contact = alice2.add_or_lookup_contact(bob).await;
assert_eq!(alice2_bob_contact.is_verified(alice2).await?, true);
// There is some verifier, but it is unknown to Alice's second device.
assert_eq!(
alice2_bob_contact.get_verifier_id(alice2).await?,
Some(None)
);
Ok(())
}
// ============== Helper Functions ==============
async fn assert_verified(this: &TestContext, other: &TestContext) {
let contact = this.add_or_lookup_contact(other).await;
assert_eq!(contact.is_verified(this).await.unwrap(), true);
}
+1 -1
View File
@@ -2,7 +2,7 @@
//! //!
//! Functions to read/write token from/to the database. A token is any string associated with a key. //! Functions to read/write token from/to the database. A token is any string associated with a key.
//! //!
//! Tokens are used in SecureJoin verification protocols. //! Tokens are used in SecureJoin protocols.
use anyhow::Result; use anyhow::Result;
use deltachat_derive::{FromSql, ToSql}; use deltachat_derive::{FromSql, ToSql};
+1 -1
View File
@@ -15,7 +15,7 @@ Push | IMAP IDLE ([RFC 2177][])
Quota | IMAP QUOTA extension ([RFC 2087][]) Quota | IMAP QUOTA extension ([RFC 2087][])
Client/server identification | IMAP ID extension ([RFC 2971][]) Client/server identification | IMAP ID extension ([RFC 2971][])
End-to-end encryption | [Autocrypt Level 1][], OpenPGP ([RFC 4880][]), Security Multiparts for MIME ([RFC 1847][]) and [“Mixed Up” Encryption repairing](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-pgpmime-message-mangling-00) End-to-end encryption | [Autocrypt Level 1][], OpenPGP ([RFC 4880][]), Security Multiparts for MIME ([RFC 1847][]) and [“Mixed Up” Encryption repairing](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-pgpmime-message-mangling-00)
Detect/prevent active attacks | [securejoin][] protocols Invite links for chats | [securejoin][] protocols
Compare public keys | [openpgp4fpr][] URI Scheme Compare public keys | [openpgp4fpr][] URI Scheme
Metadata minimization | Header Protection for Cryptographically Protected Email ([RFC 9788][]) Metadata minimization | Header Protection for Cryptographically Protected Email ([RFC 9788][])
Configuration assistance | [Autoconfigure](https://web.archive.org/web/20210402044801/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover][] Configuration assistance | [Autoconfigure](https://web.archive.org/web/20210402044801/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover][]