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}