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 — prompt once per
261 /// agent lifetime.
262 Session,
263 /// Store passphrase in OS keychain with a configurable TTL (default).
264 ///
265 /// Chosen as the default so unconfigured users get macOS Keychain +
266 /// Touch ID caching out of the box, matching the UX of other
267 /// keychain-backed developer tools. `impl Default for PassphraseConfig`
268 /// supplies the complementary defaults (`duration = "1h"`,
269 /// `biometric = true` on macOS). Users who explicitly opt into
270 /// `Session` via `~/.auths/config.toml` retain the previous
271 /// prompt-each-time behavior.
272 #[default]
273 Duration,
274 /// Never cache — always prompt.
275 Never,
276}
277
278/// Passphrase section of `~/.auths/config.toml`.
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct PassphraseConfig {
281 /// Caching policy.
282 #[serde(default)]
283 pub cache: PassphraseCachePolicy,
284 /// Duration string (e.g. `"7d"`, `"24h"`, `"30m"`). Only used when `cache = "duration"`.
285 pub duration: Option<String>,
286 /// Use Touch ID (biometric) to protect cached passphrases on macOS.
287 /// Defaults to `true` on macOS, ignored on other platforms.
288 #[serde(default = "default_biometric")]
289 pub biometric: bool,
290}
291
292fn default_biometric() -> bool {
293 cfg!(target_os = "macos")
294}
295
296impl Default for PassphraseConfig {
297 fn default() -> Self {
298 Self {
299 cache: PassphraseCachePolicy::Duration,
300 duration: Some("1h".to_string()),
301 biometric: default_biometric(),
302 }
303 }
304}
305
306/// Top-level `~/.auths/config.toml` structure.
307#[derive(Debug, Clone, Serialize, Deserialize, Default)]
308pub struct AuthsConfig {
309 /// Passphrase caching settings.
310 #[serde(default)]
311 pub passphrase: PassphraseConfig,
312}
313
314/// Loads `~/.auths/config.toml`, returning defaults on any error.
315///
316/// Args:
317/// * `store`: The config store implementation for file I/O.
318///
319/// Usage:
320/// ```ignore
321/// let config = auths_core::config::load_config(&file_store);
322/// match config.passphrase.cache {
323/// PassphraseCachePolicy::Always => { /* ... */ }
324/// _ => {}
325/// }
326/// ```
327pub fn load_config(store: &dyn crate::ports::config_store::ConfigStore) -> AuthsConfig {
328 let home = match auths_home() {
329 Ok(h) => h,
330 Err(_) => return AuthsConfig::default(),
331 };
332 let path = home.join("config.toml");
333 match store.read(&path) {
334 Ok(Some(contents)) => toml::from_str(&contents).unwrap_or_default(),
335 _ => AuthsConfig::default(),
336 }
337}
338
339/// Writes `~/.auths/config.toml`.
340///
341/// Args:
342/// * `config`: The configuration to persist.
343/// * `store`: The config store implementation for file I/O.
344///
345/// Usage:
346/// ```ignore
347/// let mut config = load_config(&file_store);
348/// config.passphrase.cache = PassphraseCachePolicy::Always;
349/// save_config(&config, &file_store)?;
350/// ```
351pub fn save_config(
352 config: &AuthsConfig,
353 store: &dyn crate::ports::config_store::ConfigStore,
354) -> Result<(), crate::ports::config_store::ConfigStoreError> {
355 let home = auths_home().map_err(|e| crate::ports::config_store::ConfigStoreError::Write {
356 path: PathBuf::from("~/.auths"),
357 source: std::io::Error::other(e.to_string()),
358 })?;
359 let path = home.join("config.toml");
360 let contents = toml::to_string_pretty(config).map_err(|e| {
361 crate::ports::config_store::ConfigStoreError::Write {
362 path: path.clone(),
363 source: std::io::Error::other(e.to_string()),
364 }
365 })?;
366 store.write(&path, &contents)
367}