Skip to main content

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 the current state of this store to its backing file.
140    ///
141    /// This is a no-op if there is no backing file.
142    ///
143    /// Yes, stores will save themselves to their backing file
144    /// when they go out of scope (i.e., are dropped).
145    /// But using this entry to take a snapshot can save
146    /// you from crashes or let you pass the store state
147    /// off to another process.
148    pub fn save(&self) -> Result<()> {
149        if self.backing.is_none() {
150            return Ok(());
151        };
152        let content = ron::ser::to_string_pretty(&self.creds, ron::ser::PrettyConfig::new())
153            .map_err(|e| PlatformFailure(Box::from(e)))?;
154        std::fs::write(self.backing.as_ref().unwrap(), content)
155            .map_err(|e| PlatformFailure(Box::from(e)))?;
156        Ok(())
157    }
158
159    /// Create a store with the given credentials and backing file.
160    pub fn new_internal(creds: CredMap, backing: Option<String>) -> Arc<Self> {
161        let store = Store {
162            id: format!(
163                "Crate version {}, Instantiated at {}",
164                env!("CARGO_PKG_VERSION"),
165                SystemTime::now()
166                    .duration_since(UNIX_EPOCH)
167                    .unwrap_or_else(|_| Duration::new(0, 0))
168                    .as_secs_f64()
169            ),
170            creds,
171            backing,
172            self_ref: RwLock::new(SelfRef {
173                inner_store: Weak::new(),
174            }),
175        };
176        debug!("Created new store: {store:?}");
177        let result = Arc::new(store);
178        result.set_store(result.clone());
179        result
180    }
181
182    /// Loads store content from a backing file.
183    ///
184    /// If the backing file does not exist, the returned store is empty.
185    pub fn load_credentials(path: &str) -> Result<CredMap> {
186        match std::fs::exists(path) {
187            Ok(true) => match std::fs::read_to_string(path) {
188                Ok(s) => Ok(ron::de::from_str(&s).map_err(|e| PlatformFailure(Box::from(e)))?),
189                Err(e) => Err(PlatformFailure(Box::from(e))),
190            },
191            Ok(false) => Ok(DashMap::new()),
192            Err(e) => Err(Invalid("Invalid path".to_string(), e.to_string())),
193        }
194    }
195
196    fn get_store(&self) -> Arc<Store> {
197        self.self_ref
198            .read()
199            .expect("RwLock bug at get!")
200            .inner_store
201            .upgrade()
202            .expect("Arc bug at get!")
203    }
204
205    fn set_store(&self, store: Arc<Store>) {
206        let mut guard = self.self_ref.write().expect("RwLock bug at set!");
207        guard.inner_store = Arc::downgrade(&store);
208    }
209}
210
211impl CredentialStoreApi for Store {
212    /// See the API docs.
213    fn vendor(&self) -> String {
214        String::from("Sample store, https://crates.io/crates/keyring-core")
215    }
216
217    /// See the API docs.
218    ///
219    /// The store ID is based on its sequence number
220    /// in the list of created stores.
221    fn id(&self) -> String {
222        self.id.clone()
223    }
224
225    /// See the API docs.
226    ///
227    /// The only modifier you can specify is `force-create`, which forces
228    /// immediate credential creation and can be used to create ambiguity.
229    ///
230    /// When the force-create modifier is specified, the created credential gets
231    /// an empty password/secret, a `comment` attribute with the value of the modifier,
232    /// and a `creation_`date` attribute with a string for the current local time.
233    fn build(
234        &self,
235        service: &str,
236        user: &str,
237        mods: Option<&HashMap<&str, &str>>,
238    ) -> Result<Entry> {
239        let id = CredId {
240            service: service.to_owned(),
241            user: user.to_owned(),
242        };
243        let key = CredKey {
244            store: self.get_store(),
245            id: id.clone(),
246            uuid: None,
247        };
248        if let Some(force_create) = parse_attributes(&["force-create"], mods)?.get("force-create") {
249            let uuid = Uuid::new_v4().to_string();
250            let value = CredValue::new_ambiguous(force_create);
251            match self.creds.get(&id) {
252                None => {
253                    let creds = DashMap::new();
254                    creds.insert(uuid, value);
255                    self.creds.insert(id, creds);
256                }
257                Some(creds) => {
258                    creds.value().insert(uuid, value);
259                }
260            };
261        }
262        Ok(Entry {
263            inner: Arc::new(key),
264        })
265    }
266
267    /// See the API docs.
268    ///
269    /// The specification must contain exactly two keys - `service` and `user` -
270    /// and their values must be valid regular expressions.
271    /// Every credential whose service name matches the service regex
272    /// _and_ whose username matches the user regex will be returned.
273    /// (The match is a substring match, so the empty string will match every value.)
274    fn search(&self, spec: &HashMap<&str, &str>) -> Result<Vec<Entry>> {
275        let spec = parse_attributes(&["service", "user", "uuid", "comment"], Some(spec))?;
276        let mut result: Vec<Entry> = Vec::new();
277        let empty = String::new();
278        let svc = regex::Regex::new(spec.get("service").unwrap_or(&empty))
279            .map_err(|e| Invalid("service regex".to_string(), e.to_string()))?;
280        let usr = regex::Regex::new(spec.get("user").unwrap_or(&empty))
281            .map_err(|e| Invalid("user regex".to_string(), e.to_string()))?;
282        let comment = regex::Regex::new(spec.get("comment").unwrap_or(&empty))
283            .map_err(|e| Invalid("comment regex".to_string(), e.to_string()))?;
284        let uuid = regex::Regex::new(spec.get("uuid").unwrap_or(&empty))
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.contains_key("comment") {
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}