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