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