use std::path::PathBuf;
use crate::signer::SignerTier;
#[derive(Debug, Clone)]
pub struct DiscoveredIdentity {
pub path: PathBuf,
pub tier: SignerTier,
pub label: String,
}
impl DiscoveredIdentity {
pub fn is_hash_only(&self) -> bool {
self.tier == SignerTier::CredentialManager && self.label.starts_with("env:STYRENE_IDENTITY_HASH")
}
}
pub fn discover() -> Option<DiscoveredIdentity> {
#[cfg(feature = "keychain")]
{
let ks = crate::keychain_signer::KeychainSigner::default();
if ks.exists() {
return Some(DiscoveredIdentity {
path: PathBuf::from("(Keychain)"),
tier: SignerTier::DeviceHsm,
label: "Keychain (biometric)".to_string(),
});
}
}
if let Some(home) = home_dir() {
let default_path = home.join(".config").join("styrene").join("identity.key");
if default_path.is_file() {
return Some(DiscoveredIdentity {
path: default_path,
tier: SignerTier::EncryptedFile,
label: "~/.config/styrene/identity.key".to_string(),
});
}
}
if let Ok(custom_path) = std::env::var("STYRENE_IDENTITY_PATH") {
let path = PathBuf::from(&custom_path);
if path.is_file() {
return Some(DiscoveredIdentity {
path,
tier: SignerTier::EncryptedFile,
label: format!("env:STYRENE_IDENTITY_PATH={custom_path}"),
});
}
}
if let Ok(hash) = std::env::var("STYRENE_IDENTITY_HASH") {
if !hash.is_empty() {
return Some(DiscoveredIdentity {
path: PathBuf::from(format!("hash:{hash}")),
tier: SignerTier::CredentialManager,
label: format!("env:STYRENE_IDENTITY_HASH={hash}"),
});
}
}
None
}
fn home_dir() -> Option<PathBuf> {
std::env::var("HOME")
.ok()
.map(PathBuf::from)
.filter(|p| p.is_absolute())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn with_clean_env<F: FnOnce(&std::path::Path)>(f: F) {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
let tmp = tempfile::tempdir().expect("tempdir");
let orig_home = std::env::var("HOME").ok();
let orig_path = std::env::var("STYRENE_IDENTITY_PATH").ok();
let orig_hash = std::env::var("STYRENE_IDENTITY_HASH").ok();
std::env::set_var("HOME", tmp.path());
std::env::remove_var("STYRENE_IDENTITY_PATH");
std::env::remove_var("STYRENE_IDENTITY_HASH");
f(tmp.path());
match orig_home {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
match orig_path {
Some(v) => std::env::set_var("STYRENE_IDENTITY_PATH", v),
None => std::env::remove_var("STYRENE_IDENTITY_PATH"),
}
match orig_hash {
Some(v) => std::env::set_var("STYRENE_IDENTITY_HASH", v),
None => std::env::remove_var("STYRENE_IDENTITY_HASH"),
}
}
#[test]
fn discover_returns_none_when_nothing_configured() {
with_clean_env(|_tmp| {
assert!(discover().is_none(), "should return None with no identity");
});
}
#[test]
fn discover_finds_default_path() {
with_clean_env(|tmp| {
let key_dir = tmp.join(".config").join("styrene");
fs::create_dir_all(&key_dir).unwrap();
fs::write(key_dir.join("identity.key"), b"fake-key-data").unwrap();
let result = discover().expect("should find default identity file");
assert_eq!(result.tier, SignerTier::EncryptedFile);
assert!(result.path.ends_with("identity.key"));
assert!(!result.is_hash_only());
});
}
#[test]
fn discover_env_path_overrides_when_no_default() {
with_clean_env(|tmp| {
let custom_file = tmp.join("custom.key");
fs::write(&custom_file, b"fake-key-data").unwrap();
std::env::set_var("STYRENE_IDENTITY_PATH", &custom_file);
let result = discover().expect("should find env var identity");
assert_eq!(result.tier, SignerTier::EncryptedFile);
assert_eq!(result.path, custom_file);
assert!(!result.is_hash_only());
});
}
#[test]
fn discover_hash_only_from_env() {
with_clean_env(|_tmp| {
std::env::set_var("STYRENE_IDENTITY_HASH", "abcdef1234567890abcdef1234567890");
let result = discover().expect("should find hash-only identity");
assert!(result.is_hash_only(), "should be hash-only");
assert_eq!(
result.label,
"env:STYRENE_IDENTITY_HASH=abcdef1234567890abcdef1234567890"
);
});
}
#[test]
fn discover_default_path_takes_priority_over_env() {
with_clean_env(|tmp| {
let key_dir = tmp.join(".config").join("styrene");
fs::create_dir_all(&key_dir).unwrap();
fs::write(key_dir.join("identity.key"), b"fake-key-data").unwrap();
std::env::set_var("STYRENE_IDENTITY_HASH", "somehash");
let result = discover().expect("should find identity");
assert_eq!(result.tier, SignerTier::EncryptedFile);
assert!(result.path.ends_with("identity.key"), "default path should win");
});
}
}