#[cfg(target_os = "macos")]
use super::super::contract::SecretBackend;
#[cfg(target_os = "macos")]
use anyhow::Result;
#[cfg(target_os = "macos")]
const SERVICE_LABEL: &str = "yana-ai";
#[cfg(target_os = "macos")]
const ITEM_NOT_FOUND_EXIT_CODE: i32 = 44;
#[cfg(target_os = "macos")]
const COMMAND_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(4);
#[cfg(target_os = "macos")]
pub struct Backend;
#[cfg(target_os = "macos")]
impl SecretBackend for Backend {
fn has_entry(&self, key: &str) -> Result<bool> {
use std::process::{Command, Stdio};
use std::time::Instant;
let mut child = Command::new("security")
.args(["find-generic-password", "-s", SERVICE_LABEL, "-a", key])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|error| anyhow::anyhow!("starting security: {error}"))?;
let deadline = Instant::now() + COMMAND_TIMEOUT;
let status = loop {
if let Some(status) = child.try_wait()? {
break status;
}
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
anyhow::bail!(
"security find-generic-password timed out after {}s",
COMMAND_TIMEOUT.as_secs()
);
}
std::thread::sleep(std::time::Duration::from_millis(20));
};
match status.code() {
Some(0) => Ok(true),
Some(ITEM_NOT_FOUND_EXIT_CODE) => Ok(false),
Some(code) => Err(anyhow::anyhow!(
"security find-generic-password exited {code} (neither found nor confirmed absent)"
)),
None => Err(anyhow::anyhow!(
"security find-generic-password terminated by signal"
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "macos")]
#[test]
fn has_entry_reports_false_for_a_key_that_was_never_stored() {
let backend = Backend;
let key = format!("yana-phase11-test-absent-{}", uuid::Uuid::new_v4());
assert!(!backend.has_entry(&key).unwrap());
}
#[cfg(target_os = "macos")]
#[test]
fn has_entry_reports_true_for_a_real_stored_entry_and_never_reads_its_value() {
let key = format!("yana-phase11-test-present-{}", uuid::Uuid::new_v4());
let add = std::process::Command::new("security")
.args([
"add-generic-password",
"-s",
SERVICE_LABEL,
"-a",
&key,
"-w",
"do-not-print-this-value",
])
.status()
.unwrap();
assert!(add.success(), "test setup: failed to seed a Keychain entry");
let backend = Backend;
let result = backend.has_entry(&key);
let _ = std::process::Command::new("security")
.args(["delete-generic-password", "-s", SERVICE_LABEL, "-a", &key])
.status();
assert!(result.unwrap());
}
}