broadcast_auth/
credentials.rs1#[non_exhaustive]
13#[derive(Clone)]
14pub enum Credentials {
15 Basic {
18 username: String,
20 password: String,
22 },
23 Digest {
26 username: String,
28 password: String,
30 },
31 Bearer {
34 token: String,
36 },
37}
38
39impl PartialEq for Credentials {
44 fn eq(&self, other: &Self) -> bool {
45 use subtle::ConstantTimeEq;
46 let ct =
47 |a: &str, b: &str| a.len() == b.len() && bool::from(a.as_bytes().ct_eq(b.as_bytes()));
48 match (self, other) {
49 (
50 Credentials::Basic {
51 username: ua,
52 password: pa,
53 },
54 Credentials::Basic {
55 username: ub,
56 password: pb,
57 },
58 )
59 | (
60 Credentials::Digest {
61 username: ua,
62 password: pa,
63 },
64 Credentials::Digest {
65 username: ub,
66 password: pb,
67 },
68 ) => ua == ub && ct(pa, pb),
69 (Credentials::Bearer { token: a }, Credentials::Bearer { token: b }) => ct(a, b),
70 _ => false,
71 }
72 }
73}
74
75impl Eq for Credentials {}
76
77impl Drop for Credentials {
80 fn drop(&mut self) {
81 use zeroize::Zeroize;
82 match self {
83 Credentials::Basic { password, .. } | Credentials::Digest { password, .. } => {
84 password.zeroize();
85 }
86 Credentials::Bearer { token } => token.zeroize(),
87 }
88 }
89}
90
91impl Credentials {
92 pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
102 Credentials::Digest {
103 username: username.into(),
104 password: password.into(),
105 }
106 }
107
108 pub fn bearer(token: impl Into<String>) -> Self {
110 Credentials::Bearer {
111 token: token.into(),
112 }
113 }
114
115 pub(crate) fn username_password(&self) -> Option<(&str, &str)> {
118 match self {
119 Credentials::Basic { username, password }
120 | Credentials::Digest { username, password } => Some((username, password)),
121 Credentials::Bearer { .. } => None,
122 }
123 }
124}
125
126impl core::fmt::Debug for Credentials {
130 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
131 match self {
132 Credentials::Basic { username, .. } => f
133 .debug_struct("Credentials::Basic")
134 .field("username", username)
135 .field("password", &"***")
136 .finish(),
137 Credentials::Digest { username, .. } => f
138 .debug_struct("Credentials::Digest")
139 .field("username", username)
140 .field("password", &"***")
141 .finish(),
142 Credentials::Bearer { .. } => f
143 .debug_struct("Credentials::Bearer")
144 .field("token", &"***")
145 .finish(),
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
158 fn equality_is_exact_across_secret_username_and_variant() {
159 let base = Credentials::new("admin", "s3cr3t-password");
160 assert_eq!(base, Credentials::new("admin", "s3cr3t-password"));
161 assert_ne!(base, Credentials::new("admin", "s3cr3t-passworD"));
162 assert_ne!(base, Credentials::new("admin", "s3cr3t-passwor"));
163 assert_ne!(base, Credentials::new("admin2", "s3cr3t-password"));
164 assert_ne!(
165 base,
166 Credentials::Basic {
167 username: "admin".into(),
168 password: "s3cr3t-password".into(),
169 }
170 );
171 assert_eq!(Credentials::bearer("tok"), Credentials::bearer("tok"));
172 assert_ne!(Credentials::bearer("tok"), Credentials::bearer("tol"));
173 assert_ne!(Credentials::bearer("tok"), base);
174 }
175
176 #[test]
180 fn basic_debug_redacts_password_but_keeps_username() {
181 let creds = Credentials::Basic {
182 username: "admin".to_string(),
183 password: "s3cr3t-password".to_string(),
184 };
185 let debug = format!("{creds:?}");
186 assert!(!debug.contains("s3cr3t-password"), "leaked: {debug}");
187 assert!(
188 debug.contains("admin"),
189 "username should be visible: {debug}"
190 );
191 assert!(debug.contains("***"), "expected redaction marker: {debug}");
192 }
193
194 #[test]
195 fn digest_debug_redacts_password_but_keeps_username() {
196 let creds = Credentials::Digest {
197 username: "camera-user".to_string(),
198 password: "hunter2-super-secret".to_string(),
199 };
200 let debug = format!("{creds:?}");
201 assert!(!debug.contains("hunter2-super-secret"), "leaked: {debug}");
202 assert!(
203 debug.contains("camera-user"),
204 "username should be visible: {debug}"
205 );
206 assert!(debug.contains("***"), "expected redaction marker: {debug}");
207 }
208
209 #[test]
210 fn bearer_debug_redacts_token() {
211 let creds = Credentials::bearer("super-secret-bearer-token-xyz");
212 let debug = format!("{creds:?}");
213 assert!(
214 !debug.contains("super-secret-bearer-token-xyz"),
215 "leaked: {debug}"
216 );
217 assert!(debug.contains("***"), "expected redaction marker: {debug}");
218 }
219}