use crate::crypto::signer::decrypt_keypair;
use crate::error::AgentError;
use crate::storage::keychain::{IdentityDID, KeyAlias, KeyStorage};
use crate::config::PassphraseCachePolicy;
use crate::storage::passphrase_cache::PassphraseCache;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use zeroize::Zeroizing;
type PassphraseCallback = dyn Fn(&str) -> Result<Zeroizing<String>, AgentError> + Send + Sync;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DidResolverError {
#[error("Unsupported DID method: {0}")]
UnsupportedMethod(String),
#[error("Invalid did:key format: {0}")]
InvalidDidKey(String),
#[error("Invalid did:key format: {0}")]
InvalidDidKeyFormat(String),
#[error("did:key decoding failed: {0}")]
DidKeyDecodingFailed(String),
#[error("Invalid did:key multicodec prefix")]
InvalidDidKeyMulticodec,
#[error("Resolution error: {0}")]
Resolution(String),
#[error("Repository error: {0}")]
Repository(String),
}
#[derive(Debug, Clone)]
pub enum ResolvedDid {
Key {
did: String,
public_key_bytes: Vec<u8>,
},
Keri {
did: String,
public_key_bytes: Vec<u8>,
curve: auths_crypto::CurveType,
sequence: u128,
can_rotate: bool,
},
}
impl ResolvedDid {
pub fn did(&self) -> &str {
match self {
ResolvedDid::Key { did, .. } | ResolvedDid::Keri { did, .. } => did,
}
}
pub fn public_key_bytes(&self) -> &[u8] {
match self {
ResolvedDid::Key {
public_key_bytes, ..
}
| ResolvedDid::Keri {
public_key_bytes, ..
} => public_key_bytes,
}
}
pub fn curve(&self) -> auths_crypto::CurveType {
match self {
ResolvedDid::Key { did, .. } => auths_crypto::did_key_decode(did)
.map(|d| d.curve())
.unwrap_or_default(),
ResolvedDid::Keri { curve, .. } => *curve,
}
}
pub fn is_key(&self) -> bool {
matches!(self, ResolvedDid::Key { .. })
}
pub fn is_keri(&self) -> bool {
matches!(self, ResolvedDid::Keri { .. })
}
}
pub trait DidResolver: Send + Sync {
fn resolve(&self, did: &str) -> Result<ResolvedDid, DidResolverError>;
}
pub trait PassphraseProvider: Send + Sync {
fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError>;
fn on_incorrect_passphrase(&self, _prompt_message: &str) {}
}
pub trait SecureSigner: Send + Sync {
fn sign_with_alias(
&self,
alias: &KeyAlias,
passphrase_provider: &dyn PassphraseProvider,
message: &[u8],
) -> Result<Vec<u8>, AgentError>;
fn sign_for_identity(
&self,
identity_did: &IdentityDID,
passphrase_provider: &dyn PassphraseProvider,
message: &[u8],
) -> Result<Vec<u8>, AgentError>;
}
pub struct StorageSigner<S: KeyStorage> {
storage: S,
}
impl<S: KeyStorage> StorageSigner<S> {
pub fn new(storage: S) -> Self {
Self { storage }
}
pub fn inner(&self) -> &S {
&self.storage
}
}
impl<S: KeyStorage + Send + Sync + 'static> SecureSigner for StorageSigner<S> {
fn sign_with_alias(
&self,
alias: &KeyAlias,
passphrase_provider: &dyn PassphraseProvider,
message: &[u8],
) -> Result<Vec<u8>, AgentError> {
if self.storage.is_hardware_backend() {
#[cfg(all(target_os = "macos", feature = "keychain-secure-enclave"))]
{
let (_identity_did, _role, handle) = self.storage.load_key(alias)?;
return crate::storage::secure_enclave::sign_with_handle(&handle, message);
}
#[cfg(not(all(target_os = "macos", feature = "keychain-secure-enclave")))]
{
return Err(AgentError::BackendUnavailable {
backend: self.storage.backend_name(),
reason: "hardware signing not available on this platform".into(),
});
}
}
let (_identity_did, _role, encrypted_data) = self.storage.load_key(alias)?;
const MAX_ATTEMPTS: u8 = 3;
let mut attempt = 0u8;
let key_bytes = loop {
let prompt = if attempt == 0 {
format!("Enter passphrase for key '{}' to sign:", alias)
} else {
format!(
"Incorrect passphrase, try again ({}/{}):",
attempt + 1,
MAX_ATTEMPTS
)
};
let passphrase = passphrase_provider.get_passphrase(&prompt)?;
match decrypt_keypair(&encrypted_data, &passphrase) {
Ok(kb) => break kb,
Err(AgentError::IncorrectPassphrase) if attempt + 1 < MAX_ATTEMPTS => {
passphrase_provider.on_incorrect_passphrase(&prompt);
attempt += 1;
}
Err(e) => return Err(e),
}
};
let parsed = auths_crypto::parse_key_material(&key_bytes)
.map_err(|e| AgentError::KeyDeserializationError(e.to_string()))?;
auths_crypto::typed_sign(&parsed.seed, message)
.map_err(|e| AgentError::CryptoError(format!("signing failed: {}", e)))
}
fn sign_for_identity(
&self,
identity_did: &IdentityDID,
passphrase_provider: &dyn PassphraseProvider,
message: &[u8],
) -> Result<Vec<u8>, AgentError> {
let aliases = self.storage.list_aliases_for_identity(identity_did)?;
let alias = aliases.first().ok_or(AgentError::KeyNotFound)?;
self.sign_with_alias(alias, passphrase_provider, message)
}
}
pub struct CallbackPassphraseProvider {
callback: Box<PassphraseCallback>,
}
impl CallbackPassphraseProvider {
pub fn new<F>(callback: F) -> Self
where
F: Fn(&str) -> Result<Zeroizing<String>, AgentError> + Send + Sync + 'static,
{
Self {
callback: Box::new(callback),
}
}
}
impl PassphraseProvider for CallbackPassphraseProvider {
fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
(self.callback)(prompt_message)
}
}
pub struct CachedPassphraseProvider {
inner: Arc<dyn PassphraseProvider + Send + Sync>,
cache: Mutex<HashMap<String, (Zeroizing<String>, Instant)>>,
ttl: Duration,
}
impl CachedPassphraseProvider {
pub fn new(inner: Arc<dyn PassphraseProvider + Send + Sync>, ttl: Duration) -> Self {
Self {
inner,
cache: Mutex::new(HashMap::new()),
ttl,
}
}
pub fn unlock(&self, passphrase: &str) {
let mut cache = self.cache.lock().unwrap_or_else(|e| e.into_inner());
cache.insert(
String::new(),
(Zeroizing::new(passphrase.to_string()), Instant::now()),
);
}
pub fn remaining_ttl(&self) -> Option<Duration> {
let cache = self.cache.lock().unwrap_or_else(|e| e.into_inner());
cache.values().next().and_then(|(_, cached_at)| {
let elapsed = cached_at.elapsed();
if elapsed < self.ttl {
Some(self.ttl - elapsed)
} else {
None
}
})
}
pub fn clear_cache(&self) {
self.cache.lock().unwrap_or_else(|e| e.into_inner()).clear();
}
}
impl PassphraseProvider for CachedPassphraseProvider {
fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
let mut cache = self
.cache
.lock()
.map_err(|e| AgentError::MutexError(e.to_string()))?;
if let Some((passphrase, cached_at)) = cache.get(prompt_message) {
if cached_at.elapsed() < self.ttl {
return Ok(passphrase.clone());
}
cache.remove(prompt_message);
}
drop(cache); let passphrase = self.inner.get_passphrase(prompt_message)?;
let mut cache = self
.cache
.lock()
.map_err(|e| AgentError::MutexError(e.to_string()))?;
cache.insert(
prompt_message.to_string(),
(passphrase.clone(), Instant::now()),
);
Ok(passphrase)
}
fn on_incorrect_passphrase(&self, prompt_message: &str) {
self.cache
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(prompt_message);
}
}
pub struct KeychainPassphraseProvider {
inner: Arc<dyn PassphraseProvider + Send + Sync>,
cache: Box<dyn PassphraseCache>,
alias: String,
policy: PassphraseCachePolicy,
ttl_secs: Option<i64>,
}
impl KeychainPassphraseProvider {
pub fn new(
inner: Arc<dyn PassphraseProvider + Send + Sync>,
cache: Box<dyn PassphraseCache>,
alias: String,
policy: PassphraseCachePolicy,
ttl_secs: Option<i64>,
) -> Self {
Self {
inner,
cache,
alias,
policy,
ttl_secs,
}
}
#[allow(clippy::disallowed_methods)] fn is_expired(&self, stored_at_unix: i64) -> bool {
match self.policy {
PassphraseCachePolicy::Always => false,
PassphraseCachePolicy::Never => true,
PassphraseCachePolicy::Session => true,
PassphraseCachePolicy::Duration => {
let ttl = self.ttl_secs.unwrap_or(3600);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
now - stored_at_unix > ttl
}
}
}
}
impl PassphraseProvider for KeychainPassphraseProvider {
#[allow(clippy::disallowed_methods)] fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
if self.policy != PassphraseCachePolicy::Never
&& let Ok(Some((passphrase, stored_at))) = self.cache.load(&self.alias)
{
if !self.is_expired(stored_at) {
return Ok(passphrase);
}
let _ = self.cache.delete(&self.alias);
}
let passphrase = self.inner.get_passphrase(prompt_message)?;
if self.policy != PassphraseCachePolicy::Never
&& self.policy != PassphraseCachePolicy::Session
{
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let _ = self.cache.store(&self.alias, &passphrase, now);
}
Ok(passphrase)
}
fn on_incorrect_passphrase(&self, prompt_message: &str) {
let _ = self.cache.delete(&self.alias);
self.inner.on_incorrect_passphrase(prompt_message);
}
}
pub struct PrefilledPassphraseProvider {
passphrase: Zeroizing<String>,
}
impl PrefilledPassphraseProvider {
pub fn new(passphrase: &str) -> Self {
Self {
passphrase: Zeroizing::new(passphrase.to_string()),
}
}
}
impl PassphraseProvider for PrefilledPassphraseProvider {
fn get_passphrase(&self, _prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
Ok(self.passphrase.clone())
}
}
pub struct UnifiedPassphraseProvider {
inner: Arc<dyn PassphraseProvider + Send + Sync>,
cached: Mutex<Option<Zeroizing<String>>>,
}
impl UnifiedPassphraseProvider {
pub fn new(inner: Arc<dyn PassphraseProvider + Send + Sync>) -> Self {
Self {
inner,
cached: Mutex::new(None),
}
}
}
impl PassphraseProvider for UnifiedPassphraseProvider {
fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
let mut guard = self
.cached
.lock()
.map_err(|e| AgentError::MutexError(e.to_string()))?;
if let Some(ref cached) = *guard {
return Ok(Zeroizing::new(cached.as_str().to_string()));
}
let passphrase = self.inner.get_passphrase(prompt_message)?;
*guard = Some(Zeroizing::new(passphrase.as_str().to_string()));
Ok(passphrase)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::signer::encrypt_keypair;
use ring::rand::SystemRandom;
use ring::signature::{ED25519, Ed25519KeyPair, KeyPair, UnparsedPublicKey};
use std::collections::HashMap;
use std::sync::Mutex;
use crate::storage::keychain::KeyRole;
struct MockKeyStorage {
#[allow(clippy::type_complexity)]
keys: Mutex<HashMap<String, (IdentityDID, KeyRole, Vec<u8>)>>,
}
impl MockKeyStorage {
fn new() -> Self {
Self {
keys: Mutex::new(HashMap::new()),
}
}
}
impl KeyStorage for MockKeyStorage {
fn store_key(
&self,
alias: &KeyAlias,
identity_did: &IdentityDID,
role: KeyRole,
encrypted_key_data: &[u8],
) -> Result<(), AgentError> {
self.keys.lock().unwrap().insert(
alias.as_str().to_string(),
(identity_did.clone(), role, encrypted_key_data.to_vec()),
);
Ok(())
}
fn load_key(
&self,
alias: &KeyAlias,
) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
self.keys
.lock()
.unwrap()
.get(alias.as_str())
.cloned()
.ok_or(AgentError::KeyNotFound)
}
fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
self.keys
.lock()
.unwrap()
.remove(alias.as_str())
.map(|_| ())
.ok_or(AgentError::KeyNotFound)
}
fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
Ok(self
.keys
.lock()
.unwrap()
.keys()
.map(|s| KeyAlias::new_unchecked(s.clone()))
.collect())
}
fn list_aliases_for_identity(
&self,
identity_did: &IdentityDID,
) -> Result<Vec<KeyAlias>, AgentError> {
Ok(self
.keys
.lock()
.unwrap()
.iter()
.filter(|(_, (did, _role, _))| did == identity_did)
.map(|(alias, _)| KeyAlias::new_unchecked(alias.clone()))
.collect())
}
fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
self.keys
.lock()
.unwrap()
.get(alias.as_str())
.map(|(did, _role, _)| did.clone())
.ok_or(AgentError::KeyNotFound)
}
fn backend_name(&self) -> &'static str {
"MockKeyStorage"
}
}
struct MockPassphraseProvider {
passphrase: String,
}
impl MockPassphraseProvider {
fn new(passphrase: &str) -> Self {
Self {
passphrase: passphrase.to_string(),
}
}
}
impl PassphraseProvider for MockPassphraseProvider {
fn get_passphrase(&self, _prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
Ok(Zeroizing::new(self.passphrase.clone()))
}
}
fn generate_test_keypair() -> (Vec<u8>, Vec<u8>) {
let rng = SystemRandom::new();
let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng).expect("Failed to generate PKCS#8");
let pkcs8_bytes = pkcs8_doc.as_ref().to_vec();
let keypair = Ed25519KeyPair::from_pkcs8(&pkcs8_bytes).expect("Failed to parse PKCS#8");
let pubkey_bytes = keypair.public_key().as_ref().to_vec();
(pkcs8_bytes, pubkey_bytes)
}
#[test]
fn test_sign_for_identity_success() {
let (pkcs8_bytes, pubkey_bytes) = generate_test_keypair();
let passphrase = "Test-P@ss12345";
#[allow(clippy::disallowed_methods)]
let identity_did = IdentityDID::new_unchecked("did:keri:ABC123");
let alias = KeyAlias::new_unchecked("test-key-alias");
let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("Failed to encrypt");
let storage = MockKeyStorage::new();
storage
.store_key(&alias, &identity_did, KeyRole::Primary, &encrypted)
.expect("Failed to store key");
let signer = StorageSigner::new(storage);
let passphrase_provider = MockPassphraseProvider::new(passphrase);
let message = b"test message for sign_for_identity";
let signature = signer
.sign_for_identity(&identity_did, &passphrase_provider, message)
.expect("Signing failed");
let public_key = UnparsedPublicKey::new(&ED25519, &pubkey_bytes);
assert!(public_key.verify(message, &signature).is_ok());
}
#[test]
fn test_sign_for_identity_no_key_for_identity() {
let storage = MockKeyStorage::new();
let signer = StorageSigner::new(storage);
let passphrase_provider = MockPassphraseProvider::new("any-passphrase");
#[allow(clippy::disallowed_methods)]
let identity_did = IdentityDID::new_unchecked("did:keri:NONEXISTENT");
let message = b"test message";
let result = signer.sign_for_identity(&identity_did, &passphrase_provider, message);
assert!(matches!(result, Err(AgentError::KeyNotFound)));
}
#[test]
fn test_sign_for_identity_multiple_aliases() {
let (pkcs8_bytes, pubkey_bytes) = generate_test_keypair();
let passphrase = "Test-P@ss12345";
#[allow(clippy::disallowed_methods)]
let identity_did = IdentityDID::new_unchecked("did:keri:MULTI123");
let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("Failed to encrypt");
let storage = MockKeyStorage::new();
let alias = KeyAlias::new_unchecked("primary-alias");
storage
.store_key(&alias, &identity_did, KeyRole::Primary, &encrypted)
.expect("Failed to store key");
let signer = StorageSigner::new(storage);
let passphrase_provider = MockPassphraseProvider::new(passphrase);
let message = b"test message with multiple aliases";
let signature = signer
.sign_for_identity(&identity_did, &passphrase_provider, message)
.expect("Signing should succeed");
let public_key = UnparsedPublicKey::new(&ED25519, &pubkey_bytes);
assert!(public_key.verify(message, &signature).is_ok());
}
#[test]
fn test_callback_passphrase_provider() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let call_count = Arc::new(AtomicUsize::new(0));
let call_count_clone = Arc::clone(&call_count);
let provider = CallbackPassphraseProvider::new(move |prompt| {
call_count_clone.fetch_add(1, Ordering::SeqCst);
assert!(prompt.contains("test-alias"));
Ok(Zeroizing::new("callback-passphrase".to_string()))
});
let result = provider.get_passphrase("Enter passphrase for test-alias:");
assert!(result.is_ok());
assert_eq!(*result.unwrap(), "callback-passphrase");
assert_eq!(call_count.load(Ordering::SeqCst), 1);
let result2 = provider.get_passphrase("Another prompt for test-alias");
assert!(result2.is_ok());
assert_eq!(call_count.load(Ordering::SeqCst), 2);
}
#[test]
fn test_callback_passphrase_provider_error() {
let provider =
CallbackPassphraseProvider::new(|_prompt| Err(AgentError::UserInputCancelled));
let result = provider.get_passphrase("Enter passphrase:");
assert!(matches!(result, Err(AgentError::UserInputCancelled)));
}
#[test]
fn test_cached_passphrase_provider_cache_hit() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
let call_count = Arc::new(AtomicUsize::new(0));
let call_count_clone = Arc::clone(&call_count);
let inner = Arc::new(CallbackPassphraseProvider::new(move |_prompt| {
call_count_clone.fetch_add(1, Ordering::SeqCst);
Ok(Zeroizing::new("cached-pass".to_string()))
}));
let cached = CachedPassphraseProvider::new(inner, Duration::from_secs(60));
let result1 = cached.get_passphrase("prompt1");
assert!(result1.is_ok());
assert_eq!(*result1.unwrap(), "cached-pass");
assert_eq!(call_count.load(Ordering::SeqCst), 1);
let result2 = cached.get_passphrase("prompt1");
assert!(result2.is_ok());
assert_eq!(*result2.unwrap(), "cached-pass");
assert_eq!(call_count.load(Ordering::SeqCst), 1); }
#[test]
fn test_cached_passphrase_provider_cache_miss() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
let call_count = Arc::new(AtomicUsize::new(0));
let call_count_clone = Arc::clone(&call_count);
let inner = Arc::new(CallbackPassphraseProvider::new(move |_prompt| {
call_count_clone.fetch_add(1, Ordering::SeqCst);
Ok(Zeroizing::new("pass".to_string()))
}));
let cached = CachedPassphraseProvider::new(inner, Duration::from_secs(60));
let _ = cached.get_passphrase("prompt1");
assert_eq!(call_count.load(Ordering::SeqCst), 1);
let _ = cached.get_passphrase("prompt2");
assert_eq!(call_count.load(Ordering::SeqCst), 2);
let _ = cached.get_passphrase("prompt3");
assert_eq!(call_count.load(Ordering::SeqCst), 3);
}
#[test]
fn test_cached_passphrase_provider_expiry() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
let call_count = Arc::new(AtomicUsize::new(0));
let call_count_clone = Arc::clone(&call_count);
let inner = Arc::new(CallbackPassphraseProvider::new(move |_prompt| {
call_count_clone.fetch_add(1, Ordering::SeqCst);
Ok(Zeroizing::new("pass".to_string()))
}));
let cached = CachedPassphraseProvider::new(inner, Duration::from_millis(10));
let _ = cached.get_passphrase("prompt");
assert_eq!(call_count.load(Ordering::SeqCst), 1);
std::thread::sleep(Duration::from_millis(20));
let _ = cached.get_passphrase("prompt");
assert_eq!(call_count.load(Ordering::SeqCst), 2);
}
#[test]
fn test_cached_passphrase_provider_clear_cache() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
let call_count = Arc::new(AtomicUsize::new(0));
let call_count_clone = Arc::clone(&call_count);
let inner = Arc::new(CallbackPassphraseProvider::new(move |_prompt| {
call_count_clone.fetch_add(1, Ordering::SeqCst);
Ok(Zeroizing::new("pass".to_string()))
}));
let cached = CachedPassphraseProvider::new(inner, Duration::from_secs(60));
let _ = cached.get_passphrase("prompt");
assert_eq!(call_count.load(Ordering::SeqCst), 1);
let _ = cached.get_passphrase("prompt");
assert_eq!(call_count.load(Ordering::SeqCst), 1);
cached.clear_cache();
let _ = cached.get_passphrase("prompt");
assert_eq!(call_count.load(Ordering::SeqCst), 2);
}
#[test]
fn test_prefilled_passphrase_provider_returns_stored_value() {
let provider = PrefilledPassphraseProvider::new("my-secret");
let result = provider.get_passphrase("any prompt").unwrap();
assert_eq!(*result, "my-secret");
let result2 = provider.get_passphrase("different prompt").unwrap();
assert_eq!(*result2, "my-secret");
}
#[test]
fn test_prefilled_passphrase_provider_empty_passphrase() {
let provider = PrefilledPassphraseProvider::new("");
let result = provider.get_passphrase("prompt").unwrap();
assert_eq!(*result, "");
}
#[test]
fn test_unified_passphrase_provider_prompts_once_for_multiple_keys() {
use std::sync::atomic::{AtomicUsize, Ordering};
let call_count = Arc::new(AtomicUsize::new(0));
let count_clone = call_count.clone();
let inner = CallbackPassphraseProvider::new(move |_prompt: &str| {
count_clone.fetch_add(1, Ordering::SeqCst);
Ok(Zeroizing::new("secret".to_string()))
});
let provider = UnifiedPassphraseProvider::new(Arc::new(inner));
let p1 = provider
.get_passphrase("Enter passphrase for DEVICE key 'dev':")
.unwrap();
let p2 = provider
.get_passphrase("Enter passphrase for IDENTITY key 'id':")
.unwrap();
assert_eq!(*p1, "secret");
assert_eq!(*p2, "secret");
assert_eq!(call_count.load(Ordering::SeqCst), 1); }
}