1use std::collections::HashMap;
26use std::sync::atomic::{AtomicU64, Ordering};
27use std::sync::Arc;
28
29use aes_gcm::aead::Aead;
30use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
31use hmac::{Hmac, Mac};
32use sha2::Sha256;
33use zeroize::Zeroize;
34
35use crate::auth::error::AuthError;
36
37const NONCE_SIZE: usize = 12;
39
40#[derive(Clone)]
46pub struct SecretKey(Vec<u8>);
47
48impl SecretKey {
49 #[must_use]
51 pub fn new(bytes: Vec<u8>) -> Self {
52 Self(bytes)
53 }
54
55 #[must_use]
57 pub fn as_bytes(&self) -> &[u8] {
58 &self.0
59 }
60}
61
62impl Drop for SecretKey {
63 fn drop(&mut self) {
64 self.0.zeroize();
65 }
66}
67
68impl std::fmt::Debug for SecretKey {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 write!(f, "SecretKey([REDACTED; {} bytes])", self.0.len())
71 }
72}
73
74impl AsRef<[u8]> for SecretKey {
75 fn as_ref(&self) -> &[u8] {
76 &self.0
77 }
78}
79
80pub trait KeyProvider: Send + Sync + std::fmt::Debug {
88 fn current_key(&self) -> Result<(SecretKey, String), AuthError>;
90
91 fn key_by_id(&self, key_id: &str) -> Result<SecretKey, AuthError>;
93
94 fn key_ids(&self) -> Vec<String>;
96}
97
98pub struct FileKeyProvider {
104 keys: HashMap<String, Vec<u8>>,
105 current_key_id: String,
106}
107
108impl Drop for FileKeyProvider {
109 fn drop(&mut self) {
110 for value in self.keys.values_mut() {
113 value.fill(0);
114 }
115 std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
117 std::hint::black_box(&self.keys);
118 }
119}
120
121impl std::fmt::Debug for FileKeyProvider {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 f.debug_struct("FileKeyProvider")
124 .field("current_key_id", &self.current_key_id)
125 .field("key_count", &self.keys.len())
126 .finish()
127 }
128}
129
130impl FileKeyProvider {
131 pub fn from_hex_key(hex_key: &str, key_id: &str) -> Result<Self, AuthError> {
133 let key = hex_decode(hex_key)?;
134 if key.len() != 32 {
135 return Err(AuthError::Config(format!(
136 "AES-256 key must be 32 bytes, got {}",
137 key.len()
138 )));
139 }
140
141 let mut keys = HashMap::new();
142 keys.insert(key_id.to_string(), key);
143
144 Ok(Self {
145 keys,
146 current_key_id: key_id.to_string(),
147 })
148 }
149
150 pub fn add_key(&mut self, hex_key: &str, key_id: &str) -> Result<(), AuthError> {
152 let key = hex_decode(hex_key)?;
153 if key.len() != 32 {
154 return Err(AuthError::Config(format!(
155 "AES-256 key must be 32 bytes, got {}",
156 key.len()
157 )));
158 }
159 self.keys.insert(key_id.to_string(), key);
160 self.current_key_id = key_id.to_string();
161 Ok(())
162 }
163}
164
165impl KeyProvider for FileKeyProvider {
166 fn current_key(&self) -> Result<(SecretKey, String), AuthError> {
167 let key = self
168 .keys
169 .get(&self.current_key_id)
170 .ok_or_else(|| AuthError::KeyNotFound(self.current_key_id.clone()))?
171 .clone();
172 Ok((SecretKey::new(key), self.current_key_id.clone()))
173 }
174
175 fn key_by_id(&self, key_id: &str) -> Result<SecretKey, AuthError> {
176 self.keys
177 .get(key_id)
178 .cloned()
179 .map(SecretKey::new)
180 .ok_or_else(|| AuthError::KeyNotFound(key_id.to_string()))
181 }
182
183 fn key_ids(&self) -> Vec<String> {
184 self.keys.keys().cloned().collect()
185 }
186}
187
188pub struct EnvKeyProvider {
215 env_var: String,
216 key_id: String,
217 cache: parking_lot::Mutex<Option<(SecretKey, std::time::Instant)>>,
219 grace_period: std::time::Duration,
221}
222
223impl std::fmt::Debug for EnvKeyProvider {
224 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225 f.debug_struct("EnvKeyProvider")
226 .field("env_var", &self.env_var)
227 .field("key_id", &self.key_id)
228 .field("has_cached_key", &self.cache.lock().is_some())
229 .finish()
230 }
231}
232
233const DEFAULT_ENV_KEY_GRACE_PERIOD: std::time::Duration = std::time::Duration::from_secs(5);
238
239impl EnvKeyProvider {
240 #[must_use]
245 pub fn new(env_var: &str, key_id: &str) -> Self {
246 Self {
247 env_var: env_var.to_string(),
248 key_id: key_id.to_string(),
249 cache: parking_lot::Mutex::new(None),
250 grace_period: DEFAULT_ENV_KEY_GRACE_PERIOD,
251 }
252 }
253
254 #[must_use]
256 pub fn with_grace_period(mut self, duration: std::time::Duration) -> Self {
257 self.grace_period = duration;
258 self
259 }
260
261 pub fn clear_cache(&self) {
263 *self.cache.lock() = None;
264 }
265}
266
267impl KeyProvider for EnvKeyProvider {
268 fn current_key(&self) -> Result<(SecretKey, String), AuthError> {
269 match std::env::var(&self.env_var) {
270 Ok(hex) => {
271 let key = hex_decode(&hex)?;
272 if key.len() != 32 {
273 return Err(AuthError::Config(format!(
274 "AES-256 key must be 32 bytes, got {}",
275 key.len()
276 )));
277 }
278 let secret = SecretKey::new(key);
279 *self.cache.lock() = Some((secret.clone(), std::time::Instant::now()));
281 Ok((secret, self.key_id.clone()))
282 }
283 Err(_) => {
284 let guard = self.cache.lock();
286 if let Some((ref cached_key, fetched_at)) = *guard {
287 if fetched_at.elapsed() < self.grace_period {
288 tracing::warn!(
289 env_var = %self.env_var,
290 grace_remaining_secs = (self.grace_period.saturating_sub(fetched_at.elapsed())).as_secs(),
291 "env var missing, using cached key within grace period"
292 );
293 metrics::counter!("chronix.auth.env_key.grace_hit").increment(1);
294 return Ok((cached_key.clone(), self.key_id.clone()));
295 }
296 }
297 drop(guard);
298 Err(AuthError::Config(format!(
299 "env var {} not set",
300 self.env_var
301 )))
302 }
303 }
304 }
305
306 fn key_by_id(&self, key_id: &str) -> Result<SecretKey, AuthError> {
307 if key_id == self.key_id {
308 self.current_key().map(|(k, _)| k)
309 } else {
310 Err(AuthError::KeyNotFound(key_id.to_string()))
311 }
312 }
313
314 fn key_ids(&self) -> Vec<String> {
315 vec![self.key_id.clone()]
316 }
317}
318
319#[derive(Debug)]
326pub struct EncryptionService {
327 provider: Box<dyn KeyProvider>,
328 invocation_count: Arc<AtomicU64>,
331}
332
333const NIST_KEY_USAGE_LIMIT: u64 = 1 << 32;
335
336impl EncryptionService {
337 #[must_use]
339 pub fn new(provider: Box<dyn KeyProvider>) -> Self {
340 Self {
341 provider,
342 invocation_count: Arc::new(AtomicU64::new(0)),
343 }
344 }
345
346 #[must_use]
348 pub fn invocation_count(&self) -> u64 {
349 self.invocation_count.load(Ordering::Relaxed)
350 }
351
352 fn derive_subkey(master: &[u8], domain: &[u8]) -> SecretKey {
359 let mut mac =
360 <Hmac<Sha256> as KeyInit>::new_from_slice(master).expect("HMAC accepts any key size");
361 mac.update(domain);
362 SecretKey::new(mac.finalize().into_bytes().to_vec())
363 }
364
365 pub fn encrypt(&self, plaintext: &[u8]) -> Result<(Vec<u8>, String), AuthError> {
370 let count = self.invocation_count.fetch_add(1, Ordering::Relaxed);
371 if count >= NIST_KEY_USAGE_LIMIT {
374 return Err(AuthError::Encryption(format!(
375 "AES-GCM key usage limit reached ({count} encryptions); \
376 rotate key to avoid nonce collision risk (NIST SP 800-38D)"
377 )));
378 }
379 if count == NIST_KEY_USAGE_LIMIT - 1 {
380 tracing::warn!(
381 count = count + 1,
382 "AES-GCM key approaching 2^32 encryption limit; rotate key soon"
383 );
384 }
385
386 let (master_key, key_id) = self.provider.current_key()?;
387 let enc_key = Self::derive_subkey(master_key.as_bytes(), b"chronix-aes-gcm-enc-v1");
388
389 let cipher = Aes256Gcm::new_from_slice(enc_key.as_bytes())
390 .map_err(|e| AuthError::Encryption(format!("key error: {e}")))?;
391
392 let nonce_bytes = generate_nonce();
394 let nonce = Nonce::try_from(&nonce_bytes[..])
395 .map_err(|_| AuthError::Config("nonce must be 12 bytes".into()))?;
396
397 let ciphertext = cipher
398 .encrypt(&nonce, plaintext)
399 .map_err(|e| AuthError::Encryption(e.to_string()))?;
400
401 let mut result = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
403 result.extend_from_slice(&nonce_bytes);
404 result.extend_from_slice(&ciphertext);
405
406 Ok((result, key_id))
407 }
408
409 pub fn decrypt(&self, data: &[u8], key_id: &str) -> Result<Vec<u8>, AuthError> {
413 if data.len() < NONCE_SIZE {
414 return Err(AuthError::Decryption("data too short for nonce".into()));
415 }
416
417 let master_key = self.provider.key_by_id(key_id)?;
418 let enc_key = Self::derive_subkey(master_key.as_bytes(), b"chronix-aes-gcm-enc-v1");
419
420 let cipher = Aes256Gcm::new_from_slice(enc_key.as_bytes())
421 .map_err(|e| AuthError::Decryption(format!("key error: {e}")))?;
422
423 let nonce = Nonce::try_from(&data[..NONCE_SIZE])
424 .map_err(|_| AuthError::Config("nonce must be 12 bytes".into()))?;
425 let ciphertext = &data[NONCE_SIZE..];
426
427 cipher
428 .decrypt(&nonce, ciphertext)
429 .map_err(|e| AuthError::Decryption(e.to_string()))
430 }
431
432 pub fn hmac(&self, data: &[u8]) -> Result<Vec<u8>, AuthError> {
434 let (master_key, _) = self.provider.current_key()?;
435 let hmac_key = Self::derive_subkey(master_key.as_bytes(), b"chronix-hmac-sha256-v1");
436
437 let mut mac = <Hmac<Sha256> as KeyInit>::new_from_slice(hmac_key.as_bytes())
438 .map_err(|e| AuthError::Encryption(format!("HMAC error: {e}")))?;
439 mac.update(data);
440 Ok(mac.finalize().into_bytes().to_vec())
441 }
442
443 pub fn verify_hmac(
445 &self,
446 data: &[u8],
447 expected: &[u8],
448 key_id: &str,
449 ) -> Result<bool, AuthError> {
450 let master_key = self.provider.key_by_id(key_id)?;
451 let hmac_key = Self::derive_subkey(master_key.as_bytes(), b"chronix-hmac-sha256-v1");
452
453 let mut mac = <Hmac<Sha256> as KeyInit>::new_from_slice(hmac_key.as_bytes())
454 .map_err(|e| AuthError::Encryption(format!("HMAC error: {e}")))?;
455 mac.update(data);
456
457 Ok(mac.verify_slice(expected).is_ok())
458 }
459
460 pub fn current_key_id(&self) -> Result<String, AuthError> {
462 self.provider.current_key().map(|(_, id)| id)
463 }
464
465 #[must_use]
467 pub fn key_ids(&self) -> Vec<String> {
468 self.provider.key_ids()
469 }
470}
471
472fn generate_nonce() -> [u8; NONCE_SIZE] {
474 let mut nonce = [0u8; NONCE_SIZE];
475 super::fill_random(&mut nonce);
476 nonce
477}
478
479fn hex_decode(hex: &str) -> Result<Vec<u8>, AuthError> {
481 if !hex.len().is_multiple_of(2) {
482 return Err(AuthError::Config("hex string must have even length".into()));
483 }
484
485 (0..hex.len())
486 .step_by(2)
487 .map(|i| {
488 u8::from_str_radix(&hex[i..i + 2], 16)
489 .map_err(|e| AuthError::Config(format!("invalid hex: {e}")))
490 })
491 .collect()
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 const TEST_KEY_HEX: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
499
500 fn test_service() -> EncryptionService {
501 let provider = FileKeyProvider::from_hex_key(TEST_KEY_HEX, "key-1").unwrap();
502 EncryptionService::new(Box::new(provider))
503 }
504
505 #[test]
506 fn encrypt_decrypt_roundtrip() {
507 let svc = test_service();
508 let plaintext = b"The quick brown fox jumps over the lazy dog";
509
510 let (ciphertext, key_id) = svc.encrypt(plaintext).unwrap();
511 assert_ne!(ciphertext.as_slice(), plaintext.as_slice());
512 assert_eq!(key_id, "key-1");
513
514 let decrypted = svc.decrypt(&ciphertext, &key_id).unwrap();
515 assert_eq!(decrypted, plaintext);
516 }
517
518 #[test]
519 fn empty_plaintext_roundtrip() {
520 let svc = test_service();
521 let plaintext = b"";
522
523 let (ciphertext, key_id) = svc.encrypt(plaintext).unwrap();
524 let decrypted = svc.decrypt(&ciphertext, &key_id).unwrap();
525 assert_eq!(decrypted, plaintext);
526 }
527
528 #[test]
529 fn large_data_roundtrip() {
530 let svc = test_service();
531 let plaintext: Vec<u8> = (0..100_000).map(|i| (i % 256) as u8).collect();
532
533 let (ciphertext, key_id) = svc.encrypt(&plaintext).unwrap();
534 let decrypted = svc.decrypt(&ciphertext, &key_id).unwrap();
535 assert_eq!(decrypted, plaintext);
536 }
537
538 #[test]
539 fn tampered_data_fails() {
540 let svc = test_service();
541 let plaintext = b"sensitive data";
542
543 let (mut ciphertext, key_id) = svc.encrypt(plaintext).unwrap();
544 if let Some(byte) = ciphertext.get_mut(NONCE_SIZE + 1) {
546 *byte ^= 0xFF;
547 }
548
549 let err = svc.decrypt(&ciphertext, &key_id).unwrap_err();
550 assert!(matches!(err, AuthError::Decryption(_)));
551 }
552
553 #[test]
554 fn wrong_key_fails() {
555 let svc = test_service();
556 let plaintext = b"test data";
557
558 let (ciphertext, _) = svc.encrypt(plaintext).unwrap();
559
560 let err = svc.decrypt(&ciphertext, "nonexistent-key").unwrap_err();
562 assert!(matches!(err, AuthError::KeyNotFound(_)));
563 }
564
565 #[test]
566 fn key_rotation() {
567 let provider1 = FileKeyProvider::from_hex_key(TEST_KEY_HEX, "key-1").unwrap();
568
569 let svc1 = EncryptionService::new(Box::new(provider1));
571 let plaintext = b"data with key 1";
572 let (ct1, kid1) = svc1.encrypt(plaintext).unwrap();
573 assert_eq!(kid1, "key-1");
574
575 let mut provider2 = FileKeyProvider::from_hex_key(TEST_KEY_HEX, "key-1").unwrap();
577 let key2_hex = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210";
578 provider2.add_key(key2_hex, "key-2").unwrap();
579
580 let svc2 = EncryptionService::new(Box::new(provider2));
581
582 let (ct2, kid2) = svc2.encrypt(b"data with key 2").unwrap();
584 assert_eq!(kid2, "key-2");
585
586 let dec1 = svc2.decrypt(&ct1, "key-1").unwrap();
588 assert_eq!(dec1, plaintext);
589
590 let dec2 = svc2.decrypt(&ct2, "key-2").unwrap();
592 assert_eq!(dec2, b"data with key 2");
593 }
594
595 #[test]
596 fn hmac_verify() {
597 let svc = test_service();
598 let data = b"manifest data to verify";
599
600 let mac = svc.hmac(data).unwrap();
601 assert!(svc.verify_hmac(data, &mac, "key-1").unwrap());
602
603 let mut tampered = data.to_vec();
605 tampered[0] ^= 0xFF;
606 assert!(!svc.verify_hmac(&tampered, &mac, "key-1").unwrap());
607 }
608
609 #[test]
610 fn hmac_tampered_mac_fails() {
611 let svc = test_service();
612 let data = b"important data";
613
614 let mut mac = svc.hmac(data).unwrap();
615 mac[0] ^= 0xFF; assert!(!svc.verify_hmac(data, &mac, "key-1").unwrap());
617 }
618
619 #[test]
620 fn short_key_rejected() {
621 let result = FileKeyProvider::from_hex_key("0123456789abcdef", "k");
622 assert!(result.is_err());
623 }
624
625 #[test]
626 fn invalid_hex_rejected() {
627 let result = FileKeyProvider::from_hex_key("zzzz", "k");
628 assert!(result.is_err());
629 }
630
631 #[test]
632 fn data_too_short_for_nonce() {
633 let svc = test_service();
634 let err = svc.decrypt(&[0u8; 5], "key-1").unwrap_err();
635 assert!(matches!(err, AuthError::Decryption(_)));
636 }
637
638 #[test]
639 fn hex_decode_works() {
640 assert_eq!(hex_decode("48656c6c6f").unwrap(), b"Hello");
641 assert_eq!(hex_decode("").unwrap(), b"");
642 assert!(hex_decode("0").is_err()); }
644
645 #[test]
648 fn env_key_default_grace_period_is_5s() {
649 let provider = EnvKeyProvider::new("CHRONIX_TEST_KEY_NOT_SET", "k1");
650 assert_eq!(provider.grace_period, std::time::Duration::from_secs(5));
651 }
652
653 #[test]
654 fn env_key_custom_grace_period() {
655 let provider = EnvKeyProvider::new("CHRONIX_TEST_KEY_NOT_SET", "k1")
656 .with_grace_period(std::time::Duration::from_secs(2));
657 assert_eq!(provider.grace_period, std::time::Duration::from_secs(2));
658 }
659
660 #[test]
661 fn env_key_clear_cache_discards_key() {
662 let provider = EnvKeyProvider::new("CHRONIX_NEVER_SET_12345", "k1");
664 {
665 let key_bytes = vec![0u8; 32];
666 *provider.cache.lock() = Some((SecretKey::new(key_bytes), std::time::Instant::now()));
667 }
668
669 assert!(provider.current_key().is_ok());
671
672 provider.clear_cache();
674 assert!(provider.current_key().is_err());
675 }
676
677 #[test]
678 fn env_key_expired_grace_returns_error() {
679 let provider = EnvKeyProvider::new("CHRONIX_NEVER_SET_67890", "k1")
680 .with_grace_period(std::time::Duration::from_millis(0));
681
682 {
684 let key_bytes = vec![0u8; 32];
685 *provider.cache.lock() = Some((SecretKey::new(key_bytes), std::time::Instant::now()));
686 }
687
688 assert!(provider.current_key().is_err());
690 }
691}