mirror of
https://github.com/chatmail/core.git
synced 2026-10-06 05:00:17 +03:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e5b440a4d | ||
|
|
5b49541e16 |
Generated
+76
-46
@@ -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
@@ -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"
|
||||
]
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
|
||||
@@ -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)?),
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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.");
|
||||
|
||||
+8
-14
@@ -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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -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
@@ -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