Skip to main content

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::{AsAttributes, Result, Secret, dbus, file};
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<RwLock<Option<file::Keyring>>>, Secret),
23    #[doc(hidden)]
24    DBus(dbus::Collection),
25}
26
27impl Keyring {
28    /// Create a new instance of the Keyring.
29    ///
30    /// Auto-detects whether the application is sandboxed and uses the
31    /// appropriate backend (file backend for sandboxed apps, D-Bus service
32    /// for host apps). Falls back to D-Bus if the secret portal is not
33    /// available.
34    pub async fn new() -> Result<Self> {
35        if ashpd::is_sandboxed() {
36            match Self::sandboxed().await {
37                Ok(keyring) => Ok(keyring),
38                // Fallback to host keyring if portal is not available
39                Err(crate::Error::File(file::Error::Portal(ashpd::Error::PortalNotFound(_)))) => {
40                    #[cfg(feature = "tracing")]
41                    tracing::debug!(
42                        "org.freedesktop.portal.Secrets is not available, falling back to the Secret Service backend"
43                    );
44                    Self::host().await
45                }
46                Err(e) => Err(e),
47            }
48        } else {
49            Self::host().await
50        }
51    }
52
53    /// Use the file backend with secret portal (for sandboxed apps).
54    pub async fn sandboxed() -> Result<Self> {
55        #[cfg(feature = "tracing")]
56        tracing::debug!("Using file backend (sandboxed mode)");
57
58        let secret = Secret::sandboxed().await?;
59        let path = crate::file::api::Keyring::default_path()?;
60        Self::sandboxed_with_path(&path, secret).await
61    }
62
63    /// Use the file backend with a custom path.
64    ///
65    /// # Arguments
66    /// * `path` - Path to the keyring file
67    /// * `secret` - Secret to unlock the keyring (use `Secret::sandboxed()` or
68    ///   `Secret::random()` for tests)
69    pub async fn sandboxed_with_path(
70        path: impl AsRef<std::path::Path>,
71        secret: Secret,
72    ) -> Result<Self> {
73        #[cfg(feature = "tracing")]
74        tracing::debug!("Using file backend with custom path");
75
76        let file = file::UnlockedKeyring::load(path, secret.clone()).await?;
77        Ok(Self::File(
78            Arc::new(RwLock::new(Some(file::Keyring::Unlocked(file)))),
79            secret,
80        ))
81    }
82
83    /// Use the D-Bus Secret Service.
84    pub async fn host() -> Result<Self> {
85        #[cfg(feature = "tracing")]
86        tracing::debug!("Using D-Bus Secret Service (host mode)");
87
88        let service = dbus::Service::new().await?;
89        let collection = service.default_collection().await?;
90        Ok(Self::DBus(collection))
91    }
92
93    /// Use the D-Bus Secret Service with a custom connection.
94    pub async fn host_with_connection(connection: zbus::Connection) -> Result<Self> {
95        #[cfg(feature = "tracing")]
96        tracing::debug!("Using D-Bus Secret Service with custom connection (test mode)");
97
98        let service = dbus::Service::new_with_connection(&connection).await?;
99        let collection = service.default_collection().await?;
100        Ok(Self::DBus(collection))
101    }
102
103    /// Unlock the used collection.
104    pub async fn unlock(&self) -> Result<()> {
105        match self {
106            Self::DBus(backend) => backend.unlock(None).await?,
107            Self::File(keyring, secret) => {
108                let mut kg = keyring.write().await;
109                let kg_value = kg.take();
110                if let Some(file::Keyring::Locked(locked)) = kg_value {
111                    #[cfg(feature = "tracing")]
112                    tracing::debug!("Unlocking file backend keyring");
113
114                    let unlocked = locked
115                        .unlock(secret.clone())
116                        .await
117                        .map_err(crate::Error::File)?;
118                    *kg = Some(file::Keyring::Unlocked(unlocked));
119                } else {
120                    *kg = kg_value;
121                }
122            }
123        };
124        Ok(())
125    }
126
127    /// Lock the used collection.
128    pub async fn lock(&self) -> Result<()> {
129        match self {
130            Self::DBus(backend) => backend.lock(None).await?,
131            Self::File(keyring, _) => {
132                let mut kg = keyring.write().await;
133                let kg_value = kg.take();
134                if let Some(file::Keyring::Unlocked(unlocked)) = kg_value {
135                    #[cfg(feature = "tracing")]
136                    tracing::debug!("Locking file backend keyring");
137
138                    let locked = unlocked.lock();
139                    *kg = Some(file::Keyring::Locked(locked));
140                } else {
141                    *kg = kg_value;
142                }
143            }
144        };
145        Ok(())
146    }
147
148    /// Whether the keyring is locked or not.
149    pub async fn is_locked(&self) -> Result<bool> {
150        match self {
151            Self::DBus(collection) => collection.is_locked().await.map_err(From::from),
152            Self::File(keyring, _) => {
153                let keyring_guard = keyring.read().await;
154                Ok(keyring_guard
155                    .as_ref()
156                    .expect("Keyring must exist")
157                    .is_locked())
158            }
159        }
160    }
161
162    /// Remove items that matches the attributes.
163    pub async fn delete(&self, attributes: &impl AsAttributes) -> Result<()> {
164        match self {
165            Self::DBus(backend) => {
166                let items = backend.search_items(attributes).await?;
167                for item in items {
168                    item.delete(None).await?;
169                }
170            }
171            Self::File(keyring, _) => {
172                let kg = keyring.read().await;
173                match kg.as_ref() {
174                    Some(file::Keyring::Unlocked(backend)) => {
175                        backend
176                            .delete(attributes)
177                            .await
178                            .map_err(crate::Error::File)?;
179                    }
180                    Some(file::Keyring::Locked(_)) => {
181                        return Err(crate::file::Error::Locked.into());
182                    }
183                    _ => unreachable!("A keyring must exist"),
184                }
185            }
186        };
187        Ok(())
188    }
189
190    /// Retrieve all the items.
191    pub async fn items(&self) -> Result<Vec<Item>> {
192        let items = match self {
193            Self::DBus(backend) => {
194                let items = backend.items().await?;
195                items.into_iter().map(Item::for_dbus).collect::<Vec<_>>()
196            }
197            Self::File(keyring, _) => {
198                let kg = keyring.read().await;
199                match kg.as_ref() {
200                    Some(file::Keyring::Unlocked(backend)) => {
201                        let items = backend.items().await.map_err(crate::Error::File)?;
202                        items
203                            .into_iter()
204                            .map(|i| Item::for_file(i.into(), Arc::clone(keyring)))
205                            .collect::<Vec<_>>()
206                    }
207                    Some(file::Keyring::Locked(_)) => {
208                        return Err(crate::file::Error::Locked.into());
209                    }
210                    _ => unreachable!("A keyring must exist"),
211                }
212            }
213        };
214        Ok(items)
215    }
216
217    /// Create a new item.
218    pub async fn create_item(
219        &self,
220        label: &str,
221        attributes: &impl AsAttributes,
222        secret: impl Into<Secret>,
223        replace: bool,
224    ) -> Result<()> {
225        match self {
226            Self::DBus(backend) => {
227                backend
228                    .create_item(label, attributes, secret, replace, None)
229                    .await?;
230            }
231            Self::File(keyring, _) => {
232                let kg = keyring.read().await;
233                match kg.as_ref() {
234                    Some(file::Keyring::Unlocked(backend)) => {
235                        backend
236                            .create_item(label, attributes, secret, replace)
237                            .await
238                            .map_err(crate::Error::File)?;
239                    }
240                    Some(file::Keyring::Locked(_)) => {
241                        return Err(crate::file::Error::Locked.into());
242                    }
243                    _ => unreachable!("A keyring must exist"),
244                }
245            }
246        };
247        Ok(())
248    }
249
250    /// Find items based on their attributes.
251    pub async fn search_items(&self, attributes: &impl AsAttributes) -> Result<Vec<Item>> {
252        let items = match self {
253            Self::DBus(backend) => {
254                let items = backend.search_items(attributes).await?;
255                items.into_iter().map(Item::for_dbus).collect::<Vec<_>>()
256            }
257            Self::File(keyring, _) => {
258                let kg = keyring.read().await;
259                match kg.as_ref() {
260                    Some(file::Keyring::Unlocked(backend)) => {
261                        let items = backend
262                            .search_items(attributes)
263                            .await
264                            .map_err(crate::Error::File)?;
265                        items
266                            .into_iter()
267                            .map(|i| Item::for_file(i.into(), Arc::clone(keyring)))
268                            .collect::<Vec<_>>()
269                    }
270                    Some(file::Keyring::Locked(_)) => {
271                        return Err(crate::file::Error::Locked.into());
272                    }
273                    _ => unreachable!("A keyring must exist"),
274                }
275            }
276        };
277        Ok(items)
278    }
279}
280
281/// A generic secret with a label and attributes.
282#[derive(Debug)]
283pub enum Item {
284    #[doc(hidden)]
285    File(
286        RwLock<Option<file::Item>>,
287        Arc<RwLock<Option<file::Keyring>>>,
288    ),
289    #[doc(hidden)]
290    DBus(dbus::Item),
291}
292
293impl Item {
294    fn for_file(item: file::Item, backend: Arc<RwLock<Option<file::Keyring>>>) -> Self {
295        Self::File(RwLock::new(Some(item)), backend)
296    }
297
298    fn for_dbus(item: dbus::Item) -> Self {
299        Self::DBus(item)
300    }
301
302    /// The item label.
303    pub async fn label(&self) -> Result<String> {
304        let label = match self {
305            Self::File(item, _) => {
306                let item_guard = item.read().await;
307                let file_item = item_guard.as_ref().expect("Item must exist");
308                match file_item {
309                    file::Item::Unlocked(unlocked) => unlocked.label().to_owned(),
310                    file::Item::Locked(_) => return Err(crate::file::Error::Locked.into()),
311                }
312            }
313            Self::DBus(item) => item.label().await?,
314        };
315        Ok(label)
316    }
317
318    /// Sets the item label.
319    pub async fn set_label(&self, label: &str) -> Result<()> {
320        match self {
321            Self::File(item, keyring) => {
322                let mut item_guard = item.write().await;
323                let file_item = item_guard.as_mut().expect("Item must exist");
324
325                match file_item {
326                    file::Item::Unlocked(unlocked) => {
327                        unlocked.set_label(label);
328
329                        let kg = keyring.read().await;
330                        match kg.as_ref() {
331                            Some(file::Keyring::Unlocked(backend)) => {
332                                backend
333                                    .create_item(
334                                        unlocked.label(),
335                                        &unlocked.attributes(),
336                                        unlocked.secret(),
337                                        true,
338                                    )
339                                    .await
340                                    .map_err(crate::Error::File)?;
341                            }
342                            Some(file::Keyring::Locked(_)) => {
343                                return Err(crate::file::Error::Locked.into());
344                            }
345                            None => unreachable!("A keyring must exist"),
346                        }
347                    }
348                    file::Item::Locked(_) => {
349                        return Err(crate::file::Error::Locked.into());
350                    }
351                }
352            }
353            Self::DBus(item) => item.set_label(label).await?,
354        };
355        Ok(())
356    }
357
358    /// Retrieve the item attributes.
359    pub async fn attributes(&self) -> Result<HashMap<String, String>> {
360        let attributes = match self {
361            Self::File(item, _) => {
362                let item_guard = item.read().await;
363                let file_item = item_guard.as_ref().expect("Item must exist");
364                match file_item {
365                    file::Item::Unlocked(unlocked) => unlocked
366                        .attributes()
367                        .iter()
368                        .map(|(k, v)| (k.to_owned(), v.to_string()))
369                        .collect::<HashMap<_, _>>(),
370                    file::Item::Locked(_) => return Err(crate::file::Error::Locked.into()),
371                }
372            }
373            Self::DBus(item) => item.attributes().await?,
374        };
375        Ok(attributes)
376    }
377
378    /// Retrieve the item attributes as a typed schema.
379    ///
380    /// # Example
381    ///
382    /// ```no_run
383    /// # use oo7::{SecretSchema, Item};
384    /// # #[derive(SecretSchema, Debug)]
385    /// # #[schema(name = "org.example.Password")]
386    /// # struct PasswordSchema {
387    /// #     username: String,
388    /// #     server: String,
389    /// # }
390    /// # async fn example(item: &Item) -> Result<(), oo7::Error> {
391    /// let schema = item.attributes_as::<PasswordSchema>().await?;
392    /// println!("Username: {}", schema.username);
393    /// # Ok(())
394    /// # }
395    /// ```
396    #[cfg(feature = "schema")]
397    #[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
398    pub async fn attributes_as<T>(&self) -> Result<T>
399    where
400        T: for<'a> std::convert::TryFrom<&'a HashMap<String, String>, Error = crate::SchemaError>,
401    {
402        match self {
403            Self::File(..) => T::try_from(&self.attributes().await?)
404                .map_err(crate::file::Error::Schema)
405                .map_err(Into::into),
406            Self::DBus(_) => T::try_from(&self.attributes().await?)
407                .map_err(crate::dbus::Error::Schema)
408                .map_err(Into::into),
409        }
410    }
411
412    /// Sets the item attributes.
413    pub async fn set_attributes(&self, attributes: &impl AsAttributes) -> Result<()> {
414        match self {
415            Self::File(item, keyring) => {
416                let kg = keyring.read().await;
417
418                match kg.as_ref() {
419                    Some(file::Keyring::Unlocked(backend)) => {
420                        let mut item_guard = item.write().await;
421                        let file_item = item_guard.as_mut().expect("Item must exist");
422
423                        match file_item {
424                            file::Item::Unlocked(unlocked) => {
425                                let index = backend
426                                    .lookup_item_index(&unlocked.attributes())
427                                    .await
428                                    .map_err(crate::Error::File)?;
429
430                                unlocked.set_attributes(attributes);
431
432                                if let Some(index) = index {
433                                    backend
434                                        .replace_item_index(index, unlocked)
435                                        .await
436                                        .map_err(crate::Error::File)?;
437                                } else {
438                                    backend
439                                        .create_item(
440                                            unlocked.label(),
441                                            attributes,
442                                            unlocked.secret(),
443                                            true,
444                                        )
445                                        .await
446                                        .map_err(crate::Error::File)?;
447                                }
448                            }
449                            file::Item::Locked(_) => {
450                                return Err(crate::file::Error::Locked.into());
451                            }
452                        }
453                    }
454                    Some(file::Keyring::Locked(_)) => {
455                        return Err(crate::file::Error::Locked.into());
456                    }
457                    None => unreachable!("A keyring must exist"),
458                }
459            }
460            Self::DBus(item) => item.set_attributes(attributes).await?,
461        };
462        Ok(())
463    }
464
465    /// Sets a new secret.
466    pub async fn set_secret(&self, secret: impl Into<Secret>) -> Result<()> {
467        match self {
468            Self::File(item, keyring) => {
469                let mut item_guard = item.write().await;
470                let file_item = item_guard.as_mut().expect("Item must exist");
471
472                match file_item {
473                    file::Item::Unlocked(unlocked) => {
474                        unlocked.set_secret(secret);
475
476                        let kg = keyring.read().await;
477                        match kg.as_ref() {
478                            Some(file::Keyring::Unlocked(backend)) => {
479                                backend
480                                    .create_item(
481                                        unlocked.label(),
482                                        &unlocked.attributes(),
483                                        unlocked.secret(),
484                                        true,
485                                    )
486                                    .await
487                                    .map_err(crate::Error::File)?;
488                            }
489                            Some(file::Keyring::Locked(_)) => {
490                                return Err(crate::file::Error::Locked.into());
491                            }
492                            None => unreachable!("A keyring must exist"),
493                        }
494                    }
495                    file::Item::Locked(_) => {
496                        return Err(crate::file::Error::Locked.into());
497                    }
498                }
499            }
500            Self::DBus(item) => item.set_secret(secret).await?,
501        };
502        Ok(())
503    }
504
505    /// Retrieves the stored secret.
506    pub async fn secret(&self) -> Result<Secret> {
507        let secret = match self {
508            Self::File(item, _) => {
509                let item_guard = item.read().await;
510                let file_item = item_guard.as_ref().expect("Item must exist");
511                match file_item {
512                    file::Item::Unlocked(unlocked) => unlocked.secret(),
513                    file::Item::Locked(_) => return Err(crate::file::Error::Locked.into()),
514                }
515            }
516            Self::DBus(item) => item.secret().await?,
517        };
518        Ok(secret)
519    }
520
521    /// Whether the item is locked or not
522    pub async fn is_locked(&self) -> Result<bool> {
523        match self {
524            Self::DBus(item) => item.is_locked().await.map_err(From::from),
525            Self::File(item, _) => {
526                let item_guard = item.read().await;
527                let file_item = item_guard.as_ref().expect("Item must exist");
528                Ok(file_item.is_locked())
529            }
530        }
531    }
532
533    /// Lock the item
534    pub async fn lock(&self) -> Result<()> {
535        match self {
536            Self::DBus(item) => item.lock(None).await?,
537            Self::File(item, keyring) => {
538                let mut item_guard = item.write().await;
539                let item_value = item_guard.take();
540                if let Some(file::Item::Unlocked(unlocked)) = item_value {
541                    let kg = keyring.read().await;
542                    match kg.as_ref() {
543                        Some(file::Keyring::Unlocked(backend)) => {
544                            let locked = backend
545                                .lock_item(unlocked)
546                                .await
547                                .map_err(crate::Error::File)?;
548                            *item_guard = Some(file::Item::Locked(locked));
549                        }
550                        Some(file::Keyring::Locked(_)) => {
551                            *item_guard = Some(file::Item::Unlocked(unlocked));
552                            return Err(crate::file::Error::Locked.into());
553                        }
554                        None => unreachable!("A keyring must exist"),
555                    }
556                } else {
557                    *item_guard = item_value;
558                }
559            }
560        }
561        Ok(())
562    }
563
564    /// Unlock the item
565    pub async fn unlock(&self) -> Result<()> {
566        match self {
567            Self::DBus(item) => item.unlock(None).await?,
568            Self::File(item, keyring) => {
569                let mut item_guard = item.write().await;
570                let item_value = item_guard.take();
571                if let Some(file::Item::Locked(locked)) = item_value {
572                    let kg = keyring.read().await;
573                    match kg.as_ref() {
574                        Some(file::Keyring::Unlocked(backend)) => {
575                            let unlocked = backend
576                                .unlock_item(locked)
577                                .await
578                                .map_err(crate::Error::File)?;
579                            *item_guard = Some(file::Item::Unlocked(unlocked));
580                        }
581                        Some(file::Keyring::Locked(_)) => {
582                            *item_guard = Some(file::Item::Locked(locked));
583                            return Err(crate::file::Error::Locked.into());
584                        }
585                        None => unreachable!("A keyring must exist"),
586                    }
587                } else {
588                    *item_guard = item_value;
589                }
590            }
591        }
592        Ok(())
593    }
594
595    /// Delete the item.
596    pub async fn delete(&self) -> Result<()> {
597        match self {
598            Self::File(item, keyring) => {
599                let item_guard = item.read().await;
600                let file_item = item_guard.as_ref().expect("Item must exist");
601
602                match file_item {
603                    file::Item::Unlocked(unlocked) => {
604                        let kg = keyring.read().await;
605                        match kg.as_ref() {
606                            Some(file::Keyring::Unlocked(backend)) => {
607                                backend
608                                    .delete(&unlocked.attributes())
609                                    .await
610                                    .map_err(crate::Error::File)?;
611                            }
612                            Some(file::Keyring::Locked(_)) => {
613                                return Err(crate::file::Error::Locked.into());
614                            }
615                            None => unreachable!("A keyring must exist"),
616                        }
617                    }
618                    file::Item::Locked(_) => {
619                        return Err(crate::file::Error::Locked.into());
620                    }
621                }
622            }
623            Self::DBus(item) => {
624                item.delete(None).await?;
625            }
626        };
627        Ok(())
628    }
629
630    /// The UNIX time when the item was created.
631    pub async fn created(&self) -> Result<Duration> {
632        match self {
633            Self::DBus(item) => Ok(item.created().await?),
634            Self::File(item, _) => {
635                let item_guard = item.read().await;
636                let file_item = item_guard.as_ref().expect("Item must exist");
637                match file_item {
638                    file::Item::Unlocked(unlocked) => Ok(unlocked.created()),
639                    file::Item::Locked(_) => Err(crate::file::Error::Locked.into()),
640                }
641            }
642        }
643    }
644
645    /// The UNIX time when the item was modified.
646    pub async fn modified(&self) -> Result<Duration> {
647        match self {
648            Self::DBus(item) => Ok(item.modified().await?),
649            Self::File(item, _) => {
650                let item_guard = item.read().await;
651                let file_item = item_guard.as_ref().expect("Item must exist");
652                match file_item {
653                    file::Item::Unlocked(unlocked) => Ok(unlocked.modified()),
654                    file::Item::Locked(_) => Err(crate::file::Error::Locked.into()),
655                }
656            }
657        }
658    }
659}