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/// Keychain backend configuration, typically sourced from environment variables.
28///
29/// Use `KeychainConfig::from_env()` at process boundaries (CLI entry point, FFI
30/// call sites) and then thread the value through the call graph.
31///
32/// `Debug` output redacts the `passphrase` field to prevent accidental log leakage.
33#[derive(Clone, Default)]
34pub struct KeychainConfig {
35    /// Override for the keychain backend (`AUTHS_KEYCHAIN_BACKEND`).
36    /// Supported values: `"file"`, `"memory"`.
37    pub backend: Option<String>,
38    /// Override for the encrypted-file storage path (`AUTHS_KEYCHAIN_FILE`).
39    pub file_path: Option<PathBuf>,
40    /// Passphrase for the encrypted-file backend (`AUTHS_PASSPHRASE`).
41    pub passphrase: Option<String>,
42}
43
44impl fmt::Debug for KeychainConfig {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.debug_struct("KeychainConfig")
47            .field("backend", &self.backend)
48            .field("file_path", &self.file_path)
49            .field(
50                "passphrase",
51                &self.passphrase.as_ref().map(|_| "[REDACTED]"),
52            )
53            .finish()
54    }
55}
56
57impl KeychainConfig {
58    /// Build a `KeychainConfig` from the process environment.
59    ///
60    /// Reads `AUTHS_KEYCHAIN_BACKEND`, `AUTHS_KEYCHAIN_FILE`, and `AUTHS_PASSPHRASE`.
61    /// Call once at the process/FFI boundary; pass the result into subsystems.
62    ///
63    /// Usage:
64    /// ```ignore
65    /// let config = KeychainConfig::from_env();
66    /// let keychain = get_platform_keychain_with_config(&EnvironmentConfig { keychain: config, ..Default::default() })?;
67    /// ```
68    pub fn from_env() -> Self {
69        Self {
70            backend: std::env::var("AUTHS_KEYCHAIN_BACKEND").ok(),
71            file_path: std::env::var("AUTHS_KEYCHAIN_FILE").ok().map(PathBuf::from),
72            passphrase: std::env::var("AUTHS_PASSPHRASE").ok(),
73        }
74    }
75}
76
77/// Full environment configuration for an Auths process.
78///
79/// Collect all environment-variable inputs at the process boundary (main, FFI entry)
80/// and thread this struct through the call graph. Subsystems accept `&EnvironmentConfig`
81/// instead of reading env vars directly.
82///
83/// Usage:
84/// ```ignore
85/// let env = EnvironmentConfig::from_env();
86/// let home = auths_home_with_config(&env)?;
87/// let keychain = get_platform_keychain_with_config(&env)?;
88/// ```
89#[derive(Debug, Clone, Default)]
90pub struct EnvironmentConfig {
91    /// Override for the Auths home directory (`AUTHS_HOME`).
92    /// `None` falls back to `~/.auths`.
93    pub auths_home: Option<PathBuf>,
94    /// Keychain backend settings.
95    pub keychain: KeychainConfig,
96    /// Path to the SSH agent socket (`SSH_AUTH_SOCK`).
97    pub ssh_agent_socket: Option<PathBuf>,
98}
99
100impl EnvironmentConfig {
101    /// Build an `EnvironmentConfig` from the process environment.
102    ///
103    /// Reads `AUTHS_HOME`, `AUTHS_KEYCHAIN_BACKEND`, `AUTHS_KEYCHAIN_FILE`,
104    /// `AUTHS_PASSPHRASE`, and `SSH_AUTH_SOCK`.
105    ///
106    /// Usage:
107    /// ```ignore
108    /// let env = EnvironmentConfig::from_env();
109    /// ```
110    pub fn from_env() -> Self {
111        Self {
112            auths_home: std::env::var("AUTHS_HOME")
113                .ok()
114                .filter(|s| !s.is_empty())
115                .map(PathBuf::from),
116            keychain: KeychainConfig::from_env(),
117            ssh_agent_socket: std::env::var("SSH_AUTH_SOCK").ok().map(PathBuf::from),
118        }
119    }
120
121    /// Returns a builder for constructing test configurations without env vars.
122    ///
123    /// Usage:
124    /// ```ignore
125    /// let env = EnvironmentConfig::builder()
126    ///     .auths_home(temp_dir.path().to_path_buf())
127    ///     .build();
128    /// ```
129    pub fn builder() -> EnvironmentConfigBuilder {
130        EnvironmentConfigBuilder::default()
131    }
132}
133
134/// Builder for `EnvironmentConfig` — use in tests to avoid env var manipulation.
135///
136/// Usage:
137/// ```ignore
138/// let env = EnvironmentConfig::builder()
139///     .auths_home(PathBuf::from("/tmp/test-auths"))
140///     .build();
141/// ```
142#[derive(Default)]
143pub struct EnvironmentConfigBuilder {
144    auths_home: Option<PathBuf>,
145    keychain: Option<KeychainConfig>,
146    ssh_agent_socket: Option<PathBuf>,
147}
148
149impl EnvironmentConfigBuilder {
150    /// Set the Auths home directory override.
151    pub fn auths_home(mut self, home: PathBuf) -> Self {
152        self.auths_home = Some(home);
153        self
154    }
155
156    /// Set the keychain configuration.
157    pub fn keychain(mut self, keychain: KeychainConfig) -> Self {
158        self.keychain = Some(keychain);
159        self
160    }
161
162    /// Set the SSH agent socket path.
163    pub fn ssh_agent_socket(mut self, path: PathBuf) -> Self {
164        self.ssh_agent_socket = Some(path);
165        self
166    }
167
168    /// Consume the builder and produce an `EnvironmentConfig`.
169    pub fn build(self) -> EnvironmentConfig {
170        EnvironmentConfig {
171            auths_home: self.auths_home,
172            keychain: self.keychain.unwrap_or_default(),
173            ssh_agent_socket: self.ssh_agent_socket,
174        }
175    }
176}
177
178/// Passphrase caching policy.
179///
180/// Controls how `auths-sign` caches the passphrase between invocations:
181/// - `always`: Store in OS keychain permanently until explicitly cleared.
182/// - `session`: Rely on the in-memory agent (Tier 1/2) — prompt once per agent lifetime.
183/// - `duration`: Store in OS keychain with a TTL (see [`PassphraseConfig::duration`]).
184/// - `never`: Always prompt interactively.
185#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
186#[serde(rename_all = "lowercase")]
187pub enum PassphraseCachePolicy {
188    /// Store passphrase in OS keychain permanently.
189    Always,
190    /// Cache passphrase in the running agent's memory (default).
191    #[default]
192    Session,
193    /// Store passphrase in OS keychain with a configurable TTL.
194    Duration,
195    /// Never cache — always prompt.
196    Never,
197}
198
199/// Passphrase section of `~/.auths/config.toml`.
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct PassphraseConfig {
202    /// Caching policy.
203    #[serde(default)]
204    pub cache: PassphraseCachePolicy,
205    /// Duration string (e.g. `"7d"`, `"24h"`, `"30m"`). Only used when `cache = "duration"`.
206    pub duration: Option<String>,
207    /// Use Touch ID (biometric) to protect cached passphrases on macOS.
208    /// Defaults to `true` on macOS, ignored on other platforms.
209    #[serde(default = "default_biometric")]
210    pub biometric: bool,
211}
212
213fn default_biometric() -> bool {
214    cfg!(target_os = "macos")
215}
216
217impl Default for PassphraseConfig {
218    fn default() -> Self {
219        Self {
220            cache: PassphraseCachePolicy::Duration,
221            duration: Some("1h".to_string()),
222            biometric: default_biometric(),
223        }
224    }
225}
226
227/// Top-level `~/.auths/config.toml` structure.
228#[derive(Debug, Clone, Serialize, Deserialize, Default)]
229pub struct AuthsConfig {
230    /// Passphrase caching settings.
231    #[serde(default)]
232    pub passphrase: PassphraseConfig,
233}
234
235/// Loads `~/.auths/config.toml`, returning defaults on any error.
236///
237/// Usage:
238/// ```ignore
239/// let config = auths_core::config::load_config();
240/// match config.passphrase.cache {
241///     PassphraseCachePolicy::Always => { /* ... */ }
242///     _ => {}
243/// }
244/// ```
245pub fn load_config() -> AuthsConfig {
246    let home = match auths_home() {
247        Ok(h) => h,
248        Err(_) => return AuthsConfig::default(),
249    };
250    let path = home.join("config.toml");
251    match std::fs::read_to_string(&path) {
252        Ok(contents) => toml::from_str(&contents).unwrap_or_default(),
253        Err(_) => AuthsConfig::default(),
254    }
255}
256
257/// Writes `~/.auths/config.toml`.
258///
259/// Args:
260/// * `config`: The configuration to persist.
261///
262/// Usage:
263/// ```ignore
264/// let mut config = load_config();
265/// config.passphrase.cache = PassphraseCachePolicy::Always;
266/// save_config(&config)?;
267/// ```
268pub fn save_config(config: &AuthsConfig) -> Result<(), std::io::Error> {
269    let home = auths_home().map_err(|e| std::io::Error::other(e.to_string()))?;
270    std::fs::create_dir_all(&home)?;
271    let path = home.join("config.toml");
272    let contents =
273        toml::to_string_pretty(config).map_err(|e| std::io::Error::other(e.to_string()))?;
274    std::fs::write(&path, contents)
275}