Skip to main content

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 in PHC format and a stable key identifier.
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/// - `key_id`: A stable, deterministic identifier derived from the API key itself.
44///   This ID never changes for the same key, making it perfect for database indexing
45///   and key lookups. Format: 32 hex characters (16 bytes of BLAKE3 hash).
46///
47/// - `hash`: The Argon2id hash in PHC format. This changes each time you hash the
48///   same key (due to random salt), but the key_id remains constant.
49///
50/// # PHC Format
51///
52/// The hash is stored in PHC (Password Hashing Competition) format which includes:
53/// - Algorithm identifier (argon2id)
54/// - Version
55/// - Parameters (memory cost, time cost, parallelism)
56/// - Salt (base64-encoded, embedded in the hash string)
57/// - Hash output (base64-encoded)
58///
59/// Example: `$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>`
60///
61/// The salt is embedded within the PHC string and can be extracted if needed using
62/// the `password_hash` crate's `PasswordHash::new()` method.
63///
64/// # Key ID vs Hash
65///
66/// - **Key ID**: Stable identifier, never changes for the same key
67/// - **Hash**: Changes each time you hash (due to different random salts)
68///
69/// Both fields can be accessed using the auto-generated getter methods `key_id()` and `hash()`
70/// provided by the `Getters` derive macro.
71#[derive(Debug, Getters, PartialEq)]
72pub struct Hash {
73    key_id: String,
74    hash: String,
75}
76
77#[derive(Debug)]
78pub struct NoHash;
79
80/// Represents a generated API key with its hash.
81///
82/// The key field is stored in a `SecureString` which automatically zeros
83/// its memory on drop, preventing potential memory disclosure.
84#[derive(Debug)]
85pub struct ApiKey<Hash> {
86    key: SecureString,
87    hash: Hash,
88}
89
90impl ApiKeyManagerV0 {
91    pub fn init(
92        prefix: impl Into<String>,
93        config: KeyConfig,
94        hash_config: HashConfig,
95        expiry_grace_period: std::time::Duration,
96    ) -> std::result::Result<Self, InitError> {
97        let include_checksum = *config.checksum_length() != 0;
98        let prefix = KeyPrefix::new(prefix)?;
99        let generator = KeyGenerator::new(prefix, config)?;
100        let hasher = KeyHasher::new(hash_config);
101
102        // Generate dummy key and its hash for timing attack protection
103        let dummy_key = generator.dummy_key().clone();
104        let (_dummy_key_id, dummy_hash) = hasher.hash(&dummy_key)?;
105
106        let validator = KeyValidator::new(include_checksum, dummy_key, dummy_hash)?;
107
108        Ok(Self {
109            generator,
110            hasher,
111            validator,
112            include_checksum,
113            expiry_grace_period,
114        })
115    }
116
117    pub fn init_default_config(prefix: impl Into<String>) -> std::result::Result<Self, InitError> {
118        Self::init(
119            prefix,
120            KeyConfig::default(),
121            HashConfig::default(),
122            std::time::Duration::from_secs(10),
123        )
124    }
125    pub fn init_high_security_config(
126        prefix: impl Into<String>,
127    ) -> std::result::Result<Self, InitError> {
128        Self::init(
129            prefix,
130            KeyConfig::high_security(),
131            HashConfig::high_security(),
132            std::time::Duration::from_secs(10),
133        )
134    }
135
136    /// Generates a new API key for the specified environment.
137    ///
138    /// The generated key includes a checksum (if enabled) for fast DoS protection.
139    ///
140    /// # Example
141    ///
142    /// ```rust
143    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, ExposeSecret};
144    /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
145    /// let key = manager.generate(Environment::production())?;
146    /// println!("Key: {}", key.key().expose_secret());
147    /// # Ok::<(), Box<dyn std::error::Error>>(())
148    /// ```
149    pub fn generate(&self, environment: impl Into<Environment>) -> Result<ApiKey<Hash>> {
150        let key = self.generator.generate(environment.into(), None)?;
151        let api_key = ApiKey::new(key).into_hashed(&self.hasher)?;
152
153        Ok(api_key)
154    }
155
156    /// Generates a new API key with an expiration timestamp.
157    ///
158    /// The expiration is embedded in the key itself, making it stateless.
159    /// Keys are automatically rejected after the expiry time without database lookups.
160    ///
161    /// # Use Cases
162    ///
163    /// - Trial keys (7-30 days)
164    /// - Temporary partner access
165    /// - Time-limited API access
166    ///
167    /// # Example
168    ///
169    /// ```rust
170    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment};
171    /// # use chrono::{Utc, Duration};
172    /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
173    /// // Create a 7-day trial key
174    /// let expiry = Utc::now() + Duration::days(7);
175    /// let key = manager.generate_with_expiry(Environment::production(), expiry)?;
176    /// # Ok::<(), Box<dyn std::error::Error>>(())
177    /// ```
178    pub fn generate_with_expiry(
179        &self,
180        environment: impl Into<Environment>,
181        expiry: DateTime<Utc>,
182    ) -> Result<ApiKey<Hash>> {
183        let key = self.generator.generate(environment.into(), Some(expiry))?;
184        let api_key = ApiKey::new(key).into_hashed(&self.hasher)?;
185
186        Ok(api_key)
187    }
188
189    /// Verifies an API key against a stored hash.
190    ///
191    /// Returns `KeyStatus` indicating whether the key is valid or invalid.
192    ///
193    /// # Parameters
194    ///
195    /// * `key` - The API key to verify
196    /// * `stored_hash` - The Argon2 hash stored in your database
197    /// * `expiry_grace_period` - Optional grace period duration after expiry.
198    ///   - `None`: Skip expiry validation (all keys treated as non-expired)
199    ///   - `Some(Duration::ZERO)`: Strict expiry check (no grace period)
200    ///   - `Some(duration)`: Key remains valid for `duration` after its expiry time
201    ///
202    /// The grace period protects against clock skew issues. Once a key expires beyond
203    /// the grace period, it stays expired even if the system clock goes backwards.
204    ///
205    /// # Security Flow
206    ///
207    /// 1. **Checksum validation** (if enabled): Rejects invalid keys in ~20μs
208    /// 2. **Argon2 verification**: Verifies hash for valid checksums (~300ms)
209    /// 3. **Expiry check**: Returns `Invalid` if expired beyond grace period
210    ///
211    /// # Returns
212    ///
213    /// - `KeyStatus::Valid` - Key is valid and not expired
214    /// - `KeyStatus::Invalid` - Key is invalid (wrong key, hash mismatch, checksum failed, or expired)
215    ///
216    /// # Note on Revocation
217    ///
218    /// This method does NOT check revocation status. To implement key revocation:
219    /// 1. Mark the hash as revoked in your database
220    /// 2. Check revocation status before calling this method
221    /// 3. Only call `verify()` for non-revoked hashes
222    ///
223    /// # Example
224    ///
225    /// ```rust
226    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, KeyStatus};
227    /// # use std::time::Duration;
228    /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
229    /// # let key = manager.generate(Environment::production()).unwrap();
230    /// match manager.verify(key.key(), key.expose_hash().hash())? {
231    ///     KeyStatus::Valid => { /* grant access */ },
232    ///     KeyStatus::Invalid => { /* reject - wrong key or expired */ },
233    /// }
234    /// # Ok::<(), Box<dyn std::error::Error>>(())
235    /// ```
236    pub fn verify(&self, key: &SecureString, stored_hash: impl AsRef<str>) -> Result<KeyStatus> {
237        if self.include_checksum && !self.verify_checksum(key)? {
238            return Ok(KeyStatus::Invalid);
239        }
240
241        self.validator.verify(
242            key.expose_secret(),
243            stored_hash.as_ref(),
244            self.expiry_grace_period,
245        )
246    }
247
248    pub fn verify_checksum(&self, key: &SecureString) -> Result<bool> {
249        self.generator.verify_checksum(key)
250    }
251
252    /// Extracts a stable key ID from an API key.
253    ///
254    /// This generates a deterministic identifier from the API key using BLAKE3 hash.
255    /// The same key will always produce the same key ID, making it perfect for:
256    /// - Database lookups and indexing
257    /// - Key identification without exposing the full key
258    /// - Tracking key usage across hash rotations
259    ///
260    /// # Format
261    ///
262    /// Returns a 32-character hex string (16 bytes / 128 bits of BLAKE3 hash).
263    ///
264    /// # Example
265    ///
266    /// ```rust
267    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, SecureString};
268    /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
269    /// // Extract key ID from an incoming API key (e.g., from Authorization header)
270    /// let provided_key = SecureString::from("sk-live-abc123def456...".to_string());
271    /// let key_id = manager.extract_key_id(&provided_key);
272    ///
273    /// // Use key_id for fast database lookup
274    /// // let stored_hash = database.find_by_key_id(&key_id)?;
275    /// // manager.verify(&provided_key, &stored_hash)?;
276    /// # Ok::<(), Box<dyn std::error::Error>>(())
277    /// ```
278    ///
279    /// # Security Note
280    ///
281    /// While the key ID is a one-way hash, it still uniquely identifies a key.
282    /// Treat it as sensitive data and don't expose it in public APIs or logs.
283    pub fn extract_key_id(&self, key: &SecureString) -> String {
284        self.hasher.generate_key_id(key)
285    }
286}
287
288impl<T> ApiKey<T> {
289    /// Returns a reference to the secure API key.
290    ///
291    /// To access the underlying string, use `.expose_secret()` on the returned `SecureString`:
292    ///
293    /// ```rust
294    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, ExposeSecret};
295    /// # let generator = ApiKeyManagerV0::init_default_config("sk").unwrap();
296    /// # let api_key = generator.generate(Environment::production()).unwrap();
297    /// let key_str: &str = api_key.key().expose_secret();
298    /// ```
299    ///
300    /// # Security Note
301    ///
302    /// The key is stored in secure memory that is automatically zeroed on drop.
303    /// Be careful NOT to clone or log the value unnecessarily.
304    pub fn key(&self) -> &SecureString {
305        &self.key
306    }
307}
308
309impl ApiKey<NoHash> {
310    /// Creates a new API key without a hash.
311    ///
312    /// This is typically used internally before converting to a hashed key.
313    pub fn new(key: SecureString) -> ApiKey<NoHash> {
314        ApiKey { key, hash: NoHash }
315    }
316
317    /// Converts this unhashed key into a hashed key by generating a new random salt
318    /// and computing the Argon2 hash.
319    ///
320    /// This method is automatically called by `ApiKeyManagerV0::generate()` and
321    /// `ApiKeyManagerV0::generate_with_expiry()`.
322    pub fn into_hashed(self, hasher: &KeyHasher) -> Result<ApiKey<Hash>> {
323        let (key_id, hash) = hasher.hash(&self.key)?;
324
325        Ok(ApiKey {
326            key: self.key,
327            hash: Hash { key_id, hash },
328        })
329    }
330
331    /// Converts this unhashed key into a hashed key using a specific PHC hash string.
332    ///
333    /// This is useful when you need to regenerate the same hash from the same key,
334    /// for example in testing or when verifying hash consistency. The salt is extracted
335    /// from the provided PHC hash string, and the key ID is derived from the key itself.
336    ///
337    /// # Parameters
338    ///
339    /// * `hasher` - The key hasher to use
340    /// * `phc_hash` - An existing PHC-formatted hash string to extract the salt from
341    ///
342    /// # Example
343    ///
344    /// ```rust
345    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, ExposeSecret};
346    /// # use api_keys_simplified::{SecureString, ApiKey};
347    /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
348    /// let key1 = manager.generate(Environment::production()).unwrap();
349    ///
350    /// // Regenerate hash with the same salt (extracted from the PHC hash)
351    /// let key2 = ApiKey::new(SecureString::from(key1.key().expose_secret()))
352    ///     .into_hashed_with_phc(manager.hasher(), key1.expose_hash().hash())
353    ///     .unwrap();
354    ///
355    /// // Both hashes should be identical
356    /// assert_eq!(key1.expose_hash(), key2.expose_hash());
357    /// # Ok::<(), Box<dyn std::error::Error>>(())
358    /// ```
359    pub fn into_hashed_with_phc(self, hasher: &KeyHasher, phc_hash: &str) -> Result<ApiKey<Hash>> {
360        let (key_id, hash) = hasher.hash_with_phc(&self.key, phc_hash)?;
361
362        Ok(ApiKey {
363            key: self.key,
364            hash: Hash { key_id, hash },
365        })
366    }
367
368    /// Consumes the API key and returns the underlying secure string.
369    pub fn into_key(self) -> SecureString {
370        self.key
371    }
372}
373
374impl ApiKey<Hash> {
375    /// Returns a reference to the hash.
376    ///
377    /// The returned `Hash` struct contains the Argon2 hash string in PHC format.
378    /// The PHC format embeds all necessary information including the salt, algorithm
379    /// parameters, and hash output. This single string should be stored in your
380    /// database for later verification.
381    ///
382    /// # Accessing Fields
383    ///
384    /// Use the auto-generated getter method:
385    /// - `.hash()` - Returns the PHC-formatted hash string as `&str`
386    ///
387    /// # PHC Format
388    ///
389    /// The hash string follows the PHC format:
390    /// `$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>`
391    ///
392    /// The salt is embedded in the hash string and can be extracted if needed using
393    /// the `password_hash` crate's `PasswordHash::new()` method.
394    ///
395    /// # Security Note
396    ///
397    /// Although it's safe to store the hash, avoid making unnecessary clones
398    /// or logging the hash to minimize exposure.
399    ///
400    /// # Example
401    ///
402    /// ```rust
403    /// # use api_keys_simplified::{ApiKeyManagerV0, Environment};
404    /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
405    /// # let api_key = manager.generate(Environment::production()).unwrap();
406    /// // Get the hash for storage
407    /// let hash_struct = api_key.expose_hash();
408    ///
409    /// // Access the hash string for database storage
410    /// let hash_str: &str = hash_struct.hash();
411    /// println!("Store this hash: {}", hash_str);
412    /// # Ok::<(), Box<dyn std::error::Error>>(())
413    /// ```
414    pub fn expose_hash(&self) -> &Hash {
415        &self.hash
416    }
417
418    /// Consumes the API key and returns the underlying secure string.
419    pub fn into_key(self) -> SecureString {
420        self.key
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427    use crate::{ExposeSecret, SecureStringExt};
428
429    #[test]
430    fn test_full_lifecycle() {
431        let generator = ApiKeyManagerV0::init_default_config("sk").unwrap();
432        let api_key = generator.generate(Environment::production()).unwrap();
433
434        let key_str = api_key.key();
435        let hash_str = api_key.expose_hash().hash();
436
437        assert!(key_str.expose_secret().starts_with("sk-live-"));
438        assert!(hash_str.starts_with("$argon2id$"));
439
440        assert_eq!(
441            generator.verify(key_str, hash_str).unwrap(),
442            KeyStatus::Valid
443        );
444        let wrong_key = SecureString::from("wrong_key".to_string());
445        assert_eq!(
446            generator.verify(&wrong_key, hash_str).unwrap(),
447            KeyStatus::Invalid
448        );
449    }
450
451    #[test]
452    fn test_different_presets() {
453        let balanced_gen = ApiKeyManagerV0::init_default_config("pk").unwrap();
454        let balanced = balanced_gen.generate(Environment::test()).unwrap();
455
456        let high_sec_gen = ApiKeyManagerV0::init_high_security_config("sk").unwrap();
457        let high_sec = high_sec_gen.generate(Environment::Production).unwrap();
458
459        assert!(!balanced.key().is_empty());
460        assert!(high_sec.key().len() > balanced.key().len());
461    }
462
463    #[test]
464    fn test_custom_config() {
465        let config = KeyConfig::new().with_entropy(32).unwrap();
466
467        let generator = ApiKeyManagerV0::init(
468            "custom",
469            config,
470            HashConfig::default(),
471            std::time::Duration::ZERO,
472        )
473        .unwrap();
474        let key = generator.generate(Environment::production()).unwrap();
475        assert!(generator.verify_checksum(key.key()).unwrap());
476    }
477
478    #[test]
479    fn compare_hash() {
480        let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
481        let key = manager.generate(Environment::production()).unwrap();
482        let new_secret = ApiKey::new(SecureString::from(key.key().expose_secret()))
483            .into_hashed_with_phc(manager.hasher(), key.expose_hash().hash())
484            .unwrap();
485
486        assert_eq!(new_secret.expose_hash(), key.expose_hash());
487    }
488
489    #[test]
490    fn test_extract_key_id() {
491        let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
492        let key1 = manager.generate(Environment::production()).unwrap();
493        let key2 = manager.generate(Environment::production()).unwrap();
494
495        let id1 = manager.extract_key_id(key1.key());
496
497        // Matches stored key_id
498        assert_eq!(id1, *key1.expose_hash().key_id());
499
500        // Deterministic: multiple extractions match
501        assert_eq!(id1, manager.extract_key_id(key1.key()));
502
503        // Unique: different keys have different IDs
504        assert_ne!(id1, manager.extract_key_id(key2.key()));
505
506        // Format: 32 hex characters
507        assert_eq!(id1.len(), 32);
508        assert!(id1.chars().all(|c| c.is_ascii_hexdigit()));
509    }
510
511    #[test]
512    fn test_key_id_stability_across_rehashing() {
513        let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
514        let key1 = manager.generate(Environment::production()).unwrap();
515
516        // Rehash with new salt
517        let key2 = ApiKey::new(SecureString::from(key1.key().expose_secret()))
518            .into_hashed(manager.hasher())
519            .unwrap();
520
521        // Key ID stays the same, hash changes
522        assert_eq!(key1.expose_hash().key_id(), key2.expose_hash().key_id());
523        assert_ne!(key1.expose_hash().hash(), key2.expose_hash().hash());
524    }
525
526    #[test]
527    fn test_key_id_database_lookup() {
528        let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
529        let api_key = manager.generate(Environment::production()).unwrap();
530
531        // Simulate: store in database
532        let stored_key_id = api_key.expose_hash().key_id().to_string();
533        let stored_hash = api_key.expose_hash().hash().to_string();
534
535        // Simulate: incoming request
536        let incoming_key = SecureString::from(api_key.key().expose_secret());
537        let lookup_key_id = manager.extract_key_id(&incoming_key);
538
539        // Fast lookup by key_id, then verify
540        assert_eq!(lookup_key_id, stored_key_id);
541        assert_eq!(
542            manager.verify(&incoming_key, &stored_hash).unwrap(),
543            KeyStatus::Valid
544        );
545    }
546}