1use std::collections::HashMap;
12use std::path::PathBuf;
13use std::time::SystemTime;
14
15use serde::{Deserialize, Serialize};
16
17use super::error::SecretsResult;
18
19#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21#[serde(default)]
22pub struct SecretsConfig {
23 pub cache: CacheConfig,
25
26 #[cfg(feature = "secrets-vault")]
28 pub openbao: Option<super::OpenBaoConfig>,
29
30 #[cfg(feature = "secrets-aws")]
32 pub aws: Option<super::AwsConfig>,
33
34 #[cfg(not(feature = "secrets-vault"))]
36 #[serde(skip)]
37 pub openbao: Option<()>,
38
39 #[cfg(not(feature = "secrets-aws"))]
41 #[serde(skip)]
42 pub aws: Option<()>,
43
44 pub sources: HashMap<String, SecretSource>,
46}
47
48impl SecretsConfig {
49 #[must_use]
51 pub fn from_cascade() -> Self {
52 #[cfg(feature = "config")]
53 {
54 if let Some(cfg) = crate::config::try_get()
55 && let Ok(secrets) = cfg.unmarshal_key_registered::<Self>("secrets")
56 {
57 return secrets;
58 }
59 }
60 Self::default()
61 }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(default)]
67pub struct CacheConfig {
68 pub enabled: bool,
70
71 pub directory: Option<PathBuf>,
73
74 pub ttl_secs: u64,
76
77 pub stale_grace_secs: u64,
79
80 pub refresh_interval_secs: u64,
82
83 pub refresh_jitter_secs: u64,
85
86 pub encryption_key: Option<crate::SensitiveString>,
89
90 pub dir_mode: Option<u32>,
96
97 pub file_mode: Option<u32>,
100}
101
102impl Default for CacheConfig {
103 fn default() -> Self {
104 Self {
105 enabled: true,
106 directory: None, ttl_secs: 3600, stale_grace_secs: 86400, refresh_interval_secs: 1800, refresh_jitter_secs: 300, encryption_key: None,
112 dir_mode: Some(0o700),
113 file_mode: Some(0o600),
114 }
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120#[serde(tag = "provider", rename_all = "snake_case")]
121pub enum SecretSource {
122 File {
124 path: String,
126 },
127
128 OpenBao {
130 path: String,
132 key: String,
134 },
135
136 Aws {
138 secret_id: String,
140 key: Option<String>,
142 },
143}
144
145#[derive(Clone)]
147pub struct SecretValue {
148 pub data: Vec<u8>,
150
151 pub fetched_at: SystemTime,
153
154 pub metadata: SecretMetadata,
156}
157
158impl std::fmt::Debug for SecretValue {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 f.debug_struct("SecretValue")
161 .field("data", &"[REDACTED]")
162 .field("fetched_at", &self.fetched_at)
163 .field("metadata", &self.metadata)
164 .finish()
165 }
166}
167
168impl SecretValue {
169 #[must_use]
171 pub fn new(data: Vec<u8>) -> Self {
172 Self {
173 data,
174 fetched_at: SystemTime::now(),
175 metadata: SecretMetadata::default(),
176 }
177 }
178
179 #[must_use]
181 pub fn with_metadata(data: Vec<u8>, metadata: SecretMetadata) -> Self {
182 Self {
183 data,
184 fetched_at: SystemTime::now(),
185 metadata,
186 }
187 }
188
189 pub fn as_str(&self) -> SecretsResult<&str> {
195 std::str::from_utf8(&self.data)
196 .map_err(|e| super::error::SecretsError::InvalidData(format!("not valid UTF-8: {e}")))
197 }
198
199 #[must_use]
201 pub fn as_bytes(&self) -> &[u8] {
202 &self.data
203 }
204
205 #[must_use]
207 pub fn is_expired(&self, ttl_secs: u64) -> bool {
208 self.fetched_at
209 .elapsed()
210 .map_or(true, |d| d.as_secs() >= ttl_secs)
211 }
212
213 #[must_use]
215 pub fn is_within_grace(&self, ttl_secs: u64, grace_secs: u64) -> bool {
216 self.fetched_at
217 .elapsed()
218 .is_ok_and(|d| d.as_secs() <= ttl_secs + grace_secs)
219 }
220}
221
222#[derive(Debug, Clone, Default, Serialize, Deserialize)]
224pub struct SecretMetadata {
225 pub version: Option<String>,
227
228 pub source_path: Option<String>,
230
231 pub provider: Option<String>,
233}
234
235#[derive(Debug, Clone)]
237pub struct RotationEvent {
238 pub name: String,
240
241 pub old_version: Option<String>,
243
244 pub new_version: String,
246
247 pub rotated_at: SystemTime,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
253pub(crate) struct CacheEntry {
254 pub data: String,
256
257 pub fetched_at_secs: u64,
259
260 pub metadata: SecretMetadata,
262}
263
264impl CacheEntry {
265 pub fn from_value(value: &SecretValue) -> Self {
267 use base64::Engine;
268 Self {
269 data: base64::engine::general_purpose::STANDARD.encode(&value.data),
270 fetched_at_secs: value
271 .fetched_at
272 .duration_since(SystemTime::UNIX_EPOCH)
273 .map_or(0, |d| d.as_secs()),
274 metadata: value.metadata.clone(),
275 }
276 }
277
278 pub fn to_value(&self) -> SecretsResult<SecretValue> {
280 use base64::Engine;
281 let data = base64::engine::general_purpose::STANDARD
282 .decode(&self.data)
283 .map_err(|e| super::error::SecretsError::CacheError(format!("invalid base64: {e}")))?;
284
285 let fetched_at =
286 SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(self.fetched_at_secs);
287
288 Ok(SecretValue {
289 data,
290 fetched_at,
291 metadata: self.metadata.clone(),
292 })
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn test_secret_value_new() {
302 let value = SecretValue::new(b"test-secret".to_vec());
303 assert_eq!(value.as_bytes(), b"test-secret");
304 assert_eq!(value.as_str().unwrap(), "test-secret");
305 }
306
307 #[test]
308 fn test_secret_value_expiry() {
309 let value = SecretValue::new(b"test".to_vec());
310 assert!(!value.is_expired(3600));
312 assert!(value.is_within_grace(3600, 86400));
313 }
314
315 #[test]
316 fn test_cache_entry_roundtrip() {
317 let value = SecretValue::new(b"secret-data".to_vec());
318 let entry = CacheEntry::from_value(&value);
319 let restored = entry.to_value().unwrap();
320 assert_eq!(restored.data, value.data);
321 }
322
323 #[test]
324 fn test_secret_source_file_serialization() {
325 let source = SecretSource::File {
326 path: "/etc/ssl/cert.pem".to_string(),
327 };
328 let json = serde_json::to_string(&source).unwrap();
329 assert!(json.contains("\"provider\":\"file\""));
330 }
331
332 #[test]
333 fn test_cache_config_default() {
334 let config = CacheConfig::default();
335 assert!(config.enabled);
336 assert_eq!(config.ttl_secs, 3600);
337 assert_eq!(config.stale_grace_secs, 86400);
338 assert!(config.encryption_key.is_none());
339 }
340}