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