From 754f17ddb4969973b9332a5324af42886e503c0f Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Fri, 2 Feb 2024 14:27:23 +0000 Subject: [PATCH] feat: add store purge command This command will delete all records from the local store that cannot be decrypted with the current key. If a verify fails before running this, it should pass _after_ running it. Required afterwards: - A `push --force`, to allow ensuring the remote store equals the local store (deletions have now occured!) - A `pull --force`, as once remote has been forced then local needs the same Nice to have: - Provide "old" keys to purge, in case the are not lost. Or maybe rekey. --- atuin-client/src/record/sqlite_store.rs | 31 +++++++++++++++++++++++++ atuin-client/src/record/store.rs | 2 ++ atuin/src/command/client/store.rs | 3 +++ atuin/src/command/client/store/purge.rs | 26 +++++++++++++++++++++ 4 files changed, 62 insertions(+) create mode 100644 atuin/src/command/client/store/purge.rs diff --git a/atuin-client/src/record/sqlite_store.rs b/atuin-client/src/record/sqlite_store.rs index 5df446b4..4e4a3756 100644 --- a/atuin-client/src/record/sqlite_store.rs +++ b/atuin-client/src/record/sqlite_store.rs @@ -145,6 +145,15 @@ impl Store for SqliteStore { Ok(res) } + async fn delete(&self, id: RecordId) -> Result<()> { + sqlx::query("delete from store where id = ?1") + .bind(id.0.as_hyphenated().to_string()) + .execute(&self.pool) + .await?; + + Ok(()) + } + async fn last(&self, host: HostId, tag: &str) -> Result>> { let res = sqlx::query("select * from store where host=?1 and tag=?2 order by idx desc limit 1") @@ -312,6 +321,28 @@ impl Store for SqliteStore { Ok(()) } + + /// Verify that every record in this store can be decrypted with the current key + /// Someday maybe also check each tag/record can be deserialized, but not for now. + async fn purge(&self, key: &[u8; 32]) -> Result<()> { + let all = self.load_all().await?; + + for record in all.iter() { + match record.clone().decrypt::(key) { + Ok(_) => continue, + Err(_) => { + println!( + "Failed to decrypt {}, deleting", + record.id.0.as_hyphenated() + ); + + self.delete(record.id).await?; + } + } + } + + Ok(()) + } } #[cfg(test)] diff --git a/atuin-client/src/record/store.rs b/atuin-client/src/record/store.rs index 04fba630..1d812549 100644 --- a/atuin-client/src/record/store.rs +++ b/atuin-client/src/record/store.rs @@ -21,6 +21,7 @@ pub trait Store { ) -> Result<()>; async fn get(&self, id: RecordId) -> Result>; + async fn delete(&self, id: RecordId) -> Result<()>; async fn len(&self, host: HostId, tag: &str) -> Result; async fn len_tag(&self, tag: &str) -> Result; @@ -30,6 +31,7 @@ pub trait Store { async fn re_encrypt(&self, old_key: &[u8; 32], new_key: &[u8; 32]) -> Result<()>; async fn verify(&self, key: &[u8; 32]) -> Result<()>; + async fn purge(&self, key: &[u8; 32]) -> Result<()>; /// Get the next `limit` records, after and including the given index async fn next( diff --git a/atuin/src/command/client/store.rs b/atuin/src/command/client/store.rs index 4729a0f3..16618b04 100644 --- a/atuin/src/command/client/store.rs +++ b/atuin/src/command/client/store.rs @@ -11,6 +11,7 @@ use time::OffsetDateTime; #[cfg(feature = "sync")] mod push; +mod purge; mod rebuild; mod rekey; mod verify; @@ -21,6 +22,7 @@ pub enum Cmd { Status, Rebuild(rebuild::Rebuild), Rekey(rekey::Rekey), + Purge(purge::Purge), Verify(verify::Verify), #[cfg(feature = "sync")] @@ -39,6 +41,7 @@ impl Cmd { Self::Rebuild(rebuild) => rebuild.run(settings, store, database).await, Self::Rekey(rekey) => rekey.run(settings, store).await, Self::Verify(verify) => verify.run(settings, store).await, + Self::Purge(purge) => purge.run(settings, store).await, #[cfg(feature = "sync")] Self::Push(push) => push.run(settings, store).await, diff --git a/atuin/src/command/client/store/purge.rs b/atuin/src/command/client/store/purge.rs new file mode 100644 index 00000000..ad2369ce --- /dev/null +++ b/atuin/src/command/client/store/purge.rs @@ -0,0 +1,26 @@ +use clap::Args; +use eyre::Result; + +use atuin_client::{ + encryption::load_key, + record::{sqlite_store::SqliteStore, store::Store}, + settings::Settings, +}; + +#[derive(Args, Debug)] +pub struct Purge {} + +impl Purge { + pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> { + println!("Purging local records that cannot be decrypted"); + + let key = load_key(settings)?; + + match store.purge(&key.into()).await { + Ok(()) => println!("Local store purge completed OK"), + Err(e) => println!("Failed to purge local store: {e:?}"), + } + + Ok(()) + } +}