1
use std::{collections::HashMap, sync::Arc, time::Duration};
2

            
3
#[cfg(feature = "async-std")]
4
use async_lock::RwLock;
5
#[cfg(feature = "tokio")]
6
use tokio::sync::RwLock;
7

            
8
use 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)]
20
pub enum Keyring {
21
    #[doc(hidden)]
22
    File(Arc<RwLock<Option<file::Keyring>>>),
23
    #[doc(hidden)]
24
    DBus(dbus::Collection),
25
}
26

            
27
impl Keyring {
28
    /// Create a new instance of the Keyring.
29
    pub async fn new() -> Result<Self> {
30
        let is_sandboxed = ashpd::is_sandboxed().await;
31
        if is_sandboxed {
32
            #[cfg(feature = "tracing")]
33
            tracing::debug!("Application is sandboxed, using the file backend");
34

            
35
            let secret = Secret::from(
36
                ashpd::desktop::secret::retrieve()
37
                    .await
38
                    .map_err(crate::file::Error::from)?,
39
            );
40
            match file::UnlockedKeyring::load(
41
                crate::file::api::Keyring::default_path()?,
42
                secret.clone(),
43
            )
44
            .await
45
            {
46
                Ok(file) => {
47
                    return Ok(Self::File(Arc::new(RwLock::new(Some(
48
                        file::Keyring::Unlocked(file),
49
                    )))));
50
                }
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
                    return Err(crate::Error::File(e));
60
                }
61
            };
62
        } else {
63
            #[cfg(feature = "tracing")]
64
            tracing::debug!(
65
                "Application is not sandboxed, falling back to the Secret Service backend"
66
            );
67
        }
68
        let service = dbus::Service::new().await?;
69
        let collection = service.default_collection().await?;
70
        Ok(Self::DBus(collection))
71
    }
72

            
73
    /// Unlock the used collection.
74
    pub async fn unlock(&self) -> Result<()> {
75
        match self {
76
            Self::DBus(backend) => backend.unlock(None).await?,
77
            Self::File(keyring) => {
78
                let mut kg = keyring.write().await;
79
                let kg_value = kg.take();
80
                if let Some(file::Keyring::Locked(locked)) = kg_value {
81
                    #[cfg(feature = "tracing")]
82
                    tracing::debug!("Unlocking file backend keyring");
83

            
84
                    // Retrieve secret from portal
85
                    let secret = Secret::from(
86
                        ashpd::desktop::secret::retrieve()
87
                            .await
88
                            .map_err(crate::file::Error::from)?,
89
                    );
90

            
91
                    let unlocked = locked.unlock(secret).await.map_err(crate::Error::File)?;
92
                    *kg = Some(file::Keyring::Unlocked(unlocked));
93
                } else {
94
                    *kg = kg_value;
95
                }
96
            }
97
        };
98
        Ok(())
99
    }
100

            
101
    /// Lock the used collection.
102
    pub async fn lock(&self) -> Result<()> {
103
        match self {
104
            Self::DBus(backend) => backend.lock(None).await?,
105
            Self::File(keyring) => {
106
                let mut kg = keyring.write().await;
107
                let kg_value = kg.take();
108
                if let Some(file::Keyring::Unlocked(unlocked)) = kg_value {
109
                    #[cfg(feature = "tracing")]
110
                    tracing::debug!("Locking file backend keyring");
111

            
112
                    let locked = unlocked.lock();
113
                    *kg = Some(file::Keyring::Locked(locked));
114
                } else {
115
                    *kg = kg_value;
116
                }
117
            }
118
        };
119
        Ok(())
120
    }
121

            
122
    /// Whether the keyring is locked or not.
123
    pub async fn is_locked(&self) -> Result<bool> {
124
        match self {
125
            Self::DBus(collection) => collection.is_locked().await.map_err(From::from),
126
            Self::File(keyring) => {
127
                let keyring_guard = keyring.read().await;
128
                Ok(keyring_guard
129
                    .as_ref()
130
                    .expect("Keyring must exist")
131
                    .is_locked())
132
            }
133
        }
134
    }
135

            
136
    /// Remove items that matches the attributes.
