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
        if ashpd::is_sandboxed() {
31
            #[cfg(feature = "tracing")]
32
            tracing::debug!("Application is sandboxed, using the file backend");
33

            
34
            let secret = Secret::from(
35
                ashpd::desktop::secret::retrieve()
36
                    .await
37
                    .map_err(crate::file::Error::from)?,
38
            );
39
            match file::UnlockedKeyring::load(
40
                crate::file::api::Keyring::default_path()?,
41
                secret.clone(),
42
            )
43
            .await
44
            {
45
                Ok(file) => {
46
                    return Ok(Self::File(Arc::new(RwLock::new(Some(
47
                        file::Keyring::Unlocked(file),
48
                    )))));
49
                }
50
                // Do nothing in this case, we are supposed to fallback to the host keyring
51
                Err(super::file::Error::Portal(ashpd::Error::PortalNotFound(_))) => {
52
                    #[cfg(feature = "tracing")]
53
                    tracing::debug!(
54
                        "org.freedesktop.portal.Secrets is not available, falling back to the Secret Service backend"
55
                    );
56
                }
57
                Err(e) => {
58
                    return Err(crate::Error::File(e));
59
                }
60
            };
61
        } else {
62
            #[cfg(feature = "tracing")]
63
            tracing::debug!(
64
                "Application is not sandboxed, falling back to the Secret Service backend"
65
            );
66
        }
67
        let service = dbus::Service::new().await?;
68
        let collection = service.default_collection().await?;
69
        Ok(Self::DBus(collection))
70
    }
71

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
371
                                unlocked.set_attributes(attributes);
372

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

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

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

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

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

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

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

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

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

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

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

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