use crate::crypto::zeroize::Zeroizing;
use crate::encoding::{base64url_encode, hex_encode};
use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RandomError {
message: String,
}
impl fmt::Display for RandomError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "random: {}", self.message)
}
}
impl std::error::Error for RandomError {}
pub fn fill_random(buf: &mut [u8]) -> Result<(), RandomError> {
use rand_core::RngCore as _;
rand_core::OsRng
.try_fill_bytes(buf)
.map_err(|e| RandomError {
message: format!("OS CSPRNG unavailable: {e}"),
})
}
#[must_use = "this returns the generated token"]
pub fn random_token_hex(byte_len: usize) -> Result<Zeroizing<String>, RandomError> {
let mut buf = vec![0u8; byte_len];
fill_random(&mut buf)?;
let token = hex_encode(&buf);
crate::crypto::zeroize::zeroize(&mut buf);
Ok(Zeroizing::new(token))
}
#[must_use = "this returns the generated token"]
pub fn random_token_base64url(byte_len: usize) -> Result<Zeroizing<String>, RandomError> {
let mut buf = vec![0u8; byte_len];
fill_random(&mut buf)?;
let token = base64url_encode(&buf);
crate::crypto::zeroize::zeroize(&mut buf);
Ok(Zeroizing::new(token))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fill_random_produces_nonzero_bytes() {
let mut buf = [0u8; 32];
fill_random(&mut buf).unwrap();
assert!(
buf.iter().any(|&b| b != 0),
"buffer should not be all zeros"
);
}
#[test]
fn fill_random_different_each_call() {
let mut a = [0u8; 32];
let mut b = [0u8; 32];
fill_random(&mut a).unwrap();
fill_random(&mut b).unwrap();
assert_ne!(a, b, "two random fills should produce different output");
}
#[test]
fn fill_random_empty_buffer_succeeds() {
let mut buf = [];
fill_random(&mut buf).unwrap();
}
#[test]
fn random_token_hex_correct_length() {
for byte_len in [0, 1, 8, 16, 32, 64] {
let token = random_token_hex(byte_len).unwrap();
assert_eq!(
token.len(),
byte_len * 2,
"hex token length should be byte_len * 2 for byte_len={byte_len}",
);
}
}
#[test]
fn random_token_base64url_not_empty() {
let token = random_token_base64url(32).unwrap();
assert!(!token.is_empty(), "base64url token should not be empty");
assert!(
token
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
"base64url token should contain only URL-safe characters: {}",
&*token,
);
}
#[test]
fn random_token_hex_different_each_call() {
let a = random_token_hex(32).unwrap();
let b = random_token_hex(32).unwrap();
assert_ne!(&*a, &*b, "two hex tokens should differ");
}
}