1use 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
15type PassphraseCallback = dyn Fn(&str) -> Result<Zeroizing<String>, AgentError> + Send + Sync;
17
18#[derive(Debug, thiserror::Error)]
31#[non_exhaustive]
32pub enum DidResolverError {
33 #[error("Unsupported DID method: {0}")]
35 UnsupportedMethod(String),
36
37 #[error("Invalid did:key format: {0}")]
39 InvalidDidKey(String),
40
41 #[error("Invalid did:key format: {0}")]
43 InvalidDidKeyFormat(String),
44
45 #[error("did:key decoding failed: {0}")]
47 DidKeyDecodingFailed(String),
48
49 #[error("Invalid did:key multicodec prefix")]
51 InvalidDidKeyMulticodec,
52
53 #[error("Resolution error: {0}")]
55 Resolution(String),
56
57 #[error("Repository error: {0}")]
59 Repository(String),
60}
61
62#[derive(Debug, Clone)]
76pub enum ResolvedDid {
77 Key {
79 did: String,
81 public_key_bytes: Vec<u8>,
83 },
84 Keri {
86 did: String,
88 public_key_bytes: Vec<u8>,
90 curve: auths_crypto::CurveType,
92 sequence: u128,
94 can_rotate: bool,
96 },
97}
98
99impl ResolvedDid {
100 pub fn did(&self) -> &str {
102 match self {
103 ResolvedDid::Key { did, .. } | ResolvedDid::Keri { did, .. } => did,
104 }
105 }
106
107 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 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 pub fn is_key(&self) -> bool {
135 matches!(self, ResolvedDid::Key { .. })
136 }
137
138 pub fn is_keri(&self) -> bool {
140 matches!(self, ResolvedDid::Keri { .. })
141 }
142}
143
144pub trait DidResolver: Send + Sync {
169 fn resolve(&self, did: &str) -> Result<ResolvedDid, DidResolverError>;
171}
172
173pub trait PassphraseProvider: Send + Sync {
179 fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError>;
189
190 fn on_incorrect_passphrase(&self, _prompt_message: &str) {}
199}
200
201pub trait SecureSigner: Send + Sync {
204 fn sign_with_alias(
219 &self,
220 alias: &KeyAlias,
221 passphrase_provider: &dyn PassphraseProvider,
222 message: &[u8],
223 ) -> Result<Vec<u8>, AgentError>;
224
225 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
251pub struct StorageSigner<S: KeyStorage> {
256 storage: S,
258}
259
260impl<S: KeyStorage> StorageSigner<S> {
261 pub fn new(storage: S) -> Self {
263 Self { storage }
264 }
265
266 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 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 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 let aliases = self.storage.list_aliases_for_identity(identity_did)?;
339
340 let alias = aliases.first().ok_or(AgentError::KeyNotFound)?;
342
343 self.sign_with_alias(alias, passphrase_provider, message)
345 }
346}
347
348pub struct CallbackPassphraseProvider {
364 callback: Box<PassphraseCallback>,
365}
366
367impl CallbackPassphraseProvider {
368 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
388pub 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 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 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 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 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 if let Some((passphrase, cached_at)) = cache.get(prompt_message) {
467 if cached_at.elapsed() < self.ttl {
468 return Ok(passphrase.clone());
470 }
471 cache.remove(prompt_message);
473 }
474
475 drop(cache); let passphrase = self.inner.get_passphrase(prompt_message)?;
478
479 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
499pub 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 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)] 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)] 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
611pub struct PrefilledPassphraseProvider {
629 passphrase: Zeroizing<String>,
630}
631
632impl PrefilledPassphraseProvider {
633 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
655pub struct UnifiedPassphraseProvider {
660 inner: Arc<dyn PassphraseProvider + Send + Sync>,
661 cached: Mutex<Option<Zeroizing<String>>>,
662}
663
664impl UnifiedPassphraseProvider {
665 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 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 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 let identity_did = IdentityDID::parse("did:keri:ABC123").unwrap();
821 let alias = KeyAlias::new_unchecked("test-key-alias");
822
823 let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("Failed to encrypt");
825
826 let storage = MockKeyStorage::new();
828 storage
829 .store_key(&alias, &identity_did, KeyRole::Primary, &encrypted)
830 .expect("Failed to store key");
831
832 let signer = StorageSigner::new(storage);
834 let passphrase_provider = MockPassphraseProvider::new(passphrase);
835
836 let message = b"test message for sign_for_identity";
838 let signature = signer
839 .sign_for_identity(&identity_did, &passphrase_provider, message)
840 .expect("Signing failed");
841
842 let public_key = UnparsedPublicKey::new(&ED25519, &pubkey_bytes);
844 assert!(public_key.verify(message, &signature).is_ok());
845 }
846
847 #[test]
848 fn test_sign_for_identity_no_key_for_identity() {
849 let storage = MockKeyStorage::new();
850 let signer = StorageSigner::new(storage);
851 let passphrase_provider = MockPassphraseProvider::new("any-passphrase");
852
853 let identity_did = IdentityDID::parse("did:keri:NONEXISTENT").unwrap();
854 let message = b"test message";
855
856 let result = signer.sign_for_identity(&identity_did, &passphrase_provider, message);
857 assert!(matches!(result, Err(AgentError::KeyNotFound)));
858 }
859
860 #[test]
861 fn test_sign_for_identity_multiple_aliases() {
862 let (pkcs8_bytes, pubkey_bytes) = generate_test_keypair();
864 let passphrase = "Test-P@ss12345";
865 let identity_did = IdentityDID::parse("did:keri:MULTI123").unwrap();
866
867 let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("Failed to encrypt");
868
869 let storage = MockKeyStorage::new();
870 let alias = KeyAlias::new_unchecked("primary-alias");
872 storage
873 .store_key(&alias, &identity_did, KeyRole::Primary, &encrypted)
874 .expect("Failed to store key");
875
876 let signer = StorageSigner::new(storage);
877 let passphrase_provider = MockPassphraseProvider::new(passphrase);
878
879 let message = b"test message with multiple aliases";
880 let signature = signer
881 .sign_for_identity(&identity_did, &passphrase_provider, message)
882 .expect("Signing should succeed");
883
884 let public_key = UnparsedPublicKey::new(&ED25519, &pubkey_bytes);
886 assert!(public_key.verify(message, &signature).is_ok());
887 }
888
889 #[test]
890 fn test_callback_passphrase_provider() {
891 use std::sync::Arc;
892 use std::sync::atomic::{AtomicUsize, Ordering};
893
894 let call_count = Arc::new(AtomicUsize::new(0));
896 let call_count_clone = Arc::clone(&call_count);
897
898 let provider = CallbackPassphraseProvider::new(move |prompt| {
899 call_count_clone.fetch_add(1, Ordering::SeqCst);
900 assert!(prompt.contains("test-alias"));
901 Ok(Zeroizing::new("callback-passphrase".to_string()))
902 });
903
904 let result = provider.get_passphrase("Enter passphrase for test-alias:");
906 assert!(result.is_ok());
907 assert_eq!(*result.unwrap(), "callback-passphrase");
908 assert_eq!(call_count.load(Ordering::SeqCst), 1);
909
910 let result2 = provider.get_passphrase("Another prompt for test-alias");
912 assert!(result2.is_ok());
913 assert_eq!(call_count.load(Ordering::SeqCst), 2);
914 }
915
916 #[test]
917 fn test_callback_passphrase_provider_error() {
918 let provider =
919 CallbackPassphraseProvider::new(|_prompt| Err(AgentError::UserInputCancelled));
920
921 let result = provider.get_passphrase("Enter passphrase:");
922 assert!(matches!(result, Err(AgentError::UserInputCancelled)));
923 }
924
925 #[test]
926 fn test_cached_passphrase_provider_cache_hit() {
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("cached-pass".to_string()))
937 }));
938
939 let cached = CachedPassphraseProvider::new(inner, Duration::from_secs(60));
940
941 let result1 = cached.get_passphrase("prompt1");
943 assert!(result1.is_ok());
944 assert_eq!(*result1.unwrap(), "cached-pass");
945 assert_eq!(call_count.load(Ordering::SeqCst), 1);
946
947 let result2 = cached.get_passphrase("prompt1");
949 assert!(result2.is_ok());
950 assert_eq!(*result2.unwrap(), "cached-pass");
951 assert_eq!(call_count.load(Ordering::SeqCst), 1); }
953
954 #[test]
955 fn test_cached_passphrase_provider_cache_miss() {
956 use std::sync::Arc;
957 use std::sync::atomic::{AtomicUsize, Ordering};
958 use std::time::Duration;
959
960 let call_count = Arc::new(AtomicUsize::new(0));
961 let call_count_clone = Arc::clone(&call_count);
962
963 let inner = Arc::new(CallbackPassphraseProvider::new(move |_prompt| {
964 call_count_clone.fetch_add(1, Ordering::SeqCst);
965 Ok(Zeroizing::new("pass".to_string()))
966 }));
967
968 let cached = CachedPassphraseProvider::new(inner, Duration::from_secs(60));
969
970 let _ = cached.get_passphrase("prompt1");
972 assert_eq!(call_count.load(Ordering::SeqCst), 1);
973
974 let _ = cached.get_passphrase("prompt2");
975 assert_eq!(call_count.load(Ordering::SeqCst), 2);
976
977 let _ = cached.get_passphrase("prompt3");
978 assert_eq!(call_count.load(Ordering::SeqCst), 3);
979 }
980
981 #[test]
982 fn test_cached_passphrase_provider_expiry() {
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_millis(10));
997
998 let _ = cached.get_passphrase("prompt");
1000 assert_eq!(call_count.load(Ordering::SeqCst), 1);
1001
1002 std::thread::sleep(Duration::from_millis(20));
1004
1005 let _ = cached.get_passphrase("prompt");
1007 assert_eq!(call_count.load(Ordering::SeqCst), 2);
1008 }
1009
1010 #[test]
1011 fn test_cached_passphrase_provider_clear_cache() {
1012 use std::sync::Arc;
1013 use std::sync::atomic::{AtomicUsize, Ordering};
1014 use std::time::Duration;
1015
1016 let call_count = Arc::new(AtomicUsize::new(0));
1017 let call_count_clone = Arc::clone(&call_count);
1018
1019 let inner = Arc::new(CallbackPassphraseProvider::new(move |_prompt| {
1020 call_count_clone.fetch_add(1, Ordering::SeqCst);
1021 Ok(Zeroizing::new("pass".to_string()))
1022 }));
1023
1024 let cached = CachedPassphraseProvider::new(inner, Duration::from_secs(60));
1025
1026 let _ = cached.get_passphrase("prompt");
1028 assert_eq!(call_count.load(Ordering::SeqCst), 1);
1029
1030 let _ = cached.get_passphrase("prompt");
1032 assert_eq!(call_count.load(Ordering::SeqCst), 1);
1033
1034 cached.clear_cache();
1036
1037 let _ = cached.get_passphrase("prompt");
1039 assert_eq!(call_count.load(Ordering::SeqCst), 2);
1040 }
1041
1042 #[test]
1043 fn test_prefilled_passphrase_provider_returns_stored_value() {
1044 let provider = PrefilledPassphraseProvider::new("my-secret");
1045 let result = provider.get_passphrase("any prompt").unwrap();
1046 assert_eq!(*result, "my-secret");
1047
1048 let result2 = provider.get_passphrase("different prompt").unwrap();
1049 assert_eq!(*result2, "my-secret");
1050 }
1051
1052 #[test]
1053 fn test_prefilled_passphrase_provider_empty_passphrase() {
1054 let provider = PrefilledPassphraseProvider::new("");
1055 let result = provider.get_passphrase("prompt").unwrap();
1056 assert_eq!(*result, "");
1057 }
1058
1059 #[test]
1060 fn test_unified_passphrase_provider_prompts_once_for_multiple_keys() {
1061 use std::sync::atomic::{AtomicUsize, Ordering};
1062
1063 let call_count = Arc::new(AtomicUsize::new(0));
1064 let count_clone = call_count.clone();
1065 let inner = CallbackPassphraseProvider::new(move |_prompt: &str| {
1066 count_clone.fetch_add(1, Ordering::SeqCst);
1067 Ok(Zeroizing::new("secret".to_string()))
1068 });
1069
1070 let provider = UnifiedPassphraseProvider::new(Arc::new(inner));
1071
1072 let p1 = provider
1074 .get_passphrase("Enter passphrase for DEVICE key 'dev':")
1075 .unwrap();
1076 let p2 = provider
1077 .get_passphrase("Enter passphrase for IDENTITY key 'id':")
1078 .unwrap();
1079
1080 assert_eq!(*p1, "secret");
1081 assert_eq!(*p2, "secret");
1082 assert_eq!(call_count.load(Ordering::SeqCst), 1); }
1084}