use anyhow::Result;
#[cfg(not(target_os = "macos"))]
use anyhow::bail;
#[cfg(target_os = "macos")]
fn log_debug(msg: &str) {
if std::env::var("PATINA_LOG").is_ok() {
eprintln!("[DEBUG secrets::keychain] {}", msg);
}
}
#[cfg(target_os = "macos")]
const KEYCHAIN_SERVICE: &str = "patina";
#[cfg(target_os = "macos")]
const KEYCHAIN_ACCOUNT: &str = "Patina Secrets";
#[cfg(target_os = "macos")]
mod platform {
use super::*;
use anyhow::Context;
pub fn store_identity(identity: &str) -> Result<()> {
use security_framework::passwords::set_generic_password;
log_debug("set_generic_password: attempting");
let result = set_generic_password(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, identity.as_bytes());
match &result {
Ok(()) => log_debug("set_generic_password: success"),
Err(e) => log_debug(&format!("set_generic_password: error: {}", e)),
}
result.context("Failed to store identity in Keychain")?;
Ok(())
}
pub fn get_identity() -> Result<String> {
use security_framework::passwords::get_generic_password;
log_debug("get_generic_password: attempting (may trigger Touch ID)");
let result = get_generic_password(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
match &result {
Ok(_) => log_debug("get_generic_password: success"),
Err(e) => log_debug(&format!("get_generic_password: error: {}", e)),
}
let password = result.context(
"Failed to retrieve identity from Keychain. Run: patina secrets --import-key",
)?;
String::from_utf8(password).context("Keychain identity is not valid UTF-8")
}
pub fn delete_identity() -> Result<()> {
use security_framework::passwords::delete_generic_password;
log_debug("delete_generic_password: attempting");
let result = delete_generic_password(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
match &result {
Ok(()) => log_debug("delete_generic_password: success"),
Err(e) => log_debug(&format!("delete_generic_password: error: {}", e)),
}
result.context("Failed to delete identity from Keychain")?;
Ok(())
}
pub fn has_identity() -> bool {
use security_framework::passwords::get_generic_password;
log_debug("has_identity: checking existence (no Touch ID)");
let exists = get_generic_password(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT).is_ok();
log_debug(&format!("has_identity: {}", exists));
exists
}
}
#[cfg(not(target_os = "macos"))]
mod platform {
use super::*;
pub fn store_identity(_identity: &str) -> Result<()> {
bail!(
"Keychain storage is only available on macOS.\n\
On Linux/Windows, set the PATINA_IDENTITY environment variable:\n\
\n\
export PATINA_IDENTITY='AGE-SECRET-KEY-1...'"
)
}
pub fn get_identity() -> Result<String> {
bail!(
"Keychain is only available on macOS.\n\
Set the PATINA_IDENTITY environment variable:\n\
\n\
export PATINA_IDENTITY='AGE-SECRET-KEY-1...'"
)
}
pub fn delete_identity() -> Result<()> {
bail!("Keychain is only available on macOS")
}
pub fn has_identity() -> bool {
false
}
}
pub fn store_identity(identity: &str) -> Result<()> {
platform::store_identity(identity)
}
pub fn get_identity() -> Result<String> {
platform::get_identity()
}
pub fn delete_identity() -> Result<()> {
platform::delete_identity()
}
pub fn has_identity() -> bool {
platform::has_identity()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(target_os = "macos")]
fn test_keychain_constants() {
assert_eq!(KEYCHAIN_SERVICE, "patina");
assert_eq!(KEYCHAIN_ACCOUNT, "Patina Secrets");
}
#[test]
#[cfg(not(target_os = "macos"))]
fn test_non_macos_stubs_return_errors() {
assert!(store_identity("test").is_err());
assert!(get_identity().is_err());
assert!(delete_identity().is_err());
assert!(!has_identity());
}
}