oo7/
keyring.rs

1use std::{collections::HashMap, sync::Arc, time::Duration};
2
3#[cfg(feature = "async-std")]
4use async_lock::RwLock;
5#[cfg(feature = "tokio")]
6use tokio::sync::RwLock;
7
8use crate::{dbus, file, AsAttributes, Result, Secret};
9
10/// A [Secret Service](crate::dbus) or [file](crate::file) backed keyring
11/// implementation.
12///
13/// It will automatically use the file backend if the application is sandboxed
14/// and otherwise falls back to the DBus service using it [default
15/// collection](crate::dbus::Service::default_collection).
16///
17/// The File backend requires a [`org.freedesktop.portal.Secret`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html) implementation
18/// to retrieve the key that will be used to encrypt the backend file.
19#[derive(Debug)]
20pub enum Keyring {
21    #[doc(hidden)]
22    File(Arc<file::Keyring>),
23    #[doc(hidden)]
24    DBus(dbus::Collection<'static>),
25}
26
27impl Keyring {
28    /// Create a new instance of the Keyring that automatically removes the
29    /// broken items from the file backend keyring.
30    ///
31    /// This method will probably be removed in future versions if the
32    /// misbehaviour is tracked and fixed.
33    #[deprecated = "The method is no longer useful as the user can fix the keyring using oo7-cli"]
34    pub async fn with_broken_item_cleanup() -> Result<Self> {
35        Self::new_inner(true).await
36    }
37
38    /// Create a new instance of the Keyring.
39    pub async fn new() -> Result<Self> {
40        Self::new_inner(false).await
41    }
42
43    async fn new_inner(auto_delete_broken_items: bool) -> Result<Self> {
44        let is_sandboxed = ashpd::is_sandboxed().await;
45        if is_sandboxed {
46            #[cfg(feature = "tracing")]
47            tracing::debug!("Application is sandboxed, using the file backend");
48
49            match file::Keyring::load_default().await {
50                Ok(file) => return Ok(Self::File(Arc::new(file))),
51                // Do nothing in this case, we are supposed to fallback to the host keyring
52                Err(super::file::Error::Portal(ashpd::Error::PortalNotFound(_))) => {
53                    #[cfg(feature = "tracing")]
54                    tracing::debug!(
55                        "org.freedesktop.portal.Secrets is not available, falling back to the Secret Service backend"
56                    );
57                }
58                Err(e) => {
59                    if matches!(e, file::Error::IncorrectSecret) && auto_delete_broken_items {
60                        let keyring = unsafe { file::Keyring::load_default_unchecked().await? };
61                        let deleted_items = keyring.delete_broken_items().await?;
62                        debug_assert!(deleted_items > 0);
63                        return Ok(Self::File(Arc::new(keyring)));
64                    }
65                    return Err(crate::Error::File(e));
66                }
67            };
68        } else {
69            #[cfg(feature = "tracing")]
70            tracing::debug!(
71                "Application is not sandboxed, falling back to the Secret Service backend"
72            );
73        }
74        let service = dbus::Service::new().await?;
75        let collection = service.default_collection().await?;
76        Ok(Self::DBus(collection))
77    }
78
79    /// Unlock the used collection if using the Secret service.
80    ///
81    /// The method does nothing if keyring is backed by a file backend.
82    pub async fn unlock(&self) -> Result<()> {
83        // No unlocking is needed for the file backend
84        if let Self::DBus(backend) = self {
85            backend.unlock(None).await?;
86        };
87        Ok(())
88    }
89
90    /// Lock the used collection if using the Secret service.
91    ///
92    /// The method does nothing if keyring is backed by a file backend.
93    pub async fn lock(&self) -> Result<()> {
94        // No locking is needed for the file backend
95        if let Self::DBus(backend) = self {
96            backend.lock(None).await?;
97        };
98        Ok(())
99    }
100
101    /// Remove items that matches the attributes.
102    pub async fn delete(&self, attributes: &impl AsAttributes) -> Result<()> {
103        match self {
104            Self::DBus(backend) => {
105                let items = backend.search_items(attributes).await?;
106                for item in items {
107                    item.delete(None).await?;
108                }
109            }
110            Self::File(backend) => {
111                backend.delete(attributes).await?;
112            }
113        };
114        Ok(())
115    }
116
117    /// Retrieve all the items.
118    pub async fn items(&self) -> Result<Vec<Item>> {
119        let items = match self {
120            Self::DBus(backend) => {
121                let items = backend.items().await?;
122                items.into_iter().map(Item::for_dbus).collect::<Vec<_>>()
123            }
124            Self::File(backend) => {
125                let items = backend.items().await?;
126                items
127                    .into_iter()
128                    // Ignore invalid items
129                    .flatten()
130                    .map(|i| Item::for_file(i, Arc::clone(backend)))
131                    .collect::<Vec<_>>()
132            }
133        };
134        Ok(items)
135    }
136
137    /// Create a new item.
138    pub async fn create_item(
139        &self,
140        label: &str,
141        attributes: &impl AsAttributes,
142        secret: impl Into<Secret>,
143        replace: bool,
144    ) -> Result<()> {
145        match self {
146            Self::DBus(backend) => {
147                backend
148                    .create_item(label, attributes, secret, replace, None)
149                    .await?;
150            }
151            Self::File(backend) => {
152                backend
153                    .create_item(label, attributes, secret, replace)
154                    .await?;
155            }
156        };
157        Ok(())
158    }
159
160    /// Find items based on their attributes.
161    pub async fn search_items(&self, attributes: &impl AsAttributes) -> Result<Vec<Item>> {
162        let items = match self {
163            Self::DBus(backend) => {
164                let items = backend.search_items(attributes).await?;
165                items.into_iter().map(Item::for_dbus).collect::<Vec<_>>()
166            }
167            Self::File(backend) => {
168                let items = backend.search_items(attributes).await?;
169                items
170                    .into_iter()
171                    .map(|i| Item::for_file(i, Arc::clone(backend)))
172                    .collect::<Vec<_>>()
173            }
174        };
175        Ok(items)
176    }
177
178    /// Get the inner file backend if the keyring is backed by one.
179    pub fn as_file(&self) -> Arc<file::Keyring> {
180        match self {
181            Self::File(keyring) => keyring.clone(),
182            _ => unreachable!(),
183        }
184    }
185
186    /// Get the inner DBus backend if the keyring is backed by one.
187    pub fn as_dbus(&self) -> &dbus::Collection {
188        match self {
189            Self::DBus(collection) => collection,
190            _ => unreachable!(),
191        }
192    }
193}
194
195/// A generic secret with a label and attributes.
196#[derive(Debug)]
197pub enum Item {
198    #[doc(hidden)]
199    File(RwLock<file::Item>, Arc<file::Keyring>),
200    #[doc(hidden)]
201    DBus(dbus::Item<'static>),
202}
203
204impl Item {
205    fn for_file(item: file::Item, backend: Arc<file::Keyring>) -> Self {
206        Self::File(RwLock::new(item), backend)
207    }
208
209    fn for_dbus(item: dbus::Item<'static>) -> Self {
210        Self::DBus(item)
211    }
212
213    /// The item label.
214    pub async fn label(&self) -> Result<String> {
215        let label = match self {
216            Self::File(item, _) => item.read().await.label().to_owned(),
217            Self::DBus(item) => item.label().await?,
218        };
219        Ok(label)
220    }
221
222    /// Sets the item label.
223    pub async fn set_label(&self, label: &str) -> Result<()> {
224        match self {
225            Self::File(item, backend) => {
226                item.write().await.set_label(label);
227
228                let item_guard = item.read().await;
229
230                backend
231                    .create_item(
232                        item_guard.label(),
233                        &item_guard.attributes(),
234                        item_guard.secret(),
235                        true,
236                    )
237                    .await?;
238            }
239            Self::DBus(item) => item.set_label(label).await?,
240        };
241        Ok(())
242    }
243
244    /// Retrieve the item attributes.
245    pub async fn attributes(&self) -> Result<HashMap<String, String>> {
246        let attributes = match self {
247            Self::File(item, _) => item
248                .read()
249                .await
250                .attributes()
251                .iter()
252                .map(|(k, v)| (k.to_owned(), v.to_string()))
253                .collect::<HashMap<_, _>>(),
254            Self::DBus(item) => item.attributes().await?,
255        };
256        Ok(attributes)
257    }
258
259    /// Sets the item attributes.
260    pub async fn set_attributes(&self, attributes: &impl AsAttributes) -> Result<()> {
261        match self {
262            Self::File(item, backend) => {
263                let index = backend
264                    .lookup_item_index(item.read().await.attributes())
265                    .await?;
266
267                item.write().await.set_attributes(attributes);
268                let item_guard = item.read().await;
269
270                if let Some(index) = index {
271                    backend.replace_item_index(index, &item_guard).await?;
272                } else {
273                    backend
274                        .create_item(item_guard.label(), attributes, item_guard.secret(), true)
275                        .await?;
276                }
277            }
278            Self::DBus(item) => item.set_attributes(attributes).await?,
279        };
280        Ok(())
281    }
282
283    /// Sets a new secret.
284    pub async fn set_secret(&self, secret: impl Into<Secret>) -> Result<()> {
285        match self {
286            Self::File(item, backend) => {
287                item.write().await.set_secret(secret);
288                let item_guard = item.read().await;
289
290                backend
291                    .create_item(
292                        item_guard.label(),
293                        &item_guard.attributes(),
294                        item_guard.secret(),
295                        true,
296                    )
297                    .await?;
298            }
299            Self::DBus(item) => item.set_secret(secret).await?,
300        };
301        Ok(())
302    }
303
304    /// Retrieves the stored secret.
305    pub async fn secret(&self) -> Result<Secret> {
306        let secret = match self {
307            Self::File(item, _) => item.read().await.secret(),
308            Self::DBus(item) => item.secret().await?,
309        };
310        Ok(secret)
311    }
312
313    /// Whether the item is locked or not
314    ///
315    /// The method always returns `false` if keyring is backed by a file
316    /// backend.
317    pub async fn is_locked(&self) -> Result<bool> {
318        if let Self::DBus(item) = self {
319            item.is_locked().await.map_err(From::from)
320        } else {
321            Ok(false)
322        }
323    }
324
325    /// Lock the item
326    ///
327    /// The method does nothing if keyring is backed by a file backend.
328    pub async fn lock(&self) -> Result<()> {
329        if let Self::DBus(item) = self {
330            item.lock(None).await?;
331        }
332        Ok(())
333    }
334
335    /// Unlock the item
336    ///
337    /// The method does nothing if keyring is backed by a file backend.
338    pub async fn unlock(&self) -> Result<()> {
339        if let Self::DBus(item) = self {
340            item.unlock(None).await?;
341        }
342        Ok(())
343    }
344
345    /// Delete the item.
346    pub async fn delete(&self) -> Result<()> {
347        match self {
348            Self::File(item, backend) => {
349                let item_guard = item.read().await;
350
351                backend.delete(&item_guard.attributes()).await?;
352            }
353            Self::DBus(item) => {
354                item.delete(None).await?;
355            }
356        };
357        Ok(())
358    }
359
360    /// The UNIX time when the item was created.
361    pub async fn created(&self) -> Result<Duration> {
362        match self {
363            Self::DBus(item) => Ok(item.created().await?),
364            Self::File(item, _) => Ok(item.read().await.created()),
365        }
366    }
367
368    /// The UNIX time when the item was modified.
369    pub async fn modified(&self) -> Result<Duration> {
370        match self {
371            Self::DBus(item) => Ok(item.modified().await?),
372            Self::File(item, _) => Ok(item.read().await.modified()),
373        }
374    }
375}
376
377#[cfg(test)]
378#[cfg(feature = "tokio")]
379mod tests {
380    use tempfile::tempdir;
381    use tokio::fs;
382
383    use super::*;
384
385    #[tokio::test]
386    async fn portal_set_attributes() -> Result<()> {
387        let data_dir = tempdir().unwrap();
388        let dir = data_dir.path().join("keyrings");
389        fs::create_dir_all(&dir).await.unwrap();
390        let path = dir.join("default.keyring");
391
392        let secret = crate::Secret::text("test");
393        let keyring = Keyring::File(file::Keyring::load(&path, secret).await?.into());
394
395        let items = keyring.items().await?;
396        assert_eq!(items.len(), 0);
397
398        keyring
399            .create_item("my item", &vec![("key", "value")], "my_secret", false)
400            .await?;
401
402        let mut items = keyring.items().await?;
403        assert_eq!(items.len(), 1);
404        let item = items.remove(0);
405        assert_eq!(item.label().await?, "my item");
406        assert_eq!(item.secret().await?, Secret::text("my_secret"));
407        let attrs = item.attributes().await?;
408        assert_eq!(attrs.len(), 2);
409        assert_eq!(attrs.get("key").unwrap(), "value");
410
411        item.set_attributes(&vec![("key", "changed_value"), ("new_key", "new_value")])
412            .await?;
413
414        let mut items = keyring.items().await?;
415        assert_eq!(items.len(), 1);
416        let item = items.remove(0);
417        assert_eq!(item.label().await?, "my item");
418        assert_eq!(item.secret().await?, Secret::text("my_secret"));
419        let attrs = item.attributes().await?;
420        assert_eq!(attrs.len(), 3);
421        assert_eq!(attrs.get("key").unwrap(), "changed_value");
422        assert_eq!(attrs.get("new_key").unwrap(), "new_value");
423
424        Ok(())
425    }
426}