Skip to main content

dnslib/core/
secret.rs

1use secrecy::{ExposeSecret, SecretString};
2
3/// An API token that cannot be accidentally printed or logged.
4///
5/// `Debug` outputs `ApiToken([REDACTED])`. There is no `Display` impl —
6/// any attempt to format the token as a string is a compile error unless
7/// the caller explicitly calls `expose_for_auth`, making every real exposure
8/// visible and searchable in code review.
9#[derive(Clone)]
10pub struct ApiToken(SecretString);
11
12impl ApiToken {
13    pub fn new(s: impl Into<String>) -> Self {
14        Self(SecretString::from(s.into()))
15    }
16
17    /// Returns the raw token value. Call only at HTTP authentication boundaries.
18    pub fn expose_for_auth(&self) -> &str {
19        self.0.expose_secret()
20    }
21}
22
23impl std::fmt::Debug for ApiToken {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.write_str("ApiToken([REDACTED])")
26    }
27}
28
29impl From<String> for ApiToken {
30    fn from(s: String) -> Self {
31        Self::new(s)
32    }
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38
39    #[test]
40    fn debug_does_not_expose_secret() {
41        let token = ApiToken::new("super-secret");
42        assert_eq!(format!("{token:?}"), "ApiToken([REDACTED])");
43        assert!(!format!("{token:?}").contains("super-secret"));
44    }
45
46    #[test]
47    fn expose_for_auth_returns_raw_value() {
48        let token = ApiToken::new("super-secret");
49        assert_eq!(token.expose_for_auth(), "super-secret");
50    }
51
52    #[test]
53    fn clone_preserves_value() {
54        let token = ApiToken::new("secret");
55        assert_eq!(token.clone().expose_for_auth(), "secret");
56    }
57}