cuenv_secrets/
lib.rs

1//! Secret Resolution for cuenv
2//!
3//! Provides a unified interface for resolving secrets from various providers
4//! (environment variables, command execution, 1Password, Vault, etc.) with
5//! support for cache key fingerprinting and salt rotation.
6//!
7//! # Batch Resolution
8//!
9//! For resolving multiple secrets efficiently, use the batch resolution API:
10//!
11//! ```ignore
12//! use cuenv_secrets::{BatchSecrets, SecretResolver, SecretSpec};
13//!
14//! // Resolve multiple secrets concurrently
15//! let secrets = resolver.resolve_batch(&specs).await?;
16//!
17//! // Use secrets during task execution
18//! for name in secrets.names() {
19//!     if let Some(secret) = secrets.get(name) {
20//!         std::env::set_var(name, secret.expose());
21//!     }
22//! }
23//! // Secrets are zeroed when `secrets` goes out of scope
24//! ```
25
26mod batch;
27mod fingerprint;
28mod resolved;
29pub mod resolvers;
30mod salt;
31mod types;
32
33pub use batch::{BatchConfig, BatchResolver, resolve_batch};
34pub use fingerprint::compute_secret_fingerprint;
35pub use resolved::ResolvedSecrets;
36pub use salt::SaltConfig;
37pub use types::{BatchSecrets, SecureSecret};
38
39// Re-export built-in resolvers (no external dependencies)
40pub use resolvers::{EnvSecretResolver, ExecSecretResolver};
41
42// Provider implementations are in separate crates:
43// - cuenv-aws: AwsResolver, AwsSecretConfig
44// - cuenv-gcp: GcpResolver, GcpSecretConfig
45// - cuenv-vault: VaultResolver, VaultSecretConfig
46// - cuenv-1password: OnePasswordResolver, OnePasswordConfig
47
48use async_trait::async_trait;
49use serde::{Deserialize, Serialize};
50use std::collections::HashMap;
51use thiserror::Error;
52
53/// Error types for secret resolution
54#[derive(Debug, Error)]
55pub enum SecretError {
56    /// Secret not found
57    #[error("Secret '{name}' not found from source '{secret_source}'")]
58    NotFound {
59        /// Secret name
60        name: String,
61        /// Source that was searched (e.g., env var name)
62        secret_source: String,
63    },
64
65    /// Secret is too short for safe fingerprinting (< 4 chars)
66    #[error("Secret '{name}' is too short ({len} chars, minimum 4) for cache key inclusion")]
67    TooShort {
68        /// Secret name
69        name: String,
70        /// Actual length of the secret value
71        len: usize,
72    },
73
74    /// Missing salt when secrets require fingerprinting
75    #[error("CUENV_SECRET_SALT required when secrets have cache_key: true")]
76    MissingSalt,
77
78    /// Resolver execution failed
79    #[error("Failed to resolve secret '{name}': {message}")]
80    ResolutionFailed {
81        /// Secret name
82        name: String,
83        /// Error message from the resolver
84        message: String,
85    },
86
87    /// Unsupported resolver type
88    #[error("Unsupported secret resolver: {resolver}")]
89    UnsupportedResolver {
90        /// The resolver type that was requested
91        resolver: String,
92    },
93}
94
95/// Configuration for a secret to resolve
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97pub struct SecretSpec {
98    /// Source reference (env var name, 1Password reference, etc.)
99    pub source: String,
100
101    /// Include secret in cache key via salted HMAC
102    #[serde(default)]
103    pub cache_key: bool,
104}
105
106impl SecretSpec {
107    /// Create a new secret spec
108    #[must_use]
109    pub fn new(source: impl Into<String>) -> Self {
110        Self {
111            source: source.into(),
112            cache_key: false,
113        }
114    }
115
116    /// Create a secret spec that affects cache keys
117    #[must_use]
118    pub fn with_cache_key(source: impl Into<String>) -> Self {
119        Self {
120            source: source.into(),
121            cache_key: true,
122        }
123    }
124}
125
126/// Trait for resolving secrets from various providers.
127///
128/// Implementors must provide:
129/// - [`resolve`](SecretResolver::resolve) - Single secret resolution
130/// - [`provider_name`](SecretResolver::provider_name) - Provider identifier for grouping
131///
132/// The trait provides default implementations for batch operations that can be
133/// overridden for providers with native batch APIs (e.g., AWS `BatchGetSecretValue`).
134#[async_trait]
135pub trait SecretResolver: Send + Sync {
136    /// Resolve a single secret by name and spec.
137    ///
138    /// This is the primary method that must be implemented by all resolvers.
139    async fn resolve(&self, name: &str, spec: &SecretSpec) -> Result<String, SecretError>;
140
141    /// Get the provider name for this resolver.
142    ///
143    /// Used for grouping secrets by provider in batch resolution.
144    /// Examples: `"env"`, `"aws"`, `"vault"`, `"onepassword"`
145    fn provider_name(&self) -> &'static str;
146
147    /// Resolve a single secret returning a secure value.
148    ///
149    /// The returned [`SecureSecret`] will automatically zero its memory on drop.
150    async fn resolve_secure(
151        &self,
152        name: &str,
153        spec: &SecretSpec,
154    ) -> Result<SecureSecret, SecretError> {
155        let value = self.resolve(name, spec).await?;
156        Ok(SecureSecret::new(value))
157    }
158
159    /// Resolve multiple secrets at once (legacy sequential API).
160    ///
161    /// This method is kept for backward compatibility. New code should use
162    /// [`resolve_batch`](SecretResolver::resolve_batch) instead.
163    async fn resolve_all(
164        &self,
165        secrets: &HashMap<String, SecretSpec>,
166    ) -> Result<HashMap<String, String>, SecretError> {
167        let mut result = HashMap::new();
168        for (name, spec) in secrets {
169            let value = self.resolve(name, spec).await?;
170            result.insert(name.clone(), value);
171        }
172        Ok(result)
173    }
174
175    /// Resolve multiple secrets in batch with concurrent execution.
176    ///
177    /// Override this method to implement provider-specific batch APIs
178    /// (e.g., AWS `BatchGetSecretValue`, 1Password `Secrets.ResolveAll`).
179    ///
180    /// The default implementation resolves secrets concurrently using
181    /// `futures::try_join_all`, which is optimal for providers without
182    /// native batch APIs.
183    ///
184    /// # Returns
185    ///
186    /// A map of secret names to [`SecureSecret`] values that will be
187    /// automatically zeroed on drop.
188    async fn resolve_batch(
189        &self,
190        secrets: &HashMap<String, SecretSpec>,
191    ) -> Result<HashMap<String, SecureSecret>, SecretError> {
192        use futures::future::try_join_all;
193
194        let futures: Vec<_> = secrets
195            .iter()
196            .map(|(name, spec)| {
197                let name = name.clone();
198                let spec = spec.clone();
199                async move {
200                    let value = self.resolve_secure(&name, &spec).await?;
201                    Ok::<_, SecretError>((name, value))
202                }
203            })
204            .collect();
205
206        let results = try_join_all(futures).await?;
207        Ok(results.into_iter().collect())
208    }
209
210    /// Check if this resolver supports native batch resolution.
211    ///
212    /// Returns `true` if the provider has a native batch API that is more
213    /// efficient than concurrent single calls.
214    fn supports_native_batch(&self) -> bool {
215        false
216    }
217}