fix(cli): reject empty HMAC keys #123

Merged
Lochnair merged 1 commits from fix/cli-empty-hmac into main 2026-08-20 03:48:54 +00:00
4 changed files with 51 additions and 1 deletions

No files matched your search

+13
View File
@@ -296,6 +296,19 @@ mod tests {
assert!(help.contains("--loose")); assert!(help.contains("--loose"));
} }
#[test]
fn an_empty_hmac_key_is_rejected() {
let err = parse(&["--hmac", "", "127.0.0.1:2112"]).unwrap_err();
assert!(err.to_string().contains("HMAC key must not be empty"));
}
#[test]
fn a_non_empty_hmac_key_is_accepted() {
let args = parse(&["--hmac", "secret", "127.0.0.1:2112"]).unwrap();
let config = args.prepare().unwrap().client;
assert_eq!(config.hmac_key, Some(b"secret".to_vec()));
}
#[test] #[test]
fn output_format_summary_policy_is_table_only() { fn output_format_summary_policy_is_table_only() {
assert!(OutputFormat::Table.prints_summary()); assert!(OutputFormat::Table.prints_summary());
+22
View File
@@ -26,6 +26,7 @@ pub struct ServerArgs {
#[arg( #[arg(
long, long,
value_name = "KEY", value_name = "KEY",
value_parser = parse_hmac_key,
long_help = "HMAC key, taken as the UTF-8 bytes of this argument.\n\nAuthentication is global: with a key configured, every request must carry a valid MAC and every reply is authenticated. Without one, authenticated requests are dropped. The key is visible in the process arguments." long_help = "HMAC key, taken as the UTF-8 bytes of this argument.\n\nAuthentication is global: with a key configured, every request must carry a valid MAC and every reply is authenticated. Without one, authenticated requests are dropped. The key is visible in the process arguments."
)] )]
pub hmac: Option<String>, pub hmac: Option<String>,
@@ -160,6 +161,15 @@ impl ServerArgs {
} }
} }
/// Rejects an empty HMAC key so a blank shell expansion cannot silently
/// enable authentication with predictable, empty key material.
fn parse_hmac_key(input: &str) -> Result<String, String> {
if input.is_empty() {
return Err("HMAC key must not be empty".to_owned());
}
Ok(input.to_owned())
}
/// Parses a duration whose zero is meaningful server policy. /// Parses a duration whose zero is meaningful server policy.
fn parse_duration_allow_zero(input: &str) -> Result<Duration, String> { fn parse_duration_allow_zero(input: &str) -> Result<Duration, String> {
if input == "0" { if input == "0" {
@@ -404,6 +414,18 @@ mod tests {
assert!(parse(&["--bind", "127.0.0.1:2112", "--idle-timeout", "ms"]).is_err()); assert!(parse(&["--bind", "127.0.0.1:2112", "--idle-timeout", "ms"]).is_err());
} }
#[test]
fn an_empty_hmac_key_is_rejected() {
let err = parse(&["--bind", "127.0.0.1:2112", "--hmac", ""]).unwrap_err();
assert!(err.to_string().contains("HMAC key must not be empty"));
}
#[test]
fn a_whitespace_only_hmac_key_is_accepted_literally() {
let config = bound(&["--hmac", " "]).server_config();
assert_eq!(config.hmac_key(), Some(b" ".as_slice()));
}
#[test] #[test]
fn a_zero_maximum_duration_is_not_a_spelling_of_no_maximum() { fn a_zero_maximum_duration_is_not_a_spelling_of_no_maximum() {
assert!(parse(&["--bind", "127.0.0.1:2112", "--max-duration", "0"]).is_err()); assert!(parse(&["--bind", "127.0.0.1:2112", "--max-duration", "0"]).is_err());
+8
View File
@@ -270,6 +270,14 @@ mod tests {
assert_eq!(shared.socket_config.ttl, tui.socket_config.ttl); assert_eq!(shared.socket_config.ttl, tui.socket_config.ttl);
} }
#[test]
fn an_empty_hmac_key_is_rejected_via_the_shared_args() {
// CommonClientArgs is flattened into both the client and TUI parsers,
// so proving rejection once here covers both without duplicating it.
let err = parse(&["--hmac", "", "127.0.0.1:2112"]).unwrap_err();
assert!(err.to_string().contains("HMAC key must not be empty"));
}
#[test] #[test]
fn tui_help_lists_multi_target_options() { fn tui_help_lists_multi_target_options() {
let help = TuiArgs::command().render_help().to_string(); let help = TuiArgs::command().render_help().to_string();
+8 -1
View File
@@ -15,7 +15,7 @@ pub struct CommonClientArgs {
pub length: u32, pub length: u32,
/// HMAC key. /// HMAC key.
#[arg(long)] #[arg(long, value_parser = parse_hmac_key)]
pub hmac: Option<String>, pub hmac: Option<String>,
/// Clock mode to request. /// Clock mode to request.
@@ -189,6 +189,13 @@ pub fn parse_length(input: &str) -> Result<u32, String> {
Ok(length) Ok(length)
} }
pub fn parse_hmac_key(input: &str) -> Result<String, String> {
if input.is_empty() {
return Err("HMAC key must not be empty".to_owned());
}
Ok(input.to_owned())
}
pub fn parse_server_fill(input: &str) -> Result<String, String> { pub fn parse_server_fill(input: &str) -> Result<String, String> {
if input.is_empty() { if input.is_empty() {
return Err("server fill must not be empty".to_owned()); return Err("server fill must not be empty".to_owned());