mirror of
https://github.com/chatmail/core.git
synced 2026-04-29 11:26:29 +03:00
Merge tag 'v1.122.0'
This commit is contained in:
@@ -1054,7 +1054,7 @@ impl ChatId {
|
||||
let sql = &context.sql;
|
||||
let query = format!(
|
||||
"SELECT {fields} \
|
||||
FROM msgs WHERE chat_id=? AND state NOT IN (?, ?, ?, ?) AND NOT hidden \
|
||||
FROM msgs WHERE chat_id=? AND state NOT IN (?, ?) AND NOT hidden \
|
||||
ORDER BY timestamp DESC, id DESC \
|
||||
LIMIT 1;"
|
||||
);
|
||||
@@ -1065,8 +1065,11 @@ impl ChatId {
|
||||
self,
|
||||
MessageState::OutPreparing,
|
||||
MessageState::OutDraft,
|
||||
MessageState::OutPending,
|
||||
MessageState::OutFailed,
|
||||
// We don't filter `OutPending` and `OutFailed` messages because the new message
|
||||
// for which `parent_query()` is done may assume that it will be received in a
|
||||
// context affected by those messages, e.g. they could add new members to a
|
||||
// group and the new message will contain them in "To:". Anyway recipients must
|
||||
// be prepared to orphaned references.
|
||||
),
|
||||
f,
|
||||
)
|
||||
|
||||
@@ -62,8 +62,15 @@ pub enum MediaQuality {
|
||||
pub enum KeyGenType {
|
||||
#[default]
|
||||
Default = 0,
|
||||
|
||||
/// 2048-bit RSA.
|
||||
Rsa2048 = 1,
|
||||
|
||||
/// [Ed25519](https://ed25519.cr.yp.to/) signature and X25519 encryption.
|
||||
Ed25519 = 2,
|
||||
|
||||
/// 4096-bit RSA.
|
||||
Rsa4096 = 3,
|
||||
}
|
||||
|
||||
/// Video chat URL type.
|
||||
@@ -224,6 +231,7 @@ mod tests {
|
||||
assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap());
|
||||
assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap());
|
||||
assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
|
||||
assert_eq!(KeyGenType::Rsa4096, KeyGenType::from_i32(3).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1465,6 +1465,35 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_context_change_passphrase() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let id = 1;
|
||||
let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
assert_eq!(context.is_open().await, true);
|
||||
|
||||
context
|
||||
.set_config(Config::Addr, Some("alice@example.org"))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.change_passphrase("bar".to_string())
|
||||
.await
|
||||
.context("Failed to change passphrase")?;
|
||||
|
||||
assert_eq!(
|
||||
context.get_config(Config::Addr).await?.unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ongoing() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
19
src/pgp.rs
19
src/pgp.rs
@@ -29,6 +29,12 @@ pub(crate) const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
|
||||
#[allow(missing_docs)]
|
||||
pub const HEADER_SETUPCODE: &str = "passphrase-begin";
|
||||
|
||||
/// Preferred symmetric encryption algorithm.
|
||||
const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128;
|
||||
|
||||
/// Preferred cryptographic hash.
|
||||
const HASH_ALGORITHM: HashAlgorithm = HashAlgorithm::SHA2_256;
|
||||
|
||||
/// A wrapper for rPGP public key types
|
||||
#[derive(Debug)]
|
||||
enum SignedPublicKeyOrSubkey<'a> {
|
||||
@@ -135,6 +141,7 @@ pub struct KeyPair {
|
||||
pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Result<KeyPair> {
|
||||
let (secret_key_type, public_key_type) = match keygen_type {
|
||||
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
|
||||
KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)),
|
||||
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
|
||||
};
|
||||
|
||||
@@ -247,11 +254,13 @@ pub async fn pk_encrypt(
|
||||
// TODO: measure time
|
||||
let encrypted_msg = if let Some(ref skey) = private_key_for_signing {
|
||||
lit_msg
|
||||
.sign(skey, || "".into(), Default::default())
|
||||
.sign(skey, || "".into(), HASH_ALGORITHM)
|
||||
.and_then(|msg| msg.compress(CompressionAlgorithm::ZLIB))
|
||||
.and_then(|msg| msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs))
|
||||
.and_then(|msg| {
|
||||
msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)
|
||||
})
|
||||
} else {
|
||||
lit_msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs)
|
||||
lit_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)
|
||||
};
|
||||
|
||||
let msg = encrypted_msg?;
|
||||
@@ -270,7 +279,7 @@ pub fn pk_calc_signature(
|
||||
let msg = Message::new_literal_bytes("", plain).sign(
|
||||
private_key_for_signing,
|
||||
|| "".into(),
|
||||
Default::default(),
|
||||
HASH_ALGORITHM,
|
||||
)?;
|
||||
let signature = msg.into_signature().to_armored_string(None)?;
|
||||
Ok(signature)
|
||||
@@ -359,7 +368,7 @@ pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
|
||||
let mut rng = thread_rng();
|
||||
let s2k = StringToKey::new_default(&mut rng);
|
||||
let msg =
|
||||
lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase)?;
|
||||
lit_msg.encrypt_with_password(&mut rng, s2k, SYMMETRIC_KEY_ALGORITHM, || passphrase)?;
|
||||
|
||||
let encoded_msg = msg.to_armored_string(None)?;
|
||||
|
||||
|
||||
@@ -550,19 +550,30 @@ async fn add_parts(
|
||||
// signals whether the current user is a bot
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
|
||||
let create_blocked = match test_normal_chat {
|
||||
Some(ChatIdBlocked {
|
||||
id: _,
|
||||
blocked: Blocked::Request,
|
||||
}) if is_bot => Blocked::Not,
|
||||
Some(ChatIdBlocked { id: _, blocked }) => blocked,
|
||||
None => {
|
||||
if is_bot {
|
||||
Blocked::Not
|
||||
} else {
|
||||
Blocked::Request
|
||||
let create_blocked_default = if is_bot {
|
||||
Blocked::Not
|
||||
} else {
|
||||
Blocked::Request
|
||||
};
|
||||
let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat {
|
||||
match blocked {
|
||||
Blocked::Request => create_blocked_default,
|
||||
Blocked::Not => Blocked::Not,
|
||||
Blocked::Yes => {
|
||||
if Contact::is_blocked_load(context, from_id).await? {
|
||||
// User has blocked the contact.
|
||||
// Block the group contact created as well.
|
||||
Blocked::Yes
|
||||
} else {
|
||||
// 1:1 chat is blocked, but the contact is not.
|
||||
// This happens when 1:1 chat is hidden
|
||||
// during scanning of a group invitation code.
|
||||
Blocked::Request
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
create_blocked_default
|
||||
};
|
||||
|
||||
if chat_id.is_none() {
|
||||
@@ -1701,40 +1712,37 @@ async fn apply_group_changes(
|
||||
false
|
||||
};
|
||||
|
||||
// Whether to allow any changes to the member list at all.
|
||||
let allow_member_list_changes = if chat::is_contact_in_chat(context, chat_id, ContactId::SELF)
|
||||
.await?
|
||||
|| self_added
|
||||
|| !mime_parser.has_chat_version()
|
||||
{
|
||||
// Reject old group changes.
|
||||
chat_id
|
||||
let is_from_in_chat = !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await?
|
||||
|| chat::is_contact_in_chat(context, chat_id, from_id).await?;
|
||||
|
||||
// Reject group membership changes from non-members and old changes.
|
||||
let allow_member_list_changes = is_from_in_chat
|
||||
&& chat_id
|
||||
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
|
||||
.await?
|
||||
} else {
|
||||
// Member list changes are not allowed if we're not in the group
|
||||
// and are not explicitly added.
|
||||
// This message comes from a Delta Chat that restored an old backup
|
||||
// or the message is a MUA reply to an old message.
|
||||
false
|
||||
};
|
||||
.await?;
|
||||
|
||||
// Whether to rebuild the member list from scratch.
|
||||
let recreate_member_list = if allow_member_list_changes {
|
||||
let recreate_member_list = {
|
||||
// Recreate member list if the message comes from a MUA as these messages do _not_ set add/remove headers.
|
||||
// Always recreate membership list if self has been added.
|
||||
if !mime_parser.has_chat_version() || self_added {
|
||||
true
|
||||
} else {
|
||||
match mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
!mime_parser.has_chat_version()
|
||||
// Always recreate membership list if SELF has been added. The older versions of DC
|
||||
// don't always set "In-Reply-To" to the latest message they sent, but to the latest
|
||||
// delivered message (so it's a race), so we have this heuristic here.
|
||||
|| self_added
|
||||
|| match mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
// If we don't know the referenced message, we missed some messages.
|
||||
// Maybe they added/removed members, so we need to recreate our member list.
|
||||
Some(reply_to) => rfc724_mid_exists(context, reply_to).await?.is_none(),
|
||||
None => false,
|
||||
}
|
||||
} && {
|
||||
if !allow_member_list_changes {
|
||||
info!(
|
||||
context,
|
||||
"Ignoring a try to recreate member list of {chat_id} by {from_id}.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
false
|
||||
allow_member_list_changes
|
||||
};
|
||||
|
||||
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
|
||||
@@ -1843,43 +1851,35 @@ async fn apply_group_changes(
|
||||
|
||||
// Recreate the member list.
|
||||
if recreate_member_list {
|
||||
if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
|
||||
warn!(
|
||||
context,
|
||||
"Contact {from_id} attempts to modify group chat {chat_id} member list without being a member."
|
||||
);
|
||||
} else {
|
||||
// Only delete old contacts if the sender is not a classical MUA user:
|
||||
// Classical MUA users usually don't intend to remove users from an email
|
||||
// thread, so if they removed a recipient then it was probably by accident.
|
||||
if mime_parser.has_chat_version() {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,))
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut members_to_add = HashSet::new();
|
||||
members_to_add.extend(to_ids);
|
||||
members_to_add.insert(ContactId::SELF);
|
||||
|
||||
if !from_id.is_special() {
|
||||
members_to_add.insert(from_id);
|
||||
}
|
||||
|
||||
if let Some(removed_id) = removed_id {
|
||||
members_to_add.remove(&removed_id);
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Recreating chat {chat_id} with members {members_to_add:?}."
|
||||
);
|
||||
|
||||
chat::add_to_chat_contacts_table(context, chat_id, &Vec::from_iter(members_to_add))
|
||||
// Only delete old contacts if the sender is not a classical MUA user:
|
||||
// Classical MUA users usually don't intend to remove users from an email
|
||||
// thread, so if they removed a recipient then it was probably by accident.
|
||||
if mime_parser.has_chat_version() {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,))
|
||||
.await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
|
||||
let mut members_to_add = HashSet::new();
|
||||
members_to_add.extend(to_ids);
|
||||
members_to_add.insert(ContactId::SELF);
|
||||
|
||||
if !from_id.is_special() {
|
||||
members_to_add.insert(from_id);
|
||||
}
|
||||
|
||||
if let Some(removed_id) = removed_id {
|
||||
members_to_add.remove(&removed_id);
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Recreating chat {chat_id} with members {members_to_add:?}."
|
||||
);
|
||||
|
||||
chat::add_to_chat_contacts_table(context, chat_id, &Vec::from_iter(members_to_add)).await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::chat::{
|
||||
};
|
||||
use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS};
|
||||
use crate::imap::prefetch_should_download;
|
||||
use crate::message::Message;
|
||||
@@ -3611,3 +3612,70 @@ async fn test_mua_can_readd() -> Result<()> {
|
||||
assert!(is_contact_in_chat(&alice, alice_chat.id, ContactId::SELF).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?,
|
||||
)
|
||||
.await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
|
||||
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
|
||||
// Bob missed the message adding them, but must recreate the member list.
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1);
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?,
|
||||
)
|
||||
.await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
|
||||
let fiona = TestContext::new_fiona().await;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
Contact::create(
|
||||
&alice,
|
||||
"fiona",
|
||||
&fiona.get_config(Config::Addr).await?.unwrap(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
.await?;
|
||||
let fiona_chat_id = fiona.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
fiona_chat_id.accept(&fiona).await?;
|
||||
|
||||
send_text_msg(&fiona, fiona_chat_id, "hi".to_string()).await?;
|
||||
bob.recv_msg(&fiona.pop_sent_msg().await).await;
|
||||
|
||||
// Bob missed the message adding fiona, but mustn't recreate the member list.
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
let bob_alice_contact = Contact::create(
|
||||
&bob,
|
||||
"alice",
|
||||
&alice.get_config(Config::Addr).await?.unwrap(),
|
||||
)
|
||||
.await?;
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, bob_alice_contact).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
36
src/sql.rs
36
src/sql.rs
@@ -310,12 +310,17 @@ impl Sql {
|
||||
/// It is impossible to turn encrypted database into unencrypted
|
||||
/// and vice versa this way, use import/export for this.
|
||||
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
|
||||
self.call_write(move |conn| {
|
||||
conn.pragma_update(None, "rekey", passphrase)
|
||||
.context("failed to set PRAGMA rekey")?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
let mut lock = self.pool.write().await;
|
||||
|
||||
let pool = lock.take().context("SQL connection pool is not open")?;
|
||||
let conn = pool.get().await?;
|
||||
conn.pragma_update(None, "rekey", passphrase.clone())
|
||||
.context("failed to set PRAGMA rekey")?;
|
||||
drop(pool);
|
||||
|
||||
*lock = Some(Self::new_pool(&self.dbfile, passphrase.to_string())?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Locks the write transactions mutex in order to make sure that there never are
|
||||
@@ -1265,7 +1270,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_change_passphrase() -> Result<()> {
|
||||
async fn test_sql_change_passphrase() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
@@ -1289,6 +1294,23 @@ mod tests {
|
||||
sql.change_passphrase("bar".to_string())
|
||||
.await
|
||||
.context("failed to change passphrase")?;
|
||||
|
||||
// Test that at least two connections are still working.
|
||||
// This ensures that not only the connection which changed the password is working,
|
||||
// but other connections as well.
|
||||
{
|
||||
let lock = sql.pool.read().await;
|
||||
let pool = lock.as_ref().unwrap();
|
||||
let conn1 = pool.get().await?;
|
||||
let conn2 = pool.get().await?;
|
||||
conn1
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.unwrap();
|
||||
conn2
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
sql.close().await;
|
||||
|
||||
let sql = Sql::new(dbfile);
|
||||
|
||||
Reference in New Issue
Block a user