Skip to main content

auths_core/
signing.rs

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