Compare commits

..
Author SHA1 Message Date
link2xt 0e5b440a4d api!: remove SQLCipher support 2026-09-23 22:32:05 +00:00
11 changed files with 45 additions and 434 deletions
Generated
-1
View File
@@ -3374,7 +3374,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8"
dependencies = [
"cc",
"openssl-sys",
"pkg-config",
"vcpkg",
]
+2 -2
View File
@@ -85,7 +85,7 @@ quick-xml = { version = "0.41", features = ["escape-html"] }
rand-old = { package = "rand", version = "0.8" }
rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rusqlite = { workspace = true, features = ["backup"] }
sanitize-filename = { workspace = true }
sdp = "0.17.1"
serde_json = { workspace = true }
@@ -209,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() {
+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<()> {
+6 -52
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(
-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.");
+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 -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;