1use std::collections::{BTreeMap, VecDeque};
37use std::sync::atomic::{AtomicU64, Ordering};
38use std::time::{Duration, Instant};
39
40use parking_lot::RwLock;
41use zeroize::Zeroize;
42
43use crate::auth::encryption::{KeyProvider, SecretKey};
44use crate::auth::error::AuthError;
45
46#[derive(Debug, Clone)]
48pub struct RotationPolicy {
49 pub max_age: Option<Duration>,
52
53 pub max_encryptions: Option<u64>,
56
57 pub retain_old_keys: usize,
60}
61
62impl Default for RotationPolicy {
63 fn default() -> Self {
64 Self {
65 max_age: Some(Duration::from_secs(24 * 60 * 60)), max_encryptions: Some(1 << 31), retain_old_keys: 5,
68 }
69 }
70}
71
72impl RotationPolicy {
73 #[must_use]
75 pub fn builder() -> RotationPolicyBuilder {
76 RotationPolicyBuilder::default()
77 }
78}
79
80#[derive(Debug, Default)]
82pub struct RotationPolicyBuilder {
83 max_age: Option<Duration>,
84 max_encryptions: Option<u64>,
85 retain_old_keys: Option<usize>,
86}
87
88impl RotationPolicyBuilder {
89 #[must_use]
91 pub fn max_age(mut self, age: Duration) -> Self {
92 self.max_age = Some(age);
93 self
94 }
95
96 #[must_use]
98 pub fn max_encryptions(mut self, count: u64) -> Self {
99 self.max_encryptions = Some(count);
100 self
101 }
102
103 #[must_use]
105 pub fn retain_old_keys(mut self, count: usize) -> Self {
106 self.retain_old_keys = Some(count);
107 self
108 }
109
110 #[must_use]
112 pub fn build(self) -> RotationPolicy {
113 RotationPolicy {
114 max_age: self.max_age,
115 max_encryptions: self.max_encryptions,
116 retain_old_keys: self.retain_old_keys.unwrap_or(5),
117 }
118 }
119}
120
121#[derive(Debug, Clone)]
123pub struct RotationEvent {
124 pub old_key_id: String,
126 pub new_key_id: String,
128 pub reason: RotationReason,
130 pub rotated_at: Instant,
132 pub old_key_encryptions: u64,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum RotationReason {
139 MaxAge,
141 MaxEncryptions,
143 Manual,
145}
146
147impl std::fmt::Display for RotationReason {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 match self {
150 Self::MaxAge => write!(f, "max_age"),
151 Self::MaxEncryptions => write!(f, "max_encryptions"),
152 Self::Manual => write!(f, "manual"),
153 }
154 }
155}
156
157struct RotatingState {
159 keys: BTreeMap<String, Vec<u8>>,
161 current_key_id: String,
163 created_at: BTreeMap<String, Instant>,
165 key_sequence: u64,
167}
168
169impl Drop for RotatingState {
170 fn drop(&mut self) {
171 for value in self.keys.values_mut() {
172 value.zeroize();
173 }
174 std::sync::atomic::fence(Ordering::SeqCst);
175 std::hint::black_box(&self.keys);
176 }
177}
178
179pub struct RotatingKeyProvider {
188 state: RwLock<RotatingState>,
189 policy: RotationPolicy,
190 current_usage: AtomicU64,
192 events: RwLock<VecDeque<RotationEvent>>,
194 max_events: usize,
196}
197
198impl std::fmt::Debug for RotatingKeyProvider {
199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 let state = self.state.read();
201 f.debug_struct("RotatingKeyProvider")
202 .field("current_key_id", &state.current_key_id)
203 .field("key_count", &state.keys.len())
204 .field("current_usage", &self.current_usage.load(Ordering::Relaxed))
205 .field("policy", &self.policy)
206 .field("event_count", &self.events.read().len())
207 .finish()
208 }
209}
210
211fn generate_random_key() -> Vec<u8> {
213 let mut key = vec![0u8; 32];
214 super::fill_random(&mut key);
215 key
216}
217
218fn format_key_id(sequence: u64) -> String {
220 format!("rk-{sequence}")
221}
222
223impl RotatingKeyProvider {
224 #[must_use]
228 pub fn new(policy: RotationPolicy) -> Self {
229 let key = generate_random_key();
230 let key_id = format_key_id(1);
231 let now = Instant::now();
232
233 let mut keys = BTreeMap::new();
234 keys.insert(key_id.clone(), key);
235
236 let mut created_at = BTreeMap::new();
237 created_at.insert(key_id.clone(), now);
238
239 let state = RotatingState {
240 keys,
241 current_key_id: key_id,
242 created_at,
243 key_sequence: 1,
244 };
245
246 Self {
247 state: RwLock::new(state),
248 policy,
249 current_usage: AtomicU64::new(0),
250 events: RwLock::new(VecDeque::new()),
251 max_events: 1000,
252 }
253 }
254
255 pub fn with_initial_key(
259 key_bytes: Vec<u8>,
260 key_id: &str,
261 policy: RotationPolicy,
262 ) -> Result<Self, AuthError> {
263 if key_bytes.len() != 32 {
264 return Err(AuthError::Config(format!(
265 "AES-256 key must be 32 bytes, got {}",
266 key_bytes.len()
267 )));
268 }
269
270 let now = Instant::now();
271 let mut keys = BTreeMap::new();
272 keys.insert(key_id.to_string(), key_bytes);
273
274 let mut created_at = BTreeMap::new();
275 created_at.insert(key_id.to_string(), now);
276
277 let state = RotatingState {
278 keys,
279 current_key_id: key_id.to_string(),
280 created_at,
281 key_sequence: 1,
282 };
283
284 Ok(Self {
285 state: RwLock::new(state),
286 policy,
287 current_usage: AtomicU64::new(0),
288 events: RwLock::new(VecDeque::new()),
289 max_events: 1000,
290 })
291 }
292
293 fn maybe_rotate(&self) -> Option<RotationEvent> {
298 let usage = self.current_usage.load(Ordering::Relaxed);
299 let state = self.state.read();
300
301 let reason = self.check_rotation_needed(&state, usage);
302 let reason = reason?;
303
304 let old_key_id = state.current_key_id.clone();
306 drop(state);
307
308 let mut state = self.state.write();
309 if state.current_key_id != old_key_id {
311 return None;
312 }
313
314 let old_usage = self.current_usage.swap(0, Ordering::Relaxed);
315 let new_key = generate_random_key();
316 state.key_sequence += 1;
317 let new_key_id = format_key_id(state.key_sequence);
318
319 state.keys.insert(new_key_id.clone(), new_key);
320 state.created_at.insert(new_key_id.clone(), Instant::now());
321 state.current_key_id = new_key_id.clone();
322
323 self.purge_old_keys(&mut state);
325
326 let event = RotationEvent {
327 old_key_id,
328 new_key_id,
329 reason,
330 rotated_at: Instant::now(),
331 old_key_encryptions: old_usage,
332 };
333
334 tracing::info!(
336 old_key = %event.old_key_id,
337 new_key = %event.new_key_id,
338 reason = %event.reason,
339 old_key_encryptions = event.old_key_encryptions,
340 "encryption key rotated"
341 );
342 metrics::counter!("chronix_auth_key_rotations_total", "reason" => event.reason.to_string())
343 .increment(1);
344
345 let mut events = self.events.write();
346 if events.len() >= self.max_events {
347 events.pop_front();
348 }
349 events.push_back(event.clone());
350
351 Some(event)
352 }
353
354 fn check_rotation_needed(&self, state: &RotatingState, usage: u64) -> Option<RotationReason> {
356 if let Some(max) = self.policy.max_encryptions {
358 if usage >= max {
359 return Some(RotationReason::MaxEncryptions);
360 }
361 }
362
363 if let Some(max_age) = self.policy.max_age {
365 if let Some(created) = state.created_at.get(&state.current_key_id) {
366 if created.elapsed() >= max_age {
367 return Some(RotationReason::MaxAge);
368 }
369 }
370 }
371
372 None
373 }
374
375 fn purge_old_keys(&self, state: &mut RotatingState) {
377 let retain = self.policy.retain_old_keys + 1; while state.keys.len() > retain {
379 let oldest = state
381 .created_at
382 .iter()
383 .filter(|(id, _)| **id != state.current_key_id)
384 .min_by_key(|(_, created)| *created)
385 .map(|(id, _)| id.clone());
386
387 if let Some(oldest_id) = oldest {
388 if let Some(mut key_bytes) = state.keys.remove(&oldest_id) {
389 key_bytes.zeroize();
390 }
391 state.created_at.remove(&oldest_id);
392 tracing::debug!(key_id = %oldest_id, "purged expired encryption key");
393 metrics::counter!("chronix_auth_keys_purged_total").increment(1);
394 } else {
395 break;
396 }
397 }
398 }
399
400 pub fn rotate_now(&self) -> RotationEvent {
404 let old_usage = self.current_usage.swap(0, Ordering::Relaxed);
405 let mut state = self.state.write();
406
407 let old_key_id = state.current_key_id.clone();
408 let new_key = generate_random_key();
409 state.key_sequence += 1;
410 let new_key_id = format_key_id(state.key_sequence);
411
412 state.keys.insert(new_key_id.clone(), new_key);
413 state.created_at.insert(new_key_id.clone(), Instant::now());
414 state.current_key_id = new_key_id.clone();
415
416 self.purge_old_keys(&mut state);
417
418 let event = RotationEvent {
419 old_key_id,
420 new_key_id,
421 reason: RotationReason::Manual,
422 rotated_at: Instant::now(),
423 old_key_encryptions: old_usage,
424 };
425
426 tracing::info!(
427 old_key = %event.old_key_id,
428 new_key = %event.new_key_id,
429 reason = %event.reason,
430 old_key_encryptions = event.old_key_encryptions,
431 "encryption key rotated (manual)"
432 );
433 metrics::counter!("chronix_auth_key_rotations_total", "reason" => "manual").increment(1);
434
435 let mut events = self.events.write();
436 if events.len() >= self.max_events {
437 events.pop_front();
438 }
439 events.push_back(event.clone());
440
441 event
442 }
443
444 pub fn record_encryption(&self) {
449 self.current_usage.fetch_add(1, Ordering::Relaxed);
450 }
451
452 #[must_use]
454 pub fn current_key_usage(&self) -> u64 {
455 self.current_usage.load(Ordering::Relaxed)
456 }
457
458 #[must_use]
460 pub fn rotation_history(&self) -> Vec<RotationEvent> {
461 self.events.read().iter().cloned().collect()
462 }
463
464 #[must_use]
466 pub fn key_count(&self) -> usize {
467 self.state.read().keys.len()
468 }
469
470 #[must_use]
472 pub fn current_key_age(&self) -> Duration {
473 let state = self.state.read();
474 state
475 .created_at
476 .get(&state.current_key_id)
477 .map(std::time::Instant::elapsed)
478 .unwrap_or_default()
479 }
480}
481
482impl KeyProvider for RotatingKeyProvider {
483 fn current_key(&self) -> Result<(SecretKey, String), AuthError> {
484 self.maybe_rotate();
486
487 let state = self.state.read();
488 let key_bytes = state
489 .keys
490 .get(&state.current_key_id)
491 .ok_or_else(|| {
492 AuthError::KeyNotFound(format!(
493 "current key {} missing from state",
494 state.current_key_id
495 ))
496 })?
497 .clone();
498
499 Ok((SecretKey::new(key_bytes), state.current_key_id.clone()))
500 }
501
502 fn key_by_id(&self, key_id: &str) -> Result<SecretKey, AuthError> {
503 let state = self.state.read();
504 state
505 .keys
506 .get(key_id)
507 .cloned()
508 .map(SecretKey::new)
509 .ok_or_else(|| AuthError::KeyNotFound(key_id.to_string()))
510 }
511
512 fn key_ids(&self) -> Vec<String> {
513 self.state.read().keys.keys().cloned().collect()
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520 use crate::auth::encryption::EncryptionService;
521
522 #[test]
523 fn default_policy_values() {
524 let policy = RotationPolicy::default();
525 assert_eq!(policy.max_age, Some(Duration::from_secs(86400)));
526 assert_eq!(policy.max_encryptions, Some(1 << 31));
527 assert_eq!(policy.retain_old_keys, 5);
528 }
529
530 #[test]
531 fn builder_creates_custom_policy() {
532 let policy = RotationPolicy::builder()
533 .max_age(Duration::from_secs(3600))
534 .max_encryptions(500_000)
535 .retain_old_keys(3)
536 .build();
537
538 assert_eq!(policy.max_age, Some(Duration::from_secs(3600)));
539 assert_eq!(policy.max_encryptions, Some(500_000));
540 assert_eq!(policy.retain_old_keys, 3);
541 }
542
543 #[test]
544 fn new_provider_generates_initial_key() {
545 let provider = RotatingKeyProvider::new(RotationPolicy::default());
546 assert_eq!(provider.key_count(), 1);
547
548 let (key, id) = provider.current_key().unwrap();
549 assert_eq!(key.as_bytes().len(), 32);
550 assert_eq!(id, "rk-1");
551 }
552
553 #[test]
554 fn with_initial_key_accepts_valid_key() {
555 let key = vec![0xAB; 32];
556 let provider = RotatingKeyProvider::with_initial_key(
557 key.clone(),
558 "my-key-1",
559 RotationPolicy::default(),
560 )
561 .unwrap();
562
563 let (retrieved, id) = provider.current_key().unwrap();
564 assert_eq!(retrieved.as_bytes(), &key);
565 assert_eq!(id, "my-key-1");
566 }
567
568 #[test]
569 fn with_initial_key_rejects_wrong_size() {
570 let result = RotatingKeyProvider::with_initial_key(
571 vec![0u8; 16], "bad-key",
573 RotationPolicy::default(),
574 );
575 assert!(result.is_err());
576 }
577
578 #[test]
579 fn manual_rotation_creates_new_key() {
580 let provider = RotatingKeyProvider::new(RotationPolicy::default());
581
582 let (_, id1) = provider.current_key().unwrap();
583 assert_eq!(id1, "rk-1");
584
585 let event = provider.rotate_now();
586 assert_eq!(event.old_key_id, "rk-1");
587 assert_eq!(event.new_key_id, "rk-2");
588 assert_eq!(event.reason, RotationReason::Manual);
589
590 let (_, id2) = provider.current_key().unwrap();
591 assert_eq!(id2, "rk-2");
592 assert_eq!(provider.key_count(), 2);
593 }
594
595 #[test]
596 fn old_keys_still_accessible_after_rotation() {
597 let provider = RotatingKeyProvider::new(RotationPolicy::default());
598
599 let (key1, _) = provider.current_key().unwrap();
601 let key1_bytes = key1.as_bytes().to_vec();
602
603 provider.rotate_now();
605
606 let old_key = provider.key_by_id("rk-1").unwrap();
608 assert_eq!(old_key.as_bytes(), &key1_bytes);
609 }
610
611 #[test]
612 fn encryption_roundtrip_after_rotation() {
613 let provider = RotatingKeyProvider::new(RotationPolicy::default());
614 let svc = EncryptionService::new(Box::new(provider));
615
616 let plaintext = b"secret data before rotation";
618 let (ct1, kid1) = svc.encrypt(plaintext).unwrap();
619 assert_eq!(kid1, "rk-1");
620
621 let dec = svc.decrypt(&ct1, &kid1).unwrap();
623 assert_eq!(dec, plaintext);
624 }
625
626 #[test]
627 fn usage_based_rotation_triggers() {
628 let policy = RotationPolicy::builder()
629 .max_encryptions(3) .retain_old_keys(2)
631 .build();
632
633 let provider = RotatingKeyProvider::new(policy);
634
635 provider.record_encryption();
637 provider.record_encryption();
638 provider.record_encryption();
639
640 let (_, id) = provider.current_key().unwrap();
642 assert_eq!(id, "rk-2", "should have auto-rotated to rk-2");
643 assert_eq!(provider.key_count(), 2);
644
645 let history = provider.rotation_history();
647 assert_eq!(history.len(), 1);
648 assert_eq!(history[0].reason, RotationReason::MaxEncryptions);
649 assert_eq!(history[0].old_key_encryptions, 3);
650 }
651
652 #[test]
653 fn key_purge_respects_retention_limit() {
654 let policy = RotationPolicy::builder()
655 .max_encryptions(1_000_000) .retain_old_keys(2) .build();
658
659 let provider = RotatingKeyProvider::new(policy);
660 provider.rotate_now(); assert_eq!(provider.key_count(), 2);
664
665 provider.rotate_now(); assert_eq!(provider.key_count(), 3);
667
668 provider.rotate_now(); assert_eq!(provider.key_count(), 3);
670
671 assert!(provider.key_by_id("rk-1").is_err());
673 assert!(provider.key_by_id("rk-2").is_ok());
675 assert!(provider.key_by_id("rk-3").is_ok());
676 assert!(provider.key_by_id("rk-4").is_ok());
677 }
678
679 #[test]
680 fn rotation_history_bounded() {
681 let policy = RotationPolicy::default();
682
683 let key = generate_random_key();
685 let key_id = format_key_id(1);
686 let now = Instant::now();
687
688 let mut keys = BTreeMap::new();
689 keys.insert(key_id.clone(), key);
690 let mut created_at = BTreeMap::new();
691 created_at.insert(key_id.clone(), now);
692
693 let provider = RotatingKeyProvider {
694 state: RwLock::new(RotatingState {
695 keys,
696 current_key_id: key_id,
697 created_at,
698 key_sequence: 1,
699 }),
700 policy,
701 current_usage: AtomicU64::new(0),
702 events: RwLock::new(VecDeque::new()),
703 max_events: 3, };
705
706 for _ in 0..5 {
707 provider.rotate_now();
708 }
709
710 let history = provider.rotation_history();
711 assert_eq!(history.len(), 3, "should cap at max_events");
712 assert_eq!(history[2].new_key_id, "rk-6");
714 }
715
716 #[test]
717 fn current_key_age_increases() {
718 let provider = RotatingKeyProvider::new(RotationPolicy::default());
719 let age1 = provider.current_key_age();
720 std::thread::sleep(Duration::from_millis(10));
721 let age2 = provider.current_key_age();
722 assert!(age2 > age1);
723 }
724
725 #[test]
726 fn record_encryption_increments_counter() {
727 let provider = RotatingKeyProvider::new(RotationPolicy::default());
728 assert_eq!(provider.current_key_usage(), 0);
729
730 provider.record_encryption();
731 provider.record_encryption();
732 assert_eq!(provider.current_key_usage(), 2);
733 }
734
735 #[test]
736 fn key_ids_lists_all() {
737 let provider = RotatingKeyProvider::new(RotationPolicy::default());
738 provider.rotate_now();
739
740 let ids = provider.key_ids();
741 assert_eq!(ids.len(), 2);
742 assert!(ids.contains(&"rk-1".to_string()));
743 assert!(ids.contains(&"rk-2".to_string()));
744 }
745
746 #[test]
747 fn debug_does_not_leak_keys() {
748 let provider = RotatingKeyProvider::new(RotationPolicy::default());
749 let debug = format!("{provider:?}");
750
751 assert!(debug.contains("RotatingKeyProvider"));
752 assert!(debug.contains("current_key_id"));
753 assert!(!debug.contains("0x"));
755 }
756
757 #[test]
758 fn concurrent_rotation_safety() {
759 use std::sync::Arc;
760
761 let policy = RotationPolicy::builder()
762 .max_encryptions(10)
763 .retain_old_keys(20)
764 .build();
765
766 let provider = Arc::new(RotatingKeyProvider::new(policy));
767 let mut handles = Vec::new();
768
769 for _ in 0..4 {
771 let p = Arc::clone(&provider);
772 handles.push(std::thread::spawn(move || {
773 for _ in 0..50 {
774 p.record_encryption();
775 let _ = p.current_key().unwrap();
776 }
777 }));
778 }
779
780 for h in handles {
781 h.join().unwrap();
782 }
783
784 let history = provider.rotation_history();
786 assert!(
787 !history.is_empty(),
788 "should have auto-rotated at least once"
789 );
790 }
791
792 #[test]
793 fn encrypt_decrypt_across_rotation() {
794 let policy = RotationPolicy::builder()
795 .max_encryptions(1_000_000)
796 .retain_old_keys(5)
797 .build();
798
799 let provider = RotatingKeyProvider::new(policy);
800
801 let svc = EncryptionService::new(Box::new(provider));
803 let (ct1, kid1) = svc.encrypt(b"data-1").unwrap();
804 assert_eq!(kid1, "rk-1");
805 let dec = svc.decrypt(&ct1, &kid1).unwrap();
806 assert_eq!(dec, b"data-1");
807 }
808
809 #[test]
810 fn rotation_reason_display() {
811 assert_eq!(RotationReason::MaxAge.to_string(), "max_age");
812 assert_eq!(
813 RotationReason::MaxEncryptions.to_string(),
814 "max_encryptions"
815 );
816 assert_eq!(RotationReason::Manual.to_string(), "manual");
817 }
818}