mithril_common/entities/
config_secret.rs

1use std::fmt;
2use std::hash::Hash;
3use std::str::FromStr;
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6
7/// A wrapper type for sensitive configuration values that should never be printed.
8///
9/// **IMPORTANT**: This type is only designed for configuration secrets and should not be used for
10/// other purposes.
11/// It does not provide protection against memory inspection, it does not zeroize the memory on drop,
12/// and may be sensible to time-based attacks.
13///
14/// This type implements `Debug` and `Display` to always show `[REDACTED]` instead
15/// of the actual value, preventing accidental exposure in logs or console output.
16///
17/// **Note on Serialization**: When serialized, this type always outputs `"[REDACTED]"`
18/// regardless of the inner value.
19/// Deserialization works normally, parsing the actual value.
20/// This asymmetry is intentional to prevent accidental secret exposure in serialized output.
21pub struct ConfigSecret<T>(T);
22
23impl<T> ConfigSecret<T> {
24    /// Creates a new secret configuration value.
25    pub fn new(value: T) -> Self {
26        Self(value)
27    }
28
29    /// Exposes the inner secret value.
30    ///
31    /// Use this method carefully and only when you actually need the secret value.
32    pub fn expose_secret(&self) -> &T {
33        &self.0
34    }
35
36    /// Consumes the wrapper and returns the inner secret value.
37    pub fn into_inner(self) -> T {
38        self.0
39    }
40}
41
42impl<T> fmt::Debug for ConfigSecret<T> {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.write_str("[REDACTED]")
45    }
46}
47
48impl<T> fmt::Display for ConfigSecret<T> {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        f.write_str("[REDACTED]")
51    }
52}
53
54impl<T: Clone> Clone for ConfigSecret<T> {
55    fn clone(&self) -> Self {
56        Self(self.0.clone())
57    }
58}
59
60impl<T: Default> Default for ConfigSecret<T> {
61    fn default() -> Self {
62        Self(T::default())
63    }
64}
65
66impl<T> From<T> for ConfigSecret<T> {
67    fn from(value: T) -> Self {
68        Self::new(value)
69    }
70}
71
72impl<T: FromStr> FromStr for ConfigSecret<T> {
73    type Err = T::Err;
74
75    fn from_str(s: &str) -> Result<Self, Self::Err> {
76        T::from_str(s).map(Self::new)
77    }
78}
79
80impl<T: PartialEq> PartialEq for ConfigSecret<T> {
81    fn eq(&self, other: &Self) -> bool {
82        self.0.eq(&other.0)
83    }
84}
85
86impl<T: Eq> Eq for ConfigSecret<T> {}
87
88impl<T: Hash> Hash for ConfigSecret<T> {
89    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
90        self.0.hash(state);
91    }
92}
93
94impl<T: Serialize> Serialize for ConfigSecret<T> {
95    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
96    where
97        S: Serializer,
98    {
99        serializer.serialize_str("[REDACTED]")
100    }
101}
102
103impl<'de, T: Deserialize<'de>> Deserialize<'de> for ConfigSecret<T> {
104    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105    where
106        D: Deserializer<'de>,
107    {
108        T::deserialize(deserializer).map(ConfigSecret::new)
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use crate::test::TestLogger;
115
116    use super::*;
117
118    #[test]
119    fn test_debug_redacted_in_display_and_debug() {
120        let secret = ConfigSecret::new("secret");
121        assert_eq!(format!("{secret}"), "[REDACTED]");
122        assert_eq!(format!("{secret:#}"), "[REDACTED]");
123        assert_eq!(format!("{secret:?}"), "[REDACTED]");
124        assert_eq!(format!("{secret:#?}"), "[REDACTED]");
125    }
126
127    #[test]
128    fn test_redacted_in_slog() {
129        let (logger, inspector) = TestLogger::memory();
130        let secret = ConfigSecret::new("0123456789ABCD");
131
132        slog::info!(
133            logger,
134            "log: {secret}, log debug: {secret:?}, log alternate: {secret:#}, log alternate debug: {secret:#?}"
135        );
136        slog::info!(logger, "log in keys";
137            "secret" => %secret, "debug" => ?secret, "alternate" => #%secret, "alternate_debug" => #?secret
138        );
139
140        assert!(!inspector.contains_log(secret.expose_secret()));
141    }
142
143    #[test]
144    fn test_from_str() {
145        let secret: ConfigSecret<String> = "my-secret".parse().unwrap();
146        assert_eq!(secret.expose_secret(), "my-secret");
147    }
148
149    #[test]
150    fn test_serde_serialization() {
151        let secret = ConfigSecret::new("secret");
152        let serialized = serde_json::to_string(&secret).unwrap();
153        assert_eq!(serialized, r#""[REDACTED]""#);
154    }
155
156    #[test]
157    fn test_serde_deserialization() {
158        let secret: ConfigSecret<String> = serde_json::from_str(r#""secret""#).unwrap();
159        assert_eq!(secret, ConfigSecret::new("secret".to_string()));
160
161        #[derive(Deserialize, PartialEq, Debug)]
162        struct Mixed {
163            secret: ConfigSecret<String>,
164            non_secret: String,
165        }
166        let mixed_struct: Mixed =
167            serde_json::from_str(r#"{ "secret": "secret", "non_secret": "non-secret" }"#).unwrap();
168        assert_eq!(
169            mixed_struct,
170            Mixed {
171                secret: ConfigSecret::new("secret".to_string()),
172                non_secret: "non-secret".to_string(),
173            }
174        );
175    }
176}