siloxide_security/
secret.rs1#[derive(Clone, PartialEq, Eq)]
9pub struct Password(String);
10
11#[derive(Clone, PartialEq, Eq)]
17pub struct AccessToken(String);
18
19macro_rules! secret {
20 ($name:ident, $redacted:literal) => {
21 impl $name {
22 #[must_use]
24 pub fn new(value: impl Into<String>) -> Self {
25 Self(value.into())
26 }
27
28 #[must_use]
32 pub fn reveal(&self) -> &str {
33 &self.0
34 }
35
36 #[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}