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/// Contains the Argon2 hash and the salt used to generate it.
36///
37/// The hash can be safely stored in your database without special security measures
38/// since it's already cryptographically hashed. However, avoid unnecessary cloning
39/// or logging to minimize exposure.
40///
41/// # Fields
42///
43/// - `hash`: The Argon2id PHC-formatted hash string (e.g., "$argon2id$v=19$m=...")
44/// - `salt`: The base64-encoded salt used during hashing (32 bytes when encoded)
45///
46/// Both fields can be accessed using the auto-generated getter methods `hash()` and `salt()`
47/// provided by the `Getters` derive macro.
48#[derive(Debug, Getters, PartialEq)]
49pub struct Hash {
50    hash: String,
51    salt: String,
52}
53
54#[derive(Debug)]
55pub struct NoHash;
56
57/// Represents a generated API key with its hash.
58///
59/// The key field is stored in a `SecureString` which automatically zeros
60/// its memory on drop, preventing potential memory disclosure.
61#[derive(Debug)]
62pub struct ApiKey<Hash> {
63    key: SecureString,
64    hash: Hash,
65}
66
67impl ApiKeyManagerV0 {
68    pub fn init(
69        prefix: impl Into<String>,
70        config: KeyConfig,
71        hash_config: HashConfig,
72        expiry_grace_period: std::time::Duration,
73    ) -> std::result::Result<Self, InitError> {
74        let include_checksum = *config.checksum_length() != 0;
75        let prefix = KeyPrefix::new(prefix)?;
76        let generator = KeyGenerator::new(prefix, config)?;
77        let hasher = KeyHasher::new(hash_config);
78
79        // Generate dummy key and its hash for timing attack protection
80        let dummy_key = generator.dummy_key().clone();
81        let (dummy_hash, _salt) = hasher.hash(&dummy_key)?;
82
83        let validator = KeyValidator::new(include_checksum, dummy_key, dummy_hash)?;
84
85        Ok(Self {
86            generator,
87            hasher,
88            validator,
89            include_checksum,
90            expiry_grace_period,
91        })
92    }
93
94    pub fn init_default_config(prefix: impl Into<String>) -> std::result::Result<Self, InitError> {
95        Self::init(
96            prefix,
97            KeyConfig::default(),
98            HashConfig::default(),
99            std::time::Duration::from_secs(10),
100        )
101    }
102    pub fn init_high_security_config(
103        prefix: impl Into<String>,
104    ) -> std::result::Result<Self, InitError> {
105        Self::init(
106            prefix,
107            KeyConfig::high_security(),
108            HashConfig::high_security(),
109            std::time::Duration::from_secs(10),
110        )
111    }
112
113    /// Generates a new API key for the specified environment.
114    ///
115    /// The generated key includes a checksum (if enabled) for fast DoS protection.
116    ///
117    /// # Example
118    ///
119    /// ```rust
120    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, ExposeSecret};
121    /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
122    /// let key = manager.generate(Environment::production())?;
123    /// println!("Key: {}", key.key().expose_secret());
124    /// # Ok::<(), Box<dyn std::error::Error>>(())
125    /// ```
126    pub fn generate(&self, environment: impl Into<Environment>) -> Result<ApiKey<Hash>> {
127        let key = self.generator.generate(environment.into(), None)?;
128        let api_key = ApiKey::new(key).into_hashed(&self.hasher)?;
129
130        Ok(api_key)
131    }
132
133    /// Generates a new API key with an expiration timestamp.
134    ///
135    /// The expiration is embedded in the key itself, making it stateless.
136    /// Keys are automatically rejected after the expiry time without database lookups.
137    ///
138    /// # Use Cases
139    ///
140    /// - Trial keys (7-30 days)
141    /// - Temporary partner access
142    /// - Time-limited API access
143    ///
144    /// # Example
145    ///
146    /// ```rust
147    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment};
148    /// # use chrono::{Utc, Duration};
149    /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
150    /// // Create a 7-day trial key
151    /// let expiry = Utc::now() + Duration::days(7);
152    /// let key = manager.generate_with_expiry(Environment::production(), expiry)?;
153    /// # Ok::<(), Box<dyn std::error::Error>>(())
154    /// ```
155    pub fn generate_with_expiry(
156        &self,
157        environment: impl Into<Environment>,
158        expiry: DateTime<Utc>,
159    ) -> Result<ApiKey<Hash>> {
160        let key = self.generator.generate(environment.into(), Some(expiry))?;
161        let api_key = ApiKey::new(key).into_hashed(&self.hasher)?;
162
163        Ok(api_key)
164    }
165
166    /// Verifies an API key against a stored hash.
167    ///
168    /// Returns `KeyStatus` indicating whether the key is valid or invalid.
169    ///
170    /// # Parameters
171    ///
172    /// * `key` - The API key to verify
173    /// * `stored_hash` - The Argon2 hash stored in your database
174    /// * `expiry_grace_period` - Optional grace period duration after expiry.
175    ///   - `None`: Skip expiry validation (all keys treated as non-expired)
176    ///   - `Some(Duration::ZERO)`: Strict expiry check (no grace period)
177    ///   - `Some(duration)`: Key remains valid for `duration` after its expiry time
178    ///
179    /// The grace period protects against clock skew issues. Once a key expires beyond
180    /// the grace period, it stays expired even if the system clock goes backwards.
181    ///
182    /// # Security Flow
183    ///
184    /// 1. **Checksum validation** (if enabled): Rejects invalid keys in ~20μs
185    /// 2. **Argon2 verification**: Verifies hash for valid checksums (~300ms)
186    /// 3. **Expiry check**: Returns `Invalid` if expired beyond grace period
187    ///
188    /// # Returns
189    ///
190    /// - `KeyStatus::Valid` - Key is valid and not expired
191    /// - `KeyStatus::Invalid` - Key is invalid (wrong key, hash mismatch, checksum failed, or expired)
192    ///
193    /// # Note on Revocation
194    ///
195    /// This method does NOT check revocation status. To implement key revocation:
196    /// 1. Mark the hash as revoked in your database
197    /// 2. Check revocation status before calling this method
198    /// 3. Only call `verify()` for non-revoked hashes
199    ///
200    /// # Example
201    ///
202    /// ```rust
203    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, KeyStatus};
204    /// # use std::time::Duration;
205    /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
206    /// # let key = manager.generate(Environment::production()).unwrap();
207    /// match manager.verify(key.key(), key.expose_hash().hash())? {
208    ///     KeyStatus::Valid => { /* grant access */ },
209    ///     KeyStatus::Invalid => { /* reject - wrong key or expired */ },
210    /// }
211    /// # Ok::<(), Box<dyn std::error::Error>>(())
212    /// ```
213    pub fn verify(&self, key: &SecureString, stored_hash: impl AsRef<str>) -> Result<KeyStatus> {
214        if self.include_checksum && !self.verify_checksum(key)? {
215            return Ok(KeyStatus::Invalid);
216        }
217
218        self.validator.verify(
219            key.expose_secret(),
220            stored_hash.as_ref(),
221            self.expiry_grace_period,
222        )
223    }
224
225    pub fn verify_checksum(&self, key: &SecureString) -> Result<bool> {
226        self.generator.verify_checksum(key)
227    }
228}
229
230impl<T> ApiKey<T> {
231    /// Returns a reference to the secure API key.
232    ///
233    /// To access the underlying string, use `.expose_secret()` on the returned `SecureString`:
234    ///
235    /// ```rust
236    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, ExposeSecret};
237    /// # let generator = ApiKeyManagerV0::init_default_config("sk").unwrap();
238    /// # let api_key = generator.generate(Environment::production()).unwrap();
239    /// let key_str: &str = api_key.key().expose_secret();
240    /// ```
241    ///
242    /// # Security Note
243    ///
244    /// The key is stored in secure memory that is automatically zeroed on drop.
245    /// Be careful NOT to clone or log the value unnecessarily.
246    pub fn key(&self) -> &SecureString {
247        &self.key
248    }
249}
250
251impl ApiKey<NoHash> {
252    /// Creates a new API key without a hash.
253    ///
254    /// This is typically used internally before converting to a hashed key.
255    pub fn new(key: SecureString) -> ApiKey<NoHash> {
256        ApiKey { key, hash: NoHash }
257    }
258
259    /// Converts this unhashed key into a hashed key by generating a new random salt
260    /// and computing the Argon2 hash.
261    ///
262    /// This method is automatically called by `ApiKeyManagerV0::generate()` and
263    /// `ApiKeyManagerV0::generate_with_expiry()`.
264    pub fn into_hashed(self, hasher: &KeyHasher) -> Result<ApiKey<Hash>> {
265        let (hash, salt) = hasher.hash(&self.key)?;
266
267        Ok(ApiKey {
268            key: self.key,
269            hash: Hash { hash, salt },
270        })
271    }
272
273    /// Converts this unhashed key into a hashed key using a specific salt.
274    ///
275    /// This is useful when you need to regenerate the same hash from the same key,
276    /// for example in testing or when verifying hash consistency.
277    ///
278    /// # Parameters
279    ///
280    /// * `hasher` - The key hasher to use
281    /// * `salt` - Base64-encoded salt string (32 bytes when decoded)
282    ///
283    /// # Example
284    ///
285    /// ```rust
286    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, ExposeSecret};
287    /// # use api_keys_simplified::{SecureString, ApiKey};
288    /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
289    /// let key1 = manager.generate(Environment::production()).unwrap();
290    ///
291    /// // Regenerate hash with the same salt
292    /// let key2 = ApiKey::new(SecureString::from(key1.key().expose_secret()))
293    ///     .into_hashed_with_salt(manager.hasher(), key1.expose_hash().salt())
294    ///     .unwrap();
295    ///
296    /// // Both hashes should be identical
297    /// assert_eq!(key1.expose_hash(), key2.expose_hash());
298    /// # Ok::<(), Box<dyn std::error::Error>>(())
299    /// ```
300    pub fn into_hashed_with_salt(self, hasher: &KeyHasher, salt: &str) -> Result<ApiKey<Hash>> {
301        let hash = hasher.hash_with_salt(&self.key, salt)?;
302
303        Ok(ApiKey {
304            key: self.key,
305            hash: Hash {
306                hash,
307                salt: salt.to_string(),
308            },
309        })
310    }
311
312    /// Consumes the API key and returns the underlying secure string.
313    pub fn into_key(self) -> SecureString {
314        self.key
315    }
316}
317
318impl ApiKey<Hash> {
319    /// Returns a reference to the hash and salt.
320    ///
321    /// The returned `Hash` struct contains both the Argon2 hash string and the
322    /// base64-encoded salt used to generate it. The hash should be stored in your
323    /// database for later verification.
324    ///
325    /// # Accessing Fields
326    ///
327    /// Use the auto-generated getter methods:
328    /// - `.hash()` - Returns the Argon2 hash string as `&str`
329    /// - `.salt()` - Returns the base64-encoded salt as `&str`
330    ///
331    /// # Security Note
332    ///
333    /// Although it's safe to store the hash, avoid making unnecessary clones
334    /// or logging the hash to minimize exposure.
335    ///
336    /// # Example
337    ///
338    /// ```rust
339    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment};
340    /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
341    /// # let api_key = manager.generate(Environment::production()).unwrap();
342    /// // Get the hash for storage
343    /// let hash_struct = api_key.expose_hash();
344    ///
345    /// // Access the hash string for database storage
346    /// let hash_str: &str = hash_struct.hash();
347    /// println!("Store this hash: {}", hash_str);
348    ///
349    /// // Access the salt (if needed for hash regeneration)
350    /// let salt: &str = hash_struct.salt();
351    /// # Ok::<(), Box<dyn std::error::Error>>(())
352    /// ```
353    pub fn expose_hash(&self) -> &Hash {
354        &self.hash
355    }
356
357    /// Consumes the API key and returns the underlying secure string.
358    pub fn into_key(self) -> SecureString {
359        self.key
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366    use crate::{ExposeSecret, SecureStringExt};
367
368    #[test]
369    fn test_full_lifecycle() {
370        let generator = ApiKeyManagerV0::init_default_config("sk").unwrap();
371        let api_key = generator.generate(Environment::production()).unwrap();
372
373        let key_str = api_key.key();
374        let hash_str = api_key.expose_hash().hash();
375
376        assert!(key_str.expose_secret().starts_with("sk-live-"));
377        assert!(hash_str.starts_with("$argon2id$"));
378
379        assert_eq!(
380            generator.verify(key_str, hash_str).unwrap(),
381            KeyStatus::Valid
382        );
383        let wrong_key = SecureString::from("wrong_key".to_string());
384        assert_eq!(
385            generator.verify(&wrong_key, hash_str).unwrap(),
386            KeyStatus::Invalid
387        );
388    }
389
390    #[test]
391    fn test_different_presets() {
392        let balanced_gen = ApiKeyManagerV0::init_default_config("pk").unwrap();
393        let balanced = balanced_gen.generate(Environment::test()).unwrap();
394
395        let high_sec_gen = ApiKeyManagerV0::init_high_security_config("sk").unwrap();
396        let high_sec = high_sec_gen.generate(Environment::Production).unwrap();
397
398        assert!(!balanced.key().is_empty());
399        assert!(high_sec.key().len() > balanced.key().len());
400    }
401
402    #[test]
403    fn test_custom_config() {
404        let config = KeyConfig::new().with_entropy(32).unwrap();
405
406        let generator = ApiKeyManagerV0::init(
407            "custom",
408            config,
409            HashConfig::default(),
410            std::time::Duration::ZERO,
411        )
412        .unwrap();
413        let key = generator.generate(Environment::production()).unwrap();
414        assert!(generator.verify_checksum(key.key()).unwrap());
415    }
416
417    #[test]
418    fn compare_hash() {
419        let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
420        let key = manager.generate(Environment::production()).unwrap();
421        let new_secret = ApiKey::new(SecureString::from(key.key().expose_secret()))
422            .into_hashed_with_salt(manager.hasher(), key.expose_hash().salt())
423            .unwrap();
424
425        assert_eq!(new_secret.expose_hash(), key.expose_hash());
426    }
427}