app

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

commit 2b76d4d3ea299a32f575958e6eb31a7f49426561
parent c27c28e8ced192b22103dad2b62595e16ed8b7af
Author: triesap <tyson@radroots.org>
Date:   Mon,  2 Feb 2026 02:49:23 +0000

app: verify nostr key integrity

- add key verification helper and error mapping
- enforce verify during setup initialization
- cover key mismatch and invalid secret cases
- update tests and nostr dependency

Diffstat:
MCargo.lock | 1+
Mapp/Cargo.toml | 1+
Mapp/src/keystore.rs | 75++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mapp/src/lib.rs | 1+
Mapp/src/logging.rs | 1+
Mapp/src/setup.rs | 29++++++++++++++++++++++++-----
6 files changed, 102 insertions(+), 6 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1679,6 +1679,7 @@ dependencies = [ "radroots-app-core", "radroots-app-ui-components", "radroots-log", + "radroots-nostr", "serde", "serde_json", "sha2", diff --git a/app/Cargo.toml b/app/Cargo.toml @@ -21,6 +21,7 @@ gloo-timers = { workspace = true, features = ["futures"] } radroots-app-core = { path = "../crates/core" } radroots-app-ui-components = { path = "../crates/ui-components" } radroots-log = { path = "../refs/crates/log", default-features = false } +radroots-nostr = { workspace = true } tracing-wasm = "0.2" console_error_panic_hook = "0.1" serde.workspace = true diff --git a/app/src/keystore.rs b/app/src/keystore.rs @@ -1,12 +1,14 @@ #![forbid(unsafe_code)] use radroots_app_core::keystore::{RadrootsClientKeystoreError, RadrootsClientKeystoreNostr}; +use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrSecretKey}; use crate::app_log_debug_emit; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RadrootsAppKeystoreError { Keystore(RadrootsClientKeystoreError), + KeyMismatch, } pub type RadrootsAppKeystoreResult<T> = Result<T, RadrootsAppKeystoreError>; @@ -15,6 +17,7 @@ impl RadrootsAppKeystoreError { pub const fn message(&self) -> &'static str { match self { RadrootsAppKeystoreError::Keystore(err) => err.message(), + RadrootsAppKeystoreError::KeyMismatch => "error.app.keystore.key_mismatch", } } } @@ -77,12 +80,28 @@ pub async fn app_keystore_nostr_ensure_key<T: RadrootsClientKeystoreNostr>( } } +pub async fn app_keystore_nostr_verify_key<T: RadrootsClientKeystoreNostr>( + keystore: &T, + public_key: &str, +) -> RadrootsAppKeystoreResult<()> { + let secret_hex = keystore.read(public_key).await.map_err(RadrootsAppKeystoreError::from)?; + let secret_key = RadrootsNostrSecretKey::parse(&secret_hex) + .map_err(|_| RadrootsAppKeystoreError::Keystore(RadrootsClientKeystoreError::NostrInvalidSecretKey))?; + let keys = RadrootsNostrKeys::new(secret_key); + let derived = keys.public_key().to_hex(); + if derived != public_key { + return Err(RadrootsAppKeystoreError::KeyMismatch); + } + Ok(()) +} + #[cfg(test)] mod tests { use super::{ app_keystore_nostr_ensure_key, app_keystore_nostr_public_key, app_keystore_nostr_keys, + app_keystore_nostr_verify_key, RadrootsAppKeystoreError, }; use async_trait::async_trait; @@ -91,10 +110,12 @@ mod tests { RadrootsClientKeystoreNostr, RadrootsClientKeystoreResult, }; + use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrSecretKey}; struct TestKeystore { keys_result: RadrootsClientKeystoreResult<Vec<String>>, generate_result: RadrootsClientKeystoreResult<String>, + read_result: RadrootsClientKeystoreResult<String>, } #[async_trait(?Send)] @@ -108,7 +129,7 @@ mod tests { } async fn read(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> { - Err(RadrootsClientKeystoreError::IdbUndefined) + self.read_result.clone() } async fn keys(&self) -> RadrootsClientKeystoreResult<Vec<String>> { @@ -129,6 +150,7 @@ mod tests { let keystore = TestKeystore { keys_result: Err(RadrootsClientKeystoreError::NostrNoResults), generate_result: Ok("generated".to_string()), + read_result: Err(RadrootsClientKeystoreError::IdbUndefined), }; let result = futures::executor::block_on(app_keystore_nostr_public_key(&keystore)) .expect("nostr key"); @@ -140,6 +162,7 @@ mod tests { let keystore = TestKeystore { keys_result: Ok(vec!["a".to_string(), "b".to_string()]), generate_result: Ok("generated".to_string()), + read_result: Err(RadrootsClientKeystoreError::IdbUndefined), }; let result = futures::executor::block_on(app_keystore_nostr_public_key(&keystore)) .expect("nostr key"); @@ -151,6 +174,7 @@ mod tests { let keystore = TestKeystore { keys_result: Err(RadrootsClientKeystoreError::IdbUndefined), generate_result: Ok("generated".to_string()), + read_result: Err(RadrootsClientKeystoreError::IdbUndefined), }; let err = futures::executor::block_on(app_keystore_nostr_keys(&keystore)) .expect_err("nostr key"); @@ -165,6 +189,7 @@ mod tests { let keystore = TestKeystore { keys_result: Err(RadrootsClientKeystoreError::NostrNoResults), generate_result: Ok("generated".to_string()), + read_result: Err(RadrootsClientKeystoreError::IdbUndefined), }; let result = futures::executor::block_on(app_keystore_nostr_ensure_key(&keystore)) .expect("nostr key"); @@ -176,9 +201,57 @@ mod tests { let keystore = TestKeystore { keys_result: Ok(vec!["a".to_string()]), generate_result: Ok("generated".to_string()), + read_result: Err(RadrootsClientKeystoreError::IdbUndefined), }; let result = futures::executor::block_on(app_keystore_nostr_ensure_key(&keystore)) .expect("nostr key"); assert_eq!(result, "a"); } + + #[test] + fn keystore_verify_matches_public_key() { + let secret_key = RadrootsNostrSecretKey::generate(); + let secret_hex = secret_key.to_secret_hex(); + let keys = RadrootsNostrKeys::new(secret_key); + let public_key = keys.public_key().to_hex(); + let keystore = TestKeystore { + keys_result: Ok(vec![]), + generate_result: Ok("generated".to_string()), + read_result: Ok(secret_hex), + }; + let result = futures::executor::block_on(app_keystore_nostr_verify_key(&keystore, &public_key)) + .expect("nostr key"); + assert_eq!(result, ()); + } + + #[test] + fn keystore_verify_rejects_mismatch() { + let secret_key = RadrootsNostrSecretKey::generate(); + let secret_hex = secret_key.to_secret_hex(); + let other_keys = RadrootsNostrKeys::new(RadrootsNostrSecretKey::generate()); + let public_key = other_keys.public_key().to_hex(); + let keystore = TestKeystore { + keys_result: Ok(vec![]), + generate_result: Ok("generated".to_string()), + read_result: Ok(secret_hex), + }; + let err = futures::executor::block_on(app_keystore_nostr_verify_key(&keystore, &public_key)) + .expect_err("nostr key"); + assert_eq!(err, RadrootsAppKeystoreError::KeyMismatch); + } + + #[test] + fn keystore_verify_rejects_invalid_secret() { + let keystore = TestKeystore { + keys_result: Ok(vec![]), + generate_result: Ok("generated".to_string()), + read_result: Ok("not-a-key".to_string()), + }; + let err = futures::executor::block_on(app_keystore_nostr_verify_key(&keystore, "pub")) + .expect_err("nostr key"); + assert_eq!( + err, + RadrootsAppKeystoreError::Keystore(RadrootsClientKeystoreError::NostrInvalidSecretKey) + ); + } } diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -64,6 +64,7 @@ pub use keystore::{ app_keystore_nostr_ensure_key, app_keystore_nostr_keys, app_keystore_nostr_public_key, + app_keystore_nostr_verify_key, RadrootsAppKeystoreError, RadrootsAppKeystoreResult, }; diff --git a/app/src/logging.rs b/app/src/logging.rs @@ -180,6 +180,7 @@ impl RadrootsAppLoggableError for RadrootsAppKeystoreError { fn log_context(&self) -> Option<String> { match self { RadrootsAppKeystoreError::Keystore(err) => Some(err.to_string()), + RadrootsAppKeystoreError::KeyMismatch => Some(self.message().to_string()), } } } diff --git a/app/src/setup.rs b/app/src/setup.rs @@ -1,7 +1,7 @@ #![forbid(unsafe_code)] use radroots_app_core::datastore::RadrootsClientDatastore; -use radroots_app_core::keystore::RadrootsClientKeystoreNostr; +use radroots_app_core::keystore::{RadrootsClientKeystoreError, RadrootsClientKeystoreNostr}; #[cfg(target_arch = "wasm32")] use js_sys::Date; @@ -13,6 +13,7 @@ use crate::{ app_datastore_create_state, app_datastore_key_nostr_key, app_keystore_nostr_ensure_key, + app_keystore_nostr_verify_key, app_log_debug_emit, RadrootsAppInitError, RadrootsAppInitResult, @@ -95,6 +96,17 @@ pub async fn app_setup_initialize<T: RadrootsClientDatastore, K: RadrootsClientK .await .map_err(|err| match err { RadrootsAppKeystoreError::Keystore(inner) => RadrootsAppInitError::Keystore(inner), + RadrootsAppKeystoreError::KeyMismatch => { + RadrootsAppInitError::Keystore(RadrootsClientKeystoreError::NostrInvalidSecretKey) + } + })?; + app_keystore_nostr_verify_key(keystore, &active_key) + .await + .map_err(|err| match err { + RadrootsAppKeystoreError::Keystore(inner) => RadrootsAppInitError::Keystore(inner), + RadrootsAppKeystoreError::KeyMismatch => { + RadrootsAppInitError::Keystore(RadrootsClientKeystoreError::NostrInvalidSecretKey) + } })?; let state = app_setup_state_new(active_key.clone(), app_setup_eula_date()); let stored_state = app_datastore_create_state(datastore, key_maps, &state).await?; @@ -131,6 +143,7 @@ mod tests { RadrootsClientKeystoreNostr, RadrootsClientKeystoreResult, }; + use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrSecretKey}; use serde::de::DeserializeOwned; use serde::Serialize; use std::cell::RefCell; @@ -139,6 +152,7 @@ mod tests { struct TestKeystore { keys_result: RadrootsClientKeystoreResult<Vec<String>>, generate_result: RadrootsClientKeystoreResult<String>, + read_result: RadrootsClientKeystoreResult<String>, } #[async_trait(?Send)] @@ -152,7 +166,7 @@ mod tests { } async fn read(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> { - Err(RadrootsClientKeystoreError::IdbUndefined) + self.read_result.clone() } async fn keys(&self) -> RadrootsClientKeystoreResult<Vec<String>> { @@ -393,13 +407,18 @@ mod tests { #[test] fn setup_initialize_creates_state_and_key() { + let secret_key = RadrootsNostrSecretKey::generate(); + let secret_hex = secret_key.to_secret_hex(); + let keys = RadrootsNostrKeys::new(secret_key); + let public_key = keys.public_key().to_hex(); let datastore = TestDatastore { record: RefCell::new(None), values: RefCell::new(BTreeMap::new()), }; let keystore = TestKeystore { keys_result: Err(RadrootsClientKeystoreError::NostrNoResults), - generate_result: Ok("pub".to_string()), + generate_result: Ok(public_key.clone()), + read_result: Ok(secret_hex), }; let key_maps = app_key_maps_default(); let state = futures::executor::block_on(app_setup_initialize( @@ -408,10 +427,10 @@ mod tests { &key_maps, )) .expect("setup"); - assert_eq!(state.active_key, "pub"); + assert_eq!(state.active_key, public_key); let key_name = app_datastore_key_nostr_key(&key_maps).expect("key name"); let stored = futures::executor::block_on(datastore.get(key_name)).expect("stored"); - assert_eq!(stored, "pub"); + assert_eq!(stored, public_key); assert!(datastore.record.borrow().is_some()); } }