use crate::config::EnvironmentConfig;
use crate::error::AgentError;
use crate::paths::auths_home_with_config;
use log::{info, warn};
use std::sync::Arc;
#[cfg(target_os = "ios")]
use super::ios_keychain::IOSKeychain;
#[cfg(target_os = "macos")]
use super::macos_keychain::MacOSKeychain;
#[cfg(all(target_os = "linux", feature = "keychain-linux-secretservice"))]
use super::linux_secret_service::LinuxSecretServiceStorage;
#[cfg(all(target_os = "windows", feature = "keychain-windows"))]
use super::windows_credential::WindowsCredentialStorage;
#[cfg(target_os = "android")]
use super::android_keystore::AndroidKeystoreStorage;
use super::encrypted_file::EncryptedFileStorage;
use super::memory::MemoryKeychainHandle;
use std::borrow::Borrow;
use std::fmt;
use std::ops::Deref;
use zeroize::Zeroizing;
#[cfg_attr(
not(any(
target_os = "macos",
target_os = "ios",
target_os = "android",
all(target_os = "linux", feature = "keychain-linux-secretservice"),
all(target_os = "windows", feature = "keychain-windows"),
test,
)),
allow(dead_code)
)]
const SERVICE_NAME: &str = "dev.auths.agent";
pub use auths_verifier::IdentityDID;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KeyRole {
Primary,
NextRotation,
DelegatedAgent,
}
impl std::fmt::Display for KeyRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KeyRole::Primary => write!(f, "primary"),
KeyRole::NextRotation => write!(f, "next_rotation"),
KeyRole::DelegatedAgent => write!(f, "delegated_agent"),
}
}
}
impl std::str::FromStr for KeyRole {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"primary" => Ok(KeyRole::Primary),
"next_rotation" => Ok(KeyRole::NextRotation),
"delegated_agent" => Ok(KeyRole::DelegatedAgent),
other => Err(format!("unknown key role: {other}")),
}
}
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
)]
#[serde(transparent)]
#[repr(transparent)]
pub struct KeyAlias(String);
impl KeyAlias {
pub fn new<S: Into<String>>(s: S) -> Result<Self, AgentError> {
let s = s.into();
if s.is_empty() {
return Err(AgentError::InvalidInput(
"key alias must not be empty".into(),
));
}
if s.contains('\0') {
return Err(AgentError::InvalidInput(
"key alias must not contain null bytes".into(),
));
}
Ok(Self(s))
}
pub fn new_unchecked<S: Into<String>>(s: S) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl fmt::Display for KeyAlias {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl Deref for KeyAlias {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for KeyAlias {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for KeyAlias {
fn borrow(&self) -> &str {
&self.0
}
}
impl From<KeyAlias> for String {
fn from(alias: KeyAlias) -> String {
alias.0
}
}
impl PartialEq<str> for KeyAlias {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for KeyAlias {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<String> for KeyAlias {
fn eq(&self, other: &String) -> bool {
self.0 == *other
}
}
pub fn migrate_legacy_alias(
keychain: &(dyn KeyStorage + Send + Sync),
old_alias: &KeyAlias,
) -> Result<KeyAlias, AgentError> {
let new_alias = KeyAlias::new_unchecked(format!("{}--0", old_alias));
if keychain.load_key(&new_alias).is_ok() {
return Ok(new_alias);
}
let (did, role, data) = match keychain.load_key(old_alias) {
Ok(v) => v,
Err(_) => {
return Ok(new_alias);
}
};
keychain.store_key(&new_alias, &did, role, &data)?;
let _ = keychain.delete_key(old_alias);
Ok(new_alias)
}
pub trait KeyStorage: Send + Sync {
fn store_key(
&self,
alias: &KeyAlias,
identity_did: &IdentityDID,
role: KeyRole,
encrypted_key_data: &[u8],
) -> Result<(), AgentError>;
fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError>;
fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError>;
fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError>;
fn list_aliases_for_identity(
&self,
identity_did: &IdentityDID,
) -> Result<Vec<KeyAlias>, AgentError>;
fn list_aliases_for_identity_with_role(
&self,
identity_did: &IdentityDID,
role: KeyRole,
) -> Result<Vec<KeyAlias>, AgentError> {
let all = self.list_aliases_for_identity(identity_did)?;
let mut filtered = Vec::new();
for alias in all {
if let Ok((_, r, _)) = self.load_key(&alias)
&& r == role
{
filtered.push(alias);
}
}
Ok(filtered)
}
fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError>;
fn backend_name(&self) -> &'static str;
fn is_hardware_backend(&self) -> bool {
false
}
fn rebind_identity(
&self,
alias: &KeyAlias,
identity_did: &IdentityDID,
) -> Result<(), AgentError> {
if self.is_hardware_backend() {
return Err(AgentError::StorageError(format!(
"hardware backend '{}' must override rebind_identity",
self.backend_name()
)));
}
let (_, role, material) = self.load_key(alias)?;
self.store_key(alias, identity_did, role, &material)
}
fn export_public_key(&self, alias: &KeyAlias) -> Result<Vec<u8>, AgentError> {
let _ = alias;
Err(AgentError::StorageError(format!(
"backend '{}' does not support passphrase-less public key export",
self.backend_name()
)))
}
fn sign_raw(&self, alias: &KeyAlias, message: &[u8]) -> Result<Vec<u8>, AgentError> {
let _ = (alias, message);
Err(AgentError::StorageError(format!(
"backend '{}' does not support passphrase-less signing",
self.backend_name()
)))
}
}
pub fn extract_public_key_bytes(
keychain: &dyn KeyStorage,
alias: &KeyAlias,
passphrase_provider: &dyn crate::signing::PassphraseProvider,
) -> Result<(Vec<u8>, auths_crypto::CurveType), AgentError> {
if keychain.is_hardware_backend() {
let (_, _role, _handle) = keychain.load_key(alias)?;
#[cfg(all(target_os = "macos", feature = "keychain-secure-enclave"))]
{
let pubkey = super::secure_enclave::public_key_from_handle(&_handle)?;
return Ok((pubkey, auths_crypto::CurveType::P256));
}
#[cfg(not(all(target_os = "macos", feature = "keychain-secure-enclave")))]
{
return Err(AgentError::BackendUnavailable {
backend: keychain.backend_name(),
reason: "hardware backend not available on this platform".into(),
});
}
}
use crate::crypto::signer::{decrypt_keypair, load_seed_and_pubkey};
let (_, _role, encrypted) = keychain.load_key(alias)?;
let passphrase = passphrase_provider
.get_passphrase(&format!("Enter passphrase for key '{alias}':"))
.map_err(|e| AgentError::SigningFailed(e.to_string()))?;
let pkcs8 = decrypt_keypair(&encrypted, &passphrase)?;
let (_, pubkey, curve) = load_seed_and_pubkey(&pkcs8)?;
Ok((pubkey.to_vec(), curve))
}
pub fn sign_with_key(
keychain: &dyn KeyStorage,
alias: &KeyAlias,
passphrase_provider: &dyn crate::signing::PassphraseProvider,
message: &[u8],
) -> Result<(Vec<u8>, Vec<u8>, auths_crypto::CurveType), AgentError> {
if keychain.is_hardware_backend() {
let (_, _role, _handle) = keychain.load_key(alias)?;
#[cfg(all(target_os = "macos", feature = "keychain-secure-enclave"))]
{
let sig = super::secure_enclave::sign_with_handle(&_handle, message)?;
let pubkey = super::secure_enclave::public_key_from_handle(&_handle)?;
return Ok((sig, pubkey, auths_crypto::CurveType::P256));
}
#[cfg(not(all(target_os = "macos", feature = "keychain-secure-enclave")))]
{
return Err(AgentError::BackendUnavailable {
backend: keychain.backend_name(),
reason: "hardware signing not available on this platform".into(),
});
}
}
use crate::crypto::signer::{decrypt_keypair, load_seed_and_pubkey};
let (_, _role, encrypted) = keychain.load_key(alias)?;
let passphrase = passphrase_provider
.get_passphrase(&format!("Enter passphrase for key '{}' to sign:", alias))
.map_err(|e| AgentError::SigningFailed(e.to_string()))?;
let pkcs8 = decrypt_keypair(&encrypted, &passphrase)?;
let (seed, pubkey, curve) = load_seed_and_pubkey(&pkcs8)?;
let typed_seed = match curve {
auths_crypto::CurveType::Ed25519 => auths_crypto::TypedSeed::Ed25519(*seed.as_bytes()),
auths_crypto::CurveType::P256 => auths_crypto::TypedSeed::P256(*seed.as_bytes()),
};
let sig = auths_crypto::typed_sign(&typed_seed, message)
.map_err(|e| AgentError::SigningFailed(e.to_string()))?;
Ok((sig, pubkey, curve))
}
pub fn get_platform_keychain_with_config(
config: &EnvironmentConfig,
) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
if let Some(ref backend) = config.keychain.backend {
return get_backend_by_name(backend, config);
}
get_platform_default(config)
}
pub fn get_platform_keychain() -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
get_platform_keychain_with_config(&EnvironmentConfig::from_env())
}
#[allow(unused_variables, unreachable_code)]
fn get_platform_default(
config: &EnvironmentConfig,
) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
#[cfg(target_os = "ios")]
{
return Ok(Box::new(IOSKeychain::new(SERVICE_NAME)));
}
#[cfg(target_os = "macos")]
{
#[cfg(feature = "keychain-secure-enclave")]
{
if super::secure_enclave::is_available()
&& let Ok(home) = auths_home_with_config(config)
{
match super::secure_enclave::SecureEnclaveKeyStorage::new(&home) {
Ok(storage) => {
log::info!("Using Secure Enclave (Touch ID signing)");
return Ok(Box::new(storage));
}
Err(e) => {
log::warn!(
"Secure Enclave available but init failed ({}), using macOS Keychain",
e
);
}
}
}
}
return Ok(Box::new(MacOSKeychain::new(SERVICE_NAME)));
}
#[cfg(all(target_os = "linux", feature = "keychain-linux-secretservice"))]
{
match LinuxSecretServiceStorage::new(SERVICE_NAME) {
Ok(storage) => return Ok(Box::new(storage)),
Err(e) => {
warn!("Secret Service unavailable ({}), trying file fallback", e);
#[cfg(feature = "keychain-file-fallback")]
{
return new_encrypted_file_storage(config).map(|s| {
let b: Box<dyn KeyStorage + Send + Sync> = Box::new(s);
b
});
}
#[cfg(not(feature = "keychain-file-fallback"))]
{
return Err(e);
}
}
}
}
#[cfg(all(target_os = "linux", not(feature = "keychain-linux-secretservice")))]
{
#[cfg(feature = "keychain-file-fallback")]
{
return new_encrypted_file_storage(config).map(|s| {
let b: Box<dyn KeyStorage + Send + Sync> = Box::new(s);
b
});
}
}
#[cfg(all(target_os = "windows", feature = "keychain-windows"))]
{
return Ok(Box::new(WindowsCredentialStorage::new(SERVICE_NAME)?));
}
#[cfg(target_os = "android")]
{
return Ok(Box::new(AndroidKeystoreStorage::new(SERVICE_NAME)?));
}
#[allow(unused_variables)]
let _ = config;
#[allow(unreachable_code)]
{
warn!("Using in-memory keychain (not recommended for production)");
Ok(Box::new(MemoryKeychainHandle))
}
}
fn get_backend_by_name(
name: &str,
config: &EnvironmentConfig,
) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
match name.to_lowercase().as_str() {
"memory" => {
info!("Using in-memory keychain (AUTHS_KEYCHAIN_BACKEND=memory)");
Ok(Box::new(MemoryKeychainHandle))
}
"file" => {
info!("Using encrypted file storage (AUTHS_KEYCHAIN_BACKEND=file)");
let storage = new_encrypted_file_storage(config)?;
Ok(Box::new(storage))
}
#[cfg(feature = "keychain-pkcs11")]
"hsm" | "pkcs11" => {
info!("Using PKCS#11 HSM backend (AUTHS_KEYCHAIN_BACKEND={name})");
let pkcs11_config =
config
.pkcs11
.as_ref()
.ok_or_else(|| AgentError::BackendInitFailed {
backend: "pkcs11",
error: "PKCS#11 configuration required (set AUTHS_PKCS11_LIBRARY)".into(),
})?;
let storage = super::pkcs11::Pkcs11KeyRef::new(pkcs11_config)?;
Ok(Box::new(storage))
}
#[cfg(all(target_os = "macos", feature = "keychain-secure-enclave"))]
"secure-enclave" => {
info!("Using Secure Enclave backend (AUTHS_KEYCHAIN_BACKEND=secure-enclave)");
let home =
auths_home_with_config(config).map_err(|e| AgentError::BackendInitFailed {
backend: "secure-enclave",
error: format!("failed to resolve auths home: {e}"),
})?;
let storage = super::secure_enclave::SecureEnclaveKeyStorage::new(&home)?;
Ok(Box::new(storage))
}
_ => {
warn!(
"Unknown keychain backend '{}', using platform default",
name
);
get_platform_default(config)
}
}
}
fn new_encrypted_file_storage(
config: &EnvironmentConfig,
) -> Result<EncryptedFileStorage, AgentError> {
let storage = if let Some(ref path) = config.keychain.file_path {
EncryptedFileStorage::with_path(path.clone())?
} else {
let home =
auths_home_with_config(config).map_err(|e| AgentError::StorageError(e.to_string()))?;
EncryptedFileStorage::new(&home)?
};
if let Some(ref passphrase) = config.keychain.passphrase {
storage.set_password(Zeroizing::new(passphrase.clone()));
}
Ok(storage)
}
#[cfg(feature = "keychain-pkcs11")]
pub fn get_pkcs11_signer(
config: &EnvironmentConfig,
) -> Result<Option<Box<dyn crate::signing::SecureSigner>>, AgentError> {
let is_pkcs11 = config
.keychain
.backend
.as_deref()
.map(|b| matches!(b.to_lowercase().as_str(), "hsm" | "pkcs11"))
.unwrap_or(false);
if !is_pkcs11 {
return Ok(None);
}
let pkcs11_config = config
.pkcs11
.as_ref()
.ok_or_else(|| AgentError::BackendInitFailed {
backend: "pkcs11",
error: "PKCS#11 configuration required (set AUTHS_PKCS11_LIBRARY)".into(),
})?;
let signer = super::pkcs11::Pkcs11Signer::new(pkcs11_config)?;
Ok(Some(Box::new(signer)))
}
impl KeyStorage for Arc<dyn KeyStorage + Send + Sync> {
fn store_key(
&self,
alias: &KeyAlias,
identity_did: &IdentityDID,
role: KeyRole,
encrypted_key_data: &[u8],
) -> Result<(), AgentError> {
self.as_ref()
.store_key(alias, identity_did, role, encrypted_key_data)
}
fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
self.as_ref().load_key(alias)
}
fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
self.as_ref().delete_key(alias)
}
fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
self.as_ref().list_aliases()
}
fn list_aliases_for_identity(
&self,
identity_did: &IdentityDID,
) -> Result<Vec<KeyAlias>, AgentError> {
self.as_ref().list_aliases_for_identity(identity_did)
}
fn list_aliases_for_identity_with_role(
&self,
identity_did: &IdentityDID,
role: KeyRole,
) -> Result<Vec<KeyAlias>, AgentError> {
self.as_ref()
.list_aliases_for_identity_with_role(identity_did, role)
}
fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
self.as_ref().get_identity_for_alias(alias)
}
fn backend_name(&self) -> &'static str {
self.as_ref().backend_name()
}
fn is_hardware_backend(&self) -> bool {
self.as_ref().is_hardware_backend()
}
}
impl KeyStorage for Box<dyn KeyStorage + Send + Sync> {
fn store_key(
&self,
alias: &KeyAlias,
identity_did: &IdentityDID,
role: KeyRole,
encrypted_key_data: &[u8],
) -> Result<(), AgentError> {
self.as_ref()
.store_key(alias, identity_did, role, encrypted_key_data)
}
fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
self.as_ref().load_key(alias)
}
fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
self.as_ref().delete_key(alias)
}
fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
self.as_ref().list_aliases()
}
fn list_aliases_for_identity(
&self,
identity_did: &IdentityDID,
) -> Result<Vec<KeyAlias>, AgentError> {
self.as_ref().list_aliases_for_identity(identity_did)
}
fn list_aliases_for_identity_with_role(
&self,
identity_did: &IdentityDID,
role: KeyRole,
) -> Result<Vec<KeyAlias>, AgentError> {
self.as_ref()
.list_aliases_for_identity_with_role(identity_did, role)
}
fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
self.as_ref().get_identity_for_alias(alias)
}
fn backend_name(&self) -> &'static str {
self.as_ref().backend_name()
}
fn is_hardware_backend(&self) -> bool {
self.as_ref().is_hardware_backend()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_service_name_constant() {
assert_eq!(SERVICE_NAME, "dev.auths.agent");
}
#[test]
fn test_get_backend_by_name_memory() {
let env = EnvironmentConfig::default();
let backend = get_backend_by_name("memory", &env).unwrap();
assert_eq!(backend.backend_name(), "Memory");
}
#[test]
fn test_get_backend_by_name_case_insensitive() {
let env = EnvironmentConfig::default();
let backend = get_backend_by_name("MEMORY", &env).unwrap();
assert_eq!(backend.backend_name(), "Memory");
}
#[test]
fn test_key_role_serde_roundtrip() {
let roles = [
KeyRole::Primary,
KeyRole::NextRotation,
KeyRole::DelegatedAgent,
];
for role in &roles {
let json = serde_json::to_string(role).unwrap();
let parsed: KeyRole = serde_json::from_str(&json).unwrap();
assert_eq!(*role, parsed);
}
}
#[test]
fn test_key_role_display_and_parse() {
assert_eq!(KeyRole::Primary.to_string(), "primary");
assert_eq!(KeyRole::NextRotation.to_string(), "next_rotation");
assert_eq!(KeyRole::DelegatedAgent.to_string(), "delegated_agent");
assert_eq!("primary".parse::<KeyRole>().unwrap(), KeyRole::Primary);
assert_eq!(
"delegated_agent".parse::<KeyRole>().unwrap(),
KeyRole::DelegatedAgent
);
assert!("unknown".parse::<KeyRole>().is_err());
}
#[test]
fn test_list_aliases_with_role_filter() {
use super::super::memory::IsolatedKeychainHandle;
let keychain = IsolatedKeychainHandle::new();
#[allow(clippy::disallowed_methods)]
let did = IdentityDID::new_unchecked("did:keri:Etest".to_string());
keychain
.store_key(
&KeyAlias::new_unchecked("operator"),
&did,
KeyRole::Primary,
b"key1",
)
.unwrap();
keychain
.store_key(
&KeyAlias::new_unchecked("operator--next-0"),
&did,
KeyRole::NextRotation,
b"key2",
)
.unwrap();
keychain
.store_key(
&KeyAlias::new_unchecked("deploy-agent"),
&did,
KeyRole::DelegatedAgent,
b"key3",
)
.unwrap();
let primary = keychain
.list_aliases_for_identity_with_role(&did, KeyRole::Primary)
.unwrap();
assert_eq!(primary.len(), 1);
assert_eq!(primary[0].as_str(), "operator");
let agents = keychain
.list_aliases_for_identity_with_role(&did, KeyRole::DelegatedAgent)
.unwrap();
assert_eq!(agents.len(), 1);
assert_eq!(agents[0].as_str(), "deploy-agent");
}
}