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