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
13 changes: 9 additions & 4 deletions ldk-server-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,10 +617,15 @@ async fn main() {
},
};

let api_key = resolve_api_key(cli.api_key, config.as_ref()).unwrap_or_else(|| {
eprintln!("API key not provided. Use --api-key or ensure the api_key file exists at {DEFAULT_DIR}/[network]/api_key");
std::process::exit(1);
});
let api_key = resolve_api_key(cli.api_key, config.as_ref())
.unwrap_or_else(|e| {
eprintln!("Failed to resolve API key: {e}");
std::process::exit(1);
})
.unwrap_or_else(|| {
eprintln!("API key not provided. Use --api-key or ensure the api_key file exists at {DEFAULT_DIR}/[network]/api_key");
std::process::exit(1);
});

let base_url = resolve_base_url(cli.base_url, config.as_ref());

Expand Down
57 changes: 44 additions & 13 deletions ldk-server-client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@
//! locating the server's TLS certificate and API key on disk, so multiple clients (CLI, MCP
//! bridge, etc.) can resolve connection credentials in a consistent way.

use std::path::PathBuf;
use std::io::{ErrorKind, Read};
use std::path::{Path, PathBuf};

use hex_conservative::DisplayHex;
use serde::{Deserialize, Serialize};

const DEFAULT_CONFIG_FILE: &str = "config.toml";
const DEFAULT_CERT_FILE: &str = "tls.crt";
const API_KEY_FILE: &str = "api_key";
const API_KEY_LEN: usize = 32;

