keyring_core/sample/
store.rs

1use std::any::Any;
2use std::collections::HashMap;
3use std::sync::{Arc, RwLock, Weak};
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use dashmap::DashMap;
7use log::{debug, error};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11use super::credential::{CredId, CredKey};
12use crate::{
13    Entry,
14    Error::{Invalid, PlatformFailure},
15    Result,
16    api::{CredentialPersistence, CredentialStoreApi},
17    attributes::parse_attributes,
18};
19
20/// The stored data for a credential
21#[derive(Debug, Serialize, Deserialize)]
22pub struct CredValue {
23    pub secret: Vec<u8>,
24    pub comment: Option<String>,
25    pub creation_date: Option<String>,
26}
27
28impl CredValue {
29    pub fn new(secret: &[u8]) -> Self {
30        CredValue {
31            secret: secret.to_vec(),
32            comment: None,
33            creation_date: None,
34        }
35    }
36
37    pub fn new_ambiguous(comment: &str) -> CredValue {
38        CredValue {
39            secret: vec![],
40            comment: Some(comment.to_string()),
41            creation_date: Some(chrono::Local::now().to_rfc2822()),
42        }
43    }
44}
45
46/// A map from <service, user> pairs to matching credentials
47pub type CredMap = DashMap<CredId, DashMap<String, CredValue>>;
48
49/// A Store's mutable weak reference to itself
50///
51/// Because credentials contain an `Arc` to their store,
52/// the store needs to keep a `Weak` to itself which can be
53/// upgraded to create the credential. Because
54/// the Store has to be created and an `Arc` of it taken
55/// before that `Arc` can be downgraded and stored inside
56/// the Store, the self-reference must be mutable.
57pub struct SelfRef {
58    inner_store: Weak<Store>,
59}
60
61/// A credential store.
62///
63/// The credential data is kept in the CredMap. We keep the index of
64/// ourself in the STORES vector, so we can get a pointer to ourself
65/// whenever we need to build a credential.
66pub struct Store {
67    pub id: String,
68    pub creds: CredMap,
69    pub backing: Option<String>, // the backing file, if any
70    pub self_ref: RwLock<SelfRef>,
71}
72
73impl std::fmt::Debug for Store {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.debug_struct("Store")
76            .field("vendor", &self.vendor())
77            .field("id", &self.id)
78            .field("backing", &self.backing)
79            .field("cred-count", &self.creds.len())
80            .finish()
81    }
82}
83
84impl Drop for Store {
85    fn drop(&mut self) {
86        if self.backing.is_none() {
87            debug!("dropping store {self:?}")
88        } else {
89            debug!("Saving store {self:?} on drop...");
90            match self.save() {
91                Ok(_) => debug!("Save of store {self:?} completed"),
92                Err(e) => error!("Save of store {self:?} failed: {e:?}"),
93            }
94        }
95    }
96}
97
98impl Store {
99    /// Create a new store with a default configuration.
100    ///
101    /// The default configuration is empty with no backing file.
102    pub fn new() -> Result<Arc<Self>> {
103        Ok(Self::new_internal(DashMap::new(), None))
104    }
105
106    /// Create a new store with a user-specified configuration.
107    ///
108    /// The only allowed configuration option is the path to the backing file,
109    /// which should be the value of the `backing_file` key in the config map.
110    /// See [new_with_backing](Store::new_with_backing) for details.
111    pub fn new_with_configuration(config: &HashMap<&str, &str>) -> Result<Arc<Self>> {
112        match parse_attributes(&["backing-file"], Some(config))?.get("backing-file") {
113            Some(path) => Self::new_with_backing(path),
114            None => Self::new(),
115        }
116    }
117
118    /// Create a new store from a backing file.
119    ///
120    /// The backing file must be a valid path, but it need not exist,
121    /// in which case the store starts off empty. If the file does
122    /// exist, the initial contents of the store are loaded from it.
123    pub fn new_with_backing(path: &str) -> Result<Arc<Self>> {
124        Ok(Self::new_internal(
125            Self::load_credentials(path)?,
126            Some(String::from(path)),
127        ))
128    }
129
130    /// Save this store to its backing file.
131    ///
132    /// This is a no-op if there is no backing file.
133    ///
134    /// Stores will save themselves to their backing file
135    /// when they go out of scope (i.e., are dropped),
136    /// but this call can be very useful if you specify
137    /// an instance of your store as the keyring-core
138    /// API default store, because the default store
139    /// is kept in a static variable
140    /// and thus is *never* dropped.
141    pub fn save(&self) -> Result<()> {
142        if self.backing.is_none() {
143            return Ok(());
144        };
145        let content = ron::ser::to_string_pretty(&self.creds, ron::ser::PrettyConfig::new())
146            .map_err(|e| PlatformFailure(Box::from(e)))?;
147        std::fs::write(self.backing.as_ref().unwrap(), content)
148            .map_err(|e| PlatformFailure(Box::from(e)))?;
149        Ok(())
150    }
151
152    /// Create a store with the given credentials and backing file.
153    pub fn new_internal(creds: CredMap, backing: Option<String>) -> Arc<Self> {
154        let store = Store {
155            id: format!(
156                "Crate version {}, Instantiated at {}",
157                env!("CARGO_PKG_VERSION"),
158                SystemTime::now()
159                    .duration_since(UNIX_EPOCH)
160                    .unwrap_or_else(|_| Duration::new(0, 0))
161                    .as_secs_f64()
162            ),
163            creds,
164            backing,
165            self_ref: RwLock::new(SelfRef {
166                inner_store: Weak::new(),
167            }),
168        };
169        debug!("Created new store: {store:?}");
170        let result = Arc::new(store);
171        result.set_store(result.clone());
172        result
173    }
174
175    /// Loads store content from a backing file.
176    ///
177    /// If the backing file does not exist, the returned store is empty.
178    pub fn load_credentials(path: &str) -> Result<CredMap> {
179        match std::fs::exists(path) {
180            Ok(true) => match std::fs::read_to_string(path) {
181                Ok(s) => Ok(ron::de::from_str(&s).map_err(|e| PlatformFailure(Box::from(e)))?),
182                Err(e) => Err(PlatformFailure(Box::from(e))),
183            },
184            Ok(false) => Ok(DashMap::new()),
185            Err(e) => Err(Invalid("Invalid path".to_string(), e.to_string())),
186        }
187    }
188
189    fn get_store(&self) -> Arc<Store> {
190        self.self_ref
191            .read()
192            .expect("RwLock bug at get!")
193            .inner_store
194            .upgrade()
195            .expect("Arc bug at get!")
196    }
197
198    fn set_store(&self, store: Arc<Store>) {
199        let mut guard = self.self_ref.write().expect("RwLock bug at set!");
200        guard.inner_store = Arc::downgrade(&store);
201    }
202}
203
204impl CredentialStoreApi for Store {
205    /// See the API docs.
206    fn vendor(&self) -> String {
207        String::from("Sample store, https://crates.io/crates/keyring-core")
208    }
209
210    /// See the API docs.
211    ///
212    /// The store ID is based on its sequence number
213    /// in the list of created stores.
214    fn id(&self) -> String {
215        self.id.clone()
216    }
217
218    /// See the API docs.
219    ///
220    /// The only modifier you can specify is `force-create`, which forces
221    /// immediate credential creation and can be used to create ambiguity.
222    ///
223    /// When the force-create modifier is specified, the created credential gets
224    /// an empty password/secret, a `comment` attribute with the value of the modifier,
225    /// and a `creation_`date` attribute with a string for the current local time.
226    fn build(
227        &self,
228        service: &str,
229        user: &str,
230        mods: Option<&HashMap<&str, &str>>,
231    ) -> Result<Entry> {
232        let id = CredId {
233            service: service.to_owned(),
234            user: user.to_owned(),
235        };
236        let key = CredKey {
237            store: self.get_store(),
238            id: id.clone(),
239            uuid: None,
240        };
241        if let Some(force_create) = parse_attributes(&["force-create"], mods)?.get("force-create") {
242            let uuid = Uuid::new_v4().to_string();
243            let value = CredValue::new_ambiguous(force_create);
244            match self.creds.get(&id) {
245                None => {
246                    let creds = DashMap::new();
247                    creds.insert(uuid, value);
248                    self.creds.insert(id, creds);
249                }
250                Some(creds) => {
251                    creds.value().insert(uuid, value);
252                }
253            };
254        }
255        Ok(Entry {
256            inner: Arc::new(key),
257        })
258    }
259
260    /// See the API docs.
261    ///
262    /// The specification must contain exactly two keys - `service` and `user` -
263    /// and their values must be valid regular expressions.
264    /// Every credential whose service name matches the service regex
265    /// _and_ whose username matches the user regex will be returned.
266    /// (The match is a substring match, so the empty string will match every value.)
267    fn search(&self, spec: &HashMap<&str, &str>) -> Result<Vec<Entry>> {
268        let mut result: Vec<Entry> = Vec::new();
269        let svc = regex::Regex::new(spec.get("service").unwrap_or(&""))
270            .map_err(|e| Invalid("service regex".to_string(), e.to_string()))?;
271        let usr = regex::Regex::new(spec.get("user").unwrap_or(&""))
272            .map_err(|e| Invalid("user regex".to_string(), e.to_string()))?;
273        let comment = regex::Regex::new(spec.get("uuid").unwrap_or(&""))
274            .map_err(|e| Invalid("comment regex".to_string(), e.to_string()))?;
275        let uuid = regex::Regex::new(spec.get("uuid").unwrap_or(&""))
276            .map_err(|e| Invalid("uuid regex".to_string(), e.to_string()))?;
277        let store = self.get_store();
278        for pair in self.creds.iter() {
279            if !svc.is_match(pair.key().service.as_str()) {
280                continue;
281            }
282            if !usr.is_match(pair.key().user.as_str()) {
283                continue;
284            }
285            for cred in pair.value().iter() {
286                if !uuid.is_match(cred.key()) {
287                    continue;
288                }
289                if spec.get("comment").is_some() {
290                    if cred.value().comment.is_none() {
291                        continue;
292                    }
293                    if !comment.is_match(cred.value().comment.as_ref().unwrap()) {
294                        continue;
295                    }
296                }
297                result.push(Entry {
298                    inner: Arc::new(CredKey {
299                        store: store.clone(),
300                        id: pair.key().clone(),
301                        uuid: Some(cred.key().clone()),
302                    }),
303                })
304            }
305        }
306        Ok(result)
307    }
308
309    //// See the API docs.
310    fn as_any(&self) -> &dyn Any {
311        self
312    }
313
314    //// See the API docs.
315    ////
316    //// If this store has a backing file, credential persistence is
317    //// `UntilDelete`. Otherwise, it's `ProcessOnly`.
318    fn persistence(&self) -> CredentialPersistence {
319        if self.backing.is_none() {
320            CredentialPersistence::ProcessOnly
321        } else {
322            CredentialPersistence::UntilDelete
323        }
324    }
325
326    /// See the API docs.
327    fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        std::fmt::Debug::fmt(self, f)
329    }
330}