diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index fe04ea829..ed90de3d1 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -301,6 +301,19 @@ 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. + * 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. * diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index f6c4c7e9f..64ee55c41 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -167,6 +167,24 @@ pub unsafe extern "C" fn dc_context_open( .unwrap_or(0) } +#[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)) + .context("dc_context_change_passphrase() failed") + .log_err(ctx) + .is_ok() as libc::c_int +} + #[no_mangle] pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int { if context.is_null() { diff --git a/src/context.rs b/src/context.rs index 270676031..ba96035fb 100644 --- a/src/context.rs +++ b/src/context.rs @@ -332,6 +332,12 @@ impl Context { } } + /// Changes encrypted database passphrase. + pub async fn change_passphrase(&self, passphrase: String) -> Result<()> { + self.sql.change_passphrase(passphrase).await?; + Ok(()) + } + /// Returns true if database is open. pub async fn is_open(&self) -> bool { self.sql.is_open().await diff --git a/src/sql.rs b/src/sql.rs index 90549dd4b..ab780a452 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -304,6 +304,20 @@ impl Sql { } } + /// 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<()> { + self.call_write(move |conn| { + conn.pragma_update(None, "rekey", passphrase) + .context("failed to set PRAGMA rekey")?; + Ok(()) + }) + .await + } + /// Locks the write transactions mutex in order to make sure that there never are /// multiple write transactions at once. /// @@ -1246,6 +1260,49 @@ mod tests { 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_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")?; + 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(()) } }