use std::ffi::OsString;
use std::process::Command;
use crate::error::{Error, Result};
pub const PLUGIN_BIN: &str = "age-plugin-se";
pub const PLUGIN_BIN_ENV: &str = "SOPSY_AGE_PLUGIN_SE_BIN";
fn plugin_bin() -> OsString {
std::env::var_os(PLUGIN_BIN_ENV).unwrap_or_else(|| OsString::from(PLUGIN_BIN))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnclaveIdentity {
pub public_key: String,
pub identity: String,
}
pub fn ensure_available() -> Result<()> {
let bin = plugin_bin();
if which::which(&bin).is_ok() {
return Ok(());
}
Err(Error::ToolNotFound(format!(
"{} (install it with `brew install age-plugin-se`)",
bin.to_string_lossy()
)))
}
pub fn generate_identity(access_control: Option<&str>) -> Result<EnclaveIdentity> {
let mut command = Command::new(plugin_bin());
command.arg("keygen");
if let Some(access_control) = access_control {
command.arg(format!("--access-control={access_control}"));
}
let output = command.output()?;
if !output.status.success() {
return Err(Error::ProcessFailed {
tool: PLUGIN_BIN.to_string(),
code: output.status.code().unwrap_or(-1),
message: String::from_utf8_lossy(&output.stderr).trim().to_string(),
});
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
parse_keygen_output(&stdout, &stderr)
}
fn parse_keygen_output(stdout: &str, stderr: &str) -> Result<EnclaveIdentity> {
let public_key = stdout
.lines()
.chain(stderr.lines())
.find_map(extract_public_key)
.ok_or_else(|| {
Error::Validation("could not find a public key in age-plugin-se output".to_string())
})?;
let identity = stdout
.lines()
.map(str::trim)
.find(|line| line.starts_with("AGE-PLUGIN-SE"))
.map(str::to_string)
.ok_or_else(|| {
Error::Validation(
"could not find an AGE-PLUGIN-SE identity in age-plugin-se output".to_string(),
)
})?;
Ok(EnclaveIdentity {
public_key,
identity,
})
}
fn extract_public_key(line: &str) -> Option<String> {
let lower = line.to_ascii_lowercase();
let idx = lower.find("public key:")?;
let rest = line[idx + "public key:".len()..].trim();
let key = rest.split_whitespace().next()?;
if key.starts_with("age1") {
Some(key.to_string())
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enclave_identity_is_constructible() {
let id = EnclaveIdentity {
public_key: "age1se1qexample".into(),
identity: "AGE-PLUGIN-SE-1...".into(),
};
assert!(id.public_key.starts_with("age1se1"));
}
#[test]
fn parses_identity_file_style_stdout() {
let stdout = "# created: 2026-06-27T00:00:00Z\n\
# access control: any biometry or passcode\n\
# public key: age1se1qg8vwwqhztnh3vpt2nf2xwn7famktxlmp0nmkflt\n\
AGE-PLUGIN-SE-1QABCDEF\n";
let id = parse_keygen_output(stdout, "").unwrap();
assert_eq!(
id.public_key,
"age1se1qg8vwwqhztnh3vpt2nf2xwn7famktxlmp0nmkflt"
);
assert_eq!(id.identity, "AGE-PLUGIN-SE-1QABCDEF");
}
#[test]
fn parses_public_key_from_stderr() {
let stdout = "AGE-PLUGIN-SE-1QXYZ\n";
let stderr = "Public key: age1se1qg8vwwqhztnh3\n";
let id = parse_keygen_output(stdout, stderr).unwrap();
assert_eq!(id.public_key, "age1se1qg8vwwqhztnh3");
assert_eq!(id.identity, "AGE-PLUGIN-SE-1QXYZ");
}
#[test]
fn missing_identity_is_an_error() {
let err = parse_keygen_output("# public key: age1se1abc\n", "").unwrap_err();
assert!(matches!(err, Error::Validation(_)));
}
#[test]
fn missing_public_key_is_an_error() {
let err = parse_keygen_output("AGE-PLUGIN-SE-1QXYZ\n", "").unwrap_err();
assert!(matches!(err, Error::Validation(_)));
}
#[test]
fn skips_public_key_lines_without_an_age_token() {
let stdout = "# public key: not-an-age-key\n\
# public key: age1se1qrealkey\n\
AGE-PLUGIN-SE-1QABC\n";
let id = parse_keygen_output(stdout, "").unwrap();
assert_eq!(id.public_key, "age1se1qrealkey");
assert!(extract_public_key("Public key: nope").is_none());
}
}