use std::fmt;
use std::sync::{Arc, RwLock};
use subtle::ConstantTimeEq;
use crate::ServeError;
use crate::base32;
use crate::token_policy::TokenPolicy;
const MINTED_ENTROPY_BYTES: usize = 32;
const BEARER_PREFIX: &str = "Bearer ";
pub(crate) struct Credential {
enforced: RwLock<Option<Arc<Enforced>>>,
}
struct Enforced {
token: String,
}
impl Credential {
pub(crate) fn new(policy: &TokenPolicy) -> Result<(Self, Option<String>), ServeError> {
let token = match policy {
TokenPolicy::Generate => Some(mint()),
TokenPolicy::Supplied(t) if !presentable(t) => return Err(ServeError::InvalidToken),
TokenPolicy::Supplied(t) => Some(t.clone()),
TokenPolicy::InsecureNoAuth => None,
};
let cell = Self {
enforced: RwLock::new(token.clone().map(Enforced::new)),
};
Ok((cell, token))
}
pub(crate) fn admits(&self, offered: Option<&[u8]>) -> bool {
let enforced = self.snapshot();
let Some(enforced) = enforced else {
return true; };
let Some(offered) = offered else {
return false;
};
let scheme = BEARER_PREFIX.len();
if offered.len() <= scheme
|| !offered[..scheme].eq_ignore_ascii_case(BEARER_PREFIX.as_bytes())
{
return false;
}
let expected = enforced.token.as_bytes();
let presented = &offered[scheme..];
expected.len() == presented.len() && bool::from(expected.ct_eq(presented))
}
pub(crate) fn token(&self) -> Option<String> {
self.snapshot().map(|e| e.token.clone())
}
pub(crate) fn set(&self, token: String) -> bool {
if !presentable(&token) {
return false;
}
*self.write() = Some(Enforced::new(token));
true
}
pub(crate) fn rotate(&self) -> String {
let token = mint();
assert!(self.set(token.clone()), "a minted token is always usable");
token
}
fn snapshot(&self) -> Option<Arc<Enforced>> {
self.read().clone()
}
fn read(&self) -> std::sync::RwLockReadGuard<'_, Option<Arc<Enforced>>> {
self.enforced
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn write(&self) -> std::sync::RwLockWriteGuard<'_, Option<Arc<Enforced>>> {
self.enforced
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
impl fmt::Debug for Credential {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let state = if self.read().is_some() {
"enforced"
} else {
"open"
};
f.debug_tuple("Credential").field(&state).finish()
}
}
impl Enforced {
fn new(token: String) -> Arc<Self> {
Arc::new(Self { token })
}
}
fn presentable(token: &str) -> bool {
!token.trim().is_empty()
}
fn mint() -> String {
let mut bytes = [0u8; MINTED_ENTROPY_BYTES];
getrandom::fill(&mut bytes).expect("the OS CSPRNG must be available");
base32::encode(&bytes)
}
#[cfg(test)]
#[path = "credential_tests.rs"]
mod credential_tests;