Skip to main content

auths_core/
signing.rs

1//! Signing abstractions and DID resolution.
2
3use auths_verifier::core::Ed25519PublicKey;
4
5use crate::crypto::provider_bridge;
6use crate::crypto::signer::{decrypt_keypair, extract_seed_from_key_bytes};
7use crate::error::AgentError;
8use crate::storage::keychain::{IdentityDID, KeyAlias, KeyStorage};
9
10use crate::config::PassphraseCachePolicy;
11use crate::storage::passphrase_cache::PassphraseCache;
12
13use std::collections::HashMap;
14use std::sync::{Arc, Mutex};
15use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
16use zeroize::Zeroizing;
17
18/// Type alias for passphrase callback functions.
19type PassphraseCallback = dyn Fn(&str) -> Result<Zeroizing<String>, AgentError> + Send + Sync;
20
21/// Error type for DID resolution.
22///
23/// Args:
24/// * Variants represent distinct failure modes during DID resolution.
25///
26/// Usage:
27/// ```ignore
28/// use auths_core::signing::DidResolverError;
29///
30/// let err = DidResolverError::UnsupportedMethod("web".to_string());
31/// assert!(err.to_string().contains("Unsupported"));
32/// ```
33#[derive(Debug, thiserror::Error)]
34pub enum DidResolverError {
35    /// The DID method is not supported.
36    #[error("Unsupported DID method: {0}")]
37    UnsupportedMethod(String),
38
39    /// The did:key identifier is invalid.
40    #[error("Invalid did:key format: {0}")]
41    InvalidDidKey(String),
42
43    /// The did:key format is malformed.
44    #[error("Invalid did:key format: {0}")]
45    InvalidDidKeyFormat(String),
46
47    /// Failed to decode the did:key.
48    #[error("did:key decoding failed: {0}")]
49    DidKeyDecodingFailed(String),
50
51    /// Unsupported multicodec prefix in did:key.
52    #[error("Invalid did:key multicodec prefix")]
53    InvalidDidKeyMulticodec,
54
55    /// DID resolution failed.
56    #[error("Resolution error: {0}")]
57    Resolution(String),
58
59    /// Repository access failed.
60    #[error("Repository error: {0}")]
61    Repository(String),
62}
63
64/// Result of DID resolution, parameterised by method.
65///
66/// Usage:
67/// ```ignore
68/// use auths_core::signing::ResolvedDid;
69/// use auths_verifier::core::Ed25519PublicKey;
70///
71/// let resolved = ResolvedDid::Key {
72///     did: "did:key:z6Mk...".to_string(),
73///     public_key: Ed25519PublicKey::from_bytes([1u8; 32]),
74/// };
75/// assert!(resolved.is_key());
76/// ```
77#[derive(Debug, Clone)]
78pub enum ResolvedDid {
79    /// Static did:key (no rotation possible).
80    Key {
81        /// The resolved DID string.
82        did: String,
83        /// The Ed25519 public key.
84        public_key: Ed25519PublicKey,
85    },
86    /// KERI-based identity with rotation capability.
87    Keri {
88        /// The resolved DID string.
89        did: String,
90        /// The Ed25519 public key.
91        public_key: Ed25519PublicKey,
92        /// Current KEL sequence number.
93        sequence: u64,
94        /// Whether key rotation is available.
95        can_rotate: bool,
96    },
97}
98
99impl ResolvedDid {
100    /// Returns the DID string.
101    pub fn did(&self) -> &str {
102        match self {
103            ResolvedDid::Key { did, .. } | ResolvedDid::Keri { did, .. } => did,
104        }
105    }
106
107    /// Returns the Ed25519 public key.
108    pub fn public_key(&self) -> &Ed25519PublicKey {
109        match self {
110            ResolvedDid::Key { public_key, .. } | ResolvedDid::Keri { public_key, .. } => {
111                public_key
112            }
113        }
114    }
115
116    /// Returns `true` if this is a `did:key` resolution.
117    pub fn is_key(&self) -> bool {
118        matches!(self, ResolvedDid::Key { .. })
119    }
120
121    /// Returns `true` if this is a `did:keri` resolution.
122    pub fn is_keri(&self) -> bool {
123        matches!(self, ResolvedDid::Keri { .. })
124    }
125}
126
127/// Resolves a Decentralized Identifier (DID) to its cryptographic material.
128///
129/// Implementations handle specific DID methods (did:key, did:keri) and return
130/// the resolved public key along with method-specific metadata. The resolver
131/// abstracts away the underlying storage and network access needed for resolution.
132///
133/// Args:
134/// * `did`: A DID string (e.g., `"did:keri:EABC..."` or `"did:key:z6Mk..."`).
135///
136/// Usage:
137/// ```ignore
138/// use auths_core::signing::DidResolver;
139///
140/// fn verify_attestation(resolver: &dyn DidResolver, issuer_did: &str) -> bool {
141///     match resolver.resolve(issuer_did) {
142///         Ok(resolved) => {
143///             let public_key = resolved.public_key();
144///             // use public_key for signature verification
145///             true
146///         }
147///         Err(_) => false,
148///     }
149/// }
150/// ```
151pub trait DidResolver: Send + Sync {
152    /// Resolve a DID to its public key and method.
153    fn resolve(&self, did: &str) -> Result<ResolvedDid, DidResolverError>;
154}
155
156/// A trait for components that can securely provide a passphrase when requested.
157///
158/// This allows the core signing logic to request a passphrase without knowing
159/// whether it's coming from a terminal prompt, a GUI dialog, or another source.
160/// Implementors should handle secure input and potential user cancellation.
161pub trait PassphraseProvider: Send + Sync {
162    /// Securely obtains a passphrase, potentially by prompting the user.
163    ///
164    /// Args:
165    /// * `prompt_message`: A message to display to the user indicating why the passphrase is needed.
166    ///
167    /// Usage:
168    /// ```ignore
169    /// let passphrase = provider.get_passphrase("Enter passphrase for key 'main':")?;
170    /// ```
171    fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError>;
172
173    /// Notifies the provider that the passphrase returned for `prompt_message` was wrong.
174    ///
175    /// The default implementation is a no-op. Caching providers override this to
176    /// evict the stale entry so subsequent calls prompt the user again rather than
177    /// replaying a known-bad passphrase.
178    ///
179    /// Args:
180    /// * `prompt_message`: The prompt for which the bad passphrase was cached.
181    fn on_incorrect_passphrase(&self, _prompt_message: &str) {}
182}
183
184/// A trait for components that can perform signing operations using stored keys,
185/// identified by an alias, while securely handling decryption and passphrase input.
186pub trait SecureSigner: Send + Sync {
187    /// Requests a signature for the given message using the key identified by the alias.
188    ///
189    /// This method handles loading the encrypted key, obtaining the necessary passphrase
190    /// via the provided `PassphraseProvider`, decrypting the key, performing the signature,
191    /// and ensuring the decrypted key material is handled securely.
192    ///
193    /// # Arguments
194    /// * `alias`: The alias of the key to use for signing.
195    /// * `passphrase_provider`: An implementation of `PassphraseProvider` used to obtain the passphrase if needed.
196    /// * `message`: The message bytes to be signed.
197    ///
198    /// # Returns
199    /// * `Ok(Vec<u8>)`: The raw signature bytes.
200    /// * `Err(AgentError)`: If any step fails (key not found, incorrect passphrase, decryption error, signing error, etc.).
201    fn sign_with_alias(
202        &self,
203        alias: &KeyAlias,
204        passphrase_provider: &dyn PassphraseProvider,
205        message: &[u8],
206    ) -> Result<Vec<u8>, AgentError>;
207
208    /// Signs a message using the key associated with the given identity DID.
209    ///
210    /// This method resolves the identity DID to an alias by looking up keys
211    /// associated with that identity in storage, then delegates to `sign_with_alias`.
212    ///
213    /// # DID to Alias Resolution Strategy
214    /// The implementation uses the storage backend's `list_aliases_for_identity`
215    /// to find aliases associated with the given DID. The first matching alias
216    /// is used for signing.
217    ///
218    /// # Arguments
219    /// * `identity_did`: The identity DID (e.g., "did:keri:ABC...") to sign for.
220    /// * `passphrase_provider`: Used to obtain the passphrase for key decryption.
221    /// * `message`: The message bytes to be signed.
222    ///
223    /// # Returns
224    /// * `Ok(Vec<u8>)`: The raw signature bytes.
225    /// * `Err(AgentError)`: If no key is found for the identity, or if signing fails.
226    fn sign_for_identity(
227        &self,
228        identity_did: &IdentityDID,
229        passphrase_provider: &dyn PassphraseProvider,
230        message: &[u8],
231    ) -> Result<Vec<u8>, AgentError>;
232}
233
234/// Concrete implementation of `SecureSigner` that uses a `KeyStorage` backend.
235///
236/// It requires a `PassphraseProvider` to be passed into the signing method
237/// to handle user interaction for passphrase input securely.
238pub struct StorageSigner<S: KeyStorage> {
239    /// The storage backend implementation (e.g., IOSKeychain, MacOSKeychain).
240    storage: S,
241}
242
243impl<S: KeyStorage> StorageSigner<S> {
244    /// Creates a new `StorageSigner` with the given storage backend.
245    pub fn new(storage: S) -> Self {
246        Self { storage }
247    }
248
249    /// Returns a reference to the underlying storage backend.
250    pub fn inner(&self) -> &S {
251        &self.storage
252    }
253}
254
255impl<S: KeyStorage + Send + Sync + 'static> SecureSigner for StorageSigner<S> {
256    fn sign_with_alias(
257        &self,
258        alias: &KeyAlias,
259        passphrase_provider: &dyn PassphraseProvider,
260        message: &[u8],
261    ) -> Result<Vec<u8>, AgentError> {
262        let (_identity_did, encrypted_data) = self.storage.load_key(alias)?;
263
264        const MAX_ATTEMPTS: u8 = 3;
265        let mut attempt = 0u8;
266        let key_bytes = loop {
267            let prompt = if attempt == 0 {
268                format!("Enter passphrase for key '{}' to sign:", alias)
269            } else {
270                format!(
271                    "Incorrect passphrase, try again ({}/{}):",
272                    attempt + 1,
273                    MAX_ATTEMPTS
274                )
275            };
276
277            let passphrase = passphrase_provider.get_passphrase(&prompt)?;
278
279            match decrypt_keypair(&encrypted_data, &passphrase) {
280                Ok(kb) => break kb,
281                Err(AgentError::IncorrectPassphrase) if attempt + 1 < MAX_ATTEMPTS => {
282                    passphrase_provider.on_incorrect_passphrase(&prompt);
283                    attempt += 1;
284                }
285                Err(e) => return Err(e),
286            }
287        };
288
289        let seed = extract_seed_from_key_bytes(&key_bytes)?;
290
291        provider_bridge::sign_ed25519_sync(&seed, message)
292            .map_err(|e| AgentError::CryptoError(format!("Ed25519 signing failed: {}", e)))
293    }
294
295    fn sign_for_identity(
296        &self,
297        identity_did: &IdentityDID,
298        passphrase_provider: &dyn PassphraseProvider,
299        message: &[u8],
300    ) -> Result<Vec<u8>, AgentError> {
301        // 1. Find aliases associated with this identity DID
302        let aliases = self.storage.list_aliases_for_identity(identity_did)?;
303
304        // 2. Get the first alias (primary key for this identity)
305        let alias = aliases.first().ok_or(AgentError::KeyNotFound)?;
306
307        // 3. Delegate to sign_with_alias
308        self.sign_with_alias(alias, passphrase_provider, message)
309    }
310}
311
312/// A `PassphraseProvider` that delegates to a callback function.
313///
314/// This is useful for GUI applications and FFI bindings where the passphrase
315/// input mechanism is provided externally.
316///
317/// # Examples
318///
319/// ```ignore
320/// use auths_core::signing::{CallbackPassphraseProvider, PassphraseProvider};
321///
322/// let provider = CallbackPassphraseProvider::new(|prompt| {
323///     // In a real GUI, this would show a dialog
324///     Ok("user-entered-passphrase".to_string())
325/// });
326/// ```
327pub struct CallbackPassphraseProvider {
328    callback: Box<PassphraseCallback>,
329}
330
331impl CallbackPassphraseProvider {
332    /// Creates a new `CallbackPassphraseProvider` with the given callback function.
333    ///
334    /// The callback receives the prompt message and should return the passphrase
335    /// entered by the user, or an error if passphrase acquisition failed.
336    pub fn new<F>(callback: F) -> Self
337    where
338        F: Fn(&str) -> Result<Zeroizing<String>, AgentError> + Send + Sync + 'static,
339    {
340        Self {
341            callback: Box::new(callback),
342        }
343    }
344}
345
346impl PassphraseProvider for CallbackPassphraseProvider {
347    fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
348        (self.callback)(prompt_message)
349    }
350}
351
352/// A `PassphraseProvider` that caches passphrases from an inner provider.
353///
354/// Cached values are stored in `Zeroizing<String>` for automatic zeroing on drop
355/// and expire after the configured TTL (time-to-live).
356///
357/// This is useful for agent sessions where prompting for every signing operation
358/// would be disruptive, but credentials shouldn't persist indefinitely.
359///
360/// # Security Considerations
361/// - Cached passphrases are wrapped in `Zeroizing<String>` for secure memory cleanup
362/// - TTL prevents stale credentials from persisting
363/// - Call `clear_cache()` on logout or lock events
364pub struct CachedPassphraseProvider {
365    inner: Arc<dyn PassphraseProvider + Send + Sync>,
366    cache: Mutex<HashMap<String, (Zeroizing<String>, Instant)>>,
367    ttl: Duration,
368}
369
370impl CachedPassphraseProvider {
371    /// Creates a new `CachedPassphraseProvider` wrapping the given provider.
372    ///
373    /// # Arguments
374    /// * `inner` - The underlying provider to fetch passphrases from on cache miss
375    /// * `ttl` - How long cached passphrases remain valid before expiring
376    pub fn new(inner: Arc<dyn PassphraseProvider + Send + Sync>, ttl: Duration) -> Self {
377        Self {
378            inner,
379            cache: Mutex::new(HashMap::new()),
380            ttl,
381        }
382    }
383
384    /// Clears all cached passphrases.
385    ///
386    /// Call this on logout, lock, or when the session ends to ensure
387    /// cached credentials don't persist in memory.
388    pub fn clear_cache(&self) {
389        self.cache.lock().unwrap_or_else(|e| e.into_inner()).clear();
390    }
391}
392
393impl PassphraseProvider for CachedPassphraseProvider {
394    fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
395        let mut cache = self
396            .cache
397            .lock()
398            .map_err(|e| AgentError::MutexError(e.to_string()))?;
399
400        // Check cache for unexpired entry
401        if let Some((passphrase, cached_at)) = cache.get(prompt_message) {
402            if cached_at.elapsed() < self.ttl {
403                // Clone the inner String and wrap in new Zeroizing
404                return Ok(passphrase.clone());
405            }
406            // Expired - remove the entry
407            cache.remove(prompt_message);
408        }
409
410        // Cache miss or expired - fetch from inner provider
411        drop(cache); // Release lock before calling inner to avoid deadlock
412        let passphrase = self.inner.get_passphrase(prompt_message)?;
413
414        // Store in cache - clone the passphrase since we return the original
415        let mut cache = self
416            .cache
417            .lock()
418            .map_err(|e| AgentError::MutexError(e.to_string()))?;
419        cache.insert(
420            prompt_message.to_string(),
421            (passphrase.clone(), Instant::now()),
422        );
423        Ok(passphrase)
424    }
425
426    fn on_incorrect_passphrase(&self, prompt_message: &str) {
427        self.cache
428            .lock()
429            .unwrap_or_else(|e| e.into_inner())
430            .remove(prompt_message);
431    }
432}
433
434/// A `PassphraseProvider` that wraps an inner provider with OS keychain caching.
435///
436/// On `get_passphrase()`, checks the OS keychain first via `PassphraseCache::load`.
437/// If a cached value exists and hasn't expired per the configured policy/TTL,
438/// returns it immediately. Otherwise delegates to the inner provider, then
439/// stores the result in the OS keychain for subsequent invocations.
440///
441/// Args:
442/// * `inner`: The underlying provider to prompt the user when cache misses.
443/// * `cache`: Platform keychain cache (macOS Security Framework, Linux Secret Service, etc.).
444/// * `alias`: Key alias used as the cache key in the OS keychain.
445/// * `policy`: The configured `PassphraseCachePolicy`.
446/// * `ttl_secs`: Optional TTL in seconds (for `Duration` policy).
447///
448/// Usage:
449/// ```ignore
450/// use auths_core::signing::{KeychainPassphraseProvider, PassphraseProvider};
451/// use auths_core::config::PassphraseCachePolicy;
452/// use auths_core::storage::passphrase_cache::get_passphrase_cache;
453///
454/// let inner = Arc::new(some_provider);
455/// let cache = get_passphrase_cache(true);
456/// let provider = KeychainPassphraseProvider::new(
457///     inner, cache, "main".to_string(),
458///     PassphraseCachePolicy::Duration, Some(3600),
459/// );
460/// let passphrase = provider.get_passphrase("Enter passphrase:")?;
461/// ```
462pub struct KeychainPassphraseProvider {
463    inner: Arc<dyn PassphraseProvider + Send + Sync>,
464    cache: Box<dyn PassphraseCache>,
465    alias: String,
466    policy: PassphraseCachePolicy,
467    ttl_secs: Option<i64>,
468}
469
470impl KeychainPassphraseProvider {
471    /// Creates a new `KeychainPassphraseProvider`.
472    ///
473    /// Args:
474    /// * `inner`: Fallback provider for cache misses.
475    /// * `cache`: OS keychain cache implementation.
476    /// * `alias`: Key alias used as the keychain entry identifier.
477    /// * `policy`: Caching policy controlling storage/expiry behavior.
478    /// * `ttl_secs`: TTL in seconds when `policy` is `Duration`.
479    pub fn new(
480        inner: Arc<dyn PassphraseProvider + Send + Sync>,
481        cache: Box<dyn PassphraseCache>,
482        alias: String,
483        policy: PassphraseCachePolicy,
484        ttl_secs: Option<i64>,
485    ) -> Self {
486        Self {
487            inner,
488            cache,
489            alias,
490            policy,
491            ttl_secs,
492        }
493    }
494
495    fn is_expired(&self, stored_at_unix: i64) -> bool {
496        match self.policy {
497            PassphraseCachePolicy::Always => false,
498            PassphraseCachePolicy::Never => true,
499            PassphraseCachePolicy::Session => true,
500            PassphraseCachePolicy::Duration => {
501                let ttl = self.ttl_secs.unwrap_or(3600);
502                let now = SystemTime::now()
503                    .duration_since(UNIX_EPOCH)
504                    .unwrap_or_default()
505                    .as_secs() as i64;
506                now - stored_at_unix > ttl
507            }
508        }
509    }
510}
511
512impl PassphraseProvider for KeychainPassphraseProvider {
513    fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
514        if self.policy != PassphraseCachePolicy::Never
515            && let Ok(Some((passphrase, stored_at))) = self.cache.load(&self.alias)
516        {
517            if !self.is_expired(stored_at) {
518                return Ok(passphrase);
519            }
520            let _ = self.cache.delete(&self.alias);
521        }
522
523        let passphrase = self.inner.get_passphrase(prompt_message)?;
524
525        if self.policy != PassphraseCachePolicy::Never
526            && self.policy != PassphraseCachePolicy::Session
527        {
528            let now = SystemTime::now()
529                .duration_since(UNIX_EPOCH)
530                .unwrap_or_default()
531                .as_secs() as i64;
532            let _ = self.cache.store(&self.alias, &passphrase, now);
533        }
534
535        Ok(passphrase)
536    }
537
538    fn on_incorrect_passphrase(&self, prompt_message: &str) {
539        let _ = self.cache.delete(&self.alias);
540        self.inner.on_incorrect_passphrase(prompt_message);
541    }
542}
543
544/// Provides a pre-collected passphrase for headless and automated environments.
545///
546/// Unlike [`CallbackPassphraseProvider`] which prompts interactively, this provider
547/// returns a passphrase that was collected or generated before construction.
548/// Intended for CI pipelines, Terraform providers, REST APIs, and integration tests.
549///
550/// Args:
551/// * `passphrase`: The passphrase to return on every `get_passphrase()` call.
552///
553/// Usage:
554/// ```ignore
555/// use auths_core::signing::{PrefilledPassphraseProvider, PassphraseProvider};
556///
557/// let provider = PrefilledPassphraseProvider::new("my-secret-passphrase");
558/// let passphrase = provider.get_passphrase("any prompt").unwrap();
559/// assert_eq!(*passphrase, "my-secret-passphrase");
560/// ```
561pub struct PrefilledPassphraseProvider {
562    passphrase: Zeroizing<String>,
563}
564
565impl PrefilledPassphraseProvider {
566    /// Creates a new `PrefilledPassphraseProvider` with the given passphrase.
567    ///
568    /// Args:
569    /// * `passphrase`: The passphrase string to store and return on every request.
570    ///
571    /// Usage:
572    /// ```ignore
573    /// let provider = PrefilledPassphraseProvider::new("hunter2");
574    /// ```
575    pub fn new(passphrase: &str) -> Self {
576        Self {
577            passphrase: Zeroizing::new(passphrase.to_string()),
578        }
579    }
580}
581
582impl PassphraseProvider for PrefilledPassphraseProvider {
583    fn get_passphrase(&self, _prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
584        Ok(self.passphrase.clone())
585    }
586}
587
588/// A passphrase provider that prompts exactly once regardless of how many
589/// distinct prompt messages are presented. Every call after the first is a
590/// cache hit. Designed for multi-key operations (e.g. device link) where the
591/// same passphrase protects all keys in the operation.
592pub struct UnifiedPassphraseProvider {
593    inner: Arc<dyn PassphraseProvider + Send + Sync>,
594    cached: Mutex<Option<Zeroizing<String>>>,
595}
596
597impl UnifiedPassphraseProvider {
598    /// Create a provider wrapping the given passphrase source.
599    pub fn new(inner: Arc<dyn PassphraseProvider + Send + Sync>) -> Self {
600        Self {
601            inner,
602            cached: Mutex::new(None),
603        }
604    }
605}
606
607impl PassphraseProvider for UnifiedPassphraseProvider {
608    fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
609        let mut guard = self
610            .cached
611            .lock()
612            .map_err(|e| AgentError::MutexError(e.to_string()))?;
613        if let Some(ref cached) = *guard {
614            return Ok(Zeroizing::new(cached.as_str().to_string()));
615        }
616        let passphrase = self.inner.get_passphrase(prompt_message)?;
617        *guard = Some(Zeroizing::new(passphrase.as_str().to_string()));
618        Ok(passphrase)
619    }
620}
621
622#[cfg(test)]
623mod tests {
624    use super::*;
625    use crate::crypto::signer::encrypt_keypair;
626    use ring::rand::SystemRandom;
627    use ring::signature::{ED25519, Ed25519KeyPair, KeyPair, UnparsedPublicKey};
628    use std::collections::HashMap;
629    use std::sync::Mutex;
630
631    /// Mock KeyStorage implementation for testing
632    struct MockKeyStorage {
633        keys: Mutex<HashMap<String, (IdentityDID, Vec<u8>)>>,
634    }
635
636    impl MockKeyStorage {
637        fn new() -> Self {
638            Self {
639                keys: Mutex::new(HashMap::new()),
640            }
641        }
642    }
643
644    impl KeyStorage for MockKeyStorage {
645        fn store_key(
646            &self,
647            alias: &KeyAlias,
648            identity_did: &IdentityDID,
649            encrypted_key_data: &[u8],
650        ) -> Result<(), AgentError> {
651            self.keys.lock().unwrap().insert(
652                alias.as_str().to_string(),
653                (identity_did.clone(), encrypted_key_data.to_vec()),
654            );
655            Ok(())
656        }
657
658        fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, Vec<u8>), AgentError> {
659            self.keys
660                .lock()
661                .unwrap()
662                .get(alias.as_str())
663                .cloned()
664                .ok_or(AgentError::KeyNotFound)
665        }
666
667        fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
668            self.keys
669                .lock()
670                .unwrap()
671                .remove(alias.as_str())
672                .map(|_| ())
673                .ok_or(AgentError::KeyNotFound)
674        }
675
676        fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
677            Ok(self
678                .keys
679                .lock()
680                .unwrap()
681                .keys()
682                .map(|s| KeyAlias::new_unchecked(s.clone()))
683                .collect())
684        }
685
686        fn list_aliases_for_identity(
687            &self,
688            identity_did: &IdentityDID,
689        ) -> Result<Vec<KeyAlias>, AgentError> {
690            Ok(self
691                .keys
692                .lock()
693                .unwrap()
694                .iter()
695                .filter(|(_, (did, _))| did == identity_did)
696                .map(|(alias, _)| KeyAlias::new_unchecked(alias.clone()))
697                .collect())
698        }
699
700        fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
701            self.keys
702                .lock()
703                .unwrap()
704                .get(alias.as_str())
705                .map(|(did, _)| did.clone())
706                .ok_or(AgentError::KeyNotFound)
707        }
708
709        fn backend_name(&self) -> &'static str {
710            "MockKeyStorage"
711        }
712    }
713
714    /// Mock PassphraseProvider that returns a fixed passphrase
715    struct MockPassphraseProvider {
716        passphrase: String,
717    }
718
719    impl MockPassphraseProvider {
720        fn new(passphrase: &str) -> Self {
721            Self {
722                passphrase: passphrase.to_string(),
723            }
724        }
725    }
726
727    impl PassphraseProvider for MockPassphraseProvider {
728        fn get_passphrase(&self, _prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
729            Ok(Zeroizing::new(self.passphrase.clone()))
730        }
731    }
732
733    fn generate_test_keypair() -> (Vec<u8>, Vec<u8>) {
734        let rng = SystemRandom::new();
735        let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng).expect("Failed to generate PKCS#8");
736        let pkcs8_bytes = pkcs8_doc.as_ref().to_vec();
737        let keypair = Ed25519KeyPair::from_pkcs8(&pkcs8_bytes).expect("Failed to parse PKCS#8");
738        let pubkey_bytes = keypair.public_key().as_ref().to_vec();
739        (pkcs8_bytes, pubkey_bytes)
740    }
741
742    #[test]
743    fn test_sign_for_identity_success() {
744        let (pkcs8_bytes, pubkey_bytes) = generate_test_keypair();
745        let passphrase = "Test-P@ss12345";
746        let identity_did = IdentityDID::new("did:keri:ABC123");
747        let alias = KeyAlias::new_unchecked("test-key-alias");
748
749        // Encrypt the key
750        let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("Failed to encrypt");
751
752        // Set up mock storage with the key
753        let storage = MockKeyStorage::new();
754        storage
755            .store_key(&alias, &identity_did, &encrypted)
756            .expect("Failed to store key");
757
758        // Create signer and mocks
759        let signer = StorageSigner::new(storage);
760        let passphrase_provider = MockPassphraseProvider::new(passphrase);
761
762        // Sign a message
763        let message = b"test message for sign_for_identity";
764        let signature = signer
765            .sign_for_identity(&identity_did, &passphrase_provider, message)
766            .expect("Signing failed");
767
768        // Verify the signature
769        let public_key = UnparsedPublicKey::new(&ED25519, &pubkey_bytes);
770        assert!(public_key.verify(message, &signature).is_ok());
771    }
772
773    #[test]
774    fn test_sign_for_identity_no_key_for_identity() {
775        let storage = MockKeyStorage::new();
776        let signer = StorageSigner::new(storage);
777        let passphrase_provider = MockPassphraseProvider::new("any-passphrase");
778
779        let identity_did = IdentityDID::new("did:keri:NONEXISTENT");
780        let message = b"test message";
781
782        let result = signer.sign_for_identity(&identity_did, &passphrase_provider, message);
783        assert!(matches!(result, Err(AgentError::KeyNotFound)));
784    }
785
786    #[test]
787    fn test_sign_for_identity_multiple_aliases() {
788        // Test that sign_for_identity works when multiple aliases exist for an identity
789        let (pkcs8_bytes, pubkey_bytes) = generate_test_keypair();
790        let passphrase = "Test-P@ss12345";
791        let identity_did = IdentityDID::new("did:keri:MULTI123");
792
793        let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("Failed to encrypt");
794
795        let storage = MockKeyStorage::new();
796        // Store the same key under multiple aliases (first one should be used)
797        let alias = KeyAlias::new_unchecked("primary-alias");
798        storage
799            .store_key(&alias, &identity_did, &encrypted)
800            .expect("Failed to store key");
801
802        let signer = StorageSigner::new(storage);
803        let passphrase_provider = MockPassphraseProvider::new(passphrase);
804
805        let message = b"test message with multiple aliases";
806        let signature = signer
807            .sign_for_identity(&identity_did, &passphrase_provider, message)
808            .expect("Signing should succeed");
809
810        // Verify the signature
811        let public_key = UnparsedPublicKey::new(&ED25519, &pubkey_bytes);
812        assert!(public_key.verify(message, &signature).is_ok());
813    }
814
815    #[test]
816    fn test_callback_passphrase_provider() {
817        use std::sync::Arc;
818        use std::sync::atomic::{AtomicUsize, Ordering};
819
820        // Track how many times the callback is invoked
821        let call_count = Arc::new(AtomicUsize::new(0));
822        let call_count_clone = Arc::clone(&call_count);
823
824        let provider = CallbackPassphraseProvider::new(move |prompt| {
825            call_count_clone.fetch_add(1, Ordering::SeqCst);
826            assert!(prompt.contains("test-alias"));
827            Ok(Zeroizing::new("callback-passphrase".to_string()))
828        });
829
830        // Test successful passphrase retrieval
831        let result = provider.get_passphrase("Enter passphrase for test-alias:");
832        assert!(result.is_ok());
833        assert_eq!(*result.unwrap(), "callback-passphrase");
834        assert_eq!(call_count.load(Ordering::SeqCst), 1);
835
836        // Test multiple invocations
837        let result2 = provider.get_passphrase("Another prompt for test-alias");
838        assert!(result2.is_ok());
839        assert_eq!(call_count.load(Ordering::SeqCst), 2);
840    }
841
842    #[test]
843    fn test_callback_passphrase_provider_error() {
844        let provider =
845            CallbackPassphraseProvider::new(|_prompt| Err(AgentError::UserInputCancelled));
846
847        let result = provider.get_passphrase("Enter passphrase:");
848        assert!(matches!(result, Err(AgentError::UserInputCancelled)));
849    }
850
851    #[test]
852    fn test_cached_passphrase_provider_cache_hit() {
853        use std::sync::Arc;
854        use std::sync::atomic::{AtomicUsize, Ordering};
855        use std::time::Duration;
856
857        let call_count = Arc::new(AtomicUsize::new(0));
858        let call_count_clone = Arc::clone(&call_count);
859
860        let inner = Arc::new(CallbackPassphraseProvider::new(move |_prompt| {
861            call_count_clone.fetch_add(1, Ordering::SeqCst);
862            Ok(Zeroizing::new("cached-pass".to_string()))
863        }));
864
865        let cached = CachedPassphraseProvider::new(inner, Duration::from_secs(60));
866
867        // First call should invoke inner
868        let result1 = cached.get_passphrase("prompt1");
869        assert!(result1.is_ok());
870        assert_eq!(*result1.unwrap(), "cached-pass");
871        assert_eq!(call_count.load(Ordering::SeqCst), 1);
872
873        // Second call with same prompt should return cached value, not calling inner
874        let result2 = cached.get_passphrase("prompt1");
875        assert!(result2.is_ok());
876        assert_eq!(*result2.unwrap(), "cached-pass");
877        assert_eq!(call_count.load(Ordering::SeqCst), 1); // Still 1, cache hit
878    }
879
880    #[test]
881    fn test_cached_passphrase_provider_cache_miss() {
882        use std::sync::Arc;
883        use std::sync::atomic::{AtomicUsize, Ordering};
884        use std::time::Duration;
885
886        let call_count = Arc::new(AtomicUsize::new(0));
887        let call_count_clone = Arc::clone(&call_count);
888
889        let inner = Arc::new(CallbackPassphraseProvider::new(move |_prompt| {
890            call_count_clone.fetch_add(1, Ordering::SeqCst);
891            Ok(Zeroizing::new("pass".to_string()))
892        }));
893
894        let cached = CachedPassphraseProvider::new(inner, Duration::from_secs(60));
895
896        // Different prompts should each call inner
897        let _ = cached.get_passphrase("prompt1");
898        assert_eq!(call_count.load(Ordering::SeqCst), 1);
899
900        let _ = cached.get_passphrase("prompt2");
901        assert_eq!(call_count.load(Ordering::SeqCst), 2);
902
903        let _ = cached.get_passphrase("prompt3");
904        assert_eq!(call_count.load(Ordering::SeqCst), 3);
905    }
906
907    #[test]
908    fn test_cached_passphrase_provider_expiry() {
909        use std::sync::Arc;
910        use std::sync::atomic::{AtomicUsize, Ordering};
911        use std::time::Duration;
912
913        let call_count = Arc::new(AtomicUsize::new(0));
914        let call_count_clone = Arc::clone(&call_count);
915
916        let inner = Arc::new(CallbackPassphraseProvider::new(move |_prompt| {
917            call_count_clone.fetch_add(1, Ordering::SeqCst);
918            Ok(Zeroizing::new("pass".to_string()))
919        }));
920
921        // Very short TTL for testing expiry
922        let cached = CachedPassphraseProvider::new(inner, Duration::from_millis(10));
923
924        // First call
925        let _ = cached.get_passphrase("prompt");
926        assert_eq!(call_count.load(Ordering::SeqCst), 1);
927
928        // Wait for TTL to expire
929        std::thread::sleep(Duration::from_millis(20));
930
931        // This should re-fetch from inner since cache expired
932        let _ = cached.get_passphrase("prompt");
933        assert_eq!(call_count.load(Ordering::SeqCst), 2);
934    }
935
936    #[test]
937    fn test_cached_passphrase_provider_clear_cache() {
938        use std::sync::Arc;
939        use std::sync::atomic::{AtomicUsize, Ordering};
940        use std::time::Duration;
941
942        let call_count = Arc::new(AtomicUsize::new(0));
943        let call_count_clone = Arc::clone(&call_count);
944
945        let inner = Arc::new(CallbackPassphraseProvider::new(move |_prompt| {
946            call_count_clone.fetch_add(1, Ordering::SeqCst);
947            Ok(Zeroizing::new("pass".to_string()))
948        }));
949
950        let cached = CachedPassphraseProvider::new(inner, Duration::from_secs(60));
951
952        // First call
953        let _ = cached.get_passphrase("prompt");
954        assert_eq!(call_count.load(Ordering::SeqCst), 1);
955
956        // Second call should be cache hit
957        let _ = cached.get_passphrase("prompt");
958        assert_eq!(call_count.load(Ordering::SeqCst), 1);
959
960        // Clear cache
961        cached.clear_cache();
962
963        // Now should call inner again
964        let _ = cached.get_passphrase("prompt");
965        assert_eq!(call_count.load(Ordering::SeqCst), 2);
966    }
967
968    #[test]
969    fn test_prefilled_passphrase_provider_returns_stored_value() {
970        let provider = PrefilledPassphraseProvider::new("my-secret");
971        let result = provider.get_passphrase("any prompt").unwrap();
972        assert_eq!(*result, "my-secret");
973
974        let result2 = provider.get_passphrase("different prompt").unwrap();
975        assert_eq!(*result2, "my-secret");
976    }
977
978    #[test]
979    fn test_prefilled_passphrase_provider_empty_passphrase() {
980        let provider = PrefilledPassphraseProvider::new("");
981        let result = provider.get_passphrase("prompt").unwrap();
982        assert_eq!(*result, "");
983    }
984
985    #[test]
986    fn test_unified_passphrase_provider_prompts_once_for_multiple_keys() {
987        use std::sync::atomic::{AtomicUsize, Ordering};
988
989        let call_count = Arc::new(AtomicUsize::new(0));
990        let count_clone = call_count.clone();
991        let inner = CallbackPassphraseProvider::new(move |_prompt: &str| {
992            count_clone.fetch_add(1, Ordering::SeqCst);
993            Ok(Zeroizing::new("secret".to_string()))
994        });
995
996        let provider = UnifiedPassphraseProvider::new(Arc::new(inner));
997
998        // Different prompt messages → should still only hit inner once
999        let p1 = provider
1000            .get_passphrase("Enter passphrase for DEVICE key 'dev':")
1001            .unwrap();
1002        let p2 = provider
1003            .get_passphrase("Enter passphrase for IDENTITY key 'id':")
1004            .unwrap();
1005
1006        assert_eq!(*p1, "secret");
1007        assert_eq!(*p2, "secret");
1008        assert_eq!(call_count.load(Ordering::SeqCst), 1); // inner called exactly once
1009    }
1010}