use rand_core::{OsRng, RngCore};
use zeroize::Zeroizing;
use security_framework::passwords::{
delete_generic_password, generic_password, set_generic_password_options, PasswordOptions,
};
use security_framework::passwords_options::AccessControlOptions;
use crate::signer::{IdentitySigner, RootSecret, SignerError, SignerTier};
pub const SERVICE: &str = "io.styrene.identity";
pub const ACCOUNT: &str = "root-secret";
pub struct KeychainSigner {
service: String,
account: String,
}
impl Default for KeychainSigner {
fn default() -> Self {
Self {
service: SERVICE.into(),
account: ACCOUNT.into(),
}
}
}
impl KeychainSigner {
pub fn new(service: impl Into<String>, account: impl Into<String>) -> Self {
Self {
service: service.into(),
account: account.into(),
}
}
pub fn exists(&self) -> bool {
match generic_password(PasswordOptions::new_generic_password(&self.service, &self.account)) {
Ok(_) => true,
Err(e) => {
let code = e.code();
code == -25308 || code == -25293
}
}
}
pub fn create(&self) -> Result<(), SignerError> {
if self.exists() {
return Err(SignerError::Unavailable(
"Identity already exists in Keychain. Delete it first.".into(),
));
}
let mut secret = Zeroizing::new([0u8; 32]);
OsRng.fill_bytes(&mut *secret);
let mut opts = PasswordOptions::new_generic_password(&self.service, &self.account);
opts.set_access_control_options(
AccessControlOptions::BIOMETRY_CURRENT_SET | AccessControlOptions::OR | AccessControlOptions::DEVICE_PASSCODE,
);
set_generic_password_options(&*secret, opts)
.map_err(|e| SignerError::SigningFailed(format!("Keychain store failed: {e}")))?;
Ok(())
}
pub fn delete(&self) -> Result<(), SignerError> {
delete_generic_password(&self.service, &self.account)
.map_err(|e| SignerError::Unavailable(format!("Keychain delete failed: {e}")))?;
Ok(())
}
}
#[async_trait::async_trait]
impl IdentitySigner for KeychainSigner {
fn tier(&self) -> SignerTier {
SignerTier::DeviceHsm
}
fn label(&self) -> &str {
"Keychain (biometric)"
}
fn is_available(&self) -> bool {
self.exists()
}
async fn root_secret(&self) -> Result<RootSecret, SignerError> {
let data = generic_password(
PasswordOptions::new_generic_password(&self.service, &self.account),
)
.map_err(|e| {
let code = e.code();
if code == -25293 || code == -128 {
SignerError::AuthRequired("Biometric authentication cancelled".into())
} else if code == -25308 {
SignerError::AuthRequired("Biometric authentication required but not available in this context".into())
} else if code == -25300 {
SignerError::KeyNotFound("No identity in Keychain".into())
} else {
SignerError::DecryptionFailed(format!("Keychain read failed: {e}"))
}
})?;
if data.len() != 32 {
return Err(SignerError::DecryptionFailed(format!(
"Invalid root secret length: {} (expected 32)",
data.len()
)));
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&data);
Ok(RootSecret::new(bytes))
}
async fn sign(&self, data: &[u8]) -> Result<Vec<u8>, SignerError> {
let root = self.root_secret().await?;
let deriver = crate::derive::KeyDeriver::new(root.as_bytes());
let seed = Zeroizing::new(deriver.derive(crate::derive::KeyPurpose::Signing));
Ok(crate::pubkey::sign_with_seed(&seed, data).to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_signer_has_correct_service() {
let signer = KeychainSigner::default();
assert_eq!(signer.service, "io.styrene.identity");
assert_eq!(signer.account, "root-secret");
}
#[test]
fn custom_signer() {
let signer = KeychainSigner::new("custom.service", "custom-key");
assert_eq!(signer.service, "custom.service");
assert_eq!(signer.account, "custom-key");
}
}