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-08-29 23:41:13 +02:00
parent 373f1840a6
commit d8912a98ac
34 changed files with 203 additions and 1553 deletions

View File

@@ -173,7 +173,7 @@ const DB_COMMANDS: [&str; 10] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 39] = [
const CHAT_COMMANDS: [&str; 38] = [
"listchats",
"listarchived",
"start-realtime",
@@ -182,7 +182,6 @@ const CHAT_COMMANDS: [&str; 39] = [
"createchat",
"creategroup",
"createbroadcast",
"createprotected",
"addmember",
"removemember",
"groupname",

View File

@@ -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})")
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
assert alice_contact_bob_snapshot.e2ee_avail
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")
# 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})")
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
assert alice_contact_bob_snapshot.e2ee_avail
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")

View File

@@ -4,10 +4,9 @@ import pytest
from deltachat_rpc_client import Chat, EventType, SpecialContactId
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)
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()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.create_contact(bob)
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()
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.create_contact(alice)
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
# 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
assert bob_contact_alice_snapshot.e2ee_avail
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)
bob.wait_for_securejoin_joiner_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.create_contact(bob)
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()
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_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
assert bob_contact_alice_snapshot.e2ee_avail
# 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.wait_for_securejoin_inviter_success()
alice2_contact_bob = alice2.create_contact(bob)
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.
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
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()
assert contact_snapshot.is_verified
assert contact_snapshot.e2ee_avail
chat = get_broadcast(ac)
chat_msgs = chat.get_messages()
@@ -350,8 +329,8 @@ def test_setup_contact_resetup(acf) -> None:
bob.wait_for_securejoin_joiner_success()
def test_verified_group_member_added_recovery(acf) -> None:
"""Tests verified group recovery by reverifying then removing and adding a member back."""
def test_group_member_added_recovery(acf) -> None:
"""Tests group recovery after a member resets its key."""
ac1, ac2, ac3 = acf.get_online_accounts(3)
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.wait_for_securejoin_joiner_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.create_contact(ac2)
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
logging.info("ac3 joins the group")
ac3_chat = ac3.secure_join(qr_code)
ac3.wait_for_securejoin_joiner_success()
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")
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()
ac2.secure_join(qr_code)
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()
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):
"""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)
logging.info("ac3: verify with ac2")
logging.info("ac3: set up contact with ac2")
qr_code = ac2.get_qr_code()
ac3.secure_join(qr_code)
ac2.wait_for_securejoin_inviter_success()
# in order for ac2 to have pending bobstate with a verified group
# we first create a fully joined verified group, and then start
# in order for ac2 to have pending bobstate with a group
# we first create a fully joined group, and then start
# joining a second time but interrupt it, to create pending bob state
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)
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")
while 1:
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
@@ -459,11 +426,11 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
ac1.remove()
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
assert ac3.create_contact(ac2).get_snapshot().is_verified
assert ac2.create_contact(ac3).get_snapshot().is_verified
# we meanwhile expect the ac3/ac2 setup-contact started in the beginning to have completed
assert ac3.create_contact(ac2).get_snapshot().e2ee_avail
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.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):
"""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,
but ac1 contact is not blocked on ac2.
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")
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."""
ac1, ac2 = acf.get_online_accounts(2)
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")
qr_code = chat.get_qr_code()
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()]
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:
"""
Regression test for a bug that prevented joining verified group with a QR code
if the group is already created and contains
a contact with inconsistent (Autocrypt and verified keys exist but don't match) key state.
Regression test for a bug that prevented joining a group with a QR code
if the group already contains a contact with an inconsistent key state
while a securejoin with that contact is still pending.
"""
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")
# 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"))
ac1_qr_code = snapshot.chat.get_qr_code()
# ac2 verifies ac1
# ac2 sets up contact with ac1
qr_code = ac1.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
# ac1 is verified for ac2.
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 = acf.resetup_account(ac1)
ac2_contact_ac1 = ac2.create_contact(ac1, "")
assert not ac2_contact_ac1.get_snapshot().is_verified
# ac1 goes offline.
ac1.remove()
# Scanning a QR code results in creating an unprotected group with an inviter.
# In this case inviter is ac1 which has an inconsistent key state.
# Normally inviter becomes verified as a result of Securejoin protocol
# and then the group chat becomes verified when "Member added" is received,
# but in this case ac1 is offline and this Securejoin process will never finish.
# Scanning a QR code creates a group with the inviter, here ac1,
# which has an inconsistent key state. Normally the securejoin protocol
# would complete and "Member added" would arrive,
# but ac1 is offline so it never finishes.
logging.info("ac2 scans ac1 QR code, this is not expected to finish")
ac2.secure_join(ac1_qr_code)
@@ -664,16 +577,13 @@ def test_securejoin_after_contact_resetup(acf) -> None:
ac2_chat = snapshot.chat
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):
alice, bob = acf.get_online_accounts(2)
logging.info("Alice creates a 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()
bob_chat = bob.secure_join(qr_code)

View File

@@ -72,8 +72,7 @@ CREATE TABLE contacts (
-- empty string for "address-contacts".
fingerprint TEXT NOT NULL DEFAULT '',
-- ID of the contact that has "introduced" us to this contact
-- by sharing the key with a verified attribute or in a "verified" chat.
-- Unused. Was the ID of the contact that introduced this contact's key.
verifier INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE);
@@ -131,9 +130,7 @@ CREATE TABLE chats (
-- 0 means the timer is disabled.
ephemeral_timer INTEGER,
-- Deprecated, but still used to send Chat-Verified headers
-- for existing protected chats.
-- All new chats are created as "not protected".
-- Unused. Was 1 for protected chats.
protected INTEGER DEFAULT 0,
gossiped_timestamp INTEGER DEFAULT 0, -- deprecated 2025-04-08, replaced with gossip_timestamp table
@@ -730,13 +727,13 @@ CREATE TABLE stats_securejoin_uipaths(
) STRICT;
CREATE TABLE stats_securejoin_invites(
already_existed INTEGER NOT NULL,
already_verified INTEGER NOT NULL,
already_verified INTEGER NOT NULL, -- unused, always 0
type TEXT NOT NULL
) STRICT;
CREATE TABLE stats_msgs(
chattype INTEGER PRIMARY KEY,
verified INTEGER NOT NULL DEFAULT 0,
unverified_encrypted INTEGER NOT NULL DEFAULT 0,
verified INTEGER NOT NULL DEFAULT 0, -- unused, always 0
unverified_encrypted INTEGER NOT NULL DEFAULT 0, -- counts all encrypted messages
unencrypted INTEGER NOT NULL DEFAULT 0,
only_to_self INTEGER NOT NULL DEFAULT 0,
last_counted_msg_id INTEGER NOT NULL DEFAULT 0

View File

@@ -1,7 +1,5 @@
import time
import deltachat as dc
class TestGroupStressTests:
def test_group_many_members_add_leave_remove(self, acfactory, lp):
@@ -63,9 +61,9 @@ class TestGroupStressTests:
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)
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")
qr = chat1.get_join_qr()
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")
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()
assert msg.text == "hello"
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")
chat2.send_text("world")
msg = ac1._evtracker.wait_next_incoming_message()
@@ -109,23 +103,13 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert ch.id >= 10
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)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
assert msg.is_system_message()
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")
chat2.send_text("hi")
# System message about the added member.
@@ -195,10 +179,10 @@ def test_ephemeral_timer(acfactory, lp):
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:
- 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.
- 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.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")
qr = chat.get_join_qr()
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
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:
- 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'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.
- 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>
@@ -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")
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")
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")

View File

@@ -796,9 +796,8 @@ def test_send_and_receive_image(acfactory, lp, data):
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification
when the database already contained the contact with a different email address capitalization.
"""Tests joining a group via QR code
when the database already contains a contact with a different email address capitalization.
"""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
@@ -822,13 +821,7 @@ def test_qr_email_capitalization(acfactory, lp):
ac2.qr_join_chat(qr)
ac1._evtracker.wait_next_incoming_message()
# ac1 should see both ac3 and ac2 as verified.
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):

18
spec.md
View File

@@ -596,24 +596,6 @@ and e.g. simply search for the line starting with `EMAIL`
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
Messengers SHOULD use the header `In-Reply-To` as usual.

View File

@@ -43,13 +43,6 @@ pub struct Aheader {
pub addr: String,
pub public_key: SignedPublicKey,
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 {
@@ -58,12 +51,6 @@ impl fmt::Display for Aheader {
if self.prefer_encrypt == EncryptPreference::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
// email crate to wrap the lines according to RFC 5322
// (which may insert a linebreak before every whitespace)
@@ -114,8 +101,6 @@ impl Aheader {
.and_then(|raw| EncryptPreference::new(&raw).ok())
.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 attribute, treat the header as invalid
if attributes.keys().any(|k| !k.starts_with('_')) {
@@ -126,7 +111,6 @@ impl Aheader {
addr,
public_key,
prefer_encrypt,
verified,
})
}
}
@@ -145,7 +129,6 @@ mod tests {
assert_eq!(h.addr, "me@mail.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
assert_eq!(h.verified, false);
Ok(())
}
@@ -243,7 +226,6 @@ mod tests {
addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::Mutual,
verified: false
}
)
.contains("prefer-encrypt=mutual;")
@@ -259,7 +241,6 @@ mod tests {
addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::NoPreference,
verified: false
}
)
.contains("prefer-encrypt")
@@ -273,24 +254,9 @@ mod tests {
addr: "TeSt@eXaMpLe.cOm".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::Mutual,
verified: false
}
)
.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")
);
}
}

View File

@@ -2683,7 +2683,7 @@ async fn prepare_send_msg(
let skip_fn = |reason: &CantSendReason| match reason {
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.
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
}

View File

@@ -3759,25 +3759,6 @@ async fn test_broadcast_joining_golden() -> Result<()> {
)
.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(())
}
@@ -4060,10 +4041,9 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
bob1.recv_msg(&member_added).await;
// 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_and_contact_is_verified(alice, bob0).await;
check_single_chat_is_hidden_and_contact_is_verified(bob0, alice).await;
check_single_chat_is_hidden_and_contact_is_verified(bob1, alice).await;
check_single_chat_is_hidden(alice, bob0).await;
check_single_chat_is_hidden(bob0, alice).await;
check_single_chat_is_hidden(bob1, alice).await;
tcm.section("Alice sends first message to broadcast.");
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
@@ -4090,10 +4070,7 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
Ok(())
}
async fn check_single_chat_is_hidden_and_contact_is_verified(
t: &TestContext,
contact: &TestContext,
) {
async fn check_single_chat_is_hidden(t: &TestContext, contact: &TestContext) {
let contact = t.add_or_lookup_contact_no_key(contact).await;
if let Some(single_chat) = ChatIdBlocked::lookup_by_contact(t, contact.id)
.await
@@ -4101,7 +4078,6 @@ async fn check_single_chat_is_hidden_and_contact_is_verified(
{
assert_eq!(single_chat.blocked, Blocked::Yes);
}
assert!(contact.is_verified(t).await.unwrap());
}
/// Test that only the owner of the broadcast channel

View File

@@ -38,9 +38,7 @@ use crate::param::{Param, Params};
use crate::pgp::{addresses_from_public_key, merge_openpgp_certificates};
use crate::sync::{self, Sync::*};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, normalize_text, time, to_lowercase};
use crate::{
chat, chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_ne, stock_str,
};
use crate::{chat, chatlist_events, ensure_and_debug_assert, stock_str};
/// Time during which a contact is considered as seen recently.
const SEEN_RECENTLY_SECONDS: i64 = 600;
@@ -1997,68 +1995,6 @@ pub(crate) async fn update_last_seen(
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) {
*ret += &format!("\n\n{name} ({addr}):\n{fingerprint}");
}

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
}
#[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)]
async fn test_sync_create() -> Result<()> {
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)]
async fn test_self_is_verified() -> Result<()> {
async fn test_self_is_key_contact() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().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());
Ok(())

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.
/// Or the verify state of a chat has changed.
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
/// and dc_remove_contact_from_chat().
///
@@ -302,8 +301,7 @@ pub enum EventType {
contact_id: ContactId,
/// Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
/// 400=vg-/vc-request-with-auth sent, typically shown as "introducing myself."
/// 1000=vg-member-added/vc-contact-confirm received
progress: u16,
},

View File

@@ -63,7 +63,6 @@ pub enum HeaderDef {
ChatGroupDescription,
ChatGroupDescriptionChanged,
ChatGroupDescriptionTimestamp,
ChatVerified,
ChatGroupAvatar,
ChatUserAvatar,
ChatVoiceMessage,

View File

@@ -436,7 +436,6 @@ pub(crate) fn render_queued_mail(
addr: from_addr,
public_key: public_key.clone(),
prefer_encrypt: EncryptPreference::Mutual,
verified: false,
};
let autocrypt_header = mail_builder::headers::raw::Raw::new(aheader.to_string());
add_header(
@@ -1563,17 +1562,6 @@ impl MimeFactory {
.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 {
continue;
}
@@ -1584,7 +1572,6 @@ impl MimeFactory {
// Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included.
prefer_encrypt: EncryptPreference::NoPreference,
verified: is_verified,
}
.to_string();
@@ -1728,43 +1715,6 @@ impl MimeFactory {
let command = msg.param.get_cmd();
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 {
// Send group ID unless it is an ad hoc group that has no ID.
if !chat.grpid.is_empty() {

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::{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.
///
/// 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
/// 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.
///
@@ -1048,7 +1037,7 @@ impl MimeMessage {
};
self.gossiped_keys
.values()
.map(|gossiped_key| gossiped_key.public_key.dc_fingerprint().hex())
.map(|gossiped_key| gossiped_key.dc_fingerprint().hex())
.chain(sender_fingerprint)
.collect()
}
@@ -2118,9 +2107,9 @@ fn remove_header(
async fn parse_gossip_headers(
context: &Context,
gossip_headers: Vec<String>,
) -> Result<BTreeMap<String, GossipedKey>> {
) -> Result<BTreeMap<String, SignedPublicKey>> {
// 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 {
let header = match Aheader::from_str(value) {
@@ -2135,12 +2124,7 @@ async fn parse_gossip_headers(
.await
.context("Failed to import Autocrypt-Gossip key")?;
let gossiped_key = GossipedKey {
public_key: header.public_key,
verified: header.verified,
};
gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key);
gossiped_keys.insert(header.addr.to_lowercase(), header.public_key);
}
Ok(gossiped_keys)

View File

@@ -45,7 +45,7 @@ pub(crate) const DCBACKUP_VERSION: i32 = 5;
/// Scanned QR code.
#[derive(Debug, Clone, PartialEq, Eq)]
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`].
AskVerifyContact {
@@ -125,7 +125,7 @@ pub enum Qr {
is_v3: bool,
},
/// Contact fingerprint is verified.
/// Contact fingerprint matches.
///
/// Ask the user if they want to start chatting.
FprOk {

View File

@@ -85,15 +85,15 @@ pub fn create_qr_svg(qrcode_content: &str) -> Result<String> {
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.
pub async fn get_securejoin_qr_svg(context: &Context, chat_id: Option<ChatId>) -> Result<String> {
if let Some(chat_id) = chat_id {
generate_join_group_qr_code(context, chat_id).await
} 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?;
inner_generate_secure_join_qr_code(

View File

@@ -19,7 +19,7 @@ use crate::chat::{
};
use crate::config::Config;
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::debug_logging::maybe_set_logging_xdc_inner;
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::headerdef::HeaderDef;
use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table};
use crate::key::{DcKey, Fingerprint};
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::key::{
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,
};
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::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() {
// handshake may mark contacts as verified and must be processed before chats are created
observe_securejoin_on_other_device(context, &mime_parser, to_id)
.await
.with_context(|| {
@@ -687,12 +686,6 @@ pub(crate) async fn receive_imf_inner(
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 received_msg = if let Some(received_msg) = received_msg {
is_old_contact_request = false;
@@ -2992,7 +2985,7 @@ struct GroupChangesInfo {
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].
///
@@ -3060,7 +3053,7 @@ async fn apply_group_changes(
// just like we look at ChatGroupMemberRemovedFpr.
// The result of the error is that info message
// 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) =
lookup_key_contact_by_fingerprint(context, &fingerprint).await?
{
@@ -3967,89 +3960,6 @@ async fn create_adhoc_group(
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.
///
/// 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(
context: &Context,
address_list: &[SingleInfo],
gossiped_keys: &BTreeMap<String, GossipedKey>,
gossiped_keys: &BTreeMap<String, SignedPublicKey>,
fingerprints: &[Fingerprint],
origin: Origin,
) -> Result<Vec<Option<ContactId>>> {
@@ -4134,7 +4044,7 @@ async fn add_or_lookup_key_contacts(
// Iterator has not ran out of fingerprints yet.
fp.hex()
} 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? {
contact_ids.push(Some(ContactId::SELF));
continue;

View File

@@ -19,9 +19,7 @@ use crate::mimefactory;
use crate::securejoin::get_securejoin_qr;
use crate::smtp;
use crate::test_utils;
use crate::test_utils::{
TestContext, TestContextManager, alice_keypair, get_chat_msg, mark_as_verified,
};
use crate::test_utils::{TestContext, TestContextManager, alice_keypair, get_chat_msg};
use crate::tools::{SystemTime, time};
#[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)]
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 alice = &tcm.alice().await;
let bob = &tcm.bob().await;
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 sent = alice.send_text(group_id, "Hello!").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
/// if the sender of a verification-gossiping message
/// if the sender of a gossiping message
/// also put itself into the To header.
#[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 alice = &tcm.alice().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 =
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 = Message::load_from_db(bob, msg.msg_ids[0]).await?;
assert_eq!(msg.text, "Hello!");
assert!(
bob.add_or_lookup_contact(fiona)
.await
.is_verified(bob)
.await?
);
Ok(())
}
@@ -4628,11 +4610,10 @@ async fn test_pre_msg_group_consistency() -> Result<()> {
}
#[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 alice = &tcm.alice().await;
let bob = &tcm.bob().await;
mark_as_verified(alice, bob).await;
let group_id = create_group(alice, "Group").await?;
let alice_bob_id = alice.add_or_lookup_contact(bob).await.id;
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?;
let fiona = &tcm.fiona().await;
mark_as_verified(alice, fiona).await;
let alice_fiona_id = alice.add_or_lookup_contact(fiona).await.id;
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)]
async fn test_unverified_member_msg() -> Result<()> {
async fn test_member_msg_downloaded() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().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_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;
assert_eq!(bob_msg.download_state, DownloadState::Done);
assert_eq!(bob_msg.text, "Hi");
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
/// to a single chat with key-contact even if the key of the contact is unknown.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

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::config::Config;
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::context::Context;
use crate::events::EventType;
@@ -34,15 +33,6 @@ pub(crate) use qrinvite::QrInvite;
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'_');
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> {
/*=======================================================
==== 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();
@@ -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.
let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, grpid).await?;
// Auth token is used to verify the key-contact
// if the token is not old
// and add the contact to the group
// Auth token authenticates the joiner
// and adds the contact to the group
// if there is an associated group ID.
//
// We always generate a new auth token
// because auth tokens "expire"
// and can only be used to join groups
// without verification afterwards.
let auth = create_id();
token::save(context, Namespace::Auth, grpid, &auth, time()).await?;
@@ -255,7 +239,7 @@ pub async fn join_securejoin_with_ux_info(
async fn securejoin(context: &Context, qr: &str) -> Result<ChatId> {
/*========================================================
==== Bob - the joiner's side =====
==== Step 2 in "Setup verified contact" protocol =====
==== Step 2 in the Setup-Contact protocol =====
========================================================*/
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)
}
/// Checks fingerprint and marks the contact as verified
/// if fingerprint matches.
/// Checks whether the contact's fingerprint matches.
async fn verify_sender_by_fingerprint(
context: &Context,
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 {
return Ok(false);
};
let is_verified = 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)
Ok(contact.fingerprint().is_some_and(|fp| &fp == fingerprint))
}
/// 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
/// database; this is done by `receive_imf()` later on as needed.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn handle_securejoin_handshake(
context: &Context,
mime_message: &mut MimeMessage,
@@ -457,7 +435,7 @@ pub(crate) async fn handle_securejoin_handshake(
let mut self_found = false;
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
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;
break;
}
@@ -474,7 +452,7 @@ pub(crate) async fn handle_securejoin_handshake(
SecureJoinStep::Request { ref invitenumber } => {
/*=======================================================
==== 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)
@@ -515,7 +493,7 @@ pub(crate) async fn handle_securejoin_handshake(
SecureJoinStep::AuthRequired => {
/*========================================================
==== 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
}
@@ -585,8 +563,8 @@ pub(crate) async fn handle_securejoin_handshake(
SecureJoinStep::RequestWithAuth => {
/*==========================================================
==== Alice - the inviter side ====
==== Steps 5+6 in "Setup verified contact" protocol ====
==== Step 6 in "Out-of-band verified groups" protocol ====
==== Steps 5+6 in the Setup-Contact protocol ====
==== Step 6 in the Join-Group protocol ====
==========================================================*/
// 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);
};
let Some((grpid, timestamp)) = context
let Some(grpid) = context
.sql
.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),
|row| {
let foreign_key: String = row.get(0)?;
let timestamp: i64 = row.get(1)?;
Ok((foreign_key, timestamp))
Ok(foreign_key)
},
)
.await?
@@ -654,12 +631,8 @@ pub(crate) async fn handle_securejoin_handshake(
);
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 {
warn!(context, "Ignoring {step} message: {contact_id} is blocked.");
return Ok(HandshakeMessage::Ignore);
@@ -692,7 +665,7 @@ pub(crate) async fn handle_securejoin_handshake(
Ok(HandshakeMessage::Done)
} else {
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")
.await
.context("failed sending vc-contact-confirm message")?;
@@ -703,7 +676,7 @@ pub(crate) async fn handle_securejoin_handshake(
}
/*=======================================================
==== Bob - the joiner's side ====
==== Step 7 in "Setup verified contact" protocol ====
==== Step 7 in the Setup-Contact protocol ====
=======================================================*/
SecureJoinStep::ContactConfirm => {
context.emit_event(EventType::SecurejoinJoinerProgress {
@@ -753,16 +726,14 @@ pub(crate) async fn handle_securejoin_handshake(
/// we can make some conclusions of it.
///
/// If we see self-sent {vc,vg}-request-with-auth,
/// we know that we are Bob (joiner-observer)
/// that just marked peer (Alice) as verified
/// we know that we are Bob (joiner-observer),
/// either after receiving {vc,vg}-auth-required
/// or immediately after scanning the QR-code
/// if the key was already known.
///
/// If we see self-sent vc-contact-confirm or vg-member-added message,
/// we know that we are Alice (inviter-observer)
/// that just marked peer (Bob) as verified
/// in response to correct vc-request-with-auth message.
/// responding to a correct vc-request-with-auth message.
pub(crate) async fn observe_securejoin_on_other_device(
context: &Context,
mime_message: &MimeMessage,
@@ -810,13 +781,12 @@ pub(crate) async fn observe_securejoin_on_other_device(
return Ok(HandshakeMessage::Ignore);
};
if key.public_key.dc_fingerprint() != contact_fingerprint {
if key.dc_fingerprint() != contact_fingerprint {
// Fingerprint does not match, ignore.
warn!(context, "Fingerprint does not match.");
return Ok(HandshakeMessage::Ignore);
}
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
if contact.blocked && step != SecureJoinStep::MemberAdded {
// Contact might be blocked after another device had issued the message. Still, to avoid
// membership inconsistency on devices, don't ignore "vg-member-added".

View File

@@ -106,7 +106,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
// If QR code is a group invite
// and we are already in the chat,
// nothing needs to be done.
// Even if Alice is not verified, we don't send anything.
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id: invite.contact_id(),
progress: JoinerProgress::Succeeded.into_u16(),
@@ -270,7 +269,7 @@ pub(super) async fn handle_auth_required_or_pubkey(
continue;
}
info!(context, "Fingerprint verified.",);
info!(context, "Fingerprint matches.",);
let chat_id = private_chat_id(context, &invite).await?;
delete_securejoin_wait_msg(context, chat_id)
.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.
///
/// When joining a group this is the [`ChatId`] of the group chat, when verifying a
/// contact this is the [`ChatId`] of the single chat.
/// When joining a group this is the [`ChatId`] of the group 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.
async fn joining_chat_id(
context: &Context,
@@ -496,7 +495,7 @@ async fn joining_chat_id(
pub(crate) enum JoinerProgress {
/// vg-vc-request-with-auth sent.
///
/// Typically shows as "alice@addr verified, introducing myself."
/// Typically shows as "introducing myself."
RequestWithAuthSent,
/// Completed securejoin.
Succeeded,

View File

@@ -1,5 +1,3 @@
use std::time::Duration;
use deltachat_contact_tools::EmailAddress;
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::constants::Chattype;
use crate::key::self_fingerprint;
use crate::mimeparser::{GossipedKey, SystemMessage};
use crate::qr::Qr;
use crate::receive_imf::receive_imf;
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,
TimeShiftFalsePositiveNote, get_chat_msg, sync,
};
use crate::tools::SystemTime;
#[derive(PartialEq)]
enum SetupContactCase {
@@ -193,10 +189,7 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
);
if case == SetupContactCase::WrongAliceGossip {
let wrong_pubkey = GossipedKey {
public_key: load_self_public_key(&bob).await.unwrap(),
verified: false,
};
let wrong_pubkey = load_self_public_key(&bob).await.unwrap();
let alice_pubkey = msg
.gossiped_keys
.insert(alice_addr.to_string(), wrong_pubkey)
@@ -206,7 +199,6 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
.await
.unwrap();
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false);
msg.gossiped_keys
.insert(alice_addr.to_string(), alice_pubkey)
@@ -215,20 +207,17 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
.await
.unwrap();
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
assert!(contact_bob.is_verified(&alice).await.unwrap());
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_id = contact_bob.id;
assert_eq!(contact_bob.is_key_contact(), true);
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false);
assert_eq!(contact_bob.get_authname(), "");
tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm");
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();
assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
assert!(contact_bob.get_name().is_empty());
@@ -248,7 +237,7 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
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 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"
);
// Bob has verified Alice already.
// Bob has Alice's key already.
//
// Alice may not have verified Bob yet.
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true);
// Alice may not have Bob's key yet.
assert_eq!(contact_alice.e2ee_avail(&bob.ctx).await.unwrap(), true);
// Step 7: Bob receives vc-contact-confirm
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)
.await
.unwrap();
@@ -359,13 +348,11 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> {
bob_fp.hex()
);
// Alice should not yet have Bob verified
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");
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.
let event = alice
@@ -399,16 +386,14 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> {
"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;
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.
tcm.section("Step 7: Bob receives vc-contact-confirm");
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(())
}
@@ -567,13 +552,11 @@ async fn test_secure_join_group_ext(v3: bool, remove_invite: bool) -> Result<()>
bob_fp
);
// Alice should not yet have Bob verified
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");
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.
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());
{
// 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.
if v3 {
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);
}
// 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;
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");
bob.recv_msg(&sent).await;
{
// Bob has Alice verified, message shows up in the group chat.
assert_eq!(contact_alice.is_verified(&bob).await?, true);
// The message shows up in the group chat.
assert_eq!(contact_alice.e2ee_avail(&bob).await?, true);
let chat = bob.get_chat(&alice).await;
assert_eq!(
chat.blocked,
@@ -832,11 +815,7 @@ async fn test_unknown_sender() -> Result<()> {
Ok(())
}
/// Tests that Bob gets Alice as verified
/// if `vc-contact-confirm` is lost.
/// Previously `vc-contact-confirm` was used
/// to confirm backward verification,
/// but backward verification is not tracked anymore.
/// Tests that Bob gets Alice's key if `vc-contact-confirm` is lost.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lost_contact_confirm() {
let mut tcm = TestContextManager::new();
@@ -858,16 +837,16 @@ async fn test_lost_contact_confirm() {
let sent = bob.pop_sent_msg().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;
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.
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;
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
@@ -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;
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.
@@ -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;
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();
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")
.await;
@@ -1027,8 +1006,6 @@ async fn test_wrong_auth_token() -> Result<()> {
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;
Ok(())
}
@@ -1100,123 +1077,6 @@ async fn test_send_avatar_in_securejoin() -> Result<()> {
Ok(())
}
/// Tests that scanning a QR code week later
/// allows Bob to establish a contact with Alice,
/// but does not mark Bob as verified for Alice.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_expired_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.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_rejoin_group() -> Result<()> {

View File

@@ -110,7 +110,6 @@ async fn test_key_contacts_migration_autocrypt() -> Result<()> {
pgp_bob.fingerprint().unwrap(),
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.
assert!(
@@ -149,7 +148,6 @@ async fn test_key_contacts_migration_email1() -> Result<()> {
assert_eq!(email_bob.origin, Origin::OutgoingTo);
assert_eq!(email_bob.e2ee_avail(&t).await?, false);
assert_eq!(email_bob.fingerprint(), None);
assert_eq!(email_bob.get_verifier_id(&t).await?, None);
Ok(())
}
@@ -183,7 +181,6 @@ async fn test_key_contacts_migration_email2() -> Result<()> {
assert_eq!(email_bob.origin, Origin::OutgoingTo);
assert_eq!(email_bob.e2ee_avail(&t).await?, false);
assert_eq!(email_bob.fingerprint(), None);
assert_eq!(email_bob.get_verifier_id(&t).await?, None);
Ok(())
}
@@ -227,7 +224,6 @@ async fn test_key_contacts_migration_verified() -> Result<()> {
pgp_bob.fingerprint().unwrap(),
pgp_bob.public_key(&t).await?.unwrap().dc_fingerprint()
);
assert_eq!(pgp_bob.get_verifier_id(&t).await?, Some(None));
Ok(())
}

View File

@@ -3,7 +3,7 @@
//! If this is enabled, a JSON file with some anonymous statistics
//! will be sent to a bot once a week.
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeMap;
use anyhow::{Context as _, Result};
use deltachat_derive::FromSql;
@@ -16,7 +16,7 @@ use serde::Serialize;
use crate::chat::{self, ChatId, MuteDuration};
use crate::config::Config;
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::key::{DcKey, load_self_public_key};
use crate::log::LogExt;
@@ -52,21 +52,12 @@ struct Statistics {
sending_disabled_timestamps: Vec<i64>,
}
#[derive(Serialize, PartialEq)]
enum VerifiedStatus {
Direct,
Transitive,
TransitiveViaBot,
Opportunistic,
Unencrypted,
}
#[derive(Serialize)]
struct ContactStat {
#[serde(skip_serializing)]
id: ContactId,
verified: VerifiedStatus,
encrypted: bool,
// If one of the boolean properties is false,
// we leave them away.
@@ -79,9 +70,6 @@ struct ContactStat {
last_seen: u64,
#[serde(skip_serializing_if = "Option::is_none")]
transitive_chain: Option<u32>,
/// Whether the contact was established after stats-sending was enabled
#[serde(skip_serializing_if = "is_false")]
new: bool,
@@ -93,8 +81,7 @@ fn is_false(b: &bool) -> bool {
#[derive(Serialize, Default)]
struct MessageStats {
verified: u32,
unverified_encrypted: u32,
encrypted: u32,
unencrypted: u32,
only_to_self: u32,
}
@@ -159,14 +146,10 @@ struct JoinedInvite {
/// Whether the contact already existed before.
/// If this is false, then a contact was newly created.
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:
/// "contact" for single chat invites that setup a verified contact,
/// "contact" for single chat invites,
/// "group" for invites that invite to a group,
/// "broadcast" for invites that invite to a broadcast channel.
/// The invite also performs the contact verification 'along the way'.
typ: String,
}
@@ -423,7 +406,6 @@ async fn get_stats_chat_id(context: &Context) -> Result<ChatId, anyhow::Error> {
.await?
.first()
.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? {
// 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>> {
let mut verified_by_map: BTreeMap<ContactId, ContactId> = BTreeMap::new();
let mut bot_ids: BTreeSet<ContactId> = BTreeSet::new();
let mut contacts = context
.sql
.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<>?",
(Origin::Hidden, STATISTICS_BOT_EMAIL),
|row| {
let id = row.get(0)?;
let is_encrypted: bool = row.get(1)?;
let verifier: ContactId = row.get(2)?;
let last_seen: u64 = 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);
}
let encrypted: bool = row.get(1)?;
let last_seen: u64 = row.get(2)?;
let bot: bool = row.get(3)?;
Ok(ContactStat {
id,
verified,
encrypted,
bot,
single_chat: false, // will be filled later
last_seen,
transitive_chain: None, // will be filled later
new: id.to_u32() > last_old_contact,
})
},
)
.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
for contact in &mut contacts {
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
.sql
.query_map_collect(
"SELECT chattype, verified, unverified_encrypted, unencrypted, only_to_self
"SELECT chattype, unverified_encrypted, unencrypted, only_to_self
FROM stats_msgs",
(),
|row| {
let chattype: Chattype = row.get(0)?;
let verified: u32 = row.get(1)?;
let unverified_encrypted: u32 = row.get(2)?;
let unencrypted: u32 = row.get(3)?;
let only_to_self: u32 = row.get(4)?;
let encrypted: u32 = row.get(1)?;
let unencrypted: u32 = row.get(2)?;
let only_to_self: u32 = row.get(3)?;
let message_stats = MessageStats {
verified,
unverified_encrypted,
encrypted,
unencrypted,
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.
t.execute(
"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();
let params = (last_counted_msg_id, ContactId::SELF, stats_bot_chat_id);
let verified: 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(
let encrypted: u32 = t.query_row(
&format!(
// (param GLOB '*\nc=1*' OR param GLOB 'c=1*') matches all messages that are end-to-end encrypted
"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 {general_requirements}"
),
@@ -697,7 +587,7 @@ async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Re
let unencrypted: u32 = t.query_row(
&format!(
"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 {general_requirements}"
),
@@ -715,7 +605,6 @@ async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Re
|row| row.get(0),
)?;
t.execute("DROP TABLE temp.verified_chats", ())?;
t.execute("DROP TABLE temp.empty_chats", ())?;
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(
"UPDATE stats_msgs SET
verified=verified+?,
unverified_encrypted=unverified_encrypted+?,
unencrypted=unencrypted+?,
only_to_self=only_to_self+?
WHERE chattype=?",
(
verified,
unverified_encrypted,
unencrypted,
only_to_self,
chattype,
),
(encrypted, unencrypted, only_to_self, chattype),
)?;
Ok(())
@@ -851,9 +733,6 @@ pub(crate) async fn count_securejoin_invite(context: &Context, invite: &QrInvite
// then its origin is 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 {
QrInvite::Contact { .. } => "contact",
QrInvite::Group { .. } => "group",
@@ -863,9 +742,10 @@ pub(crate) async fn count_securejoin_invite(context: &Context, invite: &QrInvite
context
.sql
.execute(
// already_verified is unused but NOT NULL without a default.
"INSERT INTO stats_securejoin_invites (already_existed, already_verified, type)
VALUES (?, ?, ?)",
(already_existed, already_verified, typ),
VALUES (?, 0, ?)",
(already_existed, typ),
)
.await?;
@@ -876,16 +756,14 @@ async fn get_securejoin_invite_stats(context: &Context) -> Result<Vec<JoinedInvi
context
.sql
.query_map_vec(
"SELECT already_existed, already_verified, type FROM stats_securejoin_invites",
"SELECT already_existed, type FROM stats_securejoin_invites",
(),
|row| {
let already_existed: bool = row.get(0)?;
let already_verified: bool = row.get(1)?;
let typ: String = row.get(2)?;
let typ: String = row.get(1)?;
Ok(JoinedInvite {
already_existed,
already_verified,
typ,
})
},

View File

@@ -30,9 +30,6 @@ async fn test_maybe_send_stats() -> Result<()> {
assert!(chat.is_encrypted(alice).await?);
let contacts = get_chat_contacts(alice, chat_id).await?;
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;
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(),
&serde_json::Value::Bool(true)
);
assert!(contact_info.get("transitive_chain").is_none(),);
assert_eq!(
contact_info.get("verified").unwrap(),
&serde_json::Value::String("Opportunistic".to_string())
contact_info.get("encrypted").unwrap(),
&serde_json::Value::Bool(true)
);
assert_eq!(
contact_info.get("new").unwrap(),
@@ -168,25 +164,16 @@ async fn test_message_stats() -> Result<()> {
check_stats(&update_get_stats(alice).await, &expected);
alice.send_text(encrypted_chat.id, "foo").await;
expected
.get_mut(&Chattype::Single)
.unwrap()
.unverified_encrypted += 1;
expected.get_mut(&Chattype::Single).unwrap().encrypted += 1;
check_stats(&update_get_stats(alice).await, &expected);
alice.send_text(encrypted_chat.id, "foo").await;
expected
.get_mut(&Chattype::Single)
.unwrap()
.unverified_encrypted += 1;
expected.get_mut(&Chattype::Single).unwrap().encrypted += 1;
check_stats(&update_get_stats(alice).await, &expected);
let group = alice.create_group_with_members("Pizza", &[bob]).await;
alice.send_text(group, "foo").await;
expected
.get_mut(&Chattype::Group)
.unwrap()
.unverified_encrypted += 1;
expected.get_mut(&Chattype::Group).unwrap().encrypted += 1;
check_stats(&update_get_stats(alice).await, &expected);
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;
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);
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));
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?;
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;
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);
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;
expected.push(JoinedInvite {
already_existed: false,
already_verified: false,
typ: "contact".to_string(),
});
check_stats(alice, &expected).await;
@@ -437,7 +423,6 @@ async fn test_stats_securejoin_invites() -> Result<()> {
tcm.exec_securejoin_qr(alice, bob, &qr).await;
expected.push(JoinedInvite {
already_existed: true,
already_verified: true,
typ: "contact".to_string(),
});
check_stats(alice, &expected).await;
@@ -449,7 +434,6 @@ async fn test_stats_securejoin_invites() -> Result<()> {
tcm.exec_securejoin_qr(alice, bob, &qr).await;
expected.push(JoinedInvite {
already_existed: true,
already_verified: true,
typ: "group".to_string(),
});
check_stats(alice, &expected).await;
@@ -463,7 +447,6 @@ async fn test_stats_securejoin_invites() -> Result<()> {
tcm.exec_securejoin_qr(alice, bob, &qr).await;
expected.push(JoinedInvite {
already_existed: true,
already_verified: false,
typ: "group".to_string(),
});
check_stats(alice, &expected).await;

View File

@@ -69,6 +69,7 @@ pub enum StockMessage {
#[strum(props(fallback = "Fingerprints"))]
FingerPrints = 30,
/// Unused. Was used in status messages.
#[strum(props(fallback = "%1$s verified."))]
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())
}
/// 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`.
pub(crate) fn archived_chats(context: &Context) -> String {
translated(context, StockMessage::ArchivedChats)

View File

@@ -48,12 +48,11 @@ async fn test_stock_str() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_stock_string_repl_str() {
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
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...
}

View File

@@ -30,9 +30,7 @@ use crate::chat::{
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_NO_SPECIALS};
use crate::contact::{
Contact, ContactId, Modifier, Origin, import_vcard, make_vcard, mark_contact_id_as_verified,
};
use crate::contact::{Contact, ContactId, Modifier, Origin, import_vcard, make_vcard};
use crate::context::Context;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{self, DcKey, self_fingerprint};
@@ -942,27 +940,6 @@ ORDER BY 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.
/// Panics if the chat does not exist.
///
@@ -1207,18 +1184,6 @@ ORDER BY 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.
pub async fn allow_unencrypted(&self) -> Result<()> {
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?,
public_key: public_key.clone(),
prefer_encrypt: EncryptPreference::Mutual,
verified: false,
};
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()
}
/// 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
/// alice0's side that implies sending a sync message.
pub(crate) async fn sync(alice0: &TestContext, alice1: &TestContext) {

View File

@@ -16,7 +16,6 @@ use crate::receive_imf::receive_imf;
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::test_utils::mark_as_verified;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_change_primary_self_addr() -> Result<()> {
@@ -46,35 +45,21 @@ async fn test_change_primary_self_addr() -> Result<()> {
enum ChatForTransition {
Single,
GroupChat,
VerifiedGroup,
}
use ChatForTransition::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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)]
async fn test_aeap_transition_1() {
check_aeap_transition(GroupChat, false).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;
check_aeap_transition(GroupChat).await;
}
/// Happy path test for AEAP.
/// - `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, verified: bool) {
async fn check_aeap_transition(chat_for_transition: ChatForTransition) {
const ALICE_NEW_ADDR: &str = "alice2@example.net";
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(bob, alice, "Hi back").await;
if verified {
mark_as_verified(alice, bob).await;
mark_as_verified(bob, alice).await;
}
let mut groups = vec![
let groups = vec![
chat::create_group(bob, "Group 0").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;
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 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.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 {
Single => alice.create_chat(bob).await.id,
GroupChat => group1_alice,
VerifiedGroup => group3_alice.expect("No verified group"),
};
let sent = alice
.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 qr = get_securejoin_qr(alice, Some(alice_grp_id)).await?;
tcm.exec_securejoin_qr(bob, alice, &qr).await;
let bob_alice_contact = bob.add_or_lookup_contact(alice).await;
assert!(bob_alice_contact.is_verified(bob).await?);
let _bob_alice_contact = bob.add_or_lookup_contact(alice).await;
let bob_alice_chat = bob.create_chat(alice).await;
let bob_unprotected_grp_id = bob.create_group_with_members("Group", &[alice]).await;
tcm.change_addr(alice, "alice@someotherdomain.xyz").await;
let sent = alice.send_text(alice_grp_id, "Hello!").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 mut msg = Message::new_text("hi".to_string());
chat::send_msg(bob, bob_alice_chat.id, &mut msg).await?;

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::constants::Chattype;
use crate::contact::{Contact, ContactId};
use crate::key;
use crate::key::self_fingerprint;
use crate::message;
use crate::message::{Message, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf;
use crate::securejoin::{get_securejoin_qr, join_securejoin};
use crate::stock_str;
use crate::test_utils;
use crate::test_utils::{
E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified,
};
use crate::test_utils::{TestContextManager, get_chat_msg};
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)]
async fn test_missing_key_reexecute_securejoin() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -168,108 +38,6 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> {
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)]
async fn test_outgoing_mua_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -277,11 +45,7 @@ async fn test_outgoing_mua_msg() -> Result<()> {
let bob = &tcm.bob().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;
assert_verified(alice, bob).await;
let sent = receive_imf(
alice,
@@ -313,7 +77,6 @@ async fn test_outgoing_encrypted_msg() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
mark_as_verified(alice, bob).await;
let chat_id = alice.create_chat(bob).await.id;
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
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
/// chat (see lookup_chat_by_reply()); this is meant to make aliases
/// work nicely.
/// However, if the original chat is verified, the unencrypted message
/// 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).
/// However, the unencrypted message must NOT be assigned to an encrypted chat.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reply() -> Result<()> {
for verified in [false, true] {
{
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().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;
let encrypted_msg = tcm.send_recv(&alice, &bob, "Heyho back").await;
@@ -372,8 +127,7 @@ async fn test_reply() -> Result<()> {
Ok(())
}
/// Tests that message from old DC setup does not break
/// new verified chat.
/// Tests that a message from an old DC setup does not break the new chat.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_message_from_old_dc_setup() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -381,7 +135,6 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
let bob_old = &tcm.unconfigured().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 sent_old = bob_old
.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");
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;
assert_verified(alice, bob).await;
let msg = alice.recv_msg(&sent_old).await;
assert!(msg.get_showpadlock());
let contact = alice.add_or_lookup_contact(bob).await;
// The outdated Bob's Autocrypt header isn't applied
// and the message goes to another chat, so the verification preserves.
assert!(contact.is_verified(alice).await.unwrap());
// The outdated Bob's Autocrypt header isn't applied,
// so the message goes to another chat.
assert_ne!(contact.id, msg.from_id);
Ok(())
}
/// Regression test for the following bug:
///
/// - 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.
/// Tests that on a second device the e2ee info message
/// sorts before the older message that created the group.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_grp_multidev() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -473,7 +191,7 @@ async fn test_create_grp_multidev() -> Result<()> {
}
#[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 alice = &tcm.alice().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;
// 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_chat = Chat::load_from_db(fiona, fiona_received_message.chat_id).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)]
async fn test_no_unencrypted_name_if_encrypted() -> Result<()> {
let mut tcm = TestContextManager::new();
for verified in [false, true] {
{
let alice = tcm.alice().await;
let bob = tcm.bob().await;
bob.set_config(Config::Displayname, Some("Bob Smith"))
.await?;
if verified {
mark_as_verified(&bob, &alice).await;
} else {
tcm.send_recv_accept(&alice, &bob, "hi").await;
}
tcm.send_recv_accept(&alice, &bob, "hi").await;
let chat_id = bob.create_chat(&alice).await.id;
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)]
async fn test_verified_lost_member_added() -> Result<()> {
async fn test_lost_member_added() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
@@ -609,10 +323,9 @@ async fn test_verified_lost_member_added() -> Result<()> {
Ok(())
}
/// Tests handling of resent .xdc arriving before "Member added"
/// in a verified group
/// Tests handling of resent .xdc arriving before "Member added".
#[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 alice = &tcm.alice().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.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_sent = alice.send_text(alice_chat_id, "Hi!").await;
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");
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);
tcm.section("Charlie receives member added message");
@@ -677,122 +387,8 @@ async fn test_verified_chat_editor_reordering() -> Result<()> {
charlie
.golden_test_chat(
charlie_received_xdc.chat_id,
"verified_chats_editor_reordering",
"encrypted_chats_editor_reordering",
)
.await;
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);
}

View File

@@ -2,7 +2,7 @@
//!
//! 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 deltachat_derive::{FromSql, ToSql};

View File

@@ -15,7 +15,7 @@ Push | IMAP IDLE ([RFC 2177][])
Quota | IMAP QUOTA extension ([RFC 2087][])
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)
Detect/prevent active attacks | [securejoin][] protocols
Invite links for chats | [securejoin][] protocols
Compare public keys | [openpgp4fpr][] URI Scheme
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][]