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, CredentialStoreError>; fn save(&self, service: &str, account: &str, value: &str) -> Result<(), CredentialStoreError>; fn delete(&self, service: &str, account: &str) -> Result; } #[derive(Debug)] pub struct DefaultKeyringStore; impl KeyringStore for DefaultKeyringStore { fn load(&self, service: &str, account: &str) -> Result, 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 { 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>>>, } impl MockKeyringStore { pub fn credential(&self, account: &str) -> Arc { 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 { 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, 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 { 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) } } }