use keyring::Entry;use keyring::Error as KeyringError;use std::error::Error;use std::fmt;use std::fmt::Debug;use tracing::trace;#[derive(Debug)]pub enum CredentialStoreError { Other(KeyringError),}impl CredentialStoreError { pub fn new(error: KeyringError) -> Self { Self::Other(error) } pub fn message(&self) -> String { match self { Self::Other(error) => error.to_string(), } } pub fn into_error(self) -> KeyringError { match self { Self::Other(error) => error, } }}impl fmt::Display for CredentialStoreError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Other(error) => write!(f, "{error}"), } }}impl Error for CredentialStoreError {}/// Shared credential store abstraction for keyring-backed implementations.pub trait KeyringStore: Debug + Send + Sync { fn load(&self, service: &str, account: &str) -> Result<Option<String>, CredentialStoreError>; fn save(&self, service: &str, account: &str, value: &str) -> Result<(), CredentialStoreError>; fn delete(&self, service: &str, account: &str) -> Result<bool, CredentialStoreError>;}#[derive(Debug, Clone, Copy)]pub struct DefaultKeyringStore;impl KeyringStore for DefaultKeyringStore { fn load(&self, service: &str, account: &str) -> Result<Option<String>, CredentialStoreError> { trace!("keyring.load start, service={service}, account={account}"); let entry = Entry::new(service, account).map_err(CredentialStoreError::new)?; match entry.get_password() { Ok(password) => { trace!("keyring.load success, service={service}, account={account}"); Ok(Some(password)) } Err(keyring::Error::NoEntry) => { trace!("keyring.load no entry, service={service}, account={account}"); Ok(None) } Err(error) => { trace!("keyring.load error, service={service}, account={account}, error={error}"); Err(CredentialStoreError::new(error)) } } } fn save(&self, service: &str, account: &str, value: &str) -> Result<(), CredentialStoreError> { trace!( "keyring.save start, service={service}, account={account}, value_len={}", value.len() ); let entry = Entry::new(service, account).map_err(CredentialStoreError::new)?; match entry.set_password(value) { Ok(()) => { trace!("keyring.save success, service={service}, account={account}"); Ok(()) } Err(error) => { trace!("keyring.save error, service={service}, account={account}, error={error}"); Err(CredentialStoreError::new(error)) } } } fn delete(&self, service: &str, account: &str) -> Result<bool, CredentialStoreError> { trace!("keyring.delete start, service={service}, account={account}"); let entry = Entry::new(service, account).map_err(CredentialStoreError::new)?; match entry.delete_credential() { Ok(()) => { trace!("keyring.delete success, service={service}, account={account}"); Ok(true) } Err(keyring::Error::NoEntry) => { trace!("keyring.delete no entry, service={service}, account={account}"); Ok(false) } Err(error) => { trace!("keyring.delete error, service={service}, account={account}, error={error}"); Err(CredentialStoreError::new(error)) } } }}pub mod tests { use super::CredentialStoreError; use super::KeyringStore; use keyring::Error as KeyringError; use keyring::credential::CredentialApi as _; use keyring::mock::MockCredential; use std::collections::HashMap; use std::sync::Arc; use std::sync::Mutex; use std::sync::PoisonError; #[derive(Default, Clone, Debug)] pub struct MockKeyringStore { credentials: Arc<Mutex<HashMap<String, Arc<MockCredential>>>>, } impl MockKeyringStore { pub fn credential(&self, account: &str) -> Arc<MockCredential> { let mut guard = self .credentials .lock() .unwrap_or_else(PoisonError::into_inner); guard .entry(account.to_string()) .or_insert_with(|| Arc::new(MockCredential::default())) .clone() } pub fn saved_value(&self, account: &str) -> Option<String> { let credential = { let guard = self .credentials .lock() .unwrap_or_else(PoisonError::into_inner); guard.get(account).cloned() }?; credential.get_password().ok() } pub fn set_error(&self, account: &str, error: KeyringError) { let credential = self.credential(account); credential.set_error(error); } pub fn contains(&self, account: &str) -> bool { let guard = self .credentials .lock() .unwrap_or_else(PoisonError::into_inner); guard.contains_key(account) } } impl KeyringStore for MockKeyringStore { fn load( &self, _service: &str, account: &str, ) -> Result<Option<String>, CredentialStoreError> { let credential = { let guard = self .credentials .lock() .unwrap_or_else(PoisonError::into_inner); guard.get(account).cloned() }; let Some(credential) = credential else { return Ok(None); }; match credential.get_password() { Ok(password) => Ok(Some(password)), Err(KeyringError::NoEntry) => Ok(None), Err(error) => Err(CredentialStoreError::new(error)), } } fn save( &self, _service: &str, account: &str, value: &str, ) -> Result<(), CredentialStoreError> { let credential = self.credential(account); credential .set_password(value) .map_err(CredentialStoreError::new) } fn delete(&self, _service: &str, account: &str) -> Result<bool, CredentialStoreError> { let credential = { let guard = self .credentials .lock() .unwrap_or_else(PoisonError::into_inner); guard.get(account).cloned() }; let Some(credential) = credential else { return Ok(false); }; let removed = match credential.delete_credential() { Ok(()) => Ok(true), Err(KeyringError::NoEntry) => Ok(false), Err(error) => Err(CredentialStoreError::new(error)), }?; let mut guard = self .credentials .lock() .unwrap_or_else(PoisonError::into_inner); guard.remove(account); Ok(removed) } }}