commit 6c6e736eb1f6cdd518e905889a2cd205aa6f4e40
parent 1d7b35ffa2d4423f87b1861e609ff168b9d1dcc0
Author: triesap <tyson@radroots.org>
Date: Sat, 21 Mar 2026 19:13:31 +0000
core: zeroize recovery-key ui state
- add zeroize to the app core crate for secret-bearing ui state
- store recovery input in zeroizing string state instead of plain string state
- store revealed recovery keys in zeroizing state while they remain visible in the ui
- keep the shared recovery tests green under the hardened state types
Diffstat:
3 files changed, 23 insertions(+), 10 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -2801,6 +2801,7 @@ version = "0.1.0"
dependencies = [
"eframe",
"egui",
+ "zeroize",
]
[[package]]
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
@@ -16,3 +16,4 @@ workspace = true
[dependencies]
eframe.workspace = true
egui.workspace = true
+zeroize.workspace = true
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
@@ -1,6 +1,7 @@
#![forbid(unsafe_code)]
use eframe::egui;
+use zeroize::Zeroizing;
pub const APP_NAME: &str = "Rad Roots";
@@ -87,8 +88,8 @@ pub struct RadrootsApp {
status_message: Option<String>,
pending_home_confirmation: Option<HomeActionKind>,
pending_recovery_entry: bool,
- recovery_key_input: String,
- revealed_recovery_key: Option<String>,
+ recovery_key_input: Zeroizing<String>,
+ revealed_recovery_key: Option<Zeroizing<String>>,
}
impl RadrootsApp {
@@ -99,7 +100,7 @@ impl RadrootsApp {
status_message: None,
pending_home_confirmation: None,
pending_recovery_entry: false,
- recovery_key_input: String::new(),
+ recovery_key_input: Zeroizing::new(String::new()),
revealed_recovery_key: None,
};
match app.backend.load_identity_state() {
@@ -183,7 +184,7 @@ impl RadrootsApp {
match result {
HomeActionResult::IdentityState(state) => self.apply_identity_state(state),
HomeActionResult::RevealRecoveryKey { nsec } => {
- self.revealed_recovery_key = Some(nsec);
+ self.revealed_recovery_key = Some(Zeroizing::new(nsec));
self.pending_home_confirmation = None;
}
HomeActionResult::None => {}
@@ -273,7 +274,7 @@ impl eframe::App for RadrootsApp {
);
ui.add_space(8.0);
ui.add(
- egui::TextEdit::singleline(&mut self.recovery_key_input)
+ egui::TextEdit::singleline(&mut *self.recovery_key_input)
.hint_text("nsec1...")
.desired_width(ui.available_width()),
);
@@ -362,7 +363,7 @@ impl eframe::App for RadrootsApp {
ui.add_space(20.0);
ui.label("Recovery key");
ui.add_space(8.0);
- ui.monospace(nsec);
+ ui.monospace(nsec.as_str());
ui.add_space(8.0);
if ui.button("Dismiss Recovery Key").clicked() {
self.revealed_recovery_key = None;
@@ -805,7 +806,7 @@ mod tests {
));
app.pending_recovery_entry = true;
- app.recovery_key_input = "nsec1example".into();
+ app.recovery_key_input = Zeroizing::new("nsec1example".into());
app.request_recovery_action();
assert_eq!(
@@ -816,7 +817,7 @@ mod tests {
}
);
assert_eq!(app.pending_recovery_entry, false);
- assert_eq!(app.recovery_key_input, "");
+ assert_eq!(app.recovery_key_input.as_str(), "");
}
#[test]
@@ -852,7 +853,12 @@ mod tests {
assert!(matches!(app.screen, AppScreen::Home { .. }));
assert_eq!(app.pending_home_confirmation, None);
- assert_eq!(app.revealed_recovery_key.as_deref(), Some("nsec1example"));
+ assert_eq!(
+ app.revealed_recovery_key
+ .as_ref()
+ .map(|value| value.as_str()),
+ Some("nsec1example")
+ );
}
#[test]
@@ -892,6 +898,11 @@ mod tests {
app.sync_backend();
- assert_eq!(app.revealed_recovery_key.as_deref(), Some("nsec1example"));
+ assert_eq!(
+ app.revealed_recovery_key
+ .as_ref()
+ .map(|value| value.as_str()),
+ Some("nsec1example")
+ );
}
}