Skip to main content

subx_cli/config/
masking.rs

1//! Utilities for masking sensitive configuration values for display.
2//!
3//! Configuration values such as API keys, tokens, and secrets must never be
4//! printed in their raw form to terminal output or log files. This module
5//! provides a single helper, [`mask_sensitive_value`], that inspects the
6//! configuration key and, when it refers to a sensitive item, replaces the
7//! value with a safe masked representation.
8
9/// Return a display-safe representation of a configuration value.
10///
11/// If `key` (case-insensitive) contains any of the sensitive markers
12/// (`api_key`, `token`, `secret`), the value is replaced with a masked form:
13///
14/// * Values of 4 characters or fewer are fully masked as `****`.
15/// * Longer values keep their last 4 characters prefixed by `****` to help
16///   users identify which key is configured without leaking it.
17///
18/// Non-sensitive keys return the value unchanged.
19pub fn mask_sensitive_value(key: &str, value: &str) -> String {
20    let key_lower = key.to_lowercase();
21    let is_sensitive = key_lower.contains("api_key")
22        || key_lower.contains("token")
23        || key_lower.contains("secret");
24
25    if !is_sensitive {
26        return value.to_string();
27    }
28
29    if value.is_empty() {
30        return String::new();
31    }
32
33    if value.chars().count() <= 4 {
34        "****".to_string()
35    } else {
36        let tail: String = value
37            .chars()
38            .rev()
39            .take(4)
40            .collect::<Vec<_>>()
41            .into_iter()
42            .rev()
43            .collect();
44        format!("****{tail}")
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn non_sensitive_key_returns_value_unchanged() {
54        assert_eq!(mask_sensitive_value("ai.model", "gpt-4"), "gpt-4");
55        assert_eq!(mask_sensitive_value("formats.default_output", "srt"), "srt");
56    }
57
58    #[test]
59    fn short_sensitive_value_is_fully_masked() {
60        assert_eq!(mask_sensitive_value("ai.api_key", "abcd"), "****");
61        assert_eq!(mask_sensitive_value("ai.api_key", "a"), "****");
62    }
63
64    #[test]
65    fn long_sensitive_value_preserves_last_four() {
66        assert_eq!(
67            mask_sensitive_value("ai.api_key", "sk-1234567890abcdef"),
68            "****cdef"
69        );
70    }
71
72    #[test]
73    fn empty_sensitive_value_stays_empty() {
74        assert_eq!(mask_sensitive_value("ai.api_key", ""), "");
75    }
76
77    #[test]
78    fn matching_is_case_insensitive_and_substring() {
79        assert_eq!(mask_sensitive_value("AI.API_KEY", "abcdefgh"), "****efgh");
80        assert_eq!(
81            mask_sensitive_value("auth.access_token", "tokenvalue12345"),
82            "****2345"
83        );
84        assert_eq!(
85            mask_sensitive_value("client.secret", "supersecretvalue"),
86            "****alue"
87        );
88    }
89}