app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit e0207936c5894343236f10a29640bdcf82352d7e
parent ce88bb96fd54ce45893521de1ee5defaba2a33e5
Author: triesap <tyson@radroots.org>
Date:   Sat, 28 Mar 2026 18:14:43 +0000

app: purge remote signer state during reset

Diffstat:
Mcrates/android/src/lib.rs | 1+
Mcrates/android/src/remote_signer.rs | 6++++++
Mcrates/desktop/src/main.rs | 1+
Mcrates/desktop/src/remote_signer.rs | 6++++++
Mcrates/ios/src/lib.rs | 1+
Mcrates/ios/src/remote_signer.rs | 6++++++
Mcrates/remote-signer/src/custody.rs | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/remote-signer/src/lib.rs | 4+++-
8 files changed, 83 insertions(+), 1 deletion(-)

diff --git a/crates/android/src/lib.rs b/crates/android/src/lib.rs @@ -875,6 +875,7 @@ impl AndroidBackend { manager: &RadrootsNostrAccountsManager, accounts_path: &Path, ) -> Result<IdentityGateState, String> { + remote_signer::purge_all_custody_state()?; let state = Self::remove_all_local_identities(manager)?; Self::remove_accounts_file_if_present(accounts_path)?; Ok(state) diff --git a/crates/android/src/remote_signer.rs b/crates/android/src/remote_signer.rs @@ -9,6 +9,7 @@ use radroots_app_remote_signer::{ RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSessionRecord, RadrootsAppRemoteSignerSessionStoreState, radroots_app_remote_signer_clear_pending_session, radroots_app_remote_signer_disconnect_selected, radroots_app_remote_signer_preview, + radroots_app_remote_signer_purge_all_custody_state, radroots_app_remote_signer_reconcile_startup, }; use radroots_identity::RadrootsIdentityId; @@ -203,6 +204,11 @@ pub(crate) fn cancel_pending_connection() -> Result<(), String> { Ok(()) } +pub(crate) fn purge_all_custody_state() -> Result<(), String> { + let store_path = sessions_path()?; + radroots_app_remote_signer_purge_all_custody_state(store_path.as_path(), remove_client_secret) +} + fn activate_remote_session( client_account_id: &str, user_identity: radroots_identity::RadrootsIdentityPublic, diff --git a/crates/desktop/src/main.rs b/crates/desktop/src/main.rs @@ -361,6 +361,7 @@ impl DesktopBackend { manager: &RadrootsNostrAccountsManager, accounts_path: &Path, ) -> Result<IdentityGateState, String> { + remote_signer::purge_all_custody_state()?; let state = Self::remove_all_local_identities(manager)?; Self::remove_accounts_file_if_present(accounts_path)?; Ok(state) diff --git a/crates/desktop/src/remote_signer.rs b/crates/desktop/src/remote_signer.rs @@ -9,6 +9,7 @@ use radroots_app_remote_signer::{ RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSessionRecord, RadrootsAppRemoteSignerSessionStoreState, radroots_app_remote_signer_clear_pending_session, radroots_app_remote_signer_disconnect_selected, radroots_app_remote_signer_preview, + radroots_app_remote_signer_purge_all_custody_state, radroots_app_remote_signer_reconcile_startup, }; use radroots_nostr_accounts::prelude::{ @@ -202,6 +203,11 @@ pub(crate) fn cancel_pending_connection() -> Result<(), String> { Ok(()) } +pub(crate) fn purge_all_custody_state() -> Result<(), String> { + let store_path = sessions_path()?; + radroots_app_remote_signer_purge_all_custody_state(store_path.as_path(), remove_client_secret) +} + fn activate_remote_session( client_account_id: &str, user_identity: radroots_identity::RadrootsIdentityPublic, diff --git a/crates/ios/src/lib.rs b/crates/ios/src/lib.rs @@ -316,6 +316,7 @@ impl IosBackend { manager: &RadrootsNostrAccountsManager, accounts_path: &Path, ) -> Result<IdentityGateState, String> { + remote_signer::purge_all_custody_state()?; let state = Self::remove_all_local_identities(manager)?; Self::remove_accounts_file_if_present(accounts_path)?; Ok(state) diff --git a/crates/ios/src/remote_signer.rs b/crates/ios/src/remote_signer.rs @@ -9,6 +9,7 @@ use radroots_app_remote_signer::{ RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSessionRecord, RadrootsAppRemoteSignerSessionStoreState, radroots_app_remote_signer_clear_pending_session, radroots_app_remote_signer_disconnect_selected, radroots_app_remote_signer_preview, + radroots_app_remote_signer_purge_all_custody_state, radroots_app_remote_signer_reconcile_startup, }; use radroots_identity::RadrootsIdentityId; @@ -203,6 +204,11 @@ pub(crate) fn cancel_pending_connection() -> Result<(), String> { Ok(()) } +pub(crate) fn purge_all_custody_state() -> Result<(), String> { + let store_path = sessions_path()?; + radroots_app_remote_signer_purge_all_custody_state(store_path.as_path(), remove_client_secret) +} + fn activate_remote_session( client_account_id: &str, user_identity: radroots_identity::RadrootsIdentityPublic, diff --git a/crates/remote-signer/src/custody.rs b/crates/remote-signer/src/custody.rs @@ -126,6 +126,18 @@ pub fn radroots_app_remote_signer_reconcile_startup( Ok(()) } +pub fn radroots_app_remote_signer_purge_all_custody_state( + path: &Path, + remove_client_secret: impl Fn(&str) -> Result<(), String>, +) -> Result<(), String> { + let load = load_sessions_with_recovery(path)?; + for record in &load.state.sessions { + remove_client_secret(record.client_account_id())?; + } + remove_sessions_file_if_present(path)?; + Ok(()) +} + fn remote_signer_public_only_accounts( manager: &RadrootsNostrAccountsManager, accounts: &[RadrootsNostrAccountRecord], @@ -165,6 +177,16 @@ fn save_sessions( state.save(path).map_err(|error| error.to_string()) } +fn remove_sessions_file_if_present(path: &Path) -> Result<(), String> { + match std::fs::remove_file(path) { + Ok(()) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(format!( + "failed to remove remote signer session store: {error}" + )), + } +} + #[cfg(test)] mod tests { use super::*; @@ -350,4 +372,41 @@ mod tests { .is_empty() ); } + + #[test] + fn purge_all_custody_state_removes_all_tracked_client_secrets_and_session_file() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("sessions.json"); + let pending = write_pending_state(path.as_path()); + let mut active = write_active_state(path.as_path()); + active.client_identity = fixture_public(&FIXTURE_BOB); + let mut state = + RadrootsAppRemoteSignerSessionStoreState::load(path.as_path()).expect("load"); + state.sessions.push(active.clone()); + state.save(path.as_path()).expect("save"); + + let vault = RadrootsNostrSecretVaultMemory::new(); + secret_store_secret(&vault, pending.client_account_id(), "pending"); + secret_store_secret(&vault, active.client_account_id(), "active"); + + radroots_app_remote_signer_purge_all_custody_state( + path.as_path(), + secret_remover(vault.clone()), + ) + .expect("purge"); + + assert!(!path.exists()); + assert!( + vault + .load_secret_hex(&fixture_account_id(pending.client_account_id())) + .expect("pending removed") + .is_none() + ); + assert!( + vault + .load_secret_hex(&fixture_account_id(active.client_account_id())) + .expect("active removed") + .is_none() + ); + } } diff --git a/crates/remote-signer/src/lib.rs b/crates/remote-signer/src/lib.rs @@ -10,7 +10,9 @@ mod session; pub use controller::{RadrootsAppRemoteSignerController, RadrootsAppRemoteSignerControllerHooks}; pub use custody::{ radroots_app_remote_signer_clear_pending_session, - radroots_app_remote_signer_disconnect_selected, radroots_app_remote_signer_reconcile_startup, + radroots_app_remote_signer_disconnect_selected, + radroots_app_remote_signer_purge_all_custody_state, + radroots_app_remote_signer_reconcile_startup, }; pub use error::RadrootsAppRemoteSignerError; pub use input::{