Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
8 changes: 4 additions & 4 deletions src/app/bond/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -931,7 +931,7 @@ async fn on_bond_invoice_accepted(
let order = promote_taker_context_to_order(pool, order, &current).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`.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 17 additions & 7 deletions src/app/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +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::SecretString;

/// Test helper wrapper for inspecting the shared order-message queue.
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -228,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)
.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)
}
Expand All @@ -259,8 +268,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(),
Expand Down
8 changes: 6 additions & 2 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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,
Expand All @@ -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<Settings> = OnceLock::new();
pub static NOSTR_KEYS: OnceLock<Keys> = OnceLock::new();
pub static NOSTR_CLIENT: OnceLock<Client> = OnceLock::new();
pub static LN_STATUS: OnceLock<LnStatus> = OnceLock::new();
pub static DB_POOL: OnceLock<Arc<sqlx::SqlitePool>> = OnceLock::new();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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!(
Expand Down
85 changes: 85 additions & 0 deletions src/config/secret.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! 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;
use zeroize::Zeroize;

/// Serialize a [`SecretString`] for config files (wizard / TOML export only).
pub fn serialize_nsec<S>(secret: &SecretString, serializer: S) -> Result<S::Ok, S::Error>
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<SecretString> {
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;
}
let secret = SecretString::from(trimmed.to_owned());
nsec_from_env.zeroize();
Some(secret)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// Parse a bech32 nsec into [`Keys`], exposing the secret only in this scope.
pub fn parse_mostro_keys(secret: &SecretString) -> Result<Keys, MostroError> {
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<Keys, MostroError> {
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(_))
));
}
}
30 changes: 24 additions & 6 deletions src/config/settings.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
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::error::MostroError::{self, *};
use mostro_core::error::ServiceError;
use mostro_core::transport::Transport;
use nostr_sdk::Keys;
use serde::{Deserialize, Serialize};
use std::sync::Arc;

Expand Down Expand Up @@ -38,11 +42,25 @@ pub struct Settings {
pub price: Option<PriceSettings>,
}

/// Initialize the global MOSTRO_CONFIG struct
pub fn init_mostro_settings(s: Settings) {
MOSTRO_CONFIG
.set(s)
.expect("Failed to set Mostro global settings");
/// Initialize the global `MOSTRO_CONFIG` and `NOSTR_KEYS` structs.
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.
pub fn get_mostro_keys() -> Option<&'static Keys> {
NOSTR_KEYS.get()
}

/// Get database pool for Mostro db operations to share across the thread
Expand Down
4 changes: 2 additions & 2 deletions src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,8 +408,8 @@ pub struct LightningSettings {
pub struct NostrSettings {
/// Nostr private key. Optional when `MOSTRO_NSEC_PRIVKEY` is provided via
/// environment variable or `<settings_dir>/.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<String>,
}
Expand Down
Loading