Skip to main content

ringkernel_core/
secrets.rs

1//! Secrets management for secure key storage and retrieval.
2//!
3//! This module provides a pluggable secrets management system with support for
4//! multiple backends including environment variables, HashiCorp Vault, and AWS Secrets Manager.
5//!
6//! # Feature Flags
7//!
8//! - `crypto` - Enables secure key derivation and encryption of cached secrets
9//!
10//! # Example
11//!
12//! ```rust,ignore
13//! use ringkernel_core::secrets::{SecretStore, EnvVarSecretStore, SecretKey};
14//!
15//! // Using environment variables (for development)
16//! let store = EnvVarSecretStore::new("MYAPP_");
17//! let api_key = store.get_secret(&SecretKey::new("api_key")).await?;
18//!
19//! // Using HashiCorp Vault (for production)
20//! let vault = VaultSecretStore::new("https://vault.example.com:8200")
21//!     .with_token_auth(env::var("VAULT_TOKEN")?)
22//!     .with_mount_path("secret/data/myapp");
23//! let db_password = vault.get_secret(&SecretKey::new("database/password")).await?;
24//! ```
25
26use async_trait::async_trait;
27use parking_lot::RwLock;
28use std::collections::HashMap;
29use std::fmt;
30use std::sync::Arc;
31use std::time::{Duration, Instant};
32
33#[cfg(feature = "crypto")]
34use zeroize::Zeroize;
35
36// ============================================================================
37// SECRET KEY
38// ============================================================================
39
40/// A key identifying a secret in the store.
41#[derive(Debug, Clone, PartialEq, Eq, Hash)]
42pub struct SecretKey {
43    /// The secret path/name.
44    path: String,
45    /// Optional version specifier.
46    version: Option<String>,
47}
48
49impl SecretKey {
50    /// Create a new secret key.
51    pub fn new(path: impl Into<String>) -> Self {
52        Self {
53            path: path.into(),
54            version: None,
55        }
56    }
57
58    /// Create a secret key with a specific version.
59    pub fn with_version(path: impl Into<String>, version: impl Into<String>) -> Self {
60        Self {
61            path: path.into(),
62            version: Some(version.into()),
63        }
64    }
65
66    /// Get the path.
67    pub fn path(&self) -> &str {
68        &self.path
69    }
70
71    /// Get the version.
72    pub fn version(&self) -> Option<&str> {
73        self.version.as_deref()
74    }
75}
76
77impl fmt::Display for SecretKey {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        if let Some(version) = &self.version {
80            write!(f, "{}@{}", self.path, version)
81        } else {
82            write!(f, "{}", self.path)
83        }
84    }
85}
86
87// ============================================================================
88// SECRET VALUE
89// ============================================================================
90
91/// A secret value retrieved from the store.
92///
93/// When the `crypto` feature is enabled, the secret data is automatically
94/// zeroed when dropped using the `zeroize` crate.
95pub struct SecretValue {
96    /// The secret data.
97    #[cfg(feature = "crypto")]
98    data: zeroize::Zeroizing<Vec<u8>>,
99    #[cfg(not(feature = "crypto"))]
100    data: Vec<u8>,
101    /// When the secret was retrieved.
102    retrieved_at: Instant,
103    /// Optional expiration time.
104    expires_at: Option<Instant>,
105    /// Metadata about the secret.
106    metadata: HashMap<String, String>,
107}
108
109impl SecretValue {
110    /// Create a new secret value.
111    #[cfg(feature = "crypto")]
112    pub fn new(data: Vec<u8>) -> Self {
113        Self {
114            data: zeroize::Zeroizing::new(data),
115            retrieved_at: Instant::now(),
116            expires_at: None,
117            metadata: HashMap::new(),
118        }
119    }
120
121    /// Create a new secret value (non-crypto version).
122    #[cfg(not(feature = "crypto"))]
123    pub fn new(data: Vec<u8>) -> Self {
124        Self {
125            data,
126            retrieved_at: Instant::now(),
127            expires_at: None,
128            metadata: HashMap::new(),
129        }
130    }
131
132    /// Create a secret value from a string.
133    pub fn from_string(s: impl Into<String>) -> Self {
134        Self::new(s.into().into_bytes())
135    }
136
137    /// Set the expiration time.
138    pub fn with_expiry(mut self, duration: Duration) -> Self {
139        self.expires_at = Some(Instant::now() + duration);
140        self
141    }
142
143    /// Add metadata.
144    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
145        self.metadata.insert(key.into(), value.into());
146        self
147    }
148
149    /// Get the secret data as bytes.
150    pub fn as_bytes(&self) -> &[u8] {
151        &self.data
152    }
153
154    /// Get the secret data as a string (if valid UTF-8).
155    pub fn as_str(&self) -> Option<&str> {
156        std::str::from_utf8(&self.data).ok()
157    }
158
159    /// Check if the secret has expired.
160    pub fn is_expired(&self) -> bool {
161        self.expires_at
162            .map(|exp| Instant::now() > exp)
163            .unwrap_or(false)
164    }
165
166    /// Get the age of this secret value.
167    pub fn age(&self) -> Duration {
168        self.retrieved_at.elapsed()
169    }
170
171    /// Get metadata.
172    pub fn metadata(&self) -> &HashMap<String, String> {
173        &self.metadata
174    }
175}
176
177impl fmt::Debug for SecretValue {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        f.debug_struct("SecretValue")
180            .field("length", &self.data.len())
181            .field("retrieved_at", &"<instant>")
182            .field("is_expired", &self.is_expired())
183            .field("metadata", &self.metadata)
184            .finish()
185    }
186}
187
188// ============================================================================
189// SECRET STORE TRAIT
190// ============================================================================
191
192/// Error type for secret store operations.
193#[derive(Debug, Clone)]
194pub enum SecretError {
195    /// Secret not found.
196    NotFound(String),
197    /// Access denied.
198    AccessDenied(String),
199    /// Connection error.
200    ConnectionError(String),
201    /// Invalid secret format.
202    InvalidFormat(String),
203    /// Secret has expired.
204    Expired(String),
205    /// Rate limited.
206    RateLimited(String),
207    /// Other error.
208    Other(String),
209}
210
211impl fmt::Display for SecretError {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        match self {
214            Self::NotFound(msg) => write!(f, "Secret not found: {}", msg),
215            Self::AccessDenied(msg) => write!(f, "Access denied: {}", msg),
216            Self::ConnectionError(msg) => write!(f, "Connection error: {}", msg),
217            Self::InvalidFormat(msg) => write!(f, "Invalid format: {}", msg),
218            Self::Expired(msg) => write!(f, "Secret expired: {}", msg),
219            Self::RateLimited(msg) => write!(f, "Rate limited: {}", msg),
220            Self::Other(msg) => write!(f, "Secret error: {}", msg),
221        }
222    }
223}
224
225impl std::error::Error for SecretError {}
226
227/// Result type for secret store operations.
228pub type SecretResult<T> = Result<T, SecretError>;
229
230/// Trait for pluggable secret storage backends.
231#[async_trait]
232pub trait SecretStore: Send + Sync {
233    /// Get a secret by key.
234    async fn get_secret(&self, key: &SecretKey) -> SecretResult<SecretValue>;
235
236    /// Check if a secret exists.
237    async fn secret_exists(&self, key: &SecretKey) -> SecretResult<bool>;
238
239    /// List available secrets (if supported).
240    async fn list_secrets(&self, prefix: Option<&str>) -> SecretResult<Vec<SecretKey>>;
241
242    /// Get the store name for logging/debugging.
243    fn store_name(&self) -> &str;
244
245    /// Check if the store is healthy/connected.
246    async fn health_check(&self) -> SecretResult<()>;
247}
248
249// ============================================================================
250// ENVIRONMENT VARIABLE SECRET STORE
251// ============================================================================
252
253/// Secret store backed by environment variables.
254///
255/// This is suitable for development and simple deployments.
256/// Secret keys are transformed to environment variable names by:
257/// 1. Applying the prefix
258/// 2. Converting to uppercase
259/// 3. Replacing `/` and `.` with `_`
260///
261/// Example: Key "database/password" with prefix "MYAPP_" becomes "MYAPP_DATABASE_PASSWORD"
262pub struct EnvVarSecretStore {
263    /// Prefix for environment variable names.
264    prefix: String,
265    /// Cache for retrieved secrets.
266    cache: RwLock<HashMap<SecretKey, SecretValue>>,
267    /// Cache duration.
268    cache_duration: Duration,
269}
270
271impl EnvVarSecretStore {
272    /// Create a new environment variable secret store.
273    pub fn new(prefix: impl Into<String>) -> Self {
274        Self {
275            prefix: prefix.into(),
276            cache: RwLock::new(HashMap::new()),
277            cache_duration: Duration::from_secs(300), // 5 minutes default
278        }
279    }
280
281    /// Set the cache duration.
282    pub fn with_cache_duration(mut self, duration: Duration) -> Self {
283        self.cache_duration = duration;
284        self
285    }
286
287    /// Convert a secret key to an environment variable name.
288    fn key_to_env_var(&self, key: &SecretKey) -> String {
289        let path = key.path().to_uppercase().replace(['/', '.'], "_");
290        format!("{}{}", self.prefix, path)
291    }
292}
293
294#[async_trait]
295impl SecretStore for EnvVarSecretStore {
296    async fn get_secret(&self, key: &SecretKey) -> SecretResult<SecretValue> {
297        // Check cache first
298        {
299            let cache = self.cache.read();
300            if let Some(secret) = cache.get(key) {
301                if !secret.is_expired() && secret.age() < self.cache_duration {
302                    return Ok(SecretValue::new(secret.as_bytes().to_vec()));
303                }
304            }
305        }
306
307        // Get from environment
308        let env_var = self.key_to_env_var(key);
309        let value = std::env::var(&env_var).map_err(|_| {
310            SecretError::NotFound(format!("Environment variable {} not set", env_var))
311        })?;
312
313        let secret = SecretValue::from_string(value)
314            .with_metadata("source", "environment")
315            .with_metadata("env_var", &env_var);
316
317        // Cache it
318        {
319            let mut cache = self.cache.write();
320            cache.insert(key.clone(), SecretValue::new(secret.as_bytes().to_vec()));
321        }
322
323        Ok(secret)
324    }
325
326    async fn secret_exists(&self, key: &SecretKey) -> SecretResult<bool> {
327        let env_var = self.key_to_env_var(key);
328        Ok(std::env::var(&env_var).is_ok())
329    }
330
331    async fn list_secrets(&self, prefix: Option<&str>) -> SecretResult<Vec<SecretKey>> {
332        let full_prefix = match prefix {
333            Some(p) => format!(
334                "{}{}",
335                self.prefix,
336                p.to_uppercase().replace(['/', '.'], "_")
337            ),
338            None => self.prefix.clone(),
339        };
340
341        let secrets: Vec<SecretKey> = std::env::vars()
342            .filter_map(|(name, _)| {
343                if name.starts_with(&full_prefix) {
344                    let path = name
345                        .strip_prefix(&self.prefix)?
346                        .to_lowercase()
347                        .replace('_', "/");
348                    Some(SecretKey::new(path))
349                } else {
350                    None
351                }
352            })
353            .collect();
354
355        Ok(secrets)
356    }
357
358    fn store_name(&self) -> &str {
359        "EnvVarSecretStore"
360    }
361
362    async fn health_check(&self) -> SecretResult<()> {
363        // Environment variables are always available
364        Ok(())
365    }
366}
367
368// ============================================================================
369// IN-MEMORY SECRET STORE (for testing)
370// ============================================================================
371
372/// In-memory secret store for testing.
373pub struct InMemorySecretStore {
374    secrets: RwLock<HashMap<SecretKey, Vec<u8>>>,
375}
376
377impl InMemorySecretStore {
378    /// Create a new in-memory secret store.
379    pub fn new() -> Self {
380        Self {
381            secrets: RwLock::new(HashMap::new()),
382        }
383    }
384
385    /// Add a secret to the store.
386    pub fn add_secret(&self, key: SecretKey, value: impl Into<Vec<u8>>) {
387        self.secrets.write().insert(key, value.into());
388    }
389
390    /// Add a string secret to the store.
391    pub fn add_string_secret(&self, key: SecretKey, value: impl Into<String>) {
392        self.add_secret(key, value.into().into_bytes());
393    }
394}
395
396impl Default for InMemorySecretStore {
397    fn default() -> Self {
398        Self::new()
399    }
400}
401
402#[async_trait]
403impl SecretStore for InMemorySecretStore {
404    async fn get_secret(&self, key: &SecretKey) -> SecretResult<SecretValue> {
405        let secrets = self.secrets.read();
406        secrets
407            .get(key)
408            .map(|data| SecretValue::new(data.clone()).with_metadata("source", "in_memory"))
409            .ok_or_else(|| SecretError::NotFound(key.to_string()))
410    }
411
412    async fn secret_exists(&self, key: &SecretKey) -> SecretResult<bool> {
413        Ok(self.secrets.read().contains_key(key))
414    }
415
416    async fn list_secrets(&self, prefix: Option<&str>) -> SecretResult<Vec<SecretKey>> {
417        let secrets = self.secrets.read();
418        let keys: Vec<SecretKey> = secrets
419            .keys()
420            .filter(|k| match prefix {
421                Some(p) => k.path().starts_with(p),
422                None => true,
423            })
424            .cloned()
425            .collect();
426        Ok(keys)
427    }
428
429    fn store_name(&self) -> &str {
430        "InMemorySecretStore"
431    }
432
433    async fn health_check(&self) -> SecretResult<()> {
434        Ok(())
435    }
436}
437
438// ============================================================================
439// CHAINED SECRET STORE
440// ============================================================================
441
442/// A secret store that chains multiple stores, trying each in order.
443///
444/// Useful for fallback scenarios (e.g., try Vault, fall back to env vars).
445pub struct ChainedSecretStore {
446    stores: Vec<Arc<dyn SecretStore>>,
447}
448
449impl ChainedSecretStore {
450    /// Create a new chained secret store.
451    pub fn new() -> Self {
452        Self { stores: Vec::new() }
453    }
454
455    /// Add a store to the chain.
456    pub fn with_store(mut self, store: Arc<dyn SecretStore>) -> Self {
457        self.stores.push(store);
458        self
459    }
460}
461
462impl Default for ChainedSecretStore {
463    fn default() -> Self {
464        Self::new()
465    }
466}
467
468#[async_trait]
469impl SecretStore for ChainedSecretStore {
470    async fn get_secret(&self, key: &SecretKey) -> SecretResult<SecretValue> {
471        let mut last_error = SecretError::NotFound(key.to_string());
472
473        for store in &self.stores {
474            match store.get_secret(key).await {
475                Ok(secret) => return Ok(secret),
476                Err(e) => {
477                    last_error = e;
478                    continue;
479                }
480            }
481        }
482
483        Err(last_error)
484    }
485
486    async fn secret_exists(&self, key: &SecretKey) -> SecretResult<bool> {
487        for store in &self.stores {
488            if store.secret_exists(key).await? {
489                return Ok(true);
490            }
491        }
492        Ok(false)
493    }
494
495    async fn list_secrets(&self, prefix: Option<&str>) -> SecretResult<Vec<SecretKey>> {
496        let mut all_keys = Vec::new();
497        for store in &self.stores {
498            if let Ok(keys) = store.list_secrets(prefix).await {
499                all_keys.extend(keys);
500            }
501        }
502        // Deduplicate
503        all_keys.sort_by(|a, b| a.path().cmp(b.path()));
504        all_keys.dedup_by(|a, b| a.path() == b.path());
505        Ok(all_keys)
506    }
507
508    fn store_name(&self) -> &str {
509        "ChainedSecretStore"
510    }
511
512    async fn health_check(&self) -> SecretResult<()> {
513        for store in &self.stores {
514            if store.health_check().await.is_ok() {
515                return Ok(());
516            }
517        }
518        Err(SecretError::ConnectionError(
519            "All stores in chain are unhealthy".to_string(),
520        ))
521    }
522}
523
524// ============================================================================
525// CACHING SECRET STORE WRAPPER
526// ============================================================================
527
528/// A wrapper that adds caching to any secret store.
529pub struct CachedSecretStore<S: SecretStore> {
530    inner: S,
531    cache: RwLock<HashMap<SecretKey, CachedEntry>>,
532    ttl: Duration,
533    max_entries: usize,
534}
535
536struct CachedEntry {
537    value: SecretValue,
538    cached_at: Instant,
539}
540
541impl<S: SecretStore> CachedSecretStore<S> {
542    /// Create a new cached secret store.
543    pub fn new(inner: S) -> Self {
544        Self {
545            inner,
546            cache: RwLock::new(HashMap::new()),
547            ttl: Duration::from_secs(300), // 5 minutes
548            max_entries: 1000,
549        }
550    }
551
552    /// Set the cache TTL.
553    pub fn with_ttl(mut self, ttl: Duration) -> Self {
554        self.ttl = ttl;
555        self
556    }
557
558    /// Set the maximum cache entries.
559    pub fn with_max_entries(mut self, max: usize) -> Self {
560        self.max_entries = max;
561        self
562    }
563
564    /// Clear the cache.
565    pub fn clear_cache(&self) {
566        self.cache.write().clear();
567    }
568
569    /// Invalidate a specific key.
570    pub fn invalidate(&self, key: &SecretKey) {
571        self.cache.write().remove(key);
572    }
573}
574
575#[async_trait]
576impl<S: SecretStore> SecretStore for CachedSecretStore<S> {
577    async fn get_secret(&self, key: &SecretKey) -> SecretResult<SecretValue> {
578        // Check cache
579        {
580            let cache = self.cache.read();
581            if let Some(entry) = cache.get(key) {
582                if entry.cached_at.elapsed() < self.ttl {
583                    return Ok(SecretValue::new(entry.value.as_bytes().to_vec()));
584                }
585            }
586        }
587
588        // Fetch from inner store
589        let secret = self.inner.get_secret(key).await?;
590
591        // Cache it
592        {
593            let mut cache = self.cache.write();
594
595            // Evict if at capacity
596            if cache.len() >= self.max_entries {
597                // Remove oldest entry
598                if let Some(oldest_key) = cache
599                    .iter()
600                    .min_by_key(|(_, e)| e.cached_at)
601                    .map(|(k, _)| k.clone())
602                {
603                    cache.remove(&oldest_key);
604                }
605            }
606
607            cache.insert(
608                key.clone(),
609                CachedEntry {
610                    value: SecretValue::new(secret.as_bytes().to_vec()),
611                    cached_at: Instant::now(),
612                },
613            );
614        }
615
616        Ok(secret)
617    }
618
619    async fn secret_exists(&self, key: &SecretKey) -> SecretResult<bool> {
620        // Check cache first
621        {
622            let cache = self.cache.read();
623            if let Some(entry) = cache.get(key) {
624                if entry.cached_at.elapsed() < self.ttl {
625                    return Ok(true);
626                }
627            }
628        }
629
630        self.inner.secret_exists(key).await
631    }
632
633    async fn list_secrets(&self, prefix: Option<&str>) -> SecretResult<Vec<SecretKey>> {
634        self.inner.list_secrets(prefix).await
635    }
636
637    fn store_name(&self) -> &str {
638        self.inner.store_name()
639    }
640
641    async fn health_check(&self) -> SecretResult<()> {
642        self.inner.health_check().await
643    }
644}
645
646// ============================================================================
647// KEY ROTATION MANAGER
648// ============================================================================
649
650/// Manages automatic key rotation for encryption keys.
651pub struct KeyRotationManager {
652    /// Secret store for retrieving keys.
653    store: Arc<dyn SecretStore>,
654    /// Current key version.
655    current_version: RwLock<u64>,
656    /// Rotation interval.
657    rotation_interval: Duration,
658    /// Last rotation time.
659    last_rotation: RwLock<Instant>,
660    /// Key prefix in the store.
661    key_prefix: String,
662}
663
664impl KeyRotationManager {
665    /// Create a new key rotation manager.
666    pub fn new(store: Arc<dyn SecretStore>, key_prefix: impl Into<String>) -> Self {
667        Self {
668            store,
669            current_version: RwLock::new(1),
670            rotation_interval: Duration::from_secs(3600), // 1 hour default
671            last_rotation: RwLock::new(Instant::now()),
672            key_prefix: key_prefix.into(),
673        }
674    }
675
676    /// Set the rotation interval.
677    pub fn with_rotation_interval(mut self, interval: Duration) -> Self {
678        self.rotation_interval = interval;
679        self
680    }
681
682    /// Get the current encryption key.
683    pub async fn get_current_key(&self) -> SecretResult<SecretValue> {
684        let version = *self.current_version.read();
685        let key_name = format!("{}/v{}", self.key_prefix, version);
686        self.store.get_secret(&SecretKey::new(key_name)).await
687    }
688
689    /// Get a specific key version.
690    pub async fn get_key_version(&self, version: u64) -> SecretResult<SecretValue> {
691        let key_name = format!("{}/v{}", self.key_prefix, version);
692        self.store.get_secret(&SecretKey::new(key_name)).await
693    }
694
695    /// Check if rotation is needed.
696    pub fn needs_rotation(&self) -> bool {
697        self.last_rotation.read().elapsed() >= self.rotation_interval
698    }
699
700    /// Rotate to a new key version.
701    ///
702    /// Note: The actual key must be pre-provisioned in the secret store.
703    pub fn rotate(&self) {
704        let mut version = self.current_version.write();
705        *version += 1;
706        *self.last_rotation.write() = Instant::now();
707    }
708
709    /// Get the current key version number.
710    pub fn current_version(&self) -> u64 {
711        *self.current_version.read()
712    }
713}
714
715// ============================================================================
716// TESTS
717// ============================================================================
718
719#[cfg(test)]
720mod tests {
721    use super::*;
722
723    #[test]
724    fn test_secret_key() {
725        let key = SecretKey::new("database/password");
726        assert_eq!(key.path(), "database/password");
727        assert!(key.version().is_none());
728
729        let versioned = SecretKey::with_version("api_key", "v2");
730        assert_eq!(versioned.path(), "api_key");
731        assert_eq!(versioned.version(), Some("v2"));
732    }
733
734    #[test]
735    fn test_secret_value() {
736        let secret = SecretValue::from_string("hunter2");
737        assert_eq!(secret.as_str(), Some("hunter2"));
738        assert!(!secret.is_expired());
739
740        let expired = SecretValue::from_string("old").with_expiry(Duration::from_nanos(1));
741        std::thread::sleep(Duration::from_millis(1));
742        assert!(expired.is_expired());
743    }
744
745    #[test]
746    fn test_env_var_key_conversion() {
747        let store = EnvVarSecretStore::new("MYAPP_");
748        let key = SecretKey::new("database/password");
749        assert_eq!(store.key_to_env_var(&key), "MYAPP_DATABASE_PASSWORD");
750
751        let key2 = SecretKey::new("api.key");
752        assert_eq!(store.key_to_env_var(&key2), "MYAPP_API_KEY");
753    }
754
755    #[tokio::test]
756    async fn test_in_memory_store() {
757        let store = InMemorySecretStore::new();
758        let key = SecretKey::new("test/secret");
759
760        // Initially not found
761        assert!(store.get_secret(&key).await.is_err());
762        assert!(!store.secret_exists(&key).await.unwrap());
763
764        // Add secret
765        store.add_string_secret(key.clone(), "secret_value");
766
767        // Now found
768        let secret = store.get_secret(&key).await.unwrap();
769        assert_eq!(secret.as_str(), Some("secret_value"));
770        assert!(store.secret_exists(&key).await.unwrap());
771    }
772
773    #[tokio::test]
774    async fn test_chained_store() {
775        let store1 = Arc::new(InMemorySecretStore::new());
776        let store2 = Arc::new(InMemorySecretStore::new());
777
778        let key1 = SecretKey::new("key1");
779        let key2 = SecretKey::new("key2");
780
781        store1.add_string_secret(key1.clone(), "value1");
782        store2.add_string_secret(key2.clone(), "value2");
783
784        let chain = ChainedSecretStore::new()
785            .with_store(store1)
786            .with_store(store2);
787
788        // Can find keys from both stores
789        let secret1 = chain.get_secret(&key1).await.unwrap();
790        assert_eq!(secret1.as_str(), Some("value1"));
791
792        let secret2 = chain.get_secret(&key2).await.unwrap();
793        assert_eq!(secret2.as_str(), Some("value2"));
794
795        // Unknown key fails
796        assert!(chain.get_secret(&SecretKey::new("unknown")).await.is_err());
797    }
798
799    #[tokio::test]
800    async fn test_cached_store() {
801        let inner = InMemorySecretStore::new();
802        let key = SecretKey::new("cached_key");
803        inner.add_string_secret(key.clone(), "cached_value");
804
805        let cached = CachedSecretStore::new(inner)
806            .with_ttl(Duration::from_secs(60))
807            .with_max_entries(10);
808
809        // First fetch populates cache
810        let secret = cached.get_secret(&key).await.unwrap();
811        assert_eq!(secret.as_str(), Some("cached_value"));
812
813        // Second fetch uses cache
814        let secret2 = cached.get_secret(&key).await.unwrap();
815        assert_eq!(secret2.as_str(), Some("cached_value"));
816
817        // Invalidate and re-fetch
818        cached.invalidate(&key);
819        let secret3 = cached.get_secret(&key).await.unwrap();
820        assert_eq!(secret3.as_str(), Some("cached_value"));
821    }
822
823    #[tokio::test]
824    async fn test_list_secrets() {
825        let store = InMemorySecretStore::new();
826        store.add_string_secret(SecretKey::new("db/host"), "localhost");
827        store.add_string_secret(SecretKey::new("db/port"), "5432");
828        store.add_string_secret(SecretKey::new("api/key"), "secret");
829
830        let all = store.list_secrets(None).await.unwrap();
831        assert_eq!(all.len(), 3);
832
833        let db_only = store.list_secrets(Some("db/")).await.unwrap();
834        assert_eq!(db_only.len(), 2);
835    }
836}