use std::fmt;
use std::net::SocketAddr;
use openmoat_core::Taint;
use zeroize::Zeroizing;
pub const RULE_SESSION: &str = "proxy-session";
pub const MIN_TOKEN_LEN: usize = 32;
const MAX_SESSION_LEN: usize = 128;
pub struct Credential {
session: String,
token: Zeroizing<Vec<u8>>,
}
impl fmt::Debug for Credential {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Credential")
.field("session", &self.session)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum CredentialError {
#[error("a session id is 1 to 128 ASCII letters, digits, `.`, `_` or `-`")]
Session,
#[error("a session token is at least 32 ASCII letters, digits, `.`, `_` or `-`")]
Token,
}
fn plain(bytes: &[u8]) -> bool {
bytes
.iter()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
}
impl Credential {
pub fn new(session: &str, token: &[u8]) -> Result<Self, CredentialError> {
if session.is_empty() || session.len() > MAX_SESSION_LEN || !plain(session.as_bytes()) {
return Err(CredentialError::Session);
}
if token.len() < MIN_TOKEN_LEN || !plain(token) {
return Err(CredentialError::Token);
}
Ok(Self {
session: session.to_owned(),
token: Zeroizing::new(token.to_vec()),
})
}
#[must_use]
pub fn session(&self) -> &str {
&self.session
}
#[must_use]
pub fn proxy_url(&self, addr: SocketAddr) -> Zeroizing<String> {
let token = String::from_utf8_lossy(&self.token);
Zeroizing::new(format!("http://{}:{token}@{addr}", self.session))
}
}
pub trait TaintLookup: Send + Sync {
fn taint(&self, session: &str) -> Result<Taint, String>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum AuthError {
#[error("this proxy serves sessions only: the request carries no session credential")]
Missing,
#[error("the session credential is malformed")]
Malformed,
#[error("the session credential is not valid")]
Invalid,
}
pub struct Sessions<'a> {
credentials: Vec<Credential>,
taint: &'a dyn TaintLookup,
}
impl fmt::Debug for Sessions<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Sessions")
.field("credentials", &self.credentials)
.finish_non_exhaustive()
}
}
impl<'a> Sessions<'a> {
#[must_use]
pub fn new(credentials: Vec<Credential>, taint: &'a dyn TaintLookup) -> Self {
Self { credentials, taint }
}
pub fn verify<T: AsRef<[u8]>>(&self, headers: &[T]) -> Result<&str, AuthError> {
let value = match headers {
[] => return Err(AuthError::Missing),
[one] => one.as_ref(),
_ => return Err(AuthError::Malformed),
};
let decoded = basic(value).ok_or(AuthError::Malformed)?;
let colon = decoded
.iter()
.position(|b| *b == b':')
.ok_or(AuthError::Malformed)?;
let (session, token) = (&decoded[..colon], &decoded[colon + 1..]);
let mut found = None;
for credential in &self.credentials {
let id = constant_time_eq(credential.session.as_bytes(), session);
let secret = constant_time_eq(&credential.token, token);
if id & secret {
found = Some(credential.session.as_str());
}
}
found.ok_or(AuthError::Invalid)
}
pub fn taint(&self, session: &str) -> Result<Taint, String> {
self.taint.taint(session)
}
}
fn basic(value: &[u8]) -> Option<Zeroizing<Vec<u8>>> {
let value = value.trim_ascii();
let space = value.iter().position(u8::is_ascii_whitespace)?;
let (scheme, rest) = value.split_at(space);
if !scheme.eq_ignore_ascii_case(b"basic") {
return None;
}
base64(rest.trim_ascii())
}
#[must_use]
pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let diff = a
.iter()
.zip(b)
.fold(0u8, |acc, (x, y)| acc | std::hint::black_box(x ^ y));
std::hint::black_box(diff) == 0
}
fn base64(input: &[u8]) -> Option<Zeroizing<Vec<u8>>> {
let data = input
.strip_suffix(b"==")
.or_else(|| input.strip_suffix(b"="))
.unwrap_or(input);
let padded = data.len() != input.len();
if data.is_empty() || data.len() % 4 == 1 || (padded && !input.len().is_multiple_of(4)) {
return None;
}
let mut out = Zeroizing::new(Vec::with_capacity(data.len() / 4 * 3 + 2));
let mut acc: u32 = 0;
let mut bits = 0u32;
for &c in data {
let v = match c {
b'A'..=b'Z' => c - b'A',
b'a'..=b'z' => c - b'a' + 26,
b'0'..=b'9' => c - b'0' + 52,
b'+' => 62,
b'/' => 63,
_ => return None,
};
acc = (acc << 6) | u32::from(v);
bits += 6;
if bits >= 8 {
bits -= 8;
out.push(u8::try_from((acc >> bits) & 0xff).ok()?);
acc &= (1 << bits) - 1;
}
}
Some(out)
}
#[cfg(test)]
pub(crate) mod tests;