1pub mod auditing;
7pub mod config;
8pub mod file_backend;
9pub mod vault_backend;
10
11use async_trait::async_trait;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use thiserror::Error;
15
16#[derive(Debug, Error, Clone, Serialize, Deserialize)]
18pub enum SecretError {
19 #[error("Secret not found: {key}")]
21 NotFound { key: String },
22
23 #[error("Authentication failed: {message}")]
25 AuthenticationFailed { message: String },
26
27 #[error("Connection error: {message}")]
29 ConnectionError { message: String },
30
31 #[error("Permission denied accessing secret: {key}")]
33 PermissionDenied { key: String },
34
35 #[error("Backend error: {message}")]
37 BackendError { message: String },
38
39 #[error("Configuration error: {message}")]
41 ConfigurationError { message: String },
42
43 #[error("Parse error: {message}")]
45 ParseError { message: String },
46
47 #[error("Operation timed out: {message}")]
49 Timeout { message: String },
50
51 #[error("Invalid secret key format: {key}")]
53 InvalidKeyFormat { key: String },
54
55 #[error("Backend unavailable: {backend}")]
57 BackendUnavailable { backend: String },
58
59 #[error("Rate limit exceeded: {message}")]
61 RateLimitExceeded { message: String },
62
63 #[error("Invalid secret value: {reason}")]
65 InvalidSecretValue { reason: String },
66
67 #[error("Crypto error: {message}")]
69 CryptoError { message: String },
70
71 #[error("IO error: {message}")]
73 IoError { message: String },
74
75 #[error("Operation not supported by backend: {operation}")]
77 UnsupportedOperation { operation: String },
78
79 #[error("Audit logging failed (strict mode): {message}")]
81 AuditFailed { message: String },
82}
83
84#[derive(Clone, Serialize, Deserialize)]
86pub struct Secret {
87 pub key: String,
89 pub value: String,
91 pub metadata: Option<HashMap<String, String>>,
93 pub created_at: Option<String>,
95 pub version: Option<String>,
97}
98
99impl Secret {
100 pub fn new(key: String, value: String) -> Self {
102 Self {
103 key,
104 value,
105 metadata: None,
106 created_at: None,
107 version: None,
108 }
109 }
110
111 pub fn with_metadata(key: String, value: String, metadata: HashMap<String, String>) -> Self {
113 Self {
114 key,
115 value,
116 metadata: Some(metadata),
117 created_at: None,
118 version: None,
119 }
120 }
121
122 pub fn value(&self) -> &str {
124 &self.value
125 }
126
127 pub fn get_metadata(&self, key: &str) -> Option<&String> {
129 self.metadata.as_ref()?.get(key)
130 }
131}
132
133impl std::fmt::Debug for Secret {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 f.debug_struct("Secret")
136 .field("key", &self.key)
137 .field("value", &"[REDACTED]")
138 .field("metadata", &self.metadata)
139 .field("created_at", &self.created_at)
140 .field("version", &self.version)
141 .finish()
142 }
143}
144
145#[async_trait]
147pub trait SecretStore: Send + Sync {
148 async fn get_secret(&self, key: &str) -> Result<Secret, SecretError>;
157
158 async fn list_secrets(&self) -> Result<Vec<String>, SecretError>;
164}
165
166#[derive(Debug, Clone)]
168pub struct SecretMetadata {
169 pub created_at: std::time::SystemTime,
171 pub expires_at: Option<std::time::SystemTime>,
173 pub rotation_hint: Option<std::time::Duration>,
175}
176
177impl SecretMetadata {
178 pub fn is_expired(&self) -> bool {
180 if let Some(expires) = self.expires_at {
181 std::time::SystemTime::now() > expires
182 } else {
183 false
184 }
185 }
186
187 pub fn needs_rotation(&self) -> bool {
189 if let (Some(expires), Some(hint)) = (self.expires_at, self.rotation_hint) {
190 if let Ok(remaining) = expires.duration_since(std::time::SystemTime::now()) {
191 return remaining < hint;
192 }
193 }
194 false
195 }
196}
197
198pub async fn get_secret_checked(
201 store: &dyn SecretStore,
202 key: &str,
203 metadata: Option<&SecretMetadata>,
204) -> Result<Secret, SecretError> {
205 if let Some(meta) = metadata {
206 if meta.is_expired() {
207 return Err(SecretError::BackendError {
208 message: format!("Secret '{key}' has expired"),
209 });
210 }
211 if meta.needs_rotation() {
212 tracing::warn!(
213 secret = key,
214 "Secret is approaching expiry and should be rotated"
215 );
216 }
217 }
218 store.get_secret(key).await
219}
220
221pub type SecretResult<T> = Result<T, SecretError>;
223
224pub use auditing::*;
226pub use config::*;
227pub use file_backend::FileSecretStore;
228pub use vault_backend::VaultSecretStore;
229
230pub async fn new_secret_store(
240 config: &SecretsConfig,
241 agent_id: &str,
242) -> Result<Box<dyn SecretStore + Send + Sync>, SecretError> {
243 let audit_sink = auditing::create_audit_sink(&config.common.audit);
245
246 match &config.backend {
247 SecretsBackend::File(file_config) => {
248 let store = FileSecretStore::new(file_config.clone(), audit_sink, agent_id.to_string())
249 .await
250 .map_err(|e| SecretError::ConfigurationError {
251 message: format!("Failed to initialize file backend: {}", e),
252 })?;
253 Ok(Box::new(store))
254 }
255 SecretsBackend::Vault(vault_config) => {
256 let store =
257 VaultSecretStore::new(vault_config.clone(), agent_id.to_string(), audit_sink)
258 .await
259 .map_err(|e| SecretError::ConfigurationError {
260 message: format!("Failed to initialize vault backend: {}", e),
261 })?;
262 Ok(Box::new(store))
263 }
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn test_secret_creation() {
273 let secret = Secret::new("test_key".to_string(), "test_value".to_string());
274 assert_eq!(secret.key, "test_key");
275 assert_eq!(secret.value(), "test_value");
276 assert!(secret.metadata.is_none());
277 }
278
279 #[test]
280 fn test_secret_with_metadata() {
281 let mut metadata = HashMap::new();
282 metadata.insert("description".to_string(), "Test secret".to_string());
283
284 let secret =
285 Secret::with_metadata("test_key".to_string(), "test_value".to_string(), metadata);
286
287 assert_eq!(secret.key, "test_key");
288 assert_eq!(secret.value(), "test_value");
289 assert_eq!(
290 secret.get_metadata("description"),
291 Some(&"Test secret".to_string())
292 );
293 }
294
295 #[test]
296 fn test_secret_error_display() {
297 let error = SecretError::NotFound {
298 key: "missing_key".to_string(),
299 };
300 assert!(error.to_string().contains("Secret not found: missing_key"));
301 }
302
303 #[test]
304 fn test_secret_metadata_not_expired() {
305 let meta = SecretMetadata {
306 created_at: std::time::SystemTime::now(),
307 expires_at: Some(std::time::SystemTime::now() + std::time::Duration::from_secs(3600)),
308 rotation_hint: None,
309 };
310 assert!(!meta.is_expired());
311 }
312
313 #[test]
314 fn test_secret_metadata_expired() {
315 let meta = SecretMetadata {
316 created_at: std::time::SystemTime::now() - std::time::Duration::from_secs(7200),
317 expires_at: Some(std::time::SystemTime::now() - std::time::Duration::from_secs(1)),
318 rotation_hint: None,
319 };
320 assert!(meta.is_expired());
321 }
322
323 #[test]
324 fn test_secret_metadata_no_expiry() {
325 let meta = SecretMetadata {
326 created_at: std::time::SystemTime::now(),
327 expires_at: None,
328 rotation_hint: None,
329 };
330 assert!(!meta.is_expired());
331 assert!(!meta.needs_rotation());
332 }
333
334 #[test]
335 fn test_secret_metadata_needs_rotation() {
336 let meta = SecretMetadata {
338 created_at: std::time::SystemTime::now() - std::time::Duration::from_secs(3600),
339 expires_at: Some(std::time::SystemTime::now() + std::time::Duration::from_secs(300)),
340 rotation_hint: Some(std::time::Duration::from_secs(600)),
341 };
342 assert!(!meta.is_expired());
343 assert!(meta.needs_rotation());
344 }
345
346 #[test]
347 fn test_secret_metadata_no_rotation_needed() {
348 let meta = SecretMetadata {
350 created_at: std::time::SystemTime::now(),
351 expires_at: Some(std::time::SystemTime::now() + std::time::Duration::from_secs(7200)),
352 rotation_hint: Some(std::time::Duration::from_secs(600)),
353 };
354 assert!(!meta.is_expired());
355 assert!(!meta.needs_rotation());
356 }
357}