use rand::{Rng, TryRngCore, distr::Alphanumeric, rngs::OsRng};
use crate::error::{CryptoError, Error, Result};
pub fn generate_random_bytes(length: usize) -> Result<Vec<u8>> {
let mut bytes = vec![0u8; length];
OsRng
.try_fill_bytes(&mut bytes)
.map_err(|e| Error::Crypto(CryptoError::RngFailed(format!("{:?}", e))))?;
Ok(bytes)
}
pub fn generate_random_hex(byte_length: usize) -> Result<String> {
let bytes = generate_random_bytes(byte_length)?;
Ok(hex_encode(&bytes))
}
pub fn generate_random_base64_url(byte_length: usize) -> Result<String> {
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
let bytes = generate_random_bytes(byte_length)?;
Ok(URL_SAFE_NO_PAD.encode(&bytes))
}
pub fn generate_random_alphanumeric(length: usize) -> Result<String> {
let token: String = rand::rng()
.sample_iter(Alphanumeric)
.take(length)
.map(char::from)
.collect();
Ok(token)
}
pub fn generate_session_token() -> Result<String> {
generate_random_base64_url(32)
}
pub fn generate_api_key(prefix: &str) -> Result<String> {
let random_part = generate_random_alphanumeric(32)?;
Ok(format!("{}_{}", prefix, random_part))
}
pub fn generate_reset_token() -> Result<String> {
generate_random_hex(32)
}
pub fn generate_recovery_codes(count: usize) -> Result<Vec<String>> {
let mut codes = Vec::with_capacity(count);
const CHARSET: &[u8] = b"23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
for _ in 0..count {
let mut code = String::with_capacity(9);
for i in 0..8 {
if i == 4 {
code.push('-');
}
let idx = rand::rng().random_range(0..CHARSET.len());
code.push(CHARSET[idx] as char);
}
codes.push(code);
}
Ok(codes)
}
pub fn generate_csrf_token() -> Result<String> {
generate_random_base64_url(32)
}
pub fn generate_random_u64() -> u64 {
rand::rng().random()
}
pub fn generate_random_in_range(min: u64, max: u64) -> u64 {
rand::rng().random_range(min..max)
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
pub fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
use subtle::ConstantTimeEq;
a.ct_eq(b).into()
}
pub fn constant_time_compare_str(a: &str, b: &str) -> bool {
constant_time_compare(a.as_bytes(), b.as_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn test_generate_random_bytes() {
let bytes = generate_random_bytes(32).unwrap();
assert_eq!(bytes.len(), 32);
let bytes2 = generate_random_bytes(32).unwrap();
assert_ne!(bytes, bytes2);
}
#[test]
fn test_generate_random_hex() {
let hex = generate_random_hex(16).unwrap();
assert_eq!(hex.len(), 32);
assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_generate_random_base64_url() {
let token = generate_random_base64_url(32).unwrap();
assert!(!token.contains('+'));
assert!(!token.contains('/'));
assert!(!token.contains('='));
}
#[test]
fn test_generate_random_alphanumeric() {
let token = generate_random_alphanumeric(24).unwrap();
assert_eq!(token.len(), 24);
assert!(token.chars().all(|c| c.is_alphanumeric()));
}
#[test]
fn test_generate_session_token() {
let token = generate_session_token().unwrap();
assert!(!token.is_empty());
}
#[test]
fn test_generate_api_key() {
let key = generate_api_key("sk").unwrap();
assert!(key.starts_with("sk_"));
assert_eq!(key.len(), 3 + 32); }
#[test]
fn test_generate_reset_token() {
let token = generate_reset_token().unwrap();
assert_eq!(token.len(), 64);
assert!(token.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_generate_recovery_codes() {
let codes = generate_recovery_codes(10).unwrap();
assert_eq!(codes.len(), 10);
for code in &codes {
assert_eq!(code.len(), 9);
assert_eq!(&code[4..5], "-");
}
let unique: HashSet<_> = codes.iter().collect();
assert_eq!(unique.len(), 10);
}
#[test]
fn test_generate_csrf_token() {
let token = generate_csrf_token().unwrap();
assert!(!token.is_empty());
}
#[test]
fn test_constant_time_compare() {
assert!(constant_time_compare(b"hello", b"hello"));
assert!(!constant_time_compare(b"hello", b"world"));
assert!(!constant_time_compare(b"hello", b"hell"));
}
#[test]
fn test_constant_time_compare_str() {
assert!(constant_time_compare_str("secret", "secret"));
assert!(!constant_time_compare_str("secret", "Secret"));
}
#[test]
fn test_generate_random_u64() {
let a = generate_random_u64();
let b = generate_random_u64();
assert_ne!(a, b);
}
#[test]
fn test_generate_random_in_range() {
for _ in 0..100 {
let val = generate_random_in_range(10, 20);
assert!((10..20).contains(&val));
}
}
#[test]
fn test_hex_encode() {
assert_eq!(hex_encode(&[0x00, 0xff, 0x10]), "00ff10");
assert_eq!(hex_encode(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
}
}