177 lines
5.0 KiB
Rust
177 lines
5.0 KiB
Rust
use chrono::{DateTime, Utc};
|
|
use rand::Rng;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
pub struct TokenData {
|
|
pub value: String,
|
|
pub created_at: DateTime<Utc>,
|
|
pub expires_at: Option<DateTime<Utc>>,
|
|
pub auto_rotate: bool,
|
|
pub rotation_days: Option<u32>,
|
|
pub last_rotated: Option<DateTime<Utc>>,
|
|
pub metadata: Option<TokenMetadata>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
pub struct TokenMetadata {
|
|
pub description: Option<String>,
|
|
pub service: Option<String>,
|
|
pub tags: Vec<String>,
|
|
}
|
|
|
|
impl TokenData {
|
|
pub fn new(value: String, rotation_days: Option<u32>) -> Self {
|
|
let created_at = Utc::now();
|
|
let expires_at = rotation_days.map(|days| created_at + chrono::Duration::days(days as i64));
|
|
|
|
TokenData {
|
|
value,
|
|
created_at,
|
|
expires_at,
|
|
auto_rotate: rotation_days.is_some(),
|
|
rotation_days,
|
|
last_rotated: None,
|
|
metadata: None,
|
|
}
|
|
}
|
|
|
|
pub fn is_expired(&self) -> bool {
|
|
if let Some(expires_at) = self.expires_at {
|
|
Utc::now() > expires_at
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn should_rotate(&self) -> bool {
|
|
self.auto_rotate && self.is_expired()
|
|
}
|
|
}
|
|
|
|
pub struct TokenGenerator;
|
|
|
|
impl TokenGenerator {
|
|
const CHARSET_ALPHA: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
const CHARSET_ALPHANUMERIC: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
const CHARSET_BASE64: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
|
|
pub fn generate(length: usize, charset: TokenCharset) -> String {
|
|
let charset_bytes = match charset {
|
|
TokenCharset::Alpha => Self::CHARSET_ALPHA,
|
|
TokenCharset::Alphanumeric => Self::CHARSET_ALPHANUMERIC,
|
|
TokenCharset::Base64 => Self::CHARSET_BASE64,
|
|
TokenCharset::Hex => b"0123456789abcdef",
|
|
};
|
|
|
|
let mut rng = rand::thread_rng();
|
|
let chars_len = charset_bytes.len();
|
|
|
|
(0..length)
|
|
.map(|_| {
|
|
let idx = rng.gen_range(0..chars_len);
|
|
charset_bytes[idx] as char
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn generate_secure(length: usize) -> String {
|
|
let mut bytes = vec![0u8; length];
|
|
rand::Rng::fill(&mut rand::thread_rng(), &mut bytes);
|
|
|
|
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes)
|
|
.trim_end_matches('=')
|
|
.to_string()
|
|
}
|
|
|
|
pub fn generate_uuid() -> String {
|
|
Uuid::new_v4().to_string()
|
|
}
|
|
|
|
pub fn generate_api_key(prefix: &str, length: usize) -> String {
|
|
let token = Self::generate_secure(length);
|
|
format!("{}_{}", prefix, token)
|
|
}
|
|
|
|
pub fn generate_hex(length: usize) -> String {
|
|
Self::generate(length, TokenCharset::Hex)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum TokenCharset {
|
|
Alpha,
|
|
Alphanumeric,
|
|
Base64,
|
|
Hex,
|
|
}
|
|
|
|
pub fn generate_token_value(length: Option<usize>, include_special: bool) -> String {
|
|
let length = length.unwrap_or(32);
|
|
|
|
if include_special {
|
|
generate_secure_token_with_special(length)
|
|
} else {
|
|
TokenGenerator::generate_secure(length)
|
|
}
|
|
}
|
|
|
|
fn generate_secure_token_with_special(length: usize) -> String {
|
|
let special_chars = b"!@#$%^&*()_+-=[]{}|;:,.<>?";
|
|
let base_chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
|
|
let mut rng = rand::thread_rng();
|
|
let mut result = String::with_capacity(length);
|
|
|
|
for i in 0..length {
|
|
if i % 4 == 3 && length - i > 2 {
|
|
let idx = rng.gen_range(0..special_chars.len());
|
|
result.push(special_chars[idx] as char);
|
|
} else {
|
|
let idx = rng.gen_range(0..base_chars.len());
|
|
result.push(base_chars[idx] as char);
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_token_generation() {
|
|
let token = TokenGenerator::generate(32, TokenCharset::Alphanumeric);
|
|
assert_eq!(token.len(), 32);
|
|
}
|
|
|
|
#[test]
|
|
fn test_secure_token_generation() {
|
|
let token = TokenGenerator::generate_secure(32);
|
|
assert!(!token.is_empty());
|
|
assert!(token.len() >= 32);
|
|
}
|
|
|
|
#[test]
|
|
fn test_uuid_generation() {
|
|
let uuid1 = TokenGenerator::generate_uuid();
|
|
let uuid2 = TokenGenerator::generate_uuid();
|
|
assert_ne!(uuid1, uuid2);
|
|
assert!(uuid1.parse::<uuid::Uuid>().is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_token_data_expiration() {
|
|
let token_data = TokenData::new("test".to_string(), Some(30));
|
|
assert!(!token_data.is_expired());
|
|
}
|
|
|
|
#[test]
|
|
fn test_generate_hex() {
|
|
let hex = TokenGenerator::generate_hex(32);
|
|
assert_eq!(hex.len(), 32);
|
|
assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
|
|
}
|
|
}
|