aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--atuin-client/src/record/sqlite_store.rs31
-rw-r--r--atuin-client/src/record/store.rs2
-rw-r--r--atuin/src/command/client/store.rs3
-rw-r--r--atuin/src/command/client/store/purge.rs26
4 files changed, 62 insertions, 0 deletions
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<Option<Record<EncryptedData>>> {
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::<PASETO_V4>(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<Record<EncryptedData>>;
+ async fn delete(&self, id: RecordId) -> Result<()>;
async fn len(&self, host: HostId, tag: &str) -> Result<u64>;
async fn len_tag(&self, tag: &str) -> Result<u64>;
@@ -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(())
+ }
+}