diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index ba64e7ce..2d4fb5f2 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -14,7 +14,6 @@ mod util; use std::collections::HashSet; use std::fs; -use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -56,6 +55,7 @@ use crate::util::metrics::Metrics; use crate::util::proto_adapter::{forwarded_payment_to_proto, payment_to_proto}; use crate::util::systemd; use crate::util::tls::get_or_generate_tls_config; +use crate::util::write_new; const API_KEY_FILE: &str = "api_key"; @@ -834,12 +834,7 @@ fn load_or_generate_api_key(storage_dir: &Path) -> std::io::Result { let mut key_bytes = [0u8; 32]; getrandom::getrandom(&mut key_bytes).map_err(std::io::Error::other)?; - // Write the raw bytes to the file - fs::write(&api_key_path, key_bytes)?; - - // Set permissions to 0400 (read-only for owner) - let permissions = fs::Permissions::from_mode(0o400); - fs::set_permissions(&api_key_path, permissions)?; + write_new(&api_key_path, &key_bytes, 0o400)?; debug!("Generated new API key at {}", api_key_path.display()); Ok(key_bytes.to_lower_hex_string()) diff --git a/ldk-server/src/util/mod.rs b/ldk-server/src/util/mod.rs index a57dbd00..a08295e2 100644 --- a/ldk-server/src/util/mod.rs +++ b/ldk-server/src/util/mod.rs @@ -13,3 +13,57 @@ pub(crate) mod metrics; pub(crate) mod proto_adapter; pub(crate) mod systemd; pub(crate) mod tls; + +use std::fs::{self, OpenOptions}; +use std::io::{self, Write}; +use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; +use std::path::Path; + +pub(crate) fn write_new(path: &Path, contents: &[u8], mode: u32) -> io::Result<()> { + let mut file = OpenOptions::new().create_new(true).write(true).mode(mode).open(path)?; + file.write_all(contents)?; + fs::set_permissions(path, fs::Permissions::from_mode(mode))?; + file.sync_all()?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn write_new_sets_requested_mode_and_contents() { + let dir = test_dir("mode_and_contents"); + let path = dir.join("secret"); + + write_new(&path, b"secret-bytes", 0o400).unwrap(); + + assert_eq!(fs::read(&path).unwrap(), b"secret-bytes"); + assert_eq!(fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o400); + + fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn write_new_does_not_replace_existing_file() { + let dir = test_dir("existing_file"); + let path = dir.join("secret"); + fs::write(&path, b"original").unwrap(); + + let err = write_new(&path, b"replacement", 0o400).unwrap_err(); + + assert_eq!(err.kind(), io::ErrorKind::AlreadyExists); + assert_eq!(fs::read(&path).unwrap(), b"original"); + + fs::remove_dir_all(dir).unwrap(); + } + + fn test_dir(name: &str) -> PathBuf { + let dir = std::env::temp_dir() + .join(format!("ldk-server-secure-file-test-{name}-{}", std::process::id())); + let _ = fs::remove_dir_all(&dir); + fs::create_dir(&dir).unwrap(); + dir + } +} diff --git a/ldk-server/src/util/tls.rs b/ldk-server/src/util/tls.rs index b7cc57b6..66086c6e 100644 --- a/ldk-server/src/util/tls.rs +++ b/ldk-server/src/util/tls.rs @@ -9,7 +9,7 @@ use std::fs; use std::net::IpAddr; -use std::os::unix::fs::PermissionsExt; +use std::path::Path; use base64::Engine; use ring::rand::SystemRandom; @@ -18,6 +18,7 @@ use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer}; use tokio_rustls::rustls::ServerConfig; use crate::util::config::TlsConfig; +use crate::util::write_new; // Issuer and Subject common name const ISSUER_NAME: &str = "localhost"; @@ -133,10 +134,8 @@ fn generate_self_signed_cert( let cert_pem = der_to_pem(&cert_der, PEM_CERT_BEGIN, PEM_CERT_END); let key_pem = der_to_pem(pkcs8_doc.as_ref(), PEM_KEY_BEGIN, PEM_KEY_END); - fs::write(key_path, &key_pem) + write_new(Path::new(key_path), key_pem.as_bytes(), 0o400) .map_err(|e| format!("Failed to write TLS key to '{key_path}': {e}"))?; - fs::set_permissions(key_path, fs::Permissions::from_mode(0o400)) - .map_err(|e| format!("Failed to set TLS key permissions for '{key_path}': {e}"))?; fs::write(cert_path, &cert_pem) .map_err(|e| format!("Failed to write TLS certificate to '{cert_path}': {e}"))?;