/// Default address of the `ldk-server` gRPC endpoint when no explicit value is configured.
pub const DEFAULT_GRPC_SERVICE_ADDRESS: &str = "127.0.0.1:3536";
Expand Down Expand Up @@ -146,18 +148,47 @@ pub fn resolve_base_url(override_url: Option<String>, config: Option<&Config>) -
/// Prefers `override_key`, falls back to reading the API key file from the configured storage
/// directory, and finally from the OS-specific default data directory. The raw bytes read from
/// disk are lower-hex encoded before being returned.
pub fn resolve_api_key(override_key: Option<String>, config: Option<&Config>) -> Option<String> {
override_key.or_else(|| {
let network =
config.and_then(|c| c.network().ok()).unwrap_or_else(|| "bitcoin".to_string());
storage_dir(config)
.map(|dir| api_key_path_for_storage_dir(dir, &network))
.and_then(|path| std::fs::read(&path).ok())
.or_else(|| {
get_default_api_key_path(&network).and_then(|path| std::fs::read(&path).ok())
})
.map(|bytes| bytes.to_lower_hex_string())
})
///
/// Returns an error if a candidate API key file exists but cannot be read or does not contain
/// exactly 32 bytes.
pub fn resolve_api_key(
override_key: Option<String>, config: Option<&Config>,
) -> Result<Option<String>, String> {
if override_key.is_some() {
return Ok(override_key);
}

let network = config.and_then(|c| c.network().ok()).unwrap_or_else(|| "bitcoin".to_string());
if let Some(dir) = storage_dir(config) {
let path = api_key_path_for_storage_dir(dir, &network);
if let Some(api_key) = read_api_key(&path)? {
return Ok(Some(api_key));
}
}

match get_default_api_key_path(&network) {
Some(path) => read_api_key(&path),
None => Ok(None),
}
}

fn read_api_key(path: &Path) -> Result<Option<String>, String> {
let file = match std::fs::File::open(path) {
Ok(file) => file,
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(format!("Failed to read API key file '{}': {e}", path.display())),
};
let mut bytes = Vec::with_capacity(API_KEY_LEN + 1);
file.take((API_KEY_LEN + 1) as u64)
.read_to_end(&mut bytes)
.map_err(|e| format!("Failed to read API key file '{}': {e}", path.display()))?;
if bytes.len() != API_KEY_LEN {
return Err(format!(
"API key file '{}' must contain exactly {API_KEY_LEN} bytes",
path.display()
));
}
Ok(Some(bytes.to_lower_hex_string()))
}

/// Resolves the path to the server's TLS certificate (PEM).
Expand Down
6 changes: 3 additions & 3 deletions ldk-server-mcp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub fn resolve_config(config_path: Option<String>) -> Result<ResolvedConfig, Str

let base_url = resolve_base_url(env_base_url, config.as_ref());

let api_key = resolve_api_key(env_api_key, config.as_ref()).ok_or_else(
let api_key = resolve_api_key(env_api_key, config.as_ref())?.ok_or_else(
|| "API key not provided. Set LDK_API_KEY or ensure the api_key file exists at ~/.ldk-server/[network]/api_key".to_string()
)?;

Expand Down Expand Up @@ -203,7 +203,7 @@ mod tests {

let cert_path = custom_storage.join("tls.crt");
std::fs::write(&cert_path, b"storage-cert").unwrap();
std::fs::write(custom_storage.join("regtest").join("api_key"), [0xAB, 0xCD]).unwrap();
std::fs::write(custom_storage.join("regtest").join("api_key"), [0xAB; 32]).unwrap();

std::fs::write(
&config_path,
Expand All @@ -226,7 +226,7 @@ mod tests {
let resolved = resolve_config(Some(config_path.display().to_string())).unwrap();

assert_eq!(resolved.base_url, DEFAULT_GRPC_SERVICE_ADDRESS);
assert_eq!(resolved.api_key, "abcd");
assert_eq!(resolved.api_key, "ab".repeat(32));
assert_eq!(resolved.tls_cert_pem, b"storage-cert");

std::fs::remove_dir_all(temp_dir).unwrap();
Expand Down
16 changes: 14 additions & 2 deletions ldk-server/src/io/persist/sqlite_store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@
// You may not use this file except in accordance with one or both of these
// licenses.

use std::fs::OpenOptions;
use std::io;
use std::os::unix::fs::OpenOptionsExt;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::{fs, io};

use ldk_node::lightning::types::string::PrintableString;
use rusqlite::{named_params, Connection};

use crate::io::persist::paginated_kv_store::{ListResponse, PaginatedKVStore};
use crate::io::utils::check_namespace_key_validity;
use crate::util::create_dir_all_private;

/// The default database file name.
pub const DEFAULT_SQLITE_DB_FILE_NAME: &str = "ldk_server_data.sqlite";
Expand Down Expand Up @@ -48,7 +51,7 @@ impl SqliteStore {
let paginated_kv_table_name =
paginated_kv_table_name.unwrap_or(DEFAULT_PAGINATED_KV_TABLE_NAME.to_string());

fs::create_dir_all(data_dir.clone()).map_err(|e| {
create_dir_all_private(&data_dir).map_err(|e| {
let msg = format!(
"Failed to create database destination directory {}: {}",
data_dir.display(),
Expand All @@ -58,6 +61,15 @@ impl SqliteStore {
})?;
let mut db_file_path = data_dir;
db_file_path.push(db_file_name);
match OpenOptions::new().create_new(true).write(true).mode(0o600).open(&db_file_path) {
Ok(_) => {},
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {},
Err(e) => {
let msg =
format!("Failed to create database file {}: {}", db_file_path.display(), e);
return Err(io::Error::other(msg));
},
}

let connection = Connection::open(db_file_path.clone()).map_err(|e| {
let msg =
Expand Down
45 changes: 40 additions & 5 deletions ldk-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod util;

use std::collections::HashSet;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
Expand Down Expand Up @@ -54,9 +55,10 @@ use crate::util::logger::{LogConfig, ServerLogger};
use crate::util::metrics::Metrics;
use crate::util::proto_adapter::{forwarded_payment_to_proto, payment_to_proto};
use crate::util::tls::get_or_generate_tls_config;
use crate::util::{systemd, write_new};
use crate::util::{create_dir_all_private, systemd, write_new};

const API_KEY_FILE: &str = "api_key";
const API_KEY_LEN: usize = 32;
const FULL_VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", env!("GIT_HASH"), ")");

pub fn get_default_data_dir() -> Option<PathBuf> {
Expand Down Expand Up @@ -893,15 +895,31 @@ fn upsert_payment_details(
fn load_or_generate_api_key(storage_dir: &Path) -> std::io::Result<String> {
let api_key_path = storage_dir.join(API_KEY_FILE);

if api_key_path.exists() {
let key_bytes = fs::read(&api_key_path)?;
let file = match fs::File::open(&api_key_path) {
Ok(file) => Some(file),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(e) => return Err(e),
};

if let Some(file) = file {
let mut key_bytes = Vec::with_capacity(API_KEY_LEN + 1);
file.take((API_KEY_LEN + 1) as u64).read_to_end(&mut key_bytes)?;
if key_bytes.len() != API_KEY_LEN {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"API key file '{}' must contain exactly {API_KEY_LEN} bytes",
api_key_path.display()
),
));
}
Ok(key_bytes.to_lower_hex_string())
} else {
// Ensure the storage directory exists
fs::create_dir_all(storage_dir)?;
create_dir_all_private(storage_dir)?;

// Generate a 32-byte random API key
let mut key_bytes = [0u8; 32];
let mut key_bytes = [0u8; API_KEY_LEN];
getrandom::getrandom(&mut key_bytes).map_err(std::io::Error::other)?;

write_new(&api_key_path, &key_bytes, 0o400)?;
Expand Down Expand Up @@ -929,6 +947,23 @@ mod tests {

use super::*;

#[test]
fn load_api_key_rejects_invalid_lengths() {
let nonce = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
let dir = std::env::temp_dir()
.join(format!("ldk-server-api-key-length-{}-{nonce}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
let path = dir.join(API_KEY_FILE);

for len in [0, 1, API_KEY_LEN - 1, API_KEY_LEN + 1] {
fs::write(&path, vec![0x42; len]).unwrap();
let error = load_or_generate_api_key(&dir).unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
}

fs::remove_dir_all(dir).unwrap();
}

#[test]
fn test_is_channel_open_failure_classification() {
assert!(is_channel_open_failure(Some(&ClosureReason::FundingTimedOut)));
Expand Down
4 changes: 2 additions & 2 deletions ldk-server/src/util/entropy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use ldk_node::bip39::Mnemonic;
use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy};
use log::info;

use crate::util::write_new;
use crate::util::{create_dir_all_private, write_new};

const DEFAULT_MNEMONIC_FILE: &str = "keys_mnemonic";

Expand All @@ -32,7 +32,7 @@ pub(crate) fn load_or_generate_node_entropy(storage_dir: &Path) -> io::Result<No
})?
} else {
if let Some(parent) = mnemonic_path.parent() {
fs::create_dir_all(parent)?;
create_dir_all_private(parent)?;
}
let mnemonic = generate_entropy_mnemonic(None);
write_new(&mnemonic_path, format!("{}\n", mnemonic).as_bytes(), 0o600)?;
Expand Down
30 changes: 28 additions & 2 deletions ldk-server/src/util/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@

use std::fs::{self, File, OpenOptions};
use std::io::{self, LineWriter, Write};
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;

use log::{error, Level, LevelFilter, Log, Metadata, Record};

use crate::util::create_dir_all_private;

struct LoggerState {
file: LineWriter<File>,
bytes_written: usize,
Expand Down Expand Up @@ -67,7 +70,7 @@ impl ServerLogger {
let state = if let Some(path) = &log_file_path {
// Create parent directories if they don't exist
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
create_dir_all_private(parent)?;
}

let file = open_log_file(path)?;
Expand Down Expand Up @@ -259,7 +262,7 @@ fn format_level(level: Level) -> &'static str {
}

fn open_log_file(log_file_path: &Path) -> Result<File, io::Error> {
OpenOptions::new().create(true).append(true).open(log_file_path)
OpenOptions::new().create(true).append(true).mode(0o600).open(log_file_path)
}

fn cleanup_old_logs(log_file_path: &Path, max_files: usize) -> io::Result<()> {
Expand Down Expand Up @@ -319,3 +322,26 @@ impl Log for LoggerWrapper {
self.0.flush()
}
}

#[cfg(test)]
mod tests {
use std::os::unix::fs::PermissionsExt;
use std::time::{SystemTime, UNIX_EPOCH};

use super::*;

#[test]
fn open_log_file_creates_private_file() {
let nonce = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
let dir = std::env::temp_dir()
.join(format!("ldk-server-log-mode-{}-{nonce}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
let path = dir.join("ldk-server.log");

drop(open_log_file(&path).unwrap());

let mode = fs::metadata(&path).unwrap().permissions().mode();
assert_eq!(mode & 0o077, 0);
fs::remove_dir_all(dir).unwrap();
}
}
9 changes: 7 additions & 2 deletions ldk-server/src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ pub(crate) mod proto_adapter;
pub(crate) mod systemd;
pub(crate) mod tls;

use std::fs::{self, OpenOptions};
use std::fs::{self, DirBuilder, OpenOptions};
use std::io::{self, Write};
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt, PermissionsExt};
use std::path::Path;

pub(crate) fn create_dir_all_private(path: &Path) -> io::Result<()> {
let mut builder = DirBuilder::new();
builder.recursive(true).mode(0o700).create(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)?;
Expand Down