ggen_auth/
api_keys.rs

1//! API key management
2
3use chrono::{DateTime, Duration, Utc};
4use hex;
5use sha2::{Digest, Sha256};
6use uuid::Uuid;
7
8/// API key hash for storage
9#[derive(Debug, Clone)]
10pub struct ApiKeyHash {
11    pub id: String,
12    pub hash: String,
13    pub name: String,
14    pub created_at: DateTime<Utc>,
15    pub expires_at: Option<DateTime<Utc>>,
16    pub last_used: Option<DateTime<Utc>>,
17    pub active: bool,
18}
19
20/// API key manager
21pub struct ApiKeyManager {
22    secret_salt: String,
23}
24
25impl ApiKeyManager {
26    /// Create a new API key manager
27    pub fn new(secret_salt: String) -> Self {
28        Self { secret_salt }
29    }
30
31    /// Generate a new API key
32    pub fn generate_key(&self) -> (String, String) {
33        let key = format!("ggen_{}", Uuid::new_v4().to_string().replace("-", ""));
34        let hash = self.hash_key(&key);
35        (key, hash)
36    }
37
38    /// Hash an API key for storage
39    pub fn hash_key(&self, key: &str) -> String {
40        let mut hasher = Sha256::new();
41        hasher.update(format!("{}{}", key, self.secret_salt).as_bytes());
42        hex::encode(hasher.finalize())
43    }
44
45    /// Verify an API key against a hash
46    pub fn verify_key(&self, key: &str, hash: &str) -> bool {
47        let computed_hash = self.hash_key(key);
48        // Constant-time comparison
49        computed_hash.as_bytes().len() == hash.len()
50            && computed_hash
51                .as_bytes()
52                .iter()
53                .zip(hash.as_bytes().iter())
54                .all(|(a, b)| a == b)
55    }
56
57    /// Check if a key is expired
58    pub fn is_expired(key_hash: &ApiKeyHash) -> bool {
59        if let Some(expires_at) = key_hash.expires_at {
60            expires_at < Utc::now()
61        } else {
62            false
63        }
64    }
65
66    /// Check if a key is valid (active and not expired)
67    pub fn is_valid(key_hash: &ApiKeyHash) -> bool {
68        key_hash.active && !Self::is_expired(key_hash)
69    }
70
71    /// Create an API key hash entry
72    pub fn create_hash(
73        &self,
74        key: &str,
75        name: String,
76        expires_in_days: Option<u32>,
77    ) -> ApiKeyHash {
78        let expires_at = expires_in_days.map(|days| {
79            Utc::now() + Duration::days(days as i64)
80        });
81
82        ApiKeyHash {
83            id: Uuid::new_v4().to_string(),
84            hash: self.hash_key(key),
85            name,
86            created_at: Utc::now(),
87            expires_at,
88            last_used: None,
89            active: true,
90        }
91    }
92
93    /// Revoke an API key
94    pub fn revoke(&self, key_hash: &mut ApiKeyHash) {
95        key_hash.active = false;
96    }
97
98    /// Record key usage
99    pub fn record_usage(key_hash: &mut ApiKeyHash) {
100        key_hash.last_used = Some(Utc::now());
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_generate_and_verify_key() {
110        let manager = ApiKeyManager::new("salt".to_string());
111        let (key, _hash) = manager.generate_key();
112        assert!(key.starts_with("ggen_"));
113    }
114
115    #[test]
116    fn test_hash_verification() {
117        let manager = ApiKeyManager::new("salt".to_string());
118        let key = "test_key";
119        let hash = manager.hash_key(key);
120        assert!(manager.verify_key(key, &hash));
121    }
122
123    #[test]
124    fn test_key_expiration() {
125        let manager = ApiKeyManager::new("salt".to_string());
126        let key = "test";
127        let mut key_hash = manager.create_hash(key, "test".to_string(), Some(0));
128        assert!(ApiKeyManager::is_expired(&key_hash));
129    }
130}