api_keys_simplified/
domain.rs

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