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.
This commit is contained in:
Ellie Huxtable 2024-02-02 14:27:23 +00:00
parent 212dc928c9
commit 754f17ddb4
4 changed files with 62 additions and 0 deletions

View File

@ -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)]

View File

@ -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(

View File

@ -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,

View File

@ -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(())
}
}