mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 21:46:35 +03:00
feat: key-contacts
This change introduces a new type of contacts
identified by their public key fingerprint
rather than an e-mail address.
Encrypted chats now stay encrypted
and unencrypted chats stay unencrypted.
For example, 1:1 chats with key-contacts
are encrypted and 1:1 chats with address-contacts
are unencrypted.
Groups that have a group ID are encrypted
and can only contain key-contacts
while groups that don't have a group ID ("adhoc groups")
are unencrypted and can only contain address-contacts.
JSON-RPC API `reset_contact_encryption` is removed.
Python API `Contact.reset_encryption` is removed.
"Group tracking plugin" in legacy Python API was removed because it
relied on parsing email addresses from system messages with regexps.
Co-authored-by: Hocuri <hocuri@gmx.de>
Co-authored-by: iequidoo <dgreshilov@gmail.com>
Co-authored-by: B. Petersen <r10s@b44t.com>
This commit is contained in:
@@ -289,8 +289,8 @@
|
||||
|
||||
- Use vCard in TestContext.add_or_lookup_contact().
|
||||
- Remove test_group_with_removed_message_id.
|
||||
- Use add_or_lookup_email_contact() in get_chat().
|
||||
- Use add_or_lookup_email_contact in test_setup_contact_ex.
|
||||
- Use add_or_lookup_address_contact() in get_chat().
|
||||
- Use add_or_lookup_address_contact in test_setup_contact_ex.
|
||||
- Use vCards more in Python tests.
|
||||
- Use TestContextManager in more tests.
|
||||
- Use vCards to create contacts in more Rust tests.
|
||||
|
||||
BIN
assets/icon-address-contact.png
Normal file
BIN
assets/icon-address-contact.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
47
assets/icon-address-contact.svg
Normal file
47
assets/icon-address-contact.svg
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="480"
|
||||
viewBox="0 -960 9600 9600"
|
||||
width="480"
|
||||
fill="#ffffff"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="icon-email.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.99091847"
|
||||
inkscape:cx="263.392"
|
||||
inkscape:cy="177.613"
|
||||
inkscape:window-width="1884"
|
||||
inkscape:window-height="1052"
|
||||
inkscape:window-x="36"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<rect
|
||||
style="fill:#8c8c8c;fill-opacity:1;stroke:none;stroke-width:680.523;stroke-dasharray:none;paint-order:markers fill stroke"
|
||||
id="rect1"
|
||||
width="9951.9541"
|
||||
height="9767.4756"
|
||||
x="-71.697792"
|
||||
y="-1012.83"
|
||||
ry="0.43547946" />
|
||||
<path
|
||||
d="m 2948.0033,5553.6941 q -130.7292,0 -228.7761,-96.3953 -98.0468,-96.3953 -98.0468,-224.9223 V 2447.6234 q 0,-128.527 98.0468,-224.9223 98.0469,-96.3953 228.7761,-96.3953 h 3703.9934 q 130.7292,0 228.776,96.3953 98.0469,96.3953 98.0469,224.9223 v 2784.7531 q 0,128.527 -98.0469,224.9223 -98.0468,96.3953 -228.776,96.3953 z M 4800,3936.3952 2948.0033,2742.1646 V 5232.3765 H 6651.9967 V 2742.1646 Z m 0,-321.3176 1830.2085,-1167.4541 h -3654.97 z m -1851.9967,-872.913 v -294.5412 2784.7531 z"
|
||||
id="path1"
|
||||
style="stroke-width:5.40098" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
7
assets/self-reporting-bot.vcf
Normal file
7
assets/self-reporting-bot.vcf
Normal file
@@ -0,0 +1,7 @@
|
||||
BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
EMAIL:self_reporting@testrun.org
|
||||
FN:Statistics bot
|
||||
KEY:data:application/pgp-keys;base64,xjMEZbfBlBYJKwYBBAHaRw8BAQdABpLWS2PUIGGo4pslVt4R8sylP5wZihmhf1DTDr3oCMPNHDxzZWxmX3JlcG9ydGluZ0B0ZXN0cnVuLm9yZz7CiwQQFggAMwIZAQUCZbfBlAIbAwQLCQgHBhUICQoLAgMWAgEWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohD8dAQCQV7CoH6UP4PD+NqI4kW5tbbqdh2AnDROg60qotmLExAEAxDfd3QHAK9f8b9qQUbLmHIztCLxhEuVbWPBEYeVW0gvOOARlt8GUEgorBgEEAZdVAQUBAQdAMBUhYoAAcI625vGZqnM5maPX4sGJ7qvJxPAFILPy6AcDAQgHwngEGBYIACAFAmW3wZQCGwwWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohPwCAQCvzk1ObIkj2GqsuIfaULlgdnfdZY8LNary425CEfHZDQD5AblXVrlMO1frdlc/Vo9z3pEeCrfYdD7ITD3/OeVoiQ4=
|
||||
REV:20250412T195751Z
|
||||
END:VCARD
|
||||
@@ -3838,6 +3838,21 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
||||
int dc_chat_is_protected (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the chat is encrypted.
|
||||
*
|
||||
* 1:1 chats with key-contacts and group chats with key-contacts
|
||||
* are encrypted.
|
||||
* 1:1 chats with emails contacts and ad-hoc groups
|
||||
* created for email threads are not encrypted.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return 1=chat is encrypted, 0=chat is not encrypted.
|
||||
*/
|
||||
int dc_chat_is_encrypted (const dc_chat_t *chat);
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the chat was protected, and then an incoming message broke this protection.
|
||||
*
|
||||
@@ -6886,6 +6901,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "End-to-end encryption preferred."
|
||||
///
|
||||
/// Used to build the string returned by dc_get_contact_encrinfo().
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_E2E_PREFERRED 34
|
||||
|
||||
/// "%1$s verified"
|
||||
@@ -6898,12 +6914,14 @@ void dc_event_unref(dc_event_t* event);
|
||||
///
|
||||
/// Used in status messages.
|
||||
/// - %1$s will be replaced by the name of the contact that cannot be verified
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_CONTACT_NOT_VERIFIED 36
|
||||
|
||||
/// "Changed setup for %1$s."
|
||||
///
|
||||
/// Used in status messages.
|
||||
/// - %1$s will be replaced by the name of the contact with the changed setup
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_CONTACT_SETUP_CHANGED 37
|
||||
|
||||
/// "Archived chats"
|
||||
@@ -7293,6 +7311,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "%1$s changed their address from %2$s to %3$s"
|
||||
///
|
||||
/// Used as an info message to chats with contacts that changed their address.
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_AEAP_ADDR_CHANGED 122
|
||||
|
||||
/// "You changed your email address from %1$s to %2$s.
|
||||
@@ -7599,6 +7618,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "The contact must be online to proceed. This process will continue automatically in background."
|
||||
///
|
||||
/// Used as info message.
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
|
||||
|
||||
/// "Contact". Deprecated, currently unused.
|
||||
|
||||
@@ -3153,6 +3153,18 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i
|
||||
ffi_chat.chat.is_protected() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_is_encrypted(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_is_encrypted()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
|
||||
block_on(ffi_chat.chat.is_encrypted(&ffi_chat.context))
|
||||
.unwrap_or_log_default(&ffi_chat.context, "Failed dc_chat_is_encrypted") as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
@@ -4303,6 +4315,7 @@ pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t)
|
||||
.context("failed to get verifier")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
|
||||
verifier_contact_id.to_u32()
|
||||
|
||||
@@ -354,6 +354,20 @@ impl CommandApi {
|
||||
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
|
||||
}
|
||||
|
||||
/// If there was an error while the account was opened
|
||||
/// and migrated to the current version,
|
||||
/// then this function returns it.
|
||||
///
|
||||
/// This function is useful because the key-contacts migration could fail due to bugs
|
||||
/// and then the account will not work properly.
|
||||
///
|
||||
/// After opening an account, the UI should call this function
|
||||
/// and show the error string if one is returned.
|
||||
async fn get_migration_error(&self, account_id: u32) -> Result<Option<String>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx.get_migration_error())
|
||||
}
|
||||
|
||||
/// Copy file to blob dir.
|
||||
async fn copy_to_blob_dir(&self, account_id: u32, path: String) -> Result<PathBuf> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -1542,15 +1556,6 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resets contact encryption.
|
||||
async fn reset_contact_encryption(&self, account_id: u32, contact_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let contact_id = ContactId::new(contact_id);
|
||||
|
||||
contact_id.reset_encryption(&ctx).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets display name for existing contact.
|
||||
async fn change_contact_name(
|
||||
&self,
|
||||
|
||||
@@ -30,6 +30,29 @@ pub struct FullChat {
|
||||
/// in the contact profile
|
||||
/// if 1:1 chat with this contact exists and is protected.
|
||||
is_protected: bool,
|
||||
/// True if the chat is encrypted.
|
||||
/// This means that all messages in the chat are encrypted,
|
||||
/// and all contacts in the chat are "key-contacts",
|
||||
/// i.e. identified by the PGP key fingerprint.
|
||||
///
|
||||
/// False if the chat is unencrypted.
|
||||
/// This means that all messages in the chat are unencrypted,
|
||||
/// and all contacts in the chat are "address-contacts",
|
||||
/// i.e. identified by the email address.
|
||||
/// The UI should mark this chat e.g. with a mail-letter icon.
|
||||
///
|
||||
/// Unencrypted groups are called "ad-hoc groups"
|
||||
/// and the user can't add/remove members,
|
||||
/// create a QR invite code,
|
||||
/// or set an avatar.
|
||||
/// These options should therefore be disabled in the UI.
|
||||
///
|
||||
/// Note that it can happen that an encrypted chat
|
||||
/// contains unencrypted messages that were received in core <= v1.159.*
|
||||
/// and vice versa.
|
||||
///
|
||||
/// See also `is_key_contact` on `Contact`.
|
||||
is_encrypted: bool,
|
||||
profile_image: Option<String>, //BLOBS ?
|
||||
archived: bool,
|
||||
pinned: bool,
|
||||
@@ -108,6 +131,7 @@ impl FullChat {
|
||||
id: chat_id,
|
||||
name: chat.name.clone(),
|
||||
is_protected: chat.is_protected(),
|
||||
is_encrypted: chat.is_encrypted(context).await?,
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||
@@ -159,6 +183,30 @@ pub struct BasicChat {
|
||||
/// in the contact profile
|
||||
/// if 1:1 chat with this contact exists and is protected.
|
||||
is_protected: bool,
|
||||
|
||||
/// True if the chat is encrypted.
|
||||
/// This means that all messages in the chat are encrypted,
|
||||
/// and all contacts in the chat are "key-contacts",
|
||||
/// i.e. identified by the PGP key fingerprint.
|
||||
///
|
||||
/// False if the chat is unencrypted.
|
||||
/// This means that all messages in the chat are unencrypted,
|
||||
/// and all contacts in the chat are "address-contacts",
|
||||
/// i.e. identified by the email address.
|
||||
/// The UI should mark this chat e.g. with a mail-letter icon.
|
||||
///
|
||||
/// Unencrypted groups are called "ad-hoc groups"
|
||||
/// and the user can't add/remove members,
|
||||
/// create a QR invite code,
|
||||
/// or set an avatar.
|
||||
/// These options should therefore be disabled in the UI.
|
||||
///
|
||||
/// Note that it can happen that an encrypted chat
|
||||
/// contains unencrypted messages that were received in core <= v1.159.*
|
||||
/// and vice versa.
|
||||
///
|
||||
/// See also `is_key_contact` on `Contact`.
|
||||
is_encrypted: bool,
|
||||
profile_image: Option<String>, //BLOBS ?
|
||||
archived: bool,
|
||||
pinned: bool,
|
||||
@@ -187,6 +235,7 @@ impl BasicChat {
|
||||
id: chat_id,
|
||||
name: chat.name.clone(),
|
||||
is_protected: chat.is_protected(),
|
||||
is_encrypted: chat.is_encrypted(context).await?,
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||
|
||||
@@ -30,6 +30,30 @@ pub enum ChatListItemFetchResult {
|
||||
/// showing preview if last chat message is image
|
||||
summary_preview_image: Option<String>,
|
||||
is_protected: bool,
|
||||
|
||||
/// True if the chat is encrypted.
|
||||
/// This means that all messages in the chat are encrypted,
|
||||
/// and all contacts in the chat are "key-contacts",
|
||||
/// i.e. identified by the PGP key fingerprint.
|
||||
///
|
||||
/// False if the chat is unencrypted.
|
||||
/// This means that all messages in the chat are unencrypted,
|
||||
/// and all contacts in the chat are "address-contacts",
|
||||
/// i.e. identified by the email address.
|
||||
/// The UI should mark this chat e.g. with a mail-letter icon.
|
||||
///
|
||||
/// Unencrypted groups are called "ad-hoc groups"
|
||||
/// and the user can't add/remove members,
|
||||
/// create a QR invite code,
|
||||
/// or set an avatar.
|
||||
/// These options should therefore be disabled in the UI.
|
||||
///
|
||||
/// Note that it can happen that an encrypted chat
|
||||
/// contains unencrypted messages that were received in core <= v1.159.*
|
||||
/// and vice versa.
|
||||
///
|
||||
/// See also `is_key_contact` on `Contact`.
|
||||
is_encrypted: bool,
|
||||
is_group: bool,
|
||||
fresh_message_counter: usize,
|
||||
is_self_talk: bool,
|
||||
@@ -137,6 +161,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
|
||||
summary_preview_image,
|
||||
is_protected: chat.is_protected(),
|
||||
is_encrypted: chat.is_encrypted(ctx).await?,
|
||||
is_group: chat.get_type() == Chattype::Group,
|
||||
fresh_message_counter,
|
||||
is_self_talk: chat.is_self_talk(),
|
||||
|
||||
@@ -19,6 +19,16 @@ pub struct ContactObject {
|
||||
profile_image: Option<String>, // BLOBS
|
||||
name_and_addr: String,
|
||||
is_blocked: bool,
|
||||
|
||||
/// Is the contact a key contact.
|
||||
is_key_contact: bool,
|
||||
|
||||
/// Is encryption available for this contact.
|
||||
///
|
||||
/// This can only be true for key-contacts.
|
||||
/// However, it is possible to have a key-contact
|
||||
/// for which encryption is not available because we don't have a key yet,
|
||||
/// e.g. if we just scanned the fingerprint from a QR code.
|
||||
e2ee_avail: bool,
|
||||
|
||||
/// True if the contact can be added to verified groups.
|
||||
@@ -67,6 +77,7 @@ impl ContactObject {
|
||||
let verifier_id = contact
|
||||
.get_verifier_id(context)
|
||||
.await?
|
||||
.flatten()
|
||||
.map(|contact_id| contact_id.to_u32());
|
||||
|
||||
Ok(ContactObject {
|
||||
@@ -80,6 +91,7 @@ impl ContactObject {
|
||||
profile_image, //BLOBS
|
||||
name_and_addr: contact.get_name_n_addr(),
|
||||
is_blocked: contact.is_blocked(),
|
||||
is_key_contact: contact.is_key_contact(),
|
||||
e2ee_avail: contact.e2ee_avail(context).await?,
|
||||
is_verified,
|
||||
is_profile_verified,
|
||||
|
||||
@@ -59,6 +59,13 @@ pub struct MessageObject {
|
||||
|
||||
// summary - use/create another function if you need it
|
||||
subject: String,
|
||||
|
||||
/// True if the message was correctly encrypted&signed, false otherwise.
|
||||
/// Historically, UIs showed a small padlock on the message then.
|
||||
///
|
||||
/// Today, the UIs should instead show a small email-icon on the message
|
||||
/// if `show_padlock` is `false`,
|
||||
/// and nothing if it is `true`.
|
||||
show_padlock: bool,
|
||||
is_setupmessage: bool,
|
||||
is_info: bool,
|
||||
|
||||
@@ -20,7 +20,6 @@ use deltachat::log::LogExt;
|
||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use deltachat::mimeparser::SystemMessage;
|
||||
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::qr_code_generator::create_qr_svg;
|
||||
use deltachat::reaction::send_reaction;
|
||||
@@ -35,14 +34,6 @@ use tokio::fs;
|
||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||
async fn reset_tables(context: &Context, bits: i32) {
|
||||
println!("Resetting tables ({bits})...");
|
||||
if 0 != bits & 2 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM acpeerstates;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(2) Peerstates reset.");
|
||||
}
|
||||
if 0 != bits & 4 {
|
||||
context
|
||||
.sql()
|
||||
@@ -277,7 +268,7 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
|
||||
|
||||
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
|
||||
for contact_id in contacts {
|
||||
let mut line2 = "".to_string();
|
||||
let line2 = "".to_string();
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
let name = contact.get_display_name();
|
||||
let addr = contact.get_addr();
|
||||
@@ -296,15 +287,6 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
|
||||
verified_str,
|
||||
if !addr.is_empty() { addr } else { "addr unset" }
|
||||
);
|
||||
let peerstate = Peerstate::from_addr(context, addr)
|
||||
.await
|
||||
.expect("peerstate error");
|
||||
if peerstate.is_some() && *contact_id != ContactId::SELF {
|
||||
line2 = format!(
|
||||
", prefer-encrypt={}",
|
||||
peerstate.as_ref().unwrap().prefer_encrypt
|
||||
);
|
||||
}
|
||||
|
||||
println!("Contact#{}: {}{}", *contact_id, line, line2);
|
||||
}
|
||||
@@ -514,7 +496,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||
}
|
||||
"reset" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <bits> missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config");
|
||||
ensure!(
|
||||
!arg1.is_empty(),
|
||||
"Argument <bits> missing: 4=private keys, 8=rest but server config"
|
||||
);
|
||||
let bits: i32 = arg1.parse()?;
|
||||
ensure!(bits < 16, "<bits> must be lower than 16.");
|
||||
reset_tables(&context, bits).await;
|
||||
|
||||
@@ -37,10 +37,6 @@ class Contact:
|
||||
"""Delete contact."""
|
||||
self._rpc.delete_contact(self.account.id, self.id)
|
||||
|
||||
def reset_encryption(self) -> None:
|
||||
"""Reset contact encryption."""
|
||||
self._rpc.reset_contact_encryption(self.account.id, self.id)
|
||||
|
||||
def set_name(self, name: str) -> None:
|
||||
"""Change the name of this contact."""
|
||||
self._rpc.change_contact_name(self.account.id, self.id, name)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -16,14 +15,14 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
||||
bob_contact_alice = bob.create_contact(alice)
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
@@ -84,7 +83,7 @@ def test_qr_securejoin(acfactory, protect):
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
@@ -93,7 +92,7 @@ def test_qr_securejoin(acfactory, protect):
|
||||
assert snapshot.chat.get_basic_snapshot().is_protected == protect
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
||||
bob_contact_alice = bob.create_contact(alice)
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
@@ -101,7 +100,7 @@ def test_qr_securejoin(acfactory, protect):
|
||||
# Alice observes securejoin protocol and verifies Bob on second device.
|
||||
alice2.start_io()
|
||||
alice2.wait_for_securejoin_inviter_success()
|
||||
alice2_contact_bob = alice2.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice2_contact_bob = alice2.create_contact(bob)
|
||||
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
|
||||
assert alice2_contact_bob_snapshot.is_verified
|
||||
|
||||
@@ -213,72 +212,8 @@ def test_setup_contact_resetup(acfactory) -> None:
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
def test_verified_group_recovery(acfactory) -> None:
|
||||
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("ac1 creates verified group")
|
||||
chat = ac1.create_group("Verified group", protect=True)
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
# ac1 has ac2 directly verified.
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
ac3_chat = ac3.secure_join(qr_code)
|
||||
ac3.wait_for_securejoin_joiner_success()
|
||||
ac3.wait_for_incoming_msg_event() # Member added
|
||||
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
qr_code = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
logging.info("ac3 sends a message to the group")
|
||||
assert len(ac3_chat.get_contacts()) == 3
|
||||
ac3_chat.send_text("Hi!")
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Hi!"
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.text == "Hi!"
|
||||
|
||||
# ac1 contact is verified for ac2 because ac3 gossiped ac1 key in the "Hi!" message.
|
||||
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
|
||||
assert ac1_contact.get_snapshot().is_verified
|
||||
|
||||
# ac2 can write messages to the group.
|
||||
snapshot.chat.send_text("Works again!")
|
||||
|
||||
snapshot = ac3.get_message_by_id(ac3.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
ac1_chat_messages = snapshot.chat.get_messages()
|
||||
ac2_addr = ac2.get_config("addr")
|
||||
assert ac1_chat_messages[-2].get_snapshot().text == f"Changed setup for {ac2_addr}"
|
||||
|
||||
# ac2 is now verified by ac3 for ac1
|
||||
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
|
||||
|
||||
|
||||
def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
"""Tests verified group recovery by reverifiying than removing and adding a member back."""
|
||||
"""Tests verified group recovery by reverifying then removing and adding a member back."""
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("ac1 creates verified group")
|
||||
@@ -291,7 +226,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
# ac1 has ac2 directly verified.
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
@@ -299,6 +234,8 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
ac3.wait_for_securejoin_joiner_success()
|
||||
ac3.wait_for_incoming_msg_event() # Member added
|
||||
|
||||
ac3_contact_ac2_old = ac3.create_contact(ac2)
|
||||
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
|
||||
@@ -311,21 +248,10 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
assert len(ac3_chat.get_contacts()) == 3
|
||||
ac3_chat.send_text("Hi!")
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
logging.info("Received message %s", snapshot.text)
|
||||
assert snapshot.text == "Hi!"
|
||||
|
||||
ac1.wait_for_incoming_msg_event() # Hi!
|
||||
|
||||
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
|
||||
ac3_chat.remove_contact(ac3_contact_ac2)
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert "removed" in snapshot.text
|
||||
ac3_contact_ac2 = ac3.create_contact(ac2)
|
||||
ac3_chat.remove_contact(ac3_contact_ac2_old)
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert "removed" in snapshot.text
|
||||
@@ -354,19 +280,16 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||
ac1_contact_ac3 = ac1.create_contact(ac3)
|
||||
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
||||
assert ac1_contact_ac2_snapshot.is_verified
|
||||
assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id
|
||||
|
||||
# ac2 is now verified by ac3 for ac1
|
||||
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
|
||||
assert ac1_contact_ac2_snapshot.verifier_id == ac1_contact_ac3.id
|
||||
|
||||
|
||||
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
"""Regression test for
|
||||
issue <https://github.com/deltachat/deltachat-core-rust/issues/4894>.
|
||||
issue <https://github.com/chatmail/core/issues/4894>.
|
||||
"""
|
||||
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
||||
|
||||
@@ -400,12 +323,12 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
|
||||
|
||||
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
|
||||
assert ac3.get_contact_by_addr(ac2.get_config("addr")).get_snapshot().is_verified
|
||||
assert ac2.get_contact_by_addr(ac3.get_config("addr")).get_snapshot().is_verified
|
||||
assert ac3.create_contact(ac2).get_snapshot().is_verified
|
||||
assert ac2.create_contact(ac3).get_snapshot().is_verified
|
||||
|
||||
logging.info("ac3: create a verified group VG with ac2")
|
||||
vg = ac3.create_group("ac3-created", protect=True)
|
||||
vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr")))
|
||||
vg.add_contact(ac3.create_contact(ac2))
|
||||
|
||||
# ensure ac2 receives message in VG
|
||||
vg.send_text("hello")
|
||||
@@ -443,7 +366,7 @@ def test_qr_new_group_unblocked(acfactory):
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
ac1_new_chat = ac1.create_group("Another group")
|
||||
ac1_new_chat.add_contact(ac1.get_contact_by_addr(ac2.get_config("addr")))
|
||||
ac1_new_chat.add_contact(ac1.create_contact(ac2))
|
||||
# Receive "Member added" message.
|
||||
ac2.wait_for_incoming_msg_event()
|
||||
|
||||
@@ -577,30 +500,8 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
|
||||
# ac1 resetups the account.
|
||||
ac1 = acfactory.resetup_account(ac1)
|
||||
|
||||
# Loop sending message from ac1 to ac2
|
||||
# until ac2 accepts new ac1 key.
|
||||
#
|
||||
# This may not happen immediately because resetup of ac1
|
||||
# rewinds "smeared timestamp" so Date: header for messages
|
||||
# sent by new ac1 are in the past compared to the last Date:
|
||||
# header sent by old ac1.
|
||||
while True:
|
||||
# ac1 sends a message to ac2.
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2, "")
|
||||
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
|
||||
ac1_chat_ac2.send_text("Hello!")
|
||||
|
||||
# ac2 receives a message.
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
logging.info("ac2 received Hello!")
|
||||
|
||||
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
|
||||
logging.info("ac2 addr={}, ac1 addr={}".format(ac2.get_config("addr"), ac1.get_config("addr")))
|
||||
if not ac2_contact_ac1.get_snapshot().is_verified:
|
||||
break
|
||||
time.sleep(1)
|
||||
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
||||
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||
|
||||
# ac1 goes offline.
|
||||
ac1.remove()
|
||||
|
||||
@@ -170,7 +170,7 @@ def test_account(acfactory) -> None:
|
||||
assert alice.get_size()
|
||||
assert alice.is_configured()
|
||||
assert not alice.get_avatar()
|
||||
assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
|
||||
assert alice.get_contact_by_addr(bob_addr) is None # There is no address-contact, only key-contact
|
||||
assert alice.get_contacts()
|
||||
assert alice.get_contacts(snapshot=True)
|
||||
assert alice.self_contact
|
||||
@@ -287,7 +287,6 @@ def test_contact(acfactory) -> None:
|
||||
assert repr(alice_contact_bob)
|
||||
alice_contact_bob.block()
|
||||
alice_contact_bob.unblock()
|
||||
alice_contact_bob.reset_encryption()
|
||||
alice_contact_bob.set_name("new name")
|
||||
alice_contact_bob.get_encryption_info()
|
||||
snapshot = alice_contact_bob.get_snapshot()
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# content of group_tracking.py
|
||||
|
||||
from deltachat import account_hookimpl, run_cmdline
|
||||
|
||||
|
||||
class GroupTrackingPlugin:
|
||||
@account_hookimpl
|
||||
def ac_incoming_message(self, message):
|
||||
print("process_incoming message", message)
|
||||
if message.text.strip() == "/quit":
|
||||
message.account.shutdown()
|
||||
else:
|
||||
# unconditionally accept the chat
|
||||
message.create_chat()
|
||||
addr = message.get_sender_contact().addr
|
||||
text = message.text
|
||||
message.chat.send_text(f"echoing from {addr}:\n{text}")
|
||||
|
||||
@account_hookimpl
|
||||
def ac_outgoing_message(self, message):
|
||||
print("ac_outgoing_message:", message)
|
||||
|
||||
@account_hookimpl
|
||||
def ac_configure_completed(self, success):
|
||||
print("ac_configure_completed:", success)
|
||||
|
||||
@account_hookimpl
|
||||
def ac_chat_modified(self, chat):
|
||||
print("ac_chat_modified:", chat.id, chat.get_name())
|
||||
for member in chat.get_contacts():
|
||||
print(f"chat member: {member.addr}")
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, actor, message):
|
||||
print(f"ac_member_added {contact.addr} to chat {chat.id} from {actor or message.get_sender_contact().addr}")
|
||||
for member in chat.get_contacts():
|
||||
print(f"chat member: {member.addr}")
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, actor, message):
|
||||
print(f"ac_member_removed {contact.addr} from chat {chat.id} by {actor or message.get_sender_contact().addr}")
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
run_cmdline(argv=argv, account_plugins=[GroupTrackingPlugin()])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,10 +1,7 @@
|
||||
import echo_and_quit
|
||||
import group_tracking
|
||||
import py
|
||||
import pytest
|
||||
|
||||
from deltachat.events import FFIEventLogger
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def datadir():
|
||||
@@ -36,55 +33,3 @@ def test_echo_quit_plugin(acfactory, lp):
|
||||
lp.sec("send quit sequence")
|
||||
bot_chat.send_text("/quit")
|
||||
botproc.wait()
|
||||
|
||||
|
||||
def test_group_tracking_plugin(acfactory, lp):
|
||||
lp.sec("creating one group-tracking bot and two temp accounts")
|
||||
botproc = acfactory.run_bot_process(group_tracking)
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
ac1.add_account_plugin(FFIEventLogger(ac1))
|
||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||
|
||||
lp.sec("creating bot test group with bot")
|
||||
bot_chat = ac1.qr_setup_contact(botproc.qr)
|
||||
ac1._evtracker.wait_securejoin_joiner_progress(1000)
|
||||
bot_contact = bot_chat.get_contacts()[0]
|
||||
ch = ac1.create_group_chat("bot test group")
|
||||
ch.add_contact(bot_contact)
|
||||
ch.send_text("hello")
|
||||
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
*ac_chat_modified*bot test group*
|
||||
""",
|
||||
)
|
||||
|
||||
lp.sec("adding third member {}".format(ac2.get_config("addr")))
|
||||
contact3 = ac1.create_contact(ac2)
|
||||
ch.add_contact(contact3)
|
||||
|
||||
reply = ac1._evtracker.wait_next_incoming_message()
|
||||
assert "hello" in reply.text
|
||||
|
||||
lp.sec("now looking at what the bot received")
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
*ac_member_added {}*from*{}*
|
||||
""".format(
|
||||
contact3.addr,
|
||||
ac1.get_config("addr"),
|
||||
),
|
||||
)
|
||||
|
||||
lp.sec("contact successfully added, now removing")
|
||||
ch.remove_contact(contact3)
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
*ac_member_removed {}*from*{}*
|
||||
""".format(
|
||||
contact3.addr,
|
||||
ac1.get_config("addr"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -293,6 +293,8 @@ class Account:
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_contact(self, obj) -> Optional[Contact]:
|
||||
if isinstance(obj, Account):
|
||||
return self.create_contact(obj)
|
||||
if isinstance(obj, Contact):
|
||||
return obj
|
||||
(_, addr) = self.get_contact_addr_and_name(obj)
|
||||
|
||||
@@ -417,7 +417,13 @@ class Chat:
|
||||
:raises ValueError: if contact could not be added
|
||||
:returns: None
|
||||
"""
|
||||
from .contact import Contact
|
||||
|
||||
if isinstance(obj, Contact):
|
||||
contact = obj
|
||||
else:
|
||||
contact = self.account.create_contact(obj)
|
||||
|
||||
ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id)
|
||||
if ret != 1:
|
||||
raise ValueError(f"could not add contact {contact!r} to chat")
|
||||
|
||||
@@ -13,7 +13,6 @@ from .account import Account
|
||||
from .capi import ffi, lib
|
||||
from .cutil import from_optional_dc_charpointer
|
||||
from .hookspec import account_hookimpl
|
||||
from .message import map_system_message
|
||||
|
||||
|
||||
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||
@@ -304,21 +303,15 @@ class EventThread(threading.Thread):
|
||||
elif name == "DC_EVENT_INCOMING_MSG":
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
if msg is not None:
|
||||
yield map_system_message(msg) or ("ac_incoming_message", {"message": msg})
|
||||
yield ("ac_incoming_message", {"message": msg})
|
||||
elif name == "DC_EVENT_MSGS_CHANGED":
|
||||
if ffi_event.data2 != 0:
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
if msg is not None:
|
||||
if msg.is_outgoing():
|
||||
res = map_system_message(msg)
|
||||
if res and res[0].startswith("ac_member"):
|
||||
yield res
|
||||
yield "ac_outgoing_message", {"message": msg}
|
||||
elif msg.is_in_fresh():
|
||||
yield map_system_message(msg) or (
|
||||
"ac_incoming_message",
|
||||
{"message": msg},
|
||||
)
|
||||
yield "ac_incoming_message", {"message": msg}
|
||||
elif name == "DC_EVENT_REACTIONS_CHANGED":
|
||||
assert ffi_event.data1 > 0
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Union
|
||||
|
||||
@@ -504,56 +503,3 @@ def get_viewtype_code_from_name(view_type_name):
|
||||
raise ValueError(
|
||||
f"message typecode not found for {view_type_name!r}, available {list(_view_type_mapping.keys())!r}",
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# some helper code for turning system messages into hook events
|
||||
#
|
||||
|
||||
|
||||
def map_system_message(msg):
|
||||
if msg.is_system_message():
|
||||
res = parse_system_add_remove(msg.text)
|
||||
if not res:
|
||||
return None
|
||||
action, affected, actor = res
|
||||
affected = msg.account.get_contact_by_addr(affected)
|
||||
actor = None if actor == "me" else msg.account.get_contact_by_addr(actor)
|
||||
d = {"chat": msg.chat, "contact": affected, "actor": actor, "message": msg}
|
||||
return "ac_member_" + res[0], d
|
||||
|
||||
|
||||
def extract_addr(text):
|
||||
m = re.match(r".*\((.+@.+)\)", text)
|
||||
if m:
|
||||
text = m.group(1)
|
||||
text = text.rstrip(".")
|
||||
return text.strip()
|
||||
|
||||
|
||||
def parse_system_add_remove(text):
|
||||
"""return add/remove info from parsing the given system message text.
|
||||
|
||||
returns a (action, affected, actor) triple
|
||||
"""
|
||||
# You removed member a@b.
|
||||
# You added member a@b.
|
||||
# Member Me (x@y) removed by a@b.
|
||||
# Member x@y added by a@b
|
||||
# Member With space (tmp1@x.org) removed by tmp2@x.org.
|
||||
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
# Group left by some one (tmp1@x.org).
|
||||
# Group left by tmp1@x.org.
|
||||
text = text.lower()
|
||||
m = re.match(r"member (.+) (removed|added) by (.+)", text)
|
||||
if m:
|
||||
affected, action, actor = m.groups()
|
||||
return action, extract_addr(affected), extract_addr(actor)
|
||||
m = re.match(r"you (removed|added) member (.+)", text)
|
||||
if m:
|
||||
action, affected = m.groups()
|
||||
return action, extract_addr(affected), "me"
|
||||
if text.startswith("group left by "):
|
||||
addr = extract_addr(text[13:])
|
||||
if addr:
|
||||
return "removed", addr, addr
|
||||
|
||||
@@ -187,83 +187,6 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
assert msg.is_encrypted()
|
||||
|
||||
|
||||
def test_undecipherable_group(acfactory, lp):
|
||||
"""Test how group messages that cannot be decrypted are
|
||||
handled.
|
||||
|
||||
Group name is encrypted and plaintext subject is set to "..." in
|
||||
this case, so we should assign the messages to existing chat
|
||||
instead of creating a new one. Since there is no existing group
|
||||
chat, the messages should be assigned to 1-1 chat with the sender
|
||||
of the message.
|
||||
"""
|
||||
|
||||
lp.sec("creating and configuring three accounts")
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
acfactory.introduce_each_other([ac1, ac2, ac3])
|
||||
|
||||
lp.sec("ac3 reinstalls DC and generates a new key")
|
||||
ac3.stop_io()
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac4 = acfactory.new_online_configuring_account(cloned_from=ac3)
|
||||
acfactory.wait_configured(ac4)
|
||||
# Create contacts to make sure incoming messages are not treated as contact requests
|
||||
chat41 = ac4.create_chat(ac1)
|
||||
chat42 = ac4.create_chat(ac2)
|
||||
ac4.start_io()
|
||||
ac4._evtracker.wait_idle_inbox_ready()
|
||||
|
||||
lp.sec("ac1: creating group chat with 2 other members")
|
||||
chat = ac1.create_group_chat("title", contacts=[ac2, ac3])
|
||||
|
||||
lp.sec("ac1: send message to new group chat")
|
||||
msg = chat.send_text("hello")
|
||||
|
||||
lp.sec("ac2: checking that the chat arrived correctly")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
|
||||
# ac4 cannot decrypt the message.
|
||||
# Error message should be assigned to the chat with ac1.
|
||||
lp.sec("ac4: checking that message is assigned to the sender chat")
|
||||
error_msg = ac4._evtracker.wait_next_incoming_message()
|
||||
assert error_msg.error # There is an error decrypting the message
|
||||
assert error_msg.chat == chat41
|
||||
|
||||
lp.sec("ac2: sending a reply to the chat")
|
||||
msg.chat.send_text("reply")
|
||||
reply = ac1._evtracker.wait_next_incoming_message()
|
||||
assert reply.text == "reply"
|
||||
assert reply.is_encrypted(), "Reply is not encrypted"
|
||||
|
||||
lp.sec("ac4: checking that reply is assigned to ac2 chat")
|
||||
error_reply = ac4._evtracker.wait_next_incoming_message()
|
||||
assert error_reply.error # There is an error decrypting the message
|
||||
assert error_reply.chat == chat42
|
||||
|
||||
# Test that ac4 replies to error messages don't appear in the
|
||||
# group chat on ac1 and ac2.
|
||||
lp.sec("ac4: replying to ac1 and ac2")
|
||||
|
||||
# Otherwise reply becomes a contact request.
|
||||
chat41.send_text("I can't decrypt your message, ac1!")
|
||||
chat42.send_text("I can't decrypt your message, ac2!")
|
||||
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.error is None
|
||||
assert msg.text == "I can't decrypt your message, ac1!"
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
assert msg.chat == ac1.create_chat(ac3)
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.error is None
|
||||
assert msg.text == "I can't decrypt your message, ac2!"
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
assert msg.chat == ac2.create_chat(ac4)
|
||||
|
||||
|
||||
def test_ephemeral_timer(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
|
||||
@@ -767,7 +767,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
|
||||
def test_send_receive_encrypt(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
ac1.get_device_chat().mark_noticed()
|
||||
@@ -798,12 +798,11 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
|
||||
msg3.mark_seen()
|
||||
assert not list(ac1.get_fresh_messages())
|
||||
|
||||
lp.sec("create group chat with two members, one of which has no encrypt state")
|
||||
lp.sec("create group chat with two members")
|
||||
chat = ac1.create_group_chat("encryption test")
|
||||
chat.add_contact(ac2)
|
||||
chat.add_contact(ac1.create_contact("notexisting@testrun.org"))
|
||||
msg = chat.send_text("test not encrypt")
|
||||
assert not msg.is_encrypted()
|
||||
assert msg.is_encrypted()
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
|
||||
|
||||
@@ -1139,9 +1138,9 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
|
||||
lp.sec("create some chat content")
|
||||
some1_addr = some1.get_config("addr")
|
||||
chat1 = ac1.create_contact(some1_addr, name="some1").create_chat()
|
||||
chat1 = ac1.create_contact(some1).create_chat()
|
||||
chat1.send_text("msg1")
|
||||
assert len(ac1.get_contacts(query="some1")) == 1
|
||||
assert len(ac1.get_contacts()) == 1
|
||||
|
||||
original_image_path = data.get_path("d.png")
|
||||
chat1.send_image(original_image_path)
|
||||
@@ -1153,7 +1152,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
chat1.send_file(str(path))
|
||||
|
||||
def assert_account_is_proper(ac):
|
||||
contacts = ac.get_contacts(query="some1")
|
||||
contacts = ac.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == some1_addr
|
||||
@@ -1288,79 +1287,6 @@ def test_set_get_contact_avatar(acfactory, data, lp):
|
||||
assert msg6.get_sender_contact().get_profile_image() is None
|
||||
|
||||
|
||||
def test_add_remove_member_remote_events(acfactory, lp):
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
ac1_addr = ac1.get_config("addr")
|
||||
ac3_addr = ac3.get_config("addr")
|
||||
# activate local plugin for ac2
|
||||
in_list = queue.Queue()
|
||||
|
||||
class EventHolder:
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
class InPlugin:
|
||||
@account_hookimpl
|
||||
def ac_incoming_message(self, message):
|
||||
# we immediately accept the sender because
|
||||
# otherwise we won't see member_added contacts
|
||||
message.create_chat()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_chat_modified(self, chat):
|
||||
in_list.put(EventHolder(action="chat-modified", chat=chat))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, message):
|
||||
in_list.put(EventHolder(action="added", chat=chat, contact=contact, message=message))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, message):
|
||||
in_list.put(EventHolder(action="removed", chat=chat, contact=contact, message=message))
|
||||
|
||||
ac2.add_account_plugin(InPlugin())
|
||||
|
||||
lp.sec("ac1: create group chat with ac2")
|
||||
chat = ac1.create_group_chat("hello", contacts=[ac2])
|
||||
|
||||
lp.sec("ac1: send a message to group chat to promote the group")
|
||||
chat.send_text("afterwards promoted")
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
assert chat.is_promoted()
|
||||
assert sorted(x.addr for x in chat.get_contacts()) == sorted(x.addr for x in ev.chat.get_contacts())
|
||||
|
||||
lp.sec("ac1: add address2")
|
||||
# note that if the above create_chat() would not
|
||||
# happen we would not receive a proper member_added event
|
||||
contact2 = chat.add_contact(ac3)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get()
|
||||
assert ev.action == "added"
|
||||
assert ev.message.get_sender_contact().addr == ac1_addr
|
||||
assert ev.contact.addr == ac3_addr
|
||||
|
||||
lp.sec("ac1: remove address2")
|
||||
chat.remove_contact(contact2)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get()
|
||||
assert ev.action == "removed"
|
||||
assert ev.contact.addr == contact2.addr
|
||||
assert ev.message.get_sender_contact().addr == ac1_addr
|
||||
|
||||
lp.sec("ac1: remove ac2 contact from chat")
|
||||
chat.remove_contact(ac2)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get()
|
||||
assert ev.action == "removed"
|
||||
assert ev.message.get_sender_contact().addr == ac1_addr
|
||||
|
||||
|
||||
def test_system_group_msg_from_blocked_user(acfactory, lp):
|
||||
"""
|
||||
Tests that a blocked user removes you from a group.
|
||||
@@ -1760,44 +1686,6 @@ def test_configure_error_msgs_invalid_server(acfactory):
|
||||
assert "configuration" not in ev.data2.lower()
|
||||
|
||||
|
||||
def test_name_changes(acfactory):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("displayname", "Account 1")
|
||||
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
contact = None
|
||||
|
||||
def update_name():
|
||||
"""Send a message from ac1 to ac2 to update the name"""
|
||||
nonlocal contact
|
||||
chat12.send_text("Hello")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
contact = msg.get_sender_contact()
|
||||
return contact.name
|
||||
|
||||
assert update_name() == "Account 1"
|
||||
|
||||
ac1.set_config("displayname", "Account 1 revision 2")
|
||||
assert update_name() == "Account 1 revision 2"
|
||||
|
||||
# Explicitly rename contact on ac2 to "Renamed"
|
||||
ac2.create_contact(contact, name="Renamed")
|
||||
assert contact.name == "Renamed"
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
|
||||
assert ev.data1 == contact.id
|
||||
|
||||
# ac1 also renames itself into "Renamed"
|
||||
assert update_name() == "Renamed"
|
||||
ac1.set_config("displayname", "Renamed")
|
||||
assert update_name() == "Renamed"
|
||||
|
||||
# Contact name was set to "Renamed" explicitly before,
|
||||
# so it should not be changed.
|
||||
ac1.set_config("displayname", "Renamed again")
|
||||
updated_name = update_name()
|
||||
assert updated_name == "Renamed"
|
||||
|
||||
|
||||
def test_status(acfactory):
|
||||
"""Test that status is transferred over the network."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -1,51 +1,11 @@
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
import deltachat as dc
|
||||
from deltachat.tracker import ImexFailed
|
||||
from deltachat import Account, account_hookimpl, Message
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("msgtext", "res"),
|
||||
[
|
||||
(
|
||||
"Member Me (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org"),
|
||||
),
|
||||
(
|
||||
"Member With space (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org"),
|
||||
),
|
||||
(
|
||||
"Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org"),
|
||||
),
|
||||
(
|
||||
"Member With space (tmp1@x.org) removed by me",
|
||||
("removed", "tmp1@x.org", "me"),
|
||||
),
|
||||
(
|
||||
"Group left by some one (tmp1@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org"),
|
||||
),
|
||||
("Group left by tmp1@x.org.", ("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
(
|
||||
"Member tmp1@x.org added by tmp2@x.org.",
|
||||
("added", "tmp1@x.org", "tmp2@x.org"),
|
||||
),
|
||||
("Member nothing bla bla", None),
|
||||
("Another unknown system message", None),
|
||||
],
|
||||
)
|
||||
def test_parse_system_add_remove(msgtext, res):
|
||||
from deltachat.message import parse_system_add_remove
|
||||
|
||||
out = parse_system_add_remove(msgtext)
|
||||
assert out == res
|
||||
from deltachat import Account, Message
|
||||
|
||||
|
||||
class TestOfflineAccountBasic:
|
||||
@@ -177,14 +137,15 @@ class TestOfflineContact:
|
||||
|
||||
def test_get_contacts_and_delete(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact(ac2)
|
||||
contacts = ac1.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contact1 in contacts
|
||||
|
||||
assert not ac1.get_contacts(query="some2")
|
||||
assert ac1.get_contacts(query="some1")
|
||||
assert not ac1.get_contacts(query="some1")
|
||||
assert len(ac1.get_contacts(with_self=True)) == 2
|
||||
assert contact1 in ac1.get_contacts()
|
||||
|
||||
assert ac1.delete_contact(contact1)
|
||||
assert contact1 not in ac1.get_contacts()
|
||||
@@ -199,9 +160,9 @@ class TestOfflineContact:
|
||||
def test_create_chat_flexibility(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
chat2 = ac1.create_chat(ac2.get_self_contact().addr)
|
||||
assert chat1 == chat2
|
||||
chat1 = ac1.create_chat(ac2) # This creates a key-contact chat
|
||||
chat2 = ac1.create_chat(ac2.get_self_contact().addr) # This creates address-contact chat
|
||||
assert chat1 != chat2
|
||||
ac3 = acfactory.get_unconfigured_account()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.create_chat(ac3)
|
||||
@@ -259,17 +220,18 @@ class TestOfflineChat:
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
with pytest.raises(ValueError):
|
||||
chat.add_contact(ac2.get_self_contact())
|
||||
contact = chat.add_contact(ac2)
|
||||
assert contact.addr == ac2.get_config("addr")
|
||||
assert contact.name == ac2.get_config("displayname")
|
||||
assert contact.account == ac1
|
||||
chat.remove_contact(ac2)
|
||||
|
||||
def test_group_chat_creation(self, ac1):
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some2@example.org", name="some2")
|
||||
def test_group_chat_creation(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
ac3 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact(ac2)
|
||||
contact2 = ac1.create_contact(ac3)
|
||||
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
|
||||
assert chat.get_name() == "title1"
|
||||
assert contact1 in chat.get_contacts()
|
||||
@@ -316,13 +278,14 @@ class TestOfflineChat:
|
||||
qr = chat.get_join_qr()
|
||||
assert ac2.check_qr(qr).is_ask_verifygroup
|
||||
|
||||
def test_removing_blocked_user_from_group(self, ac1, lp):
|
||||
def test_removing_blocked_user_from_group(self, ac1, acfactory, lp):
|
||||
"""
|
||||
Test that blocked contact is not unblocked when removed from a group.
|
||||
See https://github.com/deltachat/deltachat-core-rust/issues/2030
|
||||
"""
|
||||
lp.sec("Create a group chat with a contact")
|
||||
contact = ac1.create_contact("some1@example.org")
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
contact = ac1.create_contact(ac2)
|
||||
group = ac1.create_group_chat("title", contacts=[contact])
|
||||
group.send_text("First group message")
|
||||
|
||||
@@ -334,10 +297,6 @@ class TestOfflineChat:
|
||||
group.remove_contact(contact)
|
||||
assert contact.is_blocked()
|
||||
|
||||
lp.sec("ac1 adding blocked contact unblocks it")
|
||||
group.add_contact(contact)
|
||||
assert not contact.is_blocked()
|
||||
|
||||
def test_get_set_profile_image_simple(self, ac1, data):
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
p = data.get_path("d.png")
|
||||
@@ -480,7 +439,8 @@ class TestOfflineChat:
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
ac_contact = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact(ac_contact).create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
@@ -495,10 +455,10 @@ class TestOfflineChat:
|
||||
assert os.path.exists(path)
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2.import_all(path)
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
@@ -511,8 +471,9 @@ class TestOfflineChat:
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
ac1 = acfactory.get_pseudo_configured_account(passphrase=passphrase1)
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
chat = ac1.create_contact(ac2).create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
@@ -533,10 +494,10 @@ class TestOfflineChat:
|
||||
ac2.import_all(path)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
contact2_addr = contact2.addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
@@ -550,10 +511,10 @@ class TestOfflineChat:
|
||||
ac2.open(passphrase2)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
assert contact2.addr == contact2_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
@@ -566,8 +527,9 @@ class TestOfflineChat:
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac_contact = acfactory.get_pseudo_configured_account()
|
||||
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
chat = ac1.create_contact(ac_contact).create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
@@ -589,10 +551,10 @@ class TestOfflineChat:
|
||||
ac2.import_all(path, passphrase)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
@@ -611,7 +573,8 @@ class TestOfflineChat:
|
||||
backupdir.mkdir()
|
||||
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
ac_contact = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact(ac_contact).create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
@@ -634,10 +597,10 @@ class TestOfflineChat:
|
||||
ac2.import_all(path, bak_passphrase)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
@@ -651,10 +614,10 @@ class TestOfflineChat:
|
||||
ac2.open(acct_passphrase)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
@@ -681,78 +644,10 @@ class TestOfflineChat:
|
||||
assert not res.is_ask_verifygroup()
|
||||
assert res.contact_id == 10
|
||||
|
||||
def test_group_chat_many_members_add_remove(self, ac1, lp):
|
||||
lp.sec("ac1: creating group chat with 10 other members")
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
# promote chat
|
||||
chat.send_text("hello")
|
||||
assert chat.is_promoted()
|
||||
def test_audit_log_view_without_daymarker(self, acfactory, lp):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
|
||||
# activate local plugin
|
||||
in_list = []
|
||||
|
||||
class InPlugin:
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, actor):
|
||||
in_list.append(("added", chat, contact, actor))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, actor):
|
||||
in_list.append(("removed", chat, contact, actor))
|
||||
|
||||
ac1.add_account_plugin(InPlugin())
|
||||
|
||||
# perform add contact many times
|
||||
contacts = []
|
||||
for i in range(10):
|
||||
lp.sec("create contact")
|
||||
contact = ac1.create_contact(f"some{i}@example.org")
|
||||
contacts.append(contact)
|
||||
lp.sec("add contact")
|
||||
chat.add_contact(contact)
|
||||
|
||||
assert chat.num_contacts() == 11
|
||||
|
||||
# let's make sure the events perform plugin hooks
|
||||
def wait_events(cond):
|
||||
now = time.time()
|
||||
while time.time() < now + 5:
|
||||
if cond():
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
pytest.fail("failed to get events")
|
||||
|
||||
wait_events(lambda: len(in_list) == 10)
|
||||
|
||||
assert len(in_list) == 10
|
||||
chat_contacts = chat.get_contacts()
|
||||
for in_cmd, in_chat, in_contact, in_actor in in_list:
|
||||
assert in_cmd == "added"
|
||||
assert in_chat == chat
|
||||
assert in_contact in chat_contacts
|
||||
assert in_actor is None
|
||||
chat_contacts.remove(in_contact)
|
||||
|
||||
assert chat_contacts[0].id == 1 # self contact
|
||||
|
||||
in_list[:] = []
|
||||
|
||||
lp.sec("ac1: removing two contacts and checking things are right")
|
||||
chat.remove_contact(contacts[9])
|
||||
chat.remove_contact(contacts[3])
|
||||
assert chat.num_contacts() == 9
|
||||
|
||||
wait_events(lambda: len(in_list) == 2)
|
||||
assert len(in_list) == 2
|
||||
assert in_list[0][0] == "removed"
|
||||
assert in_list[0][1] == chat
|
||||
assert in_list[0][2] == contacts[9]
|
||||
assert in_list[1][0] == "removed"
|
||||
assert in_list[1][1] == chat
|
||||
assert in_list[1][2] == contacts[3]
|
||||
|
||||
def test_audit_log_view_without_daymarker(self, ac1, lp):
|
||||
lp.sec("ac1: test audit log (show only system messages)")
|
||||
chat = ac1.create_group_chat(name="audit log sample data")
|
||||
|
||||
@@ -761,7 +656,7 @@ class TestOfflineChat:
|
||||
assert chat.is_promoted()
|
||||
|
||||
lp.sec("create test data")
|
||||
chat.add_contact(ac1.create_contact("some-1@example.org"))
|
||||
chat.add_contact(ac2)
|
||||
chat.set_name("audit log test group")
|
||||
chat.send_text("a message in between")
|
||||
|
||||
|
||||
@@ -266,7 +266,6 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::mimeparser;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools;
|
||||
@@ -520,41 +519,6 @@ Authentication-Results: dkim=";
|
||||
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
|
||||
}
|
||||
|
||||
// Test that Autocrypt works with mailing list.
|
||||
//
|
||||
// Previous versions of Delta Chat ignored Autocrypt based on the List-Post header.
|
||||
// This is not needed: comparing of the From address to Autocrypt header address is enough.
|
||||
// If the mailing list is not rewriting the From header, Autocrypt should be applied.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_autocrypt_in_mailinglist_not_ignored() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let alice_bob_chat = alice.create_chat(&bob).await;
|
||||
let bob_alice_chat = bob.create_chat(&alice).await;
|
||||
let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
|
||||
sent.payload
|
||||
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
|
||||
bob.recv_msg(&sent).await;
|
||||
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
|
||||
assert!(peerstate.is_some());
|
||||
|
||||
// Bob can now write encrypted to Alice:
|
||||
let mut sent = bob
|
||||
.send_text(bob_alice_chat.id, "hellooo in the mailinglist again")
|
||||
.await;
|
||||
assert!(sent.load_from_db().await.get_showpadlock());
|
||||
|
||||
sent.payload
|
||||
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
|
||||
let rcvd = alice.recv_msg(&sent).await;
|
||||
assert!(rcvd.get_showpadlock());
|
||||
assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_authres_in_mailinglist_ignored() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
509
src/chat.rs
509
src/chat.rs
@@ -15,17 +15,14 @@ use deltachat_derive::{FromSql, ToSql};
|
||||
use mail_builder::mime::MimePart;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::EnumIter;
|
||||
use tokio::task;
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
|
||||
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX,
|
||||
TIMESTAMP_SENT_TOLERANCE,
|
||||
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
|
||||
DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX, TIMESTAMP_SENT_TOLERANCE,
|
||||
};
|
||||
use crate::contact::{self, Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
@@ -39,7 +36,6 @@ use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::receive_imf::ReceivedMsg;
|
||||
use crate::smtp::send_msg_to_smtp;
|
||||
use crate::stock_str;
|
||||
@@ -130,8 +126,8 @@ pub(crate) enum CantSendReason {
|
||||
/// Not a member of the chat.
|
||||
NotAMember,
|
||||
|
||||
/// Temporary state for 1:1 chats while SecureJoin is in progress.
|
||||
SecurejoinWait,
|
||||
/// State for 1:1 chat with a key-contact that does not have a key.
|
||||
MissingKey,
|
||||
}
|
||||
|
||||
impl fmt::Display for CantSendReason {
|
||||
@@ -151,7 +147,7 @@ impl fmt::Display for CantSendReason {
|
||||
write!(f, "mailing list does not have a know post address")
|
||||
}
|
||||
Self::NotAMember => write!(f, "not a member of the chat"),
|
||||
Self::SecurejoinWait => write!(f, "awaiting SecureJoin for 1:1 chat"),
|
||||
Self::MissingKey => write!(f, "key is missing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1326,8 +1322,12 @@ impl ChatId {
|
||||
///
|
||||
/// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`].
|
||||
pub async fn get_encryption_info(self, context: &Context) -> Result<String> {
|
||||
let mut ret_available = String::new();
|
||||
let mut ret_reset = String::new();
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
if !chat.is_encrypted(context).await? {
|
||||
return Ok(stock_str::encr_none(context).await);
|
||||
}
|
||||
|
||||
let mut ret = stock_str::e2e_available(context).await + "\n";
|
||||
|
||||
for contact_id in get_chat_contacts(context, self)
|
||||
.await?
|
||||
@@ -1336,34 +1336,15 @@ impl ChatId {
|
||||
{
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
let addr = contact.get_addr();
|
||||
let peerstate = Peerstate::from_addr(context, addr).await?;
|
||||
|
||||
match peerstate
|
||||
.filter(|peerstate| peerstate.peek_key(false).is_some())
|
||||
.map(|peerstate| peerstate.prefer_encrypt)
|
||||
{
|
||||
Some(EncryptPreference::Mutual) | Some(EncryptPreference::NoPreference) => {
|
||||
ret_available += &format!("{addr}\n")
|
||||
debug_assert!(contact.is_key_contact());
|
||||
let fingerprint = contact
|
||||
.fingerprint()
|
||||
.context("Contact does not have a fingerprint in encrypted chat")?;
|
||||
if contact.public_key(context).await?.is_some() {
|
||||
ret += &format!("\n{addr}\n{fingerprint}\n");
|
||||
} else {
|
||||
ret += &format!("\n{addr}\n(key missing)\n{fingerprint}\n");
|
||||
}
|
||||
Some(EncryptPreference::Reset) | None => ret_reset += &format!("{addr}\n"),
|
||||
};
|
||||
}
|
||||
|
||||
let mut ret = String::new();
|
||||
if !ret_reset.is_empty() {
|
||||
ret += &stock_str::encr_none(context).await;
|
||||
ret.push(':');
|
||||
ret.push('\n');
|
||||
ret += &ret_reset;
|
||||
}
|
||||
if !ret_available.is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push('\n');
|
||||
}
|
||||
ret += &stock_str::e2e_available(context).await;
|
||||
ret.push(':');
|
||||
ret.push('\n');
|
||||
ret += &ret_available;
|
||||
}
|
||||
|
||||
Ok(ret.trim().to_string())
|
||||
@@ -1473,18 +1454,6 @@ impl ChatId {
|
||||
|
||||
Ok(sort_timestamp)
|
||||
}
|
||||
|
||||
/// Spawns a task checking after a timeout whether the SecureJoin has finished for the 1:1 chat
|
||||
/// and otherwise notifying the user accordingly.
|
||||
pub(crate) fn spawn_securejoin_wait(self, context: &Context, timeout: u64) {
|
||||
let context = context.clone();
|
||||
task::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(timeout)).await;
|
||||
let chat = Chat::load_from_db(&context, self).await?;
|
||||
chat.check_securejoin_wait(&context, 0).await?;
|
||||
Result::<()>::Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ChatId {
|
||||
@@ -1696,15 +1665,18 @@ impl Chat {
|
||||
if !skip_fn(&reason) && !self.is_self_in_chat(context).await? {
|
||||
return Ok(Some(reason));
|
||||
}
|
||||
let reason = SecurejoinWait;
|
||||
if !skip_fn(&reason)
|
||||
&& self
|
||||
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
|
||||
.await?
|
||||
> 0
|
||||
{
|
||||
|
||||
let reason = MissingKey;
|
||||
if !skip_fn(&reason) && self.typ == Chattype::Single {
|
||||
let contact_ids = get_chat_contacts(context, self.id).await?;
|
||||
if let Some(contact_id) = contact_ids.first() {
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
if contact.is_key_contact() && contact.public_key(context).await?.is_none() {
|
||||
return Ok(Some(reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -1715,74 +1687,6 @@ impl Chat {
|
||||
Ok(self.why_cant_send(context).await?.is_none())
|
||||
}
|
||||
|
||||
/// Returns the remaining timeout for the 1:1 chat in-progress SecureJoin.
|
||||
///
|
||||
/// If the timeout has expired, adds an info message with additional information.
|
||||
/// See also [`CantSendReason::SecurejoinWait`].
|
||||
pub(crate) async fn check_securejoin_wait(
|
||||
&self,
|
||||
context: &Context,
|
||||
timeout: u64,
|
||||
) -> Result<u64> {
|
||||
if self.typ != Chattype::Single || self.protected != ProtectionStatus::Unprotected {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// chat is single and unprotected:
|
||||
// get last info message of type SecurejoinWait or SecurejoinWaitTimeout
|
||||
let (mut param_wait, mut param_timeout) = (Params::new(), Params::new());
|
||||
param_wait.set_cmd(SystemMessage::SecurejoinWait);
|
||||
param_timeout.set_cmd(SystemMessage::SecurejoinWaitTimeout);
|
||||
let (param_wait, param_timeout) = (param_wait.to_string(), param_timeout.to_string());
|
||||
let Some((param, ts_sort, ts_start)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT param, timestamp, timestamp_sent FROM msgs WHERE id=\
|
||||
(SELECT MAX(id) FROM msgs WHERE chat_id=? AND param IN (?, ?))",
|
||||
(self.id, ¶m_wait, ¶m_timeout),
|
||||
|row| {
|
||||
let param: String = row.get(0)?;
|
||||
let ts_sort: i64 = row.get(1)?;
|
||||
let ts_start: i64 = row.get(2)?;
|
||||
Ok((param, ts_sort, ts_start))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(0);
|
||||
};
|
||||
if param == param_timeout {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let now = time();
|
||||
// Don't await SecureJoin if the clock was set back.
|
||||
if ts_start <= now {
|
||||
let timeout = ts_start
|
||||
.saturating_add(timeout.try_into()?)
|
||||
.saturating_sub(now);
|
||||
if timeout > 0 {
|
||||
return Ok(timeout as u64);
|
||||
}
|
||||
}
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
self.id,
|
||||
&stock_str::securejoin_takes_longer(context).await,
|
||||
SystemMessage::SecurejoinWaitTimeout,
|
||||
// Use the sort timestamp of the "please wait" message, this way the added message is
|
||||
// never sorted below the protection message if the SecureJoin finishes in parallel.
|
||||
ts_sort,
|
||||
Some(now),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
context.emit_event(EventType::ChatModified(self.id));
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Checks if the user is part of a chat
|
||||
/// and has basically the permissions to edit the chat therefore.
|
||||
/// The function does not check if the chat type allows editing of concrete elements.
|
||||
@@ -1826,25 +1730,36 @@ impl Chat {
|
||||
|
||||
/// Returns profile image path for the chat.
|
||||
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
|
||||
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if self.id.is_archived_link() {
|
||||
// This is not a real chat, but the "Archive" button
|
||||
// that is shown at the top of the chats list
|
||||
return Ok(Some(get_archive_icon(context).await?));
|
||||
} else if self.is_device_talk() {
|
||||
return Ok(Some(get_device_icon(context).await?));
|
||||
} else if self.is_self_talk() {
|
||||
return Ok(Some(get_saved_messages_icon(context).await?));
|
||||
} else if self.typ == Chattype::Single {
|
||||
// For 1:1 chats, we always use the same avatar as for the contact
|
||||
// This is before the `self.is_encrypted()` check, because that function
|
||||
// has two database calls, i.e. it's slow
|
||||
let contacts = get_chat_contacts(context, self.id).await?;
|
||||
if let Some(contact_id) = contacts.first() {
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
return contact.get_profile_image(context).await;
|
||||
}
|
||||
} else if !self.is_encrypted(context).await? {
|
||||
// This is an address-contact chat, show a special avatar that marks it as such
|
||||
return Ok(Some(get_abs_path(
|
||||
context,
|
||||
Path::new(&get_address_contact_icon(context).await?),
|
||||
)));
|
||||
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
// Load the group avatar, or the device-chat / saved-messages icon
|
||||
if !image_rel.is_empty() {
|
||||
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
|
||||
}
|
||||
} else if self.id.is_archived_link() {
|
||||
if let Ok(image_rel) = get_archive_icon(context).await {
|
||||
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
|
||||
}
|
||||
} else if self.typ == Chattype::Single {
|
||||
let contacts = get_chat_contacts(context, self.id).await?;
|
||||
if let Some(contact_id) = contacts.first() {
|
||||
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
|
||||
return contact.get_profile_image(context).await;
|
||||
}
|
||||
}
|
||||
} else if self.typ == Chattype::Broadcast {
|
||||
if let Ok(image_rel) = get_broadcast_icon(context).await {
|
||||
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
|
||||
}
|
||||
return Ok(Some(get_broadcast_icon(context).await?));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -1935,6 +1850,33 @@ impl Chat {
|
||||
self.protected == ProtectionStatus::Protected
|
||||
}
|
||||
|
||||
/// Returns true if the chat is encrypted.
|
||||
pub async fn is_encrypted(&self, context: &Context) -> Result<bool> {
|
||||
let is_encrypted = self.is_protected()
|
||||
|| match self.typ {
|
||||
Chattype::Single => {
|
||||
let chat_contact_ids = get_chat_contacts(context, self.id).await?;
|
||||
if let Some(contact_id) = chat_contact_ids.first() {
|
||||
if *contact_id == ContactId::DEVICE {
|
||||
true
|
||||
} else {
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
contact.is_key_contact()
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
Chattype::Group => {
|
||||
// Do not encrypt ad-hoc groups.
|
||||
!self.grpid.is_empty()
|
||||
}
|
||||
Chattype::Mailinglist => false,
|
||||
Chattype::Broadcast => true,
|
||||
};
|
||||
Ok(is_encrypted)
|
||||
}
|
||||
|
||||
/// Returns true if the chat was protected, and then an incoming message broke this protection.
|
||||
///
|
||||
/// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
|
||||
@@ -2287,6 +2229,26 @@ impl Chat {
|
||||
|
||||
/// Sends a `SyncAction` synchronising chat contacts to other devices.
|
||||
pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> {
|
||||
if self.is_encrypted(context).await? {
|
||||
let fingerprint_addrs = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.fingerprint, c.addr
|
||||
FROM contacts c INNER JOIN chats_contacts cc
|
||||
ON c.id=cc.contact_id
|
||||
WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp",
|
||||
(self.id,),
|
||||
|row| {
|
||||
let fingerprint = row.get(0)?;
|
||||
let addr = row.get(1)?;
|
||||
Ok((fingerprint, addr))
|
||||
},
|
||||
|addrs| addrs.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
self.sync(context, SyncAction::SetPgpContacts(fingerprint_addrs))
|
||||
.await?;
|
||||
} else {
|
||||
let addrs = context
|
||||
.sql
|
||||
.query_map(
|
||||
@@ -2299,7 +2261,9 @@ impl Chat {
|
||||
|addrs| addrs.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
self.sync(context, SyncAction::SetContacts(addrs)).await
|
||||
self.sync(context, SyncAction::SetContacts(addrs)).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns chat id for the purpose of synchronisation across devices.
|
||||
@@ -2319,8 +2283,12 @@ impl Chat {
|
||||
return Ok(None);
|
||||
}
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
if let Some(fingerprint) = contact.fingerprint() {
|
||||
r = Some(SyncId::ContactFingerprint(fingerprint.hex()));
|
||||
} else {
|
||||
r = Some(SyncId::ContactAddr(contact.get_addr().to_string()));
|
||||
}
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
Chattype::Broadcast | Chattype::Group | Chattype::Mailinglist => {
|
||||
@@ -2465,69 +2433,63 @@ pub struct ChatInfo {
|
||||
// - [ ] email
|
||||
}
|
||||
|
||||
pub(crate) async fn update_saved_messages_icon(context: &Context) -> Result<()> {
|
||||
if let Some(ChatIdBlocked { id: chat_id, .. }) =
|
||||
ChatIdBlocked::lookup_by_contact(context, ContactId::SELF).await?
|
||||
{
|
||||
let icon = include_bytes!("../assets/icon-saved-messages.png");
|
||||
async fn get_asset_icon(context: &Context, name: &str, bytes: &[u8]) -> Result<PathBuf> {
|
||||
ensure!(name.starts_with("icon-"));
|
||||
if let Some(icon) = context.sql.get_raw_config(name).await? {
|
||||
return Ok(get_abs_path(context, Path::new(&icon)));
|
||||
}
|
||||
|
||||
let blob =
|
||||
BlobObject::create_and_deduplicate_from_bytes(context, icon, "saved-messages.png")?;
|
||||
BlobObject::create_and_deduplicate_from_bytes(context, bytes, &format!("{name}.png"))?;
|
||||
let icon = blob.as_name().to_string();
|
||||
context.sql.set_raw_config(name, Some(&icon)).await?;
|
||||
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
chat.param.set(Param::ProfileImage, icon);
|
||||
chat.update_param(context).await?;
|
||||
}
|
||||
Ok(())
|
||||
Ok(get_abs_path(context, Path::new(&icon)))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_device_icon(context: &Context) -> Result<()> {
|
||||
if let Some(ChatIdBlocked { id: chat_id, .. }) =
|
||||
ChatIdBlocked::lookup_by_contact(context, ContactId::DEVICE).await?
|
||||
{
|
||||
let icon = include_bytes!("../assets/icon-device.png");
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "device.png")?;
|
||||
let icon = blob.as_name().to_string();
|
||||
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
chat.param.set(Param::ProfileImage, &icon);
|
||||
chat.update_param(context).await?;
|
||||
|
||||
let mut contact = Contact::get_by_id(context, ContactId::DEVICE).await?;
|
||||
contact.param.set(Param::ProfileImage, icon);
|
||||
contact.update_param(context).await?;
|
||||
}
|
||||
Ok(())
|
||||
pub(crate) async fn get_saved_messages_icon(context: &Context) -> Result<PathBuf> {
|
||||
get_asset_icon(
|
||||
context,
|
||||
"icon-saved-messages",
|
||||
include_bytes!("../assets/icon-saved-messages.png"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_broadcast_icon(context: &Context) -> Result<String> {
|
||||
if let Some(icon) = context.sql.get_raw_config("icon-broadcast").await? {
|
||||
return Ok(icon);
|
||||
}
|
||||
|
||||
let icon = include_bytes!("../assets/icon-broadcast.png");
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "broadcast.png")?;
|
||||
let icon = blob.as_name().to_string();
|
||||
context
|
||||
.sql
|
||||
.set_raw_config("icon-broadcast", Some(&icon))
|
||||
.await?;
|
||||
Ok(icon)
|
||||
pub(crate) async fn get_device_icon(context: &Context) -> Result<PathBuf> {
|
||||
get_asset_icon(
|
||||
context,
|
||||
"icon-device",
|
||||
include_bytes!("../assets/icon-device.png"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_archive_icon(context: &Context) -> Result<String> {
|
||||
if let Some(icon) = context.sql.get_raw_config("icon-archive").await? {
|
||||
return Ok(icon);
|
||||
}
|
||||
pub(crate) async fn get_broadcast_icon(context: &Context) -> Result<PathBuf> {
|
||||
get_asset_icon(
|
||||
context,
|
||||
"icon-broadcast",
|
||||
include_bytes!("../assets/icon-broadcast.png"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
let icon = include_bytes!("../assets/icon-archive.png");
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "archive.png")?;
|
||||
let icon = blob.as_name().to_string();
|
||||
context
|
||||
.sql
|
||||
.set_raw_config("icon-archive", Some(&icon))
|
||||
.await?;
|
||||
Ok(icon)
|
||||
pub(crate) async fn get_archive_icon(context: &Context) -> Result<PathBuf> {
|
||||
get_asset_icon(
|
||||
context,
|
||||
"icon-archive",
|
||||
include_bytes!("../assets/icon-archive.png"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_address_contact_icon(context: &Context) -> Result<PathBuf> {
|
||||
get_asset_icon(
|
||||
context,
|
||||
"icon-address-contact",
|
||||
include_bytes!("../assets/icon-address-contact.png"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_special_chat_name(
|
||||
@@ -2566,34 +2528,6 @@ pub(crate) async fn update_special_chat_names(context: &Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if there is a 1:1 chat in-progress SecureJoin for Bob and, if necessary, schedules a task
|
||||
/// unblocking the chat and notifying the user accordingly.
|
||||
pub(crate) async fn resume_securejoin_wait(context: &Context) -> Result<()> {
|
||||
let chat_ids: Vec<ChatId> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT chat_id FROM bobstate",
|
||||
(),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
Ok(chat_id)
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for chat_id in chat_ids {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
let timeout = chat
|
||||
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
|
||||
.await?;
|
||||
if timeout > 0 {
|
||||
chat_id.spawn_securejoin_wait(context, timeout);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle a [`ChatId`] and its [`Blocked`] status at once.
|
||||
///
|
||||
/// This struct is an optimisation to read a [`ChatId`] and its [`Blocked`] status at once
|
||||
@@ -2677,12 +2611,7 @@ impl ChatIdBlocked {
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let protected = contact_id == ContactId::SELF || {
|
||||
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
|
||||
peerstate.is_some_and(|p| {
|
||||
p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
|
||||
})
|
||||
};
|
||||
let protected = contact_id == ContactId::SELF || contact.is_verified(context).await?;
|
||||
let smeared_time = create_smeared_timestamp(context);
|
||||
|
||||
let chat_id = context
|
||||
@@ -2734,12 +2663,6 @@ impl ChatIdBlocked {
|
||||
.await?;
|
||||
}
|
||||
|
||||
match contact_id {
|
||||
ContactId::SELF => update_saved_messages_icon(context).await?,
|
||||
ContactId::DEVICE => update_device_icon(context).await?,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
id: chat_id,
|
||||
blocked: create_blocked,
|
||||
@@ -2919,9 +2842,7 @@ async fn prepare_send_msg(
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
|
||||
let skip_fn = |reason: &CantSendReason| match reason {
|
||||
CantSendReason::ProtectionBroken
|
||||
| CantSendReason::ContactRequest
|
||||
| CantSendReason::SecurejoinWait => {
|
||||
CantSendReason::ProtectionBroken | CantSendReason::ContactRequest => {
|
||||
// Allow securejoin messages, they are supposed to repair the verification.
|
||||
// If the chat is a contact request, let the user accept it later.
|
||||
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
@@ -2930,6 +2851,10 @@ async fn prepare_send_msg(
|
||||
// Necessary checks should be made anyway before removing contact
|
||||
// from the chat.
|
||||
CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup,
|
||||
CantSendReason::MissingKey => msg
|
||||
.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
.unwrap_or_default(),
|
||||
_ => false,
|
||||
};
|
||||
if let Some(reason) = chat.why_cant_send_ex(context, &skip_fn).await? {
|
||||
@@ -3898,6 +3823,10 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
chat.typ != Chattype::Broadcast || contact_id != ContactId::SELF,
|
||||
"Cannot add SELF to broadcast."
|
||||
);
|
||||
ensure!(
|
||||
chat.is_encrypted(context).await? == contact.is_key_contact(),
|
||||
"Only key-contacts can be added to encrypted chats"
|
||||
);
|
||||
|
||||
if !chat.is_self_in_chat(context).await? {
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
@@ -3949,7 +3878,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
msg.viewtype = Viewtype::Text;
|
||||
|
||||
let contact_addr = contact.get_addr().to_lowercase();
|
||||
msg.text = stock_str::msg_add_member_local(context, &contact_addr, ContactId::SELF).await;
|
||||
msg.text = stock_str::msg_add_member_local(context, contact.id, ContactId::SELF).await;
|
||||
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
|
||||
msg.param.set(Param::Arg, contact_addr);
|
||||
msg.param.set_int(Param::Arg2, from_handshake.into());
|
||||
@@ -4143,11 +4072,8 @@ pub async fn remove_contact_from_chat(
|
||||
if contact_id == ContactId::SELF {
|
||||
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
|
||||
} else {
|
||||
msg.text = stock_str::msg_del_member_local(
|
||||
context,
|
||||
contact.get_addr(),
|
||||
ContactId::SELF,
|
||||
)
|
||||
msg.text =
|
||||
stock_str::msg_del_member_local(context, contact_id, ContactId::SELF)
|
||||
.await;
|
||||
}
|
||||
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
|
||||
@@ -4283,8 +4209,12 @@ pub async fn set_chat_profile_image(
|
||||
ensure!(!chat_id.is_special(), "Invalid chat ID");
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Group || chat.typ == Chattype::Mailinglist,
|
||||
"Failed to set profile image; group does not exist"
|
||||
chat.typ == Chattype::Group,
|
||||
"Can only set profile image for group chats"
|
||||
);
|
||||
ensure!(
|
||||
!chat.grpid.is_empty(),
|
||||
"Cannot set profile image for ad hoc groups"
|
||||
);
|
||||
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
||||
if !is_contact_in_chat(context, chat_id, ContactId::SELF).await? {
|
||||
@@ -4849,6 +4779,10 @@ pub(crate) async fn update_msg_text_and_timestamp(
|
||||
/// Set chat contacts by their addresses creating the corresponding contacts if necessary.
|
||||
async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String]) -> Result<()> {
|
||||
let chat = Chat::load_from_db(context, id).await?;
|
||||
ensure!(
|
||||
!chat.is_encrypted(context).await?,
|
||||
"Cannot add address-contacts to encrypted chat {id}"
|
||||
);
|
||||
ensure!(
|
||||
chat.typ == Chattype::Broadcast,
|
||||
"{id} is not a broadcast list",
|
||||
@@ -4884,10 +4818,64 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String])
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set chat contacts by their fingerprints creating the corresponding contacts if necessary.
|
||||
///
|
||||
/// `fingerprint_addrs` is a list of pairs of fingerprint and address.
|
||||
async fn set_contacts_by_fingerprints(
|
||||
context: &Context,
|
||||
id: ChatId,
|
||||
fingerprint_addrs: &[(String, String)],
|
||||
) -> Result<()> {
|
||||
let chat = Chat::load_from_db(context, id).await?;
|
||||
ensure!(
|
||||
chat.is_encrypted(context).await?,
|
||||
"Cannot add key-contacts to unencrypted chat {id}"
|
||||
);
|
||||
ensure!(
|
||||
chat.typ == Chattype::Broadcast,
|
||||
"{id} is not a broadcast list",
|
||||
);
|
||||
let mut contacts = HashSet::new();
|
||||
for (fingerprint, addr) in fingerprint_addrs {
|
||||
let contact_addr = ContactAddress::new(addr)?;
|
||||
let contact =
|
||||
Contact::add_or_lookup_ex(context, "", &contact_addr, fingerprint, Origin::Hidden)
|
||||
.await?
|
||||
.0;
|
||||
contacts.insert(contact);
|
||||
}
|
||||
let contacts_old = HashSet::<ContactId>::from_iter(get_chat_contacts(context, id).await?);
|
||||
if contacts == contacts_old {
|
||||
return Ok(());
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
|
||||
|
||||
// We do not care about `add_timestamp` column
|
||||
// because timestamps are not used for broadcast lists.
|
||||
let mut statement = transaction
|
||||
.prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
|
||||
for contact_id in &contacts {
|
||||
statement.execute((id, contact_id))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
context.emit_event(EventType::ChatModified(id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A cross-device chat id used for synchronisation.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub(crate) enum SyncId {
|
||||
/// E-mail address of the contact.
|
||||
ContactAddr(String),
|
||||
|
||||
/// OpenPGP key fingerprint of the contact.
|
||||
ContactFingerprint(String),
|
||||
|
||||
Grpid(String),
|
||||
/// "Message-ID"-s, from oldest to latest. Used for ad-hoc groups.
|
||||
Msgids(Vec<String>),
|
||||
@@ -4909,6 +4897,10 @@ pub(crate) enum SyncAction {
|
||||
Rename(String),
|
||||
/// Set chat contacts by their addresses.
|
||||
SetContacts(Vec<String>),
|
||||
/// Set chat contacts by their fingerprints.
|
||||
///
|
||||
/// The list is a list of pairs of fingerprint and address.
|
||||
SetPgpContacts(Vec<(String, String)>),
|
||||
Delete,
|
||||
}
|
||||
|
||||
@@ -4939,6 +4931,30 @@ impl Context {
|
||||
.await?
|
||||
.id
|
||||
}
|
||||
SyncId::ContactFingerprint(fingerprint) => {
|
||||
let name = "";
|
||||
let addr = "";
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup_ex(self, name, addr, fingerprint, Origin::Hidden)
|
||||
.await?;
|
||||
match action {
|
||||
SyncAction::Rename(to) => {
|
||||
contact_id.set_name_ex(self, Nosync, to).await?;
|
||||
self.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
return Ok(());
|
||||
}
|
||||
SyncAction::Block => {
|
||||
return contact::set_blocked(self, Nosync, contact_id, true).await
|
||||
}
|
||||
SyncAction::Unblock => {
|
||||
return contact::set_blocked(self, Nosync, contact_id, false).await
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Request)
|
||||
.await?
|
||||
.id
|
||||
}
|
||||
SyncId::Grpid(grpid) => {
|
||||
if let SyncAction::CreateBroadcast(name) = action {
|
||||
create_broadcast_list_ex(self, Nosync, grpid.clone(), name.clone()).await?;
|
||||
@@ -4969,6 +4985,9 @@ impl Context {
|
||||
}
|
||||
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
|
||||
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
|
||||
SyncAction::SetPgpContacts(fingerprint_addrs) => {
|
||||
set_contacts_by_fingerprints(self, chat_id, fingerprint_addrs).await
|
||||
}
|
||||
SyncAction::Delete => chat_id.delete_ex(self, Nosync).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::imex::{has_backup, imex, ImexMode};
|
||||
use crate::message::{delete_msgs, MessengerMessage};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{sync, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
|
||||
use pretty_assertions::assert_eq;
|
||||
use strum::IntoEnumIterator;
|
||||
use tokio::fs;
|
||||
|
||||
@@ -19,31 +20,39 @@ async fn test_chat_info() {
|
||||
// Ensure we can serialize this.
|
||||
println!("{}", serde_json::to_string_pretty(&info).unwrap());
|
||||
|
||||
let expected = r#"
|
||||
{
|
||||
let expected = format!(
|
||||
r#"{{
|
||||
"id": 10,
|
||||
"type": 100,
|
||||
"name": "bob",
|
||||
"archived": false,
|
||||
"param": "",
|
||||
"gossiped_timestamp": 0,
|
||||
"is_sending_locations": false,
|
||||
"color": 35391,
|
||||
"profile_image": "",
|
||||
"profile_image": {},
|
||||
"draft": "",
|
||||
"is_muted": false,
|
||||
"ephemeral_timer": "Disabled"
|
||||
}
|
||||
"#;
|
||||
}}"#,
|
||||
// We need to do it like this so that the test passes on Windows:
|
||||
serde_json::to_string(
|
||||
t.get_blobdir()
|
||||
.join("4138c52e5bc1c576cda7dd44d088c07.png")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// Ensure we can deserialize this.
|
||||
let loaded: ChatInfo = serde_json::from_str(expected).unwrap();
|
||||
assert_eq!(info, loaded);
|
||||
serde_json::from_str::<ChatInfo>(&expected).unwrap();
|
||||
|
||||
assert_eq!(serde_json::to_string_pretty(&info).unwrap(), expected);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_draft_no_draft() {
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.get_self_chat().await;
|
||||
let draft = chat.id.get_draft(&t).await.unwrap();
|
||||
assert!(draft.is_none());
|
||||
@@ -60,14 +69,14 @@ async fn test_get_draft_special_chat_id() {
|
||||
async fn test_get_draft_no_chat() {
|
||||
// This is a weird case, maybe this should be an error but we
|
||||
// do not get this info from the database currently.
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let draft = ChatId::new(42).get_draft(&t).await.unwrap();
|
||||
assert!(draft.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_draft() {
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = &t.get_self_chat().await.id;
|
||||
let mut msg = Message::new_text("hello".to_string());
|
||||
|
||||
@@ -320,18 +329,20 @@ async fn test_member_add_remove() -> Result<()> {
|
||||
assert_eq!(alice_bob_contact.get_display_name(), "robert");
|
||||
}
|
||||
|
||||
// Create and promote a group.
|
||||
tcm.section("Create and promote a group.");
|
||||
let alice_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
alice
|
||||
let sent = alice
|
||||
.send_text(alice_chat_id, "Hi! I created a group.")
|
||||
.await;
|
||||
let fiona_chat_id = fiona.recv_msg(&sent).await.chat_id;
|
||||
|
||||
// Alice adds Bob to the chat.
|
||||
tcm.section("Alice adds Bob to the chat.");
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
fiona.recv_msg(&sent).await;
|
||||
|
||||
// Locally set name "robert" should not leak.
|
||||
assert!(!sent.payload.contains("robert"));
|
||||
@@ -339,8 +350,15 @@ async fn test_member_add_remove() -> Result<()> {
|
||||
sent.load_from_db().await.get_text(),
|
||||
"You added member robert."
|
||||
);
|
||||
let fiona_contact_ids = get_chat_contacts(&fiona, fiona_chat_id).await?;
|
||||
assert_eq!(fiona_contact_ids.len(), 3);
|
||||
for contact_id in fiona_contact_ids {
|
||||
let contact = Contact::get_by_id(&fiona, contact_id).await?;
|
||||
assert_ne!(contact.get_name(), "robert");
|
||||
assert!(contact.is_key_contact());
|
||||
}
|
||||
|
||||
// Alice removes Bob from the chat.
|
||||
tcm.section("Alice removes Bob from the chat.");
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(!sent.payload.contains("robert"));
|
||||
@@ -371,7 +389,7 @@ async fn test_parallel_member_remove() -> Result<()> {
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
let alice_charlie_contact_id = alice.add_or_lookup_contact_id(&charlie).await;
|
||||
|
||||
// Create and promote a group.
|
||||
tcm.section("Alice creates and promotes a group");
|
||||
let alice_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
@@ -384,31 +402,31 @@ async fn test_parallel_member_remove() -> Result<()> {
|
||||
let bob_chat_id = bob_received_msg.get_chat_id();
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
|
||||
// Alice adds Charlie to the chat.
|
||||
tcm.section("Alice adds Charlie to the chat");
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_charlie_contact_id).await?;
|
||||
let alice_sent_add_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob leaves the chat.
|
||||
tcm.section("Bob leaves the chat");
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
|
||||
// Bob receives a msg about Alice adding Claire to the group.
|
||||
tcm.section("Bob receives a message about Alice adding Charlie to the group");
|
||||
bob.recv_msg(&alice_sent_add_msg).await;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
|
||||
// Alice sends a message to Bob because the message about leaving is lost.
|
||||
tcm.section("Alice sends a message to Bob because the message about leaving is lost");
|
||||
let alice_sent_msg = alice.send_text(alice_chat_id, "What a silence!").await;
|
||||
bob.recv_msg(&alice_sent_msg).await;
|
||||
|
||||
bob.golden_test_chat(bob_chat_id, "chat_test_parallel_member_remove")
|
||||
.await;
|
||||
|
||||
// Alice removes Bob from the chat.
|
||||
tcm.section("Alice removes Bob from the chat");
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let alice_sent_remove_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob receives a msg about Alice removing him from the group.
|
||||
tcm.section("Bob receives a msg about Alice removing him from the group");
|
||||
let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await;
|
||||
|
||||
// Test that remove message is rewritten.
|
||||
@@ -621,6 +639,7 @@ async fn test_lost_member_added() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob])
|
||||
.await;
|
||||
@@ -629,8 +648,8 @@ async fn test_lost_member_added() -> Result<()> {
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
|
||||
|
||||
// Attempt to add member, but message is lost.
|
||||
let claire_id = Contact::create(alice, "", "claire@foo.de").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, claire_id).await?;
|
||||
let charlie_id = alice.add_or_lookup_contact_id(charlie).await;
|
||||
add_contact_to_chat(alice, alice_chat_id, charlie_id).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
|
||||
let alice_sent = alice.send_text(alice_chat_id, "Hi again!").await;
|
||||
@@ -693,12 +712,12 @@ async fn test_leave_group() -> Result<()> {
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Create group chat with Bob.
|
||||
tcm.section("Alice creates group chat with Bob.");
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let bob_contact = alice.add_or_lookup_contact(&bob).await.id;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?;
|
||||
|
||||
// Alice sends first message to group.
|
||||
tcm.section("Alice sends first message to group.");
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
let bob_msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
@@ -711,7 +730,7 @@ async fn test_leave_group() -> Result<()> {
|
||||
// Shift the time so that we can later check the 'Group left' message's timestamp:
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
|
||||
// Bob leaves the group.
|
||||
tcm.section("Bob leaves the group.");
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
@@ -897,7 +916,11 @@ async fn test_add_device_msg_labelled() -> Result<()> {
|
||||
assert!(chat.why_cant_send(&t).await? == Some(CantSendReason::DeviceChat));
|
||||
|
||||
assert_eq!(chat.name, stock_str::device_messages(&t).await);
|
||||
assert!(chat.get_profile_image(&t).await?.is_some());
|
||||
let device_msg_icon = chat.get_profile_image(&t).await?.unwrap();
|
||||
assert_eq!(
|
||||
device_msg_icon.metadata()?.len(),
|
||||
include_bytes!("../../assets/icon-device.png").len() as u64
|
||||
);
|
||||
|
||||
// delete device message, make sure it is not added again
|
||||
message::delete_msgs(&t, &[*msg1_id.as_ref().unwrap()]).await?;
|
||||
@@ -968,7 +991,7 @@ async fn test_delete_device_chat() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_device_chat_cannot_sent() {
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
t.update_device_chats().await.unwrap();
|
||||
let device_chat_id = ChatId::get_for_contact(&t, ContactId::DEVICE)
|
||||
.await
|
||||
@@ -981,6 +1004,18 @@ async fn test_device_chat_cannot_sent() {
|
||||
assert!(forward_msgs(&t, &[msg_id], device_chat_id).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_device_chat_is_encrypted() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.update_device_chats().await.unwrap();
|
||||
let device_chat_id = ChatId::get_for_contact(&t, ContactId::DEVICE)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let device_chat = Chat::load_from_db(&t, device_chat_id).await.unwrap();
|
||||
assert!(device_chat.is_encrypted(&t).await.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_and_reset_all_device_msgs() {
|
||||
let t = TestContext::new().await;
|
||||
@@ -1015,7 +1050,7 @@ async fn chatlist_len(ctx: &Context, listflags: usize) -> usize {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_archive() {
|
||||
// create two chats
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let mut msg = Message::new_text("foo".to_string());
|
||||
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
|
||||
let chat_id1 = message::Message::load_from_db(&t, msg_id)
|
||||
@@ -1313,7 +1348,7 @@ async fn get_chats_from_chat_list(ctx: &Context, listflags: usize) -> Vec<ChatId
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_pinned() {
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
// create 3 chats, wait 1 second in between to get a reliable order (we order by time)
|
||||
let mut msg = Message::new_text("foo".to_string());
|
||||
@@ -1481,25 +1516,22 @@ async fn test_create_same_chat_twice() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_shall_attach_selfavatar() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
assert!(!shall_attach_selfavatar(&t, chat_id).await?);
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let (contact_id, _) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"",
|
||||
&ContactAddress::new("foo@bar.org")?,
|
||||
Origin::IncomingUnknownTo,
|
||||
)
|
||||
.await?;
|
||||
add_contact_to_chat(&t, chat_id, contact_id).await?;
|
||||
assert!(shall_attach_selfavatar(&t, chat_id).await?);
|
||||
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
assert!(!shall_attach_selfavatar(alice, chat_id).await?);
|
||||
|
||||
chat_id.set_selfavatar_timestamp(&t, time()).await?;
|
||||
assert!(!shall_attach_selfavatar(&t, chat_id).await?);
|
||||
let contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
add_contact_to_chat(alice, chat_id, contact_id).await?;
|
||||
assert!(shall_attach_selfavatar(alice, chat_id).await?);
|
||||
|
||||
t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending
|
||||
assert!(shall_attach_selfavatar(&t, chat_id).await?);
|
||||
chat_id.set_selfavatar_timestamp(alice, time()).await?;
|
||||
assert!(!shall_attach_selfavatar(alice, chat_id).await?);
|
||||
|
||||
alice.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending
|
||||
assert!(shall_attach_selfavatar(alice, chat_id).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1509,15 +1541,10 @@ async fn test_shall_attach_selfavatar() -> Result<()> {
|
||||
async fn test_profile_data_on_group_leave() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
|
||||
let (contact_id, _) = Contact::add_or_lookup(
|
||||
t,
|
||||
"",
|
||||
&ContactAddress::new("foo@bar.org")?,
|
||||
Origin::IncomingUnknownTo,
|
||||
)
|
||||
.await?;
|
||||
let contact_id = t.add_or_lookup_contact_id(bob).await;
|
||||
add_contact_to_chat(t, chat_id, contact_id).await?;
|
||||
|
||||
send_text_msg(t, chat_id, "populate".to_string()).await?;
|
||||
@@ -1532,7 +1559,8 @@ async fn test_profile_data_on_group_leave() -> Result<()> {
|
||||
|
||||
remove_contact_from_chat(t, chat_id, ContactId::SELF).await?;
|
||||
let sent_msg = t.pop_sent_msg().await;
|
||||
assert!(sent_msg.payload().contains("Chat-User-Avatar"));
|
||||
let msg = bob.parse_msg(&sent_msg).await;
|
||||
assert!(msg.header_exists(HeaderDef::ChatUserAvatar));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2183,40 +2211,46 @@ async fn test_forward_group() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_only_minimal_data_are_forwarded() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let charlie = tcm.charlie().await;
|
||||
|
||||
// send a message from Alice to a group with Bob
|
||||
let alice = TestContext::new_alice().await;
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("secretname"))
|
||||
.await?;
|
||||
let bob_id = Contact::create(&alice, "bob", "bob@example.net").await?;
|
||||
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let group_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?;
|
||||
add_contact_to_chat(&alice, group_id, bob_id).await?;
|
||||
let mut msg = Message::new_text("bla foo".to_owned());
|
||||
let sent_msg = alice.send_msg(group_id, &mut msg).await;
|
||||
assert!(sent_msg.payload().contains("secretgrpname"));
|
||||
assert!(sent_msg.payload().contains("secretname"));
|
||||
assert!(sent_msg.payload().contains("alice"));
|
||||
let parsed_msg = alice.parse_msg(&sent_msg).await;
|
||||
let encrypted_payload = String::from_utf8(parsed_msg.decoded_data.clone()).unwrap();
|
||||
assert!(encrypted_payload.contains("secretgrpname"));
|
||||
assert!(encrypted_payload.contains("secretname"));
|
||||
assert!(encrypted_payload.contains("alice"));
|
||||
|
||||
// Bob forwards that message to Claire -
|
||||
// Claire should not get information about Alice for the original Group
|
||||
let bob = TestContext::new_bob().await;
|
||||
let orig_msg = bob.recv_msg(&sent_msg).await;
|
||||
let claire_id = Contact::create(&bob, "claire", "claire@foo").await?;
|
||||
let single_id = ChatId::create_for_contact(&bob, claire_id).await?;
|
||||
let charlie_id = bob.add_or_lookup_contact_id(&charlie).await;
|
||||
let single_id = ChatId::create_for_contact(&bob, charlie_id).await?;
|
||||
let group_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "group2").await?;
|
||||
add_contact_to_chat(&bob, group_id, claire_id).await?;
|
||||
add_contact_to_chat(&bob, group_id, charlie_id).await?;
|
||||
let broadcast_id = create_broadcast_list(&bob).await?;
|
||||
add_contact_to_chat(&bob, broadcast_id, claire_id).await?;
|
||||
add_contact_to_chat(&bob, broadcast_id, charlie_id).await?;
|
||||
for chat_id in &[single_id, group_id, broadcast_id] {
|
||||
forward_msgs(&bob, &[orig_msg.id], *chat_id).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
assert!(sent_msg
|
||||
.payload()
|
||||
.contains("---------- Forwarded message ----------"));
|
||||
assert!(!sent_msg.payload().contains("secretgrpname"));
|
||||
assert!(!sent_msg.payload().contains("secretname"));
|
||||
assert!(!sent_msg.payload().contains("alice"));
|
||||
let parsed_msg = bob.parse_msg(&sent_msg).await;
|
||||
let encrypted_payload = String::from_utf8(parsed_msg.decoded_data.clone()).unwrap();
|
||||
|
||||
assert!(encrypted_payload.contains("---------- Forwarded message ----------"));
|
||||
assert!(!encrypted_payload.contains("secretgrpname"));
|
||||
assert!(!encrypted_payload.contains("secretname"));
|
||||
assert!(!encrypted_payload.contains("alice"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -2500,43 +2534,6 @@ async fn test_resend_foreign_message_fails() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_resend_opportunistically_encryption() -> Result<()> {
|
||||
// Alice creates group with Bob and sends an initial message
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
Contact::create(&alice, "", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
|
||||
|
||||
// Bob now can send an encrypted message
|
||||
let bob = TestContext::new_bob().await;
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
assert!(!msg.get_showpadlock());
|
||||
|
||||
msg.chat_id.accept(&bob).await?;
|
||||
let sent2 = bob.send_text(msg.chat_id, "bob->alice").await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
// Bob adds Claire and resends his last message: this will drop encryption in opportunistic chats
|
||||
add_contact_to_chat(
|
||||
&bob,
|
||||
msg.chat_id,
|
||||
Contact::create(&bob, "", "claire@example.org").await?,
|
||||
)
|
||||
.await?;
|
||||
let _sent3 = bob.pop_sent_msg().await;
|
||||
resend_msgs(&bob, &[sent2.sender_msg_id]).await?;
|
||||
let _sent4 = bob.pop_sent_msg().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_resend_info_message_fails() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -2737,53 +2734,64 @@ async fn test_create_for_contact_with_blocked() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_chat_get_encryption_info() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let contact_bob = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let contact_fiona = Contact::create(&alice, "", "fiona@example.net").await?;
|
||||
let contact_bob = alice.add_or_lookup_contact_id(bob).await;
|
||||
let contact_fiona = alice.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
assert_eq!(chat_id.get_encryption_info(&alice).await?, "");
|
||||
|
||||
add_contact_to_chat(&alice, chat_id, contact_bob).await?;
|
||||
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(&alice).await?,
|
||||
"No encryption:\n\
|
||||
bob@example.net"
|
||||
chat_id.get_encryption_info(alice).await?,
|
||||
"End-to-end encryption available"
|
||||
);
|
||||
|
||||
add_contact_to_chat(&alice, chat_id, contact_fiona).await?;
|
||||
add_contact_to_chat(alice, chat_id, contact_bob).await?;
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(&alice).await?,
|
||||
"No encryption:\n\
|
||||
fiona@example.net\n\
|
||||
bob@example.net"
|
||||
);
|
||||
|
||||
let direct_chat = bob.create_chat(&alice).await;
|
||||
send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(&alice).await?,
|
||||
"No encryption:\n\
|
||||
fiona@example.net\n\
|
||||
chat_id.get_encryption_info(alice).await?,
|
||||
"End-to-end encryption available\n\
|
||||
\n\
|
||||
End-to-end encryption available:\n\
|
||||
bob@example.net"
|
||||
bob@example.net\n\
|
||||
CCCB 5AA9 F6E1 141C 9431\n\
|
||||
65F1 DB18 B18C BCF7 0487"
|
||||
);
|
||||
|
||||
send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
|
||||
add_contact_to_chat(alice, chat_id, contact_fiona).await?;
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(&alice).await?,
|
||||
"No encryption:\n\
|
||||
fiona@example.net\n\
|
||||
chat_id.get_encryption_info(alice).await?,
|
||||
"End-to-end encryption available\n\
|
||||
\n\
|
||||
End-to-end encryption available:\n\
|
||||
bob@example.net"
|
||||
fiona@example.net\n\
|
||||
C8BA 50BF 4AC1 2FAF 38D7\n\
|
||||
F657 DDFC 8E9F 3C79 9195\n\
|
||||
\n\
|
||||
bob@example.net\n\
|
||||
CCCB 5AA9 F6E1 141C 9431\n\
|
||||
65F1 DB18 B18C BCF7 0487"
|
||||
);
|
||||
|
||||
let email_chat = alice.create_email_chat(bob).await;
|
||||
assert_eq!(
|
||||
email_chat.id.get_encryption_info(alice).await?,
|
||||
"No encryption"
|
||||
);
|
||||
|
||||
alice.sql.execute("DELETE FROM public_keys", ()).await?;
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(alice).await?,
|
||||
"End-to-end encryption available\n\
|
||||
\n\
|
||||
fiona@example.net\n\
|
||||
(key missing)\n\
|
||||
C8BA 50BF 4AC1 2FAF 38D7\n\
|
||||
F657 DDFC 8E9F 3C79 9195\n\
|
||||
\n\
|
||||
bob@example.net\n\
|
||||
(key missing)\n\
|
||||
CCCB 5AA9 F6E1 141C 9431\n\
|
||||
65F1 DB18 B18C BCF7 0487"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -3039,40 +3047,41 @@ async fn test_blob_renaming() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_blocked() -> Result<()> {
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice0 = &tcm.alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let bob = TestContext::new_bob().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let ba_chat = bob.create_chat(alice0).await;
|
||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
|
||||
alice1.recv_msg(&sent_msg).await;
|
||||
let a0b_contact_id = alice0.add_or_lookup_contact_id(&bob).await;
|
||||
let a0b_contact_id = alice0.add_or_lookup_contact_id(bob).await;
|
||||
|
||||
assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Request);
|
||||
assert_eq!(alice1.get_chat(bob).await.blocked, Blocked::Request);
|
||||
a0b_chat_id.accept(alice0).await?;
|
||||
sync(alice0, alice1).await;
|
||||
assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Not);
|
||||
assert_eq!(alice1.get_chat(bob).await.blocked, Blocked::Not);
|
||||
a0b_chat_id.block(alice0).await?;
|
||||
sync(alice0, alice1).await;
|
||||
assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Yes);
|
||||
assert_eq!(alice1.get_chat(bob).await.blocked, Blocked::Yes);
|
||||
a0b_chat_id.unblock(alice0).await?;
|
||||
sync(alice0, alice1).await;
|
||||
assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Not);
|
||||
assert_eq!(alice1.get_chat(bob).await.blocked, Blocked::Not);
|
||||
|
||||
// Unblocking a 1:1 chat doesn't unblock the contact currently.
|
||||
Contact::unblock(alice0, a0b_contact_id).await?;
|
||||
|
||||
assert!(!alice1.add_or_lookup_contact(&bob).await.is_blocked());
|
||||
assert!(!alice1.add_or_lookup_contact(bob).await.is_blocked());
|
||||
Contact::block(alice0, a0b_contact_id).await?;
|
||||
sync(alice0, alice1).await;
|
||||
assert!(alice1.add_or_lookup_contact(&bob).await.is_blocked());
|
||||
assert!(alice1.add_or_lookup_contact(bob).await.is_blocked());
|
||||
Contact::unblock(alice0, a0b_contact_id).await?;
|
||||
sync(alice0, alice1).await;
|
||||
assert!(!alice1.add_or_lookup_contact(&bob).await.is_blocked());
|
||||
assert!(!alice1.add_or_lookup_contact(bob).await.is_blocked());
|
||||
|
||||
// Test accepting and blocking groups. This way we test:
|
||||
// - Group chats synchronisation.
|
||||
@@ -3132,7 +3141,7 @@ async fn test_sync_accept_before_first_msg() -> Result<()> {
|
||||
assert_eq!(alice1_contacts.len(), 1);
|
||||
let a1b_contact_id = alice1_contacts[0];
|
||||
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
|
||||
assert_eq!(a1b_contact.get_addr(), "bob@example.net");
|
||||
assert_eq!(a1b_contact.get_addr(), "");
|
||||
assert_eq!(a1b_contact.origin, Origin::CreateChat);
|
||||
let a1b_chat = alice1.get_chat(bob).await;
|
||||
assert_eq!(a1b_chat.blocked, Blocked::Not);
|
||||
@@ -3146,23 +3155,30 @@ async fn test_sync_accept_before_first_msg() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_block_before_first_msg() -> Result<()> {
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice0 = &tcm.alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let bob = TestContext::new_bob().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let ba_chat = bob.create_chat(alice0).await;
|
||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||
let rcvd_msg = alice0.recv_msg(&sent_msg).await;
|
||||
let a0b_chat_id = rcvd_msg.chat_id;
|
||||
let a0b_contact_id = rcvd_msg.from_id;
|
||||
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request);
|
||||
assert_eq!(
|
||||
Chat::load_from_db(alice0, a0b_chat_id).await?.blocked,
|
||||
Blocked::Request
|
||||
);
|
||||
a0b_chat_id.block(alice0).await?;
|
||||
let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?;
|
||||
assert_eq!(a0b_contact.origin, Origin::IncomingUnknownFrom);
|
||||
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Yes);
|
||||
assert_eq!(
|
||||
Chat::load_from_db(alice0, a0b_chat_id).await?.blocked,
|
||||
Blocked::Yes
|
||||
);
|
||||
|
||||
sync(alice0, alice1).await;
|
||||
let alice1_contacts = Contact::get_all(alice1, 0, None).await?;
|
||||
@@ -3172,9 +3188,14 @@ async fn test_sync_block_before_first_msg() -> Result<()> {
|
||||
let a1b_contact_id = rcvd_msg.from_id;
|
||||
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
|
||||
assert_eq!(a1b_contact.origin, Origin::IncomingUnknownFrom);
|
||||
let a1b_chat = alice1.get_chat(&bob).await;
|
||||
assert_eq!(a1b_chat.blocked, Blocked::Yes);
|
||||
assert_eq!(rcvd_msg.chat_id, a1b_chat.id);
|
||||
let ChatIdBlocked {
|
||||
id: a1b_chat_id,
|
||||
blocked: a1b_chat_blocked,
|
||||
} = ChatIdBlocked::lookup_by_contact(alice1, a1b_contact_id)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(a1b_chat_blocked, Blocked::Yes);
|
||||
assert_eq!(rcvd_msg.chat_id, a1b_chat_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3366,8 +3387,8 @@ async fn test_sync_broadcast() -> Result<()> {
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let bob = TestContext::new_bob().await;
|
||||
let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id;
|
||||
let bob = &tcm.bob().await;
|
||||
let a0b_contact_id = alice0.add_or_lookup_contact(bob).await.id;
|
||||
|
||||
let a0_broadcast_id = create_broadcast_list(alice0).await?;
|
||||
sync(alice0, alice1).await;
|
||||
@@ -3382,20 +3403,19 @@ async fn test_sync_broadcast() -> Result<()> {
|
||||
assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty());
|
||||
add_contact_to_chat(alice0, a0_broadcast_id, a0b_contact_id).await?;
|
||||
sync(alice0, alice1).await;
|
||||
let a1b_contact_id = Contact::lookup_id_by_addr(
|
||||
alice1,
|
||||
&bob.get_config(Config::Addr).await?.unwrap(),
|
||||
Origin::Hidden,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
// This also imports Bob's key from the vCard.
|
||||
// Otherwise it is possible that second device
|
||||
// does not have Bob's key as only the fingerprint
|
||||
// is transferred in the sync message.
|
||||
let a1b_contact_id = alice1.add_or_lookup_contact(bob).await.id;
|
||||
assert_eq!(
|
||||
get_chat_contacts(alice1, a1_broadcast_id).await?,
|
||||
vec![a1b_contact_id]
|
||||
);
|
||||
let sent_msg = alice1.send_text(a1_broadcast_id, "hi").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||
let chat = Chat::load_from_db(bob, msg.chat_id).await?;
|
||||
assert_eq!(chat.get_type(), Chattype::Mailinglist);
|
||||
let msg = alice0.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, a0_broadcast_id);
|
||||
@@ -3514,7 +3534,7 @@ async fn test_info_contact_id() -> Result<()> {
|
||||
expected_bob_id: ContactId,
|
||||
) -> Result<()> {
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let msg = Message::load_from_db(alice, sent_msg.sender_msg_id).await?;
|
||||
let msg = sent_msg.load_from_db().await;
|
||||
assert_eq!(msg.get_info_type(), expected_type);
|
||||
assert_eq!(
|
||||
msg.get_info_contact_id(alice).await?,
|
||||
@@ -3677,19 +3697,24 @@ async fn test_past_members() -> Result<()> {
|
||||
let fiona = &tcm.fiona().await;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
tcm.section("Alice creates a chat.");
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
alice
|
||||
.send_text(alice_chat_id, "Hi! I created a group.")
|
||||
.await;
|
||||
|
||||
tcm.section("Alice removes Fiona from the chat.");
|
||||
remove_contact_from_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
tcm.section("Alice adds Bob to the chat.");
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
|
||||
tcm.section("Bob receives a message.");
|
||||
let add_message = alice.pop_sent_msg().await;
|
||||
let bob_add_message = bob.recv_msg(&add_message).await;
|
||||
let bob_chat_id = bob_add_message.chat_id;
|
||||
@@ -4212,5 +4237,108 @@ async fn test_oneone_gossip() -> Result<()> {
|
||||
assert_eq!(rcvd_msg2.get_showpadlock(), true);
|
||||
assert_eq!(rcvd_msg2.text, "Hello from second device!");
|
||||
|
||||
tcm.section("Alice sends another message from the first devicer");
|
||||
let sent_msg3 = alice.send_text(alice_chat.id, "Hello again, Bob!").await;
|
||||
|
||||
// This message has no Autocrypt-Gossip header,
|
||||
// but should still be assigned to key-contact.
|
||||
tcm.section("Alice receives a copy of another message on second device");
|
||||
let rcvd_msg3 = alice2.recv_msg(&sent_msg3).await;
|
||||
assert_eq!(rcvd_msg3.get_showpadlock(), true);
|
||||
assert_eq!(rcvd_msg3.chat_id, rcvd_msg.chat_id);
|
||||
|
||||
// Check that there was no gossip.
|
||||
let parsed_msg3 = alice2.parse_msg(&sent_msg3).await;
|
||||
assert!(!parsed_msg3.header_exists(HeaderDef::AutocryptGossip));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that address-contacts cannot be added to encrypted group chats.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_address_contacts_in_group_chats() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let bob_key_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let charlie_address_contact_id = alice.add_or_lookup_address_contact_id(charlie).await;
|
||||
|
||||
// key-contact should be added successfully.
|
||||
add_contact_to_chat(alice, chat_id, bob_key_contact_id).await?;
|
||||
|
||||
// Adding address-contact should fail.
|
||||
let res = add_contact_to_chat(alice, chat_id, charlie_address_contact_id).await;
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that key-contacts cannot be added to ad hoc groups.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_key_contacts_in_adhoc_chats() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let chat_id = receive_imf(
|
||||
alice,
|
||||
b"Subject: Email thread\r\n\
|
||||
From: alice@example.org\r\n\
|
||||
To: Bob <bob@example.net>, Fiona <fiona@example.net>\r\n\
|
||||
Date: Mon, 2 Dec 2023 16:59:39 +0000\r\n\
|
||||
Message-ID: <alice-mail@example.org>\r\n\
|
||||
\r\n\
|
||||
Starting a new thread\r\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap()
|
||||
.chat_id;
|
||||
|
||||
let bob_address_contact_id = alice.add_or_lookup_address_contact_id(bob).await;
|
||||
let charlie_key_contact_id = alice.add_or_lookup_contact_id(charlie).await;
|
||||
|
||||
// Address-contact should be added successfully.
|
||||
add_contact_to_chat(alice, chat_id, bob_address_contact_id).await?;
|
||||
|
||||
// Adding key-contact should fail.
|
||||
let res = add_contact_to_chat(alice, chat_id, charlie_key_contact_id).await;
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that avatar cannot be set in ad hoc groups.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_avatar_in_adhoc_chats() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let chat_id = receive_imf(
|
||||
alice,
|
||||
b"Subject: Email thread\r\n\
|
||||
From: alice@example.org\r\n\
|
||||
To: Bob <bob@example.net>, Fiona <fiona@example.net>\r\n\
|
||||
Date: Mon, 2 Dec 2023 16:59:39 +0000\r\n\
|
||||
Message-ID: <alice-mail@example.org>\r\n\
|
||||
\r\n\
|
||||
Starting a new thread\r\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap()
|
||||
.chat_id;
|
||||
|
||||
// Test that setting avatar in ad hoc group is not possible.
|
||||
let file = alice.dir.path().join("avatar.png");
|
||||
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
let res = set_chat_profile_image(alice, chat_id, file.to_str().unwrap()).await;
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -572,7 +572,7 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sort_self_talk_up_on_forward() {
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
t.update_device_chats().await.unwrap();
|
||||
create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
@@ -605,7 +605,7 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_special_chat_names() {
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
t.update_device_chats().await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
|
||||
|
||||
@@ -210,11 +210,6 @@ pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60;
|
||||
/// in the group membership consistency algo to reject outdated membership changes.
|
||||
pub(crate) const TIMESTAMP_SENT_TOLERANCE: i64 = 60;
|
||||
|
||||
/// How long a 1:1 chat can't be used for sending while the SecureJoin is in progress. This should
|
||||
/// be 10-20 seconds so that we are reasonably sure that the app remains active and receiving also
|
||||
/// on mobile devices. See also [`crate::chat::CantSendReason::SecurejoinWait`].
|
||||
pub(crate) const SECUREJOIN_WAIT_TIMEOUT: u64 = 15;
|
||||
|
||||
// To make text edits clearer for Non-Delta-MUA or old Delta Chats, edited text will be prefixed by EDITED_PREFIX.
|
||||
// Newer Delta Chats will remove the prefix as needed.
|
||||
pub(crate) const EDITED_PREFIX: &str = "✏️";
|
||||
|
||||
487
src/contact.rs
487
src/contact.rs
@@ -1,6 +1,6 @@
|
||||
//! Contacts module
|
||||
|
||||
use std::cmp::{min, Reverse};
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::{BinaryHeap, HashSet};
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -11,8 +11,8 @@ use async_channel::{self as channel, Receiver, Sender};
|
||||
use base64::Engine as _;
|
||||
pub use deltachat_contact_tools::may_be_valid_addr;
|
||||
use deltachat_contact_tools::{
|
||||
self as contact_tools, addr_cmp, addr_normalize, sanitize_name, sanitize_name_and_addr,
|
||||
ContactAddress, VcardContact,
|
||||
self as contact_tools, addr_normalize, sanitize_name, sanitize_name_and_addr, ContactAddress,
|
||||
VcardContact,
|
||||
};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use rusqlite::OptionalExtension;
|
||||
@@ -20,7 +20,6 @@ use serde::{Deserialize, Serialize};
|
||||
use tokio::task;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::color::str_to_color;
|
||||
@@ -28,14 +27,16 @@ use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
|
||||
use crate::key::{
|
||||
load_self_public_key, self_fingerprint, self_fingerprint_opt, DcKey, Fingerprint,
|
||||
SignedPublicKey,
|
||||
};
|
||||
use crate::log::{info, warn, LogExt};
|
||||
use crate::message::MessageState;
|
||||
use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::sync::{self, Sync::*};
|
||||
use crate::tools::{duration_to_str, get_abs_path, smeared_time, time, SystemTime};
|
||||
use crate::tools::{duration_to_str, get_abs_path, time, SystemTime};
|
||||
use crate::{chat, chatlist_events, stock_str};
|
||||
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
@@ -102,7 +103,16 @@ impl ContactId {
|
||||
/// for this contact will switch to the
|
||||
/// contact's authorized name.
|
||||
pub async fn set_name(self, context: &Context, name: &str) -> Result<()> {
|
||||
let addr = context
|
||||
self.set_name_ex(context, Sync, name).await
|
||||
}
|
||||
|
||||
pub(crate) async fn set_name_ex(
|
||||
self,
|
||||
context: &Context,
|
||||
sync: sync::Sync,
|
||||
name: &str,
|
||||
) -> Result<()> {
|
||||
let row = context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let is_changed = transaction.execute(
|
||||
@@ -111,30 +121,45 @@ impl ContactId {
|
||||
)? > 0;
|
||||
if is_changed {
|
||||
update_chat_names(context, transaction, self)?;
|
||||
let addr = transaction.query_row(
|
||||
"SELECT addr FROM contacts WHERE id=?",
|
||||
let (addr, fingerprint) = transaction.query_row(
|
||||
"SELECT addr, fingerprint FROM contacts WHERE id=?",
|
||||
(self,),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
let fingerprint: String = row.get(1)?;
|
||||
Ok((addr, fingerprint))
|
||||
},
|
||||
)?;
|
||||
Ok(Some(addr))
|
||||
context.emit_event(EventType::ContactsChanged(Some(self)));
|
||||
Ok(Some((addr, fingerprint)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
if let Some(addr) = addr {
|
||||
if sync.into() {
|
||||
if let Some((addr, fingerprint)) = row {
|
||||
if fingerprint.is_empty() {
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactAddr(addr.to_string()),
|
||||
chat::SyncId::ContactAddr(addr),
|
||||
chat::SyncAction::Rename(name.to_string()),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
} else {
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactFingerprint(fingerprint),
|
||||
chat::SyncAction::Rename(name.to_string()),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -196,31 +221,6 @@ impl ContactId {
|
||||
.await?;
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
/// Resets encryption with the contact.
|
||||
///
|
||||
/// Effect is similar to receiving a message without Autocrypt header
|
||||
/// from the contact, but this action is triggered manually by the user.
|
||||
///
|
||||
/// For example, this will result in sending the next message
|
||||
/// to 1:1 chat unencrypted, but will not remove existing verified keys.
|
||||
pub async fn reset_encryption(self, context: &Context) -> Result<()> {
|
||||
let now = time();
|
||||
|
||||
let addr = self.addr(context).await?;
|
||||
if let Some(mut peerstate) = Peerstate::from_addr(context, &addr).await? {
|
||||
peerstate.degrade_encryption(now);
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
}
|
||||
|
||||
// Reset 1:1 chat protection.
|
||||
if let Some(chat_id) = ChatId::lookup_by_contact(context, self).await? {
|
||||
chat_id
|
||||
.set_protection(context, ProtectionStatus::Unprotected, now, Some(self))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContactId {
|
||||
@@ -267,14 +267,8 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<Str
|
||||
let mut vcard_contacts = Vec::with_capacity(contacts.len());
|
||||
for id in contacts {
|
||||
let c = Contact::get_by_id(context, *id).await?;
|
||||
let key = match *id {
|
||||
ContactId::SELF => Some(load_self_public_key(context).await?),
|
||||
_ => Peerstate::from_addr(context, &c.addr)
|
||||
.await?
|
||||
.and_then(|peerstate| peerstate.take_key(false)),
|
||||
};
|
||||
let key = key.map(|k| k.to_base64());
|
||||
let profile_image = match c.get_profile_image(context).await? {
|
||||
let key = c.public_key(context).await?.map(|k| k.to_base64());
|
||||
let profile_image = match c.get_profile_image_ex(context, false).await? {
|
||||
None => None,
|
||||
Some(path) => tokio::fs::read(path)
|
||||
.await
|
||||
@@ -330,15 +324,6 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
|
||||
// mustn't use `Origin::AddressBook` here because the vCard may be created not by us, also we
|
||||
// want `contact.authname` to be saved as the authname and not a locally given name.
|
||||
let origin = Origin::CreateChat;
|
||||
let (id, modified) =
|
||||
match Contact::add_or_lookup(context, &contact.authname, &addr, origin).await {
|
||||
Err(e) => return Err(e).context("Contact::add_or_lookup() failed"),
|
||||
Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF),
|
||||
Ok(val) => val,
|
||||
};
|
||||
if modified != Modifier::None {
|
||||
context.emit_event(EventType::ContactsChanged(Some(id)));
|
||||
}
|
||||
let key = contact.key.as_ref().and_then(|k| {
|
||||
SignedPublicKey::from_base64(k)
|
||||
.with_context(|| {
|
||||
@@ -350,50 +335,35 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
|
||||
.log_err(context)
|
||||
.ok()
|
||||
});
|
||||
|
||||
let fingerprint;
|
||||
if let Some(public_key) = key {
|
||||
let timestamp = contact
|
||||
.timestamp
|
||||
.as_ref()
|
||||
.map_or(0, |&t| min(t, smeared_time(context)));
|
||||
let aheader = Aheader {
|
||||
addr: contact.addr.clone(),
|
||||
public_key,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
};
|
||||
let peerstate = match Peerstate::from_addr(context, &aheader.addr).await {
|
||||
Err(e) => {
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Cannot create peerstate from {}: {e:#}.", contact.addr
|
||||
);
|
||||
return Ok(id);
|
||||
}
|
||||
Ok(p) => p,
|
||||
};
|
||||
let peerstate = if let Some(mut p) = peerstate {
|
||||
p.apply_gossip(&aheader, timestamp);
|
||||
p
|
||||
fingerprint = public_key.dc_fingerprint().hex();
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO public_keys (fingerprint, public_key)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (fingerprint)
|
||||
DO NOTHING",
|
||||
(&fingerprint, public_key.to_bytes()),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
Peerstate::from_gossip(&aheader, timestamp)
|
||||
};
|
||||
if let Err(e) = peerstate.save_to_db(&context.sql).await {
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not save peerstate for {}: {e:#}.", contact.addr
|
||||
);
|
||||
return Ok(id);
|
||||
fingerprint = String::new();
|
||||
}
|
||||
if let Err(e) = peerstate
|
||||
.handle_fingerprint_change(context, timestamp)
|
||||
|
||||
let (id, modified) =
|
||||
match Contact::add_or_lookup_ex(context, &contact.authname, &addr, &fingerprint, origin)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: handle_fingerprint_change() failed for {}: {e:#}.",
|
||||
contact.addr
|
||||
);
|
||||
return Ok(id);
|
||||
}
|
||||
Err(e) => return Err(e).context("Contact::add_or_lookup() failed"),
|
||||
Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF),
|
||||
Ok(val) => val,
|
||||
};
|
||||
if modified != Modifier::None {
|
||||
context.emit_event(EventType::ContactsChanged(Some(id)));
|
||||
}
|
||||
if modified != Modifier::Created {
|
||||
return Ok(id);
|
||||
@@ -465,6 +435,11 @@ pub struct Contact {
|
||||
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr` to access this field.
|
||||
addr: String,
|
||||
|
||||
/// OpenPGP key fingerprint.
|
||||
/// Non-empty iff the contact is a key-contact,
|
||||
/// identified by this fingerprint.
|
||||
fingerprint: Option<String>,
|
||||
|
||||
/// Blocked state. Use contact_is_blocked to access this field.
|
||||
pub blocked: bool,
|
||||
|
||||
@@ -614,7 +589,7 @@ impl Contact {
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
|
||||
c.authname, c.param, c.status, c.is_bot
|
||||
c.authname, c.param, c.status, c.is_bot, c.fingerprint
|
||||
FROM contacts c
|
||||
WHERE c.id=?;",
|
||||
(contact_id,),
|
||||
@@ -628,11 +603,14 @@ impl Contact {
|
||||
let param: String = row.get(6)?;
|
||||
let status: Option<String> = row.get(7)?;
|
||||
let is_bot: bool = row.get(8)?;
|
||||
let fingerprint: Option<String> =
|
||||
Some(row.get(9)?).filter(|s: &String| !s.is_empty());
|
||||
let contact = Self {
|
||||
id: contact_id,
|
||||
name,
|
||||
authname,
|
||||
addr,
|
||||
fingerprint,
|
||||
blocked: blocked.unwrap_or_default(),
|
||||
last_seen,
|
||||
origin,
|
||||
@@ -655,6 +633,9 @@ impl Contact {
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
if let Some(self_fp) = self_fingerprint_opt(context).await? {
|
||||
contact.fingerprint = Some(self_fp.to_string());
|
||||
}
|
||||
contact.status = context
|
||||
.get_config(Config::Selfstatus)
|
||||
.await?
|
||||
@@ -812,8 +793,9 @@ impl Contact {
|
||||
let id = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT id FROM contacts \
|
||||
WHERE addr=?1 COLLATE NOCASE \
|
||||
"SELECT id FROM contacts
|
||||
WHERE addr=?1 COLLATE NOCASE
|
||||
AND fingerprint='' -- Do not lookup key-contacts
|
||||
AND id>?2 AND origin>=?3 AND (? OR blocked=?)",
|
||||
(
|
||||
&addr_normalized,
|
||||
@@ -827,8 +809,19 @@ impl Contact {
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub(crate) async fn add_or_lookup(
|
||||
context: &Context,
|
||||
name: &str,
|
||||
addr: &ContactAddress,
|
||||
origin: Origin,
|
||||
) -> Result<(ContactId, Modifier)> {
|
||||
Self::add_or_lookup_ex(context, name, addr, "", origin).await
|
||||
}
|
||||
|
||||
/// Lookup a contact and create it if it does not exist yet.
|
||||
/// The contact is identified by the email-address, a name and an "origin" can be given.
|
||||
/// If `fingerprint` is non-empty, a key-contact with this fingerprint is added / looked up.
|
||||
/// Otherwise, an address-contact with `addr` is added / looked up.
|
||||
/// A name and an "origin" can be given.
|
||||
///
|
||||
/// The "origin" is where the address comes from -
|
||||
/// from-header, cc-header, addressbook, qr, manual-edit etc.
|
||||
@@ -852,21 +845,32 @@ impl Contact {
|
||||
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
|
||||
///
|
||||
/// Returns the contact_id and a `Modifier` value indicating if a modification occurred.
|
||||
pub(crate) async fn add_or_lookup(
|
||||
pub(crate) async fn add_or_lookup_ex(
|
||||
context: &Context,
|
||||
name: &str,
|
||||
addr: &ContactAddress,
|
||||
addr: &str,
|
||||
fingerprint: &str,
|
||||
mut origin: Origin,
|
||||
) -> Result<(ContactId, Modifier)> {
|
||||
let mut sth_modified = Modifier::None;
|
||||
|
||||
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
|
||||
ensure!(
|
||||
!addr.is_empty() || !fingerprint.is_empty(),
|
||||
"Can not add_or_lookup empty address"
|
||||
);
|
||||
ensure!(origin != Origin::Unknown, "Missing valid origin");
|
||||
|
||||
if context.is_self_addr(addr).await? {
|
||||
return Ok((ContactId::SELF, sth_modified));
|
||||
}
|
||||
|
||||
if !fingerprint.is_empty() {
|
||||
let fingerprint_self = self_fingerprint(context).await?;
|
||||
if fingerprint == fingerprint_self {
|
||||
return Ok((ContactId::SELF, sth_modified));
|
||||
}
|
||||
}
|
||||
|
||||
let mut name = sanitize_name(name);
|
||||
if origin <= Origin::OutgoingTo {
|
||||
// The user may accidentally have written to a "noreply" address with another MUA:
|
||||
@@ -902,8 +906,10 @@ impl Contact {
|
||||
let row = transaction
|
||||
.query_row(
|
||||
"SELECT id, name, addr, origin, authname
|
||||
FROM contacts WHERE addr=? COLLATE NOCASE",
|
||||
(addr,),
|
||||
FROM contacts
|
||||
WHERE fingerprint=?1 AND
|
||||
(?1<>'' OR addr=?2 COLLATE NOCASE)",
|
||||
(fingerprint, addr),
|
||||
|row| {
|
||||
let row_id: u32 = row.get(0)?;
|
||||
let row_name: String = row.get(1)?;
|
||||
@@ -927,7 +933,7 @@ impl Contact {
|
||||
|| row_authname.is_empty());
|
||||
|
||||
row_id = id;
|
||||
if origin >= row_origin && addr.as_ref() != row_addr {
|
||||
if origin >= row_origin && addr != row_addr {
|
||||
update_addr = true;
|
||||
}
|
||||
if update_name || update_authname || update_addr || origin > row_origin {
|
||||
@@ -971,11 +977,12 @@ impl Contact {
|
||||
let update_authname = !manual;
|
||||
|
||||
transaction.execute(
|
||||
"INSERT INTO contacts (name, addr, origin, authname)
|
||||
VALUES (?, ?, ?, ?);",
|
||||
"INSERT INTO contacts (name, addr, fingerprint, origin, authname)
|
||||
VALUES (?, ?, ?, ?, ?);",
|
||||
(
|
||||
if update_name { &name } else { "" },
|
||||
&addr,
|
||||
fingerprint,
|
||||
origin,
|
||||
if update_authname { &name } else { "" },
|
||||
),
|
||||
@@ -983,7 +990,14 @@ impl Contact {
|
||||
|
||||
sth_modified = Modifier::Created;
|
||||
row_id = u32::try_from(transaction.last_insert_rowid())?;
|
||||
if fingerprint.is_empty() {
|
||||
info!(context, "Added contact id={row_id} addr={addr}.");
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Added contact id={row_id} fpr={fingerprint} addr={addr}."
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(row_id)
|
||||
})
|
||||
@@ -1076,8 +1090,8 @@ impl Contact {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.id, c.addr FROM contacts c
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
|
||||
WHERE c.id>?
|
||||
AND c.fingerprint!='' \
|
||||
AND c.origin>=? \
|
||||
AND c.blocked=0 \
|
||||
AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \
|
||||
@@ -1133,6 +1147,7 @@ impl Contact {
|
||||
.query_map(
|
||||
"SELECT id, addr FROM contacts
|
||||
WHERE id>?
|
||||
AND fingerprint!=''
|
||||
AND origin>=?
|
||||
AND blocked=0
|
||||
ORDER BY last_seen DESC, id DESC;",
|
||||
@@ -1253,17 +1268,16 @@ impl Contact {
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
|
||||
|
||||
let Some(peerstate) = peerstate.filter(|peerstate| peerstate.peek_key(false).is_some())
|
||||
else {
|
||||
let Some(fingerprint_other) = contact.fingerprint() else {
|
||||
return Ok(stock_str::encr_none(context).await);
|
||||
};
|
||||
let fingerprint_other = fingerprint_other.to_string();
|
||||
|
||||
let stock_message = match peerstate.prefer_encrypt {
|
||||
EncryptPreference::Mutual => stock_str::e2e_preferred(context).await,
|
||||
EncryptPreference::NoPreference => stock_str::e2e_available(context).await,
|
||||
EncryptPreference::Reset => stock_str::encr_none(context).await,
|
||||
let stock_message = if contact.public_key(context).await?.is_some() {
|
||||
stock_str::e2e_available(context).await
|
||||
} else {
|
||||
stock_str::encr_none(context).await
|
||||
};
|
||||
|
||||
let finger_prints = stock_str::finger_prints(context).await;
|
||||
@@ -1273,43 +1287,31 @@ impl Contact {
|
||||
.await?
|
||||
.dc_fingerprint()
|
||||
.to_string();
|
||||
let fingerprint_other_verified = peerstate
|
||||
.peek_key(true)
|
||||
.map(|k| k.dc_fingerprint().to_string())
|
||||
.unwrap_or_default();
|
||||
let fingerprint_other_unverified = peerstate
|
||||
.peek_key(false)
|
||||
.map(|k| k.dc_fingerprint().to_string())
|
||||
.unwrap_or_default();
|
||||
if addr < peerstate.addr {
|
||||
if addr < contact.addr {
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&stock_str::self_msg(context).await,
|
||||
&addr,
|
||||
&fingerprint_self,
|
||||
"",
|
||||
);
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
contact.get_display_name(),
|
||||
&peerstate.addr,
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
&contact.addr,
|
||||
&fingerprint_other,
|
||||
);
|
||||
} else {
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
contact.get_display_name(),
|
||||
&peerstate.addr,
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
&contact.addr,
|
||||
&fingerprint_other,
|
||||
);
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&stock_str::self_msg(context).await,
|
||||
&addr,
|
||||
&fingerprint_self,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1382,6 +1384,59 @@ impl Contact {
|
||||
&self.addr
|
||||
}
|
||||
|
||||
/// Returns true if the contact is a key-contact.
|
||||
/// Otherwise it is an addresss-contact.
|
||||
pub fn is_key_contact(&self) -> bool {
|
||||
self.fingerprint.is_some()
|
||||
}
|
||||
|
||||
/// Returns OpenPGP fingerprint of a contact.
|
||||
///
|
||||
/// `None` for address-contacts.
|
||||
pub fn fingerprint(&self) -> Option<Fingerprint> {
|
||||
if let Some(fingerprint) = &self.fingerprint {
|
||||
fingerprint.parse().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns OpenPGP public key of a contact.
|
||||
///
|
||||
/// Returns `None` if the contact is not a key-contact
|
||||
/// or if the key is not available.
|
||||
/// It is possible for a key-contact to not have a key,
|
||||
/// e.g. if only the fingerprint is known from a QR-code.
|
||||
pub async fn public_key(&self, context: &Context) -> Result<Option<SignedPublicKey>> {
|
||||
if self.id == ContactId::SELF {
|
||||
return Ok(Some(load_self_public_key(context).await?));
|
||||
}
|
||||
|
||||
if let Some(fingerprint) = &self.fingerprint {
|
||||
if let Some(public_key_bytes) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT public_key
|
||||
FROM public_keys
|
||||
WHERE fingerprint=?",
|
||||
(fingerprint,),
|
||||
|row| {
|
||||
let bytes: Vec<u8> = row.get(0)?;
|
||||
Ok(bytes)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
|
||||
Ok(Some(public_key))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get name authorized by the contact.
|
||||
pub fn get_authname(&self) -> &str {
|
||||
&self.authname
|
||||
@@ -1447,11 +1502,28 @@ impl Contact {
|
||||
/// This is the image set by each remote user on their own
|
||||
/// using set_config(context, "selfavatar", image).
|
||||
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
|
||||
self.get_profile_image_ex(context, true).await
|
||||
}
|
||||
|
||||
/// Get the contact's profile image.
|
||||
/// This is the image set by each remote user on their own
|
||||
/// using set_config(context, "selfavatar", image).
|
||||
async fn get_profile_image_ex(
|
||||
&self,
|
||||
context: &Context,
|
||||
show_fallback_icon: bool,
|
||||
) -> Result<Option<PathBuf>> {
|
||||
if self.id == ContactId::SELF {
|
||||
if let Some(p) = context.get_config(Config::Selfavatar).await? {
|
||||
return Ok(Some(PathBuf::from(p))); // get_config() calls get_abs_path() internally already
|
||||
}
|
||||
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
} else if self.id == ContactId::DEVICE {
|
||||
return Ok(Some(chat::get_device_icon(context).await?));
|
||||
}
|
||||
if show_fallback_icon && !self.id.is_special() && !self.is_key_contact() {
|
||||
return Ok(Some(chat::get_address_contact_icon(context).await?));
|
||||
}
|
||||
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if !image_rel.is_empty() {
|
||||
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
|
||||
}
|
||||
@@ -1477,25 +1549,21 @@ impl Contact {
|
||||
/// Returns whether end-to-end encryption to the contact is available.
|
||||
pub async fn e2ee_avail(&self, context: &Context) -> Result<bool> {
|
||||
if self.id == ContactId::SELF {
|
||||
// We don't need to check if we have our own key.
|
||||
return Ok(true);
|
||||
}
|
||||
let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
Ok(peerstate.peek_key(false).is_some())
|
||||
Ok(self.public_key(context).await?.is_some())
|
||||
}
|
||||
|
||||
/// Returns true if the contact
|
||||
/// can be added to verified chats,
|
||||
/// i.e. has a verified key
|
||||
/// and Autocrypt key matches the verified key.
|
||||
/// can be added to verified chats.
|
||||
///
|
||||
/// If contact is verified
|
||||
/// UI should display green checkmark after the contact name
|
||||
/// in contact list items and
|
||||
/// in chat member list items.
|
||||
///
|
||||
/// In contact profile view, us this function only if there is no chat with the contact,
|
||||
/// In contact profile view, use this function only if there is no chat with the contact,
|
||||
/// otherwise use is_chat_protected().
|
||||
/// Use [Self::get_verifier_id] to display the verifier contact
|
||||
/// in the info section of the contact profile.
|
||||
@@ -1506,64 +1574,31 @@ impl Contact {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let forward_verified = peerstate.is_using_verified_key();
|
||||
let backward_verified = peerstate.is_backward_verified(context).await?;
|
||||
Ok(forward_verified && backward_verified)
|
||||
}
|
||||
|
||||
/// Returns true if we have a verified key for the contact
|
||||
/// and it is the same as Autocrypt key.
|
||||
/// This is enough to send messages to the contact in verified chat
|
||||
/// and verify received messages, but not enough to display green checkmark
|
||||
/// or add the contact to verified groups.
|
||||
pub async fn is_forward_verified(&self, context: &Context) -> Result<bool> {
|
||||
if self.id == ContactId::SELF {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
Ok(peerstate.is_using_verified_key())
|
||||
Ok(self.get_verifier_id(context).await?.is_some())
|
||||
}
|
||||
|
||||
/// Returns the `ContactId` that verified the contact.
|
||||
///
|
||||
/// If the function returns non-zero result,
|
||||
/// If this returns Some(_),
|
||||
/// display green checkmark in the profile and "Introduced by ..." line
|
||||
/// with the name and address of the contact
|
||||
/// formatted by [Self::get_name_n_addr].
|
||||
///
|
||||
/// If this function returns a verifier,
|
||||
/// this does not necessarily mean
|
||||
/// you can add the contact to verified chats.
|
||||
/// Use [Self::is_verified] to check
|
||||
/// if a contact can be added to a verified chat instead.
|
||||
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
|
||||
let Some(verifier_addr) = Peerstate::from_addr(context, self.get_addr())
|
||||
/// If this returns `Some(None)`, then the contact is verified,
|
||||
/// but it's unclear by whom.
|
||||
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<Option<ContactId>>> {
|
||||
let verifier_id: u32 = context
|
||||
.sql
|
||||
.query_get_value("SELECT verifier FROM contacts WHERE id=?", (self.id,))
|
||||
.await?
|
||||
.and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned()))
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
.with_context(|| format!("Contact {} does not exist", self.id))?;
|
||||
|
||||
if addr_cmp(&verifier_addr, &self.addr) {
|
||||
// Contact is directly verified via QR code.
|
||||
return Ok(Some(ContactId::SELF));
|
||||
}
|
||||
|
||||
match Contact::lookup_id_by_addr(context, &verifier_addr, Origin::Unknown).await? {
|
||||
Some(contact_id) => Ok(Some(contact_id)),
|
||||
None => {
|
||||
let addr = &self.addr;
|
||||
warn!(context, "Could not lookup contact with address {verifier_addr} which introduced {addr}.");
|
||||
if verifier_id == 0 {
|
||||
Ok(None)
|
||||
}
|
||||
} else if verifier_id == self.id.to_u32() {
|
||||
Ok(Some(None))
|
||||
} else {
|
||||
Ok(Some(Some(ContactId::new(verifier_id))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1735,11 +1770,13 @@ WHERE type=? AND id IN (
|
||||
true => chat::SyncAction::Block,
|
||||
false => chat::SyncAction::Unblock,
|
||||
};
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactAddr(contact.addr.clone()),
|
||||
action,
|
||||
)
|
||||
let sync_id = if let Some(fingerprint) = contact.fingerprint() {
|
||||
chat::SyncId::ContactFingerprint(fingerprint.hex())
|
||||
} else {
|
||||
chat::SyncId::ContactAddr(contact.addr.clone())
|
||||
};
|
||||
|
||||
chat::sync(context, sync_id, action)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
@@ -1862,29 +1899,51 @@ pub(crate) async fn update_last_seen(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cat_fingerprint(
|
||||
ret: &mut String,
|
||||
name: &str,
|
||||
addr: &str,
|
||||
fingerprint_verified: &str,
|
||||
fingerprint_unverified: &str,
|
||||
) {
|
||||
*ret += &format!(
|
||||
"\n\n{} ({}):\n{}",
|
||||
name,
|
||||
addr,
|
||||
if !fingerprint_verified.is_empty() {
|
||||
fingerprint_verified
|
||||
} else {
|
||||
fingerprint_unverified
|
||||
},
|
||||
/// Marks contact `contact_id` as verified by `verifier_id`.
|
||||
pub(crate) async fn mark_contact_id_as_verified(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
verifier_id: ContactId,
|
||||
) -> Result<()> {
|
||||
debug_assert_ne!(
|
||||
contact_id, verifier_id,
|
||||
"Contact cannot be verified by self"
|
||||
);
|
||||
if !fingerprint_verified.is_empty()
|
||||
&& !fingerprint_unverified.is_empty()
|
||||
&& fingerprint_verified != fingerprint_unverified
|
||||
{
|
||||
*ret += &format!("\n\n{name} (alternative):\n{fingerprint_unverified}");
|
||||
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: String = transaction.query_row(
|
||||
"SELECT fingerprint FROM contacts WHERE id=?",
|
||||
(verifier_id,),
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
if verifier_fingerprint.is_empty() {
|
||||
bail!(
|
||||
"Contact {contact_id} cannot be verified by non-key-contact {verifier_id}"
|
||||
);
|
||||
}
|
||||
}
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET verifier=? WHERE id=?",
|
||||
(verifier_id, contact_id),
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) {
|
||||
*ret += &format!("\n\n{name} ({addr}):\n{fingerprint}");
|
||||
}
|
||||
|
||||
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use deltachat_contact_tools::may_be_valid_addr;
|
||||
use deltachat_contact_tools::{addr_cmp, may_be_valid_addr};
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
|
||||
@@ -56,58 +56,49 @@ fn test_split_address_book() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_contacts() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let context = tcm.bob().await;
|
||||
let alice = tcm.alice().await;
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("MyName"))
|
||||
.await?;
|
||||
|
||||
assert!(context.get_all_self_addrs().await?.is_empty());
|
||||
|
||||
// Bob is not in the contacts yet.
|
||||
let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?;
|
||||
// Alice is not in the contacts yet.
|
||||
let contacts = Contact::get_all(&context.ctx, 0, Some("Alice")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
let contacts = Contact::get_all(&context.ctx, 0, Some("MyName")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
let (id, _modified) = Contact::add_or_lookup(
|
||||
&context.ctx,
|
||||
"bob",
|
||||
&ContactAddress::new("user@example.org")?,
|
||||
Origin::IncomingReplyTo,
|
||||
)
|
||||
.await?;
|
||||
let id = context.add_or_lookup_contact_id(&alice).await;
|
||||
assert_ne!(id, ContactId::UNDEFINED);
|
||||
|
||||
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&context, id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_authname(), "bob");
|
||||
assert_eq!(contact.get_display_name(), "bob");
|
||||
assert_eq!(contact.get_authname(), "MyName");
|
||||
assert_eq!(contact.get_display_name(), "MyName");
|
||||
|
||||
// Search by name.
|
||||
let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?;
|
||||
let contacts = Contact::get_all(&context, 0, Some("myname")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.first(), Some(&id));
|
||||
|
||||
// Search by address.
|
||||
let contacts = Contact::get_all(&context.ctx, 0, Some("user")).await?;
|
||||
let contacts = Contact::get_all(&context, 0, Some("alice@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.first(), Some(&id));
|
||||
|
||||
let contacts = Contact::get_all(&context.ctx, 0, Some("alice")).await?;
|
||||
let contacts = Contact::get_all(&context, 0, Some("Foobar")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
// Set Bob name to "someone" manually.
|
||||
let (contact_bob_id, modified) = Contact::add_or_lookup(
|
||||
&context.ctx,
|
||||
"someone",
|
||||
&ContactAddress::new("user@example.org")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(contact_bob_id, id);
|
||||
assert_eq!(modified, Modifier::Modified);
|
||||
// Set Alice name to "someone" manually.
|
||||
id.set_name(&context, "someone").await?;
|
||||
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "someone");
|
||||
assert_eq!(contact.get_authname(), "bob");
|
||||
assert_eq!(contact.get_authname(), "MyName");
|
||||
assert_eq!(contact.get_display_name(), "someone");
|
||||
|
||||
// Not searchable by authname, because it is not displayed.
|
||||
let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?;
|
||||
let contacts = Contact::get_all(&context, 0, Some("MyName")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
// Search by display name (same as manually set name).
|
||||
@@ -133,7 +124,7 @@ async fn test_is_self_addr() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_or_lookup() {
|
||||
// add some contacts, this also tests add_address_book()
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let book = concat!(
|
||||
" Name one \n one@eins.org \n",
|
||||
"Name two\ntwo@deux.net\n",
|
||||
@@ -247,7 +238,7 @@ async fn test_add_or_lookup() {
|
||||
// check SELF
|
||||
let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap();
|
||||
assert_eq!(contact.get_name(), stock_str::self_msg(&t).await);
|
||||
assert_eq!(contact.get_addr(), ""); // we're not configured
|
||||
assert_eq!(contact.get_addr(), "alice@example.org");
|
||||
assert!(!contact.is_blocked());
|
||||
}
|
||||
|
||||
@@ -282,7 +273,7 @@ async fn test_contact_name_changes() -> Result<()> {
|
||||
assert_eq!(contact.get_display_name(), "f@example.org");
|
||||
assert_eq!(contact.get_name_n_addr(), "f@example.org");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
// second message inits the name
|
||||
receive_imf(
|
||||
@@ -308,9 +299,9 @@ async fn test_contact_name_changes() -> Result<()> {
|
||||
assert_eq!(contact.get_display_name(), "Flobbyfoo");
|
||||
assert_eq!(contact.get_name_n_addr(), "Flobbyfoo (f@example.org)");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.len(), 0);
|
||||
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
// third message changes the name
|
||||
receive_imf(
|
||||
@@ -338,11 +329,11 @@ async fn test_contact_name_changes() -> Result<()> {
|
||||
assert_eq!(contact.get_display_name(), "Foo Flobby");
|
||||
assert_eq!(contact.get_name_n_addr(), "Foo Flobby (f@example.org)");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.len(), 0);
|
||||
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
let contacts = Contact::get_all(&t, 0, Some("Foo Flobby")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
// change name manually
|
||||
let test_id = Contact::create(&t, "Falk", "f@example.org").await?;
|
||||
@@ -356,9 +347,9 @@ async fn test_contact_name_changes() -> Result<()> {
|
||||
assert_eq!(contact.get_display_name(), "Falk");
|
||||
assert_eq!(contact.get_name_n_addr(), "Falk (f@example.org)");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.len(), 0);
|
||||
let contacts = Contact::get_all(&t, 0, Some("falk")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -366,20 +357,13 @@ async fn test_contact_name_changes() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
assert!(Contact::delete(&alice, ContactId::SELF).await.is_err());
|
||||
|
||||
// Create Bob contact
|
||||
let (contact_id, _) = Contact::add_or_lookup(
|
||||
&alice,
|
||||
"Bob",
|
||||
&ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.net")
|
||||
.await;
|
||||
let contact_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
assert_eq!(
|
||||
Contact::get_all(&alice, 0, Some("bob@example.net"))
|
||||
.await?
|
||||
@@ -416,30 +400,57 @@ async fn test_delete() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_and_recreate_contact() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// test recreation after physical deletion
|
||||
let contact_id1 = Contact::create(&t, "Foo", "foo@bar.de").await?;
|
||||
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
|
||||
let contact_id1 = t.add_or_lookup_contact_id(&bob).await;
|
||||
assert_eq!(
|
||||
Contact::get_all(&t, 0, Some("bob@example.net"))
|
||||
.await?
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
Contact::delete(&t, contact_id1).await?;
|
||||
assert!(Contact::get_by_id(&t, contact_id1).await.is_err());
|
||||
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
|
||||
let contact_id2 = Contact::create(&t, "Foo", "foo@bar.de").await?;
|
||||
assert_eq!(
|
||||
Contact::get_all(&t, 0, Some("bob@example.net"))
|
||||
.await?
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
let contact_id2 = t.add_or_lookup_contact_id(&bob).await;
|
||||
assert_ne!(contact_id2, contact_id1);
|
||||
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Contact::get_all(&t, 0, Some("bob@example.net"))
|
||||
.await?
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
||||
// test recreation after hiding
|
||||
t.create_chat_with_contact("Foo", "foo@bar.de").await;
|
||||
t.create_chat(&bob).await;
|
||||
Contact::delete(&t, contact_id2).await?;
|
||||
let contact = Contact::get_by_id(&t, contact_id2).await?;
|
||||
assert_eq!(contact.origin, Origin::Hidden);
|
||||
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
|
||||
assert_eq!(
|
||||
Contact::get_all(&t, 0, Some("bob@example.net"))
|
||||
.await?
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
|
||||
let contact_id3 = Contact::create(&t, "Foo", "foo@bar.de").await?;
|
||||
let contact_id3 = t.add_or_lookup_contact_id(&bob).await;
|
||||
let contact = Contact::get_by_id(&t, contact_id3).await?;
|
||||
assert_eq!(contact.origin, Origin::ManuallyCreated);
|
||||
assert_eq!(contact.origin, Origin::CreateChat);
|
||||
assert_eq!(contact_id3, contact_id2);
|
||||
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Contact::get_all(&t, 0, Some("bob@example.net"))
|
||||
.await?
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -728,51 +739,59 @@ async fn test_contact_get_color() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_contact_get_encrinfo() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// Return error for special IDs
|
||||
let encrinfo = Contact::get_encrinfo(&alice, ContactId::SELF).await;
|
||||
let encrinfo = Contact::get_encrinfo(alice, ContactId::SELF).await;
|
||||
assert!(encrinfo.is_err());
|
||||
let encrinfo = Contact::get_encrinfo(&alice, ContactId::DEVICE).await;
|
||||
let encrinfo = Contact::get_encrinfo(alice, ContactId::DEVICE).await;
|
||||
assert!(encrinfo.is_err());
|
||||
|
||||
let (contact_bob_id, _modified) = Contact::add_or_lookup(
|
||||
&alice,
|
||||
"Bob",
|
||||
&ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?;
|
||||
let address_contact_bob_id = alice.add_or_lookup_address_contact_id(bob).await;
|
||||
let encrinfo = Contact::get_encrinfo(alice, address_contact_bob_id).await?;
|
||||
assert_eq!(encrinfo, "No encryption");
|
||||
let contact = Contact::get_by_id(&alice, contact_bob_id).await?;
|
||||
assert!(!contact.e2ee_avail(&alice).await?);
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_alice = bob
|
||||
.create_chat_with_contact("Alice", "alice@example.org")
|
||||
.await;
|
||||
send_text_msg(&bob, chat_alice.id, "Hello".to_string()).await?;
|
||||
let msg = bob.pop_sent_msg().await;
|
||||
alice.recv_msg(&msg).await;
|
||||
let contact = Contact::get_by_id(alice, address_contact_bob_id).await?;
|
||||
assert!(!contact.e2ee_avail(alice).await?);
|
||||
|
||||
let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?;
|
||||
let contact_bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let encrinfo = Contact::get_encrinfo(alice, contact_bob_id).await?;
|
||||
assert_eq!(
|
||||
encrinfo,
|
||||
"End-to-end encryption preferred.
|
||||
"End-to-end encryption available.
|
||||
Fingerprints:
|
||||
|
||||
Me (alice@example.org):
|
||||
2E6F A2CB 23B5 32D7 2863
|
||||
4B58 64B0 8F61 A9ED 9443
|
||||
|
||||
Bob (bob@example.net):
|
||||
bob@example.net (bob@example.net):
|
||||
CCCB 5AA9 F6E1 141C 9431
|
||||
65F1 DB18 B18C BCF7 0487"
|
||||
);
|
||||
let contact = Contact::get_by_id(&alice, contact_bob_id).await?;
|
||||
assert!(contact.e2ee_avail(&alice).await?);
|
||||
let contact = Contact::get_by_id(alice, contact_bob_id).await?;
|
||||
assert!(contact.e2ee_avail(alice).await?);
|
||||
|
||||
alice.sql.execute("DELETE FROM public_keys", ()).await?;
|
||||
let encrinfo = Contact::get_encrinfo(alice, contact_bob_id).await?;
|
||||
assert_eq!(
|
||||
encrinfo,
|
||||
"No encryption.
|
||||
Fingerprints:
|
||||
|
||||
Me (alice@example.org):
|
||||
2E6F A2CB 23B5 32D7 2863
|
||||
4B58 64B0 8F61 A9ED 9443
|
||||
|
||||
bob@example.net (bob@example.net):
|
||||
CCCB 5AA9 F6E1 141C 9431
|
||||
65F1 DB18 B18C BCF7 0487"
|
||||
);
|
||||
let contact = Contact::get_by_id(alice, contact_bob_id).await?;
|
||||
assert!(!contact.e2ee_avail(alice).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -780,24 +799,24 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
/// synchronized when the message is not encrypted.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_synchronize_status() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
// Alice has two devices.
|
||||
let alice1 = TestContext::new_alice().await;
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
|
||||
// Bob has one device.
|
||||
let bob = TestContext::new_bob().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let default_status = alice1.get_config(Config::Selfstatus).await?;
|
||||
|
||||
alice1
|
||||
.set_config(Config::Selfstatus, Some("New status"))
|
||||
.await?;
|
||||
let chat = alice1
|
||||
.create_chat_with_contact("Bob", "bob@example.net")
|
||||
.await;
|
||||
let chat = alice1.create_email_chat(bob).await;
|
||||
|
||||
// Alice sends a message to Bob from the first device.
|
||||
send_text_msg(&alice1, chat.id, "Hello".to_string()).await?;
|
||||
send_text_msg(alice1, chat.id, "Hello".to_string()).await?;
|
||||
let sent_msg = alice1.pop_sent_msg().await;
|
||||
|
||||
// Message is not encrypted.
|
||||
@@ -813,18 +832,9 @@ async fn test_synchronize_status() -> Result<()> {
|
||||
// Message was not encrypted, so status is not copied.
|
||||
assert_eq!(alice2.get_config(Config::Selfstatus).await?, default_status);
|
||||
|
||||
// Bob replies.
|
||||
let chat = bob
|
||||
.create_chat_with_contact("Alice", "alice@example.org")
|
||||
.await;
|
||||
|
||||
send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
alice1.recv_msg(&sent_msg).await;
|
||||
alice2.recv_msg(&sent_msg).await;
|
||||
|
||||
// Alice sends second message.
|
||||
send_text_msg(&alice1, chat.id, "Hello".to_string()).await?;
|
||||
// Alice sends encrypted message.
|
||||
let chat = alice1.create_chat(bob).await;
|
||||
send_text_msg(alice1, chat.id, "Hello".to_string()).await?;
|
||||
let sent_msg = alice1.pop_sent_msg().await;
|
||||
|
||||
// Second message is encrypted.
|
||||
@@ -845,12 +855,14 @@ async fn test_synchronize_status() -> Result<()> {
|
||||
/// Tests that DC_EVENT_SELFAVATAR_CHANGED is emitted on avatar changes.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_selfavatar_changed_event() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
// Alice has two devices.
|
||||
let alice1 = TestContext::new_alice().await;
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
|
||||
// Bob has one device.
|
||||
let bob = TestContext::new_bob().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
assert_eq!(alice1.get_config(Config::Selfavatar).await?, None);
|
||||
|
||||
@@ -866,20 +878,9 @@ async fn test_selfavatar_changed_event() -> Result<()> {
|
||||
.get_matching(|e| matches!(e, EventType::SelfavatarChanged))
|
||||
.await;
|
||||
|
||||
// Bob sends a message so that Alice can encrypt to him.
|
||||
let chat = bob
|
||||
.create_chat_with_contact("Alice", "alice@example.org")
|
||||
.await;
|
||||
|
||||
send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
alice1.recv_msg(&sent_msg).await;
|
||||
alice2.recv_msg(&sent_msg).await;
|
||||
|
||||
// Alice sends a message.
|
||||
let alice1_chat_id = alice1.get_last_msg().await.chat_id;
|
||||
alice1_chat_id.accept(&alice1).await?;
|
||||
send_text_msg(&alice1, alice1_chat_id, "Hello".to_string()).await?;
|
||||
let alice1_chat_id = alice1.create_chat(bob).await.id;
|
||||
send_text_msg(alice1, alice1_chat_id, "Hello".to_string()).await?;
|
||||
let sent_msg = alice1.pop_sent_msg().await;
|
||||
|
||||
// The message is encrypted.
|
||||
@@ -1008,7 +1009,7 @@ async fn test_verified_by_none() -> Result<()> {
|
||||
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 create a peerstate.
|
||||
// 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;
|
||||
@@ -1066,14 +1067,9 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
let bob_biography = bob.get_config(Config::Selfstatus).await?.unwrap();
|
||||
let chat = bob.create_chat(alice).await;
|
||||
let sent_msg = bob.send_text(chat.id, "moin").await;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
let bob_id = Contact::create(alice, "Some Bob", &bob_addr).await?;
|
||||
let key_base64 = Peerstate::from_addr(alice, &bob_addr)
|
||||
.await?
|
||||
.unwrap()
|
||||
.peek_key(false)
|
||||
.unwrap()
|
||||
.to_base64();
|
||||
let bob_id = alice.recv_msg(&sent_msg).await.from_id;
|
||||
let bob_contact = Contact::get_by_id(alice, bob_id).await?;
|
||||
let key_base64 = bob_contact.public_key(alice).await?.unwrap().to_base64();
|
||||
let fiona_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
|
||||
|
||||
assert_eq!(make_vcard(alice, &[]).await?, "".to_string());
|
||||
@@ -1157,8 +1153,10 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests importing a vCard with the same email address,
|
||||
/// but a new key.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_import_vcard_updates_only_key() -> Result<()> {
|
||||
async fn test_import_vcard_key_change() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let bob = &TestContext::new_bob().await;
|
||||
let bob_addr = &bob.get_config(Config::Addr).await?.unwrap();
|
||||
@@ -1176,28 +1174,34 @@ async fn test_import_vcard_updates_only_key() -> Result<()> {
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
let bob = &TestContext::new().await;
|
||||
bob.configure_addr(bob_addr).await;
|
||||
bob.set_config(Config::Displayname, Some("Not Bob")).await?;
|
||||
let avatar_path = bob.dir.path().join("avatar.png");
|
||||
let bob1 = &TestContext::new().await;
|
||||
bob1.configure_addr(bob_addr).await;
|
||||
bob1.set_config(Config::Displayname, Some("New Bob"))
|
||||
.await?;
|
||||
let avatar_path = bob1.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&avatar_path, avatar_bytes).await?;
|
||||
bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
|
||||
bob1.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
|
||||
.await?;
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
let vcard1 = make_vcard(bob, &[ContactId::SELF]).await?;
|
||||
assert_eq!(import_vcard(alice, &vcard1).await?, vec![alice_bob_id]);
|
||||
let vcard1 = make_vcard(bob1, &[ContactId::SELF]).await?;
|
||||
let alice_bob_id1 = import_vcard(alice, &vcard1).await?[0];
|
||||
assert_ne!(alice_bob_id1, alice_bob_id);
|
||||
let alice_bob_contact = Contact::get_by_id(alice, alice_bob_id).await?;
|
||||
assert_eq!(alice_bob_contact.get_authname(), "Bob");
|
||||
assert_eq!(alice_bob_contact.get_profile_image(alice).await?, None);
|
||||
let alice_bob_contact1 = Contact::get_by_id(alice, alice_bob_id1).await?;
|
||||
assert_eq!(alice_bob_contact1.get_authname(), "New Bob");
|
||||
assert!(alice_bob_contact1.get_profile_image(alice).await?.is_some());
|
||||
|
||||
// Last message is still the same,
|
||||
// no new messages are added.
|
||||
let msg = alice.get_last_msg_in(chat_id).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(
|
||||
msg.get_text(),
|
||||
stock_str::contact_setup_changed(alice, bob_addr).await
|
||||
);
|
||||
let sent_msg = alice.send_text(chat_id, "moin").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_text(), "moin");
|
||||
|
||||
let chat_id1 = ChatId::create_for_contact(alice, alice_bob_id1).await?;
|
||||
let sent_msg = alice.send_text(chat_id1, "moin").await;
|
||||
let msg = bob1.recv_msg(&sent_msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
// The old vCard is imported, but doesn't change Bob's key for Alice.
|
||||
@@ -1209,63 +1213,6 @@ async fn test_import_vcard_updates_only_key() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_reset_encryption() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let msg = tcm.send_recv_accept(bob, alice, "Hi!").await;
|
||||
assert_eq!(msg.get_showpadlock(), true);
|
||||
|
||||
let alice_bob_chat_id = msg.chat_id;
|
||||
let alice_bob_contact_id = msg.from_id;
|
||||
|
||||
alice_bob_contact_id.reset_encryption(alice).await?;
|
||||
|
||||
let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(msg.get_showpadlock(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_reset_verified_encryption() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
tcm.execute_securejoin(bob, alice).await;
|
||||
|
||||
let msg = tcm.send_recv(bob, alice, "Encrypted").await;
|
||||
assert_eq!(msg.get_showpadlock(), true);
|
||||
|
||||
let alice_bob_chat_id = msg.chat_id;
|
||||
let alice_bob_contact_id = msg.from_id;
|
||||
|
||||
alice_bob_contact_id.reset_encryption(alice).await?;
|
||||
|
||||
// Check that the contact is still verified after resetting encryption.
|
||||
let alice_bob_contact = Contact::get_by_id(alice, alice_bob_contact_id).await?;
|
||||
assert_eq!(alice_bob_contact.is_verified(alice).await?, true);
|
||||
|
||||
// 1:1 chat and profile is no longer verified.
|
||||
assert_eq!(alice_bob_contact.is_profile_verified(alice).await?, false);
|
||||
|
||||
let info_msg = alice.get_last_msg_in(alice_bob_chat_id).await;
|
||||
assert_eq!(
|
||||
info_msg.text,
|
||||
"bob@example.net sent a message from another device."
|
||||
);
|
||||
|
||||
let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(msg.get_showpadlock(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_self_is_verified() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -1275,9 +1222,87 @@ async fn test_self_is_verified() -> Result<()> {
|
||||
assert_eq!(contact.is_verified(&alice).await?, true);
|
||||
assert!(contact.is_profile_verified(&alice).await?);
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
assert!(contact.is_key_contact());
|
||||
|
||||
let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?;
|
||||
assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that importing a vCard with a key creates a key-contact.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_vcard_creates_key_contact() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let vcard = make_vcard(bob, &[ContactId::SELF]).await?;
|
||||
let contact_ids = import_vcard(alice, &vcard).await?;
|
||||
assert_eq!(contact_ids.len(), 1);
|
||||
let contact_id = contact_ids.first().unwrap();
|
||||
let contact = Contact::get_by_id(alice, *contact_id).await?;
|
||||
assert!(contact.is_key_contact());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests changing display name by sending a message.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_name_changes() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Alice Revision 1"))
|
||||
.await?;
|
||||
let alice_bob_chat = alice.create_chat(bob).await;
|
||||
let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await;
|
||||
let bob_alice_id = bob.recv_msg(&sent_msg).await.from_id;
|
||||
let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
|
||||
assert_eq!(bob_alice_contact.get_display_name(), "Alice Revision 1");
|
||||
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Alice Revision 2"))
|
||||
.await?;
|
||||
let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
|
||||
assert_eq!(bob_alice_contact.get_display_name(), "Alice Revision 2");
|
||||
|
||||
// Explicitly rename contact to "Renamed".
|
||||
bob.evtracker.clear_events();
|
||||
bob_alice_contact.id.set_name(bob, "Renamed").await?;
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|e| matches!(e, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
assert_eq!(
|
||||
event,
|
||||
EventType::ContactsChanged(Some(bob_alice_contact.id))
|
||||
);
|
||||
let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
|
||||
assert_eq!(bob_alice_contact.get_display_name(), "Renamed");
|
||||
|
||||
// Alice also renames self into "Renamed".
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Renamed"))
|
||||
.await?;
|
||||
let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
|
||||
assert_eq!(bob_alice_contact.get_display_name(), "Renamed");
|
||||
|
||||
// Contact name was set to "Renamed" explicitly before,
|
||||
// so it should not be changed.
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Renamed again"))
|
||||
.await?;
|
||||
let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
|
||||
assert_eq!(bob_alice_contact.get_display_name(), "Renamed");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,35 +5,32 @@ use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
use pgp::composed::SignedPublicKey;
|
||||
use pgp::types::PublicKeyTrait;
|
||||
use ratelimit::Ratelimit;
|
||||
use tokio::sync::{Mutex, Notify, RwLock};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR,
|
||||
};
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::contact::{import_vcard, mark_contact_id_as_verified, Contact, ContactId};
|
||||
use crate::debug_logging::DebugLogging;
|
||||
use crate::download::DownloadState;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
|
||||
use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
|
||||
use crate::key::{load_self_secret_key, self_fingerprint};
|
||||
use crate::log::{info, warn};
|
||||
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
|
||||
use crate::message::{self, Message, MessageState, MsgId};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peer_channels::Iroh;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::push::PushSubscriber;
|
||||
use crate::quota::QuotaInfo;
|
||||
use crate::scheduler::{convert_folder_meaning, SchedulerState};
|
||||
@@ -279,6 +276,13 @@ pub struct InnerContext {
|
||||
/// `last_error` should be used to avoid races with the event thread.
|
||||
pub(crate) last_error: parking_lot::RwLock<String>,
|
||||
|
||||
/// It's not possible to emit migration errors as an event,
|
||||
/// because at the time of the migration, there is no event emitter yet.
|
||||
/// So, this holds the error that happened during migration, if any.
|
||||
/// This is necessary for the possibly-failible PGP migration,
|
||||
/// which happened 2025-05, and can be removed a few releases later.
|
||||
pub(crate) migration_error: parking_lot::RwLock<Option<String>>,
|
||||
|
||||
/// If debug logging is enabled, this contains all necessary information
|
||||
///
|
||||
/// Standard RwLock instead of [`tokio::sync::RwLock`] is used
|
||||
@@ -294,6 +298,11 @@ pub struct InnerContext {
|
||||
|
||||
/// Iroh for realtime peer channels.
|
||||
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
|
||||
|
||||
/// The own fingerprint, if it was computed already.
|
||||
/// tokio::sync::OnceCell would be possible to use, but overkill for our usecase;
|
||||
/// the standard library's OnceLock is enough, and it's a lot smaller in memory.
|
||||
pub(crate) self_fingerprint: OnceLock<String>,
|
||||
}
|
||||
|
||||
/// The state of ongoing process.
|
||||
@@ -448,10 +457,12 @@ impl Context {
|
||||
creation_time: tools::Time::now(),
|
||||
last_full_folder_scan: Mutex::new(None),
|
||||
last_error: parking_lot::RwLock::new("".to_string()),
|
||||
migration_error: parking_lot::RwLock::new(None),
|
||||
debug_logging: std::sync::RwLock::new(None),
|
||||
push_subscriber,
|
||||
push_subscribed: AtomicBool::new(false),
|
||||
iroh: Arc::new(RwLock::new(None)),
|
||||
self_fingerprint: OnceLock::new(),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
@@ -805,10 +816,10 @@ impl Context {
|
||||
|
||||
let pub_key_cnt = self
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM acpeerstates;", ())
|
||||
.count("SELECT COUNT(*) FROM public_keys;", ())
|
||||
.await?;
|
||||
let fingerprint_str = match load_self_public_key(self).await {
|
||||
Ok(key) => key.dc_fingerprint().hex(),
|
||||
let fingerprint_str = match self_fingerprint(self).await {
|
||||
Ok(fp) => fp.to_string(),
|
||||
Err(err) => format!("<key failure: {err}>"),
|
||||
};
|
||||
|
||||
@@ -1164,32 +1175,14 @@ impl Context {
|
||||
/// On the other end, a bot will receive the message and make it available
|
||||
/// to Delta Chat's developers.
|
||||
pub async fn draft_self_report(&self) -> Result<ChatId> {
|
||||
const SELF_REPORTING_BOT: &str = "self_reporting@testrun.org";
|
||||
const SELF_REPORTING_BOT_VCARD: &str = include_str!("../assets/self-reporting-bot.vcf");
|
||||
let contact_id: ContactId = *import_vcard(self, SELF_REPORTING_BOT_VCARD)
|
||||
.await?
|
||||
.first()
|
||||
.context("Self reporting bot vCard does not contain a contact")?;
|
||||
mark_contact_id_as_verified(self, contact_id, ContactId::SELF).await?;
|
||||
|
||||
let contact_id = Contact::create(self, "Statistics bot", SELF_REPORTING_BOT).await?;
|
||||
let chat_id = ChatId::create_for_contact(self, contact_id).await?;
|
||||
|
||||
// We're including the bot's public key in Delta Chat
|
||||
// so that the first message to the bot can directly be encrypted:
|
||||
let public_key = SignedPublicKey::from_base64(
|
||||
"xjMEZbfBlBYJKwYBBAHaRw8BAQdABpLWS2PUIGGo4pslVt4R8sylP5wZihmhf1DTDr3oCM\
|
||||
PNHDxzZWxmX3JlcG9ydGluZ0B0ZXN0cnVuLm9yZz7CiwQQFggAMwIZAQUCZbfBlAIbAwQLCQgHBhUI\
|
||||
CQoLAgMWAgEWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohD8dAQCQV7CoH6UP4PD+Nq\
|
||||
I4kW5tbbqdh2AnDROg60qotmLExAEAxDfd3QHAK9f8b9qQUbLmHIztCLxhEuVbWPBEYeVW0gvOOARl\
|
||||
t8GUEgorBgEEAZdVAQUBAQdAMBUhYoAAcI625vGZqnM5maPX4sGJ7qvJxPAFILPy6AcDAQgHwngEGB\
|
||||
YIACAFAmW3wZQCGwwWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohPwCAQCvzk1ObIkj\
|
||||
2GqsuIfaULlgdnfdZY8LNary425CEfHZDQD5AblXVrlMO1frdlc/Vo9z3pEeCrfYdD7ITD3/OeVoiQ\
|
||||
4=",
|
||||
)?;
|
||||
let mut peerstate = Peerstate::from_public_key(
|
||||
SELF_REPORTING_BOT,
|
||||
0,
|
||||
EncryptPreference::Mutual,
|
||||
&public_key,
|
||||
);
|
||||
let fingerprint = public_key.dc_fingerprint();
|
||||
peerstate.set_verified(public_key, fingerprint, "".to_string())?;
|
||||
peerstate.save_to_db(&self.sql).await?;
|
||||
chat_id
|
||||
.set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id))
|
||||
.await?;
|
||||
|
||||
@@ -3,14 +3,9 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
use mailparse::ParsedMail;
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::context::Context;
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::log::info;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::key::{Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::pgp;
|
||||
|
||||
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message.
|
||||
@@ -144,83 +139,6 @@ pub(crate) fn validate_detached_signature<'a, 'b>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns public keyring for `peerstate`.
|
||||
pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Vec<SignedPublicKey> {
|
||||
let mut public_keyring_for_validate = Vec::new();
|
||||
if let Some(peerstate) = peerstate {
|
||||
if let Some(key) = &peerstate.public_key {
|
||||
public_keyring_for_validate.push(key.clone());
|
||||
} else if let Some(key) = &peerstate.gossip_key {
|
||||
public_keyring_for_validate.push(key.clone());
|
||||
}
|
||||
}
|
||||
public_keyring_for_validate
|
||||
}
|
||||
|
||||
/// Applies Autocrypt header to Autocrypt peer state and saves it into the database.
|
||||
///
|
||||
/// If we already know this fingerprint from another contact's peerstate, return that
|
||||
/// peerstate in order to make AEAP work, but don't save it into the db yet.
|
||||
///
|
||||
/// Returns updated peerstate.
|
||||
pub(crate) async fn get_autocrypt_peerstate(
|
||||
context: &Context,
|
||||
from: &str,
|
||||
autocrypt_header: Option<&Aheader>,
|
||||
message_time: i64,
|
||||
allow_aeap: bool,
|
||||
) -> Result<Option<Peerstate>> {
|
||||
let allow_change = !context.is_self_addr(from).await?;
|
||||
let mut peerstate;
|
||||
|
||||
// Apply Autocrypt header
|
||||
if let Some(header) = autocrypt_header {
|
||||
if allow_aeap {
|
||||
// If we know this fingerprint from another addr,
|
||||
// we may want to do a transition from this other addr
|
||||
// (and keep its peerstate)
|
||||
// For security reasons, for now, we only do a transition
|
||||
// if the fingerprint is verified.
|
||||
peerstate = Peerstate::from_verified_fingerprint_or_addr(
|
||||
context,
|
||||
&header.public_key.dc_fingerprint(),
|
||||
from,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
peerstate = Peerstate::from_addr(context, from).await?;
|
||||
}
|
||||
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
if addr_cmp(&peerstate.addr, from) {
|
||||
if allow_change {
|
||||
peerstate.apply_header(context, header, message_time);
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Refusing to update existing peerstate of {}", &peerstate.addr
|
||||
);
|
||||
}
|
||||
}
|
||||
// If `peerstate.addr` and `from` differ, this means that
|
||||
// someone is using the same key but a different addr, probably
|
||||
// because they made an AEAP transition.
|
||||
// But we don't know if that's legit until we checked the
|
||||
// signatures, so wait until then with writing anything
|
||||
// to the database.
|
||||
} else {
|
||||
let p = Peerstate::from_header(header, message_time);
|
||||
p.save_to_db(&context.sql).await?;
|
||||
peerstate = Some(p);
|
||||
}
|
||||
} else {
|
||||
peerstate = Peerstate::from_addr(context, from).await?;
|
||||
}
|
||||
|
||||
Ok(peerstate)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
222
src/e2ee.rs
222
src/e2ee.rs
@@ -1,9 +1,8 @@
|
||||
//! End-to-end encryption support.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::Result;
|
||||
use mail_builder::mime::MimePart;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
@@ -11,8 +10,6 @@ use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::key::{load_self_public_key, load_self_secret_key, SignedPublicKey};
|
||||
use crate::log::warn;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::pgp;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -43,93 +40,6 @@ impl EncryptHelper {
|
||||
Aheader::new(addr, pk, self.prefer_encrypt)
|
||||
}
|
||||
|
||||
/// Determines if we can and should encrypt.
|
||||
pub(crate) async fn should_encrypt(
|
||||
&self,
|
||||
context: &Context,
|
||||
peerstates: &[(Option<Peerstate>, String)],
|
||||
) -> Result<bool> {
|
||||
let is_chatmail = context.is_chatmail().await?;
|
||||
for (peerstate, _addr) in peerstates {
|
||||
if let Some(peerstate) = peerstate {
|
||||
// For chatmail we ignore the encryption preference,
|
||||
// because we can either send encrypted or not at all.
|
||||
if is_chatmail || peerstate.prefer_encrypt != EncryptPreference::Reset {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Constructs a vector of public keys for given peerstates.
|
||||
///
|
||||
/// In addition returns the set of recipient addresses
|
||||
/// for which there is no key available.
|
||||
///
|
||||
/// Returns an error if there are recipients
|
||||
/// other than self, but no recipient keys are available.
|
||||
pub(crate) fn encryption_keyring(
|
||||
&self,
|
||||
context: &Context,
|
||||
verified: bool,
|
||||
peerstates: &[(Option<Peerstate>, String)],
|
||||
) -> Result<(Vec<SignedPublicKey>, BTreeSet<String>)> {
|
||||
// Encrypt to self unconditionally,
|
||||
// even for a single-device setup.
|
||||
let mut keyring = vec![self.public_key.clone()];
|
||||
let mut missing_key_addresses = BTreeSet::new();
|
||||
|
||||
if peerstates.is_empty() {
|
||||
return Ok((keyring, missing_key_addresses));
|
||||
}
|
||||
|
||||
let mut verifier_addresses: Vec<&str> = Vec::new();
|
||||
|
||||
for (peerstate, addr) in peerstates {
|
||||
if let Some(peerstate) = peerstate {
|
||||
if let Some(key) = peerstate.clone().take_key(verified) {
|
||||
keyring.push(key);
|
||||
verifier_addresses.push(addr);
|
||||
} else {
|
||||
warn!(context, "Encryption key for {addr} is missing.");
|
||||
missing_key_addresses.insert(addr.clone());
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Peerstate for {addr} is missing.");
|
||||
missing_key_addresses.insert(addr.clone());
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
!keyring.is_empty(),
|
||||
"At least our own key is in the keyring"
|
||||
);
|
||||
if keyring.len() <= 1 {
|
||||
bail!("No recipient keys are available, cannot encrypt");
|
||||
}
|
||||
|
||||
// Encrypt to secondary verified keys
|
||||
// if we also encrypt to the introducer ("verifier") of the key.
|
||||
if verified {
|
||||
for (peerstate, _addr) in peerstates {
|
||||
if let Some(peerstate) = peerstate {
|
||||
if let (Some(key), Some(verifier)) = (
|
||||
peerstate.secondary_verified_key.as_ref(),
|
||||
peerstate.secondary_verifier.as_deref(),
|
||||
) {
|
||||
if verifier_addresses.contains(&verifier) {
|
||||
keyring.push(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((keyring, missing_key_addresses))
|
||||
}
|
||||
|
||||
/// Tries to encrypt the passed in `mail`.
|
||||
pub async fn encrypt(
|
||||
self,
|
||||
@@ -177,11 +87,9 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::chat::send_text_msg;
|
||||
use crate::config::Config;
|
||||
use crate::key::DcKey;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::message::Message;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{bob_keypair, TestContext, TestContextManager};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
mod ensure_secret_key_exists {
|
||||
use super::*;
|
||||
@@ -223,130 +131,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_encrypted_no_autocrypt() -> anyhow::Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let chat_alice = alice.create_email_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_email_chat(&alice).await.id;
|
||||
|
||||
// Alice sends unencrypted message to Bob
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
let sent = alice.send_msg(chat_alice, &mut msg).await;
|
||||
|
||||
// Bob receives unencrypted message from Alice
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert!(!msg.get_showpadlock());
|
||||
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Bob sends empty encrypted message to Alice
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
let sent = bob.send_msg(chat_bob, &mut msg).await;
|
||||
|
||||
// Alice receives an empty encrypted message from Bob.
|
||||
// This is also a regression test for previously existing bug
|
||||
// that resulted in no padlock on encrypted empty messages.
|
||||
let msg = alice.recv_msg(&sent).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
let peerstate_bob = Peerstate::from_addr(&alice.ctx, "bob@example.net")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_bob.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Now Alice and Bob have established keys.
|
||||
|
||||
// Alice sends encrypted message without Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
let sent = alice.send_msg(chat_alice, &mut msg).await;
|
||||
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Alice sends plaintext message with Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.force_plaintext();
|
||||
let sent = alice.send_msg(chat_alice, &mut msg).await;
|
||||
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert!(!msg.get_showpadlock());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Alice sends plaintext message without Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.force_plaintext();
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
let sent = alice.send_msg(chat_alice, &mut msg).await;
|
||||
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert!(!msg.get_showpadlock());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Reset);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_peerstates(prefer_encrypt: EncryptPreference) -> Vec<(Option<Peerstate>, String)> {
|
||||
let addr = "bob@foo.bar";
|
||||
let pub_key = bob_keypair().public;
|
||||
let peerstate = Peerstate {
|
||||
addr: addr.into(),
|
||||
last_seen: 13,
|
||||
last_seen_autocrypt: 14,
|
||||
prefer_encrypt,
|
||||
public_key: Some(pub_key.clone()),
|
||||
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
gossip_key: Some(pub_key.clone()),
|
||||
gossip_timestamp: 15,
|
||||
gossip_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
verified_key: Some(pub_key.clone()),
|
||||
verified_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
backward_verified_key_id: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
vec![(Some(peerstate), addr.to_string())]
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_should_encrypt() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let encrypt_helper = EncryptHelper::new(&t).await.unwrap();
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::NoPreference);
|
||||
assert!(encrypt_helper.should_encrypt(&t, &ps).await?);
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::Reset);
|
||||
assert!(!encrypt_helper.should_encrypt(&t, &ps).await?);
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::Mutual);
|
||||
assert!(encrypt_helper.should_encrypt(&t, &ps).await?);
|
||||
|
||||
// test with missing peerstate
|
||||
let ps = vec![(None, "bob@foo.bar".to_string())];
|
||||
assert!(!encrypt_helper.should_encrypt(&t, &ps).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_chatmail_can_send_unencrypted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
@@ -247,8 +247,7 @@ mod test_chatlist_events {
|
||||
|
||||
bob.evtracker.clear_events();
|
||||
// set name
|
||||
let addr = alice_on_bob.get_addr();
|
||||
Contact::create(&bob, "Alice2", addr).await?;
|
||||
alice_on_bob.id.set_name(&bob, "Alice2").await?;
|
||||
assert!(bob.add_or_lookup_contact(&alice).await.get_display_name() == "Alice2");
|
||||
|
||||
wait_for_chatlist_all_items(&bob).await;
|
||||
|
||||
@@ -75,6 +75,12 @@ pub enum HeaderDef {
|
||||
/// for members listed in the `Chat-Group-Past-Members` field.
|
||||
ChatGroupMemberTimestamps,
|
||||
|
||||
/// Space-separated PGP key fingerprints
|
||||
/// of group members listed in the `To` field
|
||||
/// followed by fingerprints
|
||||
/// of past members listed in the `Chat-Group-Past-Members` field.
|
||||
ChatGroupMemberFpr,
|
||||
|
||||
/// Duration of the attached media file.
|
||||
ChatDuration,
|
||||
|
||||
|
||||
@@ -1884,7 +1884,7 @@ async fn should_move_out_of_spam(
|
||||
};
|
||||
// No chat found.
|
||||
let (from_id, blocked_contact, _origin) =
|
||||
match from_field_to_contact_id(context, &from, true)
|
||||
match from_field_to_contact_id(context, &from, None, true, true)
|
||||
.await
|
||||
.context("from_field_to_contact_id")?
|
||||
{
|
||||
@@ -2242,7 +2242,7 @@ pub(crate) async fn prefetch_should_download(
|
||||
None => return Ok(false),
|
||||
};
|
||||
let (_from_id, blocked_contact, origin) =
|
||||
match from_field_to_contact_id(context, &from, true).await? {
|
||||
match from_field_to_contact_id(context, &from, None, true, true).await? {
|
||||
Some(res) => res,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
@@ -290,10 +290,12 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_key_transfer() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
tcm.section("Alice sends Autocrypt setup message");
|
||||
alice.set_config(Config::BccSelf, Some("0")).await?;
|
||||
let setup_code = initiate_key_transfer(&alice).await?;
|
||||
let setup_code = initiate_key_transfer(alice).await?;
|
||||
|
||||
// Test that sending Autocrypt Setup Message enables `bcc_self`.
|
||||
assert_eq!(alice.get_config_bool(Config::BccSelf).await?, true);
|
||||
@@ -301,26 +303,21 @@ mod tests {
|
||||
// Get Autocrypt Setup Message.
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
// Alice sets up a second device.
|
||||
let alice2 = TestContext::new().await;
|
||||
tcm.section("Alice sets up a second device");
|
||||
let alice2 = &tcm.unconfigured().await;
|
||||
alice2.set_name("alice2");
|
||||
alice2.configure_addr("alice@example.org").await;
|
||||
alice2.recv_msg(&sent).await;
|
||||
let msg = alice2.get_last_msg().await;
|
||||
assert!(msg.is_setupmessage());
|
||||
assert_eq!(
|
||||
crate::key::load_self_secret_keyring(&alice2).await?.len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(crate::key::load_self_secret_keyring(alice2).await?.len(), 0);
|
||||
|
||||
// Transfer the key.
|
||||
tcm.section("Alice imports a key from Autocrypt Setup Message");
|
||||
alice2.set_config(Config::BccSelf, Some("0")).await?;
|
||||
continue_key_transfer(&alice2, msg.id, &setup_code).await?;
|
||||
continue_key_transfer(alice2, msg.id, &setup_code).await?;
|
||||
assert_eq!(alice2.get_config_bool(Config::BccSelf).await?, true);
|
||||
assert_eq!(
|
||||
crate::key::load_self_secret_keyring(&alice2).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(crate::key::load_self_secret_keyring(alice2).await?.len(), 1);
|
||||
|
||||
// Alice sends a message to self from the new device.
|
||||
let sent = alice2.send_text(msg.chat_id, "Test").await;
|
||||
|
||||
56
src/key.rs
56
src/key.rs
@@ -129,8 +129,11 @@ pub(crate) trait DcKey: Serialize + Deserializable + Clone {
|
||||
fn key_id(&self) -> KeyId;
|
||||
}
|
||||
|
||||
pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPublicKey> {
|
||||
let public_key = context
|
||||
/// Attempts to load own public key.
|
||||
///
|
||||
/// Returns `None` if no key is generated yet.
|
||||
pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result<Option<SignedPublicKey>> {
|
||||
let Some(public_key_bytes) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT public_key
|
||||
@@ -142,9 +145,20 @@ pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPubl
|
||||
Ok(bytes)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
match public_key {
|
||||
Some(bytes) => SignedPublicKey::from_slice(&bytes),
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
|
||||
Ok(Some(public_key))
|
||||
}
|
||||
|
||||
/// Loads own public key.
|
||||
///
|
||||
/// If no key is generated yet, generates a new one.
|
||||
pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPublicKey> {
|
||||
match load_self_public_key_opt(context).await? {
|
||||
Some(public_key) => Ok(public_key),
|
||||
None => {
|
||||
let keypair = generate_keypair(context).await?;
|
||||
Ok(keypair.public)
|
||||
@@ -171,6 +185,38 @@ pub(crate) async fn load_self_public_keyring(context: &Context) -> Result<Vec<Si
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// Returns own public key fingerprint in (not human-readable) hex representation.
|
||||
/// This is the fingerprint format that is used in the database.
|
||||
///
|
||||
/// If no key is generated yet, generates a new one.
|
||||
///
|
||||
/// For performance reasons, the fingerprint is cached after the first invocation.
|
||||
pub(crate) async fn self_fingerprint(context: &Context) -> Result<&str> {
|
||||
if let Some(fp) = context.self_fingerprint.get() {
|
||||
Ok(fp)
|
||||
} else {
|
||||
let fp = load_self_public_key(context).await?.dc_fingerprint().hex();
|
||||
Ok(context.self_fingerprint.get_or_init(|| fp))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns own public key fingerprint in (not human-readable) hex representation.
|
||||
/// This is the fingerprint format that is used in the database.
|
||||
///
|
||||
/// Returns `None` if no key is generated yet.
|
||||
///
|
||||
/// For performance reasons, the fingerprint is cached after the first invocation.
|
||||
pub(crate) async fn self_fingerprint_opt(context: &Context) -> Result<Option<&str>> {
|
||||
if let Some(fp) = context.self_fingerprint.get() {
|
||||
Ok(Some(fp))
|
||||
} else if let Some(key) = load_self_public_key_opt(context).await? {
|
||||
let fp = key.dc_fingerprint().hex();
|
||||
Ok(Some(context.self_fingerprint.get_or_init(|| fp)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn load_self_secret_key(context: &Context) -> Result<SignedSecretKey> {
|
||||
let private_key = context
|
||||
.sql
|
||||
|
||||
@@ -74,7 +74,6 @@ mod mimefactory;
|
||||
pub mod mimeparser;
|
||||
pub mod oauth2;
|
||||
mod param;
|
||||
pub mod peerstate;
|
||||
mod pgp;
|
||||
pub mod provider;
|
||||
pub mod qr;
|
||||
|
||||
10
src/log.rs
10
src/log.rs
@@ -68,6 +68,16 @@ impl Context {
|
||||
let last_error = &*self.last_error.read();
|
||||
last_error.clone()
|
||||
}
|
||||
|
||||
pub fn set_migration_error(&self, error: &str) {
|
||||
let mut migration_error = self.migration_error.write();
|
||||
*migration_error = Some(error.to_string());
|
||||
}
|
||||
|
||||
pub fn get_migration_error(&self) -> Option<String> {
|
||||
let migration_error = &*self.migration_error.read();
|
||||
migration_error.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LogExt<T, E>
|
||||
|
||||
@@ -835,6 +835,7 @@ impl Message {
|
||||
/// Returns true if padlock indicating message encryption should be displayed in the UI.
|
||||
pub fn get_showpadlock(&self) -> bool {
|
||||
self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0
|
||||
|| self.from_id == ContactId::DEVICE
|
||||
}
|
||||
|
||||
/// Returns true if message is auto-generated.
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{
|
||||
self, add_contact_to_chat, forward_msgs, marknoticed_chat, save_msgs, send_text_msg, ChatItem,
|
||||
ProtectionStatus,
|
||||
};
|
||||
use crate::chat::{self, forward_msgs, marknoticed_chat, save_msgs, send_text_msg, ChatItem};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::reaction::send_reaction;
|
||||
@@ -106,7 +103,7 @@ async fn test_create_webrtc_instance_noroom() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_width_height() {
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
// test that get_width() and get_height() are returning some dimensions for images;
|
||||
// (as the device-chat contains a welcome-images, we check that)
|
||||
@@ -183,6 +180,8 @@ async fn test_no_quote() {
|
||||
assert!(msg.quoted_message(bob).await.unwrap().is_none());
|
||||
}
|
||||
|
||||
/// Tests that quote of encrypted message
|
||||
/// cannot be sent unencrypted.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unencrypted_quote_encrypted_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -190,40 +189,26 @@ async fn test_unencrypted_quote_encrypted_message() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_group = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "Group chat", &[bob])
|
||||
tcm.section("Bob sends encrypted message to Alice");
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let sent = alice
|
||||
.send_text(alice_chat.id, "Hi! This is encrypted.")
|
||||
.await;
|
||||
let sent = alice.send_text(alice_group, "Hi! I created a group").await;
|
||||
|
||||
let bob_received_message = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_received_message.get_showpadlock(), true);
|
||||
|
||||
let bob_group = bob_received_message.chat_id;
|
||||
bob_group.accept(bob).await?;
|
||||
let sent = bob.send_text(bob_group, "Encrypted message").await;
|
||||
let alice_received_message = alice.recv_msg(&sent).await;
|
||||
assert!(alice_received_message.get_showpadlock());
|
||||
// Bob quotes encrypted message in unencrypted chat.
|
||||
let bob_email_chat = bob.create_email_chat(alice).await;
|
||||
let mut msg = Message::new_text("I am sending an unencrypted reply.".to_string());
|
||||
msg.set_quote(bob, Some(&bob_received_message)).await?;
|
||||
chat::send_msg(bob, bob_email_chat.id, &mut msg).await?;
|
||||
|
||||
// Alice adds contact without key so chat becomes unencrypted.
|
||||
let alice_flubby_contact_id = Contact::create(alice, "Flubby", "flubby@example.org").await?;
|
||||
add_contact_to_chat(alice, alice_group, alice_flubby_contact_id).await?;
|
||||
|
||||
// Alice quotes encrypted message in unencrypted chat.
|
||||
let mut msg = Message::new_text("unencrypted".to_string());
|
||||
msg.set_quote(alice, Some(&alice_received_message)).await?;
|
||||
chat::send_msg(alice, alice_group, &mut msg).await?;
|
||||
|
||||
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(bob_received_message.quoted_text().unwrap(), "...");
|
||||
assert_eq!(bob_received_message.get_showpadlock(), false);
|
||||
|
||||
// Alice replaces a quote of encrypted message with a quote of unencrypted one.
|
||||
let mut msg1 = Message::new(Viewtype::Text);
|
||||
msg1.set_quote(alice, Some(&alice_received_message)).await?;
|
||||
msg1.set_quote(alice, Some(&msg)).await?;
|
||||
chat::send_msg(alice, alice_group, &mut msg1).await?;
|
||||
|
||||
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(bob_received_message.quoted_text().unwrap(), "unencrypted");
|
||||
assert_eq!(bob_received_message.get_showpadlock(), false);
|
||||
// Alice receives unencrypted message,
|
||||
// but the quote of encrypted message is replaced with "...".
|
||||
let alice_received_message = alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert_eq!(alice_received_message.quoted_text().unwrap(), "...");
|
||||
assert_eq!(alice_received_message.get_showpadlock(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -424,12 +409,16 @@ async fn test_markseen_not_downloaded_msg() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
|
||||
let bob_chat_id = bob.create_chat(alice).await.id;
|
||||
alice.create_chat(bob).await; // Make sure the chat is accepted.
|
||||
|
||||
tcm.section("Bob sends a large message to Alice");
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?;
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
|
||||
tcm.section("Alice receives a large message from Bob");
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert!(!msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! # MIME message production.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use std::io::Cursor;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -23,14 +23,14 @@ use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::e2ee::EncryptHelper;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::key::DcKey;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::location;
|
||||
use crate::log::{info, warn};
|
||||
use crate::message::{self, Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{is_hidden, SystemMessage};
|
||||
use crate::param::Param;
|
||||
use crate::peer_channels::create_iroh_header;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::simplify::escape_message_footer_marks;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{
|
||||
@@ -88,6 +88,13 @@ pub struct MimeFactory {
|
||||
/// but `MimeFactory` is not responsible for this.
|
||||
recipients: Vec<String>,
|
||||
|
||||
/// Vector of pairs of recipient
|
||||
/// addresses and OpenPGP keys
|
||||
/// to use for encryption.
|
||||
///
|
||||
/// `None` if the message is not encrypted.
|
||||
encryption_keys: Option<Vec<(String, SignedPublicKey)>>,
|
||||
|
||||
/// Vector of pairs of recipient name and address that goes into the `To` field.
|
||||
///
|
||||
/// The list of actual message recipient addresses may be different,
|
||||
@@ -99,11 +106,18 @@ pub struct MimeFactory {
|
||||
/// Vector of pairs of past group member names and addresses.
|
||||
past_members: Vec<(String, String)>,
|
||||
|
||||
/// Fingerprints of the members in the same order as in the `to`
|
||||
/// followed by `past_members`.
|
||||
///
|
||||
/// If this is not empty, its length
|
||||
/// should be the sum of `to` and `past_members` length.
|
||||
member_fingerprints: Vec<String>,
|
||||
|
||||
/// Timestamps of the members in the same order as in the `to`
|
||||
/// followed by `past_members`.
|
||||
///
|
||||
/// If this is not empty, its length
|
||||
/// should be the sum of `recipients` and `past_members` length.
|
||||
/// should be the sum of `to` and `past_members` length.
|
||||
member_timestamps: Vec<i64>,
|
||||
|
||||
timestamp: i64,
|
||||
@@ -185,12 +199,24 @@ impl MimeFactory {
|
||||
let mut recipients = Vec::new();
|
||||
let mut to = Vec::new();
|
||||
let mut past_members = Vec::new();
|
||||
let mut member_fingerprints = Vec::new();
|
||||
let mut member_timestamps = Vec::new();
|
||||
let mut recipient_ids = HashSet::new();
|
||||
let mut req_mdn = false;
|
||||
|
||||
let encryption_keys;
|
||||
|
||||
let self_fingerprint = self_fingerprint(context).await?;
|
||||
|
||||
if chat.is_self_talk() {
|
||||
to.push((from_displayname.to_string(), from_addr.to_string()));
|
||||
|
||||
encryption_keys = if msg.param.get_bool(Param::ForcePlaintext).unwrap_or(false) {
|
||||
None
|
||||
} else {
|
||||
// Encrypt, but only to self.
|
||||
Some(Vec::new())
|
||||
};
|
||||
} else if chat.is_mailing_list() {
|
||||
let list_post = chat
|
||||
.param
|
||||
@@ -198,6 +224,9 @@ impl MimeFactory {
|
||||
.context("Can't write to mailinglist without ListPost param")?;
|
||||
to.push(("".to_string(), list_post.to_string()));
|
||||
recipients.push(list_post.to_string());
|
||||
|
||||
// Do not encrypt messages to mailing lists.
|
||||
encryption_keys = None;
|
||||
} else {
|
||||
let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
|
||||
msg.param.get(Param::Arg)
|
||||
@@ -205,27 +234,59 @@ impl MimeFactory {
|
||||
None
|
||||
};
|
||||
|
||||
let is_encrypted = if msg
|
||||
.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
false
|
||||
} else {
|
||||
msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default()
|
||||
|| chat.is_encrypted(context).await?
|
||||
};
|
||||
|
||||
let mut keys = Vec::new();
|
||||
let mut missing_key_addresses = BTreeSet::new();
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.authname, c.addr, c.id, cc.add_timestamp, cc.remove_timestamp
|
||||
"SELECT
|
||||
c.authname,
|
||||
c.addr,
|
||||
c.fingerprint,
|
||||
c.id,
|
||||
cc.add_timestamp,
|
||||
cc.remove_timestamp,
|
||||
k.public_key
|
||||
FROM chats_contacts cc
|
||||
LEFT JOIN contacts c ON cc.contact_id=c.id
|
||||
WHERE cc.chat_id=? AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))",
|
||||
LEFT JOIN public_keys k ON k.fingerprint=c.fingerprint
|
||||
WHERE cc.chat_id=?
|
||||
AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))",
|
||||
(msg.chat_id, chat.typ == Chattype::Group),
|
||||
|row| {
|
||||
let authname: String = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
let id: ContactId = row.get(2)?;
|
||||
let add_timestamp: i64 = row.get(3)?;
|
||||
let remove_timestamp: i64 = row.get(4)?;
|
||||
Ok((authname, addr, id, add_timestamp, remove_timestamp))
|
||||
let fingerprint: String = row.get(2)?;
|
||||
let id: ContactId = row.get(3)?;
|
||||
let add_timestamp: i64 = row.get(4)?;
|
||||
let remove_timestamp: i64 = row.get(5)?;
|
||||
let public_key_bytes_opt: Option<Vec<u8>> = row.get(6)?;
|
||||
Ok((authname, addr, fingerprint, id, add_timestamp, remove_timestamp, public_key_bytes_opt))
|
||||
},
|
||||
|rows| {
|
||||
let mut past_member_timestamps = Vec::new();
|
||||
let mut past_member_fingerprints = Vec::new();
|
||||
|
||||
for row in rows {
|
||||
let (authname, addr, id, add_timestamp, remove_timestamp) = row?;
|
||||
let (authname, addr, fingerprint, id, add_timestamp, remove_timestamp, public_key_bytes_opt) = row?;
|
||||
|
||||
let public_key_opt = if let Some(public_key_bytes) = &public_key_bytes_opt {
|
||||
Some(SignedPublicKey::from_slice(public_key_bytes)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let addr = if id == ContactId::SELF {
|
||||
from_addr.to_string()
|
||||
} else {
|
||||
@@ -237,13 +298,34 @@ impl MimeFactory {
|
||||
};
|
||||
if add_timestamp >= remove_timestamp {
|
||||
if !recipients_contain_addr(&to, &addr) {
|
||||
if id != ContactId::SELF {
|
||||
recipients.push(addr.clone());
|
||||
}
|
||||
if !undisclosed_recipients {
|
||||
to.push((name, addr));
|
||||
to.push((name, addr.clone()));
|
||||
|
||||
if is_encrypted {
|
||||
if !fingerprint.is_empty() {
|
||||
member_fingerprints.push(fingerprint);
|
||||
} else if id == ContactId::SELF {
|
||||
member_fingerprints.push(self_fingerprint.to_string());
|
||||
} else {
|
||||
debug_assert!(member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too");
|
||||
}
|
||||
}
|
||||
member_timestamps.push(add_timestamp);
|
||||
}
|
||||
}
|
||||
recipient_ids.insert(id);
|
||||
|
||||
if let Some(public_key) = public_key_opt {
|
||||
keys.push((addr.clone(), public_key))
|
||||
} else if id != ContactId::SELF {
|
||||
missing_key_addresses.insert(addr.clone());
|
||||
if is_encrypted {
|
||||
warn!(context, "Missing key for {addr}");
|
||||
}
|
||||
}
|
||||
} else if remove_timestamp.saturating_add(60 * 24 * 3600) > now {
|
||||
// Row is a tombstone,
|
||||
// member is not actually part of the group.
|
||||
@@ -253,27 +335,57 @@ impl MimeFactory {
|
||||
// This is a "member removed" message,
|
||||
// we need to notify removed member
|
||||
// that it was removed.
|
||||
if id != ContactId::SELF {
|
||||
recipients.push(addr.clone());
|
||||
}
|
||||
|
||||
if let Some(public_key) = public_key_opt {
|
||||
keys.push((addr.clone(), public_key))
|
||||
} else if id != ContactId::SELF {
|
||||
missing_key_addresses.insert(addr.clone());
|
||||
if is_encrypted {
|
||||
warn!(context, "Missing key for {addr}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !undisclosed_recipients {
|
||||
past_members.push((name, addr));
|
||||
past_members.push((name, addr.clone()));
|
||||
past_member_timestamps.push(remove_timestamp);
|
||||
|
||||
if is_encrypted {
|
||||
if !fingerprint.is_empty() {
|
||||
past_member_fingerprints.push(fingerprint);
|
||||
} else if id == ContactId::SELF {
|
||||
// It's fine to have self in past members
|
||||
// if we are leaving the group.
|
||||
past_member_fingerprints.push(self_fingerprint.to_string());
|
||||
} else {
|
||||
debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert!(member_timestamps.len() >= to.len());
|
||||
debug_assert!(member_fingerprints.is_empty() || member_fingerprints.len() >= to.len());
|
||||
|
||||
if to.len() > 1 {
|
||||
if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
|
||||
to.remove(position);
|
||||
member_timestamps.remove(position);
|
||||
if is_encrypted {
|
||||
member_fingerprints.remove(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
member_timestamps.extend(past_member_timestamps);
|
||||
if is_encrypted {
|
||||
member_fingerprints.extend(past_member_fingerprints);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
@@ -287,7 +399,26 @@ impl MimeFactory {
|
||||
{
|
||||
req_mdn = true;
|
||||
}
|
||||
|
||||
encryption_keys = if !is_encrypted {
|
||||
None
|
||||
} else {
|
||||
if keys.is_empty() && !recipients.is_empty() {
|
||||
bail!(
|
||||
"No recipient keys are available, cannot encrypt to {:?}.",
|
||||
recipients
|
||||
);
|
||||
}
|
||||
|
||||
// Remove recipients for which the key is missing.
|
||||
if !missing_key_addresses.is_empty() {
|
||||
recipients.retain(|addr| !missing_key_addresses.contains(addr));
|
||||
}
|
||||
|
||||
Some(keys)
|
||||
};
|
||||
}
|
||||
|
||||
let (in_reply_to, references) = context
|
||||
.sql
|
||||
.query_row(
|
||||
@@ -320,14 +451,17 @@ impl MimeFactory {
|
||||
member_timestamps.is_empty()
|
||||
|| to.len() + past_members.len() == member_timestamps.len()
|
||||
);
|
||||
|
||||
let factory = MimeFactory {
|
||||
from_addr,
|
||||
from_displayname,
|
||||
sender_displayname,
|
||||
selfstatus,
|
||||
recipients,
|
||||
encryption_keys,
|
||||
to,
|
||||
past_members,
|
||||
member_fingerprints,
|
||||
member_timestamps,
|
||||
timestamp: msg.timestamp_sort,
|
||||
loaded: Loaded::Message { msg, chat },
|
||||
@@ -351,14 +485,27 @@ impl MimeFactory {
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
let timestamp = create_smeared_timestamp(context);
|
||||
|
||||
let addr = contact.get_addr().to_string();
|
||||
let encryption_keys = if contact.is_key_contact() {
|
||||
if let Some(key) = contact.public_key(context).await? {
|
||||
Some(vec![(addr.clone(), key)])
|
||||
} else {
|
||||
Some(Vec::new())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let res = MimeFactory {
|
||||
from_addr,
|
||||
from_displayname: "".to_string(),
|
||||
sender_displayname: None,
|
||||
selfstatus: "".to_string(),
|
||||
recipients: vec![contact.get_addr().to_string()],
|
||||
recipients: vec![addr],
|
||||
encryption_keys,
|
||||
to: vec![("".to_string(), contact.get_addr().to_string())],
|
||||
past_members: vec![],
|
||||
member_fingerprints: vec![],
|
||||
member_timestamps: vec![],
|
||||
timestamp,
|
||||
loaded: Loaded::Mdn {
|
||||
@@ -376,57 +523,6 @@ impl MimeFactory {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn peerstates_for_recipients(
|
||||
&self,
|
||||
context: &Context,
|
||||
) -> Result<Vec<(Option<Peerstate>, String)>> {
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
|
||||
let mut res = Vec::new();
|
||||
for addr in self.recipients.iter().filter(|&addr| *addr != self_addr) {
|
||||
res.push((Peerstate::from_addr(context, addr).await?, addr.clone()));
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn is_e2ee_guaranteed(&self) -> bool {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat, msg } => {
|
||||
!msg.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
.unwrap_or_default()
|
||||
&& (chat.is_protected()
|
||||
|| msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default())
|
||||
}
|
||||
Loaded::Mdn { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn verified(&self) -> bool {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat, msg } => {
|
||||
chat.is_self_talk() ||
|
||||
// Securejoin messages are supposed to verify a key.
|
||||
// In order to do this, it is necessary that they can be sent
|
||||
// to a key that is not yet verified.
|
||||
// This has to work independently of whether the chat is protected right now.
|
||||
chat.is_protected() && msg.get_info_type() != SystemMessage::SecurejoinMessage
|
||||
}
|
||||
Loaded::Mdn { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_force_plaintext(&self) -> bool {
|
||||
match &self.loaded {
|
||||
Loaded::Message { msg, .. } => msg
|
||||
.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
.unwrap_or_default(),
|
||||
Loaded::Mdn { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_skip_autocrypt(&self) -> bool {
|
||||
match &self.loaded {
|
||||
Loaded::Message { msg, .. } => {
|
||||
@@ -602,9 +698,8 @@ impl MimeFactory {
|
||||
}
|
||||
|
||||
if let Loaded::Message { chat, .. } = &self.loaded {
|
||||
if chat.typ == Chattype::Group
|
||||
&& !self.member_timestamps.is_empty()
|
||||
&& !chat.member_list_is_stale(context).await?
|
||||
if chat.typ == Chattype::Group {
|
||||
if !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await?
|
||||
{
|
||||
headers.push((
|
||||
"Chat-Group-Member-Timestamps",
|
||||
@@ -618,6 +713,21 @@ impl MimeFactory {
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
if !self.member_fingerprints.is_empty() {
|
||||
headers.push((
|
||||
"Chat-Group-Member-Fpr",
|
||||
mail_builder::headers::raw::Raw::new(
|
||||
self.member_fingerprints
|
||||
.iter()
|
||||
.map(|fp| fp.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(" "),
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let subject_str = self.subject_str(context).await?;
|
||||
@@ -727,10 +837,8 @@ impl MimeFactory {
|
||||
));
|
||||
}
|
||||
|
||||
let verified = self.verified();
|
||||
let grpimage = self.grpimage();
|
||||
let skip_autocrypt = self.should_skip_autocrypt();
|
||||
let e2ee_guaranteed = self.is_e2ee_guaranteed();
|
||||
let encrypt_helper = EncryptHelper::new(context).await?;
|
||||
|
||||
if !skip_autocrypt {
|
||||
@@ -742,6 +850,8 @@ impl MimeFactory {
|
||||
));
|
||||
}
|
||||
|
||||
let is_encrypted = self.encryption_keys.is_some();
|
||||
|
||||
// Add ephemeral timer for non-MDN messages.
|
||||
// For MDNs it does not matter because they are not visible
|
||||
// and ignored by the receiver.
|
||||
@@ -755,9 +865,6 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
let peerstates = self.peerstates_for_recipients(context).await?;
|
||||
let is_encrypted = !self.should_force_plaintext()
|
||||
&& (e2ee_guaranteed || encrypt_helper.should_encrypt(context, &peerstates).await?);
|
||||
let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded {
|
||||
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
} else {
|
||||
@@ -904,7 +1011,7 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
let outer_message = if is_encrypted {
|
||||
let outer_message = if let Some(encryption_keys) = self.encryption_keys {
|
||||
// Store protected headers in the inner message.
|
||||
let message = protected_headers
|
||||
.into_iter()
|
||||
@@ -921,7 +1028,7 @@ impl MimeFactory {
|
||||
|
||||
// Add gossip headers in chats with multiple recipients
|
||||
let multiple_recipients =
|
||||
peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
|
||||
encryption_keys.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
|
||||
|
||||
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
|
||||
let now = time();
|
||||
@@ -929,11 +1036,7 @@ impl MimeFactory {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat, msg } => {
|
||||
if chat.typ != Chattype::Broadcast {
|
||||
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
|
||||
let Some(key) = peerstate.peek_key(verified) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for (addr, key) in &encryption_keys {
|
||||
let fingerprint = key.dc_fingerprint().hex();
|
||||
let cmd = msg.param.get_cmd();
|
||||
let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup
|
||||
@@ -965,7 +1068,7 @@ impl MimeFactory {
|
||||
}
|
||||
|
||||
let header = Aheader::new(
|
||||
peerstate.addr.clone(),
|
||||
addr.clone(),
|
||||
key.clone(),
|
||||
// Autocrypt 1.1.0 specification says that
|
||||
// `prefer-encrypt` attribute SHOULD NOT be included.
|
||||
@@ -1015,8 +1118,10 @@ impl MimeFactory {
|
||||
Loaded::Mdn { .. } => true,
|
||||
};
|
||||
|
||||
let (encryption_keyring, missing_key_addresses) =
|
||||
encrypt_helper.encryption_keyring(context, verified, &peerstates)?;
|
||||
// Encrypt to self unconditionally,
|
||||
// even for a single-device setup.
|
||||
let mut encryption_keyring = vec![encrypt_helper.public_key.clone()];
|
||||
encryption_keyring.extend(encryption_keys.iter().map(|(_addr, key)| (*key).clone()));
|
||||
|
||||
// XXX: additional newline is needed
|
||||
// to pass filtermail at
|
||||
@@ -1026,12 +1131,6 @@ impl MimeFactory {
|
||||
.await?
|
||||
+ "\n";
|
||||
|
||||
// Remove recipients for which the key is missing.
|
||||
if !missing_key_addresses.is_empty() {
|
||||
self.recipients
|
||||
.retain(|addr| !missing_key_addresses.contains(addr));
|
||||
}
|
||||
|
||||
// Set the appropriate Content-Type for the outer message
|
||||
MimePart::new(
|
||||
"multipart/encrypted; protocol=\"application/pgp-encrypted\"",
|
||||
@@ -1257,6 +1356,8 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
SystemMessage::MemberAddedToGroup => {
|
||||
// TODO: lookup the contact by ID rather than email address.
|
||||
// We are adding key-contacts, the cannot be looked up by address.
|
||||
let email_to_add = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
placeholdertext =
|
||||
Some(stock_str::msg_add_member_remote(context, email_to_add).await);
|
||||
@@ -1561,7 +1662,7 @@ impl MimeFactory {
|
||||
|
||||
// we do not piggyback sync-files to other self-sent-messages
|
||||
// to not risk files becoming too larger and being skipped by download-on-demand.
|
||||
if command == SystemMessage::MultiDeviceSync && self.is_e2ee_guaranteed() {
|
||||
if command == SystemMessage::MultiDeviceSync {
|
||||
let json = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
let ids = msg.param.get(Param::Arg2).unwrap_or_default();
|
||||
parts.push(context.build_sync_part(json.to_string()));
|
||||
|
||||
@@ -339,39 +339,31 @@ async fn test_subject_in_group() -> Result<()> {
|
||||
}
|
||||
|
||||
// 6. Test that in a group, replies also take the quoted message's subject, while non-replies use the group title as subject
|
||||
let t = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let group_id = chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname") // TODO encodings, ä
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let group_id = chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname")
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_contact_id = t.add_or_lookup_contact_id(&bob).await;
|
||||
chat::add_contact_to_chat(&t, group_id, bob_contact_id).await?;
|
||||
|
||||
let subject = send_msg_get_subject(&t, group_id, None).await?;
|
||||
assert_eq!(subject, "groupname");
|
||||
let sent_message = t.send_text(group_id, "Hello!").await;
|
||||
let bob_received_message = bob.recv_msg(&sent_message).await;
|
||||
let bob_group_id = bob_received_message.chat_id;
|
||||
bob_group_id.accept(&bob).await.unwrap();
|
||||
assert_eq!(get_subject(&t, sent_message).await?, "groupname");
|
||||
|
||||
let subject = send_msg_get_subject(&t, group_id, None).await?;
|
||||
assert_eq!(subject, "Re: groupname");
|
||||
|
||||
receive_imf(
|
||||
&t,
|
||||
format!(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Different subject\n\
|
||||
In-Reply-To: {}\n\
|
||||
Message-ID: <2893@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
t.get_last_msg().await.rfc724_mid
|
||||
)
|
||||
.as_bytes(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let message_from_bob = t.get_last_msg().await;
|
||||
let subject = send_msg_get_subject(&t, group_id, None).await?;
|
||||
assert_eq!(subject, "Re: groupname");
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_subject("Different subject".to_string());
|
||||
let bob_sent_msg = bob.send_msg(bob_group_id, &mut msg).await;
|
||||
let message_from_bob = t.recv_msg(&bob_sent_msg).await;
|
||||
|
||||
let subject = send_msg_get_subject(&t, group_id, None).await?;
|
||||
assert_eq!(subject, "Re: groupname");
|
||||
@@ -623,43 +615,6 @@ async fn test_selfavatar_unencrypted() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_group_avatar_unencrypted() -> anyhow::Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
let group_id = chat::create_group_chat(t, chat::ProtectionStatus::Unprotected, "Group")
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Contact::create(t, "", "bob@example.org").await?;
|
||||
chat::add_contact_to_chat(t, group_id, bob).await?;
|
||||
|
||||
let file = t.dir.path().join("avatar.png");
|
||||
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
chat::set_chat_profile_image(t, group_id, file.to_str().unwrap()).await?;
|
||||
|
||||
// Send message to bob: that should get multipart/mixed because of the avatar moved to inner header.
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
let sent_msg = t.send_msg(group_id, &mut msg).await;
|
||||
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
|
||||
|
||||
let outer = payload.next().unwrap();
|
||||
let inner = payload.next().unwrap();
|
||||
let body = payload.next().unwrap();
|
||||
|
||||
assert_eq!(outer.match_indices("multipart/mixed").count(), 1);
|
||||
assert_eq!(outer.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(outer.match_indices("Subject:").count(), 1);
|
||||
assert_eq!(outer.match_indices("Autocrypt:").count(), 1);
|
||||
assert_eq!(outer.match_indices("Chat-Group-Avatar:").count(), 0);
|
||||
|
||||
assert_eq!(inner.match_indices("text/plain").count(), 1);
|
||||
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(inner.match_indices("Chat-Group-Avatar:").count(), 1);
|
||||
|
||||
assert_eq!(body.match_indices("this is the text!").count(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_selfavatar_unencrypted_signed() {
|
||||
// create chat with bob, set selfavatar
|
||||
|
||||
@@ -13,7 +13,7 @@ use format_flowed::unformat_flowed;
|
||||
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
|
||||
use mime::Mime;
|
||||
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::aheader::Aheader;
|
||||
use crate::authres::handle_authres;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::ChatId;
|
||||
@@ -21,10 +21,7 @@ use crate::config::Config;
|
||||
use crate::constants;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::decrypt::{
|
||||
get_autocrypt_peerstate, get_encrypted_mime, keyring_from_peerstate, try_decrypt,
|
||||
validate_detached_signature,
|
||||
};
|
||||
use crate::decrypt::{try_decrypt, validate_detached_signature};
|
||||
use crate::dehtml::dehtml;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
@@ -32,7 +29,6 @@ use crate::key::{self, load_self_secret_keyring, DcKey, Fingerprint, SignedPubli
|
||||
use crate::log::{error, info, warn};
|
||||
use crate::message::{self, get_vcard_summary, set_msg_failed, Message, MsgId, Viewtype};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::simplify::{simplify, SimplifiedText};
|
||||
use crate::sync::SyncItems;
|
||||
use crate::tools::{
|
||||
@@ -72,17 +68,12 @@ pub(crate) struct MimeMessage {
|
||||
/// `From:` address.
|
||||
pub from: SingleInfo,
|
||||
|
||||
/// Whether the From address was repeated in the signed part
|
||||
/// (and we know that the signer intended to send from this address)
|
||||
pub from_is_signed: bool,
|
||||
/// Whether the message is incoming or outgoing (self-sent).
|
||||
pub incoming: bool,
|
||||
/// The List-Post address is only set for mailing lists. Users can send
|
||||
/// messages to this address to post them to the list.
|
||||
pub list_post: Option<String>,
|
||||
pub chat_disposition_notification_to: Option<SingleInfo>,
|
||||
pub autocrypt_header: Option<Aheader>,
|
||||
pub peerstate: Option<Peerstate>,
|
||||
pub decrypting_failed: bool,
|
||||
|
||||
/// Set of valid signature fingerprints if a message is an
|
||||
@@ -91,11 +82,16 @@ pub(crate) struct MimeMessage {
|
||||
/// If a message is not encrypted or the signature is not valid,
|
||||
/// this set is empty.
|
||||
pub signatures: HashSet<Fingerprint>,
|
||||
/// The mail recipient addresses for which gossip headers were applied
|
||||
/// and their respective gossiped keys,
|
||||
/// regardless of whether they modified any peerstates.
|
||||
|
||||
/// The addresses for which there was a gossip header
|
||||
/// and their respective gossiped keys.
|
||||
pub gossiped_keys: HashMap<String, SignedPublicKey>,
|
||||
|
||||
/// Fingerprint of the key in the Autocrypt header.
|
||||
///
|
||||
/// It is not verified that the sender can use this key.
|
||||
pub autocrypt_fingerprint: Option<String>,
|
||||
|
||||
/// True if the message is a forwarded message.
|
||||
pub is_forwarded: bool,
|
||||
pub is_system_message: SystemMessage,
|
||||
@@ -118,8 +114,7 @@ pub(crate) struct MimeMessage {
|
||||
/// MIME message in this case.
|
||||
pub is_mime_modified: bool,
|
||||
|
||||
/// Decrypted, raw MIME structure. Nonempty iff `is_mime_modified` and the message was actually
|
||||
/// encrypted.
|
||||
/// Decrypted raw MIME structure.
|
||||
pub decoded_data: Vec<u8>,
|
||||
|
||||
/// Hop info for debugging.
|
||||
@@ -260,7 +255,7 @@ impl MimeMessage {
|
||||
);
|
||||
headers.retain(|k, _| {
|
||||
!is_hidden(k) || {
|
||||
headers_removed.insert(k.clone());
|
||||
headers_removed.insert(k.to_string());
|
||||
false
|
||||
}
|
||||
});
|
||||
@@ -326,12 +321,9 @@ impl MimeMessage {
|
||||
let mut from = from.context("No from in message")?;
|
||||
let private_keyring = load_self_secret_keyring(context).await?;
|
||||
|
||||
let allow_aeap = get_encrypted_mime(&mail).is_some();
|
||||
|
||||
let dkim_results = handle_authres(context, &mail, &from.addr).await?;
|
||||
|
||||
let mut gossiped_keys = Default::default();
|
||||
let mut from_is_signed = false;
|
||||
hop_info += "\n\n";
|
||||
hop_info += &dkim_results.to_string();
|
||||
|
||||
@@ -407,19 +399,37 @@ impl MimeMessage {
|
||||
None
|
||||
};
|
||||
|
||||
// The peerstate that will be used to validate the signatures.
|
||||
let mut peerstate = get_autocrypt_peerstate(
|
||||
context,
|
||||
&from.addr,
|
||||
autocrypt_header.as_ref(),
|
||||
timestamp_sent,
|
||||
allow_aeap,
|
||||
let autocrypt_fingerprint = if let Some(autocrypt_header) = &autocrypt_header {
|
||||
let fingerprint = autocrypt_header.public_key.dc_fingerprint().hex();
|
||||
let inserted = context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO public_keys (fingerprint, public_key)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (fingerprint)
|
||||
DO NOTHING",
|
||||
(&fingerprint, autocrypt_header.public_key.to_bytes()),
|
||||
)
|
||||
.await?;
|
||||
if inserted > 0 {
|
||||
info!(
|
||||
context,
|
||||
"Saved key with fingerprint {fingerprint} from the Autocrypt header"
|
||||
);
|
||||
}
|
||||
Some(fingerprint)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let public_keyring = match peerstate.is_none() && !incoming {
|
||||
true => key::load_self_public_keyring(context).await?,
|
||||
false => keyring_from_peerstate(peerstate.as_ref()),
|
||||
let public_keyring = if incoming {
|
||||
if let Some(autocrypt_header) = autocrypt_header {
|
||||
vec![autocrypt_header.public_key]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
key::load_self_public_keyring(context).await?
|
||||
};
|
||||
|
||||
let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
|
||||
@@ -482,14 +492,8 @@ impl MimeMessage {
|
||||
// but only if the mail was correctly signed. Probably it's ok to not require
|
||||
// encryption here, but let's follow the standard.
|
||||
let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip");
|
||||
gossiped_keys = update_gossip_peerstates(
|
||||
context,
|
||||
timestamp_sent,
|
||||
&from.addr,
|
||||
&recipients,
|
||||
gossip_headers,
|
||||
)
|
||||
.await?;
|
||||
gossiped_keys =
|
||||
parse_gossip_headers(context, &from.addr, &recipients, gossip_headers).await?;
|
||||
}
|
||||
|
||||
if let Some(inner_from) = inner_from {
|
||||
@@ -514,30 +518,14 @@ impl MimeMessage {
|
||||
bail!("From header is forged");
|
||||
}
|
||||
from = inner_from;
|
||||
from_is_signed = !signatures.is_empty();
|
||||
}
|
||||
}
|
||||
if signatures.is_empty() {
|
||||
Self::remove_secured_headers(&mut headers, &mut headers_removed);
|
||||
|
||||
// If it is not a read receipt, degrade encryption.
|
||||
if let (Some(peerstate), Ok(mail)) = (&mut peerstate, mail) {
|
||||
if timestamp_sent > peerstate.last_seen_autocrypt
|
||||
&& mail.ctype.mimetype != "multipart/report"
|
||||
{
|
||||
peerstate.degrade_encryption(timestamp_sent);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !is_encrypted {
|
||||
signatures.clear();
|
||||
}
|
||||
if let Some(peerstate) = &mut peerstate {
|
||||
if peerstate.prefer_encrypt != EncryptPreference::Mutual && !signatures.is_empty() {
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut parser = MimeMessage {
|
||||
parts: Vec::new(),
|
||||
@@ -549,15 +537,13 @@ impl MimeMessage {
|
||||
past_members,
|
||||
list_post,
|
||||
from,
|
||||
from_is_signed,
|
||||
incoming,
|
||||
chat_disposition_notification_to,
|
||||
autocrypt_header,
|
||||
peerstate,
|
||||
decrypting_failed: mail.is_err(),
|
||||
|
||||
// only non-empty if it was a valid autocrypt message
|
||||
signatures,
|
||||
autocrypt_fingerprint,
|
||||
gossiped_keys,
|
||||
is_forwarded: false,
|
||||
mdn_reports: Vec::new(),
|
||||
@@ -620,10 +606,7 @@ impl MimeMessage {
|
||||
parser.maybe_remove_inline_mailinglist_footer();
|
||||
parser.heuristically_parse_ndn(context).await;
|
||||
parser.parse_headers(context).await?;
|
||||
|
||||
if parser.is_mime_modified {
|
||||
parser.decoded_data = mail_raw;
|
||||
}
|
||||
|
||||
Ok(parser)
|
||||
}
|
||||
@@ -1338,15 +1321,14 @@ impl MimeMessage {
|
||||
if decoded_data.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(peerstate) = &mut self.peerstate {
|
||||
if peerstate.prefer_encrypt != EncryptPreference::Mutual
|
||||
&& mime_type.type_() == mime::APPLICATION
|
||||
|
||||
// Process attached PGP keys.
|
||||
if mime_type.type_() == mime::APPLICATION
|
||||
&& mime_type.subtype().as_str() == "pgp-keys"
|
||||
&& Self::try_set_peer_key_from_file_part(context, peerstate, decoded_data).await?
|
||||
&& Self::try_set_peer_key_from_file_part(context, decoded_data).await?
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let mut part = Part::default();
|
||||
let msg_type = if context
|
||||
.is_webxdc_file(filename, decoded_data)
|
||||
@@ -1438,10 +1420,9 @@ impl MimeMessage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether a key from the attachment was set as peer's pubkey.
|
||||
/// Returns whether a key from the attachment was saved.
|
||||
async fn try_set_peer_key_from_file_part(
|
||||
context: &Context,
|
||||
peerstate: &mut Peerstate,
|
||||
decoded_data: &[u8],
|
||||
) -> Result<bool> {
|
||||
let key = match str::from_utf8(decoded_data) {
|
||||
@@ -1455,45 +1436,30 @@ impl MimeMessage {
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"PGP key attachment is not an ASCII-armored file: {:#}", err
|
||||
"PGP key attachment is not an ASCII-armored file: {err:#}."
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
Ok((key, _)) => key,
|
||||
};
|
||||
if let Err(err) = key.verify() {
|
||||
warn!(context, "attached PGP key verification failed: {}", err);
|
||||
warn!(context, "Attached PGP key verification failed: {err:#}.");
|
||||
return Ok(false);
|
||||
}
|
||||
if !key.details.users.iter().any(|user| {
|
||||
user.id
|
||||
.id()
|
||||
.ends_with((String::from("<") + &peerstate.addr + ">").as_bytes())
|
||||
}) {
|
||||
return Ok(false);
|
||||
}
|
||||
if let Some(curr_key) = &peerstate.public_key {
|
||||
if key != *curr_key && peerstate.prefer_encrypt != EncryptPreference::Reset {
|
||||
// We don't want to break the existing Autocrypt setup. Yes, it's unlikely that a
|
||||
// user have an Autocrypt-capable MUA and also attaches a key, but if that's the
|
||||
// case, let 'em first disable Autocrypt and then change the key by attaching it.
|
||||
warn!(
|
||||
context,
|
||||
"not using attached PGP key for peer '{}' because another one is already set \
|
||||
with prefer-encrypt={}",
|
||||
peerstate.addr,
|
||||
peerstate.prefer_encrypt,
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
peerstate.public_key = Some(key);
|
||||
info!(
|
||||
context,
|
||||
"using attached PGP key for peer '{}' with prefer-encrypt=mutual", peerstate.addr,
|
||||
);
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
|
||||
let fingerprint = key.dc_fingerprint().hex();
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO public_keys (fingerprint, public_key)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (fingerprint)
|
||||
DO NOTHING",
|
||||
(&fingerprint, key.to_bytes()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!(context, "Imported PGP key {fingerprint} from attachment.");
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -1887,6 +1853,19 @@ impl MimeMessage {
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns list of fingerprints from
|
||||
/// `Chat-Group-Member-Fpr` header.
|
||||
pub fn chat_group_member_fingerprints(&self) -> Vec<Fingerprint> {
|
||||
if let Some(header) = self.get_header(HeaderDef::ChatGroupMemberFpr) {
|
||||
header
|
||||
.split_ascii_whitespace()
|
||||
.filter_map(|fpr| Fingerprint::from_str(fpr).ok())
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_header(
|
||||
@@ -1902,14 +1881,13 @@ fn remove_header(
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates.
|
||||
/// Params:
|
||||
/// from: The address which sent the message currently being parsed
|
||||
/// Parses `Autocrypt-Gossip` headers from the email,
|
||||
/// saves the keys into the `public_keys` table,
|
||||
/// and returns them in a HashMap<address, public key>.
|
||||
///
|
||||
/// Returns the set of mail recipient addresses for which valid gossip headers were found.
|
||||
async fn update_gossip_peerstates(
|
||||
/// * `from`: The address which sent the message currently being parsed
|
||||
async fn parse_gossip_headers(
|
||||
context: &Context,
|
||||
message_time: i64,
|
||||
from: &str,
|
||||
recipients: &[SingleInfo],
|
||||
gossip_headers: Vec<String>,
|
||||
@@ -1937,7 +1915,7 @@ async fn update_gossip_peerstates(
|
||||
continue;
|
||||
}
|
||||
if addr_cmp(from, &header.addr) {
|
||||
// Non-standard, but anyway we can't update the cached peerstate here.
|
||||
// Non-standard, might not be necessary to have this check here
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring gossiped \"{}\" as it equals the From address", &header.addr,
|
||||
@@ -1945,18 +1923,16 @@ async fn update_gossip_peerstates(
|
||||
continue;
|
||||
}
|
||||
|
||||
let peerstate;
|
||||
if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? {
|
||||
p.apply_gossip(&header, message_time);
|
||||
p.save_to_db(&context.sql).await?;
|
||||
peerstate = p;
|
||||
} else {
|
||||
let p = Peerstate::from_gossip(&header, message_time);
|
||||
p.save_to_db(&context.sql).await?;
|
||||
peerstate = p;
|
||||
};
|
||||
peerstate
|
||||
.handle_fingerprint_change(context, message_time)
|
||||
let fingerprint = header.public_key.dc_fingerprint().hex();
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO public_keys (fingerprint, public_key)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (fingerprint)
|
||||
DO NOTHING",
|
||||
(&fingerprint, header.public_key.to_bytes()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
gossiped_keys.insert(header.addr.to_lowercase(), header.public_key);
|
||||
|
||||
@@ -98,6 +98,11 @@ pub enum Param {
|
||||
Cmd = b'S',
|
||||
|
||||
/// For Messages
|
||||
///
|
||||
/// For "MemberRemovedFromGroup" this is the email address
|
||||
/// removed from the group.
|
||||
///
|
||||
/// For "MemberAddedToGroup" this is the email address added to the group.
|
||||
Arg = b'E',
|
||||
|
||||
/// For Messages
|
||||
|
||||
1064
src/peerstate.rs
1064
src/peerstate.rs
File diff suppressed because it is too large
Load Diff
40
src/qr.rs
40
src/qr.rs
@@ -11,9 +11,7 @@ use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub(crate) use self::dclogin_scheme::configure_from_login_qr;
|
||||
use crate::chat::ChatIdBlocked;
|
||||
use crate::config::Config;
|
||||
use crate::constants::Blocked;
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
@@ -21,7 +19,6 @@ use crate::key::Fingerprint;
|
||||
use crate::message::Message;
|
||||
use crate::net::http::post_empty;
|
||||
use crate::net::proxy::{ProxyConfig, DEFAULT_SOCKS_PORT};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::token;
|
||||
use crate::tools::validate_id;
|
||||
|
||||
@@ -44,7 +41,7 @@ pub(crate) const DCBACKUP_SCHEME_PREFIX: &str = "DCBACKUP";
|
||||
|
||||
/// Version written to Backups and Backup-QR-Codes.
|
||||
/// Imports will fail when they have a larger version.
|
||||
pub(crate) const DCBACKUP_VERSION: i32 = 2;
|
||||
pub(crate) const DCBACKUP_VERSION: i32 = 3;
|
||||
|
||||
/// Scanned QR code.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -457,15 +454,15 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
None
|
||||
};
|
||||
|
||||
// retrieve known state for this fingerprint
|
||||
let peerstate = Peerstate::from_fingerprint(context, &fingerprint)
|
||||
.await
|
||||
.context("Can't load peerstate")?;
|
||||
|
||||
if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
|
||||
let addr = ContactAddress::new(addr)?;
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledSecurejoinQrScan)
|
||||
let (contact_id, _) = Contact::add_or_lookup_ex(
|
||||
context,
|
||||
&name,
|
||||
&addr,
|
||||
&fingerprint.hex(),
|
||||
Origin::UnhandledSecurejoinQrScan,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
|
||||
|
||||
@@ -529,21 +526,18 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
})
|
||||
}
|
||||
} else if let Some(addr) = addr {
|
||||
if let Some(peerstate) = peerstate {
|
||||
let peerstate_addr = ContactAddress::new(&peerstate.addr)?;
|
||||
let fingerprint = fingerprint.hex();
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(context, &name, &peerstate_addr, Origin::UnhandledQrScan)
|
||||
.await
|
||||
.context("add_or_lookup")?;
|
||||
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request)
|
||||
.await
|
||||
.context("Failed to create (new) chat for contact")?;
|
||||
Contact::add_or_lookup_ex(context, "", &addr, &fingerprint, Origin::UnhandledQrScan)
|
||||
.await?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
|
||||
if contact.public_key(context).await?.is_some() {
|
||||
Ok(Qr::FprOk { contact_id })
|
||||
} else {
|
||||
let contact_id = Contact::lookup_id_by_addr(context, &addr, Origin::Unknown)
|
||||
.await
|
||||
.with_context(|| format!("Error looking up contact {addr:?}"))?;
|
||||
Ok(Qr::FprMismatch { contact_id })
|
||||
Ok(Qr::FprMismatch {
|
||||
contact_id: Some(contact_id),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Ok(Qr::FprWithoutAddr {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use super::*;
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{create_group_chat, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::key::DcKey;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{alice_keypair, TestContext};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_http() -> Result<()> {
|
||||
@@ -212,7 +210,7 @@ async fn test_decode_smtp() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_ideltachat_link() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = TestContext::new_alice().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
@@ -233,7 +231,7 @@ async fn test_decode_ideltachat_link() -> Result<()> {
|
||||
// see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_tolerance_for_issue_1969() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = TestContext::new_alice().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
@@ -246,7 +244,7 @@ async fn test_decode_openpgp_tolerance_for_issue_1969() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_group() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = TestContext::new_alice().await;
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||
@@ -264,7 +262,7 @@ async fn test_decode_openpgp_group() -> Result<()> {
|
||||
}
|
||||
|
||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = TestContext::new_alice().await;
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||
@@ -289,7 +287,7 @@ async fn test_decode_openpgp_group() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_invalid_token() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = TestContext::new_alice().await;
|
||||
|
||||
// Token cannot contain "/"
|
||||
let qr = check_qr(
|
||||
@@ -304,7 +302,7 @@ async fn test_decode_openpgp_invalid_token() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_secure_join() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = TestContext::new_alice().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
@@ -333,7 +331,7 @@ async fn test_decode_openpgp_secure_join() -> Result<()> {
|
||||
}
|
||||
|
||||
// Regression test
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = TestContext::new_alice().await;
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
||||
@@ -353,52 +351,29 @@ async fn test_decode_openpgp_secure_join() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_fingerprint() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let alice_contact_id = Contact::create(&ctx, "Alice", "alice@example.org")
|
||||
.await
|
||||
.context("failed to create contact")?;
|
||||
let pub_key = alice_keypair().public;
|
||||
let peerstate = Peerstate {
|
||||
addr: "alice@example.org".to_string(),
|
||||
last_seen: 1,
|
||||
last_seen_autocrypt: 1,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
public_key: Some(pub_key.clone()),
|
||||
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
gossip_key: None,
|
||||
gossip_timestamp: 0,
|
||||
gossip_key_fingerprint: None,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
backward_verified_key_id: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
assert!(
|
||||
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
|
||||
"failed to save peerstate"
|
||||
);
|
||||
let alice_contact = bob.add_or_lookup_contact(alice).await;
|
||||
let alice_contact_id = alice_contact.id;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
bob,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org",
|
||||
)
|
||||
.await?;
|
||||
if let Qr::FprMismatch { contact_id, .. } = qr {
|
||||
assert_eq!(contact_id, Some(alice_contact_id));
|
||||
assert_ne!(contact_id.unwrap(), alice_contact_id);
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
bob,
|
||||
&format!(
|
||||
"OPENPGP4FPR:{}#a=alice@example.org",
|
||||
pub_key.dc_fingerprint()
|
||||
alice_contact.fingerprint().unwrap()
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
@@ -408,14 +383,14 @@ async fn test_decode_openpgp_fingerprint() -> Result<()> {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
assert!(matches!(
|
||||
check_qr(
|
||||
&ctx.ctx,
|
||||
bob,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=bob@example.org",
|
||||
)
|
||||
.await?,
|
||||
Qr::FprMismatch { contact_id: None }
|
||||
);
|
||||
Qr::FprMismatch { .. }
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -981,10 +981,11 @@ Here's my footer -- bob@example.net"
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_reaction_multidevice() -> Result<()> {
|
||||
let alice0 = TestContext::new_alice().await;
|
||||
let alice1 = TestContext::new_alice().await;
|
||||
let bob_id = Contact::create(&alice0, "", "bob@example.net").await?;
|
||||
let chat_id = ChatId::create_for_contact(&alice0, bob_id).await?;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice0 = tcm.alice().await;
|
||||
let alice1 = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat_id = alice0.create_chat(&bob).await.id;
|
||||
|
||||
let alice0_msg_id = send_text_msg(&alice0, chat_id, "foo".to_string()).await?;
|
||||
let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await;
|
||||
|
||||
1787
src/receive_imf.rs
1787
src/receive_imf.rs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,14 @@
|
||||
//! Implementation of [SecureJoin protocols](https://securejoin.delta.chat/).
|
||||
|
||||
use anyhow::{ensure, Context as _, Error, Result};
|
||||
use deltachat_contact_tools::ContactAddress;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{self, get_chat_id_by_grpid, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT};
|
||||
use crate::contact::mark_contact_id_as_verified;
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::e2ee::ensure_secret_key_exists;
|
||||
@@ -18,13 +19,10 @@ use crate::log::{error, info, warn};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::qr::check_qr;
|
||||
use crate::securejoin::bob::JoinerProgress;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::token;
|
||||
use crate::tools::time;
|
||||
|
||||
mod bob;
|
||||
mod qrinvite;
|
||||
@@ -199,7 +197,7 @@ async fn info_chat_id(context: &Context, contact_id: ContactId) -> Result<ChatId
|
||||
Ok(chat_id_blocked.id)
|
||||
}
|
||||
|
||||
/// Checks fingerprint and marks the contact as forward verified
|
||||
/// Checks fingerprint and marks the contact as verified
|
||||
/// if fingerprint matches.
|
||||
async fn verify_sender_by_fingerprint(
|
||||
context: &Context,
|
||||
@@ -207,37 +205,11 @@ async fn verify_sender_by_fingerprint(
|
||||
contact_id: ContactId,
|
||||
) -> Result<bool> {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
|
||||
Ok(peerstate) => peerstate,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to sender peerstate for {}: {}",
|
||||
contact.get_addr(),
|
||||
err
|
||||
);
|
||||
return Ok(false);
|
||||
let is_verified = contact.fingerprint().is_some_and(|fp| &fp == fingerprint);
|
||||
if is_verified {
|
||||
mark_contact_id_as_verified(context, contact_id, ContactId::SELF).await?;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(mut peerstate) = peerstate {
|
||||
if peerstate
|
||||
.public_key_fingerprint
|
||||
.as_ref()
|
||||
.filter(|&fp| fp == fingerprint)
|
||||
.is_some()
|
||||
{
|
||||
if let Some(public_key) = &peerstate.public_key {
|
||||
let verifier = contact.get_addr().to_owned();
|
||||
peerstate.set_verified(public_key.clone(), fingerprint.clone(), verifier)?;
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
Ok(is_verified)
|
||||
}
|
||||
|
||||
/// What to do with a Secure-Join handshake message after it was handled.
|
||||
@@ -335,10 +307,21 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
|
||||
inviter_progress(context, contact_id, 300);
|
||||
|
||||
let from_addr = ContactAddress::new(&mime_message.from.addr)?;
|
||||
let autocrypt_fingerprint = mime_message.autocrypt_fingerprint.as_deref().unwrap_or("");
|
||||
let (autocrypt_contact_id, _) = Contact::add_or_lookup_ex(
|
||||
context,
|
||||
"",
|
||||
&from_addr,
|
||||
autocrypt_fingerprint,
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Alice -> Bob
|
||||
send_alice_handshake_msg(
|
||||
context,
|
||||
contact_id,
|
||||
autocrypt_contact_id,
|
||||
&format!("{}-auth-required", &step.get(..2).unwrap_or_default()),
|
||||
)
|
||||
.await
|
||||
@@ -408,26 +391,6 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
|
||||
let contact_addr = Contact::get_by_id(context, contact_id)
|
||||
.await?
|
||||
.get_addr()
|
||||
.to_owned();
|
||||
let backward_verified = true;
|
||||
let fingerprint_found = mark_peer_as_verified(
|
||||
context,
|
||||
fingerprint.clone(),
|
||||
contact_addr,
|
||||
backward_verified,
|
||||
)
|
||||
.await?;
|
||||
if !fingerprint_found {
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring {step} message because of the failure to find matching peerstate."
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
info!(context, "Fingerprint verified via Auth code.",);
|
||||
contact_id.regossip_keys(context).await?;
|
||||
ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
|
||||
@@ -499,17 +462,6 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
return Ok(HandshakeMessage::Propagate);
|
||||
}
|
||||
|
||||
// Mark peer as backward verified.
|
||||
//
|
||||
// This is needed for the case when we join a non-protected group
|
||||
// because in this case `Chat-Verified` header that otherwise
|
||||
// sets backward verification is not sent.
|
||||
if let Some(peerstate) = &mut mime_message.peerstate {
|
||||
peerstate.backward_verified_key_id =
|
||||
Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0);
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
}
|
||||
|
||||
context.emit_event(EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress: JoinerProgress::Succeeded.to_usize(),
|
||||
@@ -536,17 +488,15 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
///
|
||||
/// If we see self-sent {vc,vg}-request-with-auth,
|
||||
/// we know that we are Bob (joiner-observer)
|
||||
/// that just marked peer (Alice) as forward-verified
|
||||
/// that just marked peer (Alice) as verified
|
||||
/// either after receiving {vc,vg}-auth-required
|
||||
/// or immediately after scanning the QR-code
|
||||
/// if the key was already known.
|
||||
///
|
||||
/// If we see self-sent vc-contact-confirm or vg-member-added message,
|
||||
/// we know that we are Alice (inviter-observer)
|
||||
/// that just marked peer (Bob) as forward (and backward)-verified
|
||||
/// that just marked peer (Bob) as verified
|
||||
/// in response to correct vc-request-with-auth message.
|
||||
///
|
||||
/// In both cases we can mark the peer as forward-verified.
|
||||
pub(crate) async fn observe_securejoin_on_other_device(
|
||||
context: &Context,
|
||||
mime_message: &MimeMessage,
|
||||
@@ -568,67 +518,34 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
};
|
||||
|
||||
if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) {
|
||||
could_not_establish_secure_connection(
|
||||
warn!(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Message not encrypted correctly.",
|
||||
)
|
||||
.await?;
|
||||
"Observed SecureJoin message is not encrypted correctly."
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
|
||||
let addr = Contact::get_by_id(context, contact_id)
|
||||
.await?
|
||||
.get_addr()
|
||||
.to_lowercase();
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let addr = contact.get_addr().to_lowercase();
|
||||
|
||||
let Some(key) = mime_message.gossiped_keys.get(&addr) else {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
&format!(
|
||||
"No gossip header for '{}' at step {}, please update Delta Chat on all \
|
||||
your devices.",
|
||||
&addr, step,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
warn!(context, "No gossip header for {addr} at step {step}.");
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
|
||||
let Some(mut peerstate) = Peerstate::from_addr(context, &addr).await? else {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
&format!("No peerstate in db for '{}' at step {}", &addr, step),
|
||||
)
|
||||
.await?;
|
||||
let Some(contact_fingerprint) = contact.fingerprint() else {
|
||||
// Not a key-contact, should not happen.
|
||||
warn!(context, "Contact does not have a fingerprint.");
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
|
||||
let Some(fingerprint) = peerstate.gossip_key_fingerprint.clone() else {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
&format!(
|
||||
"No gossip key fingerprint in db for '{}' at step {}",
|
||||
&addr, step,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
if key.dc_fingerprint() != contact_fingerprint {
|
||||
// Fingerprint does not match, ignore.
|
||||
warn!(context, "Fingerprint does not match.");
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
peerstate.set_verified(key.clone(), fingerprint, addr)?;
|
||||
if matches!(step, "vg-member-added" | "vc-contact-confirm") {
|
||||
peerstate.backward_verified_key_id =
|
||||
Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0);
|
||||
}
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
|
||||
mark_contact_id_as_verified(context, contact_id, ContactId::SELF).await?;
|
||||
|
||||
ChatId::set_protection_for_contact(context, contact_id, mime_message.timestamp_sent).await?;
|
||||
|
||||
@@ -675,50 +592,6 @@ async fn secure_connection_established(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn could_not_establish_secure_connection(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
chat_id: ChatId,
|
||||
details: &str,
|
||||
) -> Result<()> {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let mut msg = stock_str::contact_not_verified(context, &contact).await;
|
||||
msg += " (";
|
||||
msg += details;
|
||||
msg += ")";
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
warn!(
|
||||
context,
|
||||
"StockMessage::ContactNotVerified posted to 1:1 chat ({})", details
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to mark peer with provided key fingerprint as verified.
|
||||
///
|
||||
/// Returns true if such key was found, false otherwise.
|
||||
async fn mark_peer_as_verified(
|
||||
context: &Context,
|
||||
fingerprint: Fingerprint,
|
||||
verifier: String,
|
||||
backward_verified: bool,
|
||||
) -> Result<bool> {
|
||||
let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, &fingerprint).await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
let Some(ref public_key) = peerstate.public_key else {
|
||||
return Ok(false);
|
||||
};
|
||||
peerstate.set_verified(public_key.clone(), fingerprint, verifier)?;
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
if backward_verified {
|
||||
peerstate.backward_verified_key_id =
|
||||
Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0);
|
||||
}
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/* ******************************************************************************
|
||||
* Tools: Misc.
|
||||
******************************************************************************/
|
||||
|
||||
@@ -5,11 +5,11 @@ use anyhow::{Context as _, Result};
|
||||
use super::qrinvite::QrInvite;
|
||||
use super::HandshakeMessage;
|
||||
use crate::chat::{self, is_contact_in_chat, ChatId, ProtectionStatus};
|
||||
use crate::constants::{self, Blocked, Chattype};
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::contact::Origin;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{load_self_public_key, DcKey};
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::log::info;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
@@ -57,11 +57,18 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
|
||||
// Now start the protocol and initialise the state.
|
||||
{
|
||||
let peer_verified =
|
||||
verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id())
|
||||
let has_key = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM public_keys WHERE fingerprint=?",
|
||||
(invite.fingerprint().hex(),),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if peer_verified {
|
||||
if has_key
|
||||
&& verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id())
|
||||
.await?
|
||||
{
|
||||
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
|
||||
info!(context, "Taking securejoin protocol shortcut");
|
||||
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth)
|
||||
@@ -130,7 +137,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
chat_id.spawn_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT);
|
||||
}
|
||||
Ok(chat_id)
|
||||
}
|
||||
@@ -268,8 +274,8 @@ pub(crate) async fn send_handshake_message(
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
|
||||
// Sends our own fingerprint in the Secure-Join-Fingerprint header.
|
||||
let bob_fp = load_self_public_key(context).await?.dc_fingerprint();
|
||||
msg.param.set(Param::Arg3, bob_fp.hex());
|
||||
let bob_fp = self_fingerprint(context).await?;
|
||||
msg.param.set(Param::Arg3, bob_fp);
|
||||
|
||||
// Sends the grpid in the Secure-Join-Group header.
|
||||
//
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use deltachat_contact_tools::{ContactAddress, EmailAddress};
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{remove_contact_from_chat, CantSendReason};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::{self, Chattype};
|
||||
use crate::imex::{imex, ImexMode};
|
||||
use crate::constants::Chattype;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::{self, chat_protection_enabled};
|
||||
use crate::test_utils::{get_chat_msg, TimeShiftFalsePositiveNote};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::test_utils::{
|
||||
get_chat_msg, TestContext, TestContextManager, TimeShiftFalsePositiveNote,
|
||||
};
|
||||
use crate::tools::SystemTime;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
@@ -18,7 +18,6 @@ enum SetupContactCase {
|
||||
Normal,
|
||||
CheckProtectionTimestamp,
|
||||
WrongAliceGossip,
|
||||
SecurejoinWaitTimeout,
|
||||
AliceIsBot,
|
||||
AliceHasName,
|
||||
}
|
||||
@@ -38,11 +37,6 @@ async fn test_setup_contact_wrong_alice_gossip() {
|
||||
test_setup_contact_ex(SetupContactCase::WrongAliceGossip).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_wait_timeout() {
|
||||
test_setup_contact_ex(SetupContactCase::SecurejoinWaitTimeout).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_alice_is_bot() {
|
||||
test_setup_contact_ex(SetupContactCase::AliceIsBot).await
|
||||
@@ -95,24 +89,26 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
0
|
||||
);
|
||||
|
||||
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
||||
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
||||
tcm.section("Step 1: Generate QR-code, ChatId(0) indicates setup-contact");
|
||||
let qr = get_securejoin_qr(&alice, None).await.unwrap();
|
||||
// We want Bob to learn Alice's name from their messages, not from the QR code.
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Alice Exampleorg"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 2: Bob scans QR-code, sends vc-request
|
||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
tcm.section("Step 2: Bob scans QR-code, sends vc-request");
|
||||
let bob_chat_id = join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||
1
|
||||
);
|
||||
let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let bob_chat = Chat::load_from_db(&bob, bob_chat_id).await.unwrap();
|
||||
assert_eq!(
|
||||
bob_chat.why_cant_send(&bob).await.unwrap(),
|
||||
Some(CantSendReason::MissingKey)
|
||||
);
|
||||
let contact_alice_id = bob.add_or_lookup_contact_no_key(&alice).await.id;
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||
assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap());
|
||||
@@ -122,7 +118,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
|
||||
assert!(!msg.header_exists(HeaderDef::AutoSubmitted));
|
||||
|
||||
// Step 3: Alice receives vc-request, sends vc-auth-required
|
||||
tcm.section("Step 3: Alice receives vc-request, sends vc-auth-required");
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&alice, 0, None, None)
|
||||
@@ -141,20 +137,14 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-auth-required"
|
||||
);
|
||||
let bob_chat = bob.get_chat(&alice).await;
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), false);
|
||||
assert_eq!(
|
||||
bob_chat.why_cant_send(&bob).await.unwrap(),
|
||||
Some(CantSendReason::SecurejoinWait)
|
||||
);
|
||||
if case == SetupContactCase::SecurejoinWaitTimeout {
|
||||
SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT));
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||
}
|
||||
|
||||
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
|
||||
let bob_chat = bob.get_chat(&alice).await;
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||
|
||||
tcm.section("Step 4: Bob receives vc-auth-required, sends vc-request-with-auth");
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
let bob_chat = bob.get_chat(&alice).await;
|
||||
assert_eq!(bob_chat.why_cant_send(&bob).await.unwrap(), None);
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
@@ -167,12 +157,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(contact_id, contact_alice_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
@@ -193,13 +178,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
"vc-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = load_self_public_key(&bob.ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.dc_fingerprint();
|
||||
let bob_fp = self_fingerprint(&bob).await.unwrap();
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp
|
||||
);
|
||||
|
||||
if case == SetupContactCase::WrongAliceGossip {
|
||||
@@ -208,12 +190,12 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
.gossiped_keys
|
||||
.insert(alice_addr.to_string(), wrong_pubkey)
|
||||
.unwrap();
|
||||
let contact_bob = alice.add_or_lookup_email_contact(&bob).await;
|
||||
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
|
||||
let handshake_msg = handle_securejoin_handshake(&alice, &mut msg, contact_bob.id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false);
|
||||
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false);
|
||||
|
||||
msg.gossiped_keys
|
||||
.insert(alice_addr.to_string(), alice_pubkey)
|
||||
@@ -222,31 +204,25 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
||||
assert!(contact_bob.is_verified(&alice.ctx).await.unwrap());
|
||||
assert!(contact_bob.is_verified(&alice).await.unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false);
|
||||
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
|
||||
let contact_bob_id = contact_bob.id;
|
||||
assert_eq!(contact_bob.is_key_contact(), true);
|
||||
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false);
|
||||
assert_eq!(contact_bob.get_authname(), "");
|
||||
|
||||
if case == SetupContactCase::CheckProtectionTimestamp {
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
}
|
||||
|
||||
// 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;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true);
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), true);
|
||||
let contact_bob = Contact::get_by_id(&alice, contact_bob_id).await.unwrap();
|
||||
assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
|
||||
assert!(contact_bob.get_name().is_empty());
|
||||
assert_eq!(contact_bob.is_bot(), false);
|
||||
@@ -283,7 +259,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
.unwrap();
|
||||
match case {
|
||||
SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"),
|
||||
_ => assert_eq!(contact_alice.get_authname(), ""),
|
||||
_ => assert_eq!(contact_alice.get_authname(), "Alice Exampleorg"),
|
||||
};
|
||||
|
||||
// Check Alice sent the right message to Bob.
|
||||
@@ -297,8 +273,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
"vc-contact-confirm"
|
||||
);
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), false);
|
||||
// Bob has verified Alice already.
|
||||
//
|
||||
// Alice may not have verified Bob yet.
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true);
|
||||
|
||||
// Step 7: Bob receives vc-contact-confirm
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
@@ -310,35 +288,12 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
assert!(contact_alice.get_name().is_empty());
|
||||
assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot);
|
||||
|
||||
if case != SetupContactCase::SecurejoinWaitTimeout {
|
||||
// Later we check that the timeout message isn't added to the already protected chat.
|
||||
SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT + 1));
|
||||
assert_eq!(
|
||||
bob_chat
|
||||
.check_securejoin_wait(&bob, constants::SECUREJOIN_WAIT_TIMEOUT)
|
||||
.await
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
// Check Bob got expected info messages in his 1:1 chat.
|
||||
let msg_cnt: usize = match case {
|
||||
SetupContactCase::SecurejoinWaitTimeout => 3,
|
||||
_ => 2,
|
||||
};
|
||||
let msg_cnt = 2;
|
||||
let mut i = 0..msg_cnt;
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
|
||||
if case == SetupContactCase::SecurejoinWaitTimeout {
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(
|
||||
msg.get_text(),
|
||||
stock_str::securejoin_takes_longer(&bob).await
|
||||
);
|
||||
}
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), chat_protection_enabled(&bob).await);
|
||||
@@ -354,37 +309,18 @@ async fn test_setup_contact_bad_qr() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// Ensure Bob knows Alice_FP
|
||||
let alice_pubkey = load_self_public_key(&alice.ctx).await?;
|
||||
let peerstate = Peerstate {
|
||||
addr: "alice@example.org".into(),
|
||||
last_seen: 10,
|
||||
last_seen_autocrypt: 10,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
public_key: Some(alice_pubkey.clone()),
|
||||
public_key_fingerprint: Some(alice_pubkey.dc_fingerprint()),
|
||||
gossip_key: Some(alice_pubkey.clone()),
|
||||
gossip_timestamp: 10,
|
||||
gossip_key_fingerprint: Some(alice_pubkey.dc_fingerprint()),
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
backward_verified_key_id: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
peerstate.save_to_db(&bob.ctx.sql).await?;
|
||||
let alice_contact_id = bob.add_or_lookup_contact_id(alice).await;
|
||||
|
||||
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
||||
let qr = get_securejoin_qr(&alice.ctx, None).await?;
|
||||
tcm.section("Step 1: Generate QR-code");
|
||||
// `None` indicates setup-contact.
|
||||
let qr = get_securejoin_qr(alice, None).await?;
|
||||
|
||||
// Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request
|
||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
tcm.section("Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request");
|
||||
join_securejoin(bob, &qr).await.unwrap();
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
let event = bob
|
||||
@@ -396,11 +332,6 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
@@ -416,26 +347,19 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
||||
"vc-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint();
|
||||
let bob_fp = load_self_public_key(bob).await?.dc_fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
);
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let (contact_bob_id, _modified) = Contact::add_or_lookup(
|
||||
&alice.ctx,
|
||||
"",
|
||||
&ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false);
|
||||
let contact_bob = alice.add_or_lookup_contact_no_key(bob).await;
|
||||
assert_eq!(contact_bob.is_verified(alice).await?, false);
|
||||
|
||||
// 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;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true);
|
||||
assert_eq!(contact_bob.is_verified(alice).await?, true);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
@@ -445,18 +369,16 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
||||
"vc-contact-confirm"
|
||||
);
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, false);
|
||||
// Bob has verified Alice already.
|
||||
let contact_alice = bob.add_or_lookup_contact_no_key(alice).await;
|
||||
assert_eq!(contact_alice.is_verified(bob).await?, true);
|
||||
|
||||
// Step 7: Bob receives vc-contact-confirm
|
||||
// Alice confirms that Bob is now verified.
|
||||
//
|
||||
// This does not change anything for Bob.
|
||||
tcm.section("Step 7: Bob receives vc-contact-confirm");
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true);
|
||||
assert_eq!(contact_alice.is_verified(bob).await?, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -503,15 +425,13 @@ async fn test_secure_join() -> Result<()> {
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
|
||||
let alice_chatid =
|
||||
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
|
||||
chat::create_group_chat(&alice, ProtectionStatus::Protected, "the chat").await?;
|
||||
|
||||
// Step 1: Generate QR-code, secure-join implied by chatid
|
||||
let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid))
|
||||
.await
|
||||
.unwrap();
|
||||
tcm.section("Step 1: Generate QR-code, secure-join implied by chatid");
|
||||
let qr = get_securejoin_qr(&alice, Some(alice_chatid)).await.unwrap();
|
||||
|
||||
// Step 2: Bob scans QR-code, sends vg-request
|
||||
let bob_chatid = join_securejoin(&bob.ctx, &qr).await?;
|
||||
tcm.section("Step 2: Bob scans QR-code, sends vg-request");
|
||||
let bob_chatid = join_securejoin(&bob, &qr).await?;
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
@@ -533,7 +453,7 @@ async fn test_secure_join() -> Result<()> {
|
||||
// is only sent in `vg-request-with-auth` for compatibility.
|
||||
assert!(!msg.header_exists(HeaderDef::SecureJoinGroup));
|
||||
|
||||
// Step 3: Alice receives vg-request, sends vg-auth-required
|
||||
tcm.section("Step 3: Alice receives vg-request, sends vg-auth-required");
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
@@ -545,10 +465,12 @@ async fn test_secure_join() -> Result<()> {
|
||||
"vg-auth-required"
|
||||
);
|
||||
|
||||
// Step 4: Bob receives vg-auth-required, sends vg-request-with-auth
|
||||
tcm.section("Step 4: Bob receives vg-auth-required, sends vg-request-with-auth");
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
|
||||
let contact_alice_id = bob.add_or_lookup_contact_no_key(&alice).await.id;
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
let event = bob
|
||||
.evtracker
|
||||
@@ -559,12 +481,7 @@ async fn test_secure_join() -> Result<()> {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(contact_id, contact_alice_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
@@ -579,22 +496,19 @@ async fn test_secure_join() -> Result<()> {
|
||||
"vg-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint();
|
||||
let bob_fp = self_fingerprint(&bob).await?;
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp
|
||||
);
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
||||
.await?
|
||||
.expect("Contact not found");
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false);
|
||||
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice).await?, false);
|
||||
|
||||
// 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;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true);
|
||||
assert_eq!(contact_bob.is_verified(&alice).await?, true);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
@@ -629,20 +543,17 @@ async fn test_secure_join() -> Result<()> {
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
}
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, false);
|
||||
// Bob has verified Alice already.
|
||||
//
|
||||
// Alice may not have verified Bob yet.
|
||||
let contact_alice = bob.add_or_lookup_contact_no_key(&alice).await;
|
||||
assert_eq!(contact_alice.is_verified(&bob).await?, true);
|
||||
|
||||
// Step 7: Bob receives vg-member-added
|
||||
tcm.section("Step 7: Bob receives vg-member-added");
|
||||
bob.recv_msg(&sent).await;
|
||||
{
|
||||
// Bob has Alice verified, message shows up in the group chat.
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true);
|
||||
assert_eq!(contact_alice.is_verified(&bob).await?, true);
|
||||
let chat = bob.get_chat(&alice).await;
|
||||
assert_eq!(
|
||||
chat.blocked,
|
||||
@@ -728,8 +639,10 @@ async fn test_unknown_sender() -> Result<()> {
|
||||
}
|
||||
|
||||
/// Tests that Bob gets Alice as verified
|
||||
/// if `vc-contact-confirm` is lost but Alice then sends
|
||||
/// a message to Bob in a verified 1:1 chat with a `Chat-Verified` header.
|
||||
/// 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)]
|
||||
async fn test_lost_contact_confirm() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -741,7 +654,7 @@ async fn test_lost_contact_confirm() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
||||
let qr = get_securejoin_qr(&alice, None).await.unwrap();
|
||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
|
||||
// vc-request
|
||||
@@ -757,94 +670,17 @@ async fn test_lost_contact_confirm() {
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
// Alice has Bob verified now.
|
||||
let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true);
|
||||
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), true);
|
||||
|
||||
// Alice sends vc-contact-confirm, but it gets lost.
|
||||
let _sent_vc_contact_confirm = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id = Contact::lookup_id_by_addr(&bob, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap();
|
||||
assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), false);
|
||||
|
||||
// Alice sends a text message to Bob.
|
||||
let received_hello = tcm.send_recv(&alice, &bob, "Hello!").await;
|
||||
let chat_id = received_hello.chat_id;
|
||||
let chat = Chat::load_from_db(&bob, chat_id).await.unwrap();
|
||||
assert_eq!(chat.is_protected(), true);
|
||||
|
||||
// Received text message in a verified 1:1 chat results in backward verification
|
||||
// and Bob now marks alice as verified.
|
||||
let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap();
|
||||
// Bob has alice as verified too, even though vc-contact-confirm is lost.
|
||||
let contact_alice = bob.add_or_lookup_contact_no_key(&alice).await;
|
||||
assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), true);
|
||||
}
|
||||
|
||||
/// An unencrypted message with already known Autocrypt key, but sent from another address,
|
||||
/// means that it's rather a new contact sharing the same key than the existing one changed its
|
||||
/// address, otherwise it would already have our key to encrypt.
|
||||
///
|
||||
/// This is a regression test for a bug where DC wrongly executed AEAP in this case.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_shared_bobs_key() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_addr = &bob.get_config(Config::Addr).await?.unwrap();
|
||||
|
||||
tcm.execute_securejoin(bob, alice).await;
|
||||
|
||||
let export_dir = tempfile::tempdir().unwrap();
|
||||
imex(bob, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
|
||||
let bob2 = &TestContext::new().await;
|
||||
let bob2_addr = "bob2@example.net";
|
||||
bob2.configure_addr(bob2_addr).await;
|
||||
imex(bob2, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
|
||||
|
||||
tcm.execute_securejoin(bob2, alice).await;
|
||||
|
||||
let bob3 = &TestContext::new().await;
|
||||
let bob3_addr = "bob3@example.net";
|
||||
bob3.configure_addr(bob3_addr).await;
|
||||
imex(bob3, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
|
||||
let chat = bob3.create_email_chat(alice).await;
|
||||
let sent = bob3.send_text(chat.id, "hi Alice!").await;
|
||||
let msg = alice.recv_msg(&sent).await;
|
||||
assert!(!msg.get_showpadlock());
|
||||
let chat = alice.create_email_chat(bob3).await;
|
||||
let sent = alice.send_text(chat.id, "hi Bob3!").await;
|
||||
let msg = bob3.recv_msg(&sent).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
let mut bob_ids = HashSet::new();
|
||||
bob_ids.insert(
|
||||
Contact::lookup_id_by_addr(alice, bob_addr, Origin::Unknown)
|
||||
.await?
|
||||
.unwrap(),
|
||||
);
|
||||
bob_ids.insert(
|
||||
Contact::lookup_id_by_addr(alice, bob2_addr, Origin::Unknown)
|
||||
.await?
|
||||
.unwrap(),
|
||||
);
|
||||
bob_ids.insert(
|
||||
Contact::lookup_id_by_addr(alice, bob3_addr, Origin::Unknown)
|
||||
.await?
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(bob_ids.len(), 3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests Bob joining two groups by scanning two QR codes
|
||||
/// from the same Alice at the same time.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -978,7 +814,7 @@ async fn test_wrong_auth_token() -> Result<()> {
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
let alice_bob_contact = alice.add_or_lookup_contact(bob).await;
|
||||
assert!(!alice_bob_contact.is_forward_verified(alice).await?);
|
||||
assert!(!alice_bob_contact.is_verified(alice).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
57
src/sql.rs
57
src/sql.rs
@@ -8,7 +8,7 @@ use rusqlite::{config::DbConfig, types::ValueRef, Connection, OpenFlags, Row};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, add_device_msg, update_device_icon, update_saved_messages_icon};
|
||||
use crate::chat::add_device_msg;
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::context::Context;
|
||||
@@ -22,7 +22,6 @@ use crate::net::dns::prune_dns_cache;
|
||||
use crate::net::http::http_cache_cleanup;
|
||||
use crate::net::prune_connection_history;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{delete_file, time, SystemTime};
|
||||
|
||||
@@ -191,7 +190,19 @@ impl Sql {
|
||||
async fn try_open(&self, context: &Context, dbfile: &Path, passphrase: String) -> Result<()> {
|
||||
*self.pool.write().await = Some(Self::new_pool(dbfile, passphrase.to_string())?);
|
||||
|
||||
self.run_migrations(context).await?;
|
||||
if let Err(e) = self.run_migrations(context).await {
|
||||
error!(context, "Running migrations failed: {e:#}");
|
||||
// Emiting an error event probably doesn't work
|
||||
// because we are in the process of opening the context,
|
||||
// so there is no event emitter yet.
|
||||
// So, try to report the error in other ways:
|
||||
eprintln!("Running migrations failed: {e:#}");
|
||||
context.set_migration_error(&format!("Updating Delta Chat failed. Please send this message to the Delta Chat developers, either at delta@merlinux.eu or at https://support.delta.chat.\n\n{e:#}"));
|
||||
// We can't simply close the db for two reasons:
|
||||
// a. backup export would fail
|
||||
// b. The UI would think that the account is unconfigured (because `is_configured()` fails)
|
||||
// and remove the account when the user presses "Back"
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -202,41 +213,14 @@ impl Sql {
|
||||
// this should be done before updates that use high-level objects that
|
||||
// rely themselves on the low-level structure.
|
||||
|
||||
let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) =
|
||||
migrations::run(context, self)
|
||||
// `update_icons` is not used anymore, since it's not necessary anymore to "update" icons:
|
||||
let (_update_icons, disable_server_delete, recode_avatar) = migrations::run(context, self)
|
||||
.await
|
||||
.context("failed to run migrations")?;
|
||||
|
||||
// (2) updates that require high-level objects
|
||||
// the structure is complete now and all objects are usable
|
||||
|
||||
if recalc_fingerprints {
|
||||
info!(context, "[migration] recalc fingerprints");
|
||||
let addrs = self
|
||||
.query_map(
|
||||
"SELECT addr FROM acpeerstates;",
|
||||
(),
|
||||
|row| row.get::<_, String>(0),
|
||||
|addrs| {
|
||||
addrs
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
for addr in &addrs {
|
||||
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? {
|
||||
peerstate.recalc_fingerprint();
|
||||
peerstate.save_to_db(self).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if update_icons {
|
||||
update_saved_messages_icon(context).await?;
|
||||
update_device_icon(context).await?;
|
||||
}
|
||||
|
||||
if disable_server_delete {
|
||||
// We now always watch all folders and delete messages there if delete_server is enabled.
|
||||
// So, for people who have delete_server enabled, disable it and add a hint to the devicechat:
|
||||
@@ -287,10 +271,7 @@ impl Sql {
|
||||
}
|
||||
|
||||
let passphrase_nonempty = !passphrase.is_empty();
|
||||
if let Err(err) = self.try_open(context, &self.dbfile, passphrase).await {
|
||||
self.close().await;
|
||||
return Err(err);
|
||||
}
|
||||
self.try_open(context, &self.dbfile, passphrase).await?;
|
||||
info!(context, "Opened database {:?}.", self.dbfile);
|
||||
*self.is_encrypted.write().await = Some(passphrase_nonempty);
|
||||
|
||||
@@ -301,10 +282,6 @@ impl Sql {
|
||||
{
|
||||
set_debug_logging_xdc(context, Some(MsgId::new(xdc_id))).await?;
|
||||
}
|
||||
chat::resume_securejoin_wait(context)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
//! Migrations module.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use pgp::composed::SignedPublicKey;
|
||||
use rusqlite::OptionalExtension;
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -9,6 +15,7 @@ use crate::configure::EnteredLoginParam;
|
||||
use crate::constants::ShowEmails;
|
||||
use crate::context::Context;
|
||||
use crate::imap;
|
||||
use crate::key::DcKey;
|
||||
use crate::log::{info, warn};
|
||||
use crate::login_param::ConfiguredLoginParam;
|
||||
use crate::message::MsgId;
|
||||
@@ -20,8 +27,12 @@ const DBVERSION: i32 = 68;
|
||||
const VERSION_CFG: &str = "dbversion";
|
||||
const TABLES: &str = include_str!("./tables.sql");
|
||||
|
||||
pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool)> {
|
||||
let mut recalc_fingerprints = false;
|
||||
#[cfg(test)]
|
||||
tokio::task_local! {
|
||||
static STOP_MIGRATIONS_AT: i32;
|
||||
}
|
||||
|
||||
pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool)> {
|
||||
let mut exists_before_update = false;
|
||||
let mut dbversion_before_update = DBVERSION;
|
||||
|
||||
@@ -159,7 +170,6 @@ CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);"#,
|
||||
34,
|
||||
)
|
||||
.await?;
|
||||
recalc_fingerprints = true;
|
||||
}
|
||||
if dbversion < 39 {
|
||||
sql.execute_migration(
|
||||
@@ -1225,6 +1235,18 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 132)?;
|
||||
if dbversion < migration_version {
|
||||
let start = Instant::now();
|
||||
sql.execute_migration_transaction(|t| migrate_key_contacts(context, t), migration_version)
|
||||
.await?;
|
||||
info!(
|
||||
context,
|
||||
"key-contacts migration took {:?} in total.",
|
||||
start.elapsed()
|
||||
);
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
@@ -1239,12 +1261,610 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
|
||||
}
|
||||
info!(context, "Database version: v{new_version}.");
|
||||
|
||||
Ok((update_icons, disable_server_delete, recode_avatar))
|
||||
}
|
||||
|
||||
fn migrate_key_contacts(
|
||||
context: &Context,
|
||||
transaction: &mut rusqlite::Transaction<'_>,
|
||||
) -> std::result::Result<(), anyhow::Error> {
|
||||
info!(context, "Starting key-contact transition.");
|
||||
|
||||
// =============================== Step 1: ===============================
|
||||
// Alter tables
|
||||
transaction.execute_batch(
|
||||
"ALTER TABLE contacts ADD COLUMN fingerprint TEXT NOT NULL DEFAULT '';
|
||||
|
||||
-- Verifier is an ID of the verifier contact.
|
||||
-- 0 if the contact is not verified.
|
||||
ALTER TABLE contacts ADD COLUMN verifier INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE INDEX contacts_fingerprint_index ON contacts (fingerprint);
|
||||
|
||||
CREATE TABLE public_keys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
fingerprint TEXT NOT NULL UNIQUE, -- Upper-case fingerprint of the key.
|
||||
public_key BLOB NOT NULL -- Binary key, not ASCII-armored
|
||||
) STRICT;
|
||||
CREATE INDEX public_key_index ON public_keys (fingerprint);
|
||||
|
||||
INSERT OR IGNORE INTO public_keys (fingerprint, public_key)
|
||||
SELECT public_key_fingerprint, public_key FROM acpeerstates
|
||||
WHERE public_key_fingerprint IS NOT NULL AND public_key IS NOT NULL;
|
||||
|
||||
INSERT OR IGNORE INTO public_keys (fingerprint, public_key)
|
||||
SELECT gossip_key_fingerprint, gossip_key FROM acpeerstates
|
||||
WHERE gossip_key_fingerprint IS NOT NULL AND gossip_key IS NOT NULL;
|
||||
|
||||
INSERT OR IGNORE INTO public_keys (fingerprint, public_key)
|
||||
SELECT verified_key_fingerprint, verified_key FROM acpeerstates
|
||||
WHERE verified_key_fingerprint IS NOT NULL AND verified_key IS NOT NULL;
|
||||
|
||||
INSERT OR IGNORE INTO public_keys (fingerprint, public_key)
|
||||
SELECT secondary_verified_key_fingerprint, secondary_verified_key FROM acpeerstates
|
||||
WHERE secondary_verified_key_fingerprint IS NOT NULL AND secondary_verified_key IS NOT NULL;",
|
||||
)
|
||||
.context("Creating key-contact tables")?;
|
||||
|
||||
let Some(self_addr): Option<String> = transaction
|
||||
.query_row(
|
||||
"SELECT value FROM config WHERE keyname='configured_addr'",
|
||||
(),
|
||||
|row| row.get(0),
|
||||
)
|
||||
.optional()
|
||||
.context("Step 0")?
|
||||
else {
|
||||
info!(
|
||||
context,
|
||||
"Not yet configured, no need to migrate key-contacts"
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// =============================== Step 2: ===============================
|
||||
// Create up to 3 new contacts for every contact that has a peerstate:
|
||||
// one from the Autocrypt key fingerprint, one from the verified key fingerprint,
|
||||
// one from the secondary verified key fingerprint.
|
||||
// In the process, build maps from old contact id to new contact id:
|
||||
// one that maps to Autocrypt key-contact, one that maps to verified key-contact.
|
||||
let mut autocrypt_key_contacts: BTreeMap<u32, u32> = BTreeMap::new();
|
||||
let mut autocrypt_key_contacts_with_reset_peerstate: BTreeMap<u32, u32> = BTreeMap::new();
|
||||
let mut verified_key_contacts: BTreeMap<u32, u32> = BTreeMap::new();
|
||||
{
|
||||
// This maps from the verified contact to the original contact id of the verifier.
|
||||
// It can't map to the verified key contact id, because at the time of constructing
|
||||
// this map, not all key-contacts are in the database.
|
||||
let mut verifications: BTreeMap<u32, u32> = BTreeMap::new();
|
||||
|
||||
let mut load_contacts_stmt = transaction
|
||||
.prepare(
|
||||
"SELECT c.id, c.name, c.addr, c.origin, c.blocked, c.last_seen,
|
||||
c.authname, c.param, c.status, c.is_bot, c.selfavatar_sent,
|
||||
IFNULL(p.public_key, p.gossip_key),
|
||||
p.verified_key, IFNULL(p.verifier, ''),
|
||||
p.secondary_verified_key, p.secondary_verifier, p.prefer_encrypted
|
||||
FROM contacts c
|
||||
INNER JOIN acpeerstates p ON c.addr=p.addr
|
||||
WHERE c.id > 9
|
||||
ORDER BY p.last_seen DESC",
|
||||
)
|
||||
.context("Step 2")?;
|
||||
|
||||
let all_address_contacts: rusqlite::Result<Vec<_>> = load_contacts_stmt
|
||||
.query_map((), |row| {
|
||||
let id: i64 = row.get(0)?;
|
||||
let name: String = row.get(1)?;
|
||||
let addr: String = row.get(2)?;
|
||||
let origin: i64 = row.get(3)?;
|
||||
let blocked: Option<bool> = row.get(4)?;
|
||||
let last_seen: i64 = row.get(5)?;
|
||||
let authname: String = row.get(6)?;
|
||||
let param: String = row.get(7)?;
|
||||
let status: Option<String> = row.get(8)?;
|
||||
let is_bot: bool = row.get(9)?;
|
||||
let selfavatar_sent: i64 = row.get(10)?;
|
||||
let autocrypt_key = row
|
||||
.get(11)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
|
||||
let verified_key = row
|
||||
.get(12)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
|
||||
let verifier: String = row.get(13)?;
|
||||
let secondary_verified_key = row
|
||||
.get(12)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
|
||||
let secondary_verifier: String = row.get(15)?;
|
||||
let prefer_encrypt: u8 = row.get(16)?;
|
||||
Ok((
|
||||
recalc_fingerprints,
|
||||
update_icons,
|
||||
disable_server_delete,
|
||||
recode_avatar,
|
||||
id,
|
||||
name,
|
||||
addr,
|
||||
origin,
|
||||
blocked,
|
||||
last_seen,
|
||||
authname,
|
||||
param,
|
||||
status,
|
||||
is_bot,
|
||||
selfavatar_sent,
|
||||
autocrypt_key,
|
||||
verified_key,
|
||||
verifier,
|
||||
secondary_verified_key,
|
||||
secondary_verifier,
|
||||
prefer_encrypt,
|
||||
))
|
||||
})
|
||||
.context("Step 3")?
|
||||
.collect();
|
||||
|
||||
let mut insert_contact_stmt = transaction
|
||||
.prepare(
|
||||
"INSERT INTO contacts (name, addr, origin, blocked, last_seen,
|
||||
authname, param, status, is_bot, selfavatar_sent, fingerprint)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?)",
|
||||
)
|
||||
.context("Step 4")?;
|
||||
let mut fingerprint_to_id_stmt = transaction
|
||||
.prepare("SELECT id FROM contacts WHERE fingerprint=? AND id>9")
|
||||
.context("Step 5")?;
|
||||
let mut original_contact_id_from_addr_stmt = transaction
|
||||
.prepare("SELECT id FROM contacts WHERE addr=? AND fingerprint='' AND id>9")
|
||||
.context("Step 6")?;
|
||||
|
||||
for row in all_address_contacts? {
|
||||
let (
|
||||
original_id,
|
||||
name,
|
||||
addr,
|
||||
origin,
|
||||
blocked,
|
||||
last_seen,
|
||||
authname,
|
||||
param,
|
||||
status,
|
||||
is_bot,
|
||||
selfavatar_sent,
|
||||
autocrypt_key,
|
||||
verified_key,
|
||||
verifier,
|
||||
secondary_verified_key,
|
||||
secondary_verifier,
|
||||
prefer_encrypt,
|
||||
) = row;
|
||||
let mut insert_contact = |key: SignedPublicKey| -> Result<u32> {
|
||||
let fingerprint = key.dc_fingerprint().hex();
|
||||
let existing_contact_id: Option<u32> = fingerprint_to_id_stmt
|
||||
.query_row((&fingerprint,), |row| row.get(0))
|
||||
.optional()
|
||||
.context("Step 7")?;
|
||||
if let Some(existing_contact_id) = existing_contact_id {
|
||||
return Ok(existing_contact_id);
|
||||
}
|
||||
insert_contact_stmt
|
||||
.execute((
|
||||
&name,
|
||||
&addr,
|
||||
origin,
|
||||
blocked,
|
||||
last_seen,
|
||||
&authname,
|
||||
¶m,
|
||||
&status,
|
||||
is_bot,
|
||||
selfavatar_sent,
|
||||
fingerprint.clone(),
|
||||
))
|
||||
.context("Step 8")?;
|
||||
let id = transaction
|
||||
.last_insert_rowid()
|
||||
.try_into()
|
||||
.context("Step 9")?;
|
||||
info!(
|
||||
context,
|
||||
"Inserted new contact id={id} name='{name}' addr='{addr}' fingerprint={fingerprint}"
|
||||
);
|
||||
Ok(id)
|
||||
};
|
||||
let mut original_contact_id_from_addr = |addr: &str, default: u32| -> Result<u32> {
|
||||
if addr_cmp(addr, &self_addr) {
|
||||
Ok(1) // ContactId::SELF
|
||||
} else if addr.is_empty() {
|
||||
Ok(default)
|
||||
} else {
|
||||
original_contact_id_from_addr_stmt
|
||||
.query_row((addr,), |row| row.get(0))
|
||||
.with_context(|| format!("Original contact '{addr}' not found"))
|
||||
}
|
||||
};
|
||||
|
||||
let Some(autocrypt_key) = autocrypt_key else {
|
||||
continue;
|
||||
};
|
||||
let new_id = insert_contact(autocrypt_key).context("Step 10")?;
|
||||
|
||||
// prefer_encrypt == 20 would mean EncryptPreference::Reset,
|
||||
// i.e. we shouldn't encrypt if possible.
|
||||
if prefer_encrypt != 20 {
|
||||
autocrypt_key_contacts.insert(original_id.try_into().context("Step 11")?, new_id);
|
||||
} else {
|
||||
autocrypt_key_contacts_with_reset_peerstate
|
||||
.insert(original_id.try_into().context("Step 12")?, new_id);
|
||||
}
|
||||
|
||||
let Some(verified_key) = verified_key else {
|
||||
continue;
|
||||
};
|
||||
let new_id = insert_contact(verified_key).context("Step 13")?;
|
||||
verified_key_contacts.insert(original_id.try_into().context("Step 14")?, new_id);
|
||||
// If the original verifier is unknown, we represent this in the database
|
||||
// by putting `new_id` into the place of the verifier,
|
||||
// i.e. we say that this contact verified itself.
|
||||
let verifier_id =
|
||||
original_contact_id_from_addr(&verifier, new_id).context("Step 15")?;
|
||||
verifications.insert(new_id, verifier_id);
|
||||
|
||||
let Some(secondary_verified_key) = secondary_verified_key else {
|
||||
continue;
|
||||
};
|
||||
let new_id = insert_contact(secondary_verified_key).context("Step 16")?;
|
||||
let verifier_id: u32 =
|
||||
original_contact_id_from_addr(&secondary_verifier, new_id).context("Step 17")?;
|
||||
// Only use secondary verification if there is no primary verification:
|
||||
verifications.entry(new_id).or_insert(verifier_id);
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"Created key-contacts identified by autocrypt key: {autocrypt_key_contacts:?}"
|
||||
);
|
||||
info!(context, "Created key-contacts with 'reset' peerstate identified by autocrypt key: {autocrypt_key_contacts_with_reset_peerstate:?}");
|
||||
info!(
|
||||
context,
|
||||
"Created key-contacts identified by verified key: {verified_key_contacts:?}"
|
||||
);
|
||||
|
||||
for (&new_contact, &verifier_original_contact) in &verifications {
|
||||
let verifier = if verifier_original_contact == 1 {
|
||||
1 // Verified by ContactId::SELF
|
||||
} else if verifier_original_contact == new_contact {
|
||||
new_contact // unkwnown verifier
|
||||
} else {
|
||||
// `verifications` contains the original contact id.
|
||||
// We need to get the new, verified-pgp-identified contact id.
|
||||
match verified_key_contacts.get(&verifier_original_contact) {
|
||||
Some(v) => *v,
|
||||
None => {
|
||||
warn!(context, "Couldn't find key-contact for {verifier_original_contact} who verified {new_contact}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE contacts SET verifier=? WHERE id=?",
|
||||
(verifier, new_contact),
|
||||
)
|
||||
.context("Step 18")?;
|
||||
}
|
||||
info!(context, "Migrated verifications: {verifications:?}");
|
||||
}
|
||||
|
||||
// ======================= Step 3: =======================
|
||||
// For each chat, modify the memberlist to retain the correct contacts
|
||||
// In the process, track the set of contacts which remained no any chat at all
|
||||
// in a `BTreeSet<u32>`, which initially contains all contact ids
|
||||
let mut orphaned_contacts: BTreeSet<u32> = transaction
|
||||
.prepare("SELECT id FROM contacts WHERE id>9")
|
||||
.context("Step 19")?
|
||||
.query_map((), |row| row.get::<usize, u32>(0))
|
||||
.context("Step 20")?
|
||||
.collect::<Result<BTreeSet<u32>, rusqlite::Error>>()
|
||||
.context("Step 21")?;
|
||||
|
||||
{
|
||||
let mut stmt = transaction
|
||||
.prepare(
|
||||
"SELECT c.id, c.type, c.grpid, c.protected
|
||||
FROM chats c
|
||||
WHERE id>9",
|
||||
)
|
||||
.context("Step 22")?;
|
||||
let all_chats = stmt
|
||||
.query_map((), |row| {
|
||||
let id: u32 = row.get(0)?;
|
||||
let typ: u32 = row.get(1)?;
|
||||
let grpid: String = row.get(2)?;
|
||||
let protected: u32 = row.get(3)?;
|
||||
Ok((id, typ, grpid, protected))
|
||||
})
|
||||
.context("Step 23")?;
|
||||
let mut load_chat_contacts_stmt = transaction
|
||||
.prepare("SELECT contact_id FROM chats_contacts WHERE chat_id=? AND contact_id>9")?;
|
||||
let is_chatmail: Option<String> = transaction
|
||||
.query_row(
|
||||
"SELECT value FROM config WHERE keyname='is_chatmail'",
|
||||
(),
|
||||
|row| row.get(0),
|
||||
)
|
||||
.optional()
|
||||
.context("Step 23.1")?;
|
||||
let is_chatmail = is_chatmail
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
.unwrap_or_default()
|
||||
!= 0;
|
||||
let map_to_key_contact = |old_member: &u32| {
|
||||
(
|
||||
*old_member,
|
||||
autocrypt_key_contacts
|
||||
.get(old_member)
|
||||
.or_else(|| {
|
||||
// For chatmail servers,
|
||||
// we send encrypted even if the peerstate is reset,
|
||||
// because an unencrypted message likely won't arrive.
|
||||
// This is the same behavior as before key-contacts migration.
|
||||
if is_chatmail {
|
||||
autocrypt_key_contacts_with_reset_peerstate.get(old_member)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.copied(),
|
||||
)
|
||||
};
|
||||
|
||||
let mut update_member_stmt = transaction
|
||||
.prepare("UPDATE chats_contacts SET contact_id=? WHERE contact_id=? AND chat_id=?")?;
|
||||
let mut addr_cmp_stmt = transaction
|
||||
.prepare("SELECT c.addr=d.addr FROM contacts c, contacts d WHERE c.id=? AND d.id=?")?;
|
||||
for chat in all_chats {
|
||||
let (chat_id, typ, grpid, protected) = chat.context("Step 24")?;
|
||||
// In groups, this also contains past members
|
||||
let old_members: Vec<u32> = load_chat_contacts_stmt
|
||||
.query_map((chat_id,), |row| row.get::<_, u32>(0))
|
||||
.context("Step 25")?
|
||||
.collect::<Result<Vec<u32>, rusqlite::Error>>()
|
||||
.context("Step 26")?;
|
||||
|
||||
let mut keep_address_contacts = |reason: &str| {
|
||||
info!(context, "Chat {chat_id} will be an unencrypted chat with contacts identified by email address: {reason}");
|
||||
for m in &old_members {
|
||||
orphaned_contacts.remove(m);
|
||||
}
|
||||
};
|
||||
let old_and_new_members: Vec<(u32, Option<u32>)> = match typ {
|
||||
// 1:1 chats retain:
|
||||
// - address-contact if peerstate is in the "reset" state,
|
||||
// or if there is no key-contact that has the right email address.
|
||||
// - key-contact identified by the Autocrypt key if Autocrypt key does not match the verified key.
|
||||
// - key-contact identified by the verified key if peerstate Autocrypt key matches the Verified key.
|
||||
// Since the autocrypt and verified key-contact are identital in this case, we can add the Autocrypt key-contact,
|
||||
// and the effect will be the same.
|
||||
100 => {
|
||||
let Some(old_member) = old_members.first() else {
|
||||
info!(context, "1:1 chat {chat_id} doesn't contain contact, probably it's self or device chat");
|
||||
continue;
|
||||
};
|
||||
|
||||
let (_, Some(new_contact)) = map_to_key_contact(old_member) else {
|
||||
keep_address_contacts("No peerstate, or peerstate in 'reset' state");
|
||||
continue;
|
||||
};
|
||||
if !addr_cmp_stmt
|
||||
.query_row((old_member, new_contact), |row| row.get::<_, bool>(0))?
|
||||
{
|
||||
// Unprotect this 1:1 chat if it was protected.
|
||||
//
|
||||
// Otherwise we get protected chat with address-contact.
|
||||
transaction
|
||||
.execute("UPDATE chats SET protected=0 WHERE id=?", (chat_id,))?;
|
||||
|
||||
keep_address_contacts("key contact has different email");
|
||||
continue;
|
||||
}
|
||||
vec![(*old_member, Some(new_contact))]
|
||||
}
|
||||
|
||||
// Group
|
||||
120 => {
|
||||
if grpid.is_empty() {
|
||||
// Ad-hoc group that has empty Chat-Group-ID
|
||||
// because it was created in response to receiving a non-chat email.
|
||||
keep_address_contacts("Empty chat-Group-ID");
|
||||
continue;
|
||||
} else if protected == 1 {
|
||||
old_members
|
||||
.iter()
|
||||
.map(|old_member| {
|
||||
(*old_member, verified_key_contacts.get(old_member).copied())
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
old_members
|
||||
.iter()
|
||||
.map(map_to_key_contact)
|
||||
.collect::<Vec<(u32, Option<u32>)>>()
|
||||
}
|
||||
}
|
||||
|
||||
// Mailinglist
|
||||
140 => {
|
||||
keep_address_contacts("Mailinglist");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Broadcast list
|
||||
160 => old_members
|
||||
.iter()
|
||||
.map(|original| {
|
||||
(
|
||||
*original,
|
||||
autocrypt_key_contacts
|
||||
.get(original)
|
||||
// There will be no unencrypted broadcast lists anymore,
|
||||
// so, if a peerstate is reset,
|
||||
// the best we can do is encrypting to this key regardless.
|
||||
.or_else(|| {
|
||||
autocrypt_key_contacts_with_reset_peerstate.get(original)
|
||||
})
|
||||
.copied(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(u32, Option<u32>)>>(),
|
||||
_ => {
|
||||
warn!(context, "Invalid chat type {typ}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// If a group contains a contact without a key or with 'reset' peerstate,
|
||||
// downgrade to unencrypted Ad-Hoc group.
|
||||
if typ == 120 && old_and_new_members.iter().any(|(_old, new)| new.is_none()) {
|
||||
transaction
|
||||
.execute("UPDATE chats SET grpid='' WHERE id=?", (chat_id,))
|
||||
.context("Step 26.1")?;
|
||||
keep_address_contacts("Group contains contact without peerstate");
|
||||
continue;
|
||||
}
|
||||
|
||||
let human_readable_transitions = old_and_new_members
|
||||
.iter()
|
||||
.map(|(old, new)| format!("{old}->{}", new.unwrap_or_default()))
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ");
|
||||
info!(
|
||||
context,
|
||||
"Migrating chat {chat_id} to key-contacts: {human_readable_transitions}"
|
||||
);
|
||||
|
||||
for (old_member, new_member) in old_and_new_members {
|
||||
if let Some(new_member) = new_member {
|
||||
orphaned_contacts.remove(&new_member);
|
||||
let res = update_member_stmt.execute((new_member, old_member, chat_id));
|
||||
if res.is_err() {
|
||||
// The same chat partner exists multiple times in the chat,
|
||||
// with mutliple profiles which have different email addresses
|
||||
// but the same key.
|
||||
// We can only keep one of them.
|
||||
// So, if one of them is not in the chat anymore, delete it,
|
||||
// otherwise delete the one that was added least recently.
|
||||
let member_to_delete: u32 = transaction
|
||||
.query_row(
|
||||
"SELECT contact_id
|
||||
FROM chats_contacts
|
||||
WHERE chat_id=? AND contact_id IN (?,?)
|
||||
ORDER BY add_timestamp>=remove_timestamp, add_timestamp LIMIT 1",
|
||||
(chat_id, new_member, old_member),
|
||||
|row| row.get(0),
|
||||
)
|
||||
.context("Step 27")?;
|
||||
info!(
|
||||
context,
|
||||
"Chat partner is in the chat {chat_id} multiple times. \
|
||||
Deleting {member_to_delete}, then trying to update \
|
||||
{old_member}->{new_member} again"
|
||||
);
|
||||
transaction
|
||||
.execute(
|
||||
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?",
|
||||
(chat_id, member_to_delete),
|
||||
)
|
||||
.context("Step 28")?;
|
||||
// If we removed `old_member`, then this will be a no-op,
|
||||
// which is exactly what we want in this case:
|
||||
update_member_stmt.execute((new_member, old_member, chat_id))?;
|
||||
}
|
||||
} else {
|
||||
info!(context, "Old member {old_member} in chat {chat_id} can't be upgraded to key-contact, removing them");
|
||||
transaction
|
||||
.execute(
|
||||
"DELETE FROM chats_contacts WHERE contact_id=? AND chat_id=?",
|
||||
(old_member, chat_id),
|
||||
)
|
||||
.context("Step 29")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ======================= Step 4: =======================
|
||||
{
|
||||
info!(
|
||||
context,
|
||||
"Marking contacts which remained in no chat at all as hidden: {orphaned_contacts:?}"
|
||||
);
|
||||
let mut mark_as_hidden_stmt = transaction
|
||||
.prepare("UPDATE contacts SET origin=? WHERE id=?")
|
||||
.context("Step 30")?;
|
||||
for contact in orphaned_contacts {
|
||||
mark_as_hidden_stmt
|
||||
.execute((0x8, contact))
|
||||
.context("Step 31")?;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================= Step 5: =======================
|
||||
// Rewrite `from_id` in messages
|
||||
{
|
||||
let start = Instant::now();
|
||||
|
||||
let mut encrypted_msgs_stmt = transaction
|
||||
.prepare(
|
||||
"SELECT id, from_id, to_id
|
||||
FROM msgs
|
||||
WHERE id>9
|
||||
AND (param LIKE '%\nc=1%' OR param LIKE 'c=1%')
|
||||
AND chat_id>9
|
||||
ORDER BY id DESC LIMIT 10000",
|
||||
)
|
||||
.context("Step 32")?;
|
||||
let mut rewrite_msg_stmt = transaction
|
||||
.prepare("UPDATE msgs SET from_id=?, to_id=? WHERE id=?")
|
||||
.context("Step 32.1")?;
|
||||
|
||||
struct LoadedMsg {
|
||||
id: u32,
|
||||
from_id: u32,
|
||||
to_id: u32,
|
||||
}
|
||||
|
||||
let encrypted_msgs = encrypted_msgs_stmt
|
||||
.query_map((), |row| {
|
||||
let id: u32 = row.get(0)?;
|
||||
let from_id: u32 = row.get(1)?;
|
||||
let to_id: u32 = row.get(2)?;
|
||||
Ok(LoadedMsg { id, from_id, to_id })
|
||||
})
|
||||
.context("Step 33")?;
|
||||
|
||||
for msg in encrypted_msgs {
|
||||
let msg = msg.context("Step 34")?;
|
||||
|
||||
let new_from_id = *autocrypt_key_contacts
|
||||
.get(&msg.from_id)
|
||||
.or_else(|| autocrypt_key_contacts_with_reset_peerstate.get(&msg.from_id))
|
||||
.unwrap_or(&msg.from_id);
|
||||
|
||||
let new_to_id = *autocrypt_key_contacts
|
||||
.get(&msg.to_id)
|
||||
.or_else(|| autocrypt_key_contacts_with_reset_peerstate.get(&msg.to_id))
|
||||
.unwrap_or(&msg.to_id);
|
||||
|
||||
rewrite_msg_stmt
|
||||
.execute((new_from_id, new_to_id, msg.id))
|
||||
.context("Step 35")?;
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"Rewriting msgs to key-contacts took {:?}.",
|
||||
start.elapsed()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Sql {
|
||||
@@ -1284,6 +1904,14 @@ impl Sql {
|
||||
migration: impl Send + FnOnce(&mut rusqlite::Transaction) -> Result<()>,
|
||||
version: i32,
|
||||
) -> Result<()> {
|
||||
#[cfg(test)]
|
||||
if STOP_MIGRATIONS_AT.try_with(|stop_migrations_at| version > *stop_migrations_at)
|
||||
== Ok(true)
|
||||
{
|
||||
println!("Not running migration {version}, because STOP_MIGRATIONS_AT is set");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.transaction(move |transaction| {
|
||||
let curr_version: String = transaction.query_row(
|
||||
"SELECT IFNULL(value, ?) FROM config WHERE keyname=?;",
|
||||
@@ -1307,28 +1935,4 @@ impl Sql {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_clear_config_cache() -> anyhow::Result<()> {
|
||||
// Some migrations change the `config` table in SQL.
|
||||
// This test checks that the config cache is invalidated in `execute_migration()`.
|
||||
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(t.get_config_bool(Config::IsChatmail).await?, false);
|
||||
|
||||
t.sql
|
||||
.execute_migration(
|
||||
"INSERT INTO config (keyname, value) VALUES ('is_chatmail', '1')",
|
||||
1000,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(t.get_config_bool(Config::IsChatmail).await?, true);
|
||||
assert_eq!(t.sql.get_raw_config_int(VERSION_CFG).await?.unwrap(), 1000);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
mod migrations_tests;
|
||||
|
||||
173
src/sql/migrations/migrations_tests.rs
Normal file
173
src/sql/migrations/migrations_tests.rs
Normal file
File diff suppressed because one or more lines are too long
@@ -179,9 +179,7 @@ async fn test_migration_flags() -> Result<()> {
|
||||
// as migrations::run() was already executed on context creation,
|
||||
// another call should not result in any action needed.
|
||||
// this test catches some bugs where dbversion was forgotten to be persisted.
|
||||
let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) =
|
||||
migrations::run(&t, &t.sql).await?;
|
||||
assert!(!recalc_fingerprints);
|
||||
let (update_icons, disable_server_delete, recode_avatar) = migrations::run(&t, &t.sql).await?;
|
||||
assert!(!update_icons);
|
||||
assert!(!disable_server_delete);
|
||||
assert!(!recode_avatar);
|
||||
|
||||
@@ -80,18 +80,9 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Fingerprints"))]
|
||||
FingerPrints = 30,
|
||||
|
||||
#[strum(props(fallback = "End-to-end encryption preferred"))]
|
||||
E2ePreferred = 34,
|
||||
|
||||
#[strum(props(fallback = "%1$s verified."))]
|
||||
ContactVerified = 35,
|
||||
|
||||
#[strum(props(fallback = "Cannot establish guaranteed end-to-end encryption with %1$s"))]
|
||||
ContactNotVerified = 36,
|
||||
|
||||
#[strum(props(fallback = "Changed setup for %1$s"))]
|
||||
ContactSetupChanged = 37,
|
||||
|
||||
#[strum(props(fallback = "Archived chats"))]
|
||||
ArchivedChats = 40,
|
||||
|
||||
@@ -268,9 +259,6 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Not connected"))]
|
||||
NotConnected = 121,
|
||||
|
||||
#[strum(props(fallback = "%1$s changed their address from %2$s to %3$s"))]
|
||||
AeapAddrChanged = 122,
|
||||
|
||||
#[strum(props(
|
||||
fallback = "You changed your email address from %1$s to %2$s.\n\nIf you now send a message to a verified group, contacts there will automatically replace the old with your new address.\n\nIt's highly advised to set up your old email provider to forward all emails to your new email address. Otherwise you might miss messages of contacts who did not get your new address yet."
|
||||
))]
|
||||
@@ -428,11 +416,6 @@ pub enum StockMessage {
|
||||
|
||||
#[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
|
||||
SecurejoinWait = 190,
|
||||
|
||||
#[strum(props(
|
||||
fallback = "The contact must be online to proceed.\n\nThis process will continue automatically in background."
|
||||
))]
|
||||
SecurejoinTakesLonger = 192,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
@@ -636,29 +619,22 @@ pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr:
|
||||
/// contacts to combine with the display name.
|
||||
pub(crate) async fn msg_add_member_local(
|
||||
context: &Context,
|
||||
added_member_addr: &str,
|
||||
added_member: ContactId,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
let addr = added_member_addr;
|
||||
let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
|
||||
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
|
||||
.await
|
||||
.map(|contact| contact.get_display_name().to_string())
|
||||
.unwrap_or_else(|_| addr.to_string()),
|
||||
_ => addr.to_string(),
|
||||
};
|
||||
let whom = added_member.get_stock_name(context).await;
|
||||
if by_contact == ContactId::UNDEFINED {
|
||||
translated(context, StockMessage::MsgAddMember)
|
||||
.await
|
||||
.replace1(whom)
|
||||
.replace1(&whom)
|
||||
} else if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouAddMember)
|
||||
.await
|
||||
.replace1(whom)
|
||||
.replace1(&whom)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgAddMemberBy)
|
||||
.await
|
||||
.replace1(whom)
|
||||
.replace1(&whom)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
@@ -687,29 +663,22 @@ pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr
|
||||
/// the contacts to combine with the display name.
|
||||
pub(crate) async fn msg_del_member_local(
|
||||
context: &Context,
|
||||
removed_member_addr: &str,
|
||||
removed_member: ContactId,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
let addr = removed_member_addr;
|
||||
let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
|
||||
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
|
||||
.await
|
||||
.map(|contact| contact.get_display_name().to_string())
|
||||
.unwrap_or_else(|_| addr.to_string()),
|
||||
_ => addr.to_string(),
|
||||
};
|
||||
let whom = removed_member.get_stock_name(context).await;
|
||||
if by_contact == ContactId::UNDEFINED {
|
||||
translated(context, StockMessage::MsgDelMember)
|
||||
.await
|
||||
.replace1(whom)
|
||||
.replace1(&whom)
|
||||
} else if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouDelMember)
|
||||
.await
|
||||
.replace1(whom)
|
||||
.replace1(&whom)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgDelMemberBy)
|
||||
.await
|
||||
.replace1(whom)
|
||||
.replace1(&whom)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
@@ -792,11 +761,6 @@ pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `End-to-end encryption preferred.`.
|
||||
pub(crate) async fn e2e_preferred(context: &Context) -> String {
|
||||
translated(context, StockMessage::E2ePreferred).await
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s invited you to join this group. Waiting for the device of %2$s to reply…`.
|
||||
pub(crate) async fn secure_join_started(
|
||||
context: &Context,
|
||||
@@ -824,11 +788,6 @@ pub(crate) async fn securejoin_wait(context: &Context) -> String {
|
||||
translated(context, StockMessage::SecurejoinWait).await
|
||||
}
|
||||
|
||||
/// Stock string: `The contact must be online to proceed. This process will continue automatically in background.`.
|
||||
pub(crate) async fn securejoin_takes_longer(context: &Context) -> String {
|
||||
translated(context, StockMessage::SecurejoinTakesLonger).await
|
||||
}
|
||||
|
||||
/// Stock string: `Scan to chat with %1$s`.
|
||||
pub(crate) async fn setup_contact_qr_description(
|
||||
context: &Context,
|
||||
@@ -861,21 +820,6 @@ pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> St
|
||||
.replace1(addr)
|
||||
}
|
||||
|
||||
/// Stock string: `Cannot establish guaranteed end-to-end encryption with %1$s`.
|
||||
pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String {
|
||||
let addr = contact.get_display_name();
|
||||
translated(context, StockMessage::ContactNotVerified)
|
||||
.await
|
||||
.replace1(addr)
|
||||
}
|
||||
|
||||
/// Stock string: `Changed setup for %1$s`.
|
||||
pub(crate) async fn contact_setup_changed(context: &Context, contact_addr: &str) -> String {
|
||||
translated(context, StockMessage::ContactSetupChanged)
|
||||
.await
|
||||
.replace1(contact_addr)
|
||||
}
|
||||
|
||||
/// Stock string: `Archived chats`.
|
||||
pub(crate) async fn archived_chats(context: &Context) -> String {
|
||||
translated(context, StockMessage::ArchivedChats).await
|
||||
@@ -1283,20 +1227,6 @@ pub(crate) async fn broadcast_list(context: &Context) -> String {
|
||||
translated(context, StockMessage::BroadcastList).await
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s changed their address from %2$s to %3$s`.
|
||||
pub(crate) async fn aeap_addr_changed(
|
||||
context: &Context,
|
||||
contact_name: &str,
|
||||
old_addr: &str,
|
||||
new_addr: &str,
|
||||
) -> String {
|
||||
translated(context, StockMessage::AeapAddrChanged)
|
||||
.await
|
||||
.replace1(contact_name)
|
||||
.replace2(old_addr)
|
||||
.replace3(new_addr)
|
||||
}
|
||||
|
||||
/// Stock string: `⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet. Tap to learn more.`.
|
||||
pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
|
||||
translated(context, StockMessage::InvalidUnencryptedMail)
|
||||
|
||||
@@ -70,20 +70,7 @@ async fn test_stock_system_msg_simple() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_system_msg_add_member_by_me() {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(
|
||||
msg_add_member_remote(&t, "alice@example.org").await,
|
||||
"I added member alice@example.org."
|
||||
);
|
||||
assert_eq!(
|
||||
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
|
||||
"You added member alice@example.org."
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_system_msg_add_member_by_me_with_displayname() {
|
||||
let t = TestContext::new().await;
|
||||
Contact::create(&t, "Alice", "alice@example.org")
|
||||
let alice_contact_id = Contact::create(&t, "Alice", "alice@example.org")
|
||||
.await
|
||||
.expect("failed to create contact");
|
||||
assert_eq!(
|
||||
@@ -91,7 +78,23 @@ async fn test_stock_system_msg_add_member_by_me_with_displayname() {
|
||||
"I added member alice@example.org."
|
||||
);
|
||||
assert_eq!(
|
||||
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
|
||||
msg_add_member_local(&t, alice_contact_id, ContactId::SELF).await,
|
||||
"You added member Alice."
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_system_msg_add_member_by_me_with_displayname() {
|
||||
let t = TestContext::new().await;
|
||||
let alice_contact_id = Contact::create(&t, "Alice", "alice@example.org")
|
||||
.await
|
||||
.expect("failed to create contact");
|
||||
assert_eq!(
|
||||
msg_add_member_remote(&t, "alice@example.org").await,
|
||||
"I added member alice@example.org."
|
||||
);
|
||||
assert_eq!(
|
||||
msg_add_member_local(&t, alice_contact_id, ContactId::SELF).await,
|
||||
"You added member Alice."
|
||||
);
|
||||
}
|
||||
@@ -99,16 +102,14 @@ async fn test_stock_system_msg_add_member_by_me_with_displayname() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_system_msg_add_member_by_other_with_displayname() {
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = {
|
||||
Contact::create(&t, "Alice", "alice@example.org")
|
||||
let alice_contact_id = Contact::create(&t, "Alice", "alice@example.org")
|
||||
.await
|
||||
.expect("Failed to create contact Alice");
|
||||
Contact::create(&t, "Bob", "bob@example.com")
|
||||
let bob_contact_id = Contact::create(&t, "Bob", "bob@example.com")
|
||||
.await
|
||||
.expect("failed to create bob")
|
||||
};
|
||||
.expect("failed to create bob");
|
||||
assert_eq!(
|
||||
msg_add_member_local(&t, "alice@example.org", contact_id,).await,
|
||||
msg_add_member_local(&t, alice_contact_id, bob_contact_id).await,
|
||||
"Member Alice added by Bob."
|
||||
);
|
||||
}
|
||||
@@ -133,7 +134,7 @@ async fn test_partial_download_msg_body() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_update_device_chats() {
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
t.update_device_chats().await.ok();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 2);
|
||||
|
||||
@@ -25,18 +25,17 @@ use crate::chat::{
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::constants::DC_GCL_NO_SPECIALS;
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::contact::{import_vcard, make_vcard, Contact, ContactId, Modifier, Origin};
|
||||
use crate::constants::{DC_CHAT_ID_TRASH, DC_GCL_NO_SPECIALS};
|
||||
use crate::contact::{
|
||||
import_vcard, make_vcard, mark_contact_id_as_verified, Contact, ContactId, Modifier, Origin,
|
||||
};
|
||||
use crate::context::Context;
|
||||
use crate::e2ee::EncryptHelper;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::key::{self, DcKey, DcSecretKey};
|
||||
use crate::key::{self, self_fingerprint, DcKey, DcSecretKey};
|
||||
use crate::log::warn;
|
||||
use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::pgp::KeyPair;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::{get_securejoin_qr, join_securejoin};
|
||||
@@ -628,9 +627,9 @@ impl TestContext {
|
||||
/// Parses a message.
|
||||
///
|
||||
/// Parsing a message does not run the entire receive pipeline, but is not without
|
||||
/// side-effects either. E.g. if the message includes autocrypt headers the relevant
|
||||
/// peerstates will be updated. Later receiving the message using [Self.recv_msg()] is
|
||||
/// unlikely to be affected as the peerstate would be processed again in exactly the
|
||||
/// side-effects either. E.g. if the message includes autocrypt headers,
|
||||
/// gossiped public keys will be saved. Later receiving the message using [Self.recv_msg()] is
|
||||
/// unlikely to be affected as the message would be processed again in exactly the
|
||||
/// same way.
|
||||
pub(crate) async fn parse_msg(&self, msg: &SentMessage<'_>) -> MimeMessage {
|
||||
MimeMessage::from_bytes(&self.ctx, msg.payload().as_bytes(), None)
|
||||
@@ -723,7 +722,7 @@ impl TestContext {
|
||||
}
|
||||
|
||||
/// Returns the [`ContactId`] for the other [`TestContext`], creating a contact if necessary.
|
||||
pub async fn add_or_lookup_email_contact_id(&self, other: &TestContext) -> ContactId {
|
||||
pub async fn add_or_lookup_address_contact_id(&self, other: &TestContext) -> ContactId {
|
||||
let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap();
|
||||
let addr = ContactAddress::new(&primary_self_addr).unwrap();
|
||||
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
|
||||
@@ -741,9 +740,11 @@ impl TestContext {
|
||||
}
|
||||
|
||||
/// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary.
|
||||
pub async fn add_or_lookup_email_contact(&self, other: &TestContext) -> Contact {
|
||||
let contact_id = self.add_or_lookup_email_contact_id(other).await;
|
||||
Contact::get_by_id(&self.ctx, contact_id).await.unwrap()
|
||||
pub async fn add_or_lookup_address_contact(&self, other: &TestContext) -> Contact {
|
||||
let contact_id = self.add_or_lookup_address_contact_id(other).await;
|
||||
let contact = Contact::get_by_id(&self.ctx, contact_id).await.unwrap();
|
||||
debug_assert_eq!(contact.is_key_contact(), false);
|
||||
contact
|
||||
}
|
||||
|
||||
/// Returns the [`ContactId`] for the other [`TestContext`], creating it if necessary.
|
||||
@@ -755,20 +756,61 @@ impl TestContext {
|
||||
}
|
||||
|
||||
/// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary.
|
||||
///
|
||||
/// This function imports a vCard, so will transfer the public key
|
||||
/// as a side effect.
|
||||
pub async fn add_or_lookup_contact(&self, other: &TestContext) -> Contact {
|
||||
let contact_id = self.add_or_lookup_contact_id(other).await;
|
||||
Contact::get_by_id(&self.ctx, contact_id).await.unwrap()
|
||||
}
|
||||
|
||||
/// Returns 1:1 [`Chat`] with another account. Panics if it doesn't exist.
|
||||
/// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary.
|
||||
///
|
||||
/// If the contact does not exist yet, a new contact will be created
|
||||
/// with the correct fingerprint, but without the public key.
|
||||
pub async fn add_or_lookup_contact_no_key(&self, other: &TestContext) -> Contact {
|
||||
let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap();
|
||||
let addr = ContactAddress::new(&primary_self_addr).unwrap();
|
||||
let fingerprint = self_fingerprint(other).await.unwrap();
|
||||
|
||||
let (contact_id, _modified) =
|
||||
Contact::add_or_lookup_ex(self, "", &addr, fingerprint, Origin::MailinglistAddress)
|
||||
.await
|
||||
.expect("add_or_lookup");
|
||||
Contact::get_by_id(&self.ctx, contact_id).await.unwrap()
|
||||
}
|
||||
|
||||
/// Returns 1:1 [`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 1:1 chat with this contact.
|
||||
pub async fn get_chat(&self, other: &TestContext) -> Chat {
|
||||
let contact = self.add_or_lookup_email_contact(other).await;
|
||||
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 1:1 [`Chat`] with another account key-contact.
|
||||
/// Panics if the chat does not exist.
|
||||
///
|
||||
/// This first creates a contact, but does not import the key,
|
||||
/// so may create a key-contact with a fingerprint
|
||||
/// but without the key.
|
||||
pub async fn get_chat(&self, other: &TestContext) -> Chat {
|
||||
let contact = self.add_or_lookup_contact_id(other).await;
|
||||
|
||||
let chat_id = ChatIdBlocked::lookup_by_contact(&self.ctx, contact)
|
||||
.await
|
||||
.unwrap()
|
||||
.map(|chat_id_blocked| chat_id_blocked.id)
|
||||
@@ -786,11 +828,8 @@ impl TestContext {
|
||||
/// and importing it into `self`,
|
||||
/// then creates a 1:1 chat with this contact.
|
||||
pub async fn create_chat(&self, other: &TestContext) -> Chat {
|
||||
let vcard = make_vcard(other, &[ContactId::SELF]).await.unwrap();
|
||||
let contact_ids = import_vcard(self, &vcard).await.unwrap();
|
||||
assert_eq!(contact_ids.len(), 1);
|
||||
let contact_id = contact_ids.first().unwrap();
|
||||
let chat_id = ChatId::create_for_contact(self, *contact_id).await.unwrap();
|
||||
let contact_id = self.add_or_lookup_contact_id(other).await;
|
||||
let chat_id = ChatId::create_for_contact(self, contact_id).await.unwrap();
|
||||
Chat::load_from_db(self, chat_id).await.unwrap()
|
||||
}
|
||||
|
||||
@@ -799,7 +838,7 @@ impl TestContext {
|
||||
///
|
||||
/// This function can be used to create unencrypted chats.
|
||||
pub async fn create_email_chat(&self, other: &TestContext) -> Chat {
|
||||
let contact = self.add_or_lookup_email_contact(other).await;
|
||||
let contact = self.add_or_lookup_address_contact(other).await;
|
||||
let chat_id = ChatId::create_for_contact(self, contact.id).await.unwrap();
|
||||
|
||||
Chat::load_from_db(self, chat_id).await.unwrap()
|
||||
@@ -914,7 +953,11 @@ impl TestContext {
|
||||
"device-talk".to_string()
|
||||
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
|
||||
let contact = Contact::get_by_id(self, members[0]).await.unwrap();
|
||||
if contact.is_key_contact() {
|
||||
format!("KEY {}", contact.get_addr())
|
||||
} else {
|
||||
contact.get_addr().to_string()
|
||||
}
|
||||
} else if sel_chat.get_type() == Chattype::Mailinglist && !members.is_empty() {
|
||||
"mailinglist".to_string()
|
||||
} else {
|
||||
@@ -934,7 +977,7 @@ impl TestContext {
|
||||
""
|
||||
},
|
||||
match sel_chat.get_profile_image(self).await.unwrap() {
|
||||
Some(icon) => match icon.to_str() {
|
||||
Some(icon) => match icon.strip_prefix(self.get_blobdir()).unwrap().to_str() {
|
||||
Some(icon) => format!(" Icon: {icon}"),
|
||||
_ => " Icon: Err".to_string(),
|
||||
},
|
||||
@@ -982,8 +1025,8 @@ impl TestContext {
|
||||
let chat_id = create_group_chat(self, protect, chat_name).await.unwrap();
|
||||
let mut to_add = vec![];
|
||||
for member in members {
|
||||
let contact = self.add_or_lookup_contact(member).await;
|
||||
to_add.push(contact.id);
|
||||
let contact_id = self.add_or_lookup_contact_id(member).await;
|
||||
to_add.push(contact_id);
|
||||
}
|
||||
add_to_chat_contacts_table(self, time(), chat_id, &to_add)
|
||||
.await
|
||||
@@ -1296,7 +1339,13 @@ pub(crate) async fn get_chat_msg(
|
||||
asserted_msgs_count: usize,
|
||||
) -> Message {
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id).await.unwrap();
|
||||
assert_eq!(msgs.len(), asserted_msgs_count);
|
||||
assert_eq!(
|
||||
msgs.len(),
|
||||
asserted_msgs_count,
|
||||
"expected {} messages in a chat but {} found",
|
||||
asserted_msgs_count,
|
||||
msgs.len()
|
||||
);
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs[index] {
|
||||
msg_id
|
||||
} else {
|
||||
@@ -1313,27 +1362,11 @@ fn print_logevent(logevent: &LogEvent) {
|
||||
}
|
||||
|
||||
/// Saves the other account's public key as verified
|
||||
/// and peerstate as backwards verified.
|
||||
pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) {
|
||||
let mut peerstate = Peerstate::from_header(
|
||||
&EncryptHelper::new(other).await.unwrap().get_aheader(),
|
||||
// We have to give 0 as the time, not the current time:
|
||||
// The time is going to be saved in peerstate.last_seen.
|
||||
// The code in `peerstate.rs` then compares `if message_time > self.last_seen`,
|
||||
// and many similar checks in peerstate.rs, and doesn't allow changes otherwise.
|
||||
// Giving the current time would mean that message_time == peerstate.last_seen,
|
||||
// so changes would not be allowed.
|
||||
// This might lead to flaky tests.
|
||||
0,
|
||||
);
|
||||
|
||||
peerstate.verified_key.clone_from(&peerstate.public_key);
|
||||
peerstate
|
||||
.verified_key_fingerprint
|
||||
.clone_from(&peerstate.public_key_fingerprint);
|
||||
peerstate.backward_verified_key_id = Some(this.get_config_i64(Config::KeyId).await.unwrap());
|
||||
|
||||
peerstate.save_to_db(&this.sql).await.unwrap();
|
||||
let contact_id = this.add_or_lookup_contact_id(other).await;
|
||||
mark_contact_id_as_verified(this, contact_id, ContactId::SELF)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Pops a sync message from alice0 and receives it on alice1. Should be used after an action on
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
//! "AEAP" means "Automatic Email Address Porting"
|
||||
//! and was the predecessor of key-contacts
|
||||
//! (i.e. identifying contacts via the fingerprint,
|
||||
//! while allowing the email address to change).
|
||||
//!
|
||||
//! These tests still pass because key-contacts
|
||||
//! allows messaging to continue after an email address change,
|
||||
//! just as AEAP did. Some other tests had to be removed.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
|
||||
use crate::contact;
|
||||
use crate::contact::Contact;
|
||||
use crate::contact::ContactId;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::message::Message;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::stock_str;
|
||||
use crate::test_utils::mark_as_verified;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
@@ -35,30 +40,6 @@ async fn test_change_primary_self_addr() -> Result<()> {
|
||||
let alice_bob_chat = alice.create_chat(&bob).await;
|
||||
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
|
||||
|
||||
tcm.section("Bob sends a message to Alice without In-Reply-To");
|
||||
// Even if Bob sends a message to Alice without In-Reply-To,
|
||||
// it's still assigned to the 1:1 chat with Bob and not to
|
||||
// a group (without secondary addresses, an ad-hoc group
|
||||
// would be created)
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: bob@example.net
|
||||
To: alice@example.org
|
||||
Chat-Version: 1.0
|
||||
Message-ID: <456@example.com>
|
||||
|
||||
Message w/out In-Reply-To
|
||||
",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
|
||||
assert_eq!(alice_msg.text, "Message w/out In-Reply-To");
|
||||
assert_eq!(alice_msg.get_showpadlock(), false);
|
||||
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -71,115 +52,67 @@ use ChatForTransition::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_aeap_transition_0() {
|
||||
check_aeap_transition(OneToOne, false, false).await;
|
||||
check_aeap_transition(OneToOne, false).await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_aeap_transition_1() {
|
||||
check_aeap_transition(GroupChat, false, false).await;
|
||||
check_aeap_transition(GroupChat, false).await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_aeap_transition_0_verified() {
|
||||
check_aeap_transition(OneToOne, true, false).await;
|
||||
check_aeap_transition(OneToOne, true).await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_aeap_transition_1_verified() {
|
||||
check_aeap_transition(GroupChat, true, false).await;
|
||||
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, false).await;
|
||||
check_aeap_transition(VerifiedGroup, true).await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_aeap_transition_0_bob_knew_new_addr() {
|
||||
check_aeap_transition(OneToOne, false, true).await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_aeap_transition_1_bob_knew_new_addr() {
|
||||
check_aeap_transition(GroupChat, false, true).await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_aeap_transition_0_verified_bob_knew_new_addr() {
|
||||
check_aeap_transition(OneToOne, true, true).await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_aeap_transition_1_verified_bob_knew_new_addr() {
|
||||
check_aeap_transition(GroupChat, true, true).await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_aeap_transition_2_verified_bob_knew_new_addr() {
|
||||
check_aeap_transition(VerifiedGroup, true, true).await;
|
||||
}
|
||||
|
||||
/// Happy path test for AEAP in various configurations.
|
||||
/// Happy path test for AEAP.
|
||||
/// - `chat_for_transition`: Which chat the transition message should be sent in
|
||||
/// - `verified`: Whether Alice and Bob verified each other
|
||||
/// - `bob_knew_new_addr`: Whether Bob already had a chat with Alice's new address
|
||||
async fn check_aeap_transition(
|
||||
chat_for_transition: ChatForTransition,
|
||||
verified: bool,
|
||||
bob_knew_new_addr: bool,
|
||||
) {
|
||||
// Alice's new address is "fiona@example.net" so that we can test
|
||||
// the case where Bob already had contact with Alice's new address
|
||||
const ALICE_NEW_ADDR: &str = "fiona@example.net";
|
||||
async fn check_aeap_transition(chat_for_transition: ChatForTransition, verified: bool) {
|
||||
const ALICE_NEW_ADDR: &str = "alice2@example.net";
|
||||
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
if bob_knew_new_addr {
|
||||
let fiona = tcm.fiona().await;
|
||||
|
||||
tcm.send_recv_accept(&fiona, &bob, "Hi").await;
|
||||
tcm.send_recv(&bob, &fiona, "Hi back").await;
|
||||
}
|
||||
|
||||
tcm.send_recv_accept(&alice, &bob, "Hi").await;
|
||||
tcm.send_recv(&bob, &alice, "Hi back").await;
|
||||
tcm.send_recv_accept(alice, bob, "Hi").await;
|
||||
tcm.send_recv(bob, alice, "Hi back").await;
|
||||
|
||||
if verified {
|
||||
mark_as_verified(&alice, &bob).await;
|
||||
mark_as_verified(&bob, &alice).await;
|
||||
mark_as_verified(alice, bob).await;
|
||||
mark_as_verified(bob, alice).await;
|
||||
}
|
||||
|
||||
let mut groups = vec![
|
||||
chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 0")
|
||||
chat::create_group_chat(bob, chat::ProtectionStatus::Unprotected, "Group 0")
|
||||
.await
|
||||
.unwrap(),
|
||||
chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 1")
|
||||
chat::create_group_chat(bob, chat::ProtectionStatus::Unprotected, "Group 1")
|
||||
.await
|
||||
.unwrap(),
|
||||
];
|
||||
if verified {
|
||||
groups.push(
|
||||
chat::create_group_chat(&bob, chat::ProtectionStatus::Protected, "Group 2")
|
||||
chat::create_group_chat(bob, chat::ProtectionStatus::Protected, "Group 2")
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
groups.push(
|
||||
chat::create_group_chat(&bob, chat::ProtectionStatus::Protected, "Group 3")
|
||||
chat::create_group_chat(bob, chat::ProtectionStatus::Protected, "Group 3")
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let old_contact = Contact::create(&bob, "Alice", "alice@example.org")
|
||||
.await
|
||||
.unwrap();
|
||||
let alice_contact = bob.add_or_lookup_contact_id(alice).await;
|
||||
for group in &groups {
|
||||
chat::add_contact_to_chat(&bob, *group, old_contact)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Already add the new contact to one of the groups.
|
||||
// We can then later check that the contact isn't in the group twice.
|
||||
let already_new_contact = Contact::create(&bob, "Alice", ALICE_NEW_ADDR)
|
||||
.await
|
||||
.unwrap();
|
||||
if verified {
|
||||
chat::add_contact_to_chat(&bob, groups[2], already_new_contact)
|
||||
chat::add_contact_to_chat(bob, *group, alice_contact)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -197,12 +130,12 @@ async fn check_aeap_transition(
|
||||
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");
|
||||
// No matter which chat Alice sends to, the transition should be done in all groups
|
||||
let chat_to_send = match chat_for_transition {
|
||||
OneToOne => alice.create_chat(&bob).await.id,
|
||||
OneToOne => alice.create_chat(bob).await.id,
|
||||
GroupChat => group1_alice,
|
||||
VerifiedGroup => group3_alice.expect("No verified group"),
|
||||
};
|
||||
@@ -210,147 +143,50 @@ async fn check_aeap_transition(
|
||||
.send_text(chat_to_send, "Hello from my new addr!")
|
||||
.await;
|
||||
let recvd = bob.recv_msg(&sent).await;
|
||||
let sent_timestamp = recvd.timestamp_sent;
|
||||
assert_eq!(recvd.text, "Hello from my new addr!");
|
||||
|
||||
tcm.section("Check that the AEAP transition worked");
|
||||
check_that_transition_worked(
|
||||
&groups[2..],
|
||||
&alice,
|
||||
"alice@example.org",
|
||||
ALICE_NEW_ADDR,
|
||||
"Alice",
|
||||
&bob,
|
||||
)
|
||||
.await;
|
||||
check_no_transition_done(&groups[0..2], "alice@example.org", &bob).await;
|
||||
|
||||
// Assert that the autocrypt header is also applied to the peerstate
|
||||
// if the address changed
|
||||
let bob_alice_peerstate = Peerstate::from_addr(&bob, ALICE_NEW_ADDR)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(bob_alice_peerstate.last_seen, sent_timestamp);
|
||||
assert_eq!(bob_alice_peerstate.last_seen_autocrypt, sent_timestamp);
|
||||
check_that_transition_worked(bob, &groups, alice_contact, ALICE_NEW_ADDR).await;
|
||||
|
||||
tcm.section("Test switching back");
|
||||
tcm.change_addr(&alice, "alice@example.org").await;
|
||||
tcm.change_addr(alice, "alice@example.org").await;
|
||||
let sent = alice
|
||||
.send_text(chat_to_send, "Hello from my old addr!")
|
||||
.await;
|
||||
let recvd = bob.recv_msg(&sent).await;
|
||||
assert_eq!(recvd.text, "Hello from my old addr!");
|
||||
|
||||
check_that_transition_worked(
|
||||
&groups[2..],
|
||||
&alice,
|
||||
// Note that "alice@example.org" and ALICE_NEW_ADDR are switched now:
|
||||
ALICE_NEW_ADDR,
|
||||
"alice@example.org",
|
||||
"Alice",
|
||||
&bob,
|
||||
)
|
||||
.await;
|
||||
check_that_transition_worked(bob, &groups, alice_contact, "alice@example.org").await;
|
||||
}
|
||||
|
||||
async fn check_that_transition_worked(
|
||||
groups: &[ChatId],
|
||||
alice: &TestContext,
|
||||
old_alice_addr: &str,
|
||||
new_alice_addr: &str,
|
||||
name: &str,
|
||||
bob: &TestContext,
|
||||
groups: &[ChatId],
|
||||
alice_contact_id: ContactId,
|
||||
alice_addr: &str,
|
||||
) {
|
||||
let new_contact = Contact::lookup_id_by_addr(bob, new_alice_addr, contact::Origin::Unknown)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
for group in groups {
|
||||
let members = chat::get_chat_contacts(bob, *group).await.unwrap();
|
||||
// In all the groups, exactly Bob and Alice's new number are members.
|
||||
// (and Alice's new number isn't in there twice)
|
||||
// In all the groups, exactly Bob and Alice are members.
|
||||
assert_eq!(
|
||||
members.len(),
|
||||
2,
|
||||
"Group {} has members {:?}, but should have members {:?} and {:?}",
|
||||
group,
|
||||
&members,
|
||||
new_contact,
|
||||
alice_contact_id,
|
||||
ContactId::SELF
|
||||
);
|
||||
assert!(
|
||||
members.contains(&new_contact),
|
||||
"Group {group} lacks {new_contact}"
|
||||
members.contains(&alice_contact_id),
|
||||
"Group {group} lacks {alice_contact_id}"
|
||||
);
|
||||
assert!(members.contains(&ContactId::SELF));
|
||||
|
||||
let info_msg = get_last_info_msg(bob, *group).await.unwrap();
|
||||
let expected_text =
|
||||
stock_str::aeap_addr_changed(bob, name, old_alice_addr, new_alice_addr).await;
|
||||
assert_eq!(info_msg.text, expected_text);
|
||||
assert_eq!(info_msg.from_id, ContactId::INFO);
|
||||
|
||||
let msg = format!("Sending to group {group}");
|
||||
let sent = bob.send_text(*group, &msg).await;
|
||||
let recvd = alice.recv_msg(&sent).await;
|
||||
assert_eq!(recvd.text, msg);
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_no_transition_done(groups: &[ChatId], old_alice_addr: &str, bob: &TestContext) {
|
||||
let old_contact = Contact::lookup_id_by_addr(bob, old_alice_addr, contact::Origin::Unknown)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
for group in groups {
|
||||
let members = chat::get_chat_contacts(bob, *group).await.unwrap();
|
||||
// In all the groups, exactly Bob and Alice's _old_ number are members.
|
||||
assert_eq!(
|
||||
members.len(),
|
||||
2,
|
||||
"Group {} has members {:?}, but should have members {:?} and {:?}",
|
||||
group,
|
||||
&members,
|
||||
old_contact,
|
||||
ContactId::SELF
|
||||
);
|
||||
assert!(members.contains(&old_contact));
|
||||
assert!(members.contains(&ContactId::SELF));
|
||||
|
||||
let last_info_msg = get_last_info_msg(bob, *group).await;
|
||||
assert!(
|
||||
last_info_msg.is_none(),
|
||||
"{last_info_msg:?} shouldn't be there (or it's an unrelated info msg)"
|
||||
);
|
||||
|
||||
let sent = bob.send_text(*group, "hi").await;
|
||||
let msg = Message::load_from_db(bob, sent.sender_msg_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.get_showpadlock(), true);
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_last_info_msg(t: &TestContext, chat_id: ChatId) -> Option<Message> {
|
||||
let msgs = chat::get_chat_msgs_ex(
|
||||
&t.ctx,
|
||||
chat_id,
|
||||
chat::MessageListOptions {
|
||||
info_only: true,
|
||||
add_daymarker: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let msg_id = if let chat::ChatItem::Message { msg_id } = msgs.last()? {
|
||||
msg_id
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
Some(Message::load_from_db(&t.ctx, *msg_id).await.unwrap())
|
||||
// Test that the email address of Alice is updated.
|
||||
let alice_contact = Contact::get_by_id(bob, alice_contact_id).await.unwrap();
|
||||
assert_eq!(alice_contact.get_addr(), alice_addr);
|
||||
}
|
||||
|
||||
/// Test that an attacker - here Fiona - can't replay a message sent by Alice
|
||||
@@ -360,6 +196,7 @@ async fn test_aeap_replay_attack() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let fiona = tcm.fiona().await;
|
||||
|
||||
tcm.send_recv_accept(&alice, &bob, "Hi").await;
|
||||
tcm.send_recv(&bob, &alice, "Hi back").await;
|
||||
@@ -367,7 +204,8 @@ async fn test_aeap_replay_attack() -> Result<()> {
|
||||
let group =
|
||||
chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 0").await?;
|
||||
|
||||
let bob_alice_contact = Contact::create(&bob, "Alice", "alice@example.org").await?;
|
||||
let bob_alice_contact = bob.add_or_lookup_contact_id(&alice).await;
|
||||
let bob_fiona_contact = bob.add_or_lookup_contact_id(&fiona).await;
|
||||
chat::add_contact_to_chat(&bob, group, bob_alice_contact).await?;
|
||||
|
||||
// Alice sends a message which Bob doesn't receive or something
|
||||
@@ -389,17 +227,22 @@ async fn test_aeap_replay_attack() -> Result<()> {
|
||||
|
||||
// Check that no transition was done
|
||||
assert!(chat::is_contact_in_chat(&bob, group, bob_alice_contact).await?);
|
||||
let bob_fiona_contact = Contact::create(&bob, "", "fiona@example.net").await?;
|
||||
assert!(!chat::is_contact_in_chat(&bob, group, bob_fiona_contact).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that writing to a contact is possible
|
||||
/// after address change.
|
||||
///
|
||||
/// This test is redundant after introduction
|
||||
/// of key-contacts, but is kept to avoid deleting the tests.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_write_to_alice_after_aeap() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_grp_id = chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_grp_id)).await?;
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
@@ -415,15 +258,13 @@ async fn test_write_to_alice_after_aeap() -> Result<()> {
|
||||
let sent = alice.send_text(alice_grp_id, "Hello!").await;
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
assert!(!bob_alice_contact.is_verified(bob).await?);
|
||||
assert!(bob_alice_contact.is_verified(bob).await?);
|
||||
let bob_alice_chat = Chat::load_from_db(bob, bob_alice_chat.id).await?;
|
||||
assert!(bob_alice_chat.is_protected());
|
||||
let mut msg = Message::new_text("hi".to_string());
|
||||
assert!(chat::send_msg(bob, bob_alice_chat.id, &mut msg)
|
||||
.await
|
||||
.is_err());
|
||||
chat::send_msg(bob, bob_alice_chat.id, &mut msg).await?;
|
||||
|
||||
// But encrypted communication is still possible in unprotected groups with old Alice.
|
||||
// Encrypted communication is also possible in unprotected groups with Alice.
|
||||
let sent = bob
|
||||
.send_text(bob_unprotected_grp_id, "Alice, how is your address change?")
|
||||
.await;
|
||||
|
||||
@@ -5,10 +5,10 @@ use crate::chat::resend_msgs;
|
||||
use crate::chat::{
|
||||
self, add_contact_to_chat, remove_contact_from_chat, send_msg, Chat, ProtectionStatus,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Chattype, DC_GCL_FOR_FORWARDING};
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::key::{load_self_public_key, DcKey};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -20,16 +20,16 @@ use crate::tools::SystemTime;
|
||||
use crate::{e2ee, message};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verified_oneonone_chat_broken_by_classical() {
|
||||
check_verified_oneonone_chat(true).await;
|
||||
async fn test_verified_oneonone_chat_not_broken_by_classical() {
|
||||
check_verified_oneonone_chat_protection_not_broken(true).await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verified_oneonone_chat_broken_by_device_change() {
|
||||
check_verified_oneonone_chat(false).await;
|
||||
async fn test_verified_oneonone_chat_not_broken_by_device_change() {
|
||||
check_verified_oneonone_chat_protection_not_broken(false).await;
|
||||
}
|
||||
|
||||
async fn check_verified_oneonone_chat(broken_by_classical_email: bool) {
|
||||
async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_email: bool) {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
@@ -59,19 +59,19 @@ async fn check_verified_oneonone_chat(broken_by_classical_email: bool) {
|
||||
// Bob's contact is still verified, but the chat isn't marked as protected anymore
|
||||
let contact = alice.add_or_lookup_contact(&bob).await;
|
||||
assert_eq!(contact.is_verified(&alice).await.unwrap(), true);
|
||||
assert_verified(&alice, &bob, ProtectionStatus::ProtectionBroken).await;
|
||||
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
|
||||
} else {
|
||||
tcm.section("Bob sets up another Delta Chat device");
|
||||
let bob2 = TestContext::new().await;
|
||||
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(&bob).await;
|
||||
let contact = alice.add_or_lookup_contact(&bob2).await;
|
||||
assert_eq!(contact.is_verified(&alice).await.unwrap(), false);
|
||||
assert_verified(&alice, &bob, ProtectionStatus::ProtectionBroken).await;
|
||||
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
|
||||
}
|
||||
|
||||
tcm.section("Bob sends another message from DC");
|
||||
@@ -157,44 +157,42 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
|
||||
tcm.send_recv(&fiona_new, &alice, "I have a new device")
|
||||
.await;
|
||||
|
||||
// The chat should be and stay unprotected
|
||||
// Alice gets a new unprotected chat with new Fiona contact.
|
||||
{
|
||||
let chat = alice.get_chat(&fiona_new).await;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(chat.is_protection_broken());
|
||||
|
||||
let msg1 = get_chat_msg(&alice, chat.id, 0, 3).await;
|
||||
assert_eq!(msg1.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
||||
|
||||
let msg2 = get_chat_msg(&alice, chat.id, 1, 3).await;
|
||||
assert_eq!(msg2.get_info_type(), SystemMessage::ChatProtectionDisabled);
|
||||
|
||||
let msg2 = get_chat_msg(&alice, chat.id, 2, 3).await;
|
||||
assert_eq!(msg2.text, "I have a new device");
|
||||
let msg = get_chat_msg(&alice, chat.id, 0, 1).await;
|
||||
assert_eq!(msg.text, "I have a new device");
|
||||
|
||||
// After recreating the chat, it should still be unprotected
|
||||
chat.id.delete(&alice).await?;
|
||||
|
||||
let chat = alice.create_chat(&fiona_new).await;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(!chat.is_protection_broken());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_missing_peerstate_reexecute_securejoin() -> Result<()> {
|
||||
async fn test_missing_key_reexecute_securejoin() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice_addr = alice.get_config(Config::Addr).await?.unwrap();
|
||||
let bob = &tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[alice, bob]).await;
|
||||
let chat_id = tcm.execute_securejoin(bob, alice).await;
|
||||
let chat = Chat::load_from_db(bob, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
bob.sql
|
||||
.execute("DELETE FROM acpeerstates WHERE addr=?", (&alice_addr,))
|
||||
.execute(
|
||||
"DELETE FROM public_keys WHERE fingerprint=?",
|
||||
(&load_self_public_key(alice)
|
||||
.await
|
||||
.unwrap()
|
||||
.dc_fingerprint()
|
||||
.hex(),),
|
||||
)
|
||||
.await?;
|
||||
let chat_id = tcm.execute_securejoin(bob, alice).await;
|
||||
let chat = Chat::load_from_db(bob, chat_id).await?;
|
||||
@@ -242,6 +240,10 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that receiving unencrypted message
|
||||
/// does not disable protection of 1:1 chat.
|
||||
///
|
||||
/// Instead, an email-chat is created.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_degrade_verified_oneonone_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -265,211 +267,16 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let contact_id = Contact::lookup_id_by_addr(&alice, "bob@example.net", Origin::Hidden)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 3).await;
|
||||
let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 1).await;
|
||||
let enabled = stock_str::chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg0.text, enabled);
|
||||
assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled);
|
||||
|
||||
let msg1 = get_chat_msg(&alice, alice_chat.id, 1, 3).await;
|
||||
let disabled = stock_str::chat_protection_disabled(&alice, contact_id).await;
|
||||
assert_eq!(msg1.text, disabled);
|
||||
assert_eq!(msg1.param.get_cmd(), SystemMessage::ChatProtectionDisabled);
|
||||
|
||||
let msg2 = get_chat_msg(&alice, alice_chat.id, 2, 3).await;
|
||||
assert_eq!(msg2.text, "hello".to_string());
|
||||
assert!(!msg2.is_system_message());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verified_oneonone_chat_enable_disable() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob]).await;
|
||||
|
||||
// Alice & Bob verify each other
|
||||
mark_as_verified(&alice, &bob).await;
|
||||
mark_as_verified(&bob, &alice).await;
|
||||
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
assert!(chat.is_protected());
|
||||
|
||||
for alice_accepts_breakage in [true, false] {
|
||||
SystemTime::shift(std::time::Duration::from_secs(300));
|
||||
// Bob uses Thunderbird to send a message
|
||||
receive_imf(
|
||||
&alice,
|
||||
format!(
|
||||
"From: Bob <bob@example.net>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <1234-2{alice_accepts_breakage}@example.org>\n\
|
||||
\n\
|
||||
Message from Thunderbird\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let chat = alice.get_chat(&bob).await;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(chat.is_protection_broken());
|
||||
|
||||
if alice_accepts_breakage {
|
||||
tcm.section("Alice clicks 'Accept' on the input-bar-dialog");
|
||||
chat.id.accept(&alice).await?;
|
||||
let chat = alice.get_chat(&bob).await;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(!chat.is_protection_broken());
|
||||
}
|
||||
|
||||
// Bob sends a message from DC again
|
||||
tcm.send_recv(&bob, &alice, "Hello from DC").await;
|
||||
let chat = alice.get_chat(&bob).await;
|
||||
assert!(chat.is_protected());
|
||||
assert!(!chat.is_protection_broken());
|
||||
}
|
||||
|
||||
alice
|
||||
.golden_test_chat(chat.id, "test_verified_oneonone_chat_enable_disable")
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Messages with old timestamps are difficult for verified chats:
|
||||
/// - They must not be sorted over a protection-changed info message.
|
||||
/// That's what `test_old_message_2` tests
|
||||
/// - If they change the protection, then they must not be sorted over existing other messages,
|
||||
/// because then the protection-changed info message would also be above these existing messages.
|
||||
/// That's what `test_old_message_3` tests.
|
||||
///
|
||||
/// `test_old_message_1` tests the case where both the old and the new message
|
||||
/// change verification
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_old_message_1() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob]).await;
|
||||
|
||||
mark_as_verified(&alice, &bob).await;
|
||||
|
||||
let chat = alice.create_chat(&bob).await; // This creates a protection-changed info message
|
||||
assert!(chat.is_protected());
|
||||
|
||||
// This creates protection-changed info message #2;
|
||||
// even though the date is old, info message and email must be sorted below the original info message.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.net>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <1234-2-3@example.org>\n\
|
||||
Date: Sat, 07 Dec 2019 19:00:27 +0000\n\
|
||||
\n\
|
||||
Message from Thunderbird\n",
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
alice.golden_test_chat(chat.id, "test_old_message_1").await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_old_message_2() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob]).await;
|
||||
|
||||
mark_as_verified(&alice, &bob).await;
|
||||
|
||||
// This creates protection-changed info message #1:
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
assert!(chat.is_protected());
|
||||
let protection_msg = alice.get_last_msg().await;
|
||||
assert_eq!(
|
||||
protection_msg.param.get_cmd(),
|
||||
SystemMessage::ChatProtectionEnabled
|
||||
);
|
||||
|
||||
// This creates protection-changed info message #2 with `timestamp_sort` greater by 1.
|
||||
let first_email = receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.net>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <1234-2-3@example.org>\n\
|
||||
Date: Sun, 08 Dec 2019 19:00:27 +0000\n\
|
||||
\n\
|
||||
Somewhat old message\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
// Both messages will get the same timestamp, so this one will be sorted under the previous one
|
||||
// even though it has an older timestamp.
|
||||
let second_email = receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.net>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <2319-2-3@example.org>\n\
|
||||
Date: Sat, 07 Dec 2019 19:00:27 +0000\n\
|
||||
\n\
|
||||
Even older message, that must NOT be shown before the info message\n",
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(first_email.sort_timestamp, second_email.sort_timestamp);
|
||||
assert_eq!(
|
||||
first_email.sort_timestamp,
|
||||
protection_msg.timestamp_sort + 1
|
||||
);
|
||||
|
||||
alice.golden_test_chat(chat.id, "test_old_message_2").await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_old_message_3() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob]).await;
|
||||
|
||||
mark_as_verified(&alice, &bob).await;
|
||||
mark_as_verified(&bob, &alice).await;
|
||||
|
||||
tcm.send_recv_accept(&bob, &alice, "Heyho from my verified device!")
|
||||
.await;
|
||||
|
||||
// This unverified message must not be sorted over the message sent in the previous line:
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.net>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <1234-2-3@example.org>\n\
|
||||
Date: Sat, 07 Dec 2019 19:00:27 +0000\n\
|
||||
\n\
|
||||
Old, unverified message\n",
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
alice
|
||||
.golden_test_chat(alice.get_chat(&bob).await.id, "test_old_message_3")
|
||||
.await;
|
||||
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(())
|
||||
}
|
||||
@@ -602,13 +409,35 @@ async fn test_outgoing_mua_msg() -> Result<()> {
|
||||
.unwrap();
|
||||
tcm.send_recv(&alice, &bob, "Sending with DC again").await;
|
||||
|
||||
// Unencrypted message from MUA gets into a separate chat.
|
||||
// PGP chat gets all encrypted messages.
|
||||
alice
|
||||
.golden_test_chat(sent.chat_id, "test_outgoing_mua_msg")
|
||||
.await;
|
||||
alice
|
||||
.golden_test_chat(alice.get_chat(&bob).await.id, "test_outgoing_mua_msg_pgp")
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_outgoing_encrypted_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[alice]).await;
|
||||
|
||||
mark_as_verified(alice, bob).await;
|
||||
let chat_id = alice.create_chat(bob).await.id;
|
||||
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
|
||||
receive_imf(alice, raw, false).await?;
|
||||
alice
|
||||
.golden_test_chat(chat_id, "test_outgoing_encrypted_msg")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// If Bob answers unencrypted from another address with a classical MUA,
|
||||
/// the message is under some circumstances still assigned to the original
|
||||
/// chat (see lookup_chat_by_reply()); this is meant to make aliases
|
||||
@@ -652,96 +481,23 @@ async fn test_reply() -> Result<()> {
|
||||
let unencrypted_msg = Message::load_from_db(&alice, unencrypted_msg.msg_ids[0]).await?;
|
||||
assert_eq!(unencrypted_msg.text, "Weird reply");
|
||||
|
||||
if verified {
|
||||
assert_ne!(unencrypted_msg.chat_id, encrypted_msg.chat_id);
|
||||
} else {
|
||||
assert_eq!(unencrypted_msg.chat_id, encrypted_msg.chat_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for the following bug:
|
||||
///
|
||||
/// - Scan your chat partner's QR Code
|
||||
/// - They change devices
|
||||
/// - They send you a message
|
||||
/// - Without accepting the encryption downgrade, scan your chat partner's QR Code again
|
||||
///
|
||||
/// -> The re-verification fails.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_break_protection_then_verify_again() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob]).await;
|
||||
|
||||
// Cave: Bob can't write a message to Alice here.
|
||||
// If he did, alice would increase his peerstate's last_seen timestamp.
|
||||
// Then, after Bob reinstalls DC, alice's `if message_time > last_seen*`
|
||||
// checks would return false (there are many checks of this form in peerstate.rs).
|
||||
// Therefore, during the securejoin, Alice wouldn't accept the new key
|
||||
// and reject the securejoin.
|
||||
|
||||
mark_as_verified(&alice, &bob).await;
|
||||
mark_as_verified(&bob, &alice).await;
|
||||
|
||||
alice.create_chat(&bob).await;
|
||||
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
|
||||
let chats = Chatlist::try_load(&alice, DC_GCL_FOR_FORWARDING, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
tcm.section("Bob reinstalls DC");
|
||||
drop(bob);
|
||||
let bob_new = tcm.unconfigured().await;
|
||||
enable_verified_oneonone_chats(&[&bob_new]).await;
|
||||
bob_new.configure_addr("bob@example.net").await;
|
||||
e2ee::ensure_secret_key_exists(&bob_new).await?;
|
||||
|
||||
tcm.send_recv(&bob_new, &alice, "I have a new device").await;
|
||||
|
||||
let contact = alice.add_or_lookup_contact(&bob_new).await;
|
||||
assert_eq!(
|
||||
contact.is_verified(&alice).await.unwrap(),
|
||||
// Bob sent a message with a new key, so he most likely doesn't have
|
||||
// the old key anymore. This means that Alice's device should show
|
||||
// him as unverified:
|
||||
false
|
||||
);
|
||||
let chat = alice.get_chat(&bob_new).await;
|
||||
assert_eq!(chat.is_protected(), false);
|
||||
assert_eq!(chat.is_protection_broken(), true);
|
||||
let chats = Chatlist::try_load(&alice, DC_GCL_FOR_FORWARDING, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
{
|
||||
let alice_bob_chat = alice.get_chat(&bob_new).await;
|
||||
assert!(!alice_bob_chat.can_send(&alice).await?);
|
||||
|
||||
// Alice's UI should still be able to save a draft, which Alice started to type right when she got Bob's message:
|
||||
let mut msg = Message::new_text("Draftttt".to_string());
|
||||
alice_bob_chat.id.set_draft(&alice, Some(&mut msg)).await?;
|
||||
assert_eq!(
|
||||
alice_bob_chat.id.get_draft(&alice).await?.unwrap().text,
|
||||
"Draftttt"
|
||||
);
|
||||
}
|
||||
|
||||
tcm.execute_securejoin(&alice, &bob_new).await;
|
||||
assert_verified(&alice, &bob_new, ProtectionStatus::Protected).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that message from old DC setup does not break
|
||||
/// new verified chat.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_message_from_old_dc_setup() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob_old = &tcm.unconfigured().await;
|
||||
|
||||
enable_verified_oneonone_chats(&[alice, bob_old]).await;
|
||||
mark_as_verified(bob_old, alice).await;
|
||||
bob_old.configure_addr("bob@example.net").await;
|
||||
mark_as_verified(bob_old, alice).await;
|
||||
let chat = bob_old.create_chat(alice).await;
|
||||
let sent_old = bob_old
|
||||
.send_text(chat.id, "Soon i'll have a new device")
|
||||
@@ -759,22 +515,15 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
|
||||
assert_verified(alice, bob, ProtectionStatus::Protected).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;
|
||||
// The outdated Bob's Autocrypt header isn't applied, so the verification preserves.
|
||||
|
||||
// The outdated Bob's Autocrypt header isn't applied
|
||||
// and the message goes to another chat, so the verification preserves.
|
||||
assert!(contact.is_verified(alice).await.unwrap());
|
||||
let chat = alice.get_chat(bob).await;
|
||||
assert!(chat.is_protected());
|
||||
assert_eq!(chat.is_protection_broken(), false);
|
||||
let protection_msg = alice.get_last_msg().await;
|
||||
assert_eq!(
|
||||
protection_msg.param.get_cmd(),
|
||||
SystemMessage::ChatProtectionEnabled
|
||||
);
|
||||
assert!(protection_msg.timestamp_sort >= msg.timestamp_rcvd);
|
||||
alice
|
||||
.golden_test_chat(msg.chat_id, "verified_chats_message_from_old_dc_setup")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -811,43 +560,6 @@ async fn test_verify_then_verify_again() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test:
|
||||
/// - Verify a contact
|
||||
/// - The contact stops using DC and sends a message from a classical MUA instead
|
||||
/// - Delete the 1:1 chat
|
||||
/// - Create a 1:1 chat
|
||||
/// - Check that the created chat is not marked as protected
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_oneonone_chat_with_former_verified_contact() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice]).await;
|
||||
|
||||
mark_as_verified(&alice, &bob).await;
|
||||
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"Subject: Message from bob\r\n\
|
||||
From: <bob@example.net>\r\n\
|
||||
To: <alice@example.org>\r\n\
|
||||
Date: Mon, 12 Dec 2022 14:33:39 +0000\r\n\
|
||||
Message-ID: <abcd@example.net>\r\n\
|
||||
\r\n\
|
||||
Heyho!\r\n",
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
alice.create_chat(&bob).await;
|
||||
|
||||
assert_verified(&alice, &bob, ProtectionStatus::Unprotected).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that on the second device of a protected group creator the first message is
|
||||
/// `SystemMessage::ChatProtectionEnabled` and the second one is the message populating the group.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -893,7 +605,7 @@ async fn test_verified_member_added_reordering() -> Result<()> {
|
||||
let fiona = &tcm.fiona().await;
|
||||
enable_verified_oneonone_chats(&[alice, bob, fiona]).await;
|
||||
|
||||
let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
// Bob and Fiona scan Alice's QR code.
|
||||
tcm.execute_securejoin(bob, alice).await;
|
||||
|
||||
@@ -153,104 +153,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_out_of_order_group_name() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <msg1@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde123456\n\
|
||||
Chat-Group-Name: initial name\n\
|
||||
Date: Sun, 22 Mar 2021 01:00:00 +0000\n\
|
||||
\n\
|
||||
first message\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
let chat = Chat::load_from_db(&t, msg.chat_id).await?;
|
||||
assert_eq!(chat.name, "initial name");
|
||||
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <msg3@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde123456\n\
|
||||
Chat-Group-Name: =?utf-8?q?another=0Aname update?=\n\
|
||||
Chat-Group-Name-Changed: =?utf-8?q?a=0Aname update?=\n\
|
||||
Date: Sun, 22 Mar 2021 03:00:00 +0000\n\
|
||||
\n\
|
||||
third message\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <msg2@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde123456\n\
|
||||
Chat-Group-Name: =?utf-8?q?a=0Aname update?=\n\
|
||||
Chat-Group-Name-Changed: initial name\n\
|
||||
Date: Sun, 22 Mar 2021 02:00:00 +0000\n\
|
||||
\n\
|
||||
second message\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
let chat = Chat::load_from_db(&t, msg.chat_id).await?;
|
||||
assert_eq!(chat.name, "another name update");
|
||||
|
||||
// Assert that the \n was correctly removed from the group name also in the system message
|
||||
assert_eq!(msg.text.contains('\n'), false);
|
||||
|
||||
// This doesn't update the name because Date is the same and name is greater.
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <msg4@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde123456\n\
|
||||
Chat-Group-Name: another name update 4\n\
|
||||
Chat-Group-Name-Changed: another name update\n\
|
||||
Date: Sun, 22 Mar 2021 03:00:00 +0000\n\
|
||||
\n\
|
||||
4th message\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(&t, chat.id).await?;
|
||||
assert_eq!(chat.name, "another name update");
|
||||
|
||||
// This updates the name because Date is the same and name is lower.
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <msg5@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde123456\n\
|
||||
Chat-Group-Name: another name updat\n\
|
||||
Chat-Group-Name-Changed: another name update\n\
|
||||
Date: Sun, 22 Mar 2021 03:00:00 +0000\n\
|
||||
\n\
|
||||
5th message\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(&t, chat.id).await?;
|
||||
assert_eq!(chat.name, "another name updat");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ use crate::constants::Chattype;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{load_self_public_key, DcKey};
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::log::{info, warn};
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
@@ -962,7 +962,7 @@ impl Message {
|
||||
}
|
||||
|
||||
async fn get_webxdc_self_addr(&self, context: &Context) -> Result<String> {
|
||||
let fingerprint = load_self_public_key(context).await?.dc_fingerprint().hex();
|
||||
let fingerprint = self_fingerprint(context).await?;
|
||||
let data = format!("{}-{}", fingerprint, self.rfc724_mid);
|
||||
let hash = Sha256::digest(data.as_bytes());
|
||||
Ok(format!("{hash:x}"))
|
||||
|
||||
@@ -10,7 +10,6 @@ use crate::chat::{
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::contact::Contact;
|
||||
use crate::download::DownloadState;
|
||||
use crate::ephemeral;
|
||||
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
|
||||
@@ -1601,58 +1600,6 @@ async fn test_webxdc_info_msg_no_cleanup_on_interrupted_series() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_webxdc_opportunistic_encryption() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Bob sends sth. to Alice, Alice has Bob's key
|
||||
let bob_chat_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "chat").await?;
|
||||
add_contact_to_chat(
|
||||
&bob,
|
||||
bob_chat_id,
|
||||
Contact::create(&bob, "", "alice@example.org").await?,
|
||||
)
|
||||
.await?;
|
||||
send_text_msg(&bob, bob_chat_id, "populate".to_string()).await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
|
||||
// Alice sends instance+update to Bob
|
||||
let alice_chat_id = alice.get_last_msg().await.chat_id;
|
||||
alice_chat_id.accept(&alice).await?;
|
||||
let alice_instance = send_webxdc_instance(&alice, alice_chat_id).await?;
|
||||
let sent1 = &alice.pop_sent_msg().await;
|
||||
alice
|
||||
.send_webxdc_status_update(alice_instance.id, r#"{"payload":42}"#)
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let sent2 = &alice.pop_sent_msg().await;
|
||||
let update_msg = sent2.load_from_db().await;
|
||||
assert!(alice_instance.get_showpadlock());
|
||||
assert!(update_msg.get_showpadlock());
|
||||
|
||||
// Bob receives instance+update
|
||||
let bob_instance = bob.recv_msg(sent1).await;
|
||||
bob.recv_msg_trash(sent2).await;
|
||||
assert!(bob_instance.get_showpadlock());
|
||||
|
||||
// Bob adds Claire with unknown key, update to Alice+Claire cannot be encrypted
|
||||
add_contact_to_chat(
|
||||
&bob,
|
||||
bob_chat_id,
|
||||
Contact::create(&bob, "", "claire@example.org").await?,
|
||||
)
|
||||
.await?;
|
||||
bob.send_webxdc_status_update(bob_instance.id, r#"{"payload":43}"#)
|
||||
.await?;
|
||||
bob.flush_status_updates().await?;
|
||||
let sent3 = bob.pop_sent_msg().await;
|
||||
let update_msg = sent3.load_from_db().await;
|
||||
assert!(!update_msg.get_showpadlock());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// check that `info.internet_access` is not set for normal, non-integrated webxdc -
|
||||
// even if they use the deprecated option `request_internet_access` in manifest.toml
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Single#Chat#10: bob@example.net [bob@example.net]
|
||||
Single#Chat#10: bob@example.net [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: Me (Contact#Contact#Self): We share this account √
|
||||
Msg#11: Me (Contact#Contact#Self): I'm Alice too √
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Single#Chat#10: Bob [bob@example.net]
|
||||
Single#Chat#10: Bob [PGP bob@example.net]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#11: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Single#Chat#10: Bob [bob@example.net]
|
||||
Single#Chat#10: Bob [PGP bob@example.net]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#11: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Single#Chat#10: Bob [bob@example.net] 🛡️
|
||||
Single#Chat#10: Bob [PGP bob@example.net] 🛡️
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#11🔒: (Contact#Contact#10): Heyho from my verified device! [FRESH]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Single#Chat#10: Bob [bob@example.net]
|
||||
Single#Chat#10: Bob [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: Me (Contact#Contact#Self): Happy birthday, Bob! √
|
||||
Msg#11: (Contact#Contact#10): Happy birthday to me, Alice! [FRESH]
|
||||
|
||||
5
test-data/golden/test_outgoing_encrypted_msg
Normal file
5
test-data/golden/test_outgoing_encrypted_msg
Normal file
@@ -0,0 +1,5 @@
|
||||
Single#Chat#10: bob@example.net [KEY bob@example.net] 🛡️
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#11🔒: Me (Contact#Contact#Self): Test – This is encrypted, signed, and has an Autocrypt Header without prefer-encrypt=mutual. √
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -1,7 +1,4 @@
|
||||
Single#Chat#10: bob@example.net [bob@example.net] 🛡️
|
||||
Single#Chat#11: bob@example.net [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#11🔒: (Contact#Contact#10): Heyho from DC [FRESH]
|
||||
Msg#12: Me (Contact#Contact#Self): One classical MUA message √
|
||||
Msg#13🔒: Me (Contact#Contact#Self): Sending with DC again √
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
6
test-data/golden/test_outgoing_mua_msg_pgp
Normal file
6
test-data/golden/test_outgoing_mua_msg_pgp
Normal file
@@ -0,0 +1,6 @@
|
||||
Single#Chat#10: bob@example.net [KEY bob@example.net] 🛡️
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#11🔒: (Contact#Contact#10): Heyho from DC [FRESH]
|
||||
Msg#13🔒: Me (Contact#Contact#Self): Sending with DC again √
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -1,4 +1,4 @@
|
||||
Single#Chat#10: Bob [bob@example.net] 🛡️
|
||||
Single#Chat#10: Bob [PGP bob@example.net] 🛡️
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#11: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌]
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
Single#Chat#10: bob@example.net [bob@example.net] 🛡️
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#11🔒: (Contact#Contact#10): Now i have it! [FRESH]
|
||||
Msg#12: info (Contact#Contact#Info): bob@example.net sent a message from another device. [NOTICED][INFO 🛡️❌]
|
||||
Msg#13: (Contact#Contact#10): Soon i'll have a new device [FRESH]
|
||||
Msg#14: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -1,6 +1,3 @@
|
||||
From - Tue, 29 Aug 2023 20:24:31 GMT
|
||||
X-Mozilla-Status: 0801
|
||||
X-Mozilla-Status2: 00000000
|
||||
Message-ID: <05eae88e-35c9-5e5e-405f-11e8a3b44513@example.org>
|
||||
Date: Tue, 29 Aug 2023 17:24:31 -0300
|
||||
MIME-Version: 1.0
|
||||
@@ -9,10 +6,6 @@ User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
|
||||
Content-Language: en-US
|
||||
To: bob@example.net
|
||||
From: Alice <alice@example.org>
|
||||
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
|
||||
attachmentreminder=0; deliveryformat=0
|
||||
X-Identity-Key: id3
|
||||
Fcc: imap://alice%40example.org@in.example.org/Sent
|
||||
Subject: ...
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="------------2IZJ0SaOTFMF25fU1nsH7bxg"
|
||||
|
||||
@@ -7,6 +7,15 @@ Content-Language: en-US
|
||||
To: Bob <bob@example.net>
|
||||
From: Alice <alice@example.org>
|
||||
Subject: ...
|
||||
Autocrypt: addr=alice@example.org; keydata=
|
||||
xjMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5DN
|
||||
GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CkAQTFggAOBYhBC5vossjtTLXKGNLWGSwj2Gp
|
||||
7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGSwj2Gp7ZRDE3oA/i4M
|
||||
CyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pMAPkBjOnGOtx2SZpQvJGTa9h804RY6iDr
|
||||
RuI8A/8tEEXAA844BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp
|
||||
01JrRe6Xqy22HQMBCAfCeAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsM
|
||||
AAoJEGSwj2Gp7ZRDLo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIy
|
||||
VfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp3jsMAg==
|
||||
Content-Type: multipart/encrypted;
|
||||
protocol="application/pgp-encrypted";
|
||||
boundary="------------68Kl9HSVGFVUMdZIowLUKskt"
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
From - Thu, 02 Nov 2023 05:20:27 GMT
|
||||
X-Mozilla-Status: 0801
|
||||
X-Mozilla-Status2: 00000000
|
||||
Message-ID: <956fad6d-206e-67af-2443-3ea5819418ff@example.org>
|
||||
Date: Thu, 2 Nov 2023 02:20:27 -0300
|
||||
MIME-Version: 1.0
|
||||
@@ -43,10 +40,6 @@ Autocrypt: addr=alice@example.org; keydata=
|
||||
MGDpk/1NVuMnIHJESRg/SSFV6sElgq38k9wAT2oUqLcYvYI07nHmnuciaGygkCcGt+l2PvAa
|
||||
j4mkQQvMU0cNRDBybk5aKi820oGIJjT7e+5RnD2mYZQdOAbQhDVCHvrfS1I60bsHT1MHqyAa
|
||||
/qMLjKwBpKEd/w==
|
||||
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
|
||||
attachmentreminder=0; deliveryformat=0
|
||||
X-Identity-Key: id3
|
||||
Fcc: imap://alice%40example.org@in.example.org/Sent
|
||||
Subject: ...
|
||||
Content-Type: multipart/encrypted;
|
||||
protocol="application/pgp-encrypted";
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
From - Sun, 19 Nov 2023 01:08:24 GMT
|
||||
X-Mozilla-Status: 0800
|
||||
X-Mozilla-Status2: 00000000
|
||||
Message-ID: <38a2a29b-8261-403b-abb5-56b0a87d2ff4@example.org>
|
||||
Date: Sat, 18 Nov 2023 22:08:23 -0300
|
||||
MIME-Version: 1.0
|
||||
@@ -42,10 +39,6 @@ Autocrypt: addr=alice@example.org; keydata=
|
||||
MGDpk/1NVuMnIHJESRg/SSFV6sElgq38k9wAT2oUqLcYvYI07nHmnuciaGygkCcGt+l2PvAa
|
||||
j4mkQQvMU0cNRDBybk5aKi820oGIJjT7e+5RnD2mYZQdOAbQhDVCHvrfS1I60bsHT1MHqyAa
|
||||
/qMLjKwBpKEd/w==
|
||||
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
|
||||
attachmentreminder=0; deliveryformat=0
|
||||
X-Identity-Key: id3
|
||||
Fcc: imap://alice%40example.org@in.example.org/Sent
|
||||
Subject: Hello!
|
||||
Content-Type: multipart/encrypted;
|
||||
protocol="application/pgp-encrypted";
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
From - Thu, 15 Dec 2022 14:45:17 GMT
|
||||
X-Mozilla-Status: 0801
|
||||
X-Mozilla-Status2: 00000000
|
||||
Message-ID: <db254c95-c23e-2ad5-14db-11ad1f374dbf@example.org>
|
||||
Date: Thu, 15 Dec 2022 11:45:16 -0300
|
||||
MIME-Version: 1.0
|
||||
@@ -10,10 +7,6 @@ Content-Language: en-US
|
||||
To: bob@example.net
|
||||
From: Alice <alice@example.org>
|
||||
Subject: test message 15:53
|
||||
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
|
||||
attachmentreminder=0; deliveryformat=0
|
||||
X-Identity-Key: id3
|
||||
Fcc: imap://alice%40example.org@in.example.org/Sent
|
||||
Content-Type: multipart/signed; micalg=pgp-sha256;
|
||||
protocol="application/pgp-signature";
|
||||
boundary="------------iX39J1p7DOgblwacjo0e7jX7"
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
From - Wed, 14 Dec 2022 18:53:03 GMT
|
||||
X-Mozilla-Status: 0801
|
||||
X-Mozilla-Status2: 00000000
|
||||
Message-ID: <87d75c7e-0f52-1335-e437-af605c09f954@example.org>
|
||||
Date: Wed, 14 Dec 2022 15:53:03 -0300
|
||||
MIME-Version: 1.0
|
||||
@@ -44,10 +41,6 @@ Autocrypt: addr=alice@example.org; keydata=
|
||||
MGDpk/1NVuMnIHJESRg/SSFV6sElgq38k9wAT2oUqLcYvYI07nHmnuciaGygkCcGt+l2PvAa
|
||||
j4mkQQvMU0cNRDBybk5aKi820oGIJjT7e+5RnD2mYZQdOAbQhDVCHvrfS1I60bsHT1MHqyAa
|
||||
/qMLjKwBpKEd/w==
|
||||
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
|
||||
attachmentreminder=0; deliveryformat=0
|
||||
X-Identity-Key: id3
|
||||
Fcc: imap://alice%40example.org@in.example.org/Sent
|
||||
Content-Type: multipart/signed; micalg=pgp-sha256;
|
||||
protocol="application/pgp-signature";
|
||||
boundary="------------x6XEHrf0vHmVgEo6f9bMGGUy"
|
||||
|
||||
Reference in New Issue
Block a user