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