1use std::{
6 collections::HashMap,
7 sync::{
8 Arc,
9 atomic::{AtomicU64, Ordering},
10 },
11};
12
13use chrono::{DateTime, Duration, Utc};
14
15pub type KeyVersion = u16;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum KeyVersionStatus {
21 Active,
23 Expiring,
25 Expired,
27 Compromised,
29}
30
31impl std::fmt::Display for KeyVersionStatus {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 Self::Active => write!(f, "active"),
35 Self::Expiring => write!(f, "expiring"),
36 Self::Expired => write!(f, "expired"),
37 Self::Compromised => write!(f, "compromised"),
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct KeyVersionMetadata {
45 pub version: KeyVersion,
47 pub issued_at: DateTime<Utc>,
49 pub expires_at: DateTime<Utc>,
51 pub status: KeyVersionStatus,
53 pub is_current: bool,
55 pub compromise_reason: Option<String>,
57}
58
59impl KeyVersionMetadata {
60 pub fn new(version: KeyVersion, ttl_days: u32) -> Self {
62 let now = Utc::now();
63 Self {
64 version,
65 issued_at: now,
66 expires_at: now + Duration::days(ttl_days as i64),
67 status: KeyVersionStatus::Active,
68 is_current: false,
69 compromise_reason: None,
70 }
71 }
72
73 pub fn is_expired(&self) -> bool {
75 Utc::now() > self.expires_at
76 }
77
78 pub fn is_expiring_soon(&self) -> bool {
80 let remaining = self.expires_at - Utc::now();
81 remaining < Duration::days(14) && !self.is_expired()
82 }
83
84 pub fn time_until_expiry(&self) -> Duration {
86 self.expires_at - Utc::now()
87 }
88
89 pub fn ttl_consumed_percent(&self) -> u32 {
91 let total_ttl = self.expires_at - self.issued_at;
92 let elapsed = Utc::now() - self.issued_at;
93 if total_ttl.num_seconds() <= 0 {
94 100
95 } else {
96 let percent = (elapsed.num_seconds() as f64 / total_ttl.num_seconds() as f64) * 100.0;
97 percent.min(100.0) as u32
98 }
99 }
100
101 pub fn should_refresh(&self) -> bool {
103 self.status == KeyVersionStatus::Active && self.ttl_consumed_percent() >= 80
104 }
105
106 pub fn update_status(&mut self) {
108 match self.status {
109 KeyVersionStatus::Compromised => {}, KeyVersionStatus::Active => {
111 if self.is_expired() {
112 self.status = KeyVersionStatus::Expired;
113 } else if self.is_expiring_soon() {
114 self.status = KeyVersionStatus::Expiring;
115 }
116 },
117 KeyVersionStatus::Expiring => {
118 if self.is_expired() {
119 self.status = KeyVersionStatus::Expired;
120 }
121 },
122 KeyVersionStatus::Expired => {}, }
124 }
125
126 pub fn mark_compromised(&mut self, reason: impl Into<String>) {
128 self.status = KeyVersionStatus::Compromised;
129 self.compromise_reason = Some(reason.into());
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135pub enum RotationSchedule {
136 Manual,
138 Cron(String),
140 Interval(u32),
142}
143
144impl std::fmt::Display for RotationSchedule {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 match self {
147 Self::Manual => write!(f, "manual"),
148 Self::Cron(expr) => write!(f, "cron: {}", expr),
149 Self::Interval(days) => write!(f, "every {} days", days),
150 }
151 }
152}
153
154#[derive(Debug, Clone)]
156pub struct RotationConfig {
157 pub ttl_days: u32,
159 pub refresh_threshold_percent: u32,
161 pub schedule: RotationSchedule,
163 pub max_retained_versions: usize,
165}
166
167impl RotationConfig {
168 pub fn new() -> Self {
170 Self {
171 ttl_days: 365,
172 refresh_threshold_percent: 80,
173 schedule: RotationSchedule::Manual,
174 max_retained_versions: 10,
175 }
176 }
177
178 pub fn with_ttl_days(mut self, days: u32) -> Self {
180 self.ttl_days = days;
181 self
182 }
183
184 pub fn with_refresh_threshold(mut self, percent: u32) -> Self {
186 self.refresh_threshold_percent = percent.min(99);
187 self
188 }
189
190 pub fn with_schedule(mut self, schedule: RotationSchedule) -> Self {
192 self.schedule = schedule;
193 self
194 }
195}
196
197impl Default for RotationConfig {
198 fn default() -> Self {
199 Self::new()
200 }
201}
202
203#[derive(Debug, Clone)]
205pub struct RotationMetrics {
206 total_rotations: Arc<AtomicU64>,
208 failed_rotations: Arc<AtomicU64>,
210 last_rotation: Arc<std::sync::Mutex<Option<DateTime<Utc>>>>,
212 last_rotation_duration_ms: Arc<AtomicU64>,
214}
215
216impl RotationMetrics {
217 pub fn new() -> Self {
219 Self {
220 total_rotations: Arc::new(AtomicU64::new(0)),
221 failed_rotations: Arc::new(AtomicU64::new(0)),
222 last_rotation: Arc::new(std::sync::Mutex::new(None)),
223 last_rotation_duration_ms: Arc::new(AtomicU64::new(0)),
224 }
225 }
226
227 pub fn record_rotation(&self, duration_ms: u64) {
229 self.total_rotations.fetch_add(1, Ordering::Relaxed);
230 self.last_rotation_duration_ms.store(duration_ms, Ordering::Relaxed);
231 if let Ok(mut last) = self.last_rotation.lock() {
232 *last = Some(Utc::now());
233 }
234 }
235
236 pub fn record_failure(&self) {
238 self.failed_rotations.fetch_add(1, Ordering::Relaxed);
239 }
240
241 pub fn total_rotations(&self) -> u64 {
243 self.total_rotations.load(Ordering::Relaxed)
244 }
245
246 pub fn failed_rotations(&self) -> u64 {
248 self.failed_rotations.load(Ordering::Relaxed)
249 }
250
251 pub fn success_rate_percent(&self) -> u32 {
253 let total = self.total_rotations();
254 if total == 0 {
255 100
256 } else {
257 let failed = self.failed_rotations();
258 let successful = total - failed;
259 ((successful as f64 / total as f64) * 100.0) as u32
260 }
261 }
262
263 pub fn last_rotation(&self) -> Option<DateTime<Utc>> {
265 if let Ok(last) = self.last_rotation.lock() {
266 *last
267 } else {
268 None
269 }
270 }
271
272 pub fn last_rotation_duration_ms(&self) -> u64 {
274 self.last_rotation_duration_ms.load(Ordering::Relaxed)
275 }
276}
277
278impl Default for RotationMetrics {
279 fn default() -> Self {
280 Self::new()
281 }
282}
283
284#[derive(Debug, Clone)]
286pub struct VersionedKeyStorage {
287 versions: Arc<std::sync::Mutex<HashMap<KeyVersion, KeyVersionMetadata>>>,
289 current_version: Arc<std::sync::Mutex<KeyVersion>>,
291 next_version: Arc<AtomicU64>,
293}
294
295impl VersionedKeyStorage {
296 pub fn new() -> Self {
298 Self {
299 versions: Arc::new(std::sync::Mutex::new(HashMap::new())),
300 current_version: Arc::new(std::sync::Mutex::new(0)),
301 next_version: Arc::new(AtomicU64::new(1)),
302 }
303 }
304
305 pub fn add_version(&self, metadata: KeyVersionMetadata) -> Result<KeyVersion, String> {
307 let mut versions =
308 self.versions.lock().map_err(|e| format!("Failed to lock versions: {}", e))?;
309
310 let version = metadata.version;
311 versions.insert(version, metadata);
312 Ok(version)
313 }
314
315 pub fn set_current_version(&self, version: KeyVersion) -> Result<(), String> {
317 let versions =
318 self.versions.lock().map_err(|e| format!("Failed to lock versions: {}", e))?;
319
320 if !versions.contains_key(&version) {
321 return Err(format!("Version {} not found", version));
322 }
323
324 let mut current = self
325 .current_version
326 .lock()
327 .map_err(|e| format!("Failed to lock current version: {}", e))?;
328 *current = version;
329 Ok(())
330 }
331
332 pub fn get_current_version(&self) -> Result<KeyVersion, String> {
334 let current = self
335 .current_version
336 .lock()
337 .map_err(|e| format!("Failed to lock current version: {}", e))?;
338 Ok(*current)
339 }
340
341 pub fn get_version(&self, version: KeyVersion) -> Result<Option<KeyVersionMetadata>, String> {
343 let versions =
344 self.versions.lock().map_err(|e| format!("Failed to lock versions: {}", e))?;
345 Ok(versions.get(&version).cloned())
346 }
347
348 pub fn get_all_versions(&self) -> Result<Vec<KeyVersionMetadata>, String> {
350 let versions =
351 self.versions.lock().map_err(|e| format!("Failed to lock versions: {}", e))?;
352
353 let mut all_versions: Vec<_> = versions.values().cloned().collect();
354 all_versions.sort_by(|a, b| b.issued_at.cmp(&a.issued_at));
355 Ok(all_versions)
356 }
357
358 pub fn next_version_number(&self) -> KeyVersion {
360 let next = self.next_version.fetch_add(1, Ordering::Relaxed);
361 next as KeyVersion
362 }
363}
364
365impl Default for VersionedKeyStorage {
366 fn default() -> Self {
367 Self::new()
368 }
369}
370
371#[derive(Debug, Clone)]
373pub struct CredentialRotationManager {
374 config: Arc<RotationConfig>,
376 storage: Arc<VersionedKeyStorage>,
378 metrics: Arc<RotationMetrics>,
380}
381
382impl CredentialRotationManager {
383 pub fn new(config: RotationConfig) -> Self {
385 Self {
386 config: Arc::new(config),
387 storage: Arc::new(VersionedKeyStorage::new()),
388 metrics: Arc::new(RotationMetrics::new()),
389 }
390 }
391
392 pub fn initialize_key(&self) -> Result<KeyVersion, String> {
394 let version = self.storage.next_version_number();
395 let metadata = KeyVersionMetadata::new(version, self.config.ttl_days);
396 self.storage.add_version(metadata)?;
397 self.storage.set_current_version(version)?;
398 Ok(version)
399 }
400
401 pub fn rotate_key(&self) -> Result<KeyVersion, String> {
403 let start = std::time::Instant::now();
404
405 let new_version = self.storage.next_version_number();
406 let mut metadata = KeyVersionMetadata::new(new_version, self.config.ttl_days);
407
408 metadata.is_current = true;
410 self.storage.add_version(metadata)?;
411 self.storage.set_current_version(new_version)?;
412
413 let duration_ms = start.elapsed().as_millis() as u64;
414 self.metrics.record_rotation(duration_ms);
415
416 Ok(new_version)
417 }
418
419 pub fn get_current_version(&self) -> Result<KeyVersion, String> {
421 self.storage.get_current_version()
422 }
423
424 pub fn needs_refresh(&self) -> Result<bool, String> {
426 let current_version = self.storage.get_current_version()?;
427 if let Some(metadata) = self.storage.get_version(current_version)? {
428 Ok(metadata.should_refresh())
429 } else {
430 Ok(false)
431 }
432 }
433
434 pub fn get_current_metadata(&self) -> Result<Option<KeyVersionMetadata>, String> {
436 let current_version = self.storage.get_current_version()?;
437 self.storage.get_version(current_version)
438 }
439
440 pub fn extract_version_from_ciphertext(ciphertext: &[u8]) -> Result<KeyVersion, String> {
442 if ciphertext.len() < 2 {
443 return Err("Ciphertext too short for version".to_string());
444 }
445 let version = u16::from_be_bytes([ciphertext[0], ciphertext[1]]);
446 Ok(version)
447 }
448
449 pub fn can_decrypt_with_version(&self, version: KeyVersion) -> Result<bool, String> {
451 if let Some(metadata) = self.storage.get_version(version)? {
452 Ok(metadata.status != KeyVersionStatus::Compromised)
454 } else {
455 Ok(false)
456 }
457 }
458
459 pub fn metrics(&self) -> Arc<RotationMetrics> {
461 Arc::clone(&self.metrics)
462 }
463
464 pub fn get_version_history(&self) -> Result<Vec<KeyVersionMetadata>, String> {
466 self.storage.get_all_versions()
467 }
468
469 pub fn has_versions_needing_attention(&self) -> Result<bool, String> {
473 let history = self.get_version_history()?;
474 Ok(history
475 .iter()
476 .any(|m| m.is_expiring_soon() || m.status == KeyVersionStatus::Compromised))
477 }
478
479 pub fn active_versions_count(&self) -> Result<usize, String> {
481 let history = self.get_version_history()?;
482 Ok(history.iter().filter(|m| m.status == KeyVersionStatus::Active).count())
483 }
484
485 pub fn expired_versions_count(&self) -> Result<usize, String> {
487 let history = self.get_version_history()?;
488 Ok(history.iter().filter(|m| m.status == KeyVersionStatus::Expired).count())
489 }
490
491 pub fn compromised_versions_count(&self) -> Result<usize, String> {
493 let history = self.get_version_history()?;
494 Ok(history.iter().filter(|m| m.status == KeyVersionStatus::Compromised).count())
495 }
496
497 pub fn current_version_needs_refresh(&self) -> Result<bool, String> {
499 let current_version = self.get_current_version()?;
500 if let Some(metadata) = self.storage.get_version(current_version)? {
501 Ok(metadata.should_refresh())
502 } else {
503 Ok(false)
504 }
505 }
506
507 pub fn emergency_rotate(&self, reason: impl Into<String>) -> Result<KeyVersion, String> {
509 let current_version = self.get_current_version()?;
510 if let Some(mut metadata) = self.storage.get_version(current_version)? {
511 metadata.mark_compromised(reason);
512 self.storage.add_version(metadata)?;
513 }
514
515 self.rotate_key()
517 }
518
519 pub fn last_rotation_time(&self) -> Option<DateTime<Utc>> {
521 self.metrics.last_rotation()
522 }
523
524 pub fn time_since_last_rotation(&self) -> Option<Duration> {
526 self.metrics.last_rotation().map(|last| Utc::now() - last)
527 }
528
529 pub fn mark_version_compromised(
531 &self,
532 version: KeyVersion,
533 reason: impl Into<String>,
534 ) -> Result<(), String> {
535 if let Some(mut metadata) = self.storage.get_version(version)? {
536 metadata.mark_compromised(reason);
537 self.storage.add_version(metadata)?;
539 Ok(())
540 } else {
541 Err(format!("Version {} not found", version))
542 }
543 }
544
545 pub fn check_hipaa_compliance(&self) -> Result<bool, String> {
547 let metadata = self.get_current_metadata()?;
548 if let Some(m) = metadata {
549 Ok(m.ttl_consumed_percent() < 100)
551 } else {
552 Ok(false)
553 }
554 }
555
556 pub fn check_pci_compliance(&self) -> Result<bool, String> {
558 let metadata = self.get_current_metadata()?;
559 if let Some(m) = metadata {
560 Ok(m.ttl_consumed_percent() < 100)
562 } else {
563 Ok(false)
564 }
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571
572 #[test]
573 fn test_key_version_metadata_creation() {
574 let metadata = KeyVersionMetadata::new(1, 365);
575 assert_eq!(metadata.version, 1);
576 assert_eq!(metadata.status, KeyVersionStatus::Active);
577 assert!(!metadata.is_current);
578 assert!(!metadata.is_expired());
579 }
580
581 #[test]
582 fn test_key_version_is_expiring_soon() {
583 let mut metadata = KeyVersionMetadata::new(1, 365);
584 metadata.expires_at = Utc::now() + Duration::days(7);
586 assert!(metadata.is_expiring_soon());
587 }
588
589 #[test]
590 fn test_key_version_ttl_consumed() {
591 let now = Utc::now();
592 let mut metadata = KeyVersionMetadata::new(1, 10);
593 metadata.issued_at = now - Duration::days(8);
595 metadata.expires_at = now + Duration::days(2);
596 let percent = metadata.ttl_consumed_percent();
597 assert!(percent >= 75);
598 }
599
600 #[test]
601 fn test_key_version_should_refresh() {
602 let now = Utc::now();
603 let mut metadata = KeyVersionMetadata::new(1, 100);
604 metadata.issued_at = now - Duration::days(81);
606 metadata.expires_at = now + Duration::days(19);
607 assert!(metadata.should_refresh());
608 }
609
610 #[test]
611 fn test_key_version_mark_compromised() {
612 let mut metadata = KeyVersionMetadata::new(1, 365);
613 metadata.mark_compromised("Leaked in incident");
614 assert_eq!(metadata.status, KeyVersionStatus::Compromised);
615 assert!(metadata.compromise_reason.is_some());
616 }
617
618 #[test]
619 fn test_rotation_config_default() {
620 let config = RotationConfig::new();
621 assert_eq!(config.ttl_days, 365);
622 assert_eq!(config.refresh_threshold_percent, 80);
623 assert_eq!(config.schedule, RotationSchedule::Manual);
624 }
625
626 #[test]
627 fn test_rotation_config_builder() {
628 let config = RotationConfig::new().with_ttl_days(90).with_refresh_threshold(75);
629 assert_eq!(config.ttl_days, 90);
630 assert_eq!(config.refresh_threshold_percent, 75);
631 }
632
633 #[test]
634 fn test_rotation_metrics_record() {
635 let metrics = RotationMetrics::new();
636 metrics.record_rotation(100);
637 assert_eq!(metrics.total_rotations(), 1);
638 assert_eq!(metrics.failed_rotations(), 0);
639 assert_eq!(metrics.success_rate_percent(), 100);
640 }
641
642 #[test]
643 fn test_rotation_metrics_failure() {
644 let metrics = RotationMetrics::new();
645 metrics.record_rotation(100);
646 metrics.record_rotation(100);
647 metrics.record_failure();
648 assert_eq!(metrics.total_rotations(), 2);
649 assert_eq!(metrics.failed_rotations(), 1);
650 assert_eq!(metrics.success_rate_percent(), 50);
651 }
652
653 #[test]
654 fn test_versioned_key_storage_add_version() {
655 let storage = VersionedKeyStorage::new();
656 let metadata = KeyVersionMetadata::new(1, 365);
657 let result = storage.add_version(metadata);
658 assert!(result.is_ok());
659 assert_eq!(result.unwrap(), 1);
660 }
661
662 #[test]
663 fn test_versioned_key_storage_current_version() {
664 let storage = VersionedKeyStorage::new();
665 let metadata = KeyVersionMetadata::new(1, 365);
666 storage.add_version(metadata).unwrap();
667 storage.set_current_version(1).unwrap();
668 assert_eq!(storage.get_current_version().unwrap(), 1);
669 }
670
671 #[test]
672 fn test_credential_rotation_manager_initialize() {
673 let config = RotationConfig::new();
674 let manager = CredentialRotationManager::new(config);
675 let version = manager.initialize_key().unwrap();
676 assert_eq!(version, 1);
677 }
678
679 #[test]
680 fn test_credential_rotation_manager_rotate() {
681 let config = RotationConfig::new();
682 let manager = CredentialRotationManager::new(config);
683 manager.initialize_key().unwrap();
684 let new_version = manager.rotate_key().unwrap();
685 assert_eq!(new_version, 2);
686 assert_eq!(manager.get_current_version().unwrap(), 2);
687 }
688
689 #[test]
690 fn test_credential_rotation_manager_extract_version() {
691 let version_bytes = [0u8, 5u8]; let version =
693 CredentialRotationManager::extract_version_from_ciphertext(&version_bytes).unwrap();
694 assert_eq!(version, 5);
695 }
696
697 #[test]
698 fn test_credential_rotation_manager_extract_version_short() {
699 let version_bytes = [0u8]; let result = CredentialRotationManager::extract_version_from_ciphertext(&version_bytes);
701 assert!(result.is_err());
702 }
703
704 #[test]
705 fn test_credential_rotation_manager_active_versions_count() {
706 let config = RotationConfig::new();
707 let manager = CredentialRotationManager::new(config);
708 manager.initialize_key().unwrap();
709 manager.rotate_key().unwrap();
710 let count = manager.active_versions_count().unwrap();
711 assert!(count > 0);
712 }
713
714 #[test]
715 fn test_credential_rotation_manager_current_needs_refresh() {
716 let config = RotationConfig::new();
717 let manager = CredentialRotationManager::new(config);
718 manager.initialize_key().unwrap();
719 let needs_refresh = manager.current_version_needs_refresh().unwrap();
720 assert!(!needs_refresh); }
722
723 #[test]
724 fn test_credential_rotation_manager_emergency_rotate() {
725 let config = RotationConfig::new();
726 let manager = CredentialRotationManager::new(config);
727 manager.initialize_key().unwrap();
728 let old_version = manager.get_current_version().unwrap();
729 let new_version = manager.emergency_rotate("Suspected compromise").unwrap();
730 assert!(new_version > old_version);
731 }
732
733 #[test]
734 fn test_credential_rotation_manager_mark_compromised() {
735 let config = RotationConfig::new();
736 let manager = CredentialRotationManager::new(config);
737 manager.initialize_key().unwrap();
738 let version = manager.get_current_version().unwrap();
739 let result = manager.mark_version_compromised(version, "Test compromise");
740 assert!(result.is_ok());
741 }
742
743 #[test]
744 fn test_credential_rotation_manager_hipaa_compliance() {
745 let config = RotationConfig::new();
746 let manager = CredentialRotationManager::new(config);
747 manager.initialize_key().unwrap();
748 let compliant = manager.check_hipaa_compliance().unwrap();
749 assert!(compliant); }
751
752 #[test]
753 fn test_credential_rotation_manager_pci_compliance() {
754 let config = RotationConfig::new();
755 let manager = CredentialRotationManager::new(config);
756 manager.initialize_key().unwrap();
757 let compliant = manager.check_pci_compliance().unwrap();
758 assert!(compliant); }
760
761 #[test]
762 fn test_credential_rotation_manager_versions_needing_attention() {
763 let config = RotationConfig::new();
764 let manager = CredentialRotationManager::new(config);
765 manager.initialize_key().unwrap();
766 let needs_attention = manager.has_versions_needing_attention().unwrap();
767 assert!(!needs_attention); }
769}