1use std::collections::BTreeMap;
2use std::fmt;
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use zeroize::{Zeroize, Zeroizing};
8
9mod env;
10mod keyring;
11
12pub use env::EnvSecretProvider;
13pub use keyring::KeyringSecretProvider;
14
15pub const DEFAULT_SECRET_PROVIDER_CHAIN: &str = "env,keyring";
16pub const SECRET_PROVIDER_CHAIN_ENV: &str = "HARN_SECRET_PROVIDERS";
17
18#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
19pub enum SecretVersion {
20 #[default]
21 Latest,
22 Exact(u64),
23}
24
25#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
26pub struct SecretId {
27 pub namespace: String,
28 pub name: String,
29 #[serde(default)]
30 pub version: SecretVersion,
31}
32
33impl SecretId {
34 pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
35 Self {
36 namespace: namespace.into(),
37 name: name.into(),
38 version: SecretVersion::Latest,
39 }
40 }
41
42 pub fn with_version(mut self, version: SecretVersion) -> Self {
43 self.version = version;
44 self
45 }
46}
47
48impl fmt::Display for SecretId {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 if self.namespace.is_empty() {
51 write!(f, "{}", self.name)?;
52 } else {
53 write!(f, "{}/{}", self.namespace, self.name)?;
54 }
55 match self.version {
56 SecretVersion::Latest => Ok(()),
57 SecretVersion::Exact(version) => write!(f, "@{version}"),
58 }
59 }
60}
61
62#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
63pub struct SecretMeta {
64 pub id: SecretId,
65 pub provider: String,
66}
67
68#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
69pub struct RotationHandle {
70 pub provider: String,
71 pub id: SecretId,
72 pub from_version: Option<u64>,
73 pub to_version: Option<u64>,
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub enum SecretError {
78 NotFound {
79 provider: String,
80 id: SecretId,
81 },
82 Unsupported {
83 provider: String,
84 operation: &'static str,
85 },
86 Backend {
87 provider: String,
88 message: String,
89 },
90 InvalidConfig(String),
91 NoProviders {
92 namespace: String,
93 },
94 All(Vec<SecretError>),
95}
96
97impl fmt::Display for SecretError {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 match self {
100 Self::NotFound { provider, id } => {
101 write!(f, "{provider}: secret '{id}' not found")
102 }
103 Self::Unsupported {
104 provider,
105 operation,
106 } => write!(f, "{provider}: operation '{operation}' is unsupported"),
107 Self::Backend { provider, message } => write!(f, "{provider}: {message}"),
108 Self::InvalidConfig(message) => write!(f, "{message}"),
109 Self::NoProviders { namespace } => {
110 write!(
111 f,
112 "no secret providers configured for namespace '{namespace}'"
113 )
114 }
115 Self::All(errors) => {
116 let rendered = errors
117 .iter()
118 .map(ToString::to_string)
119 .collect::<Vec<_>>()
120 .join("; ");
121 write!(f, "all secret providers failed: {rendered}")
122 }
123 }
124 }
125}
126
127impl std::error::Error for SecretError {}
128
129#[derive(Default)]
130struct SecretBuffer {
131 bytes: Vec<u8>,
132 #[cfg(test)]
133 drop_probe: Option<std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>>,
134}
135
136impl SecretBuffer {
137 fn new(bytes: Vec<u8>) -> Self {
138 Self {
139 bytes,
140 #[cfg(test)]
141 drop_probe: None,
142 }
143 }
144
145 fn as_slice(&self) -> &[u8] {
146 &self.bytes
147 }
148
149 #[cfg(test)]
150 fn attach_drop_probe(&mut self, probe: std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>) {
151 self.drop_probe = Some(probe);
152 }
153}
154
155impl std::ops::Deref for SecretBuffer {
156 type Target = [u8];
157
158 fn deref(&self) -> &Self::Target {
159 self.as_slice()
160 }
161}
162
163impl Zeroize for SecretBuffer {
164 fn zeroize(&mut self) {
165 self.bytes.zeroize();
166 }
167}
168
169impl Drop for SecretBuffer {
170 fn drop(&mut self) {
171 #[cfg(test)]
172 if let Some(probe) = &self.drop_probe {
173 *probe.lock().expect("drop probe poisoned") = Some(self.bytes.clone());
174 }
175 }
176}
177
178pub struct SecretBytes(Zeroizing<SecretBuffer>);
179
180impl SecretBytes {
181 pub fn new(bytes: Vec<u8>) -> Self {
182 Self(Zeroizing::new(SecretBuffer::new(bytes)))
183 }
184
185 pub fn len(&self) -> usize {
186 self.0.as_slice().len()
187 }
188
189 pub fn is_empty(&self) -> bool {
190 self.0.as_slice().is_empty()
191 }
192
193 pub fn with_exposed<R>(&self, f: impl FnOnce(&[u8]) -> R) -> R {
194 f(self.0.as_slice())
195 }
196
197 pub fn reborrow(&self) -> Self {
198 self.with_exposed(|bytes| Self::new(bytes.to_vec()))
199 }
200
201 #[cfg(test)]
202 pub(crate) fn attach_drop_probe(
203 &mut self,
204 probe: std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>,
205 ) {
206 self.0.attach_drop_probe(probe);
207 }
208}
209
210impl fmt::Debug for SecretBytes {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 write!(f, "SecretBytes {{ redacted: {} bytes }}", self.len())
213 }
214}
215
216impl Serialize for SecretBytes {
217 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
218 where
219 S: serde::Serializer,
220 {
221 serializer.serialize_str(&format!("<redacted:{} bytes>", self.len()))
222 }
223}
224
225impl From<Vec<u8>> for SecretBytes {
226 fn from(value: Vec<u8>) -> Self {
227 Self::new(value)
228 }
229}
230
231impl From<String> for SecretBytes {
232 fn from(value: String) -> Self {
233 Self::new(value.into_bytes())
234 }
235}
236
237impl From<&str> for SecretBytes {
238 fn from(value: &str) -> Self {
239 Self::new(value.as_bytes().to_vec())
240 }
241}
242
243impl From<&[u8]> for SecretBytes {
244 fn from(value: &[u8]) -> Self {
245 Self::new(value.to_vec())
246 }
247}
248
249#[async_trait]
250pub trait SecretProvider: Send + Sync {
251 async fn get(&self, id: &SecretId) -> Result<SecretBytes, SecretError>;
252 async fn put(&self, id: &SecretId, value: SecretBytes) -> Result<(), SecretError>;
253 async fn rotate(&self, id: &SecretId) -> Result<RotationHandle, SecretError>;
254 async fn list(&self, prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError>;
255
256 fn namespace(&self) -> &str;
257 fn supports_versions(&self) -> bool;
258}
259
260pub struct ChainSecretProvider {
261 namespace: String,
262 providers: Vec<Arc<dyn SecretProvider>>,
263}
264
265impl ChainSecretProvider {
266 pub fn new(namespace: impl Into<String>, providers: Vec<Arc<dyn SecretProvider>>) -> Self {
267 Self {
268 namespace: namespace.into(),
269 providers,
270 }
271 }
272
273 pub fn providers(&self) -> &[Arc<dyn SecretProvider>] {
274 &self.providers
275 }
276}
277
278#[async_trait]
279impl SecretProvider for ChainSecretProvider {
280 async fn get(&self, id: &SecretId) -> Result<SecretBytes, SecretError> {
281 if self.providers.is_empty() {
282 return Err(SecretError::NoProviders {
283 namespace: self.namespace.clone(),
284 });
285 }
286
287 let mut errors = Vec::new();
288 for provider in &self.providers {
289 match provider.get(id).await {
290 Ok(secret) => return Ok(secret),
291 Err(error) => errors.push(error),
292 }
293 }
294
295 Err(SecretError::All(errors))
296 }
297
298 async fn put(&self, id: &SecretId, value: SecretBytes) -> Result<(), SecretError> {
299 if self.providers.is_empty() {
300 return Err(SecretError::NoProviders {
301 namespace: self.namespace.clone(),
302 });
303 }
304
305 let mut last_value = Some(value);
306 let mut errors = Vec::new();
307 for (index, provider) in self.providers.iter().enumerate() {
308 let attempt_value = if index + 1 == self.providers.len() {
309 last_value
310 .take()
311 .expect("final secret write attempt missing value")
312 } else {
313 last_value
314 .as_ref()
315 .expect("intermediate secret write attempt missing value")
316 .reborrow()
317 };
318 match provider.put(id, attempt_value).await {
319 Ok(()) => return Ok(()),
320 Err(error) => errors.push(error),
321 }
322 }
323
324 Err(SecretError::All(errors))
325 }
326
327 async fn rotate(&self, id: &SecretId) -> Result<RotationHandle, SecretError> {
328 if self.providers.is_empty() {
329 return Err(SecretError::NoProviders {
330 namespace: self.namespace.clone(),
331 });
332 }
333
334 let mut errors = Vec::new();
335 for provider in &self.providers {
336 match provider.rotate(id).await {
337 Ok(handle) => return Ok(handle),
338 Err(error) => errors.push(error),
339 }
340 }
341
342 Err(SecretError::All(errors))
343 }
344
345 async fn list(&self, prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError> {
346 if self.providers.is_empty() {
347 return Err(SecretError::NoProviders {
348 namespace: self.namespace.clone(),
349 });
350 }
351
352 let mut errors = Vec::new();
353 let mut merged = BTreeMap::<SecretId, SecretMeta>::new();
354 for provider in &self.providers {
355 match provider.list(prefix).await {
356 Ok(items) => {
357 for item in items {
358 merged.entry(item.id.clone()).or_insert(item);
359 }
360 }
361 Err(error) => errors.push(error),
362 }
363 }
364
365 if merged.is_empty() && !errors.is_empty() {
366 return Err(SecretError::All(errors));
367 }
368
369 Ok(merged.into_values().collect())
370 }
371
372 fn namespace(&self) -> &str {
373 &self.namespace
374 }
375
376 fn supports_versions(&self) -> bool {
377 self.providers
378 .iter()
379 .any(|provider| provider.supports_versions())
380 }
381}
382
383pub fn configured_default_chain(
384 namespace: impl Into<String>,
385) -> Result<ChainSecretProvider, SecretError> {
386 let namespace = namespace.into();
387 let configured = std::env::var(SECRET_PROVIDER_CHAIN_ENV)
388 .unwrap_or_else(|_| DEFAULT_SECRET_PROVIDER_CHAIN.to_string());
389 let mut providers: Vec<Arc<dyn SecretProvider>> = Vec::new();
390
391 for raw_name in configured.split(',') {
392 let provider_name = raw_name.trim();
393 if provider_name.is_empty() {
394 continue;
395 }
396 match provider_name {
397 "env" => providers.push(Arc::new(EnvSecretProvider::new(namespace.clone()))),
398 "keyring" => providers.push(Arc::new(KeyringSecretProvider::new(namespace.clone()))),
399 other => {
400 return Err(SecretError::InvalidConfig(format!(
401 "unsupported secret provider '{other}' in {SECRET_PROVIDER_CHAIN_ENV}; expected a comma-separated list of env,keyring"
402 )))
403 }
404 }
405 }
406
407 Ok(ChainSecretProvider::new(namespace, providers))
408}
409
410pub(crate) fn emit_secret_access_event(provider: &str, id: &SecretId) {
411 #[derive(Serialize)]
412 struct SecretAccessEvent<'a> {
413 topic: &'a str,
414 provider: &'a str,
415 id: &'a SecretId,
416 caller_span_id: Option<u64>,
417 mutation_session_id: Option<String>,
418 timestamp: String,
419 }
420
421 let event = SecretAccessEvent {
422 topic: "audit.secret_access",
423 provider,
424 id,
425 caller_span_id: crate::tracing::current_span_id(),
426 mutation_session_id: crate::orchestration::current_mutation_session()
427 .map(|session| session.session_id),
428 timestamp: crate::orchestration::now_rfc3339(),
429 };
430 let metadata = serde_json::to_value(event)
431 .ok()
432 .and_then(|value| value.as_object().cloned())
433 .map(|object| object.into_iter().collect::<BTreeMap<_, _>>())
434 .unwrap_or_default();
435 crate::events::log_info_meta("secret.audit", "secret accessed", metadata);
436}
437
438#[cfg(test)]
439mod tests {
440 use std::sync::{Arc, Mutex, Once};
441
442 use async_trait::async_trait;
443
444 use super::*;
445
446 fn install_mock_keyring() {
447 static INIT: Once = Once::new();
448 INIT.call_once(|| {
449 ::keyring::set_default_credential_builder(::keyring::mock::default_credential_builder());
450 });
451 }
452
453 struct FakeProvider {
454 namespace: String,
455 result: Mutex<Vec<Result<SecretBytes, SecretError>>>,
456 }
457
458 impl FakeProvider {
459 fn new(
460 namespace: impl Into<String>,
461 result: Vec<Result<SecretBytes, SecretError>>,
462 ) -> Self {
463 Self {
464 namespace: namespace.into(),
465 result: Mutex::new(result),
466 }
467 }
468 }
469
470 #[async_trait]
471 impl SecretProvider for FakeProvider {
472 async fn get(&self, _id: &SecretId) -> Result<SecretBytes, SecretError> {
473 self.result
474 .lock()
475 .expect("fake provider poisoned")
476 .remove(0)
477 }
478
479 async fn put(&self, _id: &SecretId, _value: SecretBytes) -> Result<(), SecretError> {
480 Err(SecretError::Unsupported {
481 provider: self.namespace.clone(),
482 operation: "put",
483 })
484 }
485
486 async fn rotate(&self, _id: &SecretId) -> Result<RotationHandle, SecretError> {
487 Err(SecretError::Unsupported {
488 provider: self.namespace.clone(),
489 operation: "rotate",
490 })
491 }
492
493 async fn list(&self, _prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError> {
494 Err(SecretError::Unsupported {
495 provider: self.namespace.clone(),
496 operation: "list",
497 })
498 }
499
500 fn namespace(&self) -> &str {
501 &self.namespace
502 }
503
504 fn supports_versions(&self) -> bool {
505 false
506 }
507 }
508
509 #[test]
510 fn secret_bytes_debug_is_redacted() {
511 let secret = SecretBytes::from("abcd");
512 assert_eq!(format!("{secret:?}"), "SecretBytes { redacted: 4 bytes }");
513 }
514
515 #[test]
516 fn secret_bytes_zeroes_on_drop() {
517 let probe = Arc::new(Mutex::new(None));
518 let mut secret = SecretBytes::from("super-secret");
519 secret.attach_drop_probe(probe.clone());
520 drop(secret);
521
522 let dropped = probe
523 .lock()
524 .expect("drop probe poisoned")
525 .clone()
526 .expect("probe should capture bytes");
527 assert!(dropped.iter().all(|byte| *byte == 0));
528 }
529
530 #[tokio::test]
531 async fn chain_secret_provider_falls_through_to_next_hit() {
532 let id = SecretId::new("harn.test", "api-key");
533 let first = Arc::new(FakeProvider::new(
534 "first",
535 vec![Err(SecretError::NotFound {
536 provider: "first".to_string(),
537 id: id.clone(),
538 })],
539 ));
540 let second = Arc::new(FakeProvider::new(
541 "second",
542 vec![Ok(SecretBytes::from("value"))],
543 ));
544 let chain = ChainSecretProvider::new("harn/test", vec![first, second]);
545
546 let secret = chain.get(&id).await.expect("chain should resolve");
547 let exposed = secret.with_exposed(|bytes| bytes.to_vec());
548 assert_eq!(exposed, b"value");
549 }
550
551 #[tokio::test]
552 async fn chain_secret_provider_returns_all_errors_when_everything_fails() {
553 let id = SecretId::new("harn.test", "missing");
554 let first = Arc::new(FakeProvider::new(
555 "first",
556 vec![Err(SecretError::NotFound {
557 provider: "first".to_string(),
558 id: id.clone(),
559 })],
560 ));
561 let second = Arc::new(FakeProvider::new(
562 "second",
563 vec![Err(SecretError::Backend {
564 provider: "second".to_string(),
565 message: "boom".to_string(),
566 })],
567 ));
568 let chain = ChainSecretProvider::new("harn/test", vec![first, second]);
569
570 let error = chain.get(&id).await.expect_err("chain should fail");
571 match error {
572 SecretError::All(errors) => {
573 assert_eq!(errors.len(), 2);
574 assert!(matches!(errors[0], SecretError::NotFound { .. }));
575 assert!(matches!(errors[1], SecretError::Backend { .. }));
576 }
577 other => panic!("expected aggregated errors, got {other:?}"),
578 }
579 }
580
581 #[tokio::test]
582 async fn keyring_provider_round_trips_and_zeroes_on_drop() {
583 install_mock_keyring();
584
585 let provider = KeyringSecretProvider::new("harn.test");
586 let id = SecretId::new("", format!("mock-{}", uuid::Uuid::now_v7()));
587 provider
588 .put(&id, SecretBytes::from("round-trip-secret"))
589 .await
590 .expect("mock keyring write should succeed");
591
592 let probe = Arc::new(Mutex::new(None));
593 let mut secret = provider
594 .get(&id)
595 .await
596 .expect("mock keyring read should succeed");
597 assert_eq!(
598 secret.with_exposed(|bytes| bytes.to_vec()),
599 b"round-trip-secret"
600 );
601 secret.attach_drop_probe(probe.clone());
602 drop(secret);
603
604 let dropped = probe
605 .lock()
606 .expect("drop probe poisoned")
607 .clone()
608 .expect("probe should capture bytes");
609 assert!(dropped.iter().all(|byte| *byte == 0));
610
611 provider
612 .delete(&id)
613 .await
614 .expect("mock keyring delete should succeed");
615 }
616}