use rand::{rngs::OsRng, RngCore};
#[derive(Debug, Clone)]
pub struct SessionToken(String);
impl SessionToken {
pub fn generate() -> Self {
let mut bytes = [0_u8; 32];
OsRng.fill_bytes(&mut bytes);
let mut token = String::with_capacity(64);
for byte in bytes {
token.push(hex_digit(byte >> 4));
token.push(hex_digit(byte & 0x0f));
}
Self(token)
}
pub fn value(&self) -> &str {
&self.0
}
pub fn verify(&self, header: &str) -> bool {
let expected = format!("Bearer {}", self.0);
constant_time_eq(header.as_bytes(), expected.as_bytes())
}
}
fn hex_digit(nibble: u8) -> char {
match nibble {
0..=9 => (b'0' + nibble) as char,
10..=15 => (b'a' + (nibble - 10)) as char,
_ => unreachable!("nibble must be in 0..=15"),
}
}
fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
let mut diff = left.len() ^ right.len();
let max_len = left.len().max(right.len());
for index in 0..max_len {
let left_byte = left.get(index).copied().unwrap_or(0);
let right_byte = right.get(index).copied().unwrap_or(0);
diff |= usize::from(left_byte ^ right_byte);
}
diff == 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_non_empty() {
let token = SessionToken::generate();
assert!(!token.value().is_empty());
}
#[test]
fn generate_length_64() {
let token = SessionToken::generate();
assert_eq!(token.value().len(), 64, "expected 64-char hex string, got {:?}", token.value());
}
#[test]
fn generate_is_lowercase_hex() {
let token = SessionToken::generate();
assert!(
token.value().chars().all(|c| matches!(c, '0'..='9' | 'a'..='f')),
"token is not lowercase hex: {:?}",
token.value(),
);
}
#[test]
fn generate_produces_unique_tokens() {
let t1 = SessionToken::generate();
let t2 = SessionToken::generate();
assert_ne!(t1.value(), t2.value(), "two tokens must not be identical");
}
#[test]
fn value_returns_raw_hex() {
let token = SessionToken::generate();
let val = token.value();
assert!(!val.starts_with("Bearer "));
assert!(!val.contains(' '));
}
#[test]
fn verify_correct_bearer_header() {
let token = SessionToken::generate();
let header = format!("Bearer {}", token.value());
assert!(token.verify(&header), "correct header must be accepted");
}
#[test]
fn verify_empty_string_rejected() {
let token = SessionToken::generate();
assert!(!token.verify(""), "empty header must be rejected");
}
#[test]
fn verify_wrong_token_rejected() {
let token = SessionToken::generate();
assert!(!token.verify("Bearer wrongtoken0000000000000000000000000000000000000000000000000000"));
}
#[test]
fn verify_raw_token_without_prefix_rejected() {
let token = SessionToken::generate();
assert!(!token.verify(token.value()), "bare token without 'Bearer ' must be rejected");
}
#[test]
fn verify_lowercase_bearer_rejected() {
let token = SessionToken::generate();
let header = format!("bearer {}", token.value());
assert!(!token.verify(&header), "lowercase 'bearer' prefix must be rejected");
}
#[test]
fn verify_bearer_with_no_token_rejected() {
let token = SessionToken::generate();
assert!(!token.verify("Bearer"), "bare 'Bearer' with no value must be rejected");
}
#[test]
fn verify_bearer_trailing_space_rejected() {
let token = SessionToken::generate();
assert!(!token.verify("Bearer "), "'Bearer ' with empty token must be rejected");
}
#[test]
fn verify_different_session_token_rejected() {
let token1 = SessionToken::generate();
let token2 = SessionToken::generate();
let header = format!("Bearer {}", token2.value());
assert!(!token1.verify(&header), "different session token must be rejected");
}
#[test]
fn verify_single_bit_flip_rejected() {
let token = SessionToken::generate();
let mut bad = format!("Bearer {}", token.value());
let bytes = unsafe { bad.as_bytes_mut() };
bytes[7] ^= 1; let bad = String::from_utf8(bytes.to_vec()).unwrap_or_default();
assert!(!token.verify(&bad), "one-bit-flipped token must be rejected");
}
#[test]
fn verify_does_not_short_circuit_on_wrong_prefix() {
let token = SessionToken::generate();
let fake_header = format!("Hearer {}", token.value());
assert_eq!(fake_header.len(), format!("Bearer {}", token.value()).len());
assert!(!token.verify(&fake_header));
}
}