Skip to main content

nextcloud_auth/secret/
secret_string.rs

1use std::fmt;
2
3/// A string whose contents never appear in logs or `Debug` output.
4///
5/// The type on purpose has no `Display` and no `Serialize` impl: the only way
6/// to read the plaintext is [`SecretString::expose`], which keeps every such
7/// use visible at the call site.
8#[derive(Clone, PartialEq, Eq)]
9pub struct SecretString(String);
10
11impl SecretString {
12    /// Wraps `value` unchanged.
13    pub fn new(value: impl Into<String>) -> Self {
14        Self(value.into())
15    }
16
17    /// Borrows the plaintext.
18    pub fn expose(&self) -> &str {
19        &self.0
20    }
21}
22
23impl fmt::Debug for SecretString {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        f.write_str("SecretString(\"<redacted>\")")
26    }
27}
28
29#[cfg(test)]
30mod tests {
31    use super::SecretString;
32
33    #[test]
34    fn debug_redacts_the_secret() {
35        let secret = SecretString::new("s3cr3t-app-password");
36        let rendered = format!("{secret:?}");
37        assert_eq!(rendered, "SecretString(\"<redacted>\")");
38        assert!(!rendered.contains("s3cr3t-app-password"));
39    }
40
41    #[test]
42    fn expose_returns_the_wrapped_value() {
43        assert_eq!(SecretString::new("value").expose(), "value");
44    }
45
46    #[test]
47    fn equality_compares_the_plaintext() {
48        assert_eq!(SecretString::new("a"), SecretString::new("a"));
49        assert_ne!(SecretString::new("a"), SecretString::new("b"));
50    }
51}