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:
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());
}
}