Make working with encrypted storage RAII

This refactors the APIs to work with encrypted storage to folow the
Resource Acquisition Is Initialisation principle.  Having our
structures behave like this is beneficial because it reoves a lot of
edge-cases that would need to be handled.
This commit is contained in:
Floris Bruynooghe
2022-02-06 21:38:17 +01:00
parent 2e5e8f73c6
commit 8bc2ca7f25
5 changed files with 444 additions and 358 deletions

View File

@@ -179,10 +179,12 @@ typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
// create/open/config/information
/**
* Create a new context object and try to open it without passphrase. If
* database is encrypted, the result is the same as using
* dc_context_new_closed() and the database should be opened with
* dc_context_open() before using.
* Create a new context object and try to open it without passphrase.
*
* If database is encrypted NULL is returned, encrypted databases should be
* opened using dc_context_new_encrypted().
*
* If the database does not yet exist a new one will be created.
*
* @memberof dc_context_t
* @param os_name Deprecated, pass NULL or empty string here.
@@ -192,54 +194,34 @@ typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
* @return A context object with some public members.
* The object must be passed to the other context functions
* and must be freed using dc_context_unref() after usage.
* On failure NULL is returned.
*
* If you want to use multiple context objects at the same time,
* this can be managed using dc_accounts_t.
*/
dc_context_t* dc_context_new (const char* os_name, const char* dbfile, const char* blobdir);
/**
* Create a new context object. After creation it is usually opened with
* dc_context_open() and started with dc_start_io() so it is connected and
* mails are fetched.
* Create a new context object and try to open it with a passphrase.
*
* If the database does not yet exist a new one will be created. If the
* database is not encrypted this will fail.
*
* @memberof dc_context_t
* @param dbfile The file to use to store the database,
* something like `~/file` won't work, use absolute paths.
* @param passphrase The passphrase to use. This MUST be non-NULL and MUST be valid
* UTF-8, if either of this is not true this fails and NULL is retruned.
* @return A context object with some public members.
* The object must be passed to the other context functions
* and must be freed using dc_context_unref() after usage.
* On failure NULL is returned.
*
* If you want to use multiple context objects at the same time,
* this can be managed using dc_accounts_t.
*/
dc_context_t* dc_context_new_closed (const char* dbfile);
/**
* Opens the database with the given passphrase. This can only be used on
* closed context, such as created by dc_context_new_closed(). If the database
* is new, this operation sets the database passphrase. For existing databases
* the passphrase should be the one used to encrypt the database the first
* time.
*
* @memberof dc_context_t
* @param context The context object.
* @param passphrase The passphrase to use with the database. Pass NULL or
* empty string to use no passphrase and no encryption.
* @return 1 if the database is opened with this passphrase, 0 if the
* passphrase is incorrect and on error.
*/
int dc_context_open (dc_context_t *context, const char* passphrase);
/**
* Returns 1 if database is open.
*
* @memberof dc_context_t
* @param context The context object.
* @return 1 if database is open, 0 if database is closed
*/
int dc_context_is_open (dc_context_t *context);
dc_context_t* dc_context_new_encrypted (const char* dbfile, const char* passphrase);
/**
* Free a context object.
@@ -2637,21 +2619,22 @@ void dc_accounts_unref (dc_accounts_t* accounts);
uint32_t dc_accounts_add_account (dc_accounts_t* accounts);
/**
* Add a new closed account to the account manager.
* Internally, dc_context_new_closed() is called using a unique database-name
* in the directory specified at dc_accounts_new().
* Add a new account with a password to the account manager.
*
* If the function succeeds,
* dc_accounts_get_all() will return one more account
* and you can access the newly created account using dc_accounts_get_account().
* Moreover, the newly created account will be the selected one.
* The account's database will be encrypted with this password and will need
* to be opened with the password to be usable.
*
* Password-protected accounts are not automatically opened when the account
* manager is created. They need to be opened using dc_accounts_load_encrypted(),
* to find unloaded encrypted accounts use dc_accounts_get_encrypted().
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @return Account-id, use dc_accounts_get_account() to get the context object.
* @return Account-id, use dc_accounts_get_encrypted_account() to get the
* context object.
* On errors, 0 is returned.
*/
uint32_t dc_accounts_add_closed_account (dc_accounts_t* accounts);
uint32_t dc_accounts_add_encrypted_account(dc_accounts_t* accounts, char* password);
/**
* Migrate independent accounts into accounts managed by the account manager.
@@ -2687,6 +2670,10 @@ int dc_accounts_remove_account (dc_accounts_t* accounts, uint32
/**
* List all accounts.
*
* Only lists loaded accounts, some accounts might be encrypted and need to be
* explicitly loaded using dc_accounts_get_encrypted() and
* dc_accounts_load_encrypted().
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @return An array containing all account-ids,
@@ -2694,10 +2681,27 @@ int dc_accounts_remove_account (dc_accounts_t* accounts, uint32
*/
dc_array_t* dc_accounts_get_all (dc_accounts_t* accounts);
/**
* Lists encrypted accounts.
*
* This returns all *locked* encrypted accounts which must be activated using
* dc_accounts_load_encrypted(). Once an encrypted account has been loaded it will
* show up in dc_accounts_get_all() and can be retrieved using
* dc_accounts_get_account().
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @return An array containing account-ids, use dc_array_get_id() to get the ids.
*/
dc_array_t* dc_accounts_get_encrypted (dc_accounts_t* accounts);
/**
* Get an account-context from an account-id.
*
* Only accounts returned by dc_accounts_get_all() can be retrieved with this
* function, encrypted accounts first need to be loaded using
* dc_accounts_load_encrypted().
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @param account_id The account-id as returned e.g. by dc_accounts_get_all() or dc_accounts_add_account().
@@ -2708,9 +2712,30 @@ dc_array_t* dc_accounts_get_all (dc_accounts_t* accounts);
*/
dc_context_t* dc_accounts_get_account (dc_accounts_t* accounts, uint32_t account_id);
/**
* Loads an encrypted account into the Account manager.
*
* Once the account has been loaded you can also access it using
* dc_accounts_get_account() like any other account. Use *dc_accounts_get_encrypted()
* to find accounts which need to be loaded like this.
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @param account_id The account-id as returned e.g. by dc_accounts_get_encrypted(),
* dc_accounts_add_encrypted_account().
* @param passphrase The passphrase to decrypt the account with.
* @return The account-context, this can be used most similar as a normal,
* unmanaged account-context as created by dc_context_new().
* Once you do no longer need the context-object, you have to call dc_context_unref() on it,
* which, however, will not close the account but only decrease a reference counter.
* On failure NULL is returned.
*/
dc_context_t* dc_accounts_load_encrypted (dc_accounts_t* accounts, uint32_t account_id, char* passphrase);
/**
* Get the currently selected account.
*
* If there is at least one account in the account-manager,
* there is always a selected one.
* To change the selected account, use dc_accounts_select_account();
@@ -2722,7 +2747,11 @@ dc_context_t* dc_accounts_get_account (dc_accounts_t* accounts, uint32
* unmanaged account-context as created by dc_context_new().
* Once you do no longer need the context-object, you have to call dc_context_unref() on it,
* which, however, will not close the account but only decrease a reference counter.
*
* If there is no selected account, NULL is returned.
*
* If the selected account is encrypted and not yet loaded using
* dc_accounts_load_encrypted(), NULL is returned.
*/
dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);

