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 #[allow(clippy::disallowed_methods)]
821 let identity_did = IdentityDID::new_unchecked("did:keri:ABC123");
823 let alias = KeyAlias::new_unchecked("test-key-alias");
824
825 let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("Failed to encrypt");
827
828 let storage = MockKeyStorage::new();
830 storage
831 .store_key(&alias, &identity_did, KeyRole::Primary, &encrypted)
832 .expect("Failed to store key");
833
834 let signer = StorageSigner::new(storage);
836 let passphrase_provider = MockPassphraseProvider::new(passphrase);
837
838 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 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 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 let (pkcs8_bytes, pubkey_bytes) = generate_test_keypair();
868 let passphrase = "Test-P@ss12345";
869 #[allow(clippy::disallowed_methods)]
870 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 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 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 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 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 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 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 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); }
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 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 let cached = CachedPassphraseProvider::new(inner, Duration::from_millis(10));
1003
1004 let _ = cached.get_passphrase("prompt");
1006 assert_eq!(call_count.load(Ordering::SeqCst), 1);
1007
1008 std::thread::sleep(Duration::from_millis(20));
1010
1011 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 let _ = cached.get_passphrase("prompt");
1034 assert_eq!(call_count.load(Ordering::SeqCst), 1);
1035
1036 let _ = cached.get_passphrase("prompt");
1038 assert_eq!(call_count.load(Ordering::SeqCst), 1);
1039
1040 cached.clear_cache();
1042
1043 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 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); }
1090}