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:
@@ -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((
|
||||
recalc_fingerprints,
|
||||
update_icons,
|
||||
disable_server_delete,
|
||||
recode_avatar,
|
||||
))
|
||||
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((
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user