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 (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}