auths_core/config.rs
1//! Configuration types.
2
3use crate::crypto::EncryptionAlgorithm;
4use crate::paths::auths_home;
5use once_cell::sync::Lazy;
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use std::path::PathBuf;
9use std::sync::RwLock;
10
11/// Globally selected encryption algorithm (defaults to AES-GCM).
12static ENCRYPTION_ALGO: Lazy<RwLock<EncryptionAlgorithm>> =
13 Lazy::new(|| RwLock::new(EncryptionAlgorithm::AesGcm256));
14
15/// Returns the currently selected encryption algorithm.
16#[allow(clippy::unwrap_used)] // RwLock poisoning is fatal by design
17pub fn current_algorithm() -> EncryptionAlgorithm {
18 *ENCRYPTION_ALGO.read().unwrap()
19}
20
21/// Sets the encryption algorithm to use globally.
22#[allow(clippy::unwrap_used)] // RwLock poisoning is fatal by design
23pub fn set_encryption_algorithm(algo: EncryptionAlgorithm) {
24 *ENCRYPTION_ALGO.write().unwrap() = algo;
25}
26
27/// PKCS#11 HSM configuration, sourced from `AUTHS_PKCS11_*` environment variables.
28///
29/// Args:
30/// * `library_path`: Path to the PKCS#11 shared library (e.g. `libsofthsm2.so`).
31/// * `slot_id`: Numeric slot identifier (mutually exclusive with `token_label`).
32/// * `token_label`: Token label for slot lookup (mutually exclusive with `slot_id`).
33/// * `pin`: User PIN for the HSM token.
34/// * `key_label`: PKCS#11 object label for the Ed25519 key.
35///
36/// Usage:
37/// ```ignore
38/// let config = Pkcs11Config::from_env();
39/// ```
40#[derive(Debug, Clone, Default)]
41pub struct Pkcs11Config {
42 /// Path to the PKCS#11 shared library (e.g. `libsofthsm2.so`).
43 pub library_path: Option<PathBuf>,
44 /// Numeric slot identifier; mutually exclusive with `token_label`.
45 pub slot_id: Option<u64>,
46 /// Human-readable token label used to locate the slot.
47 pub token_label: Option<String>,
48 /// User PIN for the token session.
49 pub pin: Option<String>,
50 /// Default key label for signing operations.
51 pub key_label: Option<String>,
52}
53
54impl Pkcs11Config {
55 /// Build a `Pkcs11Config` from `AUTHS_PKCS11_*` environment variables.
56 #[allow(clippy::disallowed_methods)]
57 pub fn from_env() -> Self {
58 Self {
59 library_path: std::env::var("AUTHS_PKCS11_LIBRARY")
60 .ok()
61 .map(PathBuf::from),
62 slot_id: std::env::var("AUTHS_PKCS11_SLOT")
63 .ok()
64 .and_then(|s| s.parse().ok()),
65 token_label: std::env::var("AUTHS_PKCS11_TOKEN_LABEL").ok(),
66 pin: std::env::var("AUTHS_PKCS11_PIN").ok(),
67 key_label: std::env::var("AUTHS_PKCS11_KEY_LABEL").ok(),
68 }
69 }
70}
71
72/// Keychain backend configuration, typically sourced from environment variables.
73///
74/// Use `KeychainConfig::from_env()` at process boundaries (CLI entry point, FFI
75/// call sites) and then thread the value through the call graph.
76///
77/// `Debug` output redacts the `passphrase` field to prevent accidental log leakage.
78#[derive(Clone, Default)]
79pub struct KeychainConfig {
80 /// Override for the keychain backend (`AUTHS_KEYCHAIN_BACKEND`).
81 /// Supported values: `"file"`, `"memory"`.
82 pub backend: Option<String>,
83 /// Override for the encrypted-file storage path (`AUTHS_KEYCHAIN_FILE`).
84 pub file_path: Option<PathBuf>,
85 /// Passphrase for the encrypted-file backend (`AUTHS_PASSPHRASE`).
86 pub passphrase: Option<String>,
87}
88
89impl fmt::Debug for KeychainConfig {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 f.debug_struct("KeychainConfig")
92 .field("backend", &self.backend)
93 .field("file_path", &self.file_path)
94 .field(
95 "passphrase",
96 &self.passphrase.as_ref().map(|_| "[REDACTED]"),
97 )
98 .finish()
99 }
100}
101
102impl KeychainConfig {
103 /// Build a `KeychainConfig` from the process environment.
104 ///
105 /// Reads `AUTHS_KEYCHAIN_BACKEND`, `AUTHS_KEYCHAIN_FILE`, and `AUTHS_PASSPHRASE`.
106 /// Call once at the process/FFI boundary; pass the result into subsystems.
107 ///
108 /// Usage:
109 /// ```ignore
110 /// let config = KeychainConfig::from_env();
111 /// let keychain = get_platform_keychain_with_config(&EnvironmentConfig { keychain: config, ..Default::default() })?;
112 /// ```
113 #[allow(clippy::disallowed_methods)] // Designated env-var reading boundary
114 pub fn from_env() -> Self {
115 Self {
116 backend: std::env::var("AUTHS_KEYCHAIN_BACKEND").ok(),
117 file_path: std::env::var("AUTHS_KEYCHAIN_FILE").ok().map(PathBuf::from),
118 passphrase: std::env::var("AUTHS_PASSPHRASE").ok(),
119 }
120 }
121}
122
123/// Full environment configuration for an Auths process.
124///
125/// Collect all environment-variable inputs at the process boundary (main, FFI entry)
126/// and thread this struct through the call graph. Subsystems accept `&EnvironmentConfig`
127/// instead of reading env vars directly.
128///
129/// Usage:
130/// ```ignore
131/// let env = EnvironmentConfig::from_env();
132/// let home = auths_home_with_config(&env)?;
133/// let keychain = get_platform_keychain_with_config(&env)?;
134/// ```
135#[derive(Debug, Clone, Default)]
136pub struct EnvironmentConfig {
137 /// Override for the Auths home directory (`AUTHS_HOME`).
138 /// `None` falls back to `~/.auths`.
139 pub auths_home: Option<PathBuf>,
140 /// Keychain backend settings.
141 pub keychain: KeychainConfig,
142 /// Path to the SSH agent socket (`SSH_AUTH_SOCK`).
143 pub ssh_agent_socket: Option<PathBuf>,
144 /// PKCS#11 HSM configuration.
145 #[cfg(feature = "keychain-pkcs11")]
146 pub pkcs11: Option<Pkcs11Config>,
147}
148
149impl EnvironmentConfig {
150 /// Build an `EnvironmentConfig` from the process environment.
151 ///
152 /// Reads `AUTHS_HOME`, `AUTHS_KEYCHAIN_BACKEND`, `AUTHS_KEYCHAIN_FILE`,
153 /// `AUTHS_PASSPHRASE`, and `SSH_AUTH_SOCK`.
154 ///
155 /// Usage:
156 /// ```ignore
157 /// let env = EnvironmentConfig::from_env();
158 /// ```
159 #[allow(clippy::disallowed_methods)] // Designated env-var reading boundary
160 pub fn from_env() -> Self {
161 Self {
162 auths_home: std::env::var("AUTHS_HOME")
163 .ok()
164 .filter(|s| !s.is_empty())
165 .map(PathBuf::from),
166 keychain: KeychainConfig::from_env(),
167 ssh_agent_socket: std::env::var("SSH_AUTH_SOCK").ok().map(PathBuf::from),
168 #[cfg(feature = "keychain-pkcs11")]
169 pkcs11: {
170 let cfg = Pkcs11Config::from_env();
171 if cfg.library_path.is_some() {
172 Some(cfg)
173 } else {
174 None
175 }
176 },
177 }
178 }
179
180 /// Returns a builder for constructing test configurations without env vars.
181 ///
182 /// Usage:
183 /// ```ignore
184 /// let env = EnvironmentConfig::builder()
185 /// .auths_home(temp_dir.path().to_path_buf())
186 /// .build();
187 /// ```
188 pub fn builder() -> EnvironmentConfigBuilder {
189 EnvironmentConfigBuilder::default()
190 }
191}
192
193/// Builder for `EnvironmentConfig` — use in tests to avoid env var manipulation.
194///
195/// Usage:
196/// ```ignore
197/// let env = EnvironmentConfig::builder()
198/// .auths_home(PathBuf::from("/tmp/test-auths"))
199/// .build();
200/// ```
201#[derive(Default)]
202pub struct EnvironmentConfigBuilder {
203 auths_home: Option<PathBuf>,
204 keychain: Option<KeychainConfig>,
205 ssh_agent_socket: Option<PathBuf>,
206 #[cfg(feature = "keychain-pkcs11")]
207 pkcs11: Option<Pkcs11Config>,
208}
209
210impl EnvironmentConfigBuilder {
211 /// Set the Auths home directory override.
212 pub fn auths_home(mut self, home: PathBuf) -> Self {
213 self.auths_home = Some(home);
214 self
215 }
216
217 /// Set the keychain configuration.
218 pub fn keychain(mut self, keychain: KeychainConfig) -> Self {
219 self.keychain = Some(keychain);
220 self
221 }
222
223 /// Set the SSH agent socket path.
224 pub fn ssh_agent_socket(mut self, path: PathBuf) -> Self {
225 self.ssh_agent_socket = Some(path);
226 self
227 }
228
229 /// Set the PKCS#11 configuration.
230 #[cfg(feature = "keychain-pkcs11")]
231 pub fn pkcs11(mut self, config: Pkcs11Config) -> Self {
232 self.pkcs11 = Some(config);
233 self
234 }
235
236 /// Consume the builder and produce an `EnvironmentConfig`.
237 pub fn build(self) -> EnvironmentConfig {
238 EnvironmentConfig {
239 auths_home: self.auths_home,
240 keychain: self.keychain.unwrap_or_default(),
241 ssh_agent_socket: self.ssh_agent_socket,
242 #[cfg(feature = "keychain-pkcs11")]
243 pkcs11: self.pkcs11,
244 }
245 }
246}
247
248/// Passphrase caching policy.
249///
250/// Controls how `auths-sign` caches the passphrase between invocations:
251/// - `always`: Store in OS keychain permanently until explicitly cleared.
252/// - `session`: Rely on the in-memory agent (Tier 1/2) — prompt once per agent lifetime.
253/// - `duration`: Store in OS keychain with a TTL (see [`PassphraseConfig::duration`]).
254/// - `never`: Always prompt interactively.
255#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(rename_all = "lowercase")]
257pub enum PassphraseCachePolicy {
258 /// Store passphrase in OS keychain permanently.
259 Always,
260 /// Cache passphrase in the running agent's memory (default).
261 #[default]
262 Session,
263 /// Store passphrase in OS keychain with a configurable TTL.
264 Duration,
265 /// Never cache — always prompt.
266 Never,
267}
268
269/// Passphrase section of `~/.auths/config.toml`.
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct PassphraseConfig {
272 /// Caching policy.
273 #[serde(default)]
274 pub cache: PassphraseCachePolicy,
275 /// Duration string (e.g. `"7d"`, `"24h"`, `"30m"`). Only used when `cache = "duration"`.
276 pub duration: Option<String>,
277 /// Use Touch ID (biometric) to protect cached passphrases on macOS.
278 /// Defaults to `true` on macOS, ignored on other platforms.
279 #[serde(default = "default_biometric")]
280 pub biometric: bool,
281}
282
283fn default_biometric() -> bool {
284 cfg!(target_os = "macos")
285}
286
287impl Default for PassphraseConfig {
288 fn default() -> Self {
289 Self {
290 cache: PassphraseCachePolicy::Duration,
291 duration: Some("1h".to_string()),
292 biometric: default_biometric(),
293 }
294 }
295}
296
297/// Top-level `~/.auths/config.toml` structure.
298#[derive(Debug, Clone, Serialize, Deserialize, Default)]
299pub struct AuthsConfig {
300 /// Passphrase caching settings.
301 #[serde(default)]
302 pub passphrase: PassphraseConfig,
303}
304
305/// Loads `~/.auths/config.toml`, returning defaults on any error.
306///
307/// Args:
308/// * `store`: The config store implementation for file I/O.
309///
310/// Usage:
311/// ```ignore
312/// let config = auths_core::config::load_config(&file_store);
313/// match config.passphrase.cache {
314/// PassphraseCachePolicy::Always => { /* ... */ }
315/// _ => {}
316/// }
317/// ```
318pub fn load_config(store: &dyn crate::ports::config_store::ConfigStore) -> AuthsConfig {
319 let home = match auths_home() {
320 Ok(h) => h,
321 Err(_) => return AuthsConfig::default(),
322 };
323 let path = home.join("config.toml");
324 match store.read(&path) {
325 Ok(Some(contents)) => toml::from_str(&contents).unwrap_or_default(),
326 _ => AuthsConfig::default(),
327 }
328}
329
330/// Writes `~/.auths/config.toml`.
331///
332/// Args:
333/// * `config`: The configuration to persist.
334/// * `store`: The config store implementation for file I/O.
335///
336/// Usage:
337/// ```ignore
338/// let mut config = load_config(&file_store);
339/// config.passphrase.cache = PassphraseCachePolicy::Always;
340/// save_config(&config, &file_store)?;
341/// ```
342pub fn save_config(
343 config: &AuthsConfig,
344 store: &dyn crate::ports::config_store::ConfigStore,
345) -> Result<(), crate::ports::config_store::ConfigStoreError> {
346 let home = auths_home().map_err(|e| crate::ports::config_store::ConfigStoreError::Write {
347 path: PathBuf::from("~/.auths"),
348 source: std::io::Error::other(e.to_string()),
349 })?;
350 let path = home.join("config.toml");
351 let contents = toml::to_string_pretty(config).map_err(|e| {
352 crate::ports::config_store::ConfigStoreError::Write {
353 path: path.clone(),
354 source: std::io::Error::other(e.to_string()),
355 }
356 })?;
357 store.write(&path, &contents)
358}