rama_net/user/credentials/
basic.rs1use core::{fmt, str::FromStr};
2
3use crate::user::authority::StaticAuthorizer;
4
5use rama_core::error::BoxErrorExt as _;
6use rama_core::error::{BoxError, ErrorContext as _, ErrorExt};
7use rama_core::extensions::Extension;
8use rama_utils::bytes::ct::ct_eq_bytes;
9use rama_utils::str::NonEmptyStr;
10
11#[derive(Clone, Eq, Extension)]
12#[extension(tags(net))]
13pub struct Basic {
15 username: NonEmptyStr,
16 password: Option<NonEmptyStr>,
17}
18
19impl PartialEq for Basic {
20 fn eq(&self, other: &Self) -> bool {
27 let user_eq = ct_eq_bytes(self.username.as_bytes(), other.username.as_bytes());
28 let pwd_eq = match (&self.password, &other.password) {
29 (Some(a), Some(b)) => ct_eq_bytes(a.as_bytes(), b.as_bytes()),
30 (None, None) => true,
31 (Some(a), None) | (None, Some(a)) => {
34 let _ = ct_eq_bytes(a.as_bytes(), a.as_bytes());
35 false
36 }
37 };
38 user_eq & pwd_eq
39 }
40}
41
42#[macro_export]
48#[doc(hidden)]
49macro_rules! __basic {
50 ($username:expr $(,)?) => {
51 $crate::user::credentials::basic!($username, "")
52 };
53 ($username:expr, $password:expr $(,)?) => {{
54 const __BASIC_USERNAME_VALUE: $crate::__private::utils::str::NonEmptyStr =
55 $crate::__private::utils::str::non_empty_str!($username);
56 const __BASIC_PASSWORD_TEXT: &str = $password;
57
58 if __BASIC_PASSWORD_TEXT.is_empty() {
59 $crate::user::credentials::Basic::new_insecure(__BASIC_USERNAME_VALUE)
60 } else {
61 $crate::user::credentials::Basic::new(
62 __BASIC_USERNAME_VALUE,
63 $crate::__private::utils::str::non_empty_str!(__BASIC_PASSWORD_TEXT),
64 )
65 }
66 }};
67}
68
69#[doc(inline)]
70pub use crate::__basic as basic;
71
72impl fmt::Debug for Basic {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 f.debug_struct("Basic")
75 .field("username", &self.username)
76 .field("password", &"***")
77 .finish()
78 }
79}
80
81impl Basic {
82 #[must_use]
84 pub const fn new(username: NonEmptyStr, password: NonEmptyStr) -> Self {
85 Self {
86 username,
87 password: Some(password),
88 }
89 }
90
91 #[must_use]
92 pub fn clone_with_new_username(&self, username: NonEmptyStr) -> Self {
93 Self {
94 username,
95 password: self.password.clone(),
96 }
97 }
98
99 #[must_use]
100 pub fn clone_with_new_password(&self, password: NonEmptyStr) -> Self {
101 Self {
102 username: self.username.clone(),
103 password: Some(password),
104 }
105 }
106
107 #[must_use]
109 pub const fn new_insecure(username: NonEmptyStr) -> Self {
110 Self {
111 username,
112 password: None,
113 }
114 }
115
116 #[must_use]
118 pub fn username(&self) -> &str {
119 self.username.as_ref()
120 }
121
122 rama_utils::macros::generate_set_and_with! {
123 pub fn username(mut self, username: NonEmptyStr) -> Self {
125 self.username = username;
126 self
127 }
128 }
129
130 #[must_use]
134 pub fn password(&self) -> Option<&str> {
135 self.password.as_deref()
136 }
137
138 rama_utils::macros::generate_set_and_with! {
139 pub fn password(mut self, password: NonEmptyStr) -> Self {
141 self.password = Some(password);
142 self
143 }
144 }
145
146 #[must_use]
150 pub fn into_authorizer(self) -> StaticAuthorizer<Self> {
151 StaticAuthorizer::new(self)
152 }
153}
154
155impl core::hash::Hash for Basic {
156 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
157 self.username().hash(state);
158 ':'.hash(state);
159 self.password().hash(state);
160 }
161}
162
163fn validate_basic_field(field: &str, value: &str) -> Result<(), BoxError> {
175 if let Some(idx) = value
176 .as_bytes()
177 .iter()
178 .position(|b| matches!(*b, b'\r' | b'\n' | 0))
179 {
180 return Err(
181 BoxError::from_static_str("basic credential contains forbidden control byte")
182 .context_str_field("field", field)
183 .context_field("byte_index", idx),
184 );
185 }
186 Ok(())
187}
188
189impl TryFrom<&str> for Basic {
190 type Error = BoxError;
191
192 fn try_from(value: &str) -> Result<Self, Self::Error> {
193 validate_basic_field("credential blob", value)?;
194 match value.find(':') {
195 Some(0) => Err(BoxError::from_static_str(
196 "missing username in basic credential",
197 )),
198 Some(n) => Ok(Self {
199 username: NonEmptyStr::try_from(&value[..n])
200 .context("create username for secure basic credentials")?,
201 password: (n + 1 < value.len())
202 .then(|| {
203 NonEmptyStr::try_from(&value[n + 1..])
204 .context("create password for secure basic credentials")
205 })
206 .transpose()?,
207 }),
208 None => Ok(Self {
209 username: NonEmptyStr::try_from(value)
210 .context("create username for insecure basic credentials")?,
211 password: None,
212 }),
213 }
214 }
215}
216
217impl FromStr for Basic {
218 type Err = BoxError;
219
220 #[inline]
221 fn from_str(s: &str) -> Result<Self, Self::Err> {
222 s.try_into()
223 }
224}
225
226impl fmt::Display for Basic {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 write!(
229 f,
230 "{}:{}",
231 self.username(),
232 self.password().unwrap_or_default()
233 )
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 fn basic(user: &str, pwd: Option<&str>) -> Basic {
242 let username = NonEmptyStr::try_from(user).unwrap();
243 match pwd {
244 Some(p) => Basic::new(username, NonEmptyStr::try_from(p).unwrap()),
245 None => Basic::new_insecure(username),
246 }
247 }
248
249 #[test]
250 fn regression_basic_constant_time_eq() {
251 assert_eq!(
253 basic("alice", Some("hunter2")),
254 basic("alice", Some("hunter2"))
255 );
256 assert_ne!(
258 basic("alice", Some("hunter2")),
259 basic("alice", Some("hunter3"))
260 );
261 assert_ne!(
263 basic("alice", Some("hunter2")),
264 basic("alice", Some("xunter2"))
265 );
266 assert_ne!(basic("alice", Some("hunter2")), basic("alice", None));
268 assert_ne!(basic("alice", None), basic("alice", Some("hunter2")));
269 assert_ne!(
271 basic("alice", Some("hunter2")),
272 basic("bob", Some("hunter2"))
273 );
274 assert_eq!(basic("alice", None), basic("alice", None));
276 assert_ne!(basic("alice", None), basic("bob", None));
277 }
278
279 #[test]
280 fn regression_basic_rejects_crlf_nul() {
281 Basic::try_from("ali\rce:hunter2").unwrap_err();
285 Basic::try_from("ali\nce:hunter2").unwrap_err();
286 Basic::try_from("alice:hun\rter2").unwrap_err();
288 Basic::try_from("alice:hun\nter2").unwrap_err();
289 Basic::try_from("ali\0ce:hunter2").unwrap_err();
291 Basic::try_from("alice:hun\0ter2").unwrap_err();
292 Basic::try_from("alice:hunter2").unwrap();
294 }
295}