api_keys_simplified/
domain.rs

1use crate::error::ConfigError;
2use crate::validator::KeyStatus;
3use crate::{
4    config::{Environment, KeyConfig, KeyPrefix},
5    error::Result,
6    generator::KeyGenerator,
7    hasher::KeyHasher,
8    secure::SecureString,
9    validator::KeyValidator,
10    ExposeSecret, HashConfig,
11};
12use chrono::{DateTime, Utc};
13use derive_getters::Getters;
14use std::fmt::Debug;
15
16/// ApiKeyManager is storable object
17/// used to generate and verify API keys.
18/// It contains immutable config data necessary
19/// to operate. It does NOT contain ANY sensitive
20/// data.
21#[derive(Clone, Getters)]
22pub struct ApiKeyManagerV0 {
23    #[getter(skip)]
24    generator: KeyGenerator,
25    hasher: KeyHasher,
26    #[getter(skip)]
27    validator: KeyValidator,
28    #[getter(skip)]
29    include_checksum: bool,
30}
31
32// FIXME: Need better naming
33/// Hash can be safely stored as String
34/// in memory without having to worry about
35/// zeroizing. Hashes are not secrets and are meant to be stored.
36#[derive(Debug)]
37pub struct Hash(String);
38#[derive(Debug)]
39pub struct NoHash;
40
41/// Represents a generated API key with its hash.
42///
43/// The key field is stored in a `SecureString` which automatically zeros
44/// its memory on drop, preventing potential memory disclosure.
45#[derive(Debug)]
46pub struct ApiKey<Hash> {
47    key: SecureString,
48    hash: Hash,
49}
50
51impl ApiKeyManagerV0 {
52    pub fn init(
53        prefix: impl Into<String>,
54        config: KeyConfig,
55        hash_config: HashConfig,
56    ) -> std::result::Result<Self, ConfigError> {
57        let include_checksum = *config.checksum_length() != 0;
58        let prefix = KeyPrefix::new(prefix)?;
59        let generator = KeyGenerator::new(prefix, config);
60        let validator = KeyValidator::new(&hash_config, include_checksum)?;
61        let hasher = KeyHasher::new(hash_config);
62
63        Ok(Self {
64            generator,
65            hasher,
66            validator,
67            include_checksum,
68        })
69    }
70
71    pub fn init_default_config(
72        prefix: impl Into<String>,
73    ) -> std::result::Result<Self, ConfigError> {
74        Self::init(prefix, KeyConfig::default(), HashConfig::default())
75    }
76    pub fn init_high_security_config(
77        prefix: impl Into<String>,
78    ) -> std::result::Result<Self, ConfigError> {
79        Self::init(
80            prefix,
81            KeyConfig::high_security(),
82            HashConfig::high_security(),
83        )
84    }
85
86    /// Generates a new API key for the specified environment.
87    ///
88    /// The generated key includes a checksum (if enabled) for fast DoS protection.
89    ///
90    /// # Example
91    ///
92    /// ```rust
93    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, ExposeSecret};
94    /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
95    /// let key = manager.generate(Environment::production())?;
96    /// println!("Key: {}", key.key().expose_secret());
97    /// # Ok::<(), Box<dyn std::error::Error>>(())
98    /// ```
99    pub fn generate(&self, environment: impl Into<Environment>) -> Result<ApiKey<Hash>> {
100        let key = self.generator.generate(environment.into(), None)?;
101        let api_key = ApiKey::new(key).into_hashed(&self.hasher)?;
102
103        Ok(api_key)
104    }
105
106    /// Generates a new API key with an expiration timestamp.
107    ///
108    /// The expiration is embedded in the key itself, making it stateless.
109    /// Keys are automatically rejected after the expiry time without database lookups.
110    ///
111    /// # Use Cases
112    ///
113    /// - Trial keys (7-30 days)
114    /// - Temporary partner access
115    /// - Time-limited API access
116    ///
117    /// # Example
118    ///
119    /// ```rust
120    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment};
121    /// # use chrono::{Utc, Duration};
122    /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
123    /// // Create a 7-day trial key
124    /// let expiry = Utc::now() + Duration::days(7);
125    /// let key = manager.generate_with_expiry(Environment::production(), expiry)?;
126    /// # Ok::<(), Box<dyn std::error::Error>>(())
127    /// ```
128    pub fn generate_with_expiry(
129        &self,
130        environment: impl Into<Environment>,
131        expiry: DateTime<Utc>,
132    ) -> Result<ApiKey<Hash>> {
133        let key = self.generator.generate(environment.into(), Some(expiry))?;
134        let api_key = ApiKey::new(key).into_hashed(&self.hasher)?;
135
136        Ok(api_key)
137    }
138
139    /// Verifies an API key against a stored hash.
140    ///
141    /// Returns `KeyStatus` indicating whether the key is valid or invalid.
142    ///
143    /// # Security Flow
144    ///
145    /// 1. **Checksum validation** (if enabled): Rejects invalid keys in ~20μs
146    /// 2. **Argon2 verification**: Verifies hash for valid checksums (~300ms)
147    /// 3. **Expiry check**: Returns `Invalid` if the key's timestamp has passed
148    ///
149    /// # Returns
150    ///
151    /// - `KeyStatus::Valid` - Key is valid and not expired
152    /// - `KeyStatus::Invalid` - Key is invalid (wrong key, hash mismatch, checksum failed, or expired)
153    ///
154    /// # Note on Revocation
155    ///
156    /// This method does NOT check revocation status. To implement key revocation:
157    /// 1. Mark the hash as revoked in your database
158    /// 2. Check revocation status before calling this method
159    /// 3. Only call `verify()` for non-revoked hashes
160    ///
161    /// # Example
162    ///
163    /// ```rust
164    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, KeyStatus};
165    /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
166    /// # let key = manager.generate(Environment::production()).unwrap();
167    /// match manager.verify(key.key(), key.hash())? {
168    ///     KeyStatus::Valid => { /* grant access */ },
169    ///     KeyStatus::Invalid => { /* reject - wrong key or expired */ },
170    /// }
171    /// # Ok::<(), Box<dyn std::error::Error>>(())
172    /// ```
173    pub fn verify(&self, key: &SecureString, stored_hash: impl AsRef<str>) -> Result<KeyStatus> {
174        if self.include_checksum && !self.verify_checksum(key)? {
175            return Ok(KeyStatus::Invalid);
176        }
177
178        self.validator
179            .verify(key.expose_secret(), stored_hash.as_ref())
180    }
181
182    pub fn verify_checksum(&self, key: &SecureString) -> Result<bool> {
183        self.generator.verify_checksum(key)
184    }
185}
186
187impl<T> ApiKey<T> {
188    /// Returns a reference to the secure API key.
189    ///
190    /// To access the underlying string, use `.expose_secret()` on the returned `SecureString`:
191    ///
192    /// ```rust
193    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, ExposeSecret};
194    /// # let generator = ApiKeyManagerV0::init_default_config("sk").unwrap();
195    /// # let api_key = generator.generate(Environment::production()).unwrap();
196    /// let key_str: &str = api_key.key().expose_secret();
197    /// ```
198    ///
199    /// # Security Note
200    ///
201    /// The key is stored in secure memory that is automatically zeroed on drop.
202    /// Be careful NOT to clone or log the value unnecessarily.
203    pub fn key(&self) -> &SecureString {
204        &self.key
205    }
206}
207
208impl ApiKey<NoHash> {
209    pub fn new(key: SecureString) -> ApiKey<NoHash> {
210        ApiKey { key, hash: NoHash }
211    }
212    pub fn into_hashed(self, hasher: &KeyHasher) -> Result<ApiKey<Hash>> {
213        let hash = hasher.hash(&self.key)?;
214
215        Ok(ApiKey {
216            key: self.key,
217            hash: Hash(hash),
218        })
219    }
220    pub fn into_key(self) -> SecureString {
221        self.key
222    }
223}
224
225impl ApiKey<Hash> {
226    /// Returns hash.
227    /// SECURITY:
228    /// Although it's safe to store hash,
229    /// do NOT make unnecessary clones
230    /// and avoid logging the hash.
231    pub fn hash(&self) -> &str {
232        &self.hash.0
233    }
234    pub fn into_key(self) -> SecureString {
235        self.key
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use crate::{ExposeSecret, SecureStringExt};
243
244    #[test]
245    fn test_full_lifecycle() {
246        let generator = ApiKeyManagerV0::init_default_config("sk").unwrap();
247        let api_key = generator.generate(Environment::production()).unwrap();
248
249        let key_str = api_key.key();
250        let hash_str = api_key.hash();
251
252        assert!(key_str.expose_secret().starts_with("sk-live-"));
253        assert!(hash_str.starts_with("$argon2id$"));
254
255        assert_eq!(
256            generator.verify(key_str, hash_str).unwrap(),
257            KeyStatus::Valid
258        );
259        let wrong_key = SecureString::from("wrong_key".to_string());
260        assert_eq!(
261            generator.verify(&wrong_key, hash_str).unwrap(),
262            KeyStatus::Invalid
263        );
264    }
265
266    #[test]
267    fn test_different_presets() {
268        let balanced_gen = ApiKeyManagerV0::init_default_config("pk").unwrap();
269        let balanced = balanced_gen.generate(Environment::test()).unwrap();
270
271        let high_sec_gen = ApiKeyManagerV0::init_high_security_config("sk").unwrap();
272        let high_sec = high_sec_gen.generate(Environment::Production).unwrap();
273
274        assert!(!balanced.key().is_empty());
275        assert!(high_sec.key().len() > balanced.key().len());
276    }
277
278    #[test]
279    fn test_custom_config() {
280        let config = KeyConfig::new().with_entropy(32).unwrap();
281
282        let generator = ApiKeyManagerV0::init("custom", config, HashConfig::default()).unwrap();
283        let key = generator.generate(Environment::production()).unwrap();
284        assert!(generator.verify_checksum(key.key()).unwrap());
285    }
286}