Skip to main content

chronix_security/auth/
rotation.rs

1//! Automated secret rotation for encryption keys.
2//!
3//! Provides a [`RotatingKeyProvider`] that wraps any [`KeyProvider`] and
4//! supports automatic key generation and rotation based on configurable
5//! policies (time-based, usage-based, or both).
6//!
7//! # Architecture
8//!
9//! ```text
10//! RotatingKeyProvider
11//! ├── inner: Arc<RwLock<RotatingState>>
12//! │   ├── keys: BTreeMap<String, SecretKey>
13//! │   ├── current_key_id: String
14//! │   ├── created_at: HashMap<String, Instant>
15//! │   └── usage_counts: HashMap<String, u64>
16//! ├── policy: RotationPolicy
17//! └── audit: RotationAuditLog
18//! ```
19//!
20//! # Example
21//!
22//! ```no_run
23//! use chronix_security::auth::rotation::{RotatingKeyProvider, RotationPolicy};
24//! use std::time::Duration;
25//!
26//! let policy = RotationPolicy::builder()
27//!     .max_age(Duration::from_secs(86400))     // rotate every 24h
28//!     .max_encryptions(1_000_000)               // or after 1M encryptions
29//!     .retain_old_keys(5)                        // keep 5 old keys for decryption
30//!     .build();
31//!
32//! let provider = RotatingKeyProvider::new(policy);
33//! // provider implements KeyProvider — pass to EncryptionService
34//! ```
35
36use 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/// Policy governing when automatic key rotation occurs.
47#[derive(Debug, Clone)]
48pub struct RotationPolicy {
49    /// Maximum age of a key before rotation is triggered.
50    /// `None` disables time-based rotation.
51    pub max_age: Option<Duration>,
52
53    /// Maximum number of encryptions before rotation is triggered.
54    /// `None` disables usage-based rotation.
55    pub max_encryptions: Option<u64>,
56
57    /// Number of old keys to retain for decrypting historical data.
58    /// Oldest keys beyond this limit are purged.
59    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)), // 24 hours
66            max_encryptions: Some(1 << 31),                   // 2^31 (~2B)
67            retain_old_keys: 5,
68        }
69    }
70}
71
72impl RotationPolicy {
73    /// Create a builder for `RotationPolicy`.
74    #[must_use]
75    pub fn builder() -> RotationPolicyBuilder {
76        RotationPolicyBuilder::default()
77    }
78}
79
80/// Builder for [`RotationPolicy`].
81#[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    /// Set the maximum age before rotation.
90    #[must_use]
91    pub fn max_age(mut self, age: Duration) -> Self {
92        self.max_age = Some(age);
93        self
94    }
95
96    /// Set the maximum encryption count before rotation.
97    #[must_use]
98    pub fn max_encryptions(mut self, count: u64) -> Self {
99        self.max_encryptions = Some(count);
100        self
101    }
102
103    /// Set how many old keys to retain for decryption.
104    #[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    /// Build the policy.
111    #[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/// Record of a single key rotation event.
122#[derive(Debug, Clone)]
123pub struct RotationEvent {
124    /// ID of the old (retired) key.
125    pub old_key_id: String,
126    /// ID of the new (active) key.
127    pub new_key_id: String,
128    /// Reason for rotation.
129    pub reason: RotationReason,
130    /// Timestamp of the rotation (monotonic).
131    pub rotated_at: Instant,
132    /// Number of encryptions performed with the old key.
133    pub old_key_encryptions: u64,
134}
135
136/// Why a key rotation was triggered.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum RotationReason {
139    /// Key exceeded maximum age.
140    MaxAge,
141    /// Key exceeded maximum encryption count.
142    MaxEncryptions,
143    /// Manual rotation requested.
144    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
157/// Internal state for key rotation.
158struct RotatingState {
159    /// All keys indexed by ID (current + old).
160    keys: BTreeMap<String, Vec<u8>>,
161    /// The current (active) key ID.
162    current_key_id: String,
163    /// When each key was created.
164    created_at: BTreeMap<String, Instant>,
165    /// Monotonically increasing key sequence number.
166    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
179/// A key provider with automatic rotation support.
180///
181/// Generates cryptographically random AES-256 keys and rotates them
182/// based on the configured [`RotationPolicy`]. Old keys are retained
183/// (up to `retain_old_keys`) so historical data can still be decrypted.
184///
185/// Thread-safe: uses `RwLock` for state and `AtomicU64` for the
186/// encryption counter (zero contention on the hot read path).
187pub struct RotatingKeyProvider {
188    state: RwLock<RotatingState>,
189    policy: RotationPolicy,
190    /// Encryption count for the current key (atomic for hot-path performance).
191    current_usage: AtomicU64,
192    /// Audit log of rotation events (bounded ring buffer).
193    events: RwLock<VecDeque<RotationEvent>>,
194    /// Maximum number of audit events to retain.
195    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
211/// Generate a cryptographically random 256-bit key.
212fn generate_random_key() -> Vec<u8> {
213    let mut key = vec![0u8; 32];
214    super::fill_random(&mut key);
215    key
216}
217
218/// Format a key ID from a sequence number.
219fn format_key_id(sequence: u64) -> String {
220    format!("rk-{sequence}")
221}
222
223impl RotatingKeyProvider {
224    /// Create a new rotating key provider with the given policy.
225    ///
226    /// An initial random key is generated immediately.
227    #[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    /// Create a rotating provider seeded with an existing key.
256    ///
257    /// Useful for migrating from a static key to rotating keys.
258    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    /// Check if the current key needs rotation and rotate if necessary.
294    ///
295    /// Returns `Some(event)` if rotation occurred, `None` otherwise.
296    /// This is called automatically on each `current_key()` call.
297    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        // Drop read lock before upgrading to write
305        let old_key_id = state.current_key_id.clone();
306        drop(state);
307
308        let mut state = self.state.write();
309        // Double-check: another thread may have rotated between our read and write
310        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        // Purge oldest keys beyond retention limit
324        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        // Log the rotation for audit
335        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    /// Check if rotation is needed based on the policy.
355    fn check_rotation_needed(&self, state: &RotatingState, usage: u64) -> Option<RotationReason> {
356        // Check usage-based rotation first (more urgent)
357        if let Some(max) = self.policy.max_encryptions {
358            if usage >= max {
359                return Some(RotationReason::MaxEncryptions);
360            }
361        }
362
363        // Check time-based rotation
364        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    /// Purge old keys beyond the retention limit.
376    fn purge_old_keys(&self, state: &mut RotatingState) {
377        let retain = self.policy.retain_old_keys + 1; // +1 for current key
378        while state.keys.len() > retain {
379            // Find the oldest non-current key
380            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    /// Manually trigger key rotation regardless of policy thresholds.
401    ///
402    /// Returns the rotation event.
403    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    /// Record that the current key was used for an encryption operation.
445    ///
446    /// Call this from the `EncryptionService` after each `encrypt()` call
447    /// so that usage-based rotation can be tracked.
448    pub fn record_encryption(&self) {
449        self.current_usage.fetch_add(1, Ordering::Relaxed);
450    }
451
452    /// Get the current encryption count for the active key.
453    #[must_use]
454    pub fn current_key_usage(&self) -> u64 {
455        self.current_usage.load(Ordering::Relaxed)
456    }
457
458    /// Return all rotation events (most recent last).
459    #[must_use]
460    pub fn rotation_history(&self) -> Vec<RotationEvent> {
461        self.events.read().iter().cloned().collect()
462    }
463
464    /// Number of keys currently held (current + retained old).
465    #[must_use]
466    pub fn key_count(&self) -> usize {
467        self.state.read().keys.len()
468    }
469
470    /// Returns the current key's age (time since creation).
471    #[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        // Check if rotation is needed (cheap atomic read in hot path)
485        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], // too short
572            "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        // Get key-1
600        let (key1, _) = provider.current_key().unwrap();
601        let key1_bytes = key1.as_bytes().to_vec();
602
603        // Rotate to key-2
604        provider.rotate_now();
605
606        // key-1 is still accessible
607        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        // Encrypt with initial key
617        let plaintext = b"secret data before rotation";
618        let (ct1, kid1) = svc.encrypt(plaintext).unwrap();
619        assert_eq!(kid1, "rk-1");
620
621        // We can't rotate via svc — this tests the normal path
622        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) // rotate after 3 encryptions
630            .retain_old_keys(2)
631            .build();
632
633        let provider = RotatingKeyProvider::new(policy);
634
635        // Simulate 3 encryptions
636        provider.record_encryption();
637        provider.record_encryption();
638        provider.record_encryption();
639
640        // Next current_key() should trigger rotation
641        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        // Rotation event should be recorded
646        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) // won't auto-trigger
656            .retain_old_keys(2)         // keep 2 old + 1 current = 3 max
657            .build();
658
659        let provider = RotatingKeyProvider::new(policy);
660        // rk-1 is current
661
662        provider.rotate_now(); // rk-2 current, rk-1 old
663        assert_eq!(provider.key_count(), 2);
664
665        provider.rotate_now(); // rk-3 current, rk-1 + rk-2 old
666        assert_eq!(provider.key_count(), 3);
667
668        provider.rotate_now(); // rk-4 current, rk-2 + rk-3 old, rk-1 purged
669        assert_eq!(provider.key_count(), 3);
670
671        // rk-1 should be purged
672        assert!(provider.key_by_id("rk-1").is_err());
673        // rk-2 and rk-3 still accessible
674        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        // Create provider with small event buffer for testing
684        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, // small for testing
704        };
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        // Most recent events should be retained
713        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        // Should not contain raw key bytes
754        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        // 4 threads each doing 50 encryptions
770        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        // Should have rotated multiple times
785        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        // Encrypt with key rk-1
802        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}