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