Add OS keychain for persistent nsec sessions (roadmap #1)

- Rust: store_nsec / load_nsec / delete_nsec Tauri commands via keyring crate
  (macOS Keychain, Windows Credential Manager, Linux Secret Service)
- On nsec login: key is stored in OS keychain keyed by hex pubkey
- On startup: restoreSession() auto-loads nsec from keychain and re-establishes
  the NDK signer — no manual re-login required after restart
- On logout: keychain entry is deleted
- Graceful degradation: if keychain is unavailable (e.g. Linux without a Secret
  Service daemon), the app starts logged-out — same UX as before, no crash

Also updates ROADMAP.md with 4 new items from the Windows playtest (multi-account
switcher, NWC wizard, system tray, zap history view) and reorders the list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jure
2026-03-10 17:21:44 +01:00
parent ee26edfe65
commit 4ef824a26a
6 changed files with 123 additions and 22 deletions
+18 -1
View File
@@ -1807,6 +1807,16 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "keyring"
version = "3.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
dependencies = [
"log",
"zeroize",
]
[[package]]
name = "kuchikiki"
version = "0.8.8-speedreader"
@@ -4983,8 +4993,9 @@ dependencies = [
[[package]]
name = "wrystr"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"keyring",
"serde",
"serde_json",
"tauri",
@@ -5138,6 +5149,12 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zerotrie"
version = "0.2.3"
+1
View File
@@ -22,4 +22,5 @@ tauri = { version = "2", features = ["devtools"] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
keyring = "3"
+31 -4
View File
@@ -1,14 +1,41 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use keyring::Entry;
const KEYRING_SERVICE: &str = "wrystr";
/// Store an nsec in the OS keychain, keyed by pubkey (hex).
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
fn store_nsec(pubkey: String, nsec: String) -> Result<(), String> {
let entry = Entry::new(KEYRING_SERVICE, &pubkey).map_err(|e| e.to_string())?;
entry.set_password(&nsec).map_err(|e| e.to_string())
}
/// Load a stored nsec from the OS keychain. Returns None if no entry exists.
#[tauri::command]
fn load_nsec(pubkey: String) -> Result<Option<String>, String> {
let entry = Entry::new(KEYRING_SERVICE, &pubkey).map_err(|e| e.to_string())?;
match entry.get_password() {
Ok(nsec) => Ok(Some(nsec)),
Err(keyring::Error::NoEntry) => Ok(None),
Err(e) => Err(e.to_string()),
}
}
/// Delete a stored nsec from the OS keychain.
#[tauri::command]
fn delete_nsec(pubkey: String) -> Result<(), String> {
let entry = Entry::new(KEYRING_SERVICE, &pubkey).map_err(|e| e.to_string())?;
match entry.delete_credential() {
Ok(()) => Ok(()),
Err(keyring::Error::NoEntry) => Ok(()), // already gone — that's fine
Err(e) => Err(e.to_string()),
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.invoke_handler(tauri::generate_handler![store_nsec, load_nsec, delete_nsec])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}