Skip to main content

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}