use std::process::{Command, Stdio};
use std::time::Duration;
use tracing::{debug, error};
use crate::error::SynqroError;
use crate::keychain::KeychainProvider;
const SUBPROCESS_TIMEOUT: Duration = Duration::from_secs(10);
pub struct MacosKeychain {
}
impl MacosKeychain {
pub fn new() -> Result<Self, SynqroError> {
debug!("macOS Keychain backend initialised (security-framework primary)");
Ok(Self {})
}
}
impl KeychainProvider for MacosKeychain {
fn load_secret(&self, service: &str, account: &str) -> Result<Vec<u8>, SynqroError> {
#[cfg(target_os = "macos")]
{
use security_framework::passwords::get_generic_password;
match get_generic_password(service, account) {
Ok(password_bytes) => {
debug!(
service = service,
account = account,
"macOS Keychain: secret loaded via security-framework"
);
return Ok(password_bytes);
}
Err(framework_err) => {
let err_code = framework_err.code();
if err_code == -25300 {
return Err(SynqroError::Keychain(format!(
"macOS Keychain: item not found for service='{}' account='{}'",
service, account
)));
}
error!(
service = service,
account = account,
code = err_code,
"macOS Keychain: security-framework load failed; trying CLI fallback"
);
}
}
}
macos_cli_load(service, account)
}
fn store_secret(&self, service: &str, account: &str, secret: &[u8]) -> Result<(), SynqroError> {
#[cfg(target_os = "macos")]
{
use security_framework::passwords::set_generic_password;
match set_generic_password(service, account, secret) {
Ok(()) => {
debug!(
service = service,
account = account,
"macOS Keychain: secret stored via security-framework"
);
return Ok(());
}
Err(framework_err) => {
error!(
service = service,
account = account,
code = framework_err.code(),
"macOS Keychain: security-framework store failed; trying CLI fallback"
);
}
}
}
macos_cli_store(service, account, secret)
}
fn delete_secret(&self, service: &str, account: &str) -> Result<(), SynqroError> {
#[cfg(target_os = "macos")]
{
use security_framework::passwords::delete_generic_password;
match delete_generic_password(service, account) {
Ok(()) => {
debug!(
service = service,
account = account,
"macOS Keychain: secret deleted via security-framework"
);
return Ok(());
}
Err(framework_err) => {
if framework_err.code() == -25300 {
debug!(
service = service,
account = account,
"macOS Keychain: item not found during delete (already absent)"
);
return Ok(());
}
error!(
service = service,
account = account,
code = framework_err.code(),
"macOS Keychain: security-framework delete failed; trying CLI fallback"
);
}
}
}
macos_cli_delete(service, account)
}
}
fn macos_cli_load(service: &str, account: &str) -> Result<Vec<u8>, SynqroError> {
let output = run_with_timeout(
Command::new("security")
.args([
"find-generic-password",
"-s",
service,
"-a",
account,
"-w", ])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.env_clear(), SUBPROCESS_TIMEOUT,
)?;
if !output.status.success() {
return Err(SynqroError::Keychain(
"security find-generic-password: item not found or keychain locked".to_owned(),
));
}
let mut bytes = output.stdout;
if bytes.last() == Some(&b'\n') {
bytes.pop();
}
Ok(bytes)
}
fn macos_cli_store(service: &str, account: &str, secret: &[u8]) -> Result<(), SynqroError> {
let secret_hex = hex::encode(secret);
let output = run_with_timeout(
Command::new("security")
.args([
"add-generic-password",
"-s",
service,
"-a",
account,
"-w",
&secret_hex, "-U", ])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.env_clear(), SUBPROCESS_TIMEOUT,
)?;
if !output.status.success() {
return Err(SynqroError::Keychain(
"security add-generic-password: store operation failed".to_owned(),
));
}
Ok(())
}
fn macos_cli_delete(service: &str, account: &str) -> Result<(), SynqroError> {
let output = run_with_timeout(
Command::new("security")
.args(["delete-generic-password", "-s", service, "-a", account])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.env_clear(), SUBPROCESS_TIMEOUT,
)?;
match output.status.code() {
Some(0) | Some(44) => Ok(()),
_ => Err(SynqroError::Keychain(
"security delete-generic-password: operation failed".to_owned(),
)),
}
}
fn run_with_timeout(
cmd: &mut Command,
timeout: Duration,
) -> Result<std::process::Output, SynqroError> {
let mut child = cmd.spawn().map_err(|e| {
error!(error = %e, "Failed to spawn security CLI subprocess");
SynqroError::Permission(format!("Failed to spawn security CLI subprocess: {}", e))
})?;
let (tx, rx) = std::sync::mpsc::channel::<Result<std::process::Output, std::io::Error>>();
std::thread::spawn(move || {
let _ = tx.send(child.wait_with_output());
});
match rx.recv_timeout(timeout) {
Ok(Ok(output)) => Ok(output),
Ok(Err(io_err)) => {
error!(error = %io_err, "macOS security CLI subprocess I/O error");
Err(SynqroError::Io(io_err))
}
Err(_timeout) => {
error!("macOS security CLI subprocess exceeded 10-second timeout");
Err(SynqroError::Keychain(
"security CLI subprocess timed out after 10 seconds".to_owned(),
))
}
}
}