use std::borrow::Cow;
const REDACTED: &str = "****";
const SECRET_FLAG_SUBSTRINGS: [&str; 3] = ["access-key", "session-token", "sse-c-key"];
fn is_secret_flag(flag: &str) -> bool {
let name = flag.trim_start_matches('-');
SECRET_FLAG_SUBSTRINGS.iter().any(|s| name.contains(s))
}
pub(crate) fn redact_secrets(raw: &str) -> Cow<'_, str> {
if !SECRET_FLAG_SUBSTRINGS.iter().any(|s| raw.contains(s)) {
return Cow::Borrowed(raw);
}
match shlex::split(raw) {
Some(tokens) => match redact_tokens(&tokens) {
Some(redacted) => Cow::Owned(redacted),
None => Cow::Borrowed(raw),
},
None => match redact_whitespace(raw) {
Some(redacted) => Cow::Owned(redacted),
None => Cow::Borrowed(raw),
},
}
}
fn redact_tokens(tokens: &[String]) -> Option<String> {
let mut out: Vec<Cow<str>> = Vec::with_capacity(tokens.len());
let mut masked = false;
let mut i = 0;
while i < tokens.len() {
let tok = tokens[i].as_str();
if tok.starts_with('-')
&& let Some(eq) = tok.find('=')
&& is_secret_flag(&tok[..eq])
{
out.push(Cow::Owned(format!("{}={REDACTED}", &tok[..eq])));
masked = true;
i += 1;
continue;
}
if tok.starts_with('-') && is_secret_flag(tok) {
out.push(Cow::Borrowed(tok));
if i + 1 < tokens.len() {
out.push(Cow::Borrowed(REDACTED));
masked = true;
i += 2;
continue;
}
i += 1;
continue;
}
out.push(Cow::Borrowed(tok));
i += 1;
}
masked.then(|| join_tokens(&out))
}
fn redact_whitespace(raw: &str) -> Option<String> {
let mut out: Vec<String> = Vec::new();
let mut masked = false;
let mut expect_value = false;
for word in raw.split_whitespace() {
if expect_value {
out.push(REDACTED.to_string());
masked = true;
expect_value = false;
continue;
}
if word.starts_with('-')
&& let Some(eq) = word.find('=')
&& is_secret_flag(&word[..eq])
{
out.push(format!("{}={REDACTED}", &word[..eq]));
masked = true;
continue;
}
if word.starts_with('-') && is_secret_flag(word) {
out.push(word.to_string());
expect_value = true;
continue;
}
out.push(word.to_string());
}
masked.then(|| out.join(" "))
}
fn join_tokens(tokens: &[Cow<str>]) -> String {
tokens
.iter()
.map(|c| c.as_ref())
.collect::<Vec<&str>>()
.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_secret_flag_matches_every_credential_variant() {
for flag in [
"--target-access-key",
"--source-access-key",
"--target-secret-access-key",
"--source-secret-access-key",
"--target-session-token",
"--source-session-token",
"--source-sse-c-key",
"--target-sse-c-key",
"--source-sse-c-key-md5",
"--target-sse-c-key-md5",
] {
assert!(is_secret_flag(flag), "{flag} should be treated as secret");
}
}
#[test]
fn is_secret_flag_rejects_non_credential_flags() {
for flag in [
"--target-region",
"--target-endpoint-url",
"--dry-run",
"--tagging",
"--target-profile",
"--json-tracing",
"--",
"-v",
] {
assert!(!is_secret_flag(flag), "{flag} must not be masked");
}
}
#[test]
fn no_secret_flag_returns_borrowed_unchanged() {
let raw = "cp s3://b/key /tmp/dst --target-region us-east-1";
let out = redact_secrets(raw);
assert!(
matches!(out, Cow::Borrowed(_)),
"expected zero-alloc borrow"
);
assert_eq!(out, raw);
}
#[test]
fn empty_line_is_borrowed_unchanged() {
let out = redact_secrets("");
assert!(matches!(out, Cow::Borrowed(_)));
assert_eq!(out, "");
}
#[test]
fn substring_in_non_flag_token_is_not_masked() {
let raw = "cp s3://b/my-access-key-notes.txt /tmp/out";
let out = redact_secrets(raw);
assert_eq!(out, raw);
}
#[test]
fn masks_secret_access_key_space_form() {
let out = redact_secrets(
"head-bucket s3://b --target-secret-access-key wJalrXUtnFEMISECRET --target-region us-east-1",
);
assert!(!out.contains("wJalrXUtnFEMISECRET"), "secret leaked: {out}");
assert!(
out.contains("--target-secret-access-key ****"),
"got: {out}"
);
assert!(out.contains("head-bucket"));
assert!(out.contains("s3://b"));
assert!(out.contains("--target-region us-east-1"));
}
#[test]
fn masks_access_key_id_session_token_and_sse_c_key() {
for (flag, value) in [
("--target-access-key", "AKIAIOSFODNN7EXAMPLE"),
("--source-session-token", "FQoGZXIvYXdzSESSIONTOKEN"),
("--source-sse-c-key", "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="),
("--target-sse-c-key-md5", "abcdEXAMPLEmd5=="),
] {
let raw = format!("cp s3://b/k /tmp/o {flag} {value}");
let out = redact_secrets(&raw);
assert!(!out.contains(value), "{flag}: secret leaked: {out}");
assert!(out.contains(&format!("{flag} ****")), "{flag}: got: {out}");
}
}
#[test]
fn masks_multiple_secrets_in_one_line() {
let out = redact_secrets(
"cp /f s3://b/k --target-access-key AKIAEXAMPLEID --target-secret-access-key SECRETVAL --target-session-token TOKVAL",
);
assert!(!out.contains("AKIAEXAMPLEID"), "got: {out}");
assert!(!out.contains("SECRETVAL"), "got: {out}");
assert!(!out.contains("TOKVAL"), "got: {out}");
assert_eq!(out.matches("****").count(), 3, "all three masked: {out}");
}
#[test]
fn secret_flag_as_last_token_without_value_does_not_panic_or_change() {
let raw = "create-bucket s3://b --target-secret-access-key";
let out = redact_secrets(raw);
assert_eq!(out, raw);
}
#[test]
fn masks_secret_equals_form() {
let out = redact_secrets("head-bucket s3://b --target-secret-access-key=wJalrSECRETeq");
assert!(!out.contains("wJalrSECRETeq"), "got: {out}");
assert!(
out.contains("--target-secret-access-key=****"),
"got: {out}"
);
}
#[test]
fn masks_empty_equals_value() {
let out = redact_secrets("head-bucket s3://b --target-secret-access-key=");
assert!(
out.contains("--target-secret-access-key=****"),
"got: {out}"
);
}
#[test]
fn masks_quoted_value_with_spaces() {
let out = redact_secrets(r#"head-bucket s3://b --source-sse-c-key "a b c secret""#);
assert!(!out.contains("a b c secret"), "got: {out}");
assert!(out.contains("****"), "got: {out}");
}
#[test]
fn masks_secret_even_when_line_cannot_be_tokenized() {
let out = redact_secrets(r#"cp --target-secret-access-key SECRETUNBALANCED "unterminated"#);
assert!(!out.contains("SECRETUNBALANCED"), "secret leaked: {out}");
assert!(out.contains("****"), "got: {out}");
}
#[test]
fn malformed_line_without_secret_flag_is_unchanged() {
let raw = r#"cp "unterminated s3://b/access-key-thing"#;
let out = redact_secrets(raw);
assert_eq!(out, raw);
}
#[test]
fn redacted_line_keeps_leading_subcommand_token() {
let out = redact_secrets("cp s3://b/k /tmp/o --target-secret-access-key SECRET");
assert!(
out.starts_with("cp "),
"command must stay identifiable: {out}"
);
}
#[test]
fn non_secret_equals_flag_passes_through_while_secret_is_masked() {
let out = redact_secrets(
"cp s3://b/k --target-region=us-east-1 --target-secret-access-key SECRETXYZ",
);
assert!(out.contains("--target-region=us-east-1"), "got: {out}");
assert!(!out.contains("SECRETXYZ"), "secret leaked: {out}");
assert!(
out.contains("--target-secret-access-key ****"),
"got: {out}"
);
}
#[test]
fn masks_equals_form_secret_when_line_cannot_be_tokenized() {
let out = redact_secrets(r#"cp --target-secret-access-key=SECRETEQMALF "unterminated"#);
assert!(!out.contains("SECRETEQMALF"), "secret leaked: {out}");
assert!(
out.contains("--target-secret-access-key=****"),
"got: {out}"
);
}
}