View File

@@ -16,6 +16,7 @@ extern crate serde_json;
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::ffi::CStr;
use std::fmt::Write;
use std::ops::Deref;
use std::ptr;
@@ -94,20 +95,33 @@ pub unsafe extern "C" fn dc_context_new(
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *mut dc_context_t {
pub unsafe extern "C" fn dc_context_new_encrypted(
dbfile: *const libc::c_char,
passphrase: *const libc::c_char,
) -> *mut dc_context_t {
setup_panic!();
if dbfile.is_null() {
eprintln!("ignoring careless call to dc_context_new_closed()");
if dbfile.is_null() || passphrase.is_null() {
eprintln!("ignoring careless call to dc_context_new_encrypted()");
return ptr::null_mut();
}
// Generate random ID if dc_accounts_t is not used.
let id = rand::thread_rng().gen();
match block_on(Context::new_closed(
as_path(dbfile).to_path_buf().into(),
let dbfile = as_path(dbfile).to_path_buf();
let cstr = CStr::from_ptr(passphrase);
let passphrase = match cstr.to_str() {
Ok(s) => s,
Err(err) => {
eprintln!("passphrase was not UTF-8: {:#}", err);
return ptr::null_mut();
}
};
let ctx = block_on(Context::new_encrypted(
dbfile.into(),
id,
)) {
Ok(context) => Box::into_raw(Box::new(context)),
passphrase.to_string(),
));
match ctx {
Ok(ctx) => Box::into_raw(Box::new(ctx)),
Err(err) => {
eprintln!("failed to create context: {:#}", err);
ptr::null_mut()
@@ -115,35 +129,6 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_open(
context: *mut dc_context_t,
passphrase: *const libc::c_char,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_context_open()");
return 0;
}
let ctx = &*context;
let passphrase = to_string_lossy(passphrase);
block_on(ctx.open(passphrase))
.log_err(ctx, "dc_context_open() failed")
.map(|b| b as libc::c_int)
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_context_is_open()");
return 0;
}
let ctx = &*context;
block_on(ctx.is_open()) as libc::c_int
}
/// Release the context structure.
///
/// This function releases the memory of the `dc_context_t` structure.
@@ -4148,17 +4133,29 @@ pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accounts_t) -> u32 {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_add_closed_account()");
pub unsafe extern "C" fn dc_accounts_add_encrypted_account(
accounts: *mut dc_accounts_t,
passphrase: *const libc::c_char,
) -> u32 {
if accounts.is_null() || passphrase.is_null() {
eprintln!("ignoring careless call to dc_accounts_add_with_password()");
return 0;
}
let accounts = &mut *accounts;
let accounts: &AccountsWrapper = &mut *accounts;
let cstr = CStr::from_ptr(passphrase);
block_on(async move {
let mut accounts = accounts.write().await;
match accounts.add_closed_account().await {
let passphrase = match cstr.to_str() {
Ok(s) => s,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Passphrase was not UTF-8: {:#}",
err
)));
return 0;
}
};
match accounts.add_encrypted_account(passphrase.to_string()).await {
Ok(id) => id,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
@@ -4243,6 +4240,60 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
Box::into_raw(Box::new(array))
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_encrypted(
accounts: *mut dc_accounts_t,
) -> *mut dc_array_t {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_all()");
return ptr::null_mut();
}
let accounts: &AccountsWrapper = &*accounts;
let list = block_on(async move { accounts.read().await.get_encrypted() });
let array: dc_array_t = list.into();
Box::into_raw(Box::new(array))
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_load_encrypted(
accounts: *mut dc_accounts_t,
account_id: u32,
passphrase: *const libc::c_char,
) -> *mut dc_context_t {
if accounts.is_null() || passphrase.is_null() {
eprintln!("ignoring careless call to dc_context_new_encrypted()");
return ptr::null_mut();
}
let accounts: &AccountsWrapper = &*accounts;
let cstr = CStr::from_ptr(passphrase);
block_on(async move {
let mut accounts = accounts.write().await;
let passphrase = match cstr.to_str() {
Ok(s) => s,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Passphrase was not UTF-8: {:#}",
err
)));
return ptr::null_mut();
}
};
match accounts
.load_encrypted_account(account_id, passphrase.to_string())
.await
{
Ok(ctx) => Box::into_raw(Box::new(ctx)),
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Failed to load encrypted account: {:#}",
err
)));
ptr::null_mut()
}
}
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_all_work_done(accounts: *mut dc_accounts_t) -> libc::c_int {
if accounts.is_null() {