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/// Hash can be safely stored as String
36/// in memory without having to worry about
37/// zeroizing. Hashes are not secrets and are meant to be stored.
38#[derive(Debug)]
39pub struct Hash(String);
40#[derive(Debug)]
41pub struct NoHash;
42
43/// Represents a generated API key with its hash.
44///
45/// The key field is stored in a `SecureString` which automatically zeros
46/// its memory on drop, preventing potential memory disclosure.
47#[derive(Debug)]
48pub struct ApiKey<Hash> {
49 key: SecureString,
50 hash: Hash,
51}
52
53impl ApiKeyManagerV0 {
54 pub fn init(
55 prefix: impl Into<String>,
56 config: KeyConfig,
57 hash_config: HashConfig,
58 expiry_grace_period: std::time::Duration,
59 ) -> std::result::Result<Self, InitError> {
60 let include_checksum = *config.checksum_length() != 0;
61 let prefix = KeyPrefix::new(prefix)?;
62 let generator = KeyGenerator::new(prefix, config)?;
63 let hasher = KeyHasher::new(hash_config);
64
65 // Generate dummy key and its hash for timing attack protection
66 let dummy_key = generator.dummy_key().clone();
67 let dummy_hash = hasher.hash(&dummy_key)?;
68
69 let validator = KeyValidator::new(include_checksum, dummy_key, dummy_hash)?;
70
71 Ok(Self {
72 generator,
73 hasher,
74 validator,
75 include_checksum,
76 expiry_grace_period,
77 })
78 }
79
80 pub fn init_default_config(prefix: impl Into<String>) -> std::result::Result<Self, InitError> {
81 Self::init(
82 prefix,
83 KeyConfig::default(),
84 HashConfig::default(),
85 std::time::Duration::from_secs(10),
86 )
87 }
88 pub fn init_high_security_config(
89 prefix: impl Into<String>,
90 ) -> std::result::Result<Self, InitError> {
91 Self::init(
92 prefix,
93 KeyConfig::high_security(),
94 HashConfig::high_security(),
95 std::time::Duration::from_secs(10),
96 )
97 }
98
99 /// Generates a new API key for the specified environment.
100 ///
101 /// The generated key includes a checksum (if enabled) for fast DoS protection.
102 ///
103 /// # Example
104 ///
105 /// ```rust
106 /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, ExposeSecret};
107 /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
108 /// let key = manager.generate(Environment::production())?;
109 /// println!("Key: {}", key.key().expose_secret());
110 /// # Ok::<(), Box<dyn std::error::Error>>(())
111 /// ```
112 pub fn generate(&self, environment: impl Into<Environment>) -> Result<ApiKey<Hash>> {
113 let key = self.generator.generate(environment.into(), None)?;
114 let api_key = ApiKey::new(key).into_hashed(&self.hasher)?;
115
116 Ok(api_key)
117 }
118
119 /// Generates a new API key with an expiration timestamp.
120 ///
121 /// The expiration is embedded in the key itself, making it stateless.
122 /// Keys are automatically rejected after the expiry time without database lookups.
123 ///
124 /// # Use Cases
125 ///
126 /// - Trial keys (7-30 days)
127 /// - Temporary partner access
128 /// - Time-limited API access
129 ///
130 /// # Example
131 ///
132 /// ```rust
133 /// # use api_keys_simplified::{ApiKeyManagerV0, Environment};
134 /// # use chrono::{Utc, Duration};
135 /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
136 /// // Create a 7-day trial key
137 /// let expiry = Utc::now() + Duration::days(7);
138 /// let key = manager.generate_with_expiry(Environment::production(), expiry)?;
139 /// # Ok::<(), Box<dyn std::error::Error>>(())
140 /// ```
141 pub fn generate_with_expiry(
142 &self,
143 environment: impl Into<Environment>,
144 expiry: DateTime<Utc>,
145 ) -> Result<ApiKey<Hash>> {
146 let key = self.generator.generate(environment.into(), Some(expiry))?;
147 let api_key = ApiKey::new(key).into_hashed(&self.hasher)?;
148
149 Ok(api_key)
150 }
151
152 /// Verifies an API key against a stored hash.
153 ///
154 /// Returns `KeyStatus` indicating whether the key is valid or invalid.
155 ///
156 /// # Parameters
157 ///
158 /// * `key` - The API key to verify
159 /// * `stored_hash` - The Argon2 hash stored in your database
160 /// * `expiry_grace_period` - Optional grace period duration after expiry.
161 /// - `None`: Skip expiry validation (all keys treated as non-expired)
162 /// - `Some(Duration::ZERO)`: Strict expiry check (no grace period)
163 /// - `Some(duration)`: Key remains valid for `duration` after its expiry time
164 ///
165 /// The grace period protects against clock skew issues. Once a key expires beyond
166 /// the grace period, it stays expired even if the system clock goes backwards.
167 ///
168 /// # Security Flow
169 ///
170 /// 1. **Checksum validation** (if enabled): Rejects invalid keys in ~20μs
171 /// 2. **Argon2 verification**: Verifies hash for valid checksums (~300ms)
172 /// 3. **Expiry check**: Returns `Invalid` if expired beyond grace period
173 ///
174 /// # Returns
175 ///
176 /// - `KeyStatus::Valid` - Key is valid and not expired
177 /// - `KeyStatus::Invalid` - Key is invalid (wrong key, hash mismatch, checksum failed, or expired)
178 ///
179 /// # Note on Revocation
180 ///
181 /// This method does NOT check revocation status. To implement key revocation:
182 /// 1. Mark the hash as revoked in your database
183 /// 2. Check revocation status before calling this method
184 /// 3. Only call `verify()` for non-revoked hashes
185 ///
186 /// # Example
187 ///
188 /// ```rust
189 /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, KeyStatus};
190 /// # use std::time::Duration;
191 /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
192 /// # let key = manager.generate(Environment::production()).unwrap();
193 /// match manager.verify(key.key(), key.hash())? {
194 /// KeyStatus::Valid => { /* grant access */ },
195 /// KeyStatus::Invalid => { /* reject - wrong key or expired */ },
196 /// }
197 /// # Ok::<(), Box<dyn std::error::Error>>(())
198 /// ```
199 pub fn verify(&self, key: &SecureString, stored_hash: impl AsRef<str>) -> Result<KeyStatus> {
200 if self.include_checksum && !self.verify_checksum(key)? {
201 return Ok(KeyStatus::Invalid);
202 }
203
204 self.validator.verify(
205 key.expose_secret(),
206 stored_hash.as_ref(),
207 self.expiry_grace_period,
208 )
209 }
210
211 pub fn verify_checksum(&self, key: &SecureString) -> Result<bool> {
212 self.generator.verify_checksum(key)
213 }
214}
215
216impl<T> ApiKey<T> {
217 /// Returns a reference to the secure API key.
218 ///
219 /// To access the underlying string, use `.expose_secret()` on the returned `SecureString`:
220 ///
221 /// ```rust
222 /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, ExposeSecret};
223 /// # let generator = ApiKeyManagerV0::init_default_config("sk").unwrap();
224 /// # let api_key = generator.generate(Environment::production()).unwrap();
225 /// let key_str: &str = api_key.key().expose_secret();
226 /// ```
227 ///
228 /// # Security Note
229 ///
230 /// The key is stored in secure memory that is automatically zeroed on drop.
231 /// Be careful NOT to clone or log the value unnecessarily.
232 pub fn key(&self) -> &SecureString {
233 &self.key
234 }
235}
236
237impl ApiKey<NoHash> {
238 pub fn new(key: SecureString) -> ApiKey<NoHash> {
239 ApiKey { key, hash: NoHash }
240 }
241 pub fn into_hashed(self, hasher: &KeyHasher) -> Result<ApiKey<Hash>> {
242 let hash = hasher.hash(&self.key)?;
243
244 Ok(ApiKey {
245 key: self.key,
246 hash: Hash(hash),
247 })
248 }
249 pub fn into_key(self) -> SecureString {
250 self.key
251 }
252}
253
254impl ApiKey<Hash> {
255 /// Returns hash.
256 /// SECURITY:
257 /// Although it's safe to store hash,
258 /// do NOT make unnecessary clones
259 /// and avoid logging the hash.
260 pub fn hash(&self) -> &str {
261 &self.hash.0
262 }
263 pub fn into_key(self) -> SecureString {
264 self.key
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use crate::{ExposeSecret, SecureStringExt};
272
273 #[test]
274 fn test_full_lifecycle() {
275 let generator = ApiKeyManagerV0::init_default_config("sk").unwrap();
276 let api_key = generator.generate(Environment::production()).unwrap();
277
278 let key_str = api_key.key();
279 let hash_str = api_key.hash();
280
281 assert!(key_str.expose_secret().starts_with("sk-live-"));
282 assert!(hash_str.starts_with("$argon2id$"));
283
284 assert_eq!(
285 generator.verify(key_str, hash_str).unwrap(),
286 KeyStatus::Valid
287 );
288 let wrong_key = SecureString::from("wrong_key".to_string());
289 assert_eq!(
290 generator.verify(&wrong_key, hash_str).unwrap(),
291 KeyStatus::Invalid
292 );
293 }
294
295 #[test]
296 fn test_different_presets() {
297 let balanced_gen = ApiKeyManagerV0::init_default_config("pk").unwrap();
298 let balanced = balanced_gen.generate(Environment::test()).unwrap();
299
300 let high_sec_gen = ApiKeyManagerV0::init_high_security_config("sk").unwrap();
301 let high_sec = high_sec_gen.generate(Environment::Production).unwrap();
302
303 assert!(!balanced.key().is_empty());
304 assert!(high_sec.key().len() > balanced.key().len());
305 }
306
307 #[test]
308 fn test_custom_config() {
309 let config = KeyConfig::new().with_entropy(32).unwrap();
310
311 let generator = ApiKeyManagerV0::init(
312 "custom",
313 config,
314 HashConfig::default(),
315 std::time::Duration::ZERO,
316 )
317 .unwrap();
318 let key = generator.generate(Environment::production()).unwrap();
319 assert!(generator.verify_checksum(key.key()).unwrap());
320 }
321}