mirror of
https://github.com/chatmail/core.git
synced 2026-09-22 04:58:47 +03:00
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:
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
18
spec.md
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -63,7 +63,6 @@ pub enum HeaderDef {
|
||||
ChatGroupDescription,
|
||||
ChatGroupDescriptionChanged,
|
||||
ChatGroupDescriptionTimestamp,
|
||||
ChatVerified,
|
||||
ChatGroupAvatar,
|
||||
ChatUserAvatar,
|
||||
ChatVoiceMessage,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
170
src/stats.rs
170
src/stats.rs
@@ -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(¤t_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(¤t_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,
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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...
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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][]
|
||||
|
||||
Reference in New Issue
Block a user