Skip to main content

siloxide_security/
secret.rs

1//! The two secret-bearing values these Features carry.
2//!
3//! Both redact themselves in `Debug` and implement no `Display`, so a secret cannot reach a log
4//! line, a tracing field, or an error message by the usual accident of formatting a struct. The
5//! only way out is [`Password::reveal`] or [`AccessToken::reveal`], which a reader can grep for.
6
7/// A password, as it arrived from a client.
8#[derive(Clone, PartialEq, Eq)]
9pub struct Password(String);
10
11/// An opaque access token.
12///
13/// Siloxide neither parses nor caches it: its shape, scope, expiry, and revocation are the
14/// backend's business, and this type exists to stop the string being handled carelessly on the way
15/// through.
16#[derive(Clone, PartialEq, Eq)]
17pub struct AccessToken(String);
18
19macro_rules! secret {
20    ($name:ident, $redacted:literal) => {
21        impl $name {
22            /// Wrap a secret.
23            #[must_use]
24            pub fn new(value: impl Into<String>) -> Self {
25                Self(value.into())
26            }
27
28            /// The secret itself.
29            ///
30            /// Named so that every place a secret leaves this type is greppable.
31            #[must_use]
32            pub fn reveal(&self) -> &str {
33                &self.0
34            }
35
36            /// The secret itself, consuming the wrapper.
37            #[must_use]
38            pub fn into_revealed(self) -> String {
39                self.0
40            }
41        }
42
43        impl std::fmt::Debug for $name {
44            fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45                formatter.write_str($redacted)
46            }
47        }
48    };
49}
50
51secret!(Password, "Password(redacted)");
52secret!(AccessToken, "AccessToken(redacted)");
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn a_secret_never_formats_itself() {
60        let password = Password::new("hunter2");
61        let token = AccessToken::new("eyJhbGciOi");
62
63        assert_eq!(format!("{password:?}"), "Password(redacted)");
64        assert_eq!(format!("{token:?}"), "AccessToken(redacted)");
65        assert!(!format!("{password:?}{token:?}").contains("hunter2"));
66        assert!(!format!("{password:?}{token:?}").contains("eyJhbGciOi"));
67    }
68
69    #[test]
70    fn a_secret_is_readable_where_it_is_asked_for_by_name() {
71        assert_eq!(Password::new("hunter2").reveal(), "hunter2");
72        assert_eq!(AccessToken::new("t").into_revealed(), "t");
73    }
74}