From 504c0b4156c70ee4ca68221f63b4484a05d8db62 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Fri, 28 Jun 2024 11:19:52 -0400 Subject: [PATCH] add passphrase reset --- src-tauri/src/app.rs | 1 + src-tauri/src/credentials/record.rs | 22 +++- src-tauri/src/credentials/session.rs | 11 ++ src-tauri/src/fixtures/kv.sql | 13 +++ src-tauri/src/ipc.rs | 6 + src-tauri/src/kv.rs | 121 +++++++++++++++++++- src-tauri/src/state.rs | 7 ++ src/views/EnterAwsCredential.svelte | 93 --------------- src/views/Unlock.svelte | 3 + src/views/passphrase/EnterPassphrase.svelte | 5 + src/views/passphrase/ResetPassphrase.svelte | 42 +++++++ 11 files changed, 226 insertions(+), 98 deletions(-) create mode 100644 src-tauri/src/fixtures/kv.sql delete mode 100644 src/views/EnterAwsCredential.svelte create mode 100644 src/views/passphrase/ResetPassphrase.svelte diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index d3ff759..5ab5fd7 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -46,6 +46,7 @@ pub fn run() -> tauri::Result<()> { .invoke_handler(tauri::generate_handler![ ipc::unlock, ipc::lock, + ipc::reset_session, ipc::set_passphrase, ipc::respond, ipc::get_session_status, diff --git a/src-tauri/src/credentials/record.rs b/src-tauri/src/credentials/record.rs index b3f42dd..ca5e437 100644 --- a/src-tauri/src/credentials/record.rs +++ b/src-tauri/src/credentials/record.rs @@ -151,9 +151,11 @@ impl CredentialRecord { Ok(records) } - #[allow(unused_variables)] pub async fn rekey(old: &Crypto, new: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError> { - todo!() + for record in Self::list(old, pool).await? { + record.save(new, pool).await?; + } + Ok(()) } } @@ -340,6 +342,22 @@ mod tests { assert_eq!(aws_record(), records[0]); assert_eq!(aws_record_2(), records[1]); } + + + #[sqlx::test(fixtures("aws_credentials"))] + async fn test_rekey(pool: SqlitePool) { + let old = Crypto::fixed(); + let new = Crypto::random(); + + CredentialRecord::rekey(&old, &new, &pool).await + .expect("Failed to rekey credentials"); + + let records = CredentialRecord::list(&new, &pool).await + .expect("Failed to re-list credentials"); + + assert_eq!(aws_record(), records[0]); + assert_eq!(aws_record_2(), records[1]); + } } diff --git a/src-tauri/src/credentials/session.rs b/src-tauri/src/credentials/session.rs index 3afc8de..af96793 100644 --- a/src-tauri/src/credentials/session.rs +++ b/src-tauri/src/credentials/session.rs @@ -79,6 +79,17 @@ impl AppSession { Ok(()) } + pub async fn reset(&mut self, pool: &SqlitePool) -> Result<(), SaveCredentialsError> { + match self { + Self::Unlocked {..} | Self::Locked {..} => { + kv::delete_multi(pool, &["salt", "verify_nonce", "verify_blob"]).await?; + *self = Self::Empty; + }, + Self::Empty => (), + } + Ok(()) + } + pub fn try_get_crypto(&self) -> Result<&Crypto, GetCredentialsError> { match self { Self::Empty => Err(GetCredentialsError::Empty), diff --git a/src-tauri/src/fixtures/kv.sql b/src-tauri/src/fixtures/kv.sql new file mode 100644 index 0000000..00b2833 --- /dev/null +++ b/src-tauri/src/fixtures/kv.sql @@ -0,0 +1,13 @@ +INSERT INTO kv (name, value) +VALUES + -- b"hello world" (raw bytes) + ('test_bytes', X'68656C6C6F20776F726C64'), + + -- b"\"hello world\"" (JSON string) + ('test_string', X'2268656C6C6F20776F726C6422'), + + -- b"123" (JSON integer) + ('test_int', X'313233'), + + -- b"true" (JSON bool) + ('test_bool', X'74727565') diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index f42f4f2..8b8238e 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -80,6 +80,12 @@ pub async fn lock(app_state: State<'_, AppState>) -> Result<(), LockError> { } +#[tauri::command] +pub async fn reset_session(app_state: State<'_, AppState>) -> Result<(), SaveCredentialsError> { + app_state.reset_session().await +} + + #[tauri::command] pub async fn set_passphrase(passphrase: &str, app_state: State<'_, AppState>) -> Result<(), SaveCredentialsError> { app_state.set_passphrase(passphrase).await diff --git a/src-tauri/src/kv.rs b/src-tauri/src/kv.rs index cb5039e..7cdf31f 100644 --- a/src-tauri/src/kv.rs +++ b/src-tauri/src/kv.rs @@ -6,7 +6,7 @@ use crate::errors::*; pub async fn save(pool: &SqlitePool, name: &str, value: &T) -> Result<(), sqlx::Error> - where T: Serialize + where T: Serialize + ?Sized { let bytes = serde_json::to_vec(value).unwrap(); save_bytes(pool, name, &bytes).await @@ -44,9 +44,33 @@ pub async fn load_bytes(pool: &SqlitePool, name: &str) -> Result> } +pub async fn delete(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> { + sqlx::query!("DELETE FROM kv WHERE name = ?", name) + .execute(pool) + .await?; + Ok(()) +} + + +pub async fn delete_multi(pool: &SqlitePool, names: &[&str]) -> Result<(), sqlx::Error> { + let placeholder = names.iter() + .map(|_| "?") + .collect::>() + .join(","); + let query = format!("DELETE FROM kv WHERE name IN ({})", placeholder); + + let mut q = sqlx::query(&query); + for name in names { + q = q.bind(name); + } + q.execute(pool).await?; + Ok(()) +} + + macro_rules! load_bytes_multi { ( - $pool:ident, + $pool:expr, $($name:literal),* ) => { // wrap everything up in an async block for easy short-circuiting... @@ -78,7 +102,7 @@ pub(crate) use load_bytes_multi; // macro_rules! load_multi { // ( -// $pool:ident, +// $pool:expr, // $($name:literal),* // ) => { // (|| { @@ -93,3 +117,94 @@ pub(crate) use load_bytes_multi; // })() // } // } + + +#[cfg(test)] +mod tests { + use super::*; + + + #[sqlx::test] + async fn test_save_bytes(pool: SqlitePool) { + save_bytes(&pool, "test_bytes", b"hello world").await + .expect("Failed to save bytes"); + } + + + #[sqlx::test] + async fn test_save(pool: SqlitePool) { + save(&pool, "test_string", "hello world").await + .expect("Failed to save string"); + save(&pool, "test_int", &123).await + .expect("Failed to save integer"); + save(&pool, "test_bool", &true).await + .expect("Failed to save bool"); + } + + + #[sqlx::test(fixtures("kv"))] + async fn test_load_bytes(pool: SqlitePool) { + let bytes = load_bytes(&pool, "test_bytes").await + .expect("Failed to load bytes") + .expect("Test data not found in database"); + + assert_eq!(bytes, Vec::from(b"hello world")); + } + + + #[sqlx::test(fixtures("kv"))] + async fn test_load(pool: SqlitePool) { + let string: String = load(&pool, "test_string").await + .expect("Failed to load string") + .expect("Test data not found in database"); + assert_eq!(string, "hello world".to_string()); + + let integer: usize = load(&pool, "test_int").await + .expect("Failed to load integer") + .expect("Test data not found in database"); + assert_eq!(integer, 123); + + let boolean: bool = load(&pool, "test_bool").await + .expect("Failed to load boolean") + .expect("Test data not found in database"); + assert_eq!(boolean, true); + } + + + #[sqlx::test(fixtures("kv"))] + async fn test_load_multi(pool: SqlitePool) { + let (bytes, boolean) = load_bytes_multi!(&pool, "test_bytes", "test_bool") + .await + .expect("Failed to load items") + .expect("Test data not found in database"); + + assert_eq!(bytes, Vec::from(b"hello world")); + assert_eq!(boolean, Vec::from(b"true")); + } + + + #[sqlx::test(fixtures("kv"))] + async fn test_delete(pool: SqlitePool) { + delete(&pool, "test_bytes").await + .expect("Failed to delete data"); + + let loaded = load_bytes(&pool, "test_bytes").await + .expect("Failed to load data"); + assert_eq!(loaded, None); + } + + + #[sqlx::test(fixtures("kv"))] + async fn test_delete_multi(pool: SqlitePool) { + delete_multi(&pool, &["test_bytes", "test_string"]).await + .expect("Failed to delete keys"); + + let bytes_opt = load_bytes(&pool, "test_bytes").await + .expect("Failed to load bytes"); + assert_eq!(bytes_opt, None); + + let string_opt = load_bytes(&pool, "test_string").await + .expect("Failed to load string"); + assert_eq!(string_opt, None); + } +} diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 3a9e878..3d9da12 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -254,6 +254,13 @@ impl AppState { } } + pub async fn reset_session(&self) -> Result<(), SaveCredentialsError> { + let mut session = self.app_session.write().await; + session.reset(&self.pool).await?; + sqlx::query!("DELETE FROM credentials").execute(&self.pool).await?; + Ok(()) + } + pub async fn get_aws_base(&self, name: &str) -> Result { let app_session = self.app_session.read().await; let crypto = app_session.try_get_crypto()?; diff --git a/src/views/EnterAwsCredential.svelte b/src/views/EnterAwsCredential.svelte deleted file mode 100644 index 720e739..0000000 --- a/src/views/EnterAwsCredential.svelte +++ /dev/null @@ -1,93 +0,0 @@ - - - - -
-

Enter your credentials

- - {#if errorMsg} - {errorMsg} - {/if} - - - - - - - - - - -
diff --git a/src/views/Unlock.svelte b/src/views/Unlock.svelte index 9f887ea..b0aba93 100644 --- a/src/views/Unlock.svelte +++ b/src/views/Unlock.svelte @@ -10,6 +10,7 @@ import ErrorAlert from '../ui/ErrorAlert.svelte'; import Link from '../ui/Link.svelte'; import PassphraseInput from '../ui/PassphraseInput.svelte'; + import ResetPassphrase from './passphrase/ResetPassphrase.svelte'; import Spinner from '../ui/Spinner.svelte'; import vaultDoorSvg from '../assets/vault_door.svg?raw'; @@ -75,4 +76,6 @@ Submit {/if} + + diff --git a/src/views/passphrase/EnterPassphrase.svelte b/src/views/passphrase/EnterPassphrase.svelte index 59a07bb..458ef9a 100644 --- a/src/views/passphrase/EnterPassphrase.svelte +++ b/src/views/passphrase/EnterPassphrase.svelte @@ -6,6 +6,7 @@ import ErrorAlert from '../../ui/ErrorAlert.svelte'; import Link from '../../ui/Link.svelte'; import PassphraseInput from '../../ui/PassphraseInput.svelte'; + import ResetPassphrase from './ResetPassphrase.svelte'; import Spinner from '../../ui/Spinner.svelte'; export let cancellable = false; @@ -81,4 +82,8 @@ {/if} + + {#if $appState.sessionStatus === 'locked'} + + {/if} diff --git a/src/views/passphrase/ResetPassphrase.svelte b/src/views/passphrase/ResetPassphrase.svelte new file mode 100644 index 0000000..c6c76e0 --- /dev/null +++ b/src/views/passphrase/ResetPassphrase.svelte @@ -0,0 +1,42 @@ + + + + + + + +