Skip to main content

harn_vm/secrets/
mod.rs

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}