diff --git a/src/token.rs b/src/token.rs new file mode 100644 index 0000000..6f0eccc --- /dev/null +++ b/src/token.rs @@ -0,0 +1,176 @@ +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, + pub expires_at: Option>, + pub auto_rotate: bool, + pub rotation_days: Option, + pub last_rotated: Option>, + pub metadata: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct TokenMetadata { + pub description: Option, + pub service: Option, + pub tags: Vec, +} + +impl TokenData { + pub fn new(value: String, rotation_days: Option) -> 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, 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::().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())); + } +}