mirror of
https://github.com/chatmail/core.git
synced 2026-10-03 11:40:38 +03:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e5b440a4d | ||
|
|
5b49541e16 |
Generated
+76
-45
@@ -2091,6 +2091,12 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@@ -2411,16 +2417,34 @@ checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.10.0"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2954,7 +2978,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3261,11 +3285,12 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
version = "0.3.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@@ -3344,12 +3369,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.35.0"
|
||||
version = "0.38.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
|
||||
checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
@@ -3421,7 +3445,7 @@ version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5211,10 +5235,20 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.37.0"
|
||||
name = "rsqlite-vfs"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
|
||||
checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.40.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"fallible-iterator",
|
||||
@@ -5222,6 +5256,7 @@ dependencies = [
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
"sqlite-wasm-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5926,6 +5961,18 @@ dependencies = [
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlite-wasm-rs"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"js-sys",
|
||||
"rsqlite-vfs",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
@@ -6833,48 +6880,32 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.50"
|
||||
version = "0.4.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
|
||||
checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.100"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -6882,22 +6913,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.100"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"wasm-bindgen-backend",
|
||||
"syn 3.0.4",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.100"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -6917,9 +6948,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.77"
|
||||
version = "0.3.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
|
||||
checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
||||
+3
-3
@@ -85,7 +85,7 @@ quick-xml = { version = "0.41", features = ["escape-html"] }
|
||||
rand-old = { package = "rand", version = "0.8" }
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rusqlite = { workspace = true, features = ["backup"] }
|
||||
sanitize-filename = { workspace = true }
|
||||
sdp = "0.17.1"
|
||||
serde_json = { workspace = true }
|
||||
@@ -194,7 +194,7 @@ nu-ansi-term = "0.50"
|
||||
num-traits = "0.2"
|
||||
rand = "0.9"
|
||||
regex = "1.12"
|
||||
rusqlite = "0.37"
|
||||
rusqlite = "0.40.2"
|
||||
sanitize-filename = "0.6"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
@@ -209,7 +209,7 @@ yerpc = "0.7"
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
vendored = [
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
||||
"rusqlite/bundled",
|
||||
"async-native-tls/vendored"
|
||||
]
|
||||
|
||||
|
||||
@@ -304,21 +304,6 @@ dc_context_t* dc_context_new_closed (const char* dbfile);
|
||||
int dc_context_open (dc_context_t *context, const char* passphrase);
|
||||
|
||||
|
||||
/**
|
||||
* Changes the passphrase on the open database.
|
||||
* Deprecated 2025-11, see `dc_context_open()` for reasoning.
|
||||
*
|
||||
* Existing database must already be encrypted and the passphrase cannot be NULL or empty.
|
||||
* It is impossible to encrypt unencrypted database with this method and vice versa.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param passphrase The new passphrase.
|
||||
* @return 1 on success, 0 on error.
|
||||
*/
|
||||
int dc_context_change_passphrase (dc_context_t* context, const char* passphrase);
|
||||
|
||||
|
||||
/**
|
||||
* Returns 1 if database is open.
|
||||
*
|
||||
|
||||
@@ -160,24 +160,6 @@ pub unsafe extern "C" fn dc_context_open(
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_context_change_passphrase(
|
||||
context: *mut dc_context_t,
|
||||
passphrase: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_context_change_passphrase()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let ctx = unsafe { &*context };
|
||||
let passphrase = to_string_lossy(passphrase);
|
||||
block_on(ctx.change_passphrase(passphrase))
|
||||
.context("dc_context_change_passphrase() failed")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct JsonrpcReaction {
|
||||
emoji: String,
|
||||
|
||||
/// Emoji frequency.
|
||||
count: usize,
|
||||
count: u32,
|
||||
|
||||
/// True if we reacted with this emoji.
|
||||
is_from_self: bool,
|
||||
|
||||
@@ -69,8 +69,11 @@ skip = [
|
||||
{ name = "derive_more-impl", version = "1.0.0" },
|
||||
{ name = "derive_more", version = "1.0.0" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "foldhash", version = "0.1.5" },
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "getrandom", version = "0.3.3" },
|
||||
{ name = "hashbrown", version = "0.15.4" },
|
||||
{ name = "hashbrown", version = "0.16.1" },
|
||||
{ name = "heck", version = "0.4.1" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "hybrid-array", version = "0.2.3" },
|
||||
|
||||
+2
-52
@@ -169,9 +169,7 @@ impl Accounts {
|
||||
.with_push_subscriber(self.push_subscriber.clone())
|
||||
.build()
|
||||
.await?;
|
||||
// Try to open without a passphrase,
|
||||
// but do not return an error if account is passphare-protected.
|
||||
ctx.open("".to_string()).await?;
|
||||
ctx.open().await?;
|
||||
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
self.emit_event(EventType::AccountsChanged);
|
||||
@@ -821,9 +819,7 @@ impl Config {
|
||||
.build()
|
||||
.await
|
||||
.with_context(|| format!("failed to create context from file {dbfile:?}"))?;
|
||||
// Try to open without a passphrase,
|
||||
// but do not return an error if account is passphare-protected.
|
||||
ctx.open("".to_string()).await?;
|
||||
ctx.open().await?;
|
||||
|
||||
accounts.insert(account_config.id, ctx);
|
||||
}
|
||||
@@ -1272,52 +1268,6 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_encrypted_account() -> Result<()> {
|
||||
let dir = tempfile::tempdir().context("failed to create tempdir")?;
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable)
|
||||
.await
|
||||
.context("failed to create accounts manager")?;
|
||||
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
let account_id = accounts
|
||||
.add_closed_account()
|
||||
.await
|
||||
.context("failed to add closed account")?;
|
||||
let account = accounts
|
||||
.get_selected_account()
|
||||
.context("failed to get account")?;
|
||||
assert_eq!(account.id, account_id);
|
||||
let passphrase_set_success = account
|
||||
.open("foobar".to_string())
|
||||
.await
|
||||
.context("failed to set passphrase")?;
|
||||
assert!(passphrase_set_success);
|
||||
drop(accounts);
|
||||
|
||||
let writable = false;
|
||||
let accounts = Accounts::new(p.clone(), writable)
|
||||
.await
|
||||
.context("failed to create second accounts manager")?;
|
||||
let account = accounts
|
||||
.get_selected_account()
|
||||
.context("failed to get account")?;
|
||||
assert_eq!(account.is_open().await, false);
|
||||
|
||||
// Try wrong passphrase.
|
||||
assert_eq!(account.open("barfoo".to_string()).await?, false);
|
||||
assert_eq!(account.open("".to_string()).await?, false);
|
||||
|
||||
assert_eq!(account.open("foobar".to_string()).await?, true);
|
||||
assert_eq!(account.is_open().await, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that accounts share stock string translations.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_accounts_share_translations() -> Result<()> {
|
||||
|
||||
+1
-1
@@ -3985,7 +3985,7 @@ ORDER BY timestamp DESC, id DESC -- final ORDER BY is needed as UNION does not g
|
||||
(
|
||||
chat_id,
|
||||
Viewtype::Webxdc,
|
||||
constants::N_MSGS_TO_NEW_BROADCAST_MEMBER,
|
||||
constants::N_MSGS_TO_NEW_BROADCAST_MEMBER as u32,
|
||||
ContactId::INFO,
|
||||
),
|
||||
|row: &rusqlite::Row| Ok(row.get::<_, MsgId>(0)?),
|
||||
|
||||
+6
-52
@@ -67,7 +67,6 @@ pub struct ContextBuilder {
|
||||
id: u32,
|
||||
events: Events,
|
||||
stock_strings: StockStrings,
|
||||
password: Option<String>,
|
||||
|
||||
push_subscriber: Option<PushSubscriber>,
|
||||
}
|
||||
@@ -84,7 +83,6 @@ impl ContextBuilder {
|
||||
id: rand::random(),
|
||||
events: Events::new(),
|
||||
stock_strings: StockStrings::new(),
|
||||
password: None,
|
||||
push_subscriber: None,
|
||||
}
|
||||
}
|
||||
@@ -131,19 +129,6 @@ impl ContextBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the password to unlock the database.
|
||||
/// Deprecated 2025-11:
|
||||
/// - Db encryption does nothing with blobs, so fs/disk encryption is recommended.
|
||||
/// - Isolation from other apps is needed anyway.
|
||||
///
|
||||
/// If an encrypted database is used it must be opened with a password. Setting a
|
||||
/// password on a new database will enable encryption.
|
||||
#[deprecated(since = "TBD")]
|
||||
pub fn with_password(mut self, password: String) -> Self {
|
||||
self.password = Some(password);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets push subscriber.
|
||||
pub(crate) fn with_push_subscriber(mut self, push_subscriber: PushSubscriber) -> Self {
|
||||
self.push_subscriber = Some(push_subscriber);
|
||||
@@ -168,11 +153,10 @@ impl ContextBuilder {
|
||||
///
|
||||
/// Returns error if context cannot be opened.
|
||||
pub async fn open(self) -> Result<Context> {
|
||||
let password = self.password.clone().unwrap_or_default();
|
||||
let context = self.build().await?;
|
||||
match context.open(password).await? {
|
||||
match context.open().await? {
|
||||
true => Ok(context),
|
||||
false => bail!("database could not be decrypted, incorrect or missing password"),
|
||||
false => bail!("FIXME database could not be decrypted, incorrect or missing password"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -386,10 +370,7 @@ impl Context {
|
||||
let context =
|
||||
Self::new_closed(dbfile, id, events, stock_strings, Default::default()).await?;
|
||||
|
||||
// Open the database if is not encrypted.
|
||||
if context.check_passphrase("".to_string()).await? {
|
||||
context.sql.open(&context, "".to_string()).await?;
|
||||
}
|
||||
context.sql.open(&context).await?;
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
@@ -433,20 +414,9 @@ impl Context {
|
||||
/// Returns true if passphrase is correct, false is passphrase is not correct. Fails on other
|
||||
/// errors.
|
||||
#[deprecated(since = "TBD")]
|
||||
pub async fn open(&self, passphrase: String) -> Result<bool> {
|
||||
if self.sql.check_passphrase(passphrase.clone()).await? {
|
||||
self.sql.open(self, passphrase).await?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes encrypted database passphrase.
|
||||
/// Deprecated 2025-11, see [`ContextBuilder::with_password()`] for reasoning.
|
||||
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
|
||||
self.sql.change_passphrase(passphrase).await?;
|
||||
Ok(())
|
||||
pub async fn open(&self) -> Result<bool> {
|
||||
self.sql.open(self).await?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Returns true if database is open.
|
||||
@@ -454,15 +424,6 @@ impl Context {
|
||||
self.sql.is_open().await
|
||||
}
|
||||
|
||||
/// Tests the database passphrase.
|
||||
///
|
||||
/// Returns true if passphrase is correct.
|
||||
///
|
||||
/// Fails if database is already open.
|
||||
pub(crate) async fn check_passphrase(&self, passphrase: String) -> Result<bool> {
|
||||
self.sql.check_passphrase(passphrase).await
|
||||
}
|
||||
|
||||
pub(crate) fn with_blobdir(
|
||||
dbfile: PathBuf,
|
||||
blobdir: PathBuf,
|
||||
@@ -850,13 +811,6 @@ impl Context {
|
||||
res.insert("number_of_contacts", contacts.to_string());
|
||||
res.insert("database_dir", self.get_dbfile().display().to_string());
|
||||
res.insert("database_version", dbversion.to_string());
|
||||
res.insert(
|
||||
"database_encrypted",
|
||||
self.sql
|
||||
.is_encrypted()
|
||||
.await
|
||||
.map_or_else(|| "closed".to_string(), |b| b.to_string()),
|
||||
);
|
||||
res.insert("journal_mode", journal_mode);
|
||||
res.insert("blobdir", self.get_blobdir().display().to_string());
|
||||
res.insert(
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use anyhow::Context as _;
|
||||
use strum::IntoEnumIterator;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{Chat, MuteDuration, get_chat_contacts, get_chat_msgs, send_msg, set_muted};
|
||||
@@ -486,63 +484,6 @@ async fn test_limit_search_msgs() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_check_passphrase() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let context = ContextBuilder::new(dbfile.clone())
|
||||
.with_id(1)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
assert_eq!(context.is_open().await, true);
|
||||
drop(context);
|
||||
|
||||
let context = ContextBuilder::new(dbfile)
|
||||
.with_id(2)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.is_open().await, false);
|
||||
assert_eq!(context.check_passphrase("bar".to_string()).await?, false);
|
||||
assert_eq!(context.open("false".to_string()).await?, false);
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
|
||||
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 context = ContextBuilder::new(dbfile)
|
||||
.with_id(1)
|
||||
.build()
|
||||
.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;
|
||||
|
||||
+16
-24
@@ -200,6 +200,9 @@ async fn import_backup(
|
||||
backup_to_import: &Path,
|
||||
passphrase: String,
|
||||
) -> Result<()> {
|
||||
if !passphrase.is_empty() {
|
||||
bail!("Encrypted passphrase is not supported");
|
||||
}
|
||||
let backup_file = File::open(backup_to_import).await?;
|
||||
let file_size = backup_file.metadata().await?.len();
|
||||
info!(
|
||||
@@ -210,7 +213,7 @@ async fn import_backup(
|
||||
context.get_dbfile().display()
|
||||
);
|
||||
|
||||
import_backup_stream(context, backup_file, file_size, passphrase).await?;
|
||||
import_backup_stream(context, backup_file, file_size).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -231,7 +234,6 @@ pub(crate) async fn import_backup_stream<R: tokio::io::AsyncRead + Unpin>(
|
||||
context: &Context,
|
||||
backup_file: R,
|
||||
file_size: u64,
|
||||
passphrase: String,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!context.is_configured().await?,
|
||||
@@ -242,7 +244,7 @@ pub(crate) async fn import_backup_stream<R: tokio::io::AsyncRead + Unpin>(
|
||||
"Cannot import backup, IO is running"
|
||||
);
|
||||
|
||||
import_backup_stream_inner(context, backup_file, file_size, passphrase)
|
||||
import_backup_stream_inner(context, backup_file, file_size)
|
||||
.await
|
||||
.0
|
||||
}
|
||||
@@ -315,7 +317,6 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
|
||||
context: &Context,
|
||||
backup_file: R,
|
||||
file_size: u64,
|
||||
passphrase: String,
|
||||
) -> (Result<()>,) {
|
||||
let backup_file = ProgressReader::new(backup_file, context.clone(), file_size);
|
||||
let mut archive = Archive::new(backup_file);
|
||||
@@ -362,7 +363,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
|
||||
if res.is_ok() {
|
||||
res = context
|
||||
.sql
|
||||
.import(&unpacked_database, passphrase.clone())
|
||||
.import(&unpacked_database)
|
||||
.await
|
||||
.context("cannot import unpacked database");
|
||||
}
|
||||
@@ -390,7 +391,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.open(context, "".to_string())
|
||||
.open(context)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
@@ -735,7 +736,7 @@ where
|
||||
/// overwritten.
|
||||
///
|
||||
/// This also verifies that IO is not running during the export.
|
||||
async fn export_database(context: &Context, dest: &Path, passphrase: String) -> Result<()> {
|
||||
async fn export_database(context: &Context, dest: &Path, _passphrase: String) -> Result<()> {
|
||||
ensure!(
|
||||
!context.scheduler.is_running().await,
|
||||
"cannot export backup, IO is running"
|
||||
@@ -745,6 +746,7 @@ async fn export_database(context: &Context, dest: &Path, passphrase: String) ->
|
||||
let dest = dest
|
||||
.to_str()
|
||||
.with_context(|| format!("path {} is not valid unicode", dest.display()))?;
|
||||
let mut dest_conn = rusqlite::Connection::open(dest)?;
|
||||
|
||||
context.set_config(Config::BccSelf, Some("1")).await?;
|
||||
context
|
||||
@@ -755,22 +757,12 @@ async fn export_database(context: &Context, dest: &Path, passphrase: String) ->
|
||||
context
|
||||
.sql
|
||||
.call_write(|conn| {
|
||||
conn.execute("VACUUM;", ())
|
||||
.map_err(|err| warn!(context, "Vacuum failed, exporting anyway {err}"))
|
||||
.ok();
|
||||
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (dest, passphrase))
|
||||
.context("failed to attach backup database")?;
|
||||
let res = conn
|
||||
.query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
|
||||
.context("failed to export to attached backup database");
|
||||
conn.execute(
|
||||
"UPDATE backup.config SET value='0' WHERE keyname='verified_one_on_one_chats';",
|
||||
[],
|
||||
)
|
||||
.ok(); // Deprecated 2025-07. If verified_one_on_one_chats was not set, this errors, which we ignore
|
||||
conn.execute("DETACH DATABASE backup", [])
|
||||
.context("failed to detach backup database")?;
|
||||
res?;
|
||||
if let Err(err) = conn.execute("VACUUM", ()) {
|
||||
warn!(context, "Vacuum failed, exporting anyway: {err:#}.");
|
||||
}
|
||||
|
||||
let backup = rusqlite::backup::Backup::new(conn, &mut dest_conn)?;
|
||||
backup.run_to_completion(5, std::time::Duration::ZERO, None)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@@ -1040,7 +1032,7 @@ mod tests {
|
||||
ar.unpack(&unpack_dir).await?;
|
||||
|
||||
let sql = sql::Sql::new(unpack_dir.path().join(DBFILE_BACKUP_NAME));
|
||||
sql.open(&context2, "".to_string()).await?;
|
||||
sql.open(&context2).await?;
|
||||
assert_eq!(
|
||||
sql.get_raw_config_int("backup_version").await?.unwrap(),
|
||||
DCBACKUP_VERSION
|
||||
|
||||
@@ -324,7 +324,6 @@ pub async fn get_backup2(
|
||||
info!(context, "Sending backup authentication token.");
|
||||
send_stream.write_all(auth_token.as_bytes()).await?;
|
||||
|
||||
let passphrase = String::new();
|
||||
info!(context, "Starting to read backup from the stream.");
|
||||
|
||||
let mut file_size_buf = [0u8; 8];
|
||||
@@ -334,7 +333,7 @@ pub async fn get_backup2(
|
||||
// Emit a nonzero progress so that UIs can display smth like "Transferring...".
|
||||
context.emit_event(EventType::ImexProgress(1));
|
||||
|
||||
import_backup_stream(context, recv_stream, file_size, passphrase)
|
||||
import_backup_stream(context, recv_stream, file_size)
|
||||
.await
|
||||
.context("Failed to import backup from QUIC stream")?;
|
||||
info!(context, "Finished importing backup from the stream.");
|
||||
|
||||
+2
-2
@@ -52,7 +52,7 @@ const KEYUPDATE_CHUNK_CONTACTS: usize = 200;
|
||||
const KEYUPDATE_MAX_SILENCE: i64 = 3 * 365 * 24 * 3600;
|
||||
|
||||
/// Upper bound on the contacts informed after a relay list change, keeping the freshest.
|
||||
const KEYUPDATE_MAX_RECIPIENTS: usize = 5000;
|
||||
const KEYUPDATE_MAX_RECIPIENTS: u32 = 5000;
|
||||
|
||||
/// A contact to inform: the relays to reach them at, and the key to encrypt to.
|
||||
struct KeyupdateRecipient {
|
||||
@@ -63,7 +63,7 @@ struct KeyupdateRecipient {
|
||||
/// Returns at most `max_recipients` key-contacts to inform.
|
||||
async fn keyupdate_recipients(
|
||||
context: &Context,
|
||||
max_recipients: usize,
|
||||
max_recipients: u32,
|
||||
) -> Result<Vec<KeyupdateRecipient>> {
|
||||
// Single chat contacts only become keyupdate recipient candidates
|
||||
// if we have a record of a sent message or `last_seen` is not 0.
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ use tls::wrap_tls;
|
||||
pub(crate) const TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// TTL for caches in seconds.
|
||||
pub(crate) const CACHE_TTL: u64 = 30 * 24 * 60 * 60;
|
||||
pub(crate) const CACHE_TTL: u32 = 30 * 24 * 60 * 60;
|
||||
|
||||
/// Removes connection history entries after `CACHE_TTL`.
|
||||
pub(crate) async fn prune_connection_history(context: &Context) -> Result<()> {
|
||||
|
||||
+2
-2
@@ -84,7 +84,7 @@ pub struct ReactionFrequency {
|
||||
pub reaction: Reaction,
|
||||
|
||||
/// Number of contacts that reacted with this emoji.
|
||||
pub count: usize,
|
||||
pub count: u32,
|
||||
|
||||
/// True if `ContactId::SELF` is among the contacts that reacted with this emoji.
|
||||
pub is_from_self: bool,
|
||||
@@ -420,7 +420,7 @@ pub(crate) async fn apply_pending_reactions(
|
||||
/// sorted in descending order of frequency.
|
||||
fn calc_frequencies(by_contact: &BTreeMap<ContactId, Reaction>) -> Vec<ReactionFrequency> {
|
||||
let mut self_reaction = Reaction::new("");
|
||||
let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
|
||||
let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
|
||||
for (contact_id, reaction) in by_contact {
|
||||
let count = counts.entry(reaction.as_str()).or_insert(0);
|
||||
*count = count.saturating_add(1);
|
||||
|
||||
@@ -42,7 +42,7 @@ struct WireMessage {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct WireEntry {
|
||||
emoji: String,
|
||||
count: usize,
|
||||
count: u32,
|
||||
}
|
||||
|
||||
/// Renders one or more message's states as a JSON string, ready to be sent in `Chat-Broadcast-States:` header.
|
||||
@@ -235,10 +235,10 @@ pub(crate) async fn load_broadcast_reactions(
|
||||
(msg_id,),
|
||||
|row| {
|
||||
let reaction: String = row.get(0)?;
|
||||
let count: i64 = row.get(1)?;
|
||||
let count: u32 = row.get(1)?;
|
||||
Ok(ReactionFrequency {
|
||||
reaction: Reaction::new(&reaction),
|
||||
count: count as usize,
|
||||
count,
|
||||
is_from_self: false,
|
||||
})
|
||||
},
|
||||
@@ -405,7 +405,7 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_modify_frequencies() {
|
||||
// Helper to create a ReactionFrequency entry
|
||||
let freq = |emoji: &str, count: usize, is_from_self: bool| -> ReactionFrequency {
|
||||
let freq = |emoji: &str, count: u32, is_from_self: bool| -> ReactionFrequency {
|
||||
ReactionFrequency {
|
||||
reaction: Reaction::new(emoji),
|
||||
count,
|
||||
|
||||
+7
-1
@@ -2716,6 +2716,12 @@ async fn lookup_or_create_adhoc_group(
|
||||
for &id in &contact_ids {
|
||||
stmt.execute((id,)).context("INSERT INTO temp.contacts")?;
|
||||
}
|
||||
|
||||
// Contact IDs are 32-bit internally,
|
||||
// so this conversion of contact ID set size
|
||||
// to u32 should never fail.
|
||||
let contact_ids_len = u32::try_from(contact_ids.len())?;
|
||||
|
||||
let val = t
|
||||
.query_row(
|
||||
"SELECT c.id, c.blocked
|
||||
@@ -2729,7 +2735,7 @@ async fn lookup_or_create_adhoc_group(
|
||||
AND contact_id NOT IN (SELECT id FROM temp.contacts)
|
||||
AND add_timestamp >= remove_timestamp)=0
|
||||
ORDER BY m.timestamp DESC",
|
||||
(&grpname, contact_ids.len()),
|
||||
(&grpname, contact_ids_len),
|
||||
|row| {
|
||||
let id: ChatId = row.get(0)?;
|
||||
let blocked: Blocked = row.get(1)?;
|
||||
|
||||
+15
-113
@@ -56,10 +56,6 @@ pub struct Sql {
|
||||
/// SQL connection pool.
|
||||
pool: RwLock<Option<Pool>>,
|
||||
|
||||
/// None if the database is not open, true if it is open with passphrase and false if it is
|
||||
/// open without a passphrase.
|
||||
is_encrypted: RwLock<Option<bool>>,
|
||||
|
||||
/// Cache of `config` table.
|
||||
pub(crate) config_cache: RwLock<HashMap<String, Option<String>>>,
|
||||
}
|
||||
@@ -70,52 +66,15 @@ impl Sql {
|
||||
Self {
|
||||
dbfile,
|
||||
pool: Default::default(),
|
||||
is_encrypted: Default::default(),
|
||||
config_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests SQLCipher passphrase.
|
||||
///
|
||||
/// Returns true if passphrase is correct, i.e. the database is new or can be unlocked with
|
||||
/// this passphrase, and false if the database is already encrypted with another passphrase or
|
||||
/// corrupted.
|
||||
///
|
||||
/// Fails if database is already open.
|
||||
pub async fn check_passphrase(&self, passphrase: String) -> Result<bool> {
|
||||
if self.is_open().await {
|
||||
bail!("Database is already opened.");
|
||||
}
|
||||
|
||||
// Hold the lock to prevent other thread from opening the database.
|
||||
let _lock = self.pool.write().await;
|
||||
|
||||
// Test that the key is correct using a single connection.
|
||||
let connection = Connection::open(&self.dbfile)?;
|
||||
if !passphrase.is_empty() {
|
||||
connection
|
||||
.pragma_update(None, "key", &passphrase)
|
||||
.context("Failed to set PRAGMA key")?;
|
||||
}
|
||||
let key_is_correct = connection
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.is_ok();
|
||||
|
||||
Ok(key_is_correct)
|
||||
}
|
||||
|
||||
/// Checks if there is currently a connection to the underlying Sqlite database.
|
||||
pub async fn is_open(&self) -> bool {
|
||||
self.pool.read().await.is_some()
|
||||
}
|
||||
|
||||
/// Returns true if the database is encrypted.
|
||||
///
|
||||
/// If database is not open, returns `None`.
|
||||
pub(crate) async fn is_encrypted(&self) -> Option<bool> {
|
||||
*self.is_encrypted.read().await
|
||||
}
|
||||
|
||||
/// Closes all underlying Sqlite connections.
|
||||
pub(crate) async fn close(&self) {
|
||||
let _ = self.pool.write().await.take();
|
||||
@@ -123,52 +82,22 @@ impl Sql {
|
||||
}
|
||||
|
||||
/// Imports the database from a separate file with the given passphrase.
|
||||
pub(crate) async fn import(&self, path: &Path, passphrase: String) -> Result<()> {
|
||||
let path_str = path
|
||||
.to_str()
|
||||
.with_context(|| format!("path {path:?} is not valid unicode"))?
|
||||
.to_string();
|
||||
|
||||
pub(crate) async fn import(&self, path: &Path) -> Result<()> {
|
||||
// Keep `config_cache` locked all the time the db is imported so that nobody can use invalid
|
||||
// values from there. And clear it immediately so as not to forget in case of errors.
|
||||
let mut config_cache = self.config_cache.write().await;
|
||||
config_cache.clear();
|
||||
|
||||
let src_conn =
|
||||
rusqlite::Connection::open(path).context("Failed to open source database")?;
|
||||
let query_only = false;
|
||||
self.call(query_only, move |conn| {
|
||||
// Check that backup passphrase is correct before resetting our database.
|
||||
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (path_str, passphrase))
|
||||
.context("failed to attach backup database")?;
|
||||
let res = conn
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.context("backup passphrase is not correct");
|
||||
let backup = rusqlite::backup::Backup::new(&src_conn, &mut *conn)?;
|
||||
backup.run_to_completion(5, std::time::Duration::ZERO, None)?;
|
||||
drop(backup);
|
||||
|
||||
// Reset the database without reopening it. We don't want to reopen the database because we
|
||||
// don't have main database passphrase at this point.
|
||||
// See <https://sqlite.org/c3ref/c_dbconfig_enable_fkey.html> for documentation.
|
||||
// Without resetting import may fail due to existing tables.
|
||||
res.and_then(|_| {
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)
|
||||
.context("failed to set SQLITE_DBCONFIG_RESET_DATABASE")
|
||||
})
|
||||
.and_then(|_| {
|
||||
conn.execute("VACUUM", [])
|
||||
.context("failed to vacuum the database")
|
||||
})
|
||||
.and(
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)
|
||||
.context("failed to unset SQLITE_DBCONFIG_RESET_DATABASE"),
|
||||
)
|
||||
.and_then(|_| {
|
||||
conn.query_row("SELECT sqlcipher_export('main', 'backup')", [], |_row| {
|
||||
Ok(())
|
||||
})
|
||||
.context("failed to import from attached backup database")
|
||||
})
|
||||
.and(
|
||||
conn.execute("DETACH DATABASE backup", [])
|
||||
.context("failed to detach backup database"),
|
||||
)?;
|
||||
conn.execute("VACUUM", [])
|
||||
.context("failed to vacuum the database")?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@@ -177,10 +106,10 @@ impl Sql {
|
||||
const N_DB_CONNECTIONS: usize = 3;
|
||||
|
||||
/// Creates a new connection pool.
|
||||
fn new_pool(dbfile: &Path, passphrase: String) -> Result<Pool> {
|
||||
fn new_pool(dbfile: &Path) -> Result<Pool> {
|
||||
let mut connections = Vec::with_capacity(Self::N_DB_CONNECTIONS);
|
||||
for _ in 0..Self::N_DB_CONNECTIONS {
|
||||
let connection = new_connection(dbfile, &passphrase)?;
|
||||
let connection = new_connection(dbfile)?;
|
||||
connections.push(connection);
|
||||
}
|
||||
|
||||
@@ -188,8 +117,8 @@ impl Sql {
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
async fn try_open(&self, context: &Context, dbfile: &Path, passphrase: String) -> Result<()> {
|
||||
*self.pool.write().await = Some(Self::new_pool(dbfile, passphrase.to_string())?);
|
||||
async fn try_open(&self, context: &Context, dbfile: &Path) -> Result<()> {
|
||||
*self.pool.write().await = Some(Self::new_pool(dbfile)?);
|
||||
|
||||
if let Err(e) = self.run_migrations(context).await {
|
||||
error!(context, "Running migrations failed: {e:#}");
|
||||
@@ -247,7 +176,7 @@ impl Sql {
|
||||
|
||||
/// Opens the provided database and runs any necessary migrations.
|
||||
/// If a database is already open, this will return an error.
|
||||
pub async fn open(&self, context: &Context, passphrase: String) -> Result<()> {
|
||||
pub async fn open(&self, context: &Context) -> Result<()> {
|
||||
if self.is_open().await {
|
||||
error!(
|
||||
context,
|
||||
@@ -256,10 +185,8 @@ impl Sql {
|
||||
bail!("SQL database is already opened.");
|
||||
}
|
||||
|
||||
let passphrase_nonempty = !passphrase.is_empty();
|
||||
self.try_open(context, &self.dbfile, passphrase).await?;
|
||||
self.try_open(context, &self.dbfile).await?;
|
||||
info!(context, "Opened database {:?}.", self.dbfile);
|
||||
*self.is_encrypted.write().await = Some(passphrase_nonempty);
|
||||
|
||||
// setup debug logging if there is an entry containing its id
|
||||
if let Some(xdc_id) = self
|
||||
@@ -271,28 +198,6 @@ impl Sql {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Changes the passphrase of encrypted database.
|
||||
///
|
||||
/// The database must already be encrypted and the passphrase cannot be empty.
|
||||
/// 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<()> {
|
||||
let mut lock = self.pool.write().await;
|
||||
|
||||
let pool = lock.take().context("SQL connection pool is not open")?;
|
||||
let query_only = false;
|
||||
let conn = pool.get(query_only).await?;
|
||||
if !passphrase.is_empty() {
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Allocates a connection and calls `function` with the connection.
|
||||
///
|
||||
/// If `query_only` is true, allocates read-only connection,
|
||||
@@ -690,7 +595,7 @@ impl Sql {
|
||||
///
|
||||
/// `passphrase` is the SQLCipher database passphrase.
|
||||
/// Empty string if database is not encrypted.
|
||||
fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
|
||||
fn new_connection(path: &Path) -> Result<Connection> {
|
||||
let flags = OpenFlags::SQLITE_OPEN_NO_MUTEX
|
||||
| OpenFlags::SQLITE_OPEN_READ_WRITE
|
||||
| OpenFlags::SQLITE_OPEN_CREATE;
|
||||
@@ -726,9 +631,6 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
|
||||
conn.busy_timeout(Duration::ZERO)?;
|
||||
}
|
||||
|
||||
if !passphrase.is_empty() {
|
||||
conn.pragma_update(None, "key", passphrase)?;
|
||||
}
|
||||
// Try to enable auto_vacuum. This will only be
|
||||
// applied if the database is new or after successful
|
||||
// VACUUM, which usually happens before backup export.
|
||||
|
||||
@@ -657,7 +657,7 @@ pub(crate) async fn msgs_to_key_contacts(context: &Context) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
let mut first_key_contacts_msg_id: u64 = t
|
||||
let mut first_key_contacts_msg_id: u32 = t
|
||||
.query_one(
|
||||
"SELECT CAST(value AS INTEGER) FROM config WHERE keyname='first_key_contacts_msg_id'",
|
||||
(),
|
||||
@@ -681,10 +681,10 @@ pub(crate) async fn msgs_to_key_contacts(context: &Context) -> Result<()> {
|
||||
)
|
||||
.context("Prepare stmt")?;
|
||||
let msgs_to_migrate = 1000;
|
||||
let mut msgs_migrated: u64 = 0;
|
||||
let mut msgs_migrated: u32 = 0;
|
||||
while first_key_contacts_msg_id > 0 && msgs_migrated < msgs_to_migrate {
|
||||
let start_msg_id = first_key_contacts_msg_id.saturating_sub(msgs_to_migrate);
|
||||
let cnt: u64 = stmt
|
||||
let cnt: u32 = stmt
|
||||
.execute((start_msg_id, first_key_contacts_msg_id))
|
||||
.context("UPDATE msgs")?
|
||||
.try_into()?;
|
||||
|
||||
+3
-96
@@ -83,7 +83,7 @@ async fn test_housekeeping_db_closed() {
|
||||
|
||||
t.sql.close().await;
|
||||
housekeeping(&t).await.unwrap(); // housekeeping should emit warnings but not fail
|
||||
t.sql.open(&t, "".to_string()).await.unwrap();
|
||||
t.sql.open(&t).await.unwrap();
|
||||
|
||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]);
|
||||
@@ -155,11 +155,11 @@ async fn test_db_reopen() -> Result<()> {
|
||||
let sql = Sql::new(dbfile);
|
||||
|
||||
// Create database with all the tables.
|
||||
sql.open(&t, "".to_string()).await.unwrap();
|
||||
sql.open(&t).await.unwrap();
|
||||
sql.close().await;
|
||||
|
||||
// Reopen the database
|
||||
sql.open(&t, "".to_string()).await?;
|
||||
sql.open(&t).await?;
|
||||
sql.execute(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
||||
("foo", "bar"),
|
||||
@@ -209,99 +209,6 @@ async fn test_migration_flags() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_check_passphrase() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
|
||||
sql.check_passphrase("foo".to_string()).await?;
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database first time")?;
|
||||
sql.close().await;
|
||||
|
||||
// Reopen the database
|
||||
let sql = Sql::new(dbfile);
|
||||
|
||||
// Test that we can't open encrypted database without a passphrase.
|
||||
assert!(sql.open(&t, "".to_string()).await.is_err());
|
||||
|
||||
// Now open the database with passpharse, it should succeed.
|
||||
sql.check_passphrase("foo".to_string()).await?;
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database second time")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sql_change_passphrase() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database first time")?;
|
||||
sql.close().await;
|
||||
|
||||
// Change the passphrase from "foo" to "bar".
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database second time")?;
|
||||
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 query_only = true;
|
||||
let conn1 = pool.get(query_only).await?;
|
||||
let conn2 = pool.get(query_only).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);
|
||||
|
||||
// Test that old passphrase is not working.
|
||||
assert!(sql.open(&t, "foo".to_string()).await.is_err());
|
||||
|
||||
// Open the database with the new passphrase.
|
||||
sql.check_passphrase("bar".to_string()).await?;
|
||||
sql.open(&t, "bar".to_string())
|
||||
.await
|
||||
.context("failed to open the database third time")?;
|
||||
sql.close().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_query_only() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
+3
-3
@@ -69,7 +69,7 @@ struct ContactStat {
|
||||
#[serde(skip_serializing_if = "is_false", rename = "direct_chat")]
|
||||
single_chat: bool,
|
||||
|
||||
last_seen: u64,
|
||||
last_seen: i64,
|
||||
|
||||
/// Whether the contact was established after stats-sending was enabled
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
@@ -312,7 +312,7 @@ async fn ensure_last_old_contact_id(context: &Context) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let last_contact_id: u64 = context
|
||||
let last_contact_id: u32 = context
|
||||
.sql
|
||||
.query_get_value("SELECT MAX(id) FROM contacts", ())
|
||||
.await?
|
||||
@@ -436,7 +436,7 @@ async fn get_contact_stats(context: &Context, last_old_contact: u32) -> Result<V
|
||||
|row| {
|
||||
let id = row.get(0)?;
|
||||
let encrypted: bool = row.get(1)?;
|
||||
let last_seen: u64 = row.get(2)?;
|
||||
let last_seen: i64 = row.get(2)?;
|
||||
let bot: bool = row.get(3)?;
|
||||
|
||||
Ok(ContactStat {
|
||||
|
||||
+16
-8
@@ -56,16 +56,18 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
|
||||
let blobdir_size =
|
||||
tokio::task::spawn_blocking(move || get_blobdir_storage_usage(&context_clone));
|
||||
|
||||
let page_size: u64 = ctx
|
||||
let page_size: i64 = ctx
|
||||
.sql
|
||||
.query_get_value("PRAGMA page_size", ())
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let page_count: u64 = ctx
|
||||
let page_size = u64::try_from(page_size)?;
|
||||
let page_count: i64 = ctx
|
||||
.sql
|
||||
.query_get_value("PRAGMA page_count", ())
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let page_count = u64::try_from(page_count)?;
|
||||
|
||||
let mut largest_tables = ctx
|
||||
.sql
|
||||
@@ -78,7 +80,8 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
|
||||
(),
|
||||
|row| {
|
||||
let name: String = row.get(0)?;
|
||||
let size: u64 = row.get(1)?;
|
||||
let size: i64 = row.get(1)?;
|
||||
let size: u64 = u64::try_from(size)?;
|
||||
Ok((name, size, None))
|
||||
},
|
||||
)
|
||||
@@ -86,12 +89,13 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
|
||||
|
||||
for row in &mut largest_tables {
|
||||
let name = &row.0;
|
||||
let row_count: Result<Option<u64>> = ctx
|
||||
let row_count: Option<i64> = ctx
|
||||
.sql
|
||||
// SECURITY: the table name comes from the db, not from the user
|
||||
.query_get_value(&format!("SELECT COUNT(*) FROM {name}"), ())
|
||||
.await;
|
||||
row.2 = row_count.unwrap_or_default();
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
row.2 = row_count.map(|count| u64::try_from(count).unwrap_or_default());
|
||||
}
|
||||
|
||||
let largest_webxdc_data = ctx
|
||||
@@ -103,8 +107,12 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
|
||||
(),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let size: u64 = row.get(1)?;
|
||||
let count: u64 = row.get(2)?;
|
||||
let size: i64 = row.get(1)?;
|
||||
let count: i64 = row.get(2)?;
|
||||
|
||||
// This should never fail as the count cannot be negative.
|
||||
let size: u64 = u64::try_from(size)?;
|
||||
let count: u64 = u64::try_from(count)?;
|
||||
|
||||
Ok((msg_id, size, count))
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user