1use 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
18type PassphraseCallback = dyn Fn(&str) -> Result<Zeroizing<String>, AgentError> + Send + Sync;
20
21#[derive(Debug, thiserror::Error)]
34#[non_exhaustive]
35pub enum DidResolverError {
36 #[error("Unsupported DID method: {0}")]
38 UnsupportedMethod(String),
39
40 #[error("Invalid did:key format: {0}")]
42 InvalidDidKey(String),
43
44 #[error("Invalid did:key format: {0}")]
46 InvalidDidKeyFormat(String),
47
48 #[error("did:key decoding failed: {0}")]
50 DidKeyDecodingFailed(String),
51
52 #[error("Invalid did:key multicodec prefix")]
54 InvalidDidKeyMulticodec,
55
56 #[error("Resolution error: {0}")]
58 Resolution(String),
59
60 #[error("Repository error: {0}")]
62 Repository(String),
63}
64
65#[derive(Debug, Clone)]
79pub enum ResolvedDid {
80 Key {
82 did: String,
84 public_key: Ed25519PublicKey,
86 },
87 Keri {
89 did: String,
91 public_key: Ed25519PublicKey,
93 sequence: u64,
95 can_rotate: bool,
97 },
98}
99
100impl ResolvedDid {
101 pub fn did(&self) -> &str {
103 match self {
104 ResolvedDid::Key { did, .. } | ResolvedDid::Keri { did, .. } => did,
105 }
106 }
107
108 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 pub fn is_key(&self) -> bool {
119 matches!(self, ResolvedDid::Key { .. })
120 }
121
122 pub fn is_keri(&self) -> bool {
124 matches!(self, ResolvedDid::Keri { .. })
125 }
126}
127
128pub trait DidResolver: Send + Sync {
153 fn resolve(&self, did: &str) -> Result<ResolvedDid, DidResolverError>;
155}
156
157pub trait PassphraseProvider: Send + Sync {
163 fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError>;
173
174 fn on_incorrect_passphrase(&self, _prompt_message: &str) {}
183}
184
185pub trait SecureSigner: Send + Sync {
188 fn sign_with_alias(
203 &self,
204 alias: &KeyAlias,
205 passphrase_provider: &dyn PassphraseProvider,
206 message: &[u8],
207 ) -> Result<Vec<u8>, AgentError>;
208
209 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
235pub struct StorageSigner<S: KeyStorage> {
240 storage: S,
242}
243
244impl<S: KeyStorage> StorageSigner<S> {
245 pub fn new(storage: S) -> Self {
247 Self { storage }
248 }
249
250 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 let aliases = self.storage.list_aliases_for_identity(identity_did)?;
304
305 let alias = aliases.first().ok_or(AgentError::KeyNotFound)?;
307
308 self.sign_with_alias(alias, passphrase_provider, message)
310 }
311}
312
313pub struct CallbackPassphraseProvider {
329 callback: Box<PassphraseCallback>,
330}
331
332impl CallbackPassphraseProvider {
333 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
353pub 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 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 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 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 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 if let Some((passphrase, cached_at)) = cache.get(prompt_message) {
432 if cached_at.elapsed() < self.ttl {
433 return Ok(passphrase.clone());
435 }
436 cache.remove(prompt_message);
438 }
439
440 drop(cache); let passphrase = self.inner.get_passphrase(prompt_message)?;
443
444 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
464pub 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 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)] 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)] 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
576pub struct PrefilledPassphraseProvider {
594 passphrase: Zeroizing<String>,
595}
596
597impl PrefilledPassphraseProvider {
598 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
620pub struct UnifiedPassphraseProvider {
625 inner: Arc<dyn PassphraseProvider + Send + Sync>,
626 cached: Mutex<Option<Zeroizing<String>>>,
627}
628
629impl UnifiedPassphraseProvider {
630 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 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 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 let identity_did = IdentityDID::new_unchecked("did:keri:ABC123");
788 let alias = KeyAlias::new_unchecked("test-key-alias");
789
790 let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("Failed to encrypt");
792
793 let storage = MockKeyStorage::new();
795 storage
796 .store_key(&alias, &identity_did, KeyRole::Primary, &encrypted)
797 .expect("Failed to store key");
798
799 let signer = StorageSigner::new(storage);
801 let passphrase_provider = MockPassphraseProvider::new(passphrase);
802
803 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 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 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 let (pkcs8_bytes, pubkey_bytes) = generate_test_keypair();
833 let passphrase = "Test-P@ss12345";
834 #[allow(clippy::disallowed_methods)]
835 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 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 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 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 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 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 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 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); }
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 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 let cached = CachedPassphraseProvider::new(inner, Duration::from_millis(10));
968
969 let _ = cached.get_passphrase("prompt");
971 assert_eq!(call_count.load(Ordering::SeqCst), 1);
972
973 std::thread::sleep(Duration::from_millis(20));
975
976 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 let _ = cached.get_passphrase("prompt");
999 assert_eq!(call_count.load(Ordering::SeqCst), 1);
1000
1001 let _ = cached.get_passphrase("prompt");
1003 assert_eq!(call_count.load(Ordering::SeqCst), 1);
1004
1005 cached.clear_cache();
1007
1008 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 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); }
1055}