1use 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
42pub struct SecretKey {
43 path: String,
45 version: Option<String>,
47}
48
49impl SecretKey {
50 pub fn new(path: impl Into<String>) -> Self {
52 Self {
53 path: path.into(),
54 version: None,
55 }
56 }
57
58 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 pub fn path(&self) -> &str {
68 &self.path
69 }
70
71 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
87pub struct SecretValue {
96 #[cfg(feature = "crypto")]
98 data: zeroize::Zeroizing<Vec<u8>>,
99 #[cfg(not(feature = "crypto"))]
100 data: Vec<u8>,
101 retrieved_at: Instant,
103 expires_at: Option<Instant>,
105 metadata: HashMap<String, String>,
107}
108
109impl SecretValue {
110 #[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 #[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 pub fn from_string(s: impl Into<String>) -> Self {
134 Self::new(s.into().into_bytes())
135 }
136
137 pub fn with_expiry(mut self, duration: Duration) -> Self {
139 self.expires_at = Some(Instant::now() + duration);
140 self
141 }
142
143 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 pub fn as_bytes(&self) -> &[u8] {
151 &self.data
152 }
153
154 pub fn as_str(&self) -> Option<&str> {
156 std::str::from_utf8(&self.data).ok()
157 }
158
159 pub fn is_expired(&self) -> bool {
161 self.expires_at
162 .map(|exp| Instant::now() > exp)
163 .unwrap_or(false)
164 }
165
166 pub fn age(&self) -> Duration {
168 self.retrieved_at.elapsed()
169 }
170
171 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#[derive(Debug, Clone)]
194pub enum SecretError {
195 NotFound(String),
197 AccessDenied(String),
199 ConnectionError(String),
201 InvalidFormat(String),
203 Expired(String),
205 RateLimited(String),
207 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
227pub type SecretResult<T> = Result<T, SecretError>;
229
230#[async_trait]
232pub trait SecretStore: Send + Sync {
233 async fn get_secret(&self, key: &SecretKey) -> SecretResult<SecretValue>;
235
236 async fn secret_exists(&self, key: &SecretKey) -> SecretResult<bool>;
238
239 async fn list_secrets(&self, prefix: Option<&str>) -> SecretResult<Vec<SecretKey>>;
241
242 fn store_name(&self) -> &str;
244
245 async fn health_check(&self) -> SecretResult<()>;
247}
248
249pub struct EnvVarSecretStore {
263 prefix: String,
265 cache: RwLock<HashMap<SecretKey, SecretValue>>,
267 cache_duration: Duration,
269}
270
271impl EnvVarSecretStore {
272 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), }
279 }
280
281 pub fn with_cache_duration(mut self, duration: Duration) -> Self {
283 self.cache_duration = duration;
284 self
285 }
286
287 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 {
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 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 {
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 Ok(())
365 }
366}
367
368pub struct InMemorySecretStore {
374 secrets: RwLock<HashMap<SecretKey, Vec<u8>>>,
375}
376
377impl InMemorySecretStore {
378 pub fn new() -> Self {
380 Self {
381 secrets: RwLock::new(HashMap::new()),
382 }
383 }
384
385 pub fn add_secret(&self, key: SecretKey, value: impl Into<Vec<u8>>) {
387 self.secrets.write().insert(key, value.into());
388 }
389
390 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
438pub struct ChainedSecretStore {
446 stores: Vec<Arc<dyn SecretStore>>,
447}
448
449impl ChainedSecretStore {
450 pub fn new() -> Self {
452 Self { stores: Vec::new() }
453 }
454
455 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 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
524pub 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 pub fn new(inner: S) -> Self {
544 Self {
545 inner,
546 cache: RwLock::new(HashMap::new()),
547 ttl: Duration::from_secs(300), max_entries: 1000,
549 }
550 }
551
552 pub fn with_ttl(mut self, ttl: Duration) -> Self {
554 self.ttl = ttl;
555 self
556 }
557
558 pub fn with_max_entries(mut self, max: usize) -> Self {
560 self.max_entries = max;
561 self
562 }
563
564 pub fn clear_cache(&self) {
566 self.cache.write().clear();
567 }
568
569 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 {
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 let secret = self.inner.get_secret(key).await?;
590
591 {
593 let mut cache = self.cache.write();
594
595 if cache.len() >= self.max_entries {
597 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 {
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
646pub struct KeyRotationManager {
652 store: Arc<dyn SecretStore>,
654 current_version: RwLock<u64>,
656 rotation_interval: Duration,
658 last_rotation: RwLock<Instant>,
660 key_prefix: String,
662}
663
664impl KeyRotationManager {
665 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), last_rotation: RwLock::new(Instant::now()),
672 key_prefix: key_prefix.into(),
673 }
674 }
675
676 pub fn with_rotation_interval(mut self, interval: Duration) -> Self {
678 self.rotation_interval = interval;
679 self
680 }
681
682 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 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 pub fn needs_rotation(&self) -> bool {
697 self.last_rotation.read().elapsed() >= self.rotation_interval
698 }
699
700 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 pub fn current_version(&self) -> u64 {
711 *self.current_version.read()
712 }
713}
714
715#[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 assert!(store.get_secret(&key).await.is_err());
762 assert!(!store.secret_exists(&key).await.unwrap());
763
764 store.add_string_secret(key.clone(), "secret_value");
766
767 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 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 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 let secret = cached.get_secret(&key).await.unwrap();
811 assert_eq!(secret.as_str(), Some("cached_value"));
812
813 let secret2 = cached.get_secret(&key).await.unwrap();
815 assert_eq!(secret2.as_str(), Some("cached_value"));
816
817 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}