use core::{fmt, str::FromStr};
use crate::user::authority::StaticAuthorizer;
use rama_core::error::BoxErrorExt as _;
use rama_core::error::{BoxError, ErrorContext as _, ErrorExt};
use rama_core::extensions::Extension;
use rama_utils::bytes::ct::ct_eq_bytes;
use rama_utils::str::NonEmptyStr;
#[derive(Clone, Eq, Extension)]
#[extension(tags(net))]
pub struct Basic {
username: NonEmptyStr,
password: Option<NonEmptyStr>,
}
impl PartialEq for Basic {
fn eq(&self, other: &Self) -> bool {
let user_eq = ct_eq_bytes(self.username.as_bytes(), other.username.as_bytes());
let pwd_eq = match (&self.password, &other.password) {
(Some(a), Some(b)) => ct_eq_bytes(a.as_bytes(), b.as_bytes()),
(None, None) => true,
(Some(a), None) | (None, Some(a)) => {
let _ = ct_eq_bytes(a.as_bytes(), a.as_bytes());
false
}
};
user_eq & pwd_eq
}
}
#[macro_export]
#[doc(hidden)]
macro_rules! __basic {
($username:expr $(,)?) => {
$crate::user::credentials::basic!($username, "")
};
($username:expr, $password:expr $(,)?) => {{
const __BASIC_USERNAME_VALUE: $crate::__private::utils::str::NonEmptyStr =
$crate::__private::utils::str::non_empty_str!($username);
const __BASIC_PASSWORD_TEXT: &str = $password;
if __BASIC_PASSWORD_TEXT.is_empty() {
$crate::user::credentials::Basic::new_insecure(__BASIC_USERNAME_VALUE)
} else {
$crate::user::credentials::Basic::new(
__BASIC_USERNAME_VALUE,
$crate::__private::utils::str::non_empty_str!(__BASIC_PASSWORD_TEXT),
)
}
}};
}
#[doc(inline)]
pub use crate::__basic as basic;
impl fmt::Debug for Basic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Basic")
.field("username", &self.username)
.field("password", &"***")
.finish()
}
}
impl Basic {
#[must_use]
pub const fn new(username: NonEmptyStr, password: NonEmptyStr) -> Self {
Self {
username,
password: Some(password),
}
}
#[must_use]
pub fn clone_with_new_username(&self, username: NonEmptyStr) -> Self {
Self {
username,
password: self.password.clone(),
}
}
#[must_use]
pub fn clone_with_new_password(&self, password: NonEmptyStr) -> Self {
Self {
username: self.username.clone(),
password: Some(password),
}
}
#[must_use]
pub const fn new_insecure(username: NonEmptyStr) -> Self {
Self {
username,
password: None,
}
}
#[must_use]
pub fn username(&self) -> &str {
self.username.as_ref()
}
rama_utils::macros::generate_set_and_with! {
pub fn username(mut self, username: NonEmptyStr) -> Self {
self.username = username;
self
}
}
#[must_use]
pub fn password(&self) -> Option<&str> {
self.password.as_deref()
}
rama_utils::macros::generate_set_and_with! {
pub fn password(mut self, password: NonEmptyStr) -> Self {
self.password = Some(password);
self
}
}
#[must_use]
pub fn into_authorizer(self) -> StaticAuthorizer<Self> {
StaticAuthorizer::new(self)
}
}
impl core::hash::Hash for Basic {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.username().hash(state);
':'.hash(state);
self.password().hash(state);
}
}
fn validate_basic_field(field: &str, value: &str) -> Result<(), BoxError> {
if let Some(idx) = value
.as_bytes()
.iter()
.position(|b| matches!(*b, b'\r' | b'\n' | 0))
{
return Err(
BoxError::from_static_str("basic credential contains forbidden control byte")
.context_str_field("field", field)
.context_field("byte_index", idx),
);
}
Ok(())
}
impl TryFrom<&str> for Basic {
type Error = BoxError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
validate_basic_field("credential blob", value)?;
match value.find(':') {
Some(0) => Err(BoxError::from_static_str(
"missing username in basic credential",
)),
Some(n) => Ok(Self {
username: NonEmptyStr::try_from(&value[..n])
.context("create username for secure basic credentials")?,
password: (n + 1 < value.len())
.then(|| {
NonEmptyStr::try_from(&value[n + 1..])
.context("create password for secure basic credentials")
})
.transpose()?,
}),
None => Ok(Self {
username: NonEmptyStr::try_from(value)
.context("create username for insecure basic credentials")?,
password: None,
}),
}
}
}
impl FromStr for Basic {
type Err = BoxError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.try_into()
}
}
impl fmt::Display for Basic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}:{}",
self.username(),
self.password().unwrap_or_default()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn basic(user: &str, pwd: Option<&str>) -> Basic {
let username = NonEmptyStr::try_from(user).unwrap();
match pwd {
Some(p) => Basic::new(username, NonEmptyStr::try_from(p).unwrap()),
None => Basic::new_insecure(username),
}
}
#[test]
fn regression_basic_constant_time_eq() {
assert_eq!(
basic("alice", Some("hunter2")),
basic("alice", Some("hunter2"))
);
assert_ne!(
basic("alice", Some("hunter2")),
basic("alice", Some("hunter3"))
);
assert_ne!(
basic("alice", Some("hunter2")),
basic("alice", Some("xunter2"))
);
assert_ne!(basic("alice", Some("hunter2")), basic("alice", None));
assert_ne!(basic("alice", None), basic("alice", Some("hunter2")));
assert_ne!(
basic("alice", Some("hunter2")),
basic("bob", Some("hunter2"))
);
assert_eq!(basic("alice", None), basic("alice", None));
assert_ne!(basic("alice", None), basic("bob", None));
}
#[test]
fn regression_basic_rejects_crlf_nul() {
Basic::try_from("ali\rce:hunter2").unwrap_err();
Basic::try_from("ali\nce:hunter2").unwrap_err();
Basic::try_from("alice:hun\rter2").unwrap_err();
Basic::try_from("alice:hun\nter2").unwrap_err();
Basic::try_from("ali\0ce:hunter2").unwrap_err();
Basic::try_from("alice:hun\0ter2").unwrap_err();
Basic::try_from("alice:hunter2").unwrap();
}
}