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
109 changes: 105 additions & 4 deletions src/config/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use crate::config::{init_mostro_settings, Settings};
use mostro_core::error::MostroError::{self, *};
use mostro_core::error::ServiceError;
use std::fs;
use std::io::IsTerminal;
use std::path::PathBuf;
use std::io::{IsTerminal, Write};
use std::path::{Path, PathBuf};
use zeroize::Zeroizing;

const DB_FILENAME: &str = "mostro.db";
Expand Down Expand Up @@ -129,6 +129,47 @@ fn validate_cashu_settings(
Ok(())
}

/// Write `contents` to `path` with owner-only permissions (0o600).
///
/// On Unix the file is created with mode 0o600 and permissions are tightened
/// after opening so that pre-existing files are also restricted. This matches
/// how the `.env` file and the SQLite database are protected.
pub fn write_private_file(path: &Path, contents: &[u8]) -> Result<(), MostroError> {
#[cfg(unix)]
let mut file = {
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(path)
.map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?
};

#[cfg(not(unix))]
let mut file = {
std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?
};

#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let permissions = std::fs::Permissions::from_mode(0o600);
file.set_permissions(permissions)
.map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?;
}

file.write_all(contents)
.map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?;
Ok(())
}

/// Initialize the default settings directory and create a settings file from the template if it doesn't exist.
/// Checks if the directory already exists, and if not, creates it and writes the template file.
/// If a custom config path is provided, it uses that instead of the default `~/.mostro` directory.
Expand Down Expand Up @@ -163,8 +204,7 @@ pub fn init_configuration_file(config_path: Option<String>) -> Result<(), Mostro
wizard::run_setup_menu(&settings_dir, &config_file_path)?
} else {
// Non-interactive (Docker, CI, systemd): copy template and exit
std::fs::write(&config_file_path, include_bytes!("../../settings.tpl.toml"))
.map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?;
write_private_file(&config_file_path, include_bytes!("../../settings.tpl.toml"))?;
println!(
"Created settings file from template at {} - Edit it to configure your Mostro instance",
config_file_path.display()
Expand Down Expand Up @@ -582,3 +622,64 @@ mod init_configuration_file_tests {
assert!(result.is_err());
}
}

#[cfg(test)]
mod write_private_file_tests {
use super::*;

fn temp_dir(tag: &str) -> std::path::PathBuf {
let dir =
std::env::temp_dir().join(format!("mostro-write-private-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create temp dir");
dir
}

#[cfg(unix)]
fn mode_of(path: &Path) -> u32 {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.expect("stat file")
.permissions()
.mode()
& 0o777
}

#[test]
fn writes_expected_content() {
let dir = temp_dir("content");
let path = dir.join("settings.toml");
write_private_file(&path, b"nsec_privkey = 'nsec1...'\n").expect("write file");
let contents = std::fs::read_to_string(&path).expect("read file");
assert_eq!(contents, "nsec_privkey = 'nsec1...'\n");
}

#[test]
#[cfg(unix)]
fn new_file_is_owner_only() {
let dir = temp_dir("new");
let path = dir.join("settings.toml");
write_private_file(&path, b"secret").expect("write file");
assert_eq!(mode_of(&path), 0o600);
}

#[test]
#[cfg(unix)]
fn preexisting_world_readable_file_is_tightened() {
let dir = temp_dir("existing");
let path = dir.join("settings.toml");
std::fs::write(&path, "old content").expect("seed file");
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644))
.expect("loosen permissions");
}
assert_eq!(mode_of(&path), 0o644);

write_private_file(&path, b"new secret").expect("overwrite file");

assert_eq!(mode_of(&path), 0o600);
let contents = std::fs::read_to_string(&path).expect("read file");
assert_eq!(contents, "new secret");
}
}
28 changes: 4 additions & 24 deletions src/config/wizard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use super::settings::Settings;
use super::types::{
DatabaseSettings, LightningSettings, MostroSettings, NostrSettings, RpcSettings,
};
use super::util;

const TEMPLATE_BYTES: &[u8] = include_bytes!("../../settings.tpl.toml");

Expand Down Expand Up @@ -43,7 +44,7 @@ pub fn run_setup_menu(
Ok(settings)
}
_ => {
std::fs::write(config_file_path, TEMPLATE_BYTES)
util::write_private_file(config_file_path, TEMPLATE_BYTES)
.map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?;
println!(
"Created settings file from template at {} - Edit it to configure your Mostro instance",
Expand Down Expand Up @@ -82,29 +83,8 @@ fn run_setup_wizard(settings_dir: &Path, config_file_path: &Path) -> Result<Sett
let toml_content = toml::to_string_pretty(&settings)
.map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?;

{
#[cfg(unix)]
let file = {
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(config_file_path)
};
#[cfg(not(unix))]
let file = {
std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(config_file_path)
};
let mut file = file.map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?;
file.write_all(toml_content.as_bytes())
.map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?;
}
util::write_private_file(config_file_path, toml_content.as_bytes())
.map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?;

println!("\nConfiguration saved to {}\n", config_file_path.display());

Expand Down