137
    pub async fn delete(&self, attributes: &impl AsAttributes) -> Result<()> {
138
        match self {
139
            Self::DBus(backend) => {
140
                let items = backend.search_items(attributes).await?;
141
                for item in items {
142
                    item.delete(None).await?;
143
                }
144
            }
145
            Self::File(keyring) => {
146
                let kg = keyring.read().await;
147
                match kg.as_ref() {
148
                    Some(file::Keyring::Unlocked(backend)) => {
149
                        backend
150
                            .delete(attributes)
151
                            .await
152
                            .map_err(crate::Error::File)?;
153
                    }
154
                    Some(file::Keyring::Locked(_)) => {
155
                        return Err(crate::file::Error::Locked.into());
156
                    }
157
                    _ => unreachable!("A keyring must exist"),
158
                }
159
            }
160
        };
161
        Ok(())
162
    }
163

            
164
    /// Retrieve all the items.
165
    pub async fn items(&self) -> Result<Vec<Item>> {
166
        let items = match self {
167
            Self::DBus(backend) => {
168
                let items = backend.items().await?;
169
                items.into_iter().map(Item::for_dbus).collect::<Vec<_>>()
170
            }
171
            Self::File(keyring) => {
172
                let kg = keyring.read().await;
173
                match kg.as_ref() {
174
                    Some(file::Keyring::Unlocked(backend)) => {
175
                        let items = backend.items().await.map_err(crate::Error::File)?;
176
                        items
177
                            .into_iter()
178
                            // Ignore invalid items
179
                            .flatten()
180
                            .map(|i| Item::for_file(i, Arc::clone(keyring)))
181
                            .collect::<Vec<_>>()
182
                    }
183
                    Some(file::Keyring::Locked(_)) => {
184
                        return Err(crate::file::Error::Locked.into());
185
                    }
186
                    _ => unreachable!("A keyring must exist"),
187
                }
188
            }
189
        };
190
        Ok(items)
191
    }
192

            
193
    /// Create a new item.
194
    pub async fn create_item(
195
        &self,
196
        label: &str,
197
        attributes: &impl AsAttributes,
198
        secret: impl Into<Secret>,
199
        replace: bool,
200
    ) -> Result<()> {
201
        match self {
202
            Self::DBus(backend) => {
203
                backend
204
                    .create_item(label, attributes, secret, replace, None)
205
                    .await?;
206
            }
207
            Self::File(keyring) => {
208
                let kg = keyring.read().await;
209
                match kg.as_ref() {
210
                    Some(file::Keyring::Unlocked(backend)) => {
211
                        backend
212
                            .create_item(label, attributes, secret, replace)
213
                            .await
214
                            .map_err(crate::Error::File)?;
215
                    }
216
                    Some(file::Keyring::Locked(_)) => {
217
                        return Err(crate::file::Error::Locked.into());
218
                    }
219
                    _ => unreachable!("A keyring must exist"),
220
                }
221
            }
222
        };
223
        Ok(())
224
    }
225

            
226
    /// Find items based on their attributes.
227
    pub async fn search_items(&self, attributes: &impl AsAttributes) -> Result<Vec<Item>> {
228
        let items = match self {
229
            Self::DBus(backend) => {
230
                let items = backend.search_items(attributes).await?;
231
                items.into_iter().map(Item::for_dbus).collect::<Vec<_>>()
232
            }
233
            Self::File(keyring) => {
234
                let kg = keyring.read().await;
235
                match kg.as_ref() {
236
                    Some(file::Keyring::Unlocked(backend)) => {
237
                        let items = backend
238
                            .search_items(attributes)
239
                            .await
240
                            .map_err(crate::Error::File)?;
241
                        items
242
                            .into_iter()
243
                            .map(|i| Item::for_file(i, Arc::clone(keyring)))
244
                            .collect::<Vec<_>>()
245
                    }
246
                    Some(file::Keyring::Locked(_)) => {
247
                        return Err(crate::file::Error::Locked.into());
248
                    }
249
                    _ => unreachable!("A keyring must exist"),
250
                }
251
            }
252
        };
253
        Ok(items)
254
    }
