Compare commits

..
Author SHA1 Message Date
link2xt 0e5b440a4d api!: remove SQLCipher support 2026-09-23 22:32:05 +00:00
link2xt 5b49541e16 chore: update rusqlite from 0.37 to 0.40.2
Largest change comes from removal of u64 and usize serialization
in rusqlite 0.38.0.
It is possible to enable it back with `fallible_uint` feature
as suggested in https://github.com/rusqlite/rusqlite/pull/1732,
but there is a good reason for this change
as it may result in unexpected failures when
trying to store u64 integers that don't fit in i64,
so I don't enable it back.
With this change conversions between i64 and u64 are explicit.

Using less `usize` is generally good as its size
differs on 32-bit platforms that are rarely tested
but still supported and we have changes that reduce its usage
such as 0defa117a0,
014d2ace76 etc.
2026-09-23 18:31:27 +00:00
30 changed files with 247 additions and 1391 deletions
Generated
+76 -46
View File
@@ -1355,7 +1355,6 @@ dependencies = [
"futures",
"futures-lite",
"hex",
"hkdf",
"http-body-util",
"humansize",
"hyper",
@@ -2092,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"
@@ -2412,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]]
@@ -2955,7 +2978,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.15.4",
]
[[package]]
@@ -3262,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",
]
@@ -3345,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",
]
@@ -3422,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]]
@@ -5212,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",
@@ -5223,6 +5256,7 @@ dependencies = [
"hashlink",
"libsqlite3-sys",
"smallvec",
"sqlite-wasm-rs",
]
[[package]]
@@ -5927,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"
@@ -6834,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",
@@ -6883,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",
]
@@ -6918,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 -4
View File
@@ -61,7 +61,6 @@ fd-lock = "4"
futures-lite = { workspace = true }
futures = { workspace = true }
hex = "0.4.0"
hkdf = { version = "0.12", default-features = false }
http-body-util = "0.1.3"
humansize = "2"
hyper = "1"
@@ -86,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 }
@@ -195,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"
@@ -210,7 +209,7 @@ yerpc = "0.7"
default = ["vendored"]
internals = []
vendored = [
"rusqlite/bundled-sqlcipher-vendored-openssl",
"rusqlite/bundled",
"async-native-tls/vendored"
]
-15
View File
@@ -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.
*
-18
View File
@@ -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() {
+1 -1
View File
@@ -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,
+59
View File
@@ -27,6 +27,55 @@ use deltachat::sql;
use deltachat::tools::*;
use tokio::fs;
/// Reset database tables.
/// Argument is a bitmask, executing single or multiple actions in one call.
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
async fn reset_tables(context: &Context, bits: i32) {
println!("Resetting tables ({bits})...");
if 0 != bits & 4 {
context
.sql()
.execute("DELETE FROM keypairs;", ())
.await
.unwrap();
println!("(4) Private keypairs reset.");
}
if 0 != bits & 8 {
context
.sql()
.execute("DELETE FROM contacts WHERE id>9;", ())
.await
.unwrap();
context
.sql()
.execute("DELETE FROM chats WHERE id>9;", ())
.await
.unwrap();
context
.sql()
.execute("DELETE FROM chats_contacts;", ())
.await
.unwrap();
context
.sql()
.execute("DELETE FROM msgs WHERE id>9;", ())
.await
.unwrap();
context
.sql()
.execute(
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
(),
)
.await
.unwrap();
context.sql().config_cache().write().await.clear();
println!("(8) Rest but server config reset.");
}
context.emit_msgs_changed_without_ids();
}
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
let data = read_file(context, filename).await?;
@@ -255,6 +304,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
export-keys\n\
import-keys <key-file>\n\
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
reset <flags>\n\
stop\n\
============================================="
),
@@ -394,6 +444,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"poke" => {
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
}
"reset" => {
ensure!(
!arg1.is_empty(),
"Argument <bits> missing: 4=private keys, 8=rest but server config"
);
let bits: i32 = arg1.parse()?;
ensure!(bits < 16, "<bits> must be lower than 16.");
reset_tables(&context, bits).await;
}
"stop" => {
context.stop_ongoing().await;
}
+2 -1
View File
@@ -147,7 +147,7 @@ impl Completer for DcHelper {
}
}
const IMEX_COMMANDS: [&str; 9] = [
const IMEX_COMMANDS: [&str; 10] = [
"has-backup",
"export-backup",
"import-backup",
@@ -156,6 +156,7 @@ const IMEX_COMMANDS: [&str; 9] = [
"export-keys",
"import-keys",
"poke",
"reset",
"stop",
];
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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)?),
-4
View File
@@ -467,10 +467,6 @@ pub enum Config {
/// and incoming unencrypted messages are not fetched and not processed.
#[strum(props(default = "1"))]
ForceEncryption,
/// Generate Autocrypt 2 instead of Autocrypt 1 key.
#[strum(props(default = "1"))]
Autocrypt2,
}
impl Config {
+6 -56
View File
@@ -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(
@@ -1030,10 +984,6 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"autocrypt2",
self.get_config_bool(Config::Autocrypt2).await?.to_string(),
);
let elapsed = time_elapsed(&self.creation_time);
res.insert("uptime", duration_to_str(elapsed));
-59
View File
@@ -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
View File
@@ -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
+1 -2
View File
@@ -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.");
+8 -14
View File
@@ -17,12 +17,10 @@ use pgp::packet::{
SubpacketData,
};
use pgp::ser::Serialize;
use pgp::types::Timestamp as PgpTimestamp;
use pgp::types::{CompressionAlgorithm, KeyDetails, KeyVersion};
use rand_old::thread_rng;
use tokio::runtime::Handle;
use crate::config::Config;
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
@@ -126,11 +124,14 @@ pub trait DcKey: Serialize + Deserializable + Clone {
/// Converts secret key to public key.
pub(crate) fn secret_key_to_public_key(
context: &Context,
mut signed_secret_key: SignedSecretKey,
timestamp: u32,
addr: &str,
relay_addrs: &str,
) -> Result<SignedPublicKey> {
info!(context, "Converting secret key to public key.");
// Make sure timestamp of created signatures
// is not in the past compared to the primary key timestamp.
let timestamp = std::cmp::max(
@@ -154,7 +155,7 @@ pub(crate) fn secret_key_to_public_key(
};
Ok(vec![
Subpacket::critical(SubpacketData::SignatureCreationTime(timestamp))?,
Subpacket::regular(SubpacketData::SignatureCreationTime(timestamp))?,
Subpacket::regular(SubpacketData::IssuerFingerprint(
signed_secret_key.fingerprint(),
))?,
@@ -303,7 +304,7 @@ pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result<Option
let addr = context.get_primary_self_addr().await?;
let all_addrs = context.get_self_addrs().await?.join(",");
let signed_public_key =
secret_key_to_public_key(signed_secret_key, timestamp, &addr, &all_addrs)?;
secret_key_to_public_key(context, signed_secret_key, timestamp, &addr, &all_addrs)?;
*lock = Some(signed_public_key.clone());
Ok(Some(signed_public_key))
@@ -479,16 +480,9 @@ async fn generate_keypair(context: &Context) -> Result<SignedSecretKey> {
None => {
let start = tools::Time::now();
info!(context, "Generating keypair.");
let keypair = if context.get_config_bool(Config::Autocrypt2).await? {
let now = PgpTimestamp::now();
Handle::current()
.spawn_blocking(move || crate::pgp::autocrypt2::create_autocrypt2_keypair(now))
.await??
} else {
Handle::current()
.spawn_blocking(move || crate::pgp::create_keypair(addr))
.await??
};
let keypair = Handle::current()
.spawn_blocking(move || crate::pgp::create_keypair(addr))
.await??;
store_self_keypair(context, &keypair).await?;
info!(
+2 -2
View File
@@ -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.
+2 -5
View File
@@ -331,10 +331,9 @@ pub(crate) fn render_queued_mail(
outer_headers.extend(b"MIME-Version: 1.0\r\n");
if should_attach_pubkey {
let public_key = crate::pgp::minimize_autocrypt_certificate(public_key);
let aheader = Aheader {
addr: from_addr,
public_key,
public_key: public_key.clone(),
prefer_encrypt: EncryptPreference::Mutual,
};
let autocrypt_header = mail_builder::headers::raw::Raw::new(aheader.to_string());
@@ -1473,11 +1472,9 @@ impl MimeFactory {
continue;
}
let public_key = crate::pgp::minimize_autocrypt_certificate(key);
let header = Aheader {
addr: addr.clone(),
public_key,
public_key: key.clone(),
// Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included.
prefer_encrypt: EncryptPreference::NoPreference,
+1
View File
@@ -341,6 +341,7 @@ async fn test_mdn_sent_to_all_relays() -> Result<()> {
// Bob's key gets a second relay address and Alice merges the newer key.
let bob_secret_key = load_self_secret_key(bob).await?;
let bob_public_key = secret_key_to_public_key(
bob,
bob_secret_key,
u32::try_from(time())? + 100,
"bob@example.net",
+1 -1
View File
@@ -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<()> {
+10 -268
View File
@@ -1,8 +1,6 @@
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
use std::cmp::Ordering;
use std::collections::btree_map::Entry as BTreeMapEntry;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::collections::{HashMap, HashSet};
use std::io::Cursor;
use anyhow::{Context as _, Result, ensure};
@@ -16,7 +14,7 @@ use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
use pgp::crypto::ecc_curve::ECCCurve;
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::packet::{Signature, SignatureType, Subpacket, SubpacketData};
use pgp::packet::{Signature, Subpacket, SubpacketData};
use pgp::types::{
CompressionAlgorithm, Imprint, KeyDetails, KeyVersion, Password, SignedUser, SigningKey as _,
StringToKey,
@@ -27,8 +25,6 @@ use sha2::Sha256;
use crate::configure::MAX_RELAYS;
use crate::key::{DcKey, Fingerprint};
pub(crate) mod autocrypt2;
/// Preferred symmetric encryption algorithm.
const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128;
@@ -87,62 +83,13 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> Result<SignedSecretKey> {
/// Selects a subkey of the public key to use for encryption.
///
/// The key is selected according to
/// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-03.html#section-4.3-4>.
/// If multiple keys are available, the one that will expire sooner is selected.
///
/// Returns `None` if the public key cannot be used for encryption.
fn select_pk_for_encryption(now: u32, key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
///
/// TODO: take key flags and expiration dates into account
fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
key.public_subkeys
.iter()
.filter(|subkey| subkey.algorithm().can_encrypt())
.filter_map(|subkey| {
let signature = subkey.signatures.first()?;
let key_flags = signature.key_flags();
if !key_flags.encrypt_comms() {
return None;
}
if let Some(expiration_duration) = signature
.key_expiration_time()
.filter(|duration| duration.as_secs() != 0)
&& now
> subkey
.created_at()
.as_secs()
.saturating_add(expiration_duration.as_secs())
{
// Key is expired.
return None;
}
Some((subkey, signature))
})
.min_by(|(subkey1, signature1), (subkey2, signature2)| {
match (
signature1
.key_expiration_time()
.filter(|duration| duration.as_secs() != 0),
signature2
.key_expiration_time()
.filter(|duration| duration.as_secs() != 0),
) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Greater,
(Some(_), None) => Ordering::Less,
(Some(expiration1), Some(expiration2)) => (subkey1
.created_at()
.as_secs()
.saturating_add(expiration1.as_secs()))
.cmp(
&(subkey2
.created_at()
.as_secs()
.saturating_add(expiration2.as_secs())),
),
}
})
.map(|(subkey, _signature)| subkey)
.find(|subkey| subkey.algorithm().can_encrypt())
}
/// Version of SEIPD packet to use.
@@ -204,11 +151,10 @@ pub fn pk_encrypt(
) -> Result<String> {
tokio::task::block_in_place(|| {
let mut rng = thread_rng();
let now = pgp::types::Timestamp::now();
let pkeys = public_keys_for_encryption
.iter()
.filter_map(|key| select_pk_for_encryption(now.as_secs(), key));
.filter_map(select_pk_for_encryption);
let msg = MessageBuilder::from_bytes("", plain);
let encoded_msg = match seipd_version {
@@ -364,161 +310,6 @@ pub fn symm_encrypt_message(
})
}
/// Minimizes the signatures of a subkey.
///
/// Keeps at most one subkey binding signature
/// and at most one revocation signature,
/// preferring the newest signatures.
///
/// Subkey binding signature is kept
/// even if the revocation signature exists
/// because according to
/// <https://www.rfc-editor.org/rfc/rfc9580.html#name-openpgp-version-6-certifica>
/// "Every subkey MUST have at least one Subkey Binding signature."
/// Distributing subkey with only a revocation signature
/// is not allowed according to the standard,
/// so we keep a subkey binding signature next to it
/// for interoperability.
///
/// This function does not check if the signatures are valid.
/// Such properties should be validated when importing OpenPGP certificates.
fn minimize_subpacket_signatures(signatures: Vec<Signature>) -> Vec<Signature> {
let mut newest_revocation_signature: Option<Signature> = None;
let mut newest_binding_signature: Option<Signature> = None;
for signature in signatures {
let Some(config) = signature.config() else {
// Skip unknown signatures.
continue;
};
match config.typ {
SignatureType::SubkeyBinding => {
if newest_binding_signature
.as_ref()
.is_none_or(|s| s.created() < signature.created())
{
newest_binding_signature = Some(signature)
}
}
SignatureType::SubkeyRevocation => {
if newest_revocation_signature
.as_ref()
.is_none_or(|s| s.created() < signature.created())
{
newest_revocation_signature = Some(signature)
}
}
_ => continue,
}
}
newest_revocation_signature
.into_iter()
.chain(newest_binding_signature)
.collect()
}
/// Minimizes OpenPGP certificate for Autocrypt and Autocrypt-Gossip headers.
pub fn minimize_autocrypt_certificate(certificate: &SignedPublicKey) -> SignedPublicKey {
let primary_key = certificate.primary_key.clone();
let details = certificate.details.clone();
// Select the newest non-expiring subkey and the newest expiring subkey.
let fallback_subkey = certificate
.public_subkeys
.iter()
.filter(|subkey| {
subkey
.signatures
.iter()
.find(|signature| {
signature
.config()
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
})
.is_some_and(|signature| signature.key_expiration_time().is_none())
})
.max_by_key(|subkey| {
subkey
.signatures
.iter()
.find(|signature| {
signature
.config()
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
})
.map(|signature| signature.created().unwrap_or(subkey.created_at()))
});
let rotating_subkey = certificate
.public_subkeys
.iter()
.filter(|subkey| {
subkey
.signatures
.iter()
.find(|signature| {
signature
.config()
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
})
.is_some_and(|signature| signature.key_expiration_time().is_some())
})
.max_by_key(|subkey| {
subkey
.signatures
.iter()
.find(|signature| {
signature
.config()
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
})
.map(|signature| signature.created().unwrap_or(subkey.created_at()))
});
let public_subkeys: Vec<_> = fallback_subkey
.into_iter()
.chain(rotating_subkey)
.cloned()
.collect();
// We do not want to ever gossip more than two subkeys
// to save the traffic.
debug_assert!(public_subkeys.len() <= 2);
SignedPublicKey {
primary_key,
details,
public_subkeys,
}
}
/// Merges two OpenPGP subkeys.
fn merge_openpgp_subkey(old_subkey: &mut SignedPublicSubKey, new_subkey: SignedPublicSubKey) {
debug_assert_eq!(old_subkey.fingerprint(), new_subkey.fingerprint());
old_subkey.signatures = minimize_subpacket_signatures(
std::mem::take(&mut old_subkey.signatures)
.into_iter()
.chain(new_subkey.signatures)
.collect(),
);
}
/// Merges OpenPGP subkey vectors.
pub fn merge_openpgp_subkeys(
subkeys: impl IntoIterator<Item = SignedPublicSubKey>,
) -> Result<Vec<SignedPublicSubKey>> {
let mut merged_subkeys: BTreeMap<_, SignedPublicSubKey> = BTreeMap::new();
for subkey in subkeys {
let imprint = subkey.imprint::<Sha256>()?;
match merged_subkeys.entry(imprint) {
BTreeMapEntry::Vacant(entry) => {
entry.insert(subkey);
}
BTreeMapEntry::Occupied(entry) => {
merge_openpgp_subkey(entry.into_mut(), subkey);
}
}
}
Ok(merged_subkeys.into_values().collect())
}
/// Merges and minimizes OpenPGP certificates.
///
/// Keeps at most one direct key signature and
@@ -555,7 +346,7 @@ pub fn merge_openpgp_certificates(
let SignedPublicKey {
primary_key: new_primary_key,
details: new_details,
public_subkeys: new_public_subkeys,
public_subkeys: _new_public_subkeys,
} = new_certificate;
// Public keys may be serialized differently, e.g. using old and new packet type,
@@ -631,55 +422,7 @@ pub fn merge_openpgp_certificates(
});
let users: Vec<SignedUser> = best_user.into_iter().collect();
let (fallback_subkeys, mut rotating_subkeys): (Vec<_>, Vec<_>) =
merge_openpgp_subkeys(old_public_subkeys.into_iter().chain(new_public_subkeys))?
.into_iter()
.filter_map(|subkey| {
// Select the newest subkey binding signature.
//
// There is at most one subkey binding signature at this point
// because older subkey binding signatures are removed during merging.
let signature = subkey.signatures.iter().find(|signature| {
signature
.config()
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
})?;
let created_at_secs = signature.created().unwrap_or(subkey.created_at()).as_secs();
let expires_at_secs: Option<u32> = signature
.key_expiration_time()
.map(|duration| duration.as_secs())
.filter(|duration_secs| *duration_secs != 0)
.map(|duration_secs| {
subkey.created_at().as_secs().saturating_add(duration_secs)
});
Some((subkey, created_at_secs, expires_at_secs))
})
.partition(|(_subkey, _created_at_secs, expires_at_secs)| expires_at_secs.is_none());
let fallback_subkey: Option<SignedPublicSubKey> = fallback_subkeys
.into_iter()
.max_by_key(|(_subkey, created_at_secs, _)| *created_at_secs)
.map(|(subkey, _, _)| subkey);
rotating_subkeys
.sort_by_key(|(_subkey, created_at_secs, _)| std::cmp::Reverse(*created_at_secs));
// Put the fallback subkey first so it is gossiped first.
//
// We want to always gossip non-expiring key first
// for older versions that always encrypted to the first subkey.
//
// Keep 10 newest rotating subkeys to avoid storing indefinitely growing number of subkeys locally.
let public_subkeys = fallback_subkey
.into_iter()
.chain(
rotating_subkeys
.into_iter()
.take(10)
.map(|(subkey, _, _)| subkey),
)
.collect();
let public_subkeys = old_public_subkeys;
Ok(SignedPublicKey {
primary_key: old_primary_key,
@@ -742,8 +485,7 @@ pub(crate) fn relay_addrs(public_key: &SignedPublicKey, addr: &str) -> Vec<Strin
/// Returns true if the key can be encrypted to, i.e. has an encryption subkey.
pub(crate) fn pubkey_can_encrypt(public_key: &SignedPublicKey) -> bool {
let now = pgp::types::Timestamp::now();
select_pk_for_encryption(now.as_secs(), public_key).is_some()
select_pk_for_encryption(public_key).is_some()
}
/// Returns true if public key advertises SEIPDv2 feature.
-588
View File
@@ -1,588 +0,0 @@
//! Autocrypt2 implementation.
use anyhow::Context as _;
use anyhow::Result;
use anyhow::bail;
use anyhow::ensure;
use anyhow::format_err;
use hkdf::Hkdf;
use pgp::composed::SignedKeyDetails;
use pgp::composed::SignedSecretKey;
use pgp::composed::SignedSecretSubKey;
use pgp::crypto::aead::AeadAlgorithm;
use pgp::crypto::ed25519;
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::ml_kem768_x25519;
use pgp::crypto::public_key::PublicKeyAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::packet::Features;
use pgp::packet::KeyFlags;
use pgp::packet::PacketTrait as _;
use pgp::packet::PubKeyInner;
use pgp::packet::PublicKey;
use pgp::packet::PublicSubkey;
use pgp::packet::SecretKey;
use pgp::packet::SecretSubkey;
use pgp::packet::SignatureConfig;
use pgp::packet::SignatureType;
use pgp::packet::Subpacket;
use pgp::packet::SubpacketData;
use pgp::ser::Serialize as _;
use pgp::types::Duration as PgpDuration;
use pgp::types::Ed25519PublicParams;
use pgp::types::KeyDetails;
use pgp::types::KeyVersion;
use pgp::types::MlKem768X25519PublicParams;
use pgp::types::Password;
use pgp::types::PlainSecretParams;
use pgp::types::PublicParams;
use pgp::types::SecretParams;
use pgp::types::Timestamp;
use rand_old::thread_rng;
use sha2::Digest;
use sha2::Sha512;
/// Creates an Autocrypt 2 TSK.
///
/// <https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/>
pub(crate) fn create_autocrypt2_keypair(now: Timestamp) -> Result<SignedSecretKey> {
let mut rng = thread_rng();
// Fake zero timestamp for primary key and fallback key creation.
// We do not want to leak the key creation date to contacts.
// This is not to be used for rotating subkey timestamps.
let zero_timestamp = Timestamp::from_secs(0);
let public_key_algorithm = PublicKeyAlgorithm::Ed25519;
let primary_key_packet = {
let ed25519_secret = ed25519::SecretKey::generate(&mut rng, ed25519::Mode::Ed25519);
let public_params = PublicParams::Ed25519(Ed25519PublicParams::from(&ed25519_secret));
let secret_params = SecretParams::Plain(PlainSecretParams::Ed25519(ed25519_secret));
let pubkey_inner = PubKeyInner::new(
KeyVersion::V6,
public_key_algorithm,
zero_timestamp,
None,
public_params,
)?;
let pubkey = PublicKey::from_inner(pubkey_inner)?;
SecretKey::new(pubkey, secret_params)?
};
let details = {
let mut signature_config =
SignatureConfig::from_key(&mut rng, &primary_key_packet, SignatureType::Key)?;
let mut keyflags = KeyFlags::default();
keyflags.set_certify(true);
keyflags.set_sign(true);
let mut features = Features::default();
features.set_seipd_v1(true);
features.set_seipd_v2(true);
signature_config.hashed_subpackets = vec![
Subpacket::critical(SubpacketData::SignatureCreationTime(now))?,
Subpacket::regular(SubpacketData::KeyFlags(keyflags))?,
Subpacket::regular(SubpacketData::Features(features))?,
Subpacket::regular(SubpacketData::IssuerFingerprint(
primary_key_packet.fingerprint(),
))?,
Subpacket::regular(SubpacketData::PreferredAeadAlgorithms(smallvec![(
SymmetricKeyAlgorithm::AES256,
AeadAlgorithm::Ocb
)]))?,
];
let signature = signature_config.sign_key(
&primary_key_packet,
&Password::empty(),
&primary_key_packet.public_key(),
)?;
SignedKeyDetails {
revocation_signatures: vec![],
direct_signatures: vec![signature],
users: vec![],
user_attributes: vec![],
}
};
let fallback_subkey_packet = {
let ml_kem_secret = ml_kem768_x25519::SecretKey::generate(&mut rng);
let public_params =
PublicParams::MlKem768X25519(MlKem768X25519PublicParams::from(&ml_kem_secret));
let secret_params = SecretParams::Plain(PlainSecretParams::MlKem768X25519(ml_kem_secret));
let pubkey_inner = PubKeyInner::new(
KeyVersion::V6,
PublicKeyAlgorithm::MlKem768X25519,
zero_timestamp,
None,
public_params,
)?;
let public_subkey = PublicSubkey::from_inner(pubkey_inner)?;
SecretSubkey::new(public_subkey, secret_params)?
};
let signed_fallback_subkey = {
let mut keyflags = KeyFlags::default();
keyflags.set_encrypt_storage(true);
keyflags.set_encrypt_comms(true);
let mut signature_config = SignatureConfig::v6(
&mut rng,
SignatureType::SubkeyBinding,
public_key_algorithm,
HashAlgorithm::Sha256,
)?;
signature_config.hashed_subpackets = vec![
Subpacket::critical(SubpacketData::SignatureCreationTime(zero_timestamp))?,
Subpacket::critical(SubpacketData::KeyFlags(keyflags))?,
Subpacket::regular(SubpacketData::IssuerFingerprint(
primary_key_packet.fingerprint(),
))?,
];
let signature = signature_config.sign_subkey_binding(
&primary_key_packet,
primary_key_packet.public_key(),
&Password::empty(),
fallback_subkey_packet.public_key(),
)?;
SignedSecretSubKey {
key: fallback_subkey_packet,
signatures: vec![signature],
}
};
let rotating_subkey_packet = {
let ml_kem_secret = ml_kem768_x25519::SecretKey::generate(&mut rng);
let public_params =
PublicParams::MlKem768X25519(MlKem768X25519PublicParams::from(&ml_kem_secret));
let secret_params = SecretParams::Plain(PlainSecretParams::MlKem768X25519(ml_kem_secret));
let mut keyflags = KeyFlags::default();
keyflags.set_encrypt_comms(true);
let pubkey_inner = PubKeyInner::new(
KeyVersion::V6,
PublicKeyAlgorithm::MlKem768X25519,
now,
None,
public_params,
)?;
let public_subkey = PublicSubkey::from_inner(pubkey_inner)?;
SecretSubkey::new(public_subkey, secret_params)?
};
let signed_rotating_subkey = {
let mut keyflags = KeyFlags::default();
keyflags.set_encrypt_comms(true);
let mut signature_config = SignatureConfig::v6(
&mut rng,
SignatureType::SubkeyBinding,
public_key_algorithm,
HashAlgorithm::Sha256,
)?;
// Expiration duration is 10 days according to
// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-2.2-2.6.2.2.1>
let expiration_duration = PgpDuration::from_secs(864000);
signature_config.hashed_subpackets = vec![
Subpacket::critical(SubpacketData::SignatureCreationTime(now))?,
Subpacket::critical(SubpacketData::KeyFlags(keyflags))?,
// XXX: marking expiration as critical
// even though reference implementation does not:
// <https://codeberg.org/autocrypt2/autocrypt-v2-cert/issues/53>
Subpacket::critical(SubpacketData::KeyExpirationTime(expiration_duration))?,
Subpacket::regular(SubpacketData::IssuerFingerprint(
primary_key_packet.fingerprint(),
))?,
];
let signature = signature_config.sign_subkey_binding(
&primary_key_packet,
primary_key_packet.public_key(),
&Password::empty(),
rotating_subkey_packet.public_key(),
)?;
SignedSecretSubKey {
key: rotating_subkey_packet,
signatures: vec![signature],
}
};
let secret_key = SignedSecretKey {
primary_key: primary_key_packet,
details,
public_subkeys: Vec::new(),
secret_subkeys: vec![signed_fallback_subkey, signed_rotating_subkey],
};
secret_key
.verify_bindings()
.context("Invalid Autocrypt2 key generated")?;
Ok(secret_key)
}
/// Returns true if TSK is an Autocrypt 2 TSK.
///
/// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#name-identification-by-tsk-struc>
fn is_autocrypt2_tsk(tsk: &SignedSecretKey) -> bool {
if tsk.primary_key.version() != KeyVersion::V6
|| tsk.primary_key.algorithm() != PublicKeyAlgorithm::Ed25519
{
return false;
}
// Direct key signature.
let [direct_key_signature] = &tsk.details.direct_signatures[..] else {
return false;
};
let Some(features) = direct_key_signature.features() else {
return false;
};
// SEIPDv2 feature is required according to
// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-2.2-2.2.2.4.1>
if !features.seipd_v2() {
return false;
}
// Primary key must have certification (0x01) and signing (0x02) flags.
let dks_key_flags = direct_key_signature.key_flags();
if !dks_key_flags.certify() || !dks_key_flags.sign() {
return false;
}
// No expiration:
// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-2.2-2.2.2.6.1>
// No key expiration (<https://www.rfc-editor.org/rfc/rfc9580.html#name-key-expiration-time>)
// and no signature expiration (<https://docs.rs/pgp/latest/pgp/packet/struct.Signature.html#method.signature_expiration_time>).
//
// XXX: spec should say explicitly that both key expiration and signature expiration should not be there
if direct_key_signature
.key_expiration_time()
.is_some_and(|duration| duration.as_secs() != 0)
|| direct_key_signature
.signature_expiration_time()
.is_some_and(|duration| duration.as_secs() != 0)
{
return false;
}
if !(tsk.details.revocation_signatures.is_empty()
&& tsk.details.users.is_empty()
&& tsk.details.user_attributes.is_empty())
{
return false;
}
if !tsk.public_subkeys.is_empty() {
return false;
}
// TODO: check all rotating subkeys
// Subkeys may overlap, as long as subkey is not expired, it does not need to be deleted.
let [ref fallback_subkey, .., ref rotating_subkey] = tsk.secret_subkeys[..] else {
return false;
};
let [ref fallback_subkey_signature] = fallback_subkey.signatures[..] else {
return false;
};
let fallback_subkey_flags = fallback_subkey_signature.key_flags();
if !fallback_subkey_flags.encrypt_comms() || !fallback_subkey_flags.encrypt_storage() {
return false;
}
if fallback_subkey_signature
.key_expiration_time()
.is_some_and(|duration| duration.as_secs() != 0)
{
return false;
}
let [ref rotating_subkey_signature] = rotating_subkey.signatures[..] else {
return false;
};
let rotating_subkey_flags = rotating_subkey_signature.key_flags();
// Rotating subkey can be used to encrypt communications, but not storage:
// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-2.2-2.6.2.3.1>
if !rotating_subkey_flags.encrypt_comms() || rotating_subkey_flags.encrypt_storage() {
return false;
}
if rotating_subkey_signature
.key_expiration_time()
.is_none_or(|duration| duration.as_secs() == 0)
{
return false;
}
true
}
fn normalize_x25519_scalar(m: &mut [u8]) {
// From decodeScalar25519 in <https://www.rfc-editor.org/info/rfc7748/#section-5>
m[0] &= 248;
m[31] &= 127;
m[31] |= 64;
}
/// Generates new rotating subkey from a previous one.
///
/// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-4.1.1>
fn ratchet(mut tsk: SignedSecretKey) -> Result<SignedSecretKey> {
// Extract the last rotating subkey.
// Other rotating subkeys do not matter.
// This corresponds to
// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-4.1.1-6.2.1>
let [ref _fallback_subkey, .., ref rotating_subkey] = tsk.secret_subkeys[..] else {
bail!("Cannot extract last rotating subkey");
};
let [ref rotating_subkey_signature] = rotating_subkey.signatures[..] else {
bail!("Rotating subkey must have exactly one signature");
};
let rotating_subkey_flags = rotating_subkey_signature.key_flags();
// We do not search for the latest-expiring subkey
// with the ability to encrypt communications.
// It must be the last one by convention.
// TODO: write TSK structure explicitly in the specification.
let max_rd: u32 = rotating_subkey_signature
.key_expiration_time()
.context("Last subkey is not expiring")?
.as_secs();
let min_rd: u32 = max_rd / 2;
ensure!(
rotating_subkey_flags.encrypt_comms(),
"Last rotating subkey cannot be used to encrypt communications"
);
let start: u32 = rotating_subkey
.created_at()
.as_secs()
.checked_add(min_rd)
.context("Overflow while adding min_rd")?;
let mut salt = Vec::from(start.to_be_bytes());
rotating_subkey
.public_key()
.to_writer_with_header(&mut salt)
.context("Failed to serialize rotating subkey")?;
debug_assert_eq!(
salt.len(),
4 + rotating_subkey.public_key().write_len_with_header()
);
let SecretParams::Plain(PlainSecretParams::MlKem768X25519(old_ml_kem768_x25519_secret_key)) =
rotating_subkey.secret_params()
else {
bail!("Cannot extract ML-KEM-768 + X25519 secret key");
};
let mut ikm = Vec::with_capacity(old_ml_kem768_x25519_secret_key.write_len());
old_ml_kem768_x25519_secret_key
.to_writer(&mut ikm)
.context("Failed to serialize IKM")?;
// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-4.1.1-6.6.1>
normalize_x25519_scalar(&mut ikm);
debug_assert_eq!(ikm.len(), 96);
let info = {
let mut info = b"Autocrypt_v2_ratchet".to_vec();
tsk.primary_key
.public_key()
.to_writer_with_header(&mut info)
.context("Failed to serialize primary key")?;
info.extend_from_slice(&max_rd.to_be_bytes());
info
};
let hkdf = Hkdf::<Sha512>::new(Some(&salt), &ikm);
let mut ks = [0u8; 160];
hkdf.expand(&info, &mut ks)
.map_err(|_err: hkdf::InvalidLength| {
format_err!("HKDF-Expand failed because of invalid output length")
})?;
let new_ml_kem768_x25519_secret_key = {
let mut new_x25519 = [0u8; 32];
let mut new_ml_kem = [0u8; 64];
new_x25519.copy_from_slice(&ks[64..96]);
new_ml_kem.copy_from_slice(&ks[96..160]);
normalize_x25519_scalar(&mut new_x25519[..]);
ml_kem768_x25519::SecretKey::try_from_bytes(new_x25519, new_ml_kem)?
};
let new_rotating_subkey = {
let public_params = PublicParams::MlKem768X25519(MlKem768X25519PublicParams::from(
&new_ml_kem768_x25519_secret_key,
));
let secret_params = SecretParams::Plain(PlainSecretParams::MlKem768X25519(
new_ml_kem768_x25519_secret_key,
));
let pubkey_inner = PubKeyInner::new(
KeyVersion::V6,
PublicKeyAlgorithm::MlKem768X25519,
Timestamp::from_secs(start),
None,
public_params,
)?;
let public_subkey = PublicSubkey::from_inner(pubkey_inner)?;
SecretSubkey::new(public_subkey, secret_params)?
};
let new_signed_rotating_subkey = {
let mut keyflags = KeyFlags::default();
keyflags.set_encrypt_comms(true);
let digest = Sha512::digest(&ks[0..64]);
let bssalt = digest[0..16].to_vec();
let mut signature_config = SignatureConfig::v6_with_salt(
SignatureType::SubkeyBinding,
tsk.primary_key.algorithm(),
HashAlgorithm::Sha256,
bssalt,
);
// FIXME
let expiration_duration = PgpDuration::from_secs(864000);
signature_config.hashed_subpackets = vec![
Subpacket::critical(SubpacketData::SignatureCreationTime(Timestamp::from_secs(
start,
)))?,
Subpacket::critical(SubpacketData::KeyFlags(keyflags))?,
// XXX: marking expiration as critical
// even though reference implementation does not:
// <https://codeberg.org/autocrypt2/autocrypt-v2-cert/issues/53>
Subpacket::critical(SubpacketData::KeyExpirationTime(expiration_duration))?,
Subpacket::regular(SubpacketData::IssuerFingerprint(
tsk.primary_key.public_key().fingerprint(),
))?,
];
let signature = signature_config.sign_subkey_binding(
&tsk.primary_key,
tsk.primary_key.public_key(),
&Password::empty(),
new_rotating_subkey.public_key(),
)?;
SignedSecretSubKey {
key: new_rotating_subkey,
signatures: vec![signature],
}
};
tsk.secret_subkeys.push(new_signed_rotating_subkey);
Ok(tsk)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::key;
use crate::pgp::DcKey;
use crate::test_utils;
/// Tests creating Autocrypt 2 TSK and detecting it.
#[test]
fn test_create_autocrypt2_keypair() {
let now = Timestamp::now();
let keypair = create_autocrypt2_keypair(now).unwrap();
assert!(is_autocrypt2_tsk(&keypair));
// Test that Autocrypt 2 TSK can be serialized and deserialized.
let secret_key_bytes = DcKey::to_bytes(&keypair);
let signed_secret_key = SignedSecretKey::from_slice(&secret_key_bytes)
.expect("Cannot deserialize Autocrypt2 TSK");
assert!(is_autocrypt2_tsk(&signed_secret_key));
}
/// Tests that the key does not leak creation timestamp.
#[test]
fn test_tsk_timestamps() {
let now = Timestamp::now();
let tsk = create_autocrypt2_keypair(now).unwrap();
// Primary key creation timestamp is zero.
assert_eq!(tsk.primary_key.created_at().as_secs(), 0);
// Primary key direct key signature timestamp is zero.
let [ref direct_signature] = tsk.details.direct_signatures[..] else {
panic!("Autocrypt 2 TSK must have exactly one direct key signature");
};
// Direct key signature is a real key creation timestamp
// and should not be zero.
// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-2.2-2.2.2.1.1>
// This timestamp from TSK should not leak into the public key however
// as we recreate the signature every time relay list is changed:
let created_timestamp = direct_signature.created().unwrap();
assert_ne!(created_timestamp.as_secs(), 0);
let fallback_subkey = tsk
.secret_subkeys
.first()
.expect("Fallback subkey not found");
// Fallback subkey creation timestamp should be zero.
// We will not be able to change this timestamp and it should not leak
// the profile creation timestamp.
assert_eq!(fallback_subkey.key.created_at().as_secs(), 0);
// Fallback subkey binding signature timestamp must match
// the direct key signature timestamp.
// TODO: it should be recreated each time Direct Key Signature is recreated.
let [ref fallback_subkey_signature] = fallback_subkey.signatures[..] else {
panic!("Fallback subkey does not have exactly one binding signature");
};
}
/// Tests that Autocrypt 2 TSK detection is not triggered for existing non-AC2 test keys.
#[test]
fn test_is_autocrypt2_tsk_no_false_positives() {
assert!(!is_autocrypt2_tsk(&test_utils::alice_keypair()));
assert!(!is_autocrypt2_tsk(&test_utils::bob_keypair()));
assert!(!is_autocrypt2_tsk(&test_utils::charlie_keypair()));
assert!(!is_autocrypt2_tsk(&test_utils::dom_keypair()));
assert!(!is_autocrypt2_tsk(&test_utils::elena_keypair()));
assert!(!is_autocrypt2_tsk(&test_utils::pqc_keypair()));
}
#[test]
fn test_ratchet() {
let now = Timestamp::now();
let tsk = create_autocrypt2_keypair(now).unwrap();
assert!(is_autocrypt2_tsk(&tsk));
let new_tsk = ratchet(tsk).expect("Ratchet failed");
assert!(is_autocrypt2_tsk(&new_tsk));
}
#[test]
fn test_autocrypt2_key_selection() {
let now = Timestamp::now();
let tsk = create_autocrypt2_keypair(now).unwrap();
let public_key = key::secret_key_to_public_key(
tsk.clone(),
now.as_secs(),
"alice@example.org",
"alice@example.org",
)
.expect("Failed to convert secret key to public key");
// For Autocrypt 2 certificate rotating key should be selected for encryption.
let pk_for_encryption =
crate::pgp::select_pk_for_encryption(now.as_secs(), &public_key).unwrap();
let [ref pk_for_encryption_signature] = pk_for_encryption.signatures[..] else {
panic!("Selected public key has multiple signatures");
};
let key_flags = pk_for_encryption_signature.key_flags();
assert!(key_flags.encrypt_comms());
assert!(!key_flags.encrypt_storage());
}
}
+2 -2
View File
@@ -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);
+4 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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))
},