Skip to main content

omni_dev/utils/
secret.rs

1//! Redacting wrapper for secret string values.
2//!
3//! [`Secret`] holds sensitive credential material (API tokens, session
4//! tokens) and guarantees it cannot leak through `Debug` formatting: the
5//! hand-written `Debug` impl prints `<redacted>` instead of the value.
6//! The type deliberately implements neither `Display` nor any serde
7//! traits, so a secret cannot be `{}`-formatted or accidentally
8//! serialized; the only way out is an explicit, greppable
9//! [`expose_secret`](Secret::expose_secret) call.
10
11use std::fmt;
12
13/// A string secret that redacts itself in `Debug` output.
14///
15/// Wrapping a credential field in `Secret` makes a containing struct's
16/// derived `Debug` print `<redacted>` for that field, so a future
17/// `tracing::debug!("{creds:?}")` or `.context(format!("… {creds:?}"))`
18/// cannot leak the value into logs or error chains.
19///
20/// `Secret` implements neither `Display` nor serde traits by design; the
21/// wrapped value is only reachable through
22/// [`expose_secret`](Self::expose_secret).
23///
24/// # Examples
25///
26/// ```
27/// use omni_dev::utils::secret::Secret;
28///
29/// let token = Secret::new("s3cr3t");
30/// assert_eq!(format!("{token:?}"), "<redacted>");
31/// assert_eq!(token.expose_secret(), "s3cr3t");
32/// ```
33// PartialEq is derived (not constant-time): fine for test/config equality,
34// never use it as an authentication check.
35#[derive(Clone, PartialEq, Eq)]
36pub struct Secret(String);
37
38impl Secret {
39    /// Creates a `Secret` wrapping the given value.
40    #[must_use]
41    pub fn new(value: impl Into<String>) -> Self {
42        Self(value.into())
43    }
44
45    /// Returns the wrapped secret value.
46    ///
47    /// The name is deliberately loud: every call site is an auditable
48    /// point where the secret leaves the redacting wrapper.
49    #[must_use]
50    pub fn expose_secret(&self) -> &str {
51        &self.0
52    }
53}
54
55impl fmt::Debug for Secret {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        f.write_str("<redacted>")
58    }
59}
60
61impl From<String> for Secret {
62    fn from(value: String) -> Self {
63        Self(value)
64    }
65}
66
67impl From<&str> for Secret {
68    fn from(value: &str) -> Self {
69        Self(value.to_string())
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn debug_output_is_redacted() {
79        let secret = Secret::new("super-sekret-value");
80        let debug = format!("{secret:?}");
81        assert_eq!(debug, "<redacted>");
82        assert!(!debug.contains("super-sekret-value"));
83    }
84
85    #[test]
86    fn debug_redacts_inside_derived_container() {
87        #[derive(Debug)]
88        struct Holder {
89            // Read only through the derived Debug impl, which dead-code
90            // analysis intentionally ignores.
91            #[allow(dead_code)]
92            token: Secret,
93        }
94        let holder = Holder {
95            token: "super-sekret-value".into(),
96        };
97        let debug = format!("{holder:?}");
98        assert!(debug.contains("token: <redacted>"));
99        assert!(!debug.contains("super-sekret-value"));
100    }
101
102    #[test]
103    fn expose_secret_returns_the_wrapped_value() {
104        assert_eq!(Secret::new("v").expose_secret(), "v");
105    }
106
107    #[test]
108    fn from_string_and_str_compare_equal() {
109        let a: Secret = "tok".into();
110        let b: Secret = String::from("tok").into();
111        assert_eq!(a, b);
112        assert_eq!(a.clone(), a);
113    }
114}