255
}
256

            
257
/// A generic secret with a label and attributes.
258
#[derive(Debug)]
259
pub enum Item {
260
    #[doc(hidden)]
261
    File(
262
        RwLock<Option<file::Item>>,
263
        Arc<RwLock<Option<file::Keyring>>>,
264
    ),
265
    #[doc(hidden)]
266
    DBus(dbus::Item),
267
}
268

            
269
impl Item {
270
    fn for_file(item: file::Item, backend: Arc<RwLock<Option<file::Keyring>>>) -> Self {
271
        Self::File(RwLock::new(Some(item)), backend)
272
    }
273

            
274
    fn for_dbus(item: dbus::Item) -> Self {
275
        Self::DBus(item)
276
    }
277

            
278
    /// The item label.
279
    pub async fn label(&self) -> Result<String> {
280
        let label = match self {
281
            Self::File(item, _) => {
282
                let item_guard = item.read().await;
283
                let file_item = item_guard.as_ref().expect("Item must exist");
284
                match file_item {
285
                    file::Item::Unlocked(unlocked) => unlocked.label().to_owned(),
286
                    file::Item::Locked(_) => return Err(crate::file::Error::Locked.into()),
287
                }
288
            }
289
            Self::DBus(item) => item.label().await?,
290
        };
291
        Ok(label)
292
    }
293

            
294
    /// Sets the item label.
295
    pub async fn set_label(&self, label: &str) -> Result<()> {
296
        match self {
297
            Self::File(item, keyring) => {
298
                let mut item_guard = item.write().await;
299
                let file_item = item_guard.as_mut().expect("Item must exist");
300

            
301
                match file_item {
302
                    file::Item::Unlocked(unlocked) => {
303
                        unlocked.set_label(label);
304

            
305
                        let kg = keyring.read().await;
306
                        match kg.as_ref() {
307
                            Some(file::Keyring::Unlocked(backend)) => {
308
                                backend
309
                                    .create_item(
310
                                        unlocked.label(),
311
                                        &unlocked.attributes(),
312
                                        unlocked.secret(),
313
                                        true,
314
                                    )
315
                                    .await
316
                                    .map_err(crate::Error::File)?;
317
                            }
318
                            Some(file::Keyring::Locked(_)) => {
319
                                return Err(crate::file::Error::Locked.into());
320
                            }
321
                            None => unreachable!("A keyring must exist"),
322
                        }
323
                    }
324
                    file::Item::Locked(_) => {
325
                        return Err(crate::file::Error::Locked.into());
326
                    }
327
                }
328
            }
329
            Self::DBus(item) => item.set_label(label).await?,
330
        };
331
        Ok(())
332
    }
333

            
334
    /// Retrieve the item attributes.
335
    pub async fn attributes(&self) -> Result<HashMap<String, String>> {
336
        let attributes = match self {
337
            Self::File(item, _) => {
338
                let item_guard = item.read().await;
339
                let file_item = item_guard.as_ref().expect("Item must exist");
340
                match file_item {
341
                    file::Item::Unlocked(unlocked) => unlocked
342
                        .attributes()
343
                        .iter()
344
                        .map(|(k, v)| (k.to_owned(), v.to_string()))
345
                        .collect::<HashMap<_, _>>(),
346
                    file::Item::Locked(_) => return Err(crate::file::Error::Locked.into()),
347
                }
348
            }
349
            Self::DBus(item) => item.attributes().await?,
350
        };
351
        Ok(attributes)
352
    }
353

            
354
    /// Sets the item attributes.
355
    pub async fn set_attributes(&self, attributes: &impl AsAttributes) -> Result<()> {
356
        match self {
357
            Self::File(item, keyring) => {
358
                let kg = keyring.read().await;
359

            
360
                match kg.as_ref() {
361
                    Some(file::Keyring::Unlocked(backend)) => {
362
                        let mut item_guard = item.write().await;
363
                        let file_item = item_guard.as_mut().expect("Item must exist");
364

            
365
                        match file_item {
366
                            file::Item::Unlocked(unlocked) => {
367
                                let index = backend
368
                                    .lookup_item_index(&unlocked.attributes())
369
                                    .await
370
                                    .map_err(crate::Error::File)?;
371

            
372
                                unlocked.set_attributes(attributes);
373

            
374
                                if let Some(index) = index {
375
                                    backend
376
                                        .replace_item_index(index, unlocked)
377
                                        .await
378
                                        .map_err(crate::Error::File)?;
379
                                } else {
380
                                    backend
381
                                        .create_item(
382
                                            unlocked.label(),
383
                                            attributes,
384
                                            unlocked.secret(),
385
                                            true,
386
                                        )
387
                                        .await
388
                                        .map_err(crate::Error::File)?;
389
                                }
390
                            }
391
                            file::Item::Locked(_) => {
392
                                return Err(crate::file::Error::Locked.into());
393
                            }
394
                        }
395
                    }
396
                    Some(file::Keyring::Locked(_)) => {
397
                        return Err(crate::file::Error::Locked.into());
398
                    }
399
                    None => unreachable!("A keyring must exist"),
400
                }
401
            }
402
            Self::DBus(item) => item.set_attributes(attributes).await?,
403
        };
404
        Ok(())
405
    }
406

            
407
    /// Sets a new secret.
408
    pub async fn set_secret(&self, secret: impl Into<Secret>) -> Result<()> {
409
        match self {
410
            Self::File(item, keyring) => {
411
                let mut item_guard = item.write().await;
412
                let file_item = item_guard.as_mut().expect("Item must exist");
413

            
414
                match file_item {
415
                    file::Item::Unlocked(unlocked) => {
416
                        unlocked.set_secret(secret);
417

            
418
                        let kg = keyring.read().await;
419
                        match kg.as_ref() {
420
                            Some(file::Keyring::Unlocked(backend)) => {
421
                                backend
422
                                    .create_item(
423
                                        unlocked.label(),
424
                                        &unlocked.attributes(),
425
                                        unlocked.secret(),
426
                                        true,
427
                                    )
428
                                    .await
429
                                    .map_err(crate::Error::File)?;
430
                            }
431
                            Some(file::Keyring::Locked(_)) => {
432
                                return Err(crate::file::Error::Locked.into());
433
                            }
434
                            None => unreachable!("A keyring must exist"),
435
                        }
436
                    }
437
                    file::Item::Locked(_) => {
438
                        return Err(crate::file::Error::Locked.into());
439
                    }
440
                }
441
            }
442
            Self::DBus(item) => item.set_secret(secret).await?,
443
        };
444
        Ok(())
445
    }
446

            
447
    /// Retrieves the stored secret.
448
    pub async fn secret(&self) -> Result<Secret> {
449
        let secret = match self {
450
            Self::File(item, _) => {
451
                let item_guard = item.read().await;
452
                let file_item = item_guard.as_ref().expect("Item must exist");
453
                match file_item {
454
                    file::Item::Unlocked(unlocked) => unlocked.secret(),
455
                    file::Item::Locked(_) => return Err(crate::file::Error::Locked.into()),
456
                }
457
            }
458
            Self::DBus(item) => item.secret().await?,
459
        };
460
        Ok(secret)
461
    }
462

            
463
    /// Whether the item is locked or not
464
    pub async fn is_locked(&self) -> Result<bool> {
465
        match self {
466
            Self::DBus(item) => item.is_locked().await.map_err(From::from),
467
            Self::File(item, _) => {
468
                let item_guard = item.read().await;
469
                let file_item = item_guard.as_ref().expect("Item must exist");
470
                Ok(file_item.is_locked())
471
            }
472
        }
473
    }
474

            
475
    /// Lock the item
476
    pub async fn lock(&self) -> Result<()> {
477
        match self {
478
            Self::DBus(item) => item.lock(None).await?,
479
            Self::File(item, keyring) => {
480
                let mut item_guard = item.write().await;
481
                let item_value = item_guard.take();
482
                if let Some(file::Item::Unlocked(unlocked)) = item_value {
483
                    let kg = keyring.read().await;
484
                    match kg.as_ref() {
485
                        Some(file::Keyring::Unlocked(backend)) => {
486
                            let locked = backend
487
                                .lock_item(unlocked)
488
                                .await
489
                                .map_err(crate::Error::File)?;
490
                            *item_guard = Some(file::Item::Locked(locked));
491
                        }
492
                        Some(file::Keyring::Locked(_)) => {
493
                            *item_guard = Some(file::Item::Unlocked(unlocked));
494
                            return Err(crate::file::Error::Locked.into());
495
                        }
496
                        None => unreachable!("A keyring must exist"),
497
                    }
498
                } else {
499
                    *item_guard = item_value;
500
                }
501
            }
502
        }
503
        Ok(())
504
    }
505

            
506
    /// Unlock the item
507
    pub async fn unlock(&self) -> Result<()> {
508
        match self {
509
            Self::DBus(item) => item.unlock(None).await?,
510
            Self::File(item, keyring) => {
511
                let mut item_guard = item.write().await;
512
                let item_value = item_guard.take();
513
                if let Some(file::Item::Locked(locked)) = item_value {
514
                    let kg = keyring.read().await;
515
                    match kg.as_ref() {
516
                        Some(file::Keyring::Unlocked(backend)) => {
517
                            let unlocked = backend
518
                                .unlock_item(locked)
519
                                .await
520
                                .map_err(crate::Error::File)?;
521
                            *item_guard = Some(file::Item::Unlocked(unlocked));
522
                        }
523
                        Some(file::Keyring::Locked(_)) => {
524
                            *item_guard = Some(file::Item::Locked(locked));
525
                            return Err(crate::file::Error::Locked.into());
526
                        }
527
                        None => unreachable!("A keyring must exist"),
528
                    }
529
                } else {
530
                    *item_guard = item_value;
531
                }
532
            }
533
        }
534
        Ok(())
535
    }
536

            
537
    /// Delete the item.
538
    pub async fn delete(&self) -> Result<()> {
539
        match self {
540
            Self::File(item, keyring) => {
541
                let item_guard = item.read().await;
542
                let file_item = item_guard.as_ref().expect("Item must exist");
543

            
544
                match file_item {
545
                    file::Item::Unlocked(unlocked) => {
546
                        let kg = keyring.read().await;
547
                        match kg.as_ref() {
548
                            Some(file::Keyring::Unlocked(backend)) => {
549
                                backend
550
                                    .delete(&unlocked.attributes())
551
                                    .await
552
                                    .map_err(crate::Error::File)?;
553
                            }
554
                            Some(file::Keyring::Locked(_)) => {
555
                                return Err(crate::file::Error::Locked.into());
556
                            }
557
                            None => unreachable!("A keyring must exist"),
558
                        }
559
                    }
560
                    file::Item::Locked(_) => {
561
                        return Err(crate::file::Error::Locked.into());
562
                    }
563
                }
564
            }
565
            Self::DBus(item) => {
566
                item.delete(None).await?;
567
            }
568
        };
569
        Ok(())
570
    }
571

            
572
    /// The UNIX time when the item was created.
573
    pub async fn created(&self) -> Result<Duration> {
574
        match self {
575
            Self::DBus(item) => Ok(item.created().await?),
576
            Self::File(item, _) => {
577
                let item_guard = item.read().await;
578
                let file_item = item_guard.as_ref().expect("Item must exist");
579
                match file_item {
580
                    file::Item::Unlocked(unlocked) => Ok(unlocked.created()),
581
                    file::Item::Locked(_) => Err(crate::file::Error::Locked.into()),
582
                }
583
            }
584
        }
585
    }
586

            
587
    /// The UNIX time when the item was modified.
588
    pub async fn modified(&self) -> Result<Duration> {
589
        match self {
590
            Self::DBus(item) => Ok(item.modified().await?),
591
            Self::File(item, _) => {
592
                let item_guard = item.read().await;
593
                let file_item = item_guard.as_ref().expect("Item must exist");
594
                match file_item {
595
                    file::Item::Unlocked(unlocked) => Ok(unlocked.modified()),
596
                    file::Item::Locked(_) => Err(crate::file::Error::Locked.into()),
597
                }
598
            }
599
        }
600
    }
601
}