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