commit cee710880cf5a40687412feba0c2ccb8d6578f5d
parent 6c6e736eb1f6cdd518e905889a2cd205aa6f4e40
Author: triesap <tyson@radroots.org>
Date: Sat, 21 Mar 2026 19:38:50 +0000
desktop: add secret-key import for local identities
- rename the shared backup and import language from recovery key to secret key
- add the macos desktop setup action to import a local identity from an nsec secret key
- wire the imported identity through the existing accounts manager and select it immediately
- keep ios android web and the lifecycle doc aligned with the corrected secret-key terminology
Diffstat:
5 files changed, 205 insertions(+), 137 deletions(-)
diff --git a/crates/android/src/lib.rs b/crates/android/src/lib.rs
@@ -80,13 +80,13 @@ impl RadrootsAppBackend for AndroidBackend {
fn home_action_states(&self) -> Vec<HomeActionState> {
#[cfg(target_os = "android")]
{
- let recovery_key_export_pending = Self::recovery_key_export_pending();
+ let secret_key_export_pending = Self::secret_key_export_pending();
return vec![
HomeActionState {
- kind: HomeActionKind::BackupRecoveryKey,
- label: "Back Up Recovery Key".to_owned(),
- enabled: !recovery_key_export_pending,
- pending: recovery_key_export_pending,
+ kind: HomeActionKind::BackupSecretKey,
+ label: "Back Up Secret Key".to_owned(),
+ enabled: !secret_key_export_pending,
+ pending: secret_key_export_pending,
},
HomeActionState {
kind: HomeActionKind::RemoveLocalKey,
@@ -113,8 +113,8 @@ impl RadrootsAppBackend for AndroidBackend {
#[cfg(target_os = "android")]
{
return match action {
- HomeActionKind::BackupRecoveryKey => {
- Self::begin_recovery_key_export().map(|()| HomeActionResult::None)
+ HomeActionKind::BackupSecretKey => {
+ Self::begin_secret_key_export().map(|()| HomeActionResult::None)
}
HomeActionKind::RemoveLocalKey => {
let manager = Self::accounts_manager()?;
@@ -141,7 +141,7 @@ impl RadrootsAppBackend for AndroidBackend {
fn poll_home_action_result(&self) -> Result<Option<HomeActionResult>, String> {
#[cfg(target_os = "android")]
{
- return Self::poll_recovery_key_export();
+ return Self::poll_secret_key_export();
}
#[cfg(not(target_os = "android"))]
@@ -214,7 +214,7 @@ impl AndroidBackend {
Self::identity_state_from_manager(manager)
}
- fn export_selected_local_recovery_key(
+ fn export_selected_local_secret_key(
manager: &RadrootsNostrAccountsManager,
) -> Result<String, String> {
let Some(account_id) = manager
@@ -238,35 +238,35 @@ impl AndroidBackend {
}
#[cfg(target_os = "android")]
- fn begin_recovery_key_export() -> Result<(), String> {
- security::begin_user_presence_verification("reveal the current recovery key")
+ fn begin_secret_key_export() -> Result<(), String> {
+ security::begin_user_presence_verification("reveal the current secret key")
.map_err(|source| source.to_string())
}
#[cfg(not(target_os = "android"))]
- fn begin_recovery_key_export() -> Result<(), String> {
+ fn begin_secret_key_export() -> Result<(), String> {
Ok(())
}
#[cfg(target_os = "android")]
- fn recovery_key_export_pending() -> bool {
+ fn secret_key_export_pending() -> bool {
security::is_user_presence_verification_pending().unwrap_or(false)
}
#[cfg(not(target_os = "android"))]
- fn recovery_key_export_pending() -> bool {
+ fn secret_key_export_pending() -> bool {
false
}
#[cfg(target_os = "android")]
- fn poll_recovery_key_export() -> Result<Option<HomeActionResult>, String> {
+ fn poll_secret_key_export() -> Result<Option<HomeActionResult>, String> {
match security::take_user_presence_verification_result()
.map_err(|source| source.to_string())?
{
Some(security::AndroidUserPresenceVerificationResult::Verified) => {
let manager = Self::accounts_manager()?;
- Self::export_selected_local_recovery_key(&manager)
- .map(|nsec| Some(HomeActionResult::RevealRecoveryKey { nsec }))
+ Self::export_selected_local_secret_key(&manager)
+ .map(|nsec| Some(HomeActionResult::RevealSecretKey { nsec }))
}
Some(security::AndroidUserPresenceVerificationResult::Failed(message)) => Err(message),
None => Ok(None),
@@ -274,7 +274,7 @@ impl AndroidBackend {
}
#[cfg(not(target_os = "android"))]
- fn poll_recovery_key_export() -> Result<Option<HomeActionResult>, String> {
+ fn poll_secret_key_export() -> Result<Option<HomeActionResult>, String> {
Ok(None)
}
@@ -494,7 +494,7 @@ mod tests {
}
#[test]
- fn export_selected_local_recovery_key_returns_nsec() {
+ fn export_selected_local_secret_key_returns_nsec() {
let manager = RadrootsNostrAccountsManager::new_in_memory();
let identity = RadrootsIdentity::generate();
@@ -503,7 +503,7 @@ mod tests {
.expect("store identity");
let nsec =
- AndroidBackend::export_selected_local_recovery_key(&manager).expect("export recovery");
+ AndroidBackend::export_selected_local_secret_key(&manager).expect("export secret");
assert_eq!(nsec, identity.nsec());
assert!(nsec.starts_with("nsec1"));
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
@@ -13,7 +13,7 @@ pub struct SetupActionState {
}
#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RecoveryActionState {
+pub struct ImportActionState {
pub label: String,
pub enabled: bool,
pub pending: bool,
@@ -29,7 +29,7 @@ pub struct HomeActionState {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HomeActionKind {
- BackupRecoveryKey,
+ BackupSecretKey,
RemoveLocalKey,
ResetDevice,
DisconnectSigner,
@@ -39,7 +39,7 @@ pub enum HomeActionKind {
pub enum HomeActionResult {
None,
IdentityState(IdentityGateState),
- RevealRecoveryKey { nsec: String },
+ RevealSecretKey { nsec: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -53,12 +53,12 @@ pub trait RadrootsAppBackend {
fn load_identity_state(&self) -> Result<IdentityGateState, String>;
fn setup_action_state(&self) -> SetupActionState;
fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String>;
- fn recovery_action_state(&self) -> Option<RecoveryActionState> {
+ fn import_action_state(&self) -> Option<ImportActionState> {
None
}
- fn request_recovery_action(
+ fn request_import_action(
&self,
- _recovery_key: &str,
+ _secret_key: &str,
) -> Result<Option<IdentityGateState>, String> {
Ok(None)
}
@@ -87,9 +87,9 @@ pub struct RadrootsApp {
screen: AppScreen,
status_message: Option<String>,
pending_home_confirmation: Option<HomeActionKind>,
- pending_recovery_entry: bool,
- recovery_key_input: Zeroizing<String>,
- revealed_recovery_key: Option<Zeroizing<String>>,
+ pending_import_entry: bool,
+ secret_key_input: Zeroizing<String>,
+ revealed_secret_key: Option<Zeroizing<String>>,
}
impl RadrootsApp {
@@ -99,9 +99,9 @@ impl RadrootsApp {
screen: AppScreen::Setup,
status_message: None,
pending_home_confirmation: None,
- pending_recovery_entry: false,
- recovery_key_input: Zeroizing::new(String::new()),
- revealed_recovery_key: None,
+ pending_import_entry: false,
+ secret_key_input: Zeroizing::new(String::new()),
+ revealed_secret_key: None,
};
match app.backend.load_identity_state() {
Ok(state) => app.apply_identity_state(state),
@@ -119,32 +119,32 @@ impl RadrootsApp {
self.screen = AppScreen::Setup;
self.status_message = None;
self.pending_home_confirmation = None;
- self.pending_recovery_entry = false;
- self.recovery_key_input.clear();
- self.revealed_recovery_key = None;
+ self.pending_import_entry = false;
+ self.secret_key_input.clear();
+ self.revealed_secret_key = None;
}
IdentityGateState::Ready { account_id, npub } => {
self.screen = AppScreen::Home { account_id, npub };
self.status_message = None;
self.pending_home_confirmation = None;
- self.pending_recovery_entry = false;
- self.recovery_key_input.clear();
- self.revealed_recovery_key = None;
+ self.pending_import_entry = false;
+ self.secret_key_input.clear();
+ self.revealed_secret_key = None;
}
IdentityGateState::Unsupported { reason } => {
self.screen = AppScreen::Setup;
self.status_message = Some(reason);
self.pending_home_confirmation = None;
- self.pending_recovery_entry = false;
- self.recovery_key_input.clear();
- self.revealed_recovery_key = None;
+ self.pending_import_entry = false;
+ self.secret_key_input.clear();
+ self.revealed_secret_key = None;
}
}
}
fn request_setup_action(&mut self) {
self.status_message = None;
- self.revealed_recovery_key = None;
+ self.revealed_secret_key = None;
match self.backend.request_setup_action() {
Ok(Some(state)) => self.apply_identity_state(state),
Ok(None) => {}
@@ -154,12 +154,12 @@ impl RadrootsApp {
}
}
- fn request_recovery_action(&mut self) {
+ fn request_import_action(&mut self) {
self.status_message = None;
- self.revealed_recovery_key = None;
+ self.revealed_secret_key = None;
match self
.backend
- .request_recovery_action(self.recovery_key_input.trim())
+ .request_import_action(self.secret_key_input.trim())
{
Ok(Some(state)) => self.apply_identity_state(state),
Ok(None) => {}
@@ -171,7 +171,7 @@ impl RadrootsApp {
fn request_home_action(&mut self, action: HomeActionKind) {
self.status_message = None;
- self.revealed_recovery_key = None;
+ self.revealed_secret_key = None;
match self.backend.request_home_action(action) {
Ok(result) => self.apply_home_action_result(result),
Err(err) => {
@@ -183,8 +183,8 @@ impl RadrootsApp {
fn apply_home_action_result(&mut self, result: HomeActionResult) {
match result {
HomeActionResult::IdentityState(state) => self.apply_identity_state(state),
- HomeActionResult::RevealRecoveryKey { nsec } => {
- self.revealed_recovery_key = Some(Zeroizing::new(nsec));
+ HomeActionResult::RevealSecretKey { nsec } => {
+ self.revealed_secret_key = Some(Zeroizing::new(nsec));
self.pending_home_confirmation = None;
}
HomeActionResult::None => {}
@@ -192,13 +192,13 @@ impl RadrootsApp {
}
fn home_action_requires_confirmation(action: HomeActionKind) -> bool {
- !matches!(action, HomeActionKind::BackupRecoveryKey)
+ !matches!(action, HomeActionKind::BackupSecretKey)
}
fn home_action_confirmation_message(action: HomeActionKind) -> &'static str {
match action {
- HomeActionKind::BackupRecoveryKey => {
- "This reveals the current local recovery key for backup. Do not share it."
+ HomeActionKind::BackupSecretKey => {
+ "This reveals the current local secret key for backup. Do not share it."
}
HomeActionKind::RemoveLocalKey => {
"This removes the current key from this device and returns the app to setup."
@@ -246,9 +246,9 @@ impl eframe::App for RadrootsApp {
if action.pending {
ctx.request_repaint();
}
- let recovery_action = self.backend.recovery_action_state();
- if let Some(recovery_action) = &recovery_action {
- if recovery_action.pending {
+ let import_action = self.backend.import_action_state();
+ if let Some(import_action) = &import_action {
+ if import_action.pending {
ctx.request_repaint();
}
}
@@ -264,17 +264,17 @@ impl eframe::App for RadrootsApp {
self.request_setup_action();
}
- if let Some(recovery_action) = recovery_action {
+ if let Some(import_action) = import_action {
ui.add_space(12.0);
- if self.pending_recovery_entry {
+ if self.pending_import_entry {
ui.vertical_centered(|ui| {
ui.set_max_width(ui.available_width().min(560.0));
ui.label(
- "Recover an existing local identity by entering its nsec recovery key.",
+ "Import an existing local identity by entering its nsec secret key.",
);
ui.add_space(8.0);
ui.add(
- egui::TextEdit::singleline(&mut *self.recovery_key_input)
+ egui::TextEdit::singleline(&mut *self.secret_key_input)
.hint_text("nsec1...")
.desired_width(ui.available_width()),
);
@@ -282,23 +282,23 @@ impl eframe::App for RadrootsApp {
ui.horizontal_centered(|ui| {
let confirm_clicked = ui
.add_enabled(
- recovery_action.enabled,
- egui::Button::new(recovery_action.label.clone()),
+ import_action.enabled,
+ egui::Button::new(import_action.label.clone()),
)
.clicked();
if confirm_clicked {
- self.request_recovery_action();
+ self.request_import_action();
}
if ui.button("Cancel").clicked() {
- self.pending_recovery_entry = false;
- self.recovery_key_input.clear();
+ self.pending_import_entry = false;
+ self.secret_key_input.clear();
self.status_message = None;
}
});
});
- } else if ui.button(recovery_action.label).clicked() {
- self.pending_recovery_entry = true;
+ } else if ui.button(import_action.label).clicked() {
+ self.pending_import_entry = true;
self.status_message = None;
}
}
@@ -359,14 +359,14 @@ impl eframe::App for RadrootsApp {
}
}
- if let Some(nsec) = &self.revealed_recovery_key {
+ if let Some(nsec) = &self.revealed_secret_key {
ui.add_space(20.0);
- ui.label("Recovery key");
+ ui.label("Secret key");
ui.add_space(8.0);
ui.monospace(nsec.as_str());
ui.add_space(8.0);
- if ui.button("Dismiss Recovery Key").clicked() {
- self.revealed_recovery_key = None;
+ if ui.button("Dismiss Secret Key").clicked() {
+ self.revealed_secret_key = None;
}
}
}
@@ -392,10 +392,10 @@ mod tests {
struct MockBackend {
load: Result<IdentityGateState, String>,
action_state: Rc<RefCell<SetupActionState>>,
- recovery_action_state: Rc<RefCell<Option<RecoveryActionState>>>,
+ import_action_state: Rc<RefCell<Option<ImportActionState>>>,
home_action_states: Rc<RefCell<Vec<HomeActionState>>>,
request: Rc<RefCell<VecDeque<Result<Option<IdentityGateState>, String>>>>,
- recovery_request: Rc<RefCell<VecDeque<Result<Option<IdentityGateState>, String>>>>,
+ import_request: Rc<RefCell<VecDeque<Result<Option<IdentityGateState>, String>>>>,
home_request: Rc<RefCell<VecDeque<(HomeActionKind, Result<HomeActionResult, String>)>>>,
home_poll: Rc<RefCell<VecDeque<Result<Option<HomeActionResult>, String>>>>,
poll: Rc<RefCell<VecDeque<Result<Option<IdentityGateState>, String>>>>,
@@ -411,23 +411,23 @@ mod tests {
Self {
load,
action_state: Rc::new(RefCell::new(action_state)),
- recovery_action_state: Rc::new(RefCell::new(None)),
+ import_action_state: Rc::new(RefCell::new(None)),
home_action_states: Rc::new(RefCell::new(Vec::new())),
request: Rc::new(RefCell::new(request.into())),
- recovery_request: Rc::new(RefCell::new(VecDeque::new())),
+ import_request: Rc::new(RefCell::new(VecDeque::new())),
home_request: Rc::new(RefCell::new(VecDeque::new())),
home_poll: Rc::new(RefCell::new(VecDeque::new())),
poll: Rc::new(RefCell::new(poll.into())),
}
}
- fn with_recovery_action(
+ fn with_import_action(
self,
- action_state: RecoveryActionState,
+ action_state: ImportActionState,
request: Vec<Result<Option<IdentityGateState>, String>>,
) -> Self {
- *self.recovery_action_state.borrow_mut() = Some(action_state);
- self.recovery_request.borrow_mut().extend(request);
+ *self.import_action_state.borrow_mut() = Some(action_state);
+ self.import_request.borrow_mut().extend(request);
self
}
@@ -472,15 +472,15 @@ mod tests {
.unwrap_or_else(|| Err("missing request response".into()))
}
- fn recovery_action_state(&self) -> Option<RecoveryActionState> {
- self.recovery_action_state.borrow().clone()
+ fn import_action_state(&self) -> Option<ImportActionState> {
+ self.import_action_state.borrow().clone()
}
- fn request_recovery_action(
+ fn request_import_action(
&self,
- _recovery_key: &str,
+ _secret_key: &str,
) -> Result<Option<IdentityGateState>, String> {
- self.recovery_request
+ self.import_request
.borrow_mut()
.pop_front()
.unwrap_or(Ok(None))
@@ -780,7 +780,7 @@ mod tests {
}
#[test]
- fn recovery_action_transitions_to_home() {
+ fn import_action_transitions_to_home() {
let mut app = RadrootsApp::new(Box::new(
MockBackend::new(
Ok(IdentityGateState::Missing),
@@ -792,9 +792,9 @@ mod tests {
pending: false,
},
)
- .with_recovery_action(
- RecoveryActionState {
- label: "Recover Existing Key".into(),
+ .with_import_action(
+ ImportActionState {
+ label: "Import Secret Key".into(),
enabled: true,
pending: false,
},
@@ -805,9 +805,9 @@ mod tests {
),
));
- app.pending_recovery_entry = true;
- app.recovery_key_input = Zeroizing::new("nsec1example".into());
- app.request_recovery_action();
+ app.pending_import_entry = true;
+ app.secret_key_input = Zeroizing::new("nsec1example".into());
+ app.request_import_action();
assert_eq!(
app.screen,
@@ -816,12 +816,12 @@ mod tests {
npub: "npub1abc".into(),
}
);
- assert_eq!(app.pending_recovery_entry, false);
- assert_eq!(app.recovery_key_input.as_str(), "");
+ assert_eq!(app.pending_import_entry, false);
+ assert_eq!(app.secret_key_input.as_str(), "");
}
#[test]
- fn backup_home_action_reveals_recovery_key_without_leaving_home() {
+ fn backup_home_action_reveals_secret_key_without_leaving_home() {
let mut app = RadrootsApp::new(Box::new(
MockBackend::new(
Ok(IdentityGateState::Ready {
@@ -838,31 +838,29 @@ mod tests {
)
.with_home_action(
HomeActionState {
- kind: HomeActionKind::BackupRecoveryKey,
- label: "Back Up Recovery Key".into(),
+ kind: HomeActionKind::BackupSecretKey,
+ label: "Back Up Secret Key".into(),
enabled: true,
pending: false,
},
- vec![Ok(HomeActionResult::RevealRecoveryKey {
+ vec![Ok(HomeActionResult::RevealSecretKey {
nsec: "nsec1example".into(),
})],
),
));
- app.request_home_action(HomeActionKind::BackupRecoveryKey);
+ app.request_home_action(HomeActionKind::BackupSecretKey);
assert!(matches!(app.screen, AppScreen::Home { .. }));
assert_eq!(app.pending_home_confirmation, None);
assert_eq!(
- app.revealed_recovery_key
- .as_ref()
- .map(|value| value.as_str()),
+ app.revealed_secret_key.as_ref().map(|value| value.as_str()),
Some("nsec1example")
);
}
#[test]
- fn deferred_backup_home_action_reveals_recovery_key_after_poll() {
+ fn deferred_backup_home_action_reveals_secret_key_after_poll() {
let mut app = RadrootsApp::new(Box::new(
MockBackend::new(
Ok(IdentityGateState::Ready {
@@ -879,29 +877,25 @@ mod tests {
)
.with_home_action(
HomeActionState {
- kind: HomeActionKind::BackupRecoveryKey,
- label: "Back Up Recovery Key".into(),
+ kind: HomeActionKind::BackupSecretKey,
+ label: "Back Up Secret Key".into(),
enabled: true,
pending: true,
},
vec![Ok(HomeActionResult::None)],
)
- .with_home_action_poll(vec![Ok(Some(
- HomeActionResult::RevealRecoveryKey {
- nsec: "nsec1example".into(),
- },
- ))]),
+ .with_home_action_poll(vec![Ok(Some(HomeActionResult::RevealSecretKey {
+ nsec: "nsec1example".into(),
+ }))]),
));
- app.request_home_action(HomeActionKind::BackupRecoveryKey);
- assert_eq!(app.revealed_recovery_key, None);
+ app.request_home_action(HomeActionKind::BackupSecretKey);
+ assert_eq!(app.revealed_secret_key, None);
app.sync_backend();
assert_eq!(
- app.revealed_recovery_key
- .as_ref()
- .map(|value| value.as_str()),
+ app.revealed_secret_key.as_ref().map(|value| value.as_str()),
Some("nsec1example")
);
}
diff --git a/crates/desktop/src/main.rs b/crates/desktop/src/main.rs
@@ -9,8 +9,8 @@ use radroots_app_apple_security::{
APPLE_NOSTR_SERVICE, RadrootsAppleKeychainVault, verify_user_presence,
};
use radroots_app_core::{
- APP_NAME, HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, RadrootsApp,
- RadrootsAppBackend, SetupActionState,
+ APP_NAME, HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState,
+ ImportActionState, RadrootsApp, RadrootsAppBackend, SetupActionState,
};
#[cfg(target_os = "macos")]
use radroots_identity::RadrootsIdentity;
@@ -145,10 +145,10 @@ impl DesktopBackend {
}
#[cfg(target_os = "macos")]
- fn export_selected_local_recovery_key(
+ fn export_selected_local_secret_key(
manager: &RadrootsNostrAccountsManager,
) -> Result<String, String> {
- verify_user_presence("reveal the current recovery key")
+ verify_user_presence("reveal the current secret key")
.map_err(|source| source.to_string())?;
let Some(account_id) = manager
@@ -172,6 +172,24 @@ impl DesktopBackend {
}
#[cfg(target_os = "macos")]
+ fn import_local_identity(
+ manager: &RadrootsNostrAccountsManager,
+ secret_key: &str,
+ ) -> Result<IdentityGateState, String> {
+ let identity = RadrootsIdentity::from_secret_key_str(secret_key)
+ .map_err(|_| "invalid secret key".to_owned())?;
+
+ manager
+ .upsert_identity(&identity, None, true)
+ .map_err(|source| source.to_string())?;
+
+ let status = manager
+ .selected_account_status()
+ .map_err(|source| source.to_string())?;
+ Ok(Self::map_status(status))
+ }
+
+ #[cfg(target_os = "macos")]
fn remove_all_local_identities(
manager: &RadrootsNostrAccountsManager,
) -> Result<IdentityGateState, String> {
@@ -273,13 +291,43 @@ impl RadrootsAppBackend for DesktopBackend {
}
}
+ fn import_action_state(&self) -> Option<ImportActionState> {
+ #[cfg(target_os = "macos")]
+ {
+ return Some(ImportActionState {
+ label: "Import Secret Key".to_owned(),
+ enabled: true,
+ pending: false,
+ });
+ }
+
+ #[cfg(not(target_os = "macos"))]
+ {
+ None
+ }
+ }
+
+ fn request_import_action(&self, secret_key: &str) -> Result<Option<IdentityGateState>, String> {
+ #[cfg(target_os = "macos")]
+ {
+ let manager = Self::accounts_manager()?;
+ return Self::import_local_identity(&manager, secret_key).map(Some);
+ }
+
+ #[cfg(not(target_os = "macos"))]
+ {
+ let _ = secret_key;
+ Ok(None)
+ }
+ }
+
fn home_action_states(&self) -> Vec<HomeActionState> {
#[cfg(target_os = "macos")]
{
return vec![
HomeActionState {
- kind: HomeActionKind::BackupRecoveryKey,
- label: "Back Up Recovery Key".to_owned(),
+ kind: HomeActionKind::BackupSecretKey,
+ label: "Back Up Secret Key".to_owned(),
enabled: true,
pending: false,
},
@@ -309,10 +357,8 @@ impl RadrootsAppBackend for DesktopBackend {
{
let manager = Self::accounts_manager()?;
return match action {
- HomeActionKind::BackupRecoveryKey => {
- Self::export_selected_local_recovery_key(&manager)
- .map(|nsec| HomeActionResult::RevealRecoveryKey { nsec })
- }
+ HomeActionKind::BackupSecretKey => Self::export_selected_local_secret_key(&manager)
+ .map(|nsec| HomeActionResult::RevealSecretKey { nsec }),
HomeActionKind::RemoveLocalKey => Self::remove_selected_local_identity(&manager)
.map(HomeActionResult::IdentityState),
HomeActionKind::ResetDevice => {
@@ -434,7 +480,7 @@ mod tests {
}
#[test]
- fn export_selected_local_recovery_key_returns_nsec() {
+ fn export_selected_local_secret_key_returns_nsec() {
let manager =
radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager::new_in_memory();
let identity = RadrootsIdentity::generate();
@@ -444,13 +490,42 @@ mod tests {
.expect("store identity");
let nsec =
- DesktopBackend::export_selected_local_recovery_key(&manager).expect("export recovery");
+ DesktopBackend::export_selected_local_secret_key(&manager).expect("export secret");
assert_eq!(nsec, identity.nsec());
assert!(nsec.starts_with("nsec1"));
}
#[test]
+ fn import_local_identity_imports_nsec_and_selects_account() {
+ let manager =
+ radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager::new_in_memory();
+ let identity = RadrootsIdentity::generate();
+
+ let state = DesktopBackend::import_local_identity(&manager, identity.nsec().as_str())
+ .expect("import identity");
+
+ assert_eq!(
+ state,
+ radroots_app_core::IdentityGateState::Ready {
+ account_id: identity.id().to_string(),
+ npub: identity.npub(),
+ }
+ );
+ assert_eq!(
+ manager.selected_account_id().expect("selected"),
+ Some(identity.id())
+ );
+ assert_eq!(manager.list_accounts().expect("list").len(), 1);
+ assert_eq!(
+ manager
+ .export_secret_hex(&identity.id())
+ .expect("export secret"),
+ Some(identity.secret_key_hex())
+ );
+ }
+
+ #[test]
fn remove_accounts_file_if_present_deletes_existing_file() {
let unique = format!(
"radroots-desktop-reset-{}-{}.json",
diff --git a/crates/ios/src/lib.rs b/crates/ios/src/lib.rs
@@ -80,10 +80,10 @@ impl IosBackend {
Self::identity_state_from_manager(manager)
}
- fn export_selected_local_recovery_key(
+ fn export_selected_local_secret_key(
manager: &RadrootsNostrAccountsManager,
) -> Result<String, String> {
- Self::authorize_recovery_key_export()?;
+ Self::authorize_secret_key_export()?;
let Some(account_id) = manager
.selected_account_id()
@@ -106,12 +106,12 @@ impl IosBackend {
}
#[cfg(target_os = "ios")]
- fn authorize_recovery_key_export() -> Result<(), String> {
- verify_user_presence("reveal the current recovery key").map_err(|source| source.to_string())
+ fn authorize_secret_key_export() -> Result<(), String> {
+ verify_user_presence("reveal the current secret key").map_err(|source| source.to_string())
}
#[cfg(not(target_os = "ios"))]
- fn authorize_recovery_key_export() -> Result<(), String> {
+ fn authorize_secret_key_export() -> Result<(), String> {
Ok(())
}
@@ -176,8 +176,8 @@ impl RadrootsAppBackend for IosBackend {
fn home_action_states(&self) -> Vec<HomeActionState> {
vec![
HomeActionState {
- kind: HomeActionKind::BackupRecoveryKey,
- label: "Back Up Recovery Key".to_owned(),
+ kind: HomeActionKind::BackupSecretKey,
+ label: "Back Up Secret Key".to_owned(),
enabled: true,
pending: false,
},
@@ -199,8 +199,8 @@ impl RadrootsAppBackend for IosBackend {
fn request_home_action(&self, action: HomeActionKind) -> Result<HomeActionResult, String> {
let manager = Self::accounts_manager()?;
match action {
- HomeActionKind::BackupRecoveryKey => Self::export_selected_local_recovery_key(&manager)
- .map(|nsec| HomeActionResult::RevealRecoveryKey { nsec }),
+ HomeActionKind::BackupSecretKey => Self::export_selected_local_secret_key(&manager)
+ .map(|nsec| HomeActionResult::RevealSecretKey { nsec }),
HomeActionKind::RemoveLocalKey => {
Self::remove_selected_local_identity(&manager).map(HomeActionResult::IdentityState)
}
@@ -326,7 +326,7 @@ mod tests {
}
#[test]
- fn export_selected_local_recovery_key_returns_nsec() {
+ fn export_selected_local_secret_key_returns_nsec() {
let manager = RadrootsNostrAccountsManager::new_in_memory();
let identity = RadrootsIdentity::generate();
@@ -334,8 +334,7 @@ mod tests {
.upsert_identity(&identity, Some("primary".into()), true)
.expect("store identity");
- let nsec =
- IosBackend::export_selected_local_recovery_key(&manager).expect("export recovery");
+ let nsec = IosBackend::export_selected_local_secret_key(&manager).expect("export secret");
assert_eq!(nsec, identity.nsec());
assert!(nsec.starts_with("nsec1"));
diff --git a/crates/web/src/lib.rs b/crates/web/src/lib.rs
@@ -173,7 +173,7 @@ impl RadrootsAppBackend for WebBackend {
HomeActionKind::DisconnectSigner => {
Ok(HomeActionResult::IdentityState(self.disconnect_signer()))
}
- HomeActionKind::BackupRecoveryKey
+ HomeActionKind::BackupSecretKey
| HomeActionKind::RemoveLocalKey
| HomeActionKind::ResetDevice => Ok(HomeActionResult::None),
}