diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d7911a46..f20ddecd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ - allow to remove quotes on drafts `dc_msg_set_quote(msg, NULL)` #2950 +- Use second parameter of `dc_imex` to provide backup passphrase #2980 + #### Removed - Removed `mvbox_watch` option. #2906 diff --git a/Cargo.lock b/Cargo.lock index 19a55e18e..4ce658241 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2105,8 +2105,7 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" [[package]] name = "libsqlite3-sys" version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cafc7c74096c336d9d27145f7ebd4f4b6f95ba16aa5a282387267e6925cb58" +source = "git+https://github.com/rusqlite/rusqlite?branch=master#ddb7141c6dee4b8956af85b2e4a01a28e5fdbacc" dependencies = [ "cc", "openssl-sys", @@ -3079,8 +3078,7 @@ dependencies = [ [[package]] name = "rusqlite" version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba4d3462c8b2e4d7f4fcfcf2b296dc6b65404fbbc7b63daa37fd485c149daf7" +source = "git+https://github.com/rusqlite/rusqlite?branch=master#ddb7141c6dee4b8956af85b2e4a01a28e5fdbacc" dependencies = [ "bitflags", "fallible-iterator", diff --git a/Cargo.toml b/Cargo.toml index 2ba7fb49e..60a9ce676 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,9 @@ debug = 0 [profile.release] lto = true +[patch.crates-io] +rusqlite = { git = "https://github.com/rusqlite/rusqlite", branch="master" } + [dependencies] deltachat_derive = { path = "./deltachat_derive" } diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index e503becc1..d8c6aa057 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -2023,8 +2023,8 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co #define DC_IMEX_EXPORT_SELF_KEYS 1 // param1 is a directory where the keys are written to #define DC_IMEX_IMPORT_SELF_KEYS 2 // param1 is a directory where the keys are searched in and read from -#define DC_IMEX_EXPORT_BACKUP 11 // param1 is a directory where the backup is written to -#define DC_IMEX_IMPORT_BACKUP 12 // param1 is the file with the backup to import +#define DC_IMEX_EXPORT_BACKUP 11 // param1 is a directory where the backup is written to, param2 is a passphrase to encrypt the backup +#define DC_IMEX_IMPORT_BACKUP 12 // param1 is the file with the backup to import, param2 is the backup's passphrase /** @@ -2033,14 +2033,16 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co * if needed stop IO using dc_accounts_stop_io() or dc_stop_io() first. * What to do is defined by the _what_ parameter which may be one of the following: * - * - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1`. + * - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1` + * encrypted with the passphrase given as `param2`. If `param2` is NULL or empty string, + * the backup is not encrypted. * The backup contains all contacts, chats, images and other data and device independent settings. * The backup does not contain device dependent settings as ringtones or LED notification settings. * The name of the backup is typically `delta-chat-.tar`, if more than one backup is create on a day, * the format is `delta-chat--.tar` * - * - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. The file is normally - * created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup + * - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase. + * The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup * is only possible as long as the context is not configured or used in another way. * * - **DC_IMEX_EXPORT_SELF_KEYS** (1) - Export all private keys and all public keys of the user to the diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 8846ab57f..153f7cd22 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -2022,7 +2022,7 @@ pub unsafe extern "C" fn dc_imex( context: *mut dc_context_t, what_raw: libc::c_int, param1: *const libc::c_char, - _param2: *const libc::c_char, + param2: *const libc::c_char, ) { if context.is_null() { eprintln!("ignoring careless call to dc_imex()"); @@ -2035,12 +2035,13 @@ pub unsafe extern "C" fn dc_imex( return; } }; + let passphrase = to_opt_string_lossy(param2); let ctx = &*context; if let Some(param1) = to_opt_string_lossy(param1) { spawn(async move { - imex::imex(ctx, what, param1.as_ref()) + imex::imex(ctx, what, param1.as_ref(), passphrase) .await .log_err(ctx, "IMEX failed") }); diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index c1f7b49d9..838e5ffc0 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -472,20 +472,32 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu } "export-backup" => { let dir = dirs::home_dir().unwrap_or_default(); - imex(&context, ImexMode::ExportBackup, dir.as_ref()).await?; + imex( + &context, + ImexMode::ExportBackup, + dir.as_ref(), + Some(arg2.to_string()), + ) + .await?; println!("Exported to {}.", dir.to_string_lossy()); } "import-backup" => { ensure!(!arg1.is_empty(), "Argument missing."); - imex(&context, ImexMode::ImportBackup, arg1.as_ref()).await?; + imex( + &context, + ImexMode::ImportBackup, + arg1.as_ref(), + Some(arg2.to_string()), + ) + .await?; } "export-keys" => { let dir = dirs::home_dir().unwrap_or_default(); - imex(&context, ImexMode::ExportSelfKeys, dir.as_ref()).await?; + imex(&context, ImexMode::ExportSelfKeys, dir.as_ref(), None).await?; println!("Exported to {}.", dir.to_string_lossy()); } "import-keys" => { - imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref()).await?; + imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?; } "export-setup" => { let setup_code = create_setup_code(&context); diff --git a/src/context.rs b/src/context.rs index 880bc7b8c..7d12f681a 100644 --- a/src/context.rs +++ b/src/context.rs @@ -109,8 +109,8 @@ impl Context { let context = Self::new_closed(dbfile, id).await?; // Open the database if is not encrypted. - if context.set_passphrase("".to_string()).await? { - context.sql.open(&context).await?; + if context.check_passphrase("".to_string()).await? { + context.sql.open(&context, "".to_string()).await?; } Ok(context) } @@ -133,8 +133,8 @@ impl Context { /// Returns true if passphrase is correct, false is passphrase is not correct. Fails on other /// errors. pub async fn open(&self, passphrase: String) -> Result { - if self.sql.set_passphrase(passphrase).await? { - self.sql.open(self).await?; + if self.sql.check_passphrase(passphrase.clone()).await? { + self.sql.open(self, passphrase).await?; Ok(true) } else { Ok(false) @@ -146,13 +146,13 @@ impl Context { self.sql.is_open().await } - /// Sets the database passphrase. + /// Tests the database passphrase. /// /// Returns true if passphrase is correct. /// /// Fails if database is already open. - pub async fn set_passphrase(&self, passphrase: String) -> Result { - self.sql.set_passphrase(passphrase).await + pub(crate) async fn check_passphrase(&self, passphrase: String) -> Result { + self.sql.check_passphrase(passphrase).await } pub(crate) async fn with_blobdir( @@ -1039,7 +1039,7 @@ mod tests { } #[async_std::test] - async fn test_set_passphrase() -> Result<()> { + async fn test_check_passphrase() -> Result<()> { let dir = tempdir()?; let dbfile = dir.path().join("db.sqlite"); @@ -1056,7 +1056,7 @@ mod tests { .await .context("failed to create context")?; assert_eq!(context.is_open().await, false); - assert_eq!(context.set_passphrase("bar".to_string()).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); diff --git a/src/dc_tools.rs b/src/dc_tools.rs index 3c3d8980c..4c3a1e48a 100644 --- a/src/dc_tools.rs +++ b/src/dc_tools.rs @@ -13,7 +13,7 @@ use async_std::path::{Path, PathBuf}; use async_std::prelude::*; use async_std::{fs, io}; -use anyhow::{bail, Error}; +use anyhow::Error; use chrono::{Local, TimeZone}; use mailparse::dateparse; use mailparse::headers::Headers; @@ -451,33 +451,6 @@ pub fn dc_open_file_std>( } } -/// Returns Ok((temp_path, dest_path)) on success. The backup can then be written to temp_path. If the backup succeeded, -/// it can be renamed to dest_path. This guarantees that the backup is complete. -pub(crate) async fn get_next_backup_path( - folder: impl AsRef, - backup_time: i64, -) -> Result<(PathBuf, PathBuf), Error> { - let folder = PathBuf::from(folder.as_ref()); - let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0) - // Don't change this file name format, in has_backup() we use string comparison to determine which backup is newer: - .format("delta-chat-backup-%Y-%m-%d") - .to_string(); - - // 64 backup files per day should be enough for everyone - for i in 0..64 { - let mut tempfile = folder.clone(); - tempfile.push(format!("{}-{:02}.tar.part", stem, i)); - - let mut destfile = folder.clone(); - destfile.push(format!("{}-{:02}.tar", stem, i)); - - if !tempfile.exists().await && !destfile.exists().await { - return Ok((tempfile, destfile)); - } - } - bail!("could not create backup file, disk full?"); -} - pub(crate) fn time() -> i64 { SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) diff --git a/src/imex.rs b/src/imex.rs index 70259f40f..d14cde960 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -20,7 +20,7 @@ use crate::constants::{Viewtype, DC_CONTACT_ID_SELF}; use crate::context::Context; use crate::dc_tools::{ dc_create_folder, dc_delete_file, dc_delete_files_in_dir, dc_get_filesuffix_lc, - dc_open_file_std, dc_read_file, dc_write_file, get_next_backup_path, time, EmailAddress, + dc_open_file_std, dc_read_file, dc_write_file, time, EmailAddress, }; use crate::e2ee; use crate::events::EventType; @@ -41,24 +41,24 @@ const BLOBS_BACKUP_NAME: &str = "blobs_backup"; #[repr(u32)] pub enum ImexMode { /// Export all private keys and all public keys of the user to the - /// directory given as `param1`. The default key is written to the files `public-key-default.asc` + /// directory given as `path`. The default key is written to the files `public-key-default.asc` /// and `private-key-default.asc`, if there are more keys, they are written to files as /// `public-key-.asc` and `private-key-.asc` ExportSelfKeys = 1, - /// Import private keys found in the directory given as `param1`. + /// Import private keys found in the directory given as `path`. /// The last imported key is made the default keys unless its name contains the string `legacy`. /// Public keys are not imported. ImportSelfKeys = 2, - /// Export a backup to the directory given as `param1`. + /// Export a backup to the directory given as `path` with the given `passphrase`. /// The backup contains all contacts, chats, images and other data and device independent settings. /// The backup does not contain device dependent settings as ringtones or LED notification settings. /// The name of the backup is typically `delta-chat-.tar`, if more than one backup is create on a day, /// the format is `delta-chat--.tar` ExportBackup = 11, - /// `param1` is the file (not: directory) to import. The file is normally + /// `path` is the file (not: directory) to import. The file is normally /// created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup /// is only possible as long as the context is not configured or used in another way. ImportBackup = 12, @@ -78,11 +78,16 @@ pub enum ImexMode { /// /// Only one import-/export-progress can run at the same time. /// To cancel an import-/export-progress, drop the future returned by this function. -pub async fn imex(context: &Context, what: ImexMode, param1: &Path) -> Result<()> { +pub async fn imex( + context: &Context, + what: ImexMode, + path: &Path, + passphrase: Option, +) -> Result<()> { let cancel = context.alloc_ongoing().await?; let res = async { - let success = imex_inner(context, what, param1).await; + let success = imex_inner(context, what, path, passphrase).await; match success { Ok(()) => { info!(context, "IMEX successfully completed"); @@ -115,11 +120,6 @@ async fn cleanup_aborted_imex(context: &Context, what: ImexMode) { dc_delete_file(context, context.get_dbfile()).await; dc_delete_files_in_dir(context, context.get_blobdir()).await; } - if what == ImexMode::ExportBackup || what == ImexMode::ImportBackup { - if let Err(e) = context.sql.open(context).await { - warn!(context, "Re-opening db after imex failed: {}", e); - } - } } /// Returns the filename of the backup found (otherwise an error) @@ -396,7 +396,12 @@ fn normalize_setup_code(s: &str) -> String { out } -async fn imex_inner(context: &Context, what: ImexMode, path: &Path) -> Result<()> { +async fn imex_inner( + context: &Context, + what: ImexMode, + path: &Path, + passphrase: Option, +) -> Result<()> { info!(context, "Import/export dir: {}", path.display()); ensure!(context.sql.is_open().await, "Database not opened."); context.emit_event(EventType::ImexProgress(10)); @@ -414,13 +419,26 @@ async fn imex_inner(context: &Context, what: ImexMode, path: &Path) -> Result<() ImexMode::ExportSelfKeys => export_self_keys(context, path).await, ImexMode::ImportSelfKeys => import_self_keys(context, path).await, - ImexMode::ExportBackup => export_backup(context, path).await, - ImexMode::ImportBackup => import_backup(context, path).await, + ImexMode::ExportBackup => { + export_backup(context, path, passphrase.unwrap_or_default()).await + } + ImexMode::ImportBackup => { + import_backup(context, path, passphrase.unwrap_or_default()).await + } } } -/// Import Backup -async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()> { +/// Imports backup into the currently open database. +/// +/// The contents of the currently open database will be lost. +/// +/// `passphrase` is the passphrase used to open backup database. If backup is unencrypted, pass +/// empty string here. +async fn import_backup( + context: &Context, + backup_to_import: &Path, + passphrase: String, +) -> Result<()> { info!( context, "Import \"{}\" to \"{}\".", @@ -436,12 +454,6 @@ async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()> !context.scheduler.read().await.is_running(), "cannot import backup, IO already running" ); - context.sql.close().await; - dc_delete_file(context, context.get_dbfile()).await; - ensure!( - !context.get_dbfile().exists().await, - "Cannot delete old database." - ); let backup_file = File::open(backup_to_import).await?; let file_size = backup_file.metadata().await?.len(); @@ -463,11 +475,15 @@ async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()> if f.path()?.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) { // async_tar can't unpack to a specified file name, so we just unpack to the blobdir and then move the unpacked file. f.unpack_in(context.get_blobdir()).await?; - fs::rename( - context.get_blobdir().join(DBFILE_BACKUP_NAME), - context.get_dbfile(), - ) - .await?; + let unpacked_database = context.get_blobdir().join(DBFILE_BACKUP_NAME); + context + .sql + .import(&unpacked_database, passphrase.clone()) + .await + .context("cannot import unpacked database")?; + fs::remove_file(unpacked_database) + .await + .context("cannot remove unpacked database")?; } else { // async_tar will unpack to blobdir/BLOBS_BACKUP_NAME, so we move the file afterwards. f.unpack_in(context.get_blobdir()).await?; @@ -482,12 +498,6 @@ async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()> } } - context - .sql - .open(context) - .await - .context("Could not re-open db")?; - delete_and_reset_all_device_msgs(context).await?; Ok(()) @@ -496,12 +506,44 @@ async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()> /******************************************************************************* * Export backup ******************************************************************************/ -#[allow(unused)] -async fn export_backup(context: &Context, dir: &Path) -> Result<()> { + +/// Returns Ok((temp_db_path, temp_path, dest_path)) on success. Unencrypted database can be +/// written to temp_db_path. The backup can then be written to temp_path. If the backup succeeded, +/// it can be renamed to dest_path. This guarantees that the backup is complete. +async fn get_next_backup_path( + folder: &Path, + backup_time: i64, +) -> Result<(PathBuf, PathBuf, PathBuf)> { + let folder = PathBuf::from(folder); + let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0) + // Don't change this file name format, in has_backup() we use string comparison to determine which backup is newer: + .format("delta-chat-backup-%Y-%m-%d") + .to_string(); + + // 64 backup files per day should be enough for everyone + for i in 0..64 { + let mut tempdbfile = folder.clone(); + tempdbfile.push(format!("{}-{:02}.db", stem, i)); + + let mut tempfile = folder.clone(); + tempfile.push(format!("{}-{:02}.tar.part", stem, i)); + + let mut destfile = folder.clone(); + destfile.push(format!("{}-{:02}.tar", stem, i)); + + if !tempdbfile.exists().await && !tempfile.exists().await && !destfile.exists().await { + return Ok((tempdbfile, tempfile, destfile)); + } + } + bail!("could not create backup file, disk full?"); +} + +async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> { // get a fine backup file name (the name includes the date so that multiple backup instances are possible) let now = time(); - let (temp_path, dest_path) = get_next_backup_path(dir, now).await?; - let _d = DeleteOnDrop(temp_path.clone()); + let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, now).await?; + let _d1 = DeleteOnDrop(temp_db_path.clone()); + let _d2 = DeleteOnDrop(temp_path.clone()); context .sql @@ -513,16 +555,14 @@ async fn export_backup(context: &Context, dir: &Path) -> Result<()> { .sql .execute("VACUUM;", paramsv![]) .await - .map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e)); + .map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e)) + .ok(); ensure!( !context.scheduler.read().await.is_running(), "cannot export backup, IO already running" ); - // we close the database during the export - context.sql.close().await; - info!( context, "Backup '{}' to '{}'.", @@ -530,10 +570,13 @@ async fn export_backup(context: &Context, dir: &Path) -> Result<()> { dest_path.display(), ); - let res = export_backup_inner(context, &temp_path).await; + context + .sql + .export(&temp_db_path, passphrase) + .await + .with_context(|| format!("failed to backup plaintext database to {:?}", temp_db_path))?; - // we re-open the database after export is finished - context.sql.open(context).await; + let res = export_backup_inner(context, &temp_db_path, &temp_path).await; match &res { Ok(_) => { @@ -552,18 +595,21 @@ impl Drop for DeleteOnDrop { fn drop(&mut self) { let file = self.0.clone(); // Not using dc_delete_file() here because it would send a DeletedBlobFile event - async_std::task::block_on(async move { fs::remove_file(file).await.ok() }); + async_std::task::block_on(fs::remove_file(file)).ok(); } } -async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<()> { +async fn export_backup_inner( + context: &Context, + temp_db_path: &Path, + temp_path: &Path, +) -> Result<()> { let file = File::create(temp_path).await?; let mut builder = async_tar::Builder::new(file); - // append_path_with_name() wants the source path as the first argument, append_dir_all() wants it as the second argument. builder - .append_path_with_name(context.get_dbfile(), DBFILE_BACKUP_NAME) + .append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME) .await?; let read_dir: Vec<_> = fs::read_dir(context.get_blobdir()).await?.collect().await; @@ -842,12 +888,12 @@ mod tests { async fn test_export_and_import_key() { let context = TestContext::new_alice().await; let blobdir = context.ctx.get_blobdir(); - if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir).await { + if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir, None).await { panic!("got error on export: {:?}", err); } let context2 = TestContext::new_alice().await; - if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir).await { + if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir, None).await { panic!("got error on import: {:?}", err); } } diff --git a/src/sql.rs b/src/sql.rs index e1290072c..895bfb29d 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -10,7 +10,7 @@ use std::time::Duration; use anyhow::{bail, Context as _, Result}; use async_std::path::PathBuf; use async_std::prelude::*; -use rusqlite::{Connection, OpenFlags}; +use rusqlite::{config::DbConfig, Connection, OpenFlags}; use crate::blob::BlobObject; use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon}; @@ -43,11 +43,6 @@ pub struct Sql { pub(crate) dbfile: PathBuf, pool: RwLock>>, - - /// SQLCipher passphrase. - /// - /// Empty string if database is not encrypted. - passphrase: RwLock, } impl Sql { @@ -55,18 +50,17 @@ impl Sql { Self { dbfile, pool: Default::default(), - passphrase: Default::default(), } } - /// Sets SQLCipher passphrase for key derivation. + /// 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 set_passphrase(&self, passphrase: String) -> Result { + pub async fn check_passphrase(&self, passphrase: String) -> Result { if self.is_open().await { bail!("Database is already opened."); } @@ -83,10 +77,6 @@ impl Sql { .query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(())) .is_ok(); - if key_is_correct { - *self.passphrase.write().await = passphrase; - } - Ok(key_is_correct) } @@ -96,11 +86,67 @@ impl Sql { } /// Closes all underlying Sqlite connections. - pub async fn close(&self) { + async fn close(&self) { let _ = self.pool.write().await.take(); // drop closes the connection } + /// Exports the database to a separate file with the given passphrase. + /// + /// Set passphrase to empty string to export the database unencrypted. + pub(crate) async fn export(&self, path: &Path, passphrase: String) -> Result<()> { + let path_str = path + .to_str() + .with_context(|| format!("path {:?} is not valid unicode", path))?; + let conn = self.get_conn().await?; + conn.execute( + "ATTACH DATABASE ? AS backup KEY ?", + paramsv![path_str, 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("DETACH DATABASE backup", []) + .context("failed to detach backup database")?; + res?; + Ok(()) + } + + /// 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 {:?} is not valid unicode", path))?; + let conn = self.get_conn().await?; + + // 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 for documentation. + // Without resetting import may fail due to existing tables. + conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true) + .context("failed to set SQLITE_DBCONFIG_RESET_DATABASE")?; + conn.execute("VACUUM", []) + .context("failed to vacuum the database")?; + conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false) + .context("failed to unset SQLITE_DBCONFIG_RESET_DATABASE")?; + + conn.execute( + "ATTACH DATABASE ? AS backup KEY ?", + paramsv![path_str, passphrase], + ) + .context("failed to attach backup database")?; + let res = conn + .query_row("SELECT sqlcipher_export('main', 'backup')", [], |_row| { + Ok(()) + }) + .context("failed to import from attached backup database"); + conn.execute("DETACH DATABASE backup", []) + .context("failed to detach backup database")?; + res?; + Ok(()) + } + fn new_pool( dbfile: &Path, passphrase: String, @@ -231,7 +277,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) -> Result<()> { + pub async fn open(&self, context: &Context, passphrase: String) -> Result<()> { if self.is_open().await { error!( context, @@ -240,13 +286,7 @@ impl Sql { bail!("SQL database is already opened."); } - let passphrase_lock = self.passphrase.read().await; - let passphrase: &str = passphrase_lock.as_ref(); - - if let Err(err) = self - .try_open(context, &self.dbfile, passphrase.to_string()) - .await - { + if let Err(err) = self.try_open(context, &self.dbfile, passphrase).await { self.close().await; Err(err) } else { @@ -828,7 +868,7 @@ mod tests { t.sql.close().await; housekeeping(&t).await.unwrap_err(); // housekeeping should fail as the db is closed - t.sql.open(&t).await.unwrap(); + t.sql.open(&t, "".to_string()).await.unwrap(); let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]); @@ -871,11 +911,11 @@ mod tests { let sql = Sql::new(dbfile.into()); // Create database with all the tables. - sql.open(&t).await.unwrap(); + sql.open(&t, "".to_string()).await.unwrap(); sql.close().await; // Reopen the database - sql.open(&t).await?; + sql.open(&t, "".to_string()).await?; sql.execute( "INSERT INTO config (keyname, value) VALUES (?, ?);", paramsv!("foo", "bar"), @@ -930,7 +970,7 @@ mod tests { } #[async_std::test] - async fn test_set_passphrase() -> Result<()> { + async fn test_check_passphrase() -> Result<()> { use tempfile::tempdir; // The context is used only for logging. @@ -941,8 +981,8 @@ mod tests { let dbfile = dir.path().join("testdb.sqlite"); let sql = Sql::new(dbfile.clone().into()); - sql.set_passphrase("foo".to_string()).await?; - sql.open(&t) + 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; @@ -951,11 +991,11 @@ mod tests { let sql = Sql::new(dbfile.into()); // Test that we can't open encrypted database without a passphrase. - assert!(sql.open(&t).await.is_err()); + assert!(sql.open(&t, "".to_string()).await.is_err()); - // Now set the passphrase and open the database, it should succeed. - sql.set_passphrase("foo".to_string()).await?; - sql.open(&t) + // 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(())