commit 9e35760e5941576cacd8951a4ad2b75f7a6c416b
parent 54ea1910c3912cad5a072be440948a1d43092d93
Author: triesap <tyson@radroots.org>
Date: Sat, 21 Mar 2026 18:20:34 +0000
ios: add recovery-key backup for local identities
- add the ios home action to back up the selected local recovery key
- export the selected local secret through the existing accounts manager and convert it to nsec
- reveal the recovery key through the shared backup action result without changing other ios identity flows
- add ios coverage for selected local recovery-key export
Diffstat:
1 file changed, 51 insertions(+), 1 deletion(-)
diff --git a/crates/ios/src/lib.rs b/crates/ios/src/lib.rs
@@ -10,11 +10,15 @@ use radroots_app_core::{
SetupActionState,
};
#[cfg(any(target_os = "ios", test))]
+use radroots_identity::RadrootsIdentity;
+#[cfg(any(target_os = "ios", test))]
use radroots_nostr_accounts::prelude::{
RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus,
};
#[cfg(any(target_os = "ios", test))]
use std::path::Path;
+#[cfg(any(target_os = "ios", test))]
+use zeroize::Zeroizing;
#[cfg(any(target_os = "ios", test))]
mod storage;
@@ -74,6 +78,29 @@ impl IosBackend {
Self::identity_state_from_manager(manager)
}
+ fn export_selected_local_recovery_key(
+ manager: &RadrootsNostrAccountsManager,
+ ) -> Result<String, String> {
+ let Some(account_id) = manager
+ .selected_account_id()
+ .map_err(|source| source.to_string())?
+ else {
+ return Err("no selected local identity is available to back up".to_owned());
+ };
+
+ let Some(secret_key_hex) = manager
+ .export_secret_hex(&account_id)
+ .map_err(|source| source.to_string())?
+ else {
+ return Err("selected local identity does not have an exportable secret".to_owned());
+ };
+
+ let secret_key_hex = Zeroizing::new(secret_key_hex);
+ let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str())
+ .map_err(|source| source.to_string())?;
+ Ok(identity.nsec())
+ }
+
fn remove_all_local_identities(
manager: &RadrootsNostrAccountsManager,
) -> Result<IdentityGateState, String> {
@@ -135,6 +162,12 @@ impl RadrootsAppBackend for IosBackend {
fn home_action_states(&self) -> Vec<HomeActionState> {
vec![
HomeActionState {
+ kind: HomeActionKind::BackupRecoveryKey,
+ label: "Back Up Recovery Key".to_owned(),
+ enabled: true,
+ pending: false,
+ },
+ HomeActionState {
kind: HomeActionKind::RemoveLocalKey,
label: "Remove Key From This Device".to_owned(),
enabled: true,
@@ -152,7 +185,8 @@ impl RadrootsAppBackend for IosBackend {
fn request_home_action(&self, action: HomeActionKind) -> Result<HomeActionResult, String> {
let manager = Self::accounts_manager()?;
match action {
- HomeActionKind::BackupRecoveryKey => Ok(HomeActionResult::None),
+ HomeActionKind::BackupRecoveryKey => Self::export_selected_local_recovery_key(&manager)
+ .map(|nsec| HomeActionResult::RevealRecoveryKey { nsec }),
HomeActionKind::RemoveLocalKey => {
Self::remove_selected_local_identity(&manager).map(HomeActionResult::IdentityState)
}
@@ -278,6 +312,22 @@ mod tests {
}
#[test]
+ fn export_selected_local_recovery_key_returns_nsec() {
+ let manager = RadrootsNostrAccountsManager::new_in_memory();
+ let identity = RadrootsIdentity::generate();
+
+ manager
+ .upsert_identity(&identity, Some("primary".into()), true)
+ .expect("store identity");
+
+ let nsec =
+ IosBackend::export_selected_local_recovery_key(&manager).expect("export recovery");
+
+ assert_eq!(nsec, identity.nsec());
+ assert!(nsec.starts_with("nsec1"));
+ }
+
+ #[test]
fn remove_accounts_file_if_present_deletes_existing_file() {
let unique = format!(
"radroots-ios-reset-{}-{}.json",