Skip to content

Commit e02512e

Browse files
committed
Bound client API key file reads
API keys are exactly 32 bytes. Reading malformed or special files in full can otherwise consume unbounded memory. Read only enough bytes to detect an oversized key. Treat missing files separately from read and format errors. This keeps a malformed configured key from being silently ignored in favor of a default key. Use the same open-first handling in the daemon so only NotFound triggers key generation and all other open failures are reported. This commit was created with assistance from Codex.
1 parent 59643fc commit e02512e

4 files changed

Lines changed: 63 additions & 22 deletions

File tree

ldk-server-cli/src/main.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -617,10 +617,15 @@ async fn main() {
617617
},
618618
};
619619

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

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

ldk-server-client/src/config.rs

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@
1313
//! locating the server's TLS certificate and API key on disk, so multiple clients (CLI, MCP
1414
//! bridge, etc.) can resolve connection credentials in a consistent way.
1515
16-
use std::path::PathBuf;
16+
use std::io::{ErrorKind, Read};
17+
use std::path::{Path, PathBuf};
1718

1819
use hex_conservative::DisplayHex;
1920
use serde::{Deserialize, Serialize};
2021

2122
const DEFAULT_CONFIG_FILE: &str = "config.toml";
2223
const DEFAULT_CERT_FILE: &str = "tls.crt";
2324
const API_KEY_FILE: &str = "api_key";
25+
const API_KEY_LEN: usize = 32;
2426

2527
/// Default address of the `ldk-server` gRPC endpoint when no explicit value is configured.
2628
pub const DEFAULT_GRPC_SERVICE_ADDRESS: &str = "127.0.0.1:3536";
@@ -146,18 +148,47 @@ pub fn resolve_base_url(override_url: Option<String>, config: Option<&Config>) -
146148
/// Prefers `override_key`, falls back to reading the API key file from the configured storage
147149
/// directory, and finally from the OS-specific default data directory. The raw bytes read from
148150
/// disk are lower-hex encoded before being returned.
149-
pub fn resolve_api_key(override_key: Option<String>, config: Option<&Config>) -> Option<String> {
150-
override_key.or_else(|| {
151-
let network =
152-
config.and_then(|c| c.network().ok()).unwrap_or_else(|| "bitcoin".to_string());
153-
storage_dir(config)
154-
.map(|dir| api_key_path_for_storage_dir(dir, &network))
155-
.and_then(|path| std::fs::read(&path).ok())
156-
.or_else(|| {
157-
get_default_api_key_path(&network).and_then(|path| std::fs::read(&path).ok())
158-
})
159-
.map(|bytes| bytes.to_lower_hex_string())
160-
})
151+
///
152+
/// Returns an error if a candidate API key file exists but cannot be read or does not contain
153+
/// exactly 32 bytes.
154+
pub fn resolve_api_key(
155+
override_key: Option<String>, config: Option<&Config>,
156+
) -> Result<Option<String>, String> {
157+
if override_key.is_some() {
158+
return Ok(override_key);
159+
}
160+
161+
let network = config.and_then(|c| c.network().ok()).unwrap_or_else(|| "bitcoin".to_string());
162+
if let Some(dir) = storage_dir(config) {
163+
let path = api_key_path_for_storage_dir(dir, &network);
164+
if let Some(api_key) = read_api_key(&path)? {
165+
return Ok(Some(api_key));
166+
}
167+
}
168+
169+
match get_default_api_key_path(&network) {
170+
Some(path) => read_api_key(&path),
171+
None => Ok(None),
172+
}
173+
}
174+
175+
fn read_api_key(path: &Path) -> Result<Option<String>, String> {
176+
let file = match std::fs::File::open(path) {
177+
Ok(file) => file,
178+
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None),
179+
Err(e) => return Err(format!("Failed to read API key file '{}': {e}", path.display())),
180+
};
181+
let mut bytes = Vec::with_capacity(API_KEY_LEN + 1);
182+
file.take((API_KEY_LEN + 1) as u64)
183+
.read_to_end(&mut bytes)
184+
.map_err(|e| format!("Failed to read API key file '{}': {e}", path.display()))?;
185+
if bytes.len() != API_KEY_LEN {
186+
return Err(format!(
187+
"API key file '{}' must contain exactly {API_KEY_LEN} bytes",
188+
path.display()
189+
));
190+
}
191+
Ok(Some(bytes.to_lower_hex_string()))
161192
}
162193

163194
/// Resolves the path to the server's TLS certificate (PEM).

ldk-server-mcp/src/config.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pub fn resolve_config(config_path: Option<String>) -> Result<ResolvedConfig, Str
3939

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

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

@@ -203,7 +203,7 @@ mod tests {
203203

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

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

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

232232
std::fs::remove_dir_all(temp_dir).unwrap();

ldk-server/src/main.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -895,8 +895,13 @@ fn upsert_payment_details(
895895
fn load_or_generate_api_key(storage_dir: &Path) -> std::io::Result<String> {
896896
let api_key_path = storage_dir.join(API_KEY_FILE);
897897

898-
if api_key_path.exists() {
899-
let file = fs::File::open(&api_key_path)?;
898+
let file = match fs::File::open(&api_key_path) {
899+
Ok(file) => Some(file),
900+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
901+
Err(e) => return Err(e),
902+
};
903+
904+
if let Some(file) = file {
900905
let mut key_bytes = Vec::with_capacity(API_KEY_LEN + 1);
901906
file.take((API_KEY_LEN + 1) as u64).read_to_end(&mut key_bytes)?;
902907
if key_bytes.len() != API_KEY_LEN {

0 commit comments

Comments
 (0)