notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

keyring_store.rs (5631B)


      1 use enostr::{Pubkey, SecretKey};
      2 use keyring::Entry;
      3 
      4 use crate::{Error, Result};
      5 
      6 const KEYRING_SERVICE_NAME: &str = "com.damus.notedeck";
      7 
      8 type BackendResult<T> = std::result::Result<T, keyring::Error>;
      9 
     10 #[derive(Clone, Debug)]
     11 enum KeyringBackendType {
     12     OS(OsKeyringBackend),
     13     #[cfg(test)]
     14     Memory(MemoryKeyringBackend),
     15 }
     16 
     17 impl KeyringBackendType {
     18     pub fn set(&self, service: &str, account: &str, secret: &str) -> BackendResult<()> {
     19         match self {
     20             KeyringBackendType::OS(os_keyring_backend) => {
     21                 os_keyring_backend.set(service, account, secret)
     22             }
     23             #[cfg(test)]
     24             KeyringBackendType::Memory(mem) => mem.set(service, account, secret),
     25         }
     26     }
     27 
     28     pub fn get(&self, service: &str, account: &str) -> BackendResult<Option<String>> {
     29         match self {
     30             KeyringBackendType::OS(os_keyring_backend) => os_keyring_backend.get(service, account),
     31             #[cfg(test)]
     32             KeyringBackendType::Memory(memory_keyring_backend) => {
     33                 memory_keyring_backend.get(service, account)
     34             }
     35         }
     36     }
     37 
     38     pub fn delete(&self, service: &str, account: &str) -> BackendResult<()> {
     39         match self {
     40             KeyringBackendType::OS(os_keyring_backend) => {
     41                 os_keyring_backend.delete(service, account)
     42             }
     43             #[cfg(test)]
     44             KeyringBackendType::Memory(memory_keyring_backend) => {
     45                 memory_keyring_backend.delete(service, account)
     46             }
     47         }
     48     }
     49 }
     50 
     51 #[derive(Clone, Debug)]
     52 struct OsKeyringBackend;
     53 
     54 impl OsKeyringBackend {
     55     fn set(&self, service: &str, account: &str, secret: &str) -> BackendResult<()> {
     56         let entry = Entry::new(service, account)?;
     57         entry.set_password(secret)
     58     }
     59 
     60     fn get(&self, service: &str, account: &str) -> BackendResult<Option<String>> {
     61         let entry = Entry::new(service, account)?;
     62 
     63         match entry.get_password() {
     64             Ok(secret) => Ok(Some(secret)),
     65             Err(keyring::Error::NoEntry) => Ok(None),
     66             Err(err) => Err(err),
     67         }
     68     }
     69 
     70     fn delete(&self, service: &str, account: &str) -> BackendResult<()> {
     71         let entry = Entry::new(service, account)?;
     72 
     73         match entry.delete_credential() {
     74             Ok(_) => Ok(()),
     75             Err(keyring::Error::NoEntry) => Ok(()),
     76             Err(err) => Err(err),
     77         }
     78     }
     79 }
     80 
     81 #[derive(Clone, Debug)]
     82 pub struct KeyringStore {
     83     backend: KeyringBackendType,
     84 }
     85 
     86 impl KeyringStore {
     87     #[cfg(test)]
     88     pub fn in_memory() -> Self {
     89         Self {
     90             backend: KeyringBackendType::Memory(MemoryKeyringBackend::default()),
     91         }
     92     }
     93 
     94     pub fn store_secret(&self, pubkey: &Pubkey, secret: &SecretKey) -> Result<()> {
     95         let res = self
     96             .backend
     97             .set(
     98                 KEYRING_SERVICE_NAME,
     99                 &Self::account_id(pubkey),
    100                 &secret.to_secret_hex(),
    101             )
    102             .map_err(Error::from);
    103 
    104         tracing::trace!("Store secret result: {res:?}");
    105 
    106         res
    107     }
    108 
    109     pub fn get_secret(&self, pubkey: &Pubkey) -> Result<Option<SecretKey>> {
    110         let maybe_secret = self
    111             .backend
    112             .get(KEYRING_SERVICE_NAME, &Self::account_id(pubkey))
    113             .map_err(Error::from);
    114 
    115         let secret_hex = match maybe_secret {
    116             Ok(m_secret) => {
    117                 let Some(secret) = m_secret else {
    118                     tracing::trace!("Keyring gave us empty secret for {pubkey}");
    119                     return Ok(None);
    120                 };
    121                 tracing::trace!("Received an actual secret for {pubkey} successfully");
    122                 secret
    123             }
    124             Err(e) => {
    125                 tracing::trace!("Failed to retrieve secret for {pubkey}: {e}");
    126                 return Err(e);
    127             }
    128         };
    129 
    130         let secret_key = SecretKey::from_hex(secret_hex).map_err(|err| {
    131             Error::Generic(format!(
    132                 "invalid secret key from keyring for {}: {err}",
    133                 Self::account_id(pubkey)
    134             ))
    135         })?;
    136 
    137         Ok(Some(secret_key))
    138     }
    139 
    140     pub fn remove_secret(&self, pubkey: &Pubkey) -> Result<()> {
    141         self.backend
    142             .delete(KEYRING_SERVICE_NAME, &Self::account_id(pubkey))
    143             .map_err(Error::from)
    144     }
    145 
    146     fn account_id(pubkey: &Pubkey) -> String {
    147         pubkey.hex()
    148     }
    149 }
    150 
    151 impl Default for KeyringStore {
    152     fn default() -> Self {
    153         Self {
    154             backend: KeyringBackendType::OS(OsKeyringBackend),
    155         }
    156     }
    157 }
    158 
    159 #[cfg(test)]
    160 #[derive(Clone, Default, Debug)]
    161 struct MemoryKeyringBackend {
    162     // RwLock to not make the KeyringBackendType api mutable... it's only for testing so it's ok
    163     entries: std::sync::Arc<std::sync::RwLock<std::collections::HashMap<(String, String), String>>>,
    164 }
    165 
    166 #[cfg(test)]
    167 impl MemoryKeyringBackend {
    168     fn set(&self, service: &str, account: &str, secret: &str) -> BackendResult<()> {
    169         self.entries
    170             .write()
    171             .unwrap()
    172             .insert((service.to_owned(), account.to_owned()), secret.to_owned());
    173         Ok(())
    174     }
    175 
    176     fn get(&self, service: &str, account: &str) -> BackendResult<Option<String>> {
    177         Ok(self
    178             .entries
    179             .read()
    180             .unwrap()
    181             .get(&(service.to_owned(), account.to_owned()))
    182             .cloned())
    183     }
    184 
    185     fn delete(&self, service: &str, account: &str) -> BackendResult<()> {
    186         self.entries
    187             .write()
    188             .unwrap()
    189             .remove(&(service.to_owned(), account.to_owned()));
    190         Ok(())
    191     }
    192 }