1use std::collections::HashMap;
17use std::sync::Arc;
18use std::sync::atomic::{AtomicU64, Ordering};
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 #[serde(default)]
80 pub roles: Vec<String>,
81}
82
83impl ApiKeyEntry {
84 #[must_use]
86 pub fn allows_namespace(&self, namespace: &str) -> bool {
87 self.namespaces.is_empty() || self.namespaces.iter().any(|n| n == namespace)
88 }
89}
90
91#[derive(Clone)]
102pub struct ApiKeyStore {
103 keys: HashMap<String, ApiKeyEntry>,
104 prefix_index: HashMap<[u8; 8], Vec<String>>,
106 per_prefix_failures: Arc<DashMap<[u8; 8], (u64, u64)>>,
108 failure_count: Arc<AtomicU64>,
110 failure_window_start: Arc<AtomicU64>,
112}
113
114impl std::fmt::Debug for ApiKeyStore {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 f.debug_struct("ApiKeyStore")
117 .field("keys", &self.keys.len())
118 .finish()
119 }
120}
121
122const MAX_FAILURES_PER_WINDOW: u64 = 20;
124const MAX_PREFIX_FAILURES_PER_WINDOW: u64 = 5;
126const FAILURE_WINDOW_SECS: u64 = 60;
128
129impl ApiKeyStore {
130 #[must_use]
132 pub fn new() -> Self {
133 Self {
134 keys: HashMap::new(),
135 prefix_index: HashMap::new(),
136 per_prefix_failures: Arc::new(DashMap::new()),
137 failure_count: Arc::new(AtomicU64::new(0)),
138 failure_window_start: Arc::new(AtomicU64::new(0)),
139 }
140 }
141
142 #[must_use]
148 pub fn key_exists(&self, name: &str) -> bool {
149 self.keys.contains_key(name)
150 }
151
152 pub fn create_key(&mut self, name: &str, expires_at: Option<u64>) -> Result<String, AuthError> {
157 if self.keys.contains_key(name) {
158 return Err(AuthError::Config(format!(
159 "API key with name '{name}' already exists"
160 )));
161 }
162 let mut key_bytes = [0u8; 32];
165 super::fill_random(&mut key_bytes);
166 use base64::Engine;
167 let raw_key = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(key_bytes);
168 let argon2 = Argon2::default();
169 let hash = argon2
173 .hash_password(raw_key.as_bytes())
174 .map_err(|e| AuthError::Config(format!("hash error: {e}")))?
175 .to_string();
176
177 let now = SystemTime::now()
178 .duration_since(UNIX_EPOCH)
179 .unwrap_or_default()
180 .as_secs();
181
182 let entry = ApiKeyEntry {
183 name: name.to_string(),
184 hash,
185 expires_at,
186 created_at: now,
187 namespaces: Vec::new(),
188 admin: false,
189 roles: Vec::new(),
190 };
191
192 self.keys.insert(name.to_string(), entry);
193 self.prefix_index
194 .entry(key_prefix(&raw_key))
195 .or_default()
196 .push(name.to_string());
197 debug!(name, "created API key");
198 Ok(raw_key)
199 }
200
201 pub fn bind_namespaces(&mut self, name: &str, namespaces: Vec<String>) -> bool {
206 match self.keys.get_mut(name) {
207 Some(entry) => {
208 entry.namespaces = namespaces;
209 true
210 }
211 None => false,
212 }
213 }
214
215 pub fn set_admin(&mut self, name: &str, admin: bool) -> bool {
219 match self.keys.get_mut(name) {
220 Some(entry) => {
221 entry.admin = admin;
222 true
223 }
224 None => false,
225 }
226 }
227
228 pub fn authenticate(&self, token: &str) -> Result<crate::auth::AuthContext, AuthError> {
242 let name = self.validate(token)?;
243 Ok(crate::auth::AuthContext {
244 namespaces: self.namespaces_for(&name).to_vec(),
245 admin: self.is_admin(&name),
246 roles: self.roles(&name),
247 principal: name,
248 method: crate::auth::AuthMethod::ApiKey,
249 claims: std::collections::HashMap::new(),
250 })
251 }
252
253 #[must_use]
259 pub fn entry(&self, name: &str) -> Option<&ApiKeyEntry> {
260 self.keys.get(name)
261 }
262
263 pub fn set_roles(&mut self, name: &str, roles: Vec<String>) -> bool {
267 match self.keys.get_mut(name) {
268 Some(entry) => {
269 entry.roles = roles;
270 true
271 }
272 None => false,
273 }
274 }
275
276 #[must_use]
278 pub fn roles(&self, name: &str) -> Vec<String> {
279 self.keys
280 .get(name)
281 .map(|e| e.roles.clone())
282 .unwrap_or_default()
283 }
284
285 #[must_use]
287 pub fn is_admin(&self, name: &str) -> bool {
288 self.keys.get(name).is_some_and(|e| e.admin)
289 }
290
291 #[must_use]
293 pub fn namespaces_for(&self, name: &str) -> &[String] {
294 self.keys
295 .get(name)
296 .map_or(&[][..], |entry| entry.namespaces.as_slice())
297 }
298
299 #[must_use]
305 pub fn unconfined_keys(&self) -> Vec<&str> {
306 self.keys
307 .values()
308 .filter(|e| e.namespaces.is_empty())
309 .map(|e| e.name.as_str())
310 .collect()
311 }
312
313 pub fn add_entry(&mut self, entry: ApiKeyEntry) {
320 self.keys.insert(entry.name.clone(), entry);
321 }
322
323 pub fn register_plaintext(
332 &mut self,
333 name: &str,
334 raw_key: &str,
335 expires_at: Option<u64>,
336 ) -> Result<(), AuthError> {
337 if self.keys.contains_key(name) {
338 return Err(AuthError::Config(format!(
339 "API key with name '{name}' already exists"
340 )));
341 }
342 let argon2 = Argon2::default();
343 let mut hash = argon2
344 .hash_password(raw_key.as_bytes())
345 .map_err(|e| AuthError::Config(format!("hash error: {e}")))?
346 .to_string();
347
348 tracing::warn!(
353 name,
354 "registering API key from plaintext config — prefer runtime key generation",
355 );
356
357 let now = SystemTime::now()
358 .duration_since(UNIX_EPOCH)
359 .unwrap_or_default()
360 .as_secs();
361
362 let entry = ApiKeyEntry {
363 name: name.to_string(),
364 hash: hash.clone(),
365 expires_at,
366 created_at: now,
367 namespaces: Vec::new(),
368 admin: false,
369 roles: Vec::new(),
370 };
371
372 hash.zeroize();
374
375 self.keys.insert(name.to_string(), entry);
376 self.prefix_index
377 .entry(key_prefix(raw_key))
378 .or_default()
379 .push(name.to_string());
380 debug!(name, "registered plaintext API key");
381 Ok(())
382 }
383
384 pub fn validate(&self, raw_key: &str) -> Result<String, AuthError> {
398 let prefix = key_prefix(raw_key);
399 self.check_rate_limit(prefix)?;
400
401 let argon2 = Argon2::default();
402
403 let mut matched_names: Vec<&str> = Vec::new();
407 for (stored_prefix, names) in &self.prefix_index {
408 if bool::from(stored_prefix.ct_eq(&prefix)) {
409 matched_names.extend(names.iter().map(String::as_str));
410 }
411 }
412
413 for name in &matched_names {
415 if let Some(entry) = self.keys.get(*name) {
416 match self.try_verify(&argon2, entry, raw_key) {
417 Ok(()) => return Ok(entry.name.clone()),
418 Err(AuthError::InvalidApiKey(_)) => continue,
419 Err(other) => return Err(other),
420 }
421 }
422 }
423
424 for entry in self.keys.values() {
426 if matched_names.contains(&entry.name.as_str()) {
428 continue;
429 }
430 if let Err(e) = self.try_verify(&argon2, entry, raw_key) {
431 match e {
432 AuthError::InvalidApiKey(_) => continue,
433 other => return Err(other),
434 }
435 } else {
436 return Ok(entry.name.clone());
437 }
438 }
439
440 self.record_failure(prefix);
441 Err(AuthError::InvalidApiKey("key not found".into()))
442 }
443
444 fn check_rate_limit(&self, prefix: [u8; 8]) -> Result<(), AuthError> {
446 let now = SystemTime::now()
447 .duration_since(UNIX_EPOCH)
448 .unwrap_or_default()
449 .as_secs();
450
451 if let Some(entry) = self.per_prefix_failures.get(&prefix) {
453 let (count, window_start) = *entry;
454 if now.saturating_sub(window_start) < FAILURE_WINDOW_SECS
455 && count >= MAX_PREFIX_FAILURES_PER_WINDOW
456 {
457 tracing::warn!("API key validation per-prefix rate limit exceeded");
458 return Err(AuthError::InvalidApiKey("rate limit exceeded".into()));
459 }
460 }
461
462 let window_start = self.failure_window_start.load(Ordering::Relaxed);
464 if now.saturating_sub(window_start) >= FAILURE_WINDOW_SECS {
465 self.failure_window_start.store(now, Ordering::Relaxed);
467 self.failure_count.store(0, Ordering::Relaxed);
468 }
469 if self.failure_count.load(Ordering::Relaxed) >= MAX_FAILURES_PER_WINDOW {
470 tracing::warn!("API key validation global rate limit exceeded");
471 return Err(AuthError::InvalidApiKey("rate limit exceeded".into()));
472 }
473 Ok(())
474 }
475
476 fn record_failure(&self, prefix: [u8; 8]) {
478 let now = SystemTime::now()
479 .duration_since(UNIX_EPOCH)
480 .unwrap_or_default()
481 .as_secs();
482
483 let mut entry = self.per_prefix_failures.entry(prefix).or_insert((0, now));
485 let (ref mut count, ref mut window_start) = *entry;
486 if now.saturating_sub(*window_start) >= FAILURE_WINDOW_SECS {
487 *count = 1;
488 *window_start = now;
489 } else {
490 *count += 1;
491 }
492
493 self.failure_count.fetch_add(1, Ordering::Relaxed);
495 }
496
497 fn try_verify(
503 &self,
504 argon2: &Argon2<'_>,
505 entry: &ApiKeyEntry,
506 raw_key: &str,
507 ) -> Result<(), AuthError> {
508 let parsed_hash = match PasswordHash::new(&entry.hash) {
509 Ok(h) => h,
510 Err(e) => {
511 tracing::warn!(
512 name = %entry.name,
513 error = %e,
514 "skipping API key with corrupted hash"
515 );
516 return Err(AuthError::InvalidApiKey("corrupted hash".into()));
517 }
518 };
519
520 if argon2
521 .verify_password(raw_key.as_bytes(), &parsed_hash)
522 .is_ok()
523 {
524 if let Some(expires) = entry.expires_at {
526 let now = SystemTime::now()
527 .duration_since(UNIX_EPOCH)
528 .unwrap_or_default()
529 .as_secs();
530 if now > expires {
531 return Err(AuthError::ExpiredApiKey(entry.name.clone()));
532 }
533 }
534 return Ok(());
535 }
536
537 Err(AuthError::InvalidApiKey("no match".into()))
538 }
539
540 pub fn revoke(&mut self, name: &str) -> bool {
547 let removed = self.keys.remove(name).is_some();
548 if removed {
549 self.prefix_index.retain(|_prefix, names| {
551 names.retain(|n| n != name);
552 !names.is_empty()
553 });
554 }
555 removed
556 }
557
558 pub fn list_keys(&self) -> Vec<&str> {
560 self.keys.keys().map(String::as_str).collect()
561 }
562
563 #[must_use]
565 pub fn len(&self) -> usize {
566 self.keys.len()
567 }
568
569 #[must_use]
571 pub fn is_empty(&self) -> bool {
572 self.keys.is_empty()
573 }
574}
575
576impl Default for ApiKeyStore {
577 fn default() -> Self {
578 Self::new()
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 #[test]
587 fn create_and_validate() {
588 let mut store = ApiKeyStore::new();
589 let key = store.create_key("test", None).unwrap();
590 assert_eq!(store.len(), 1);
591
592 let name = store.validate(&key).unwrap();
593 assert_eq!(name, "test");
594 }
595
596 #[test]
597 fn invalid_key_rejected() {
598 let mut store = ApiKeyStore::new();
599 let _key = store.create_key("test", None).unwrap();
600
601 let result = store.validate("wrong-key");
602 assert!(result.is_err());
603 assert!(matches!(result, Err(AuthError::InvalidApiKey(_))));
604 }
605
606 #[test]
607 fn expired_key_rejected() {
608 let mut store = ApiKeyStore::new();
609 let key = store.create_key("expired", Some(0)).unwrap();
611
612 let result = store.validate(&key);
613 assert!(result.is_err());
614 assert!(matches!(result, Err(AuthError::ExpiredApiKey(_))));
615 }
616
617 #[test]
618 fn revoke_key() {
619 let mut store = ApiKeyStore::new();
620 let key = store.create_key("revokable", None).unwrap();
621
622 assert!(store.revoke("revokable"));
623 assert!(store.validate(&key).is_err());
624 assert!(!store.revoke("revokable")); }
626
627 #[test]
628 fn multiple_keys() {
629 let mut store = ApiKeyStore::new();
630 let key1 = store.create_key("admin", None).unwrap();
631 let key2 = store.create_key("reader", None).unwrap();
632 assert_eq!(store.len(), 2);
633
634 assert_eq!(store.validate(&key1).unwrap(), "admin");
635 assert_eq!(store.validate(&key2).unwrap(), "reader");
636 }
637
638 #[test]
639 fn list_keys() {
640 let mut store = ApiKeyStore::new();
641 let _k1 = store.create_key("a", None).unwrap();
642 let _k2 = store.create_key("b", None).unwrap();
643
644 let mut names = store.list_keys();
645 names.sort();
646 assert_eq!(names, vec!["a", "b"]);
647 }
648
649 #[test]
650 fn register_plaintext_duplicate_rejected() {
651 let mut store = ApiKeyStore::new();
652 store
653 .register_plaintext("admin", "my-secret", None)
654 .unwrap();
655 let err = store
656 .register_plaintext("admin", "other-secret", None)
657 .unwrap_err();
658 assert!(matches!(err, AuthError::Config(_)));
659 }
660
661 #[test]
662 fn per_prefix_rate_limit_isolates_keys() {
663 let mut store = ApiKeyStore::new();
664 let good_key = store.create_key("good", None).unwrap();
665
666 let bad_key = "definitely-wrong-key";
668 for _ in 0..MAX_PREFIX_FAILURES_PER_WINDOW {
669 let _ = store.validate(bad_key);
670 }
671
672 let result = store.validate(bad_key);
674 assert!(result.is_err());
675
676 let result = store.validate(&good_key);
678 assert!(result.is_ok());
679 }
680}