Merge tag 'v1.122.0'

This commit is contained in:
link2xt
2023-09-12 18:04:05 +00:00
24 changed files with 321 additions and 125 deletions

View File

@@ -1,5 +1,33 @@
# Changelog
## [1.122.0] - 2023-09-12
### API-Changes
- jsonrpc: Return only chat IDs for similar chats.
### Fixes
- Reopen all connections on database passpharse change.
- Do not block new group chats if 1:1 chat is blocked.
- Improve group membership consistency algorithm ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782))([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
- Forbid membership changes from possible non-members ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782)).
- `ChatId::parent_query()`: Don't filter out OutPending and OutFailed messages.
### Build system
- Update to OpenSSL 3.0.
- Bump webpki from 0.22.0 to 0.22.1.
- python: Add link to Mastodon into projects.urls.
### Features / Changes
- Add RSA-4096 key generation support.
### Refactor
- pgp: Add constants for encryption algorithm and hash.
## [1.121.0] - 2023-09-06
### API-Changes
@@ -2787,3 +2815,4 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1
[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0
[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0
[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0

24
Cargo.lock generated
View File

@@ -1085,7 +1085,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.121.0"
version = "1.122.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1162,7 +1162,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.121.0"
version = "1.122.0"
dependencies = [
"anyhow",
"async-channel",
@@ -1186,7 +1186,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.121.0"
version = "1.122.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1201,7 +1201,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.121.0"
version = "1.122.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1226,7 +1226,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.121.0"
version = "1.122.0"
dependencies = [
"anyhow",
"deltachat",
@@ -3025,11 +3025,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.55"
version = "0.10.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.3.3",
"cfg-if",
"foreign-types",
"libc",
@@ -3057,18 +3057,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "111.26.0+1.1.1u"
version = "300.1.3+3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37"
checksum = "cd2c101a165fff9935e34def4669595ab1c7847943c42be86e21503e482be107"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.90"
version = "0.9.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d"
dependencies = [
"cc",
"libc",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.121.0"
version = "1.122.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.67"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.121.0"
version = "1.122.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -443,7 +443,9 @@ char* dc_get_blobdir (const dc_context_t* context);
* DC_KEY_GEN_RSA2048 (1)=
* generate RSA 2048 keypair
* DC_KEY_GEN_ED25519 (2)=
* generate Ed25519 keypair
* generate Curve25519 keypair
* DC_KEY_GEN_RSA4096 (3)=
* generate RSA 4096 keypair
* - `save_mime_headers` = 1=save mime headers
* and make dc_get_mime_headers() work for subsequent calls,
* 0=do not save mime headers (default)
@@ -6309,6 +6311,7 @@ void dc_event_unref(dc_event_t* event);
#define DC_KEY_GEN_DEFAULT 0
#define DC_KEY_GEN_RSA2048 1
#define DC_KEY_GEN_ED25519 2
#define DC_KEY_GEN_RSA4096 3
/**

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.121.0"
version = "1.122.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"

View File

@@ -39,7 +39,6 @@ pub mod types;
use num_traits::FromPrimitive;
use types::account::Account;
use types::chat::FullChat;
use types::chat_list::ChatListEntry;
use types::contact::ContactObject;
use types::events::Event;
use types::http::HttpResponse;
@@ -571,22 +570,18 @@ impl CommandApi {
}
/// Returns chats similar to the given one.
async fn get_similar_chatlist_entries(
&self,
account_id: u32,
chat_id: u32,
) -> Result<Vec<ChatListEntry>> {
///
/// Experimental API, subject to change without notice.
async fn get_similar_chat_ids(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let chat_id = ChatId::new(chat_id);
let list = chat_id.get_similar_chatlist(&ctx).await?;
let mut l: Vec<ChatListEntry> = Vec::with_capacity(list.len());
for i in 0..list.len() {
l.push(ChatListEntry(
list.get_chat_id(i)?.to_u32(),
list.get_msg_id(i)?.unwrap_or_default().to_u32(),
));
}
Ok(l)
let list = chat_id
.get_similar_chat_ids(&ctx)
.await?
.into_iter()
.map(|(chat_id, _metric)| chat_id.to_u32())
.collect();
Ok(list)
}
async fn get_chatlist_items_by_entries(

View File

@@ -8,15 +8,12 @@ use deltachat::{
chatlist::Chatlist,
};
use num_traits::cast::ToPrimitive;
use serde::{Deserialize, Serialize};
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::message::MessageViewtype;
#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)]
pub struct ChatListEntry(pub u32, pub u32);
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "kind")]
pub enum ChatListItemFetchResult {

View File

@@ -55,5 +55,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.121.0"
"version": "1.122.0"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.121.0"
version = "1.122.0"
license = "MPL-2.0"
edition = "2021"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.121.0"
version = "1.122.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -90,6 +90,7 @@ module.exports = {
DC_KEY_GEN_DEFAULT: 0,
DC_KEY_GEN_ED25519: 2,
DC_KEY_GEN_RSA2048: 1,
DC_KEY_GEN_RSA4096: 3,
DC_LP_AUTH_NORMAL: 4,
DC_LP_AUTH_OAUTH2: 2,
DC_MEDIA_QUALITY_BALANCED: 0,

View File

@@ -90,6 +90,7 @@ export enum C {
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
DC_KEY_GEN_RSA4096 = 3,
DC_LP_AUTH_NORMAL = 4,
DC_LP_AUTH_OAUTH2 = 2,
DC_MEDIA_QUALITY_BALANCED = 0,

View File

@@ -60,5 +60,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.121.0"
"version": "1.122.0"
}

View File

@@ -34,6 +34,7 @@ dynamic = [
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
"Documentation" = "https://py.delta.chat/"
"Mastodon" = "https://chaos.social/@delta"
[project.entry-points.pytest11]
"deltachat.testplugin" = "deltachat.testplugin"

View File

@@ -1699,6 +1699,36 @@ def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
assert not ch.is_protected()
def test_qr_new_group_unblocked(acfactory, lp):
"""Regression test for a bug intoduced in core v1.113.0.
ac2 scans a verified group QR code created by ac1.
This results in creation of a blocked 1:1 chat with ac1 on ac2,
but ac1 contact is not blocked on ac2.
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
ac2 should receive a message and create a contact request for the group.
Due to a bug previously ac2 created a blocked group.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group_chat("Group for joining", verified=True)
qr = ac1_chat.get_join_qr()
ac2.qr_join_chat(qr)
ac1._evtracker.wait_securejoin_inviter_progress(1000)
ac1_new_chat = ac1.create_group_chat("Another group")
ac1_new_chat.add_contact(ac2)
ac1_new_chat.send_text("Hello!")
# Receive "Member added" message.
ac2._evtracker.wait_next_incoming_message()
# Receive "Hello!" message.
ac2_msg = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg.text == "Hello!"
assert ac2_msg.chat.is_contact_request()
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification via gossip in a verified group

View File

@@ -1 +1 @@
2023-09-06
2023-09-12

View File

@@ -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,
)

View File

@@ -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]

View File

@@ -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;

View File

@@ -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)?;

View File

@@ -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 {

View File

@@ -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(())
}

View File

@@ -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);