use std::fmt;
#[derive(Clone, PartialEq, Eq)]
pub(crate) enum Sasl {
Plain {
user: String,
password: String,
},
External,
}
impl Sasl {
pub(crate) fn mechanism(&self) -> &'static str {
match self {
Sasl::Plain { .. } => "PLAIN",
Sasl::External => "EXTERNAL",
}
}
pub(crate) fn response(&self) -> String {
match self {
Sasl::Plain { user, password } => {
base64_encode(format!("\0{user}\0{password}").as_bytes())
}
Sasl::External => String::new(),
}
}
}
impl fmt::Debug for Sasl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Sasl::Plain { user, .. } => f
.debug_struct("Plain")
.field("user", user)
.field("password", &"<redacted>")
.finish(),
Sasl::External => f.write_str("External"),
}
}
}
#[derive(Clone, Default, PartialEq, Eq)]
pub(crate) struct Auth {
pub(crate) password: Option<String>,
pub(crate) sasl: Option<Sasl>,
pub(crate) extra_caps: Vec<String>,
}
impl Auth {
pub(crate) fn wanted_caps(&self) -> Vec<&str> {
let mut caps = Vec::with_capacity(1 + self.extra_caps.len());
if self.sasl.is_some() {
caps.push("sasl");
}
caps.extend(self.extra_caps.iter().map(String::as_str));
caps
}
pub(crate) fn negotiates_caps(&self) -> bool {
!self.wanted_caps().is_empty()
}
}
impl fmt::Debug for Auth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Auth")
.field("password", &self.password.as_ref().map(|_| "<redacted>"))
.field("sasl", &self.sasl)
.field("extra_caps", &self.extra_caps)
.finish()
}
}
const BASE64_ALPHABET: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
fn base64_encode(input: &[u8]) -> String {
let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
for chunk in input.chunks(3) {
let bytes = [
chunk[0],
chunk.get(1).copied().unwrap_or(0),
chunk.get(2).copied().unwrap_or(0),
];
let group = (u32::from(bytes[0]) << 16) | (u32::from(bytes[1]) << 8) | u32::from(bytes[2]);
out.push(char::from(BASE64_ALPHABET[(group >> 18) as usize & 0x3f]));
out.push(char::from(BASE64_ALPHABET[(group >> 12) as usize & 0x3f]));
out.push(if chunk.len() > 1 {
char::from(BASE64_ALPHABET[(group >> 6) as usize & 0x3f])
} else {
'='
});
out.push(if chunk.len() > 2 {
char::from(BASE64_ALPHABET[group as usize & 0x3f])
} else {
'='
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64_encodes_the_rfc_4648_vectors() {
let cases = [
("", ""),
("f", "Zg=="),
("fo", "Zm8="),
("foo", "Zm9v"),
("foob", "Zm9vYg=="),
("fooba", "Zm9vYmE="),
("foobar", "Zm9vYmFy"),
];
for (input, expected) in cases {
assert_eq!(
base64_encode(input.as_bytes()),
expected,
"input: {input:?}"
);
}
}
#[test]
fn base64_encodes_high_bytes() {
assert_eq!(base64_encode(&[0xff, 0xfe, 0xfd]), "//79");
}
#[test]
fn base64_encodes_nul_bytes() {
assert_eq!(base64_encode(b"\0a\0b"), "AGEAYg==");
}
#[test]
fn plain_response_encodes_authzid_authcid_password() {
let sasl = Sasl::Plain {
user: "bot".to_string(),
password: "hunter2".to_string(),
};
assert_eq!(sasl.response(), "AGJvdABodW50ZXIy");
assert_eq!(sasl.mechanism(), "PLAIN");
}
#[test]
fn external_response_is_empty() {
assert_eq!(Sasl::External.response(), "");
assert_eq!(Sasl::External.mechanism(), "EXTERNAL");
}
#[test]
fn debug_redacts_the_sasl_password() {
let sasl = Sasl::Plain {
user: "bot".to_string(),
password: "hunter2".to_string(),
};
let rendered = format!("{sasl:?}");
assert!(!rendered.contains("hunter2"), "password leaked: {rendered}");
assert!(rendered.contains("bot"), "{rendered}");
}
#[test]
fn debug_redacts_the_server_password() {
let auth = Auth {
password: Some("s3cret".to_string()),
..Auth::default()
};
let rendered = format!("{auth:?}");
assert!(!rendered.contains("s3cret"), "password leaked: {rendered}");
}
#[test]
fn no_credentials_means_no_cap_exchange() {
assert!(!Auth::default().negotiates_caps());
assert!(Auth::default().wanted_caps().is_empty());
}
#[test]
fn a_server_password_alone_needs_no_cap_exchange() {
let auth = Auth {
password: Some("s3cret".to_string()),
..Auth::default()
};
assert!(!auth.negotiates_caps());
}
#[test]
fn sasl_requests_the_sasl_capability_first() {
let auth = Auth {
sasl: Some(Sasl::External),
extra_caps: vec!["server-time".to_string()],
..Auth::default()
};
assert_eq!(auth.wanted_caps(), vec!["sasl", "server-time"]);
}
#[test]
fn extra_capabilities_alone_still_negotiate() {
let auth = Auth {
extra_caps: vec!["server-time".to_string()],
..Auth::default()
};
assert!(auth.negotiates_caps());
assert_eq!(auth.wanted_caps(), vec!["server-time"]);
}
}