fix(secrets): harden windows icacls username edge cases

This commit is contained in:
argenis de la rosa 2026-02-14 19:25:30 -05:00
parent 6fd4b2d750
commit a68004184c

View file

@ -192,30 +192,31 @@ impl SecretStore {
{ {
// On Windows, use icacls to restrict permissions to current user only // On Windows, use icacls to restrict permissions to current user only
let username = std::env::var("USERNAME").unwrap_or_default(); let username = std::env::var("USERNAME").unwrap_or_default();
if username.is_empty() { let Some(grant_arg) = build_windows_icacls_grant_arg(&username) else {
tracing::warn!( tracing::warn!(
"USERNAME environment variable is empty; \ "USERNAME environment variable is empty; \
cannot restrict key file permissions via icacls" cannot restrict key file permissions via icacls"
); );
} else { return Ok(key);
match std::process::Command::new("icacls") };
.arg(&self.key_path)
.args(["/inheritance:r", "/grant:r"]) match std::process::Command::new("icacls")
.arg(format!("{username}:F")) .arg(&self.key_path)
.output() .args(["/inheritance:r", "/grant:r"])
{ .arg(grant_arg)
Ok(o) if !o.status.success() => { .output()
tracing::warn!( {
"Failed to set key file permissions via icacls (exit code {:?})", Ok(o) if !o.status.success() => {
o.status.code() tracing::warn!(
); "Failed to set key file permissions via icacls (exit code {:?})",
} o.status.code()
Err(e) => { );
tracing::warn!("Could not set key file permissions: {e}"); }
} Err(e) => {
_ => { tracing::warn!("Could not set key file permissions: {e}");
tracing::debug!("Key file permissions restricted via icacls"); }
} _ => {
tracing::debug!("Key file permissions restricted via icacls");
} }
} }
} }
@ -258,9 +259,19 @@ fn hex_encode(data: &[u8]) -> String {
s s
} }
/// Build the `/grant` argument for `icacls` using a normalized username.
/// Returns `None` when the username is empty or whitespace-only.
fn build_windows_icacls_grant_arg(username: &str) -> Option<String> {
let normalized = username.trim();
if normalized.is_empty() {
return None;
}
Some(format!("{normalized}:F"))
}
/// Hex-decode a hex string to bytes. /// Hex-decode a hex string to bytes.
fn hex_decode(hex: &str) -> Result<Vec<u8>> { fn hex_decode(hex: &str) -> Result<Vec<u8>> {
if !hex.len().is_multiple_of(2) { if (hex.len() & 1) != 0 {
anyhow::bail!("Hex string has odd length"); anyhow::bail!("Hex string has odd length");
} }
(0..hex.len()) (0..hex.len())
@ -751,6 +762,28 @@ mod tests {
assert!(hex_decode("zzzz").is_err()); assert!(hex_decode("zzzz").is_err());
} }
#[test]
fn windows_icacls_grant_arg_rejects_empty_username() {
assert_eq!(build_windows_icacls_grant_arg(""), None);
assert_eq!(build_windows_icacls_grant_arg(" \t\n"), None);
}
#[test]
fn windows_icacls_grant_arg_trims_username() {
assert_eq!(
build_windows_icacls_grant_arg(" alice "),
Some("alice:F".to_string())
);
}
#[test]
fn windows_icacls_grant_arg_preserves_valid_characters() {
assert_eq!(
build_windows_icacls_grant_arg("DOMAIN\\svc-user"),
Some("DOMAIN\\svc-user:F".to_string())
);
}
#[test] #[test]
fn generate_random_key_correct_length() { fn generate_random_key_correct_length() {
let key = generate_random_key(); let key = generate_random_key();