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}