1use std::collections::HashMap;
17use std::sync::atomic::{AtomicU64, Ordering};
18use std::sync::Arc;
19use std::time::{SystemTime, UNIX_EPOCH};
20
21use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
22use dashmap::DashMap;
23use sha2::{Digest, Sha256};
24use subtle::ConstantTimeEq;
25use tracing::debug;
26use zeroize::Zeroize;
27
28use crate::auth::error::AuthError;
29
30fn key_prefix(raw_key: &str) -> [u8; 8] {
38 let hash = Sha256::digest(raw_key.as_bytes());
39 let mut prefix = [0u8; 8];
40 prefix.copy_from_slice(&hash[..8]);
41 prefix
42}
43
44#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
46pub struct ApiKeyEntry {
47 pub name: String,
49 pub hash: String,
51 pub expires_at: Option<u64>,
53 pub created_at: u64,
55 #[serde(default)]
64 pub namespaces: Vec<String>,
65 #[serde(default)]
71 pub admin: bool,
72}
73
74impl ApiKeyEntry {
75 #[must_use]
77 pub fn allows_namespace(&self, namespace: &str) -> bool {
78 self.namespaces.is_empty() || self.namespaces.iter().any(|n| n == namespace)
79 }
80}
81
82#[derive(Clone)]
93pub struct ApiKeyStore {
94 keys: HashMap<String, ApiKeyEntry>,
95 prefix_index: HashMap<[u8; 8], Vec<String>>,
97 per_prefix_failures: Arc<DashMap<[u8; 8], (u64, u64)>>,
99 failure_count: Arc<AtomicU64>,
101 failure_window_start: Arc<AtomicU64>,
103}
104
105impl std::fmt::Debug for ApiKeyStore {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 f.debug_struct("ApiKeyStore")
108 .field("keys", &self.keys.len())
109 .finish()
110 }
111}
112
113const MAX_FAILURES_PER_WINDOW: u64 = 20;
115const MAX_PREFIX_FAILURES_PER_WINDOW: u64 = 5;
117const FAILURE_WINDOW_SECS: u64 = 60;
119
120impl ApiKeyStore {
121 #[must_use]
123 pub fn new() -> Self {
124 Self {
125 keys: HashMap::new(),
126 prefix_index: HashMap::new(),
127 per_prefix_failures: Arc::new(DashMap::new()),
128 failure_count: Arc::new(AtomicU64::new(0)),
129 failure_window_start: Arc::new(AtomicU64::new(0)),
130 }
131 }
132
133 #[must_use]
139 pub fn key_exists(&self, name: &str) -> bool {
140 self.keys.contains_key(name)
141 }
142
143 pub fn create_key(&mut self, name: &str, expires_at: Option<u64>) -> Result<String, AuthError> {
148 if self.keys.contains_key(name) {
149 return Err(AuthError::Config(format!(
150 "API key with name '{name}' already exists"
151 )));
152 }
153 let mut key_bytes = [0u8; 32];
156 super::fill_random(&mut key_bytes);
157 use base64::Engine;
158 let raw_key = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(key_bytes);
159 let argon2 = Argon2::default();
160 let hash = argon2
164 .hash_password(raw_key.as_bytes())
165 .map_err(|e| AuthError::Config(format!("hash error: {e}")))?
166 .to_string();
167
168 let now = SystemTime::now()
169 .duration_since(UNIX_EPOCH)
170 .unwrap_or_default()
171 .as_secs();
172
173 let entry = ApiKeyEntry {
174 name: name.to_string(),
175 hash,
176 expires_at,
177 created_at: now,
178 namespaces: Vec::new(),
179 admin: false,
180 };
181
182 self.keys.insert(name.to_string(), entry);
183 self.prefix_index
184 .entry(key_prefix(&raw_key))
185 .or_default()
186 .push(name.to_string());
187 debug!(name, "created API key");
188 Ok(raw_key)
189 }
190
191 pub fn bind_namespaces(&mut self, name: &str, namespaces: Vec<String>) -> bool {
196 match self.keys.get_mut(name) {
197 Some(entry) => {
198 entry.namespaces = namespaces;
199 true
200 }
201 None => false,
202 }
203 }
204
205 pub fn set_admin(&mut self, name: &str, admin: bool) -> bool {
209 match self.keys.get_mut(name) {
210 Some(entry) => {
211 entry.admin = admin;
212 true
213 }
214 None => false,
215 }
216 }
217
218 #[must_use]
220 pub fn is_admin(&self, name: &str) -> bool {
221 self.keys.get(name).is_some_and(|e| e.admin)
222 }
223
224 #[must_use]
226 pub fn namespaces_for(&self, name: &str) -> &[String] {
227 self.keys
228 .get(name)
229 .map_or(&[][..], |entry| entry.namespaces.as_slice())
230 }
231
232 #[must_use]
238 pub fn unconfined_keys(&self) -> Vec<&str> {
239 self.keys
240 .values()
241 .filter(|e| e.namespaces.is_empty())
242 .map(|e| e.name.as_str())
243 .collect()
244 }
245
246 pub fn add_entry(&mut self, entry: ApiKeyEntry) {
253 self.keys.insert(entry.name.clone(), entry);
254 }
255
256 pub fn register_plaintext(
265 &mut self,
266 name: &str,
267 raw_key: &str,
268 expires_at: Option<u64>,
269 ) -> Result<(), AuthError> {
270 if self.keys.contains_key(name) {
271 return Err(AuthError::Config(format!(
272 "API key with name '{name}' already exists"
273 )));
274 }
275 let argon2 = Argon2::default();
276 let mut hash = argon2
277 .hash_password(raw_key.as_bytes())
278 .map_err(|e| AuthError::Config(format!("hash error: {e}")))?
279 .to_string();
280
281 tracing::warn!(
286 name,
287 "registering API key from plaintext config — prefer runtime key generation",
288 );
289
290 let now = SystemTime::now()
291 .duration_since(UNIX_EPOCH)
292 .unwrap_or_default()
293 .as_secs();
294
295 let entry = ApiKeyEntry {
296 name: name.to_string(),
297 hash: hash.clone(),
298 expires_at,
299 created_at: now,
300 namespaces: Vec::new(),
301 admin: false,
302 };
303
304 hash.zeroize();
306
307 self.keys.insert(name.to_string(), entry);
308 self.prefix_index
309 .entry(key_prefix(raw_key))
310 .or_default()
311 .push(name.to_string());
312 debug!(name, "registered plaintext API key");
313 Ok(())
314 }
315
316 pub fn validate(&self, raw_key: &str) -> Result<String, AuthError> {
330 let prefix = key_prefix(raw_key);
331 self.check_rate_limit(prefix)?;
332
333 let argon2 = Argon2::default();
334
335 let mut matched_names: Vec<&str> = Vec::new();
339 for (stored_prefix, names) in &self.prefix_index {
340 if bool::from(stored_prefix.ct_eq(&prefix)) {
341 matched_names.extend(names.iter().map(String::as_str));
342 }
343 }
344
345 for name in &matched_names {
347 if let Some(entry) = self.keys.get(*name) {
348 match self.try_verify(&argon2, entry, raw_key) {
349 Ok(()) => return Ok(entry.name.clone()),
350 Err(AuthError::InvalidApiKey(_)) => continue,
351 Err(other) => return Err(other),
352 }
353 }
354 }
355
356 for entry in self.keys.values() {
358 if matched_names.contains(&entry.name.as_str()) {
360 continue;
361 }
362 if let Err(e) = self.try_verify(&argon2, entry, raw_key) {
363 match e {
364 AuthError::InvalidApiKey(_) => continue,
365 other => return Err(other),
366 }
367 } else {
368 return Ok(entry.name.clone());
369 }
370 }
371
372 self.record_failure(prefix);
373 Err(AuthError::InvalidApiKey("key not found".into()))
374 }
375
376 fn check_rate_limit(&self, prefix: [u8; 8]) -> Result<(), AuthError> {
378 let now = SystemTime::now()
379 .duration_since(UNIX_EPOCH)
380 .unwrap_or_default()
381 .as_secs();
382
383 if let Some(entry) = self.per_prefix_failures.get(&prefix) {
385 let (count, window_start) = *entry;
386 if now.saturating_sub(window_start) < FAILURE_WINDOW_SECS
387 && count >= MAX_PREFIX_FAILURES_PER_WINDOW
388 {
389 tracing::warn!("API key validation per-prefix rate limit exceeded");
390 return Err(AuthError::InvalidApiKey("rate limit exceeded".into()));
391 }
392 }
393
394 let window_start = self.failure_window_start.load(Ordering::Relaxed);
396 if now.saturating_sub(window_start) >= FAILURE_WINDOW_SECS {
397 self.failure_window_start.store(now, Ordering::Relaxed);
399 self.failure_count.store(0, Ordering::Relaxed);
400 }
401 if self.failure_count.load(Ordering::Relaxed) >= MAX_FAILURES_PER_WINDOW {
402 tracing::warn!("API key validation global rate limit exceeded");
403 return Err(AuthError::InvalidApiKey("rate limit exceeded".into()));
404 }
405 Ok(())
406 }
407
408 fn record_failure(&self, prefix: [u8; 8]) {
410 let now = SystemTime::now()
411 .duration_since(UNIX_EPOCH)
412 .unwrap_or_default()
413 .as_secs();
414
415 let mut entry = self.per_prefix_failures.entry(prefix).or_insert((0, now));
417 let (ref mut count, ref mut window_start) = *entry;
418 if now.saturating_sub(*window_start) >= FAILURE_WINDOW_SECS {
419 *count = 1;
420 *window_start = now;
421 } else {
422 *count += 1;
423 }
424
425 self.failure_count.fetch_add(1, Ordering::Relaxed);
427 }
428
429 fn try_verify(
435 &self,
436 argon2: &Argon2<'_>,
437 entry: &ApiKeyEntry,
438 raw_key: &str,
439 ) -> Result<(), AuthError> {
440 let parsed_hash = match PasswordHash::new(&entry.hash) {
441 Ok(h) => h,
442 Err(e) => {
443 tracing::warn!(
444 name = %entry.name,
445 error = %e,
446 "skipping API key with corrupted hash"
447 );
448 return Err(AuthError::InvalidApiKey("corrupted hash".into()));
449 }
450 };
451
452 if argon2
453 .verify_password(raw_key.as_bytes(), &parsed_hash)
454 .is_ok()
455 {
456 if let Some(expires) = entry.expires_at {
458 let now = SystemTime::now()
459 .duration_since(UNIX_EPOCH)
460 .unwrap_or_default()
461 .as_secs();
462 if now > expires {
463 return Err(AuthError::ExpiredApiKey(entry.name.clone()));
464 }
465 }
466 return Ok(());
467 }
468
469 Err(AuthError::InvalidApiKey("no match".into()))
470 }
471
472 pub fn revoke(&mut self, name: &str) -> bool {
479 let removed = self.keys.remove(name).is_some();
480 if removed {
481 self.prefix_index.retain(|_prefix, names| {
483 names.retain(|n| n != name);
484 !names.is_empty()
485 });
486 }
487 removed
488 }
489
490 pub fn list_keys(&self) -> Vec<&str> {
492 self.keys.keys().map(String::as_str).collect()
493 }
494
495 #[must_use]
497 pub fn len(&self) -> usize {
498 self.keys.len()
499 }
500
501 #[must_use]
503 pub fn is_empty(&self) -> bool {
504 self.keys.is_empty()
505 }
506}
507
508impl Default for ApiKeyStore {
509 fn default() -> Self {
510 Self::new()
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517
518 #[test]
519 fn create_and_validate() {
520 let mut store = ApiKeyStore::new();
521 let key = store.create_key("test", None).unwrap();
522 assert_eq!(store.len(), 1);
523
524 let name = store.validate(&key).unwrap();
525 assert_eq!(name, "test");
526 }
527
528 #[test]
529 fn invalid_key_rejected() {
530 let mut store = ApiKeyStore::new();
531 let _key = store.create_key("test", None).unwrap();
532
533 let result = store.validate("wrong-key");
534 assert!(result.is_err());
535 assert!(matches!(result, Err(AuthError::InvalidApiKey(_))));
536 }
537
538 #[test]
539 fn expired_key_rejected() {
540 let mut store = ApiKeyStore::new();
541 let key = store.create_key("expired", Some(0)).unwrap();
543
544 let result = store.validate(&key);
545 assert!(result.is_err());
546 assert!(matches!(result, Err(AuthError::ExpiredApiKey(_))));
547 }
548
549 #[test]
550 fn revoke_key() {
551 let mut store = ApiKeyStore::new();
552 let key = store.create_key("revokable", None).unwrap();
553
554 assert!(store.revoke("revokable"));
555 assert!(store.validate(&key).is_err());
556 assert!(!store.revoke("revokable")); }
558
559 #[test]
560 fn multiple_keys() {
561 let mut store = ApiKeyStore::new();
562 let key1 = store.create_key("admin", None).unwrap();
563 let key2 = store.create_key("reader", None).unwrap();
564 assert_eq!(store.len(), 2);
565
566 assert_eq!(store.validate(&key1).unwrap(), "admin");
567 assert_eq!(store.validate(&key2).unwrap(), "reader");
568 }
569
570 #[test]
571 fn list_keys() {
572 let mut store = ApiKeyStore::new();
573 let _k1 = store.create_key("a", None).unwrap();
574 let _k2 = store.create_key("b", None).unwrap();
575
576 let mut names = store.list_keys();
577 names.sort();
578 assert_eq!(names, vec!["a", "b"]);
579 }
580
581 #[test]
582 fn register_plaintext_duplicate_rejected() {
583 let mut store = ApiKeyStore::new();
584 store
585 .register_plaintext("admin", "my-secret", None)
586 .unwrap();
587 let err = store
588 .register_plaintext("admin", "other-secret", None)
589 .unwrap_err();
590 assert!(matches!(err, AuthError::Config(_)));
591 }
592
593 #[test]
594 fn per_prefix_rate_limit_isolates_keys() {
595 let mut store = ApiKeyStore::new();
596 let good_key = store.create_key("good", None).unwrap();
597
598 let bad_key = "definitely-wrong-key";
600 for _ in 0..MAX_PREFIX_FAILURES_PER_WINDOW {
601 let _ = store.validate(bad_key);
602 }
603
604 let result = store.validate(bad_key);
606 assert!(result.is_err());
607
608 let result = store.validate(&good_key);
610 assert!(result.is_ok());
611 }
612}