Skip to main content

wowlab_types/types/
sensitive.rs

1/// An opaque wrapper for sensitive values; `Debug` and `Display` always redact.
2///
3/// ```rust
4/// use wowlab_types::sensitive::Sensitive;
5/// let token = Sensitive::new("my-secret-token".to_string());
6/// assert_eq!(format!("{:?}", token), "Sensitive(\"[REDACTED]\")");
7/// assert_eq!(format!("{}", token), "[REDACTED]");
8/// assert_eq!(token.expose(), "my-secret-token");
9/// ```
10#[derive(Clone, Default, Eq, Hash, PartialEq)]
11pub struct Sensitive<T>(T);
12
13impl<T> Sensitive<T> {
14    /// Wraps `value` as sensitive data.
15    #[must_use]
16    pub fn new(value: T) -> Self {
17        Self(value)
18    }
19
20    /// Explicitly borrows the sensitive value.
21    #[must_use]
22    pub fn expose(&self) -> &T {
23        &self.0
24    }
25
26    /// Explicitly consumes the wrapper and returns the sensitive value.
27    #[must_use]
28    pub fn into_inner(self) -> T {
29        self.0
30    }
31}
32
33impl<T> std::fmt::Debug for Sensitive<T> {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        f.debug_tuple("Sensitive").field(&"[REDACTED]").finish()
36    }
37}
38
39impl<T> std::fmt::Display for Sensitive<T> {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        f.write_str("[REDACTED]")
42    }
43}
44
45impl<T> From<T> for Sensitive<T> {
46    fn from(value: T) -> Self {
47        Self::new(value)
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use googletest::prelude::*;
54
55    use super::*;
56
57    #[gtest]
58    fn debug_redacts_string_inner() -> Result<()> {
59        let secret = Sensitive::new("secret".to_string());
60        let rendered = format!("{secret:?}");
61
62        verify_that!(rendered, eq("Sensitive(\"[REDACTED]\")"))?;
63
64        verify_that!(rendered, not(contains_substring("secret")))
65    }
66
67    #[gtest]
68    fn display_redacts_string_inner() -> Result<()> {
69        let secret = Sensitive::new("secret".to_string());
70        let rendered = format!("{secret}");
71
72        verify_that!(rendered, eq("[REDACTED]"))?;
73
74        verify_that!(rendered, not(contains_substring("secret")))
75    }
76
77    #[gtest]
78    fn clone_and_equality_do_not_require_exposure() -> Result<()> {
79        let token = Sensitive::new("opaque-token".to_string());
80        let cloned = token.clone();
81
82        verify_that!(&cloned, eq(&token))?;
83        verify_that!(
84            format!("{cloned:?}"),
85            not(contains_substring("opaque-token"))
86        )?;
87
88        verify_that!(format!("{cloned}"), not(contains_substring("opaque-token")))
89    }
90
91    #[gtest]
92    fn empty_and_non_jwt_strings_remain_opaque_values() -> Result<()> {
93        let empty = Sensitive::new(String::new());
94        let arbitrary = Sensitive::new("not-a-jwt\nwith-control".to_string());
95
96        verify_that!(empty.expose(), eq(""))?;
97        verify_that!(arbitrary.expose(), eq("not-a-jwt\nwith-control"))?;
98        verify_that!(
99            format!("{arbitrary:?}"),
100            not(contains_substring("not-a-jwt"))
101        )?;
102
103        verify_that!(format!("{arbitrary}"), eq("[REDACTED]"))
104    }
105
106    #[gtest]
107    fn debug_redacts_non_string_inner() -> Result<()> {
108        let secret = Sensitive::new(1234u32);
109        let rendered = format!("{secret:?}");
110
111        verify_that!(rendered, eq("Sensitive(\"[REDACTED]\")"))?;
112
113        verify_that!(rendered, not(contains_substring("1234")))
114    }
115
116    #[gtest]
117    fn expose_returns_inner_ref() -> Result<()> {
118        verify_that!(Sensitive::new(42).expose(), eq(&42))
119    }
120
121    #[gtest]
122    fn into_inner_returns_owned() -> Result<()> {
123        verify_that!(Sensitive::new(42).into_inner(), eq(42))
124    }
125
126    #[gtest]
127    fn from_wraps_value() -> Result<()> {
128        verify_that!(Sensitive::from("x".to_string()).expose(), eq("x"))
129    }
130}