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}