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}