From ddf27118443e948ff3386604f0c4b1b42fe48470 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Thu, 11 Jun 2026 16:25:32 +0200 Subject: [PATCH 1/3] Harden nsec storage with SecretString and one-time key init. Wrap the Nostr private key in secrecy/zeroize, parse Keys once at startup, wipe nsec from global Settings, and stop re-parsing on every get_keys call. Co-authored-by: Cursor --- Cargo.toml | 2 ++ src/app/bond/flow.rs | 8 ++--- src/app/context.rs | 8 +++-- src/config/mod.rs | 8 +++-- src/config/secret.rs | 81 ++++++++++++++++++++++++++++++++++++++++++ src/config/settings.rs | 17 +++++++-- src/config/types.rs | 4 +-- src/config/util.rs | 47 ++++++++++++++---------- src/config/wizard.rs | 30 ++++++++++------ src/main.rs | 2 +- src/util.rs | 20 +++++------ 11 files changed, 171 insertions(+), 56 deletions(-) create mode 100644 src/config/secret.rs diff --git a/Cargo.toml b/Cargo.toml index 57416606..f6bdd106 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,6 +86,8 @@ tonic = "0.14.2" prost = "0.14.1" tonic-prost = "0.14.1" cdk = { version = "0.17.2", default-features = false, features = ["wallet"] } +secrecy = { version = "0.10", features = ["serde"] } +zeroize = "1.8" [dev-dependencies] tokio = { version = "1.47.1", features = ["full", "test-util", "macros"] } diff --git a/src/app/bond/flow.rs b/src/app/bond/flow.rs index f2a29d1c..d234ace8 100644 --- a/src/app/bond/flow.rs +++ b/src/app/bond/flow.rs @@ -285,7 +285,7 @@ pub async fn request_taker_bond( }; if claimed { let my_keys = get_keys()?; - match crate::util::update_order_event(&my_keys, Status::WaitingTakerBond, order).await { + match crate::util::update_order_event(my_keys, Status::WaitingTakerBond, order).await { Ok(updated) => { if let Err(e) = sqlx::query("UPDATE orders SET event_id = ? WHERE id = ?") .bind(&updated.event_id) @@ -931,7 +931,7 @@ async fn on_bond_invoice_accepted( let order = promote_taker_context_to_order(pool, order, ¤t).await?; let my_keys = get_keys()?; - resume_take_after_bond(pool, order, &my_keys, request_id).await + resume_take_after_bond(pool, order, my_keys, request_id).await } /// Subscriber callback path for a **maker** bond reaching `Accepted`. @@ -1015,7 +1015,7 @@ async fn on_maker_bond_accepted( } let my_keys = get_keys()?; - crate::util::resume_publish_after_maker_bond(pool, &my_keys, order, request_id).await + crate::util::resume_publish_after_maker_bond(pool, my_keys, order, request_id).await } /// Message the taker of a losing concurrent bond that their take was @@ -1227,7 +1227,7 @@ pub(crate) async fn maybe_drop_waiting_taker_bond( None => return Ok(()), }; let my_keys = get_keys()?; - match crate::util::update_order_event(&my_keys, Status::Pending, &fresh).await { + match crate::util::update_order_event(my_keys, Status::Pending, &fresh).await { Ok(updated) => { if let Err(e) = sqlx::query("UPDATE orders SET event_id = ? WHERE id = ?") .bind(&updated.event_id) diff --git a/src/app/context.rs b/src/app/context.rs index afd552a9..b3b97704 100644 --- a/src/app/context.rs +++ b/src/app/context.rs @@ -99,6 +99,7 @@ pub mod test_utils { DatabaseSettings, ExpirationSettings, LightningSettings, MostroSettings, NostrSettings, RpcSettings, }; + use secrecy::{ExposeSecret, SecretString}; /// Test helper wrapper for inspecting the shared order-message queue. #[derive(Debug, Clone)] @@ -230,7 +231,7 @@ pub mod test_utils { // Use provided keys or parse from settings let keys = self.keys.unwrap_or_else(|| { - Keys::parse(&settings.nostr.nsec_privkey) + Keys::parse(settings.nostr.nsec_privkey.expose_secret()) .expect("TestContextBuilder: invalid nsec_privkey in settings") }); @@ -259,8 +260,9 @@ pub mod test_utils { }, nostr: NostrSettings { // Valid test nsec from src/config/mod.rs tests - nsec_privkey: "nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd" - .to_string(), + nsec_privkey: SecretString::from( + "nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd", + ), relays: vec!["wss://relay.test".to_string()], }, mostro: MostroSettings::default(), diff --git a/src/config/mod.rs b/src/config/mod.rs index 6d66c92d..899c5594 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,5 +1,6 @@ // Mostro module for configurataion settings pub mod constants; +pub mod secret; pub mod settings; /// This module provides functionality to manage and initialize settings for the Mostro application. /// It includes structures for database, lightning, Nostr, and Mostro settings, as well as functions to initialize and access these settings. @@ -19,7 +20,8 @@ use tokio::sync::RwLock; pub use constants::{DEV_FEE_LIGHTNING_ADDRESS, MAX_DEV_FEE_PERCENTAGE, MIN_DEV_FEE_PERCENTAGE}; use mostro_core::prelude::*; use nostr_sdk::prelude::*; -pub use settings::{get_db_pool, init_mostro_settings, Settings}; +pub use secret::{parse_mostro_keys, read_nsec_env_var, take_nsec_for_init}; +pub use settings::{get_db_pool, get_mostro_keys, init_mostro_settings, Settings}; pub use types::{ AntiAbuseBondSettings, BondApplyTo, DatabaseSettings, ExpirationSettings, LightningSettings, MostroSettings, NostrSettings, @@ -29,6 +31,7 @@ pub use types::{ // almost all of them are initialized with OnceLock to ensure they are set only once // They are shared across the application using Arc and Mutex/RwLock for thread safety pub static MOSTRO_CONFIG: OnceLock = OnceLock::new(); +pub static NOSTR_KEYS: OnceLock = OnceLock::new(); pub static NOSTR_CLIENT: OnceLock = OnceLock::new(); pub static LN_STATUS: OnceLock = OnceLock::new(); pub static DB_POOL: OnceLock> = OnceLock::new(); @@ -56,6 +59,7 @@ mod tests { use super::*; use crate::config::constants::DEV_FEE_AUDIT_EVENT_KIND; use mostro_core::prelude::{NOSTR_DISPUTE_EVENT_KIND, NOSTR_ORDER_EVENT_KIND}; + use secrecy::ExposeSecret; use serde::Deserialize; // Fake settings for the test @@ -195,7 +199,7 @@ mod tests { let nostr_settings: StubSettingsNostr = toml::from_str(NOSTR_SETTINGS).expect("Failed to deserialize"); assert_eq!( - nostr_settings.nostr.nsec_privkey, + nostr_settings.nostr.nsec_privkey.expose_secret(), "nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd" ); assert_eq!( diff --git a/src/config/secret.rs b/src/config/secret.rs new file mode 100644 index 00000000..996fb909 --- /dev/null +++ b/src/config/secret.rs @@ -0,0 +1,81 @@ +//! Helpers for loading and parsing the Mostro Nostr private key with +//! zeroization of transient buffers. + +use crate::config::constants::NSEC_ENV_VAR; +use crate::config::types::NostrSettings; +use mostro_core::error::MostroError::{self, *}; +use mostro_core::error::ServiceError; +use nostr_sdk::Keys; +use secrecy::{ExposeSecret, SecretString}; +use serde::Serializer; + +/// Serialize a [`SecretString`] for config files (wizard / TOML export only). +pub fn serialize_nsec(secret: &SecretString, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(secret.expose_secret()) +} + +/// Read `MOSTRO_NSEC_PRIVKEY` from the process environment, trim whitespace, +/// and wrap in a [`SecretString`]. Returns `None` when unset or blank. +pub fn read_nsec_env_var() -> Option { + let nsec_from_env = std::env::var(NSEC_ENV_VAR).ok()?; + let trimmed = nsec_from_env.trim(); + if trimmed.is_empty() { + return None; + } + Some(SecretString::from(trimmed.to_owned())) +} + +/// Parse a bech32 nsec into [`Keys`], exposing the secret only in this scope. +pub fn parse_mostro_keys(secret: &SecretString) -> Result { + let nsec = secret.expose_secret(); + if nsec.is_empty() { + return Err(MostroInternalErr(ServiceError::NostrError( + "Nostr private key is not configured".to_string(), + ))); + } + Keys::parse(nsec).map_err(|e| { + tracing::error!("Failed to parse nostr private key: {}", e); + MostroInternalErr(ServiceError::NostrError(e.to_string())) + }) +} + +/// Take the nsec from nostr settings (env override must already be applied), +/// parse it into [`Keys`], and clear the field so global settings no longer +/// retain plaintext. +pub fn take_nsec_for_init(nostr: &mut NostrSettings) -> Result { + let secret = std::mem::take(&mut nostr.nsec_privkey); + let keys = parse_mostro_keys(&secret)?; + nostr.nsec_privkey = SecretString::default(); + Ok(keys) +} + +#[cfg(test)] +mod tests { + use super::*; + use secrecy::ExposeSecret; + + #[test] + fn take_nsec_clears_settings_field() { + let mut nostr = NostrSettings { + nsec_privkey: SecretString::from( + "nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd", + ), + relays: vec![], + }; + let keys = take_nsec_for_init(&mut nostr).expect("valid test nsec"); + assert!(nostr.nsec_privkey.expose_secret().is_empty()); + assert!(!keys.public_key().to_hex().is_empty()); + } + + #[test] + fn parse_mostro_keys_rejects_empty() { + let err = parse_mostro_keys(&SecretString::default()).unwrap_err(); + assert!(matches!( + err, + MostroInternalErr(ServiceError::NostrError(_)) + )); + } +} diff --git a/src/config/settings.rs b/src/config/settings.rs index a16943a3..2fc691da 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -1,10 +1,12 @@ -use super::{DB_POOL, MOSTRO_CONFIG}; +use super::{DB_POOL, MOSTRO_CONFIG, NOSTR_KEYS}; +use crate::config::secret::take_nsec_for_init; use crate::config::types::{ AntiAbuseBondSettings, CashuSettings, DatabaseSettings, EscrowMode, ExpirationSettings, LightningSettings, MostroSettings, NostrSettings, RpcSettings, }; use crate::price::PriceSettings; use mostro_core::transport::Transport; +use nostr_sdk::Keys; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -38,13 +40,22 @@ pub struct Settings { pub price: Option, } -/// Initialize the global MOSTRO_CONFIG struct -pub fn init_mostro_settings(s: Settings) { +/// Initialize the global `MOSTRO_CONFIG` and `NOSTR_KEYS` structs. +pub fn init_mostro_settings(mut s: Settings) { + let keys = take_nsec_for_init(&mut s.nostr).expect("Failed to parse nostr private key"); + NOSTR_KEYS + .set(keys) + .expect("Failed to set Mostro nostr keys"); MOSTRO_CONFIG .set(s) .expect("Failed to set Mostro global settings"); } +/// Parsed Mostro Nostr signing keys, initialized once at startup. +pub fn get_mostro_keys() -> Option<&'static Keys> { + NOSTR_KEYS.get() +} + /// Get database pool for Mostro db operations to share across the thread pub fn get_db_pool() -> Arc { DB_POOL.get().expect("No database pool found").clone() diff --git a/src/config/types.rs b/src/config/types.rs index 91b6cac3..947beaf4 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -408,8 +408,8 @@ pub struct LightningSettings { pub struct NostrSettings { /// Nostr private key. Optional when `MOSTRO_NSEC_PRIVKEY` is provided via /// environment variable or `/.env`. - #[serde(default)] - pub nsec_privkey: String, + #[serde(default, serialize_with = "crate::config::secret::serialize_nsec")] + pub nsec_privkey: secrecy::SecretString, /// Nostr relays list pub relays: Vec, } diff --git a/src/config/util.rs b/src/config/util.rs index e446eaf4..ff3346d7 100644 --- a/src/config/util.rs +++ b/src/config/util.rs @@ -2,9 +2,8 @@ /// This module provides utility functions for the config module. /// It includes functions to initialize the default settings directory and create a settings file from the template if it doesn't exist. /// It also includes functions to add a trailing slash to a path if it doesn't already have one. -use crate::config::constants::{ - ENV_FILENAME, MAX_DEV_FEE_PERCENTAGE, MIN_DEV_FEE_PERCENTAGE, NSEC_ENV_VAR, -}; +use crate::config::constants::{ENV_FILENAME, MAX_DEV_FEE_PERCENTAGE, MIN_DEV_FEE_PERCENTAGE}; +use crate::config::secret::read_nsec_env_var; use crate::config::wizard; use crate::config::{init_mostro_settings, Settings}; use mostro_core::error::MostroError::{self, *}; @@ -12,6 +11,7 @@ use mostro_core::error::ServiceError; use std::fs; use std::io::IsTerminal; use std::path::PathBuf; +use zeroize::Zeroizing; const DB_FILENAME: &str = "mostro.db"; @@ -41,11 +41,8 @@ fn load_env_file(settings_dir: &std::path::Path) { /// value, override the nsec loaded from `settings.toml`. Whitespace is /// trimmed; blank values are ignored so the TOML stays the fallback. fn apply_nsec_env_override(settings: &mut Settings) { - if let Ok(nsec_from_env) = std::env::var(NSEC_ENV_VAR) { - let trimmed = nsec_from_env.trim(); - if !trimmed.is_empty() { - settings.nostr.nsec_privkey = trimmed.to_string(); - } + if let Some(nsec) = read_nsec_env_var() { + settings.nostr.nsec_privkey = nsec; } } @@ -182,9 +179,12 @@ pub fn init_configuration_file(config_path: Option) -> Result<(), Mostro return Ok(()); } - // Read the file content - let contents = fs::read_to_string(&config_file_path) - .map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?; + // Read the file content into a zeroizing buffer so TOML plaintext is wiped + // after parsing. + let contents = Zeroizing::new( + fs::read_to_string(&config_file_path) + .map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?, + ); // Parse TOML content let mut settings: Settings = toml::from_str(&contents) @@ -211,9 +211,11 @@ pub fn init_configuration_file(config_path: Option) -> Result<(), Mostro #[cfg(test)] mod tests { use super::*; + use crate::config::constants::NSEC_ENV_VAR; use crate::config::types::{ DatabaseSettings, LightningSettings, MostroSettings, NostrSettings, RpcSettings, }; + use secrecy::{ExposeSecret, SecretString}; use std::sync::Mutex; // Tests that read/write MOSTRO_NSEC_PRIVKEY must run serially because the @@ -253,7 +255,7 @@ mod tests { database: DatabaseSettings::default(), lightning: LightningSettings::default(), nostr: NostrSettings { - nsec_privkey: nsec.to_string(), + nsec_privkey: SecretString::from(nsec.to_owned()), relays: vec!["wss://relay.test".to_string()], }, mostro: MostroSettings::default(), @@ -274,7 +276,7 @@ mod tests { let mut settings = make_settings("nsec_from_toml"); apply_nsec_env_override(&mut settings); - assert_eq!(settings.nostr.nsec_privkey, "nsec_from_env"); + assert_eq!(settings.nostr.nsec_privkey.expose_secret(), "nsec_from_env"); } #[test] @@ -286,7 +288,10 @@ mod tests { let mut settings = make_settings("nsec_from_toml"); apply_nsec_env_override(&mut settings); - assert_eq!(settings.nostr.nsec_privkey, "nsec_from_toml"); + assert_eq!( + settings.nostr.nsec_privkey.expose_secret(), + "nsec_from_toml" + ); } #[test] @@ -297,7 +302,10 @@ mod tests { let mut settings = make_settings("nsec_from_toml"); apply_nsec_env_override(&mut settings); - assert_eq!(settings.nostr.nsec_privkey, "nsec_from_toml"); + assert_eq!( + settings.nostr.nsec_privkey.expose_secret(), + "nsec_from_toml" + ); } #[test] @@ -309,7 +317,10 @@ mod tests { let mut settings = make_settings("nsec_from_toml"); apply_nsec_env_override(&mut settings); - assert_eq!(settings.nostr.nsec_privkey, "nsec_from_toml"); + assert_eq!( + settings.nostr.nsec_privkey.expose_secret(), + "nsec_from_toml" + ); } #[test] @@ -321,7 +332,7 @@ mod tests { let mut settings = make_settings("nsec_from_toml"); apply_nsec_env_override(&mut settings); - assert_eq!(settings.nostr.nsec_privkey, "nsec_from_env"); + assert_eq!(settings.nostr.nsec_privkey.expose_secret(), "nsec_from_env"); } #[test] @@ -331,7 +342,7 @@ mod tests { let toml_without_nsec = r#"relays = ["wss://relay.test"]"#; let nostr: NostrSettings = toml::from_str(toml_without_nsec).expect("nsec_privkey should be optional in TOML"); - assert_eq!(nostr.nsec_privkey, ""); + assert!(nostr.nsec_privkey.expose_secret().is_empty()); assert_eq!(nostr.relays, vec!["wss://relay.test"]); } } diff --git a/src/config/wizard.rs b/src/config/wizard.rs index f13fbad9..d3259652 100644 --- a/src/config/wizard.rs +++ b/src/config/wizard.rs @@ -5,6 +5,8 @@ use dialoguer::{Confirm, Input, Select}; use mostro_core::error::MostroError::{self, MostroInternalErr}; use mostro_core::error::ServiceError; use nostr_sdk::prelude::*; +use secrecy::{ExposeSecret, SecretString}; +use zeroize::Zeroizing; use super::constants::{ENV_FILENAME, NSEC_ENV_VAR}; use super::settings::Settings; @@ -154,11 +156,14 @@ fn prompt_nostr_settings(settings_dir: &Path) -> Result Result Result Result { +fn prompt_nsec_storage( + settings_dir: &Path, + nsec: &SecretString, +) -> Result { println!("\nMostro supports two storage locations for your nsec. Both are fully supported;"); println!("pick the one that fits your threat model and deployment setup. You can also"); println!("provide MOSTRO_NSEC_PRIVKEY via the real process environment (systemd, Docker,"); @@ -221,21 +229,21 @@ fn prompt_nsec_storage(settings_dir: &Path, nsec: &str) -> Result Result<()> { } if has_metadata { - if let Ok(metadata_ev) = EventBuilder::metadata(&metadata).sign_with_keys(&mostro_keys) { + if let Ok(metadata_ev) = EventBuilder::metadata(&metadata).sign_with_keys(mostro_keys) { let _ = client.send_event(&metadata_ev).await; tracing::info!("Published NIP-01 kind 0 metadata event"); } diff --git a/src/util.rs b/src/util.rs index f98d0481..0f639537 100644 --- a/src/util.rs +++ b/src/util.rs @@ -789,7 +789,7 @@ pub async fn publish_dev_fee_audit_event( // Create and sign event let event = EventBuilder::new(nostr_sdk::Kind::Custom(DEV_FEE_AUDIT_EVENT_KIND), "") .tags(tags) - .sign_with_keys(&keys) + .sign_with_keys(keys) .map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?; // Publish event to relays @@ -806,16 +806,12 @@ pub async fn publish_dev_fee_audit_event( Ok(()) } -pub fn get_keys() -> Result { - let nostr_settings = Settings::get_nostr(); - // nostr private key - match Keys::parse(&nostr_settings.nsec_privkey) { - Ok(my_keys) => Ok(my_keys), - Err(e) => { - tracing::error!("Failed to parse nostr private key: {}", e); - Err(MostroInternalErr(ServiceError::NostrError(e.to_string()))) - } - } +pub fn get_keys() -> Result<&'static Keys, MostroError> { + crate::config::get_mostro_keys().ok_or_else(|| { + MostroInternalErr(ServiceError::NostrError( + "Nostr keys not initialized".to_string(), + )) + }) } #[allow(clippy::too_many_arguments)] @@ -1079,7 +1075,7 @@ pub async fn invoice_subscribe(hash: Vec, request_id: Option) -> Result continue; } }; - if let Err(e) = flow::hold_invoice_paid(&hash, request_id, &pool, &keys).await { + if let Err(e) = flow::hold_invoice_paid(&hash, request_id, &pool, keys).await { info!("Invoice flow error {e}"); } else { info!("Invoice with hash {hash} accepted!"); From e58511e030b3f40819ad72bfb8c017aef8560492 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Thu, 11 Jun 2026 16:53:19 +0200 Subject: [PATCH 2/3] chore: rabbit nipticks --- Cargo.lock | 12 ++++++++++++ src/app/context.rs | 20 ++++++++++++++------ src/config/secret.rs | 8 ++++++-- src/config/settings.rs | 23 +++++++++++++++-------- src/config/util.rs | 4 ++-- src/config/wizard.rs | 10 +++++----- src/util.rs | 11 +++++++++++ 7 files changed, 65 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d5ccacd..a2301919 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2324,6 +2324,7 @@ dependencies = [ "once_cell", "prost 0.14.4", "reqwest", + "secrecy", "serde", "serde_json", "sqlx", @@ -2336,6 +2337,7 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", + "zeroize", ] [[package]] @@ -3355,6 +3357,16 @@ dependencies = [ "cc", ] +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "serde", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" diff --git a/src/app/context.rs b/src/app/context.rs index b3b97704..33c499cf 100644 --- a/src/app/context.rs +++ b/src/app/context.rs @@ -95,11 +95,12 @@ impl AppContext { #[cfg(test)] pub mod test_utils { use super::*; + use crate::config::secret::take_nsec_for_init; use crate::config::types::{ DatabaseSettings, ExpirationSettings, LightningSettings, MostroSettings, NostrSettings, RpcSettings, }; - use secrecy::{ExposeSecret, SecretString}; + use secrecy::SecretString; /// Test helper wrapper for inspecting the shared order-message queue. #[derive(Debug, Clone)] @@ -229,11 +230,18 @@ pub mod test_utils { .order_msg_queue .unwrap_or_else(|| Arc::new(RwLock::new(Vec::new()))); - // Use provided keys or parse from settings - let keys = self.keys.unwrap_or_else(|| { - Keys::parse(settings.nostr.nsec_privkey.expose_secret()) - .expect("TestContextBuilder: invalid nsec_privkey in settings") - }); + let mut settings = Arc::try_unwrap(settings).unwrap_or_else(|arc| (*arc).clone()); + + let keys = match self.keys { + Some(keys) => { + settings.nostr.nsec_privkey = SecretString::default(); + keys + } + None => take_nsec_for_init(&mut settings.nostr) + .expect("TestContextBuilder: invalid nsec_privkey in settings"), + }; + + let settings = Arc::new(settings); AppContext::new(pool, nostr_client, settings, order_msg_queue, keys) } diff --git a/src/config/secret.rs b/src/config/secret.rs index 996fb909..e1bbc47b 100644 --- a/src/config/secret.rs +++ b/src/config/secret.rs @@ -8,6 +8,7 @@ use mostro_core::error::ServiceError; use nostr_sdk::Keys; use secrecy::{ExposeSecret, SecretString}; use serde::Serializer; +use zeroize::Zeroize; /// Serialize a [`SecretString`] for config files (wizard / TOML export only). pub fn serialize_nsec(secret: &SecretString, serializer: S) -> Result @@ -20,12 +21,15 @@ where /// Read `MOSTRO_NSEC_PRIVKEY` from the process environment, trim whitespace, /// and wrap in a [`SecretString`]. Returns `None` when unset or blank. pub fn read_nsec_env_var() -> Option { - let nsec_from_env = std::env::var(NSEC_ENV_VAR).ok()?; + let mut nsec_from_env = std::env::var(NSEC_ENV_VAR).ok()?; let trimmed = nsec_from_env.trim(); if trimmed.is_empty() { + nsec_from_env.zeroize(); return None; } - Some(SecretString::from(trimmed.to_owned())) + let secret = SecretString::from(trimmed.to_owned()); + nsec_from_env.zeroize(); + Some(secret) } /// Parse a bech32 nsec into [`Keys`], exposing the secret only in this scope. diff --git a/src/config/settings.rs b/src/config/settings.rs index 2fc691da..a2df5895 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -5,6 +5,8 @@ use crate::config::types::{ LightningSettings, MostroSettings, NostrSettings, RpcSettings, }; use crate::price::PriceSettings; +use mostro_core::error::MostroError::{self, *}; +use mostro_core::error::ServiceError; use mostro_core::transport::Transport; use nostr_sdk::Keys; use serde::{Deserialize, Serialize}; @@ -41,14 +43,19 @@ pub struct Settings { } /// Initialize the global `MOSTRO_CONFIG` and `NOSTR_KEYS` structs. -pub fn init_mostro_settings(mut s: Settings) { - let keys = take_nsec_for_init(&mut s.nostr).expect("Failed to parse nostr private key"); - NOSTR_KEYS - .set(keys) - .expect("Failed to set Mostro nostr keys"); - MOSTRO_CONFIG - .set(s) - .expect("Failed to set Mostro global settings"); +pub fn init_mostro_settings(mut s: Settings) -> Result<(), MostroError> { + let keys = take_nsec_for_init(&mut s.nostr)?; + NOSTR_KEYS.set(keys).map_err(|_| { + MostroInternalErr(ServiceError::IOError( + "Mostro nostr keys already initialized".to_string(), + )) + })?; + MOSTRO_CONFIG.set(s).map_err(|_| { + MostroInternalErr(ServiceError::IOError( + "Mostro settings already initialized".to_string(), + )) + })?; + Ok(()) } /// Parsed Mostro Nostr signing keys, initialized once at startup. diff --git a/src/config/util.rs b/src/config/util.rs index ff3346d7..82cb2d6f 100644 --- a/src/config/util.rs +++ b/src/config/util.rs @@ -174,7 +174,7 @@ pub fn init_configuration_file(config_path: Option) -> Result<(), Mostro apply_nsec_env_override(&mut settings); validate_mostro_settings(&settings)?; - init_mostro_settings(settings); + init_mostro_settings(settings)?; tracing::info!("Settings correctly loaded!"); return Ok(()); } @@ -201,7 +201,7 @@ pub fn init_configuration_file(config_path: Option) -> Result<(), Mostro settings.database.url = format!("sqlite://{}", settings_dir.join(DB_FILENAME).display()); // Initialize the global settings variable - init_mostro_settings(settings); + init_mostro_settings(settings)?; tracing::info!("Settings correctly loaded!"); diff --git a/src/config/wizard.rs b/src/config/wizard.rs index d3259652..562618a7 100644 --- a/src/config/wizard.rs +++ b/src/config/wizard.rs @@ -1,7 +1,7 @@ use std::io::Write; use std::path::{Path, PathBuf}; -use dialoguer::{Confirm, Input, Select}; +use dialoguer::{Confirm, Input, Password, Select}; use mostro_core::error::MostroError::{self, MostroInternalErr}; use mostro_core::error::ServiceError; use nostr_sdk::prelude::*; @@ -157,10 +157,10 @@ fn prompt_nostr_settings(settings_dir: &Path) -> Result Result Result<&'static Keys, MostroError> { crate::config::get_mostro_keys().ok_or_else(|| { MostroInternalErr(ServiceError::NostrError( From dc2dfc3b96858e8061c610903b15f60c4c5fb487 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Fri, 12 Jun 2026 10:53:55 +0200 Subject: [PATCH 3/3] fix: cargo clippy fix --- src/price/manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/price/manager.rs b/src/price/manager.rs index 7802259c..f2a478c6 100644 --- a/src/price/manager.rs +++ b/src/price/manager.rs @@ -528,7 +528,7 @@ impl PriceManager { Tag::expiration(Timestamp::from(expiration as u64)), ]); - let event = match crate::nip33::new_exchange_rates_event(&keys, &content, tags) { + let event = match crate::nip33::new_exchange_rates_event(keys, &content, tags) { Ok(e) => e, Err(e) => { error!("price: failed to build exchange-rates event: {e}");