diff --git a/CHANGELOG.md b/CHANGELOG.md index 3198bc71e..15eaa9601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### API Changes +- added APIs to check if database is encrypted and to change the passphrase: + `dc_context_is_encrypted()` and `dc_context_change_passphrase()` #3029. + ### Changes - don't watch Sent folder by default #3025 - use webxdc app name in chatlist/quotes/replies etc. #3027 diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 391ce1af8..4db69b492 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -241,6 +241,32 @@ int dc_context_open (dc_context_t *context, const char* int dc_context_is_open (dc_context_t *context); +/** + * Return 1 if database is encrypted. Can only be checked on open database. + * Use this method to decide whether to present an option to change passphrase + * to the user. + * + * @member dc_context_t + * @param context The context object. + * @return 1 if database is encrypted, 0 if database is not encrypted or on + * error. + */ +int dc_context_is_encrypted (dc_context_t *context); + + +/** + * Changes passphrase for the open database. The database must be encrypted + * already, i.e. have a non-empty password. Unencrypted databases can only be + * encrypted during import/export. + * @memberof dc_context_t + * @param context The context object. + * @param passpharse New passphrase. + * @return 1 if database was reencrypted with the new passphrase, 0 on error + * (database is closed, database is not encrypted, other SQLCipher error). + */ +int dc_context_change_passphrase (dc_context_t *context, char *passphrase); + + /** * Free a context object. * diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 153f7cd22..fc93e5d97 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -144,6 +144,34 @@ pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc: block_on(ctx.is_open()) as libc::c_int } +#[no_mangle] +pub unsafe extern "C" fn dc_context_is_encrypted(context: *mut dc_context_t) -> libc::c_int { + if context.is_null() { + eprintln!("ignoring careless call to dc_context_is_encrypted()"); + return 0; + } + + let ctx = &*context; + block_on(ctx.is_encrypted()) as libc::c_int +} + +#[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 = &*context; + let passphrase = to_string_lossy(passphrase); + block_on(ctx.change_passphrase(passphrase)) + .log_err(ctx, "change_passphrase failed") + .is_ok() as libc::c_int +} + /// Release the context structure. /// /// This function releases the memory of the `dc_context_t` structure. diff --git a/src/context.rs b/src/context.rs index 3ed0278fd..b73ee4ab2 100644 --- a/src/context.rs +++ b/src/context.rs @@ -146,6 +146,12 @@ impl Context { self.sql.is_open().await } + /// Returns true if database is encrypted. Returns false if database is not open yet or not + /// encrypted. + pub async fn is_encrypted(&self) -> bool { + self.sql.is_encrypted().await.unwrap_or(false) + } + /// Tests the database passphrase. /// /// Returns true if passphrase is correct. @@ -155,6 +161,14 @@ impl Context { self.sql.check_passphrase(passphrase).await } + /// Changes the database passphrase. + /// + /// Works only for encrypted databases. Encrypted database can only be converted to unencrypted + /// one and backwards via import/export. + pub async fn change_passphrase(&self, passphrase: String) -> Result<()> { + self.sql.change_passphrase(self, passphrase).await + } + pub(crate) async fn with_blobdir( dbfile: PathBuf, blobdir: PathBuf, @@ -1069,4 +1083,34 @@ mod tests { Ok(()) } + + #[async_std::test] + async fn test_change_passphrase() -> Result<()> { + let dir = tempdir()?; + let dbfile = dir.path().join("db.sqlite"); + + let id = 1; + let context = Context::new_closed(dbfile.clone().into(), id) + .await + .context("failed to create context")?; + assert_eq!(context.open("foo".to_string()).await?, true); + assert_eq!(context.is_open().await, true); + assert_eq!(context.is_encrypted().await, true); + + context.change_passphrase("bar".to_string()).await?; + drop(context); + + let id = 2; + let context = Context::new(dbfile.into(), id) + .await + .context("failed to create context")?; + assert_eq!(context.is_open().await, false); + assert_eq!(context.check_passphrase("foo".to_string()).await?, false); + assert_eq!(context.check_passphrase("bar".to_string()).await?, true); + assert_eq!(context.open("foo".to_string()).await?, false); + assert_eq!(context.open("bar".to_string()).await?, true); + assert_eq!(context.is_encrypted().await, true); + + Ok(()) + } } diff --git a/src/sql.rs b/src/sql.rs index 4da334bff..6e7d5bdca 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -97,6 +97,40 @@ impl Sql { *self.is_encrypted.read().await } + /// Changes the database passpharse. + /// + /// The database must be open and encrypted already. + pub(crate) async fn change_passphrase( + &self, + context: &Context, + passphrase: String, + ) -> Result<()> { + // Take the whole pool so nobody opens another connection in parallel. + let pool = self + .pool + .write() + .await + .take() + .context("the database must be open before rekeying")?; + + // Get one connection and rekey the database. + // All other connections will stop working after that. + let connection = pool + .get() + .context("failed to get connection from the pool")?; + connection + .pragma_update(None, "rekey", &passphrase) + .context("failed to set PRAGMA rekey")?; + drop(pool); + + // Reopen the database with new passphrase. + self.open(context, passphrase) + .await + .context("failed to reopen the database after rekeying")?; + + Ok(()) + } + /// Closes all underlying Sqlite connections. async fn close(&self) { let _ = self.pool.write().await.take();