@@ -7,9 +7,12 @@ use crate::crypto::signer::{decrypt_keypair, extract_seed_from_key_bytes};
77use crate :: error:: AgentError ;
88use crate :: storage:: keychain:: { IdentityDID , KeyAlias , KeyStorage } ;
99
10+ use crate :: config:: PassphraseCachePolicy ;
11+ use crate :: storage:: passphrase_cache:: PassphraseCache ;
12+
1013use std:: collections:: HashMap ;
1114use std:: sync:: { Arc , Mutex } ;
12- use std:: time:: { Duration , Instant } ;
15+ use std:: time:: { Duration , Instant , SystemTime , UNIX_EPOCH } ;
1316use zeroize:: Zeroizing ;
1417
1518/// Type alias for passphrase callback functions.
@@ -428,6 +431,116 @@ impl PassphraseProvider for CachedPassphraseProvider {
428431 }
429432}
430433
434+ /// A `PassphraseProvider` that wraps an inner provider with OS keychain caching.
435+ ///
436+ /// On `get_passphrase()`, checks the OS keychain first via `PassphraseCache::load`.
437+ /// If a cached value exists and hasn't expired per the configured policy/TTL,
438+ /// returns it immediately. Otherwise delegates to the inner provider, then
439+ /// stores the result in the OS keychain for subsequent invocations.
440+ ///
441+ /// Args:
442+ /// * `inner`: The underlying provider to prompt the user when cache misses.
443+ /// * `cache`: Platform keychain cache (macOS Security Framework, Linux Secret Service, etc.).
444+ /// * `alias`: Key alias used as the cache key in the OS keychain.
445+ /// * `policy`: The configured `PassphraseCachePolicy`.
446+ /// * `ttl_secs`: Optional TTL in seconds (for `Duration` policy).
447+ ///
448+ /// Usage:
449+ /// ```ignore
450+ /// use auths_core::signing::{KeychainPassphraseProvider, PassphraseProvider};
451+ /// use auths_core::config::PassphraseCachePolicy;
452+ /// use auths_core::storage::passphrase_cache::get_passphrase_cache;
453+ ///
454+ /// let inner = Arc::new(some_provider);
455+ /// let cache = get_passphrase_cache(true);
456+ /// let provider = KeychainPassphraseProvider::new(
457+ /// inner, cache, "main".to_string(),
458+ /// PassphraseCachePolicy::Duration, Some(3600),
459+ /// );
460+ /// let passphrase = provider.get_passphrase("Enter passphrase:")?;
461+ /// ```
462+ pub struct KeychainPassphraseProvider {
463+ inner : Arc < dyn PassphraseProvider + Send + Sync > ,
464+ cache : Box < dyn PassphraseCache > ,
465+ alias : String ,
466+ policy : PassphraseCachePolicy ,
467+ ttl_secs : Option < i64 > ,
468+ }
469+
470+ impl KeychainPassphraseProvider {
471+ /// Creates a new `KeychainPassphraseProvider`.
472+ ///
473+ /// Args:
474+ /// * `inner`: Fallback provider for cache misses.
475+ /// * `cache`: OS keychain cache implementation.
476+ /// * `alias`: Key alias used as the keychain entry identifier.
477+ /// * `policy`: Caching policy controlling storage/expiry behavior.
478+ /// * `ttl_secs`: TTL in seconds when `policy` is `Duration`.
479+ pub fn new (
480+ inner : Arc < dyn PassphraseProvider + Send + Sync > ,
481+ cache : Box < dyn PassphraseCache > ,
482+ alias : String ,
483+ policy : PassphraseCachePolicy ,
484+ ttl_secs : Option < i64 > ,
485+ ) -> Self {
486+ Self {
487+ inner,
488+ cache,
489+ alias,
490+ policy,
491+ ttl_secs,
492+ }
493+ }
494+
495+ fn is_expired ( & self , stored_at_unix : i64 ) -> bool {
496+ match self . policy {
497+ PassphraseCachePolicy :: Always => false ,
498+ PassphraseCachePolicy :: Never => true ,
499+ PassphraseCachePolicy :: Session => true ,
500+ PassphraseCachePolicy :: Duration => {
501+ let ttl = self . ttl_secs . unwrap_or ( 3600 ) ;
502+ let now = SystemTime :: now ( )
503+ . duration_since ( UNIX_EPOCH )
504+ . unwrap_or_default ( )
505+ . as_secs ( ) as i64 ;
506+ now - stored_at_unix > ttl
507+ }
508+ }
509+ }
510+ }
511+
512+ impl PassphraseProvider for KeychainPassphraseProvider {
513+ fn get_passphrase ( & self , prompt_message : & str ) -> Result < Zeroizing < String > , AgentError > {
514+ if self . policy != PassphraseCachePolicy :: Never
515+ && let Ok ( Some ( ( passphrase, stored_at) ) ) = self . cache . load ( & self . alias )
516+ {
517+ if !self . is_expired ( stored_at) {
518+ return Ok ( passphrase) ;
519+ }
520+ let _ = self . cache . delete ( & self . alias ) ;
521+ }
522+
523+ let passphrase = self . inner . get_passphrase ( prompt_message) ?;
524+
525+ if self . policy != PassphraseCachePolicy :: Never
526+ && self . policy != PassphraseCachePolicy :: Session
527+ {
528+ let now = SystemTime :: now ( )
529+ . duration_since ( UNIX_EPOCH )
530+ . unwrap_or_default ( )
531+ . as_secs ( ) as i64 ;
532+ let _ = self . cache . store ( & self . alias , & passphrase, now) ;
533+ }
534+
535+ Ok ( passphrase)
536+ }
537+
538+ fn on_incorrect_passphrase ( & self , prompt_message : & str ) {
539+ let _ = self . cache . delete ( & self . alias ) ;
540+ self . inner . on_incorrect_passphrase ( prompt_message) ;
541+ }
542+ }
543+
431544/// Provides a pre-collected passphrase for headless and automated environments.
432545///
433546/// Unlike [`CallbackPassphraseProvider`] which prompts interactively, this provider
0 commit comments