use crate::crypto::EncryptionAlgorithm;
use crate::paths::auths_home;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
use std::sync::RwLock;
static ENCRYPTION_ALGO: Lazy<RwLock<EncryptionAlgorithm>> =
Lazy::new(|| RwLock::new(EncryptionAlgorithm::AesGcm256));
#[allow(clippy::unwrap_used)] pub fn current_algorithm() -> EncryptionAlgorithm {
*ENCRYPTION_ALGO.read().unwrap()
}
#[allow(clippy::unwrap_used)] pub fn set_encryption_algorithm(algo: EncryptionAlgorithm) {
*ENCRYPTION_ALGO.write().unwrap() = algo;
}
#[derive(Debug, Clone, Default)]
pub struct Pkcs11Config {
pub library_path: Option<PathBuf>,
pub slot_id: Option<u64>,
pub token_label: Option<String>,
pub pin: Option<String>,
pub key_label: Option<String>,
}
impl Pkcs11Config {
#[allow(clippy::disallowed_methods)]
pub fn from_env() -> Self {
Self {
library_path: std::env::var("AUTHS_PKCS11_LIBRARY")
.ok()
.map(PathBuf::from),
slot_id: std::env::var("AUTHS_PKCS11_SLOT")
.ok()
.and_then(|s| s.parse().ok()),
token_label: std::env::var("AUTHS_PKCS11_TOKEN_LABEL").ok(),
pin: std::env::var("AUTHS_PKCS11_PIN").ok(),
key_label: std::env::var("AUTHS_PKCS11_KEY_LABEL").ok(),
}
}
}
#[derive(Clone, Default)]
pub struct KeychainConfig {
pub backend: Option<String>,
pub file_path: Option<PathBuf>,
pub passphrase: Option<String>,
}
impl fmt::Debug for KeychainConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KeychainConfig")
.field("backend", &self.backend)
.field("file_path", &self.file_path)
.field(
"passphrase",
&self.passphrase.as_ref().map(|_| "[REDACTED]"),
)
.finish()
}
}
impl KeychainConfig {
#[allow(clippy::disallowed_methods)] pub fn from_env() -> Self {
Self {
backend: std::env::var("AUTHS_KEYCHAIN_BACKEND").ok(),
file_path: std::env::var("AUTHS_KEYCHAIN_FILE").ok().map(PathBuf::from),
passphrase: std::env::var("AUTHS_PASSPHRASE").ok(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct EnvironmentConfig {
pub auths_home: Option<PathBuf>,
pub keychain: KeychainConfig,
pub ssh_agent_socket: Option<PathBuf>,
#[cfg(feature = "keychain-pkcs11")]
pub pkcs11: Option<Pkcs11Config>,
}
impl EnvironmentConfig {
#[allow(clippy::disallowed_methods)] pub fn from_env() -> Self {
Self {
auths_home: std::env::var("AUTHS_HOME")
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from),
keychain: KeychainConfig::from_env(),
ssh_agent_socket: std::env::var("SSH_AUTH_SOCK").ok().map(PathBuf::from),
#[cfg(feature = "keychain-pkcs11")]
pkcs11: {
let cfg = Pkcs11Config::from_env();
if cfg.library_path.is_some() {
Some(cfg)
} else {
None
}
},
}
}
pub fn builder() -> EnvironmentConfigBuilder {
EnvironmentConfigBuilder::default()
}
}
#[derive(Default)]
pub struct EnvironmentConfigBuilder {
auths_home: Option<PathBuf>,
keychain: Option<KeychainConfig>,
ssh_agent_socket: Option<PathBuf>,
#[cfg(feature = "keychain-pkcs11")]
pkcs11: Option<Pkcs11Config>,
}
impl EnvironmentConfigBuilder {
pub fn auths_home(mut self, home: PathBuf) -> Self {
self.auths_home = Some(home);
self
}
pub fn keychain(mut self, keychain: KeychainConfig) -> Self {
self.keychain = Some(keychain);
self
}
pub fn ssh_agent_socket(mut self, path: PathBuf) -> Self {
self.ssh_agent_socket = Some(path);
self
}
#[cfg(feature = "keychain-pkcs11")]
pub fn pkcs11(mut self, config: Pkcs11Config) -> Self {
self.pkcs11 = Some(config);
self
}
pub fn build(self) -> EnvironmentConfig {
EnvironmentConfig {
auths_home: self.auths_home,
keychain: self.keychain.unwrap_or_default(),
ssh_agent_socket: self.ssh_agent_socket,
#[cfg(feature = "keychain-pkcs11")]
pkcs11: self.pkcs11,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PassphraseCachePolicy {
Always,
Session,
#[default]
Duration,
Never,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PassphraseConfig {
#[serde(default)]
pub cache: PassphraseCachePolicy,
pub duration: Option<String>,
#[serde(default = "default_biometric")]
pub biometric: bool,
}
fn default_biometric() -> bool {
cfg!(target_os = "macos")
}
impl Default for PassphraseConfig {
fn default() -> Self {
Self {
cache: PassphraseCachePolicy::Duration,
duration: Some("1h".to_string()),
biometric: default_biometric(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AuthsConfig {
#[serde(default)]
pub passphrase: PassphraseConfig,
}
pub fn load_config(store: &dyn crate::ports::config_store::ConfigStore) -> AuthsConfig {
let home = match auths_home() {
Ok(h) => h,
Err(_) => return AuthsConfig::default(),
};
let path = home.join("config.toml");
match store.read(&path) {
Ok(Some(contents)) => toml::from_str(&contents).unwrap_or_default(),
_ => AuthsConfig::default(),
}
}
pub fn save_config(
config: &AuthsConfig,
store: &dyn crate::ports::config_store::ConfigStore,
) -> Result<(), crate::ports::config_store::ConfigStoreError> {
let home = auths_home().map_err(|e| crate::ports::config_store::ConfigStoreError::Write {
path: PathBuf::from("~/.auths"),
source: std::io::Error::other(e.to_string()),
})?;
let path = home.join("config.toml");
let contents = toml::to_string_pretty(config).map_err(|e| {
crate::ports::config_store::ConfigStoreError::Write {
path: path.clone(),
source: std::io::Error::other(e.to_string()),
}
})?;
store.write(&path, &contents)
}