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<'static>),
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
8
    pub async fn lock(&self) -> Result<()> {
103
2
        match self {
104
            Self::DBus(backend) => backend.lock(None).await?,
105
2
            Self::File(keyring) => {
106
4
                let mut kg = keyring.write().await;
107
4
                let kg_value = kg.take();
108
6
                if let Some(file::Keyring::Unlocked(unlocked)) = kg_value {
109
4
                    #[cfg(feature = "tracing")]
110
                    tracing::debug!("Locking file backend keyring");
111

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

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

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

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

            
193
    /// Create a new item.
194
4
    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
4
        match self {
202
4
            Self::DBus(backend) => {
203
12
                backend
204
4
                    .create_item(label, attributes, secret, replace, None)
205
20
                    .await?;
206
            }
207
4
            Self::File(keyring) => {
208
8
                let kg = keyring.read().await;
209
8
                match kg.as_ref() {
210
4
                    Some(file::Keyring::Unlocked(backend)) => {
211
12
                        backend
212
4
                            .create_item(label, attributes, secret, replace)
213
16
                            .await
214
8
                            .map_err(crate::Error::File)?;
215
                    }
216
                    Some(file::Keyring::Locked(_)) => {
217
2
                        return Err(crate::file::Error::Locked.into());
218
                    }
219
                    _ => unreachable!("A keyring must exist"),
220
                }
221
            }
222
        };
223
4
        Ok(())
224
    }
225

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

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

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

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

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

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

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

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

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

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

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

            
372
2
                                unlocked.set_attributes(attributes);
373

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

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

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

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

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

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

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

            
506
    /// Unlock the item
507
8
    pub async fn unlock(&self) -> Result<()> {
508
2
        match self {
509
            Self::DBus(item) => item.unlock(None).await?,
510
2
            Self::File(item, keyring) => {
511
4
                let mut item_guard = item.write().await;
512
4
                let item_value = item_guard.take();
513
6
                if let Some(file::Item::Locked(locked)) = item_value {
514
4
                    let kg = keyring.read().await;
515
4
                    match kg.as_ref() {
516
2
                        Some(file::Keyring::Unlocked(backend)) => {
517
6
                            let unlocked = backend
518
2
                                .unlock_item(locked)
519
6
                                .await
520
2
                                .map_err(crate::Error::File)?;
521
2
                            *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
2
                    *item_guard = item_value;
531
                }
532
            }
533
        }
534
2
        Ok(())
535
    }
536

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

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

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

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

            
603
#[cfg(test)]
604
#[cfg(feature = "tokio")]
605
mod tests {
606
    use tempfile::tempdir;
607

            
608
    use super::*;
609

            
610
    async fn all_backends(temp_dir: tempfile::TempDir) -> Vec<Keyring> {
611
        let mut backends = Vec::new();
612

            
613
        let keyring_path = temp_dir.path().join("test.keyring");
614
        let secret = Secret::from([1, 2].into_iter().cycle().take(64).collect::<Vec<_>>());
615
        let unlocked = file::UnlockedKeyring::load(&keyring_path, secret)
616
            .await
617
            .unwrap();
618
        let keyring = Keyring::File(Arc::new(RwLock::new(Some(file::Keyring::Unlocked(
619
            unlocked,
620
        )))));
621

            
622
        backends.push(keyring);
623

            
624
        let service = dbus::Service::new().await.unwrap();
625
        if let Ok(collection) = service.default_collection().await {
626
            backends.push(Keyring::DBus(collection));
627
        }
628

            
629
        backends
630
    }
631

            
632
    #[tokio::test]
633
    async fn create_and_retrieve_items() {
634
        let temp_dir = tempdir().unwrap();
635
        let backends = all_backends(temp_dir).await;
636

            
637
        for (idx, keyring) in backends.iter().enumerate() {
638
            println!("Running test on backend {}", idx);
639

            
640
            keyring
641
                .create_item(
642
                    "Item 1",
643
                    &[
644
                        ("test-name", "create_and_retrieve_items"),
645
                        ("user", "alice"),
646
                    ],
647
                    "secret1",
648
                    false,
649
                )
650
                .await
651
                .unwrap();
652
            keyring
653
                .create_item(
654
                    "Item 2",
655
                    &[("test-name", "create_and_retrieve_items"), ("user", "bob")],
656
                    "secret2",
657
                    false,
658
                )
659
                .await
660
                .unwrap();
661

            
662
            let items = keyring
663
                .search_items(&[("test-name", "create_and_retrieve_items")])
664
                .await
665
                .unwrap();
666
            assert_eq!(items.len(), 2);
667

            
668
            let alice_items = keyring
669
                .search_items(&[
670
                    ("test-name", "create_and_retrieve_items"),
671
                    ("user", "alice"),
672
                ])
673
                .await
674
                .unwrap();
675
            assert_eq!(alice_items.len(), 1);
676
            assert_eq!(alice_items[0].label().await.unwrap(), "Item 1");
677

            
678
            keyring
679
                .delete(&[("test-name", "create_and_retrieve_items")])
680
                .await
681
                .unwrap();
682
        }
683
    }
684

            
685
    #[tokio::test]
686
    async fn delete_items() {
687
        let temp_dir = tempdir().unwrap();
688
        let backends = all_backends(temp_dir).await;
689

            
690
        for (idx, keyring) in backends.iter().enumerate() {
691
            println!("Running test on backend {}", idx);
692

            
693
            keyring
694
                .create_item(
695
                    "Item 1",
696
                    &[("test-name", "delete_items"), ("app", "test")],
697
                    "secret1",
698
                    false,
699
                )
700
                .await
701
                .unwrap();
702
            keyring
703
                .create_item(
704
                    "Item 2",
705
                    &[("test-name", "delete_items"), ("app", "other")],
706
                    "secret2",
707
                    false,
708
                )
709
                .await
710
                .unwrap();
711

            
712
            keyring
713
                .delete(&[("test-name", "delete_items"), ("app", "test")])
714
                .await
715
                .unwrap();
716

            
717
            let items = keyring
718
                .search_items(&[("test-name", "delete_items")])
719
                .await
720
                .unwrap();
721
            assert_eq!(items.len(), 1);
722
            assert_eq!(items[0].label().await.unwrap(), "Item 2");
723

            
724
            keyring
725
                .delete(&[("test-name", "delete_items")])
726
                .await
727
                .unwrap();
728
        }
729
    }
730

            
731
    #[tokio::test]
732
    async fn item_update_label() {
733
        let temp_dir = tempdir().unwrap();
734
        let backends = all_backends(temp_dir).await;
735

            
736
        for (idx, keyring) in backends.iter().enumerate() {
737
            println!("Running test on backend {}", idx);
738

            
739
            keyring
740
                .create_item(
741
                    "Original Label",
742
                    &[("test-name", "item_update_label")],
743
                    "secret",
744
                    false,
745
                )
746
                .await
747
                .unwrap();
748

            
749
            let items = keyring
750
                .search_items(&[("test-name", "item_update_label")])
751
                .await
752
                .unwrap();
753
            let item = &items[0];
754

            
755
            assert_eq!(item.label().await.unwrap(), "Original Label");
756

            
757
            item.set_label("New Label").await.unwrap();
758
            assert_eq!(item.label().await.unwrap(), "New Label");
759

            
760
            let items = keyring
761
                .search_items(&[("test-name", "item_update_label")])
762
                .await
763
                .unwrap();
764
            assert_eq!(items[0].label().await.unwrap(), "New Label");
765

            
766
            keyring
767
                .delete(&[("test-name", "item_update_label")])
768
                .await
769
                .unwrap();
770
        }
771
    }
772

            
773
    #[tokio::test]
774
    async fn item_update_attributes() {
775
        let temp_dir = tempdir().unwrap();
776
        let backends = all_backends(temp_dir).await;
777

            
778
        for (idx, keyring) in backends.iter().enumerate() {
779
            println!("Running test on backend {}", idx);
780

            
781
            keyring
782
                .create_item(
783
                    "Test",
784
                    &[("test-name", "item_update_attributes"), ("version", "1.0")],
785
                    "secret",
786
                    false,
787
                )
788
                .await
789
                .unwrap();
790

            
791
            let items = keyring
792
                .search_items(&[("test-name", "item_update_attributes")])
793
                .await
794
                .unwrap();
795
            let item = &items[0];
796

            
797
            item.set_attributes(&[("test-name", "item_update_attributes"), ("version", "2.0")])
798
                .await
799
                .unwrap();
800

            
801
            let attrs = item.attributes().await.unwrap();
802
            assert_eq!(attrs.get("version").unwrap(), "2.0");
803

            
804
            // Test edge case: set_attributes when item doesn't exist in keyring
805
            if idx == 0 {
806
                keyring
807
                    .delete(&[("test-name", "item_update_attributes")])
808
                    .await
809
                    .unwrap();
810

            
811
                item.set_attributes(&[("test-name", "item_update_attributes"), ("version", "3.0")])
812
                    .await
813
                    .unwrap();
814

            
815
                let new_items = keyring
816
                    .search_items(&[("test-name", "item_update_attributes")])
817
                    .await
818
                    .unwrap();
819
                assert_eq!(new_items.len(), 1);
820
            }
821

            
822
            keyring
823
                .delete(&[("test-name", "item_update_attributes")])
824
                .await
825
                .unwrap();
826
        }
827
    }
828

            
829
    #[tokio::test]
830
    async fn item_update_secret() {
831
        let temp_dir = tempdir().unwrap();
832
        let backends = all_backends(temp_dir).await;
833

            
834
        for (idx, keyring) in backends.iter().enumerate() {
835
            println!("Running test on backend {}", idx);
836

            
837
            keyring
838
                .create_item(
839
                    "Test",
840
                    &[("test-name", "item_update_secret")],
841
                    "old_secret",
842
                    false,
843
                )
844
                .await
845
                .unwrap();
846

            
847
            let items = keyring
848
                .search_items(&[("test-name", "item_update_secret")])
849
                .await
850
                .unwrap();
851
            let item = &items[0];
852

            
853
            assert_eq!(item.secret().await.unwrap(), Secret::text("old_secret"));
854

            
855
            item.set_secret("new_secret").await.unwrap();
856
            assert_eq!(item.secret().await.unwrap(), Secret::text("new_secret"));
857

            
858
            keyring
859
                .delete(&[("test-name", "item_update_secret")])
860
                .await
861
                .unwrap();
862
        }
863
    }
864

            
865
    #[tokio::test]
866
    async fn item_delete() {
867
        let temp_dir = tempdir().unwrap();
868
        let backends = all_backends(temp_dir).await;
869

            
870
        for (idx, keyring) in backends.iter().enumerate() {
871
            println!("Running test on backend {}", idx);
872

            
873
            keyring
874
                .create_item(
875
                    "Item 1",
876
                    &[("test-name", "item_delete"), ("id", "1")],
877
                    "secret1",
878
                    false,
879
                )
880
                .await
881
                .unwrap();
882
            keyring
883
                .create_item(
884
                    "Item 2",
885
                    &[("test-name", "item_delete"), ("id", "2")],
886
                    "secret2",
887
                    false,
888
                )
889
                .await
890
                .unwrap();
891

            
892
            let items = keyring
893
                .search_items(&[("test-name", "item_delete")])
894
                .await
895
                .unwrap();
896
            assert_eq!(items.len(), 2);
897

            
898
            items[0].delete().await.unwrap();
899

            
900
            let items = keyring
901
                .search_items(&[("test-name", "item_delete")])
902
                .await
903
                .unwrap();
904
            assert_eq!(items.len(), 1);
905

            
906
            keyring
907
                .delete(&[("test-name", "item_delete")])
908
                .await
909
                .unwrap();
910
        }
911
    }
912

            
913
    #[tokio::test]
914
    async fn item_replace() {
915
        let temp_dir = tempdir().unwrap();
916
        let backends = all_backends(temp_dir).await;
917

            
918
        for (idx, keyring) in backends.iter().enumerate() {
919
            println!("Running test on backend {}", idx);
920

            
921
            keyring
922
                .create_item("Item 1", &[("test-name", "item_replace")], "secret1", false)
923
                .await
924
                .unwrap();
925

            
926
            keyring
927
                .create_item("Item 2", &[("test-name", "item_replace")], "secret2", true)
928
                .await
929
                .unwrap();
930

            
931
            let items = keyring
932
                .search_items(&[("test-name", "item_replace")])
933
                .await
934
                .unwrap();
935
            assert_eq!(items.len(), 1);
936
            assert_eq!(items[0].label().await.unwrap(), "Item 2");
937
            assert_eq!(items[0].secret().await.unwrap(), Secret::text("secret2"));
938

            
939
            // Cleanup
940
            keyring
941
                .delete(&[("test-name", "item_replace")])
942
                .await
943
                .unwrap();
944
        }
945
    }
946

            
947
    #[tokio::test]
948
    async fn item_timestamps() {
949
        let temp_dir = tempdir().unwrap();
950
        let backends = all_backends(temp_dir).await;
951

            
952
        for (idx, keyring) in backends.iter().enumerate() {
953
            println!("Running test on backend {}", idx);
954

            
955
            keyring
956
                .create_item("Test", &[("test-name", "item_timestamps")], "secret", false)
957
                .await
958
                .unwrap();
959

            
960
            let items = keyring
961
                .search_items(&[("test-name", "item_timestamps")])
962
                .await
963
                .unwrap();
964
            let item = &items[0];
965

            
966
            let created = item.created().await.unwrap();
967
            let modified = item.modified().await.unwrap();
968

            
969
            assert!(created.as_secs() > 0);
970
            assert!(modified.as_secs() > 0);
971

            
972
            assert!(modified >= created);
973

            
974
            // Cleanup
975
            keyring
976
                .delete(&[("test-name", "item_timestamps")])
977
                .await
978
                .unwrap();
979
        }
980
    }
981

            
982
    #[tokio::test]
983
    async fn item_is_locked() {
984
        let temp_dir = tempdir().unwrap();
985
        let backends = all_backends(temp_dir).await;
986

            
987
        for (idx, keyring) in backends.iter().enumerate() {
988
            println!("Running test on backend {}", idx);
989

            
990
            keyring
991
                .create_item("Test", &[("test-name", "item_is_locked")], "secret", false)
992
                .await
993
                .unwrap();
994

            
995
            let items = keyring
996
                .search_items(&[("test-name", "item_is_locked")])
997
                .await
998
                .unwrap();
999
            let item = &items[0];
            assert!(!item.is_locked().await.unwrap());
            let all_items = keyring.items().await.unwrap();
            assert!(!all_items.is_empty());
            keyring
                .delete(&[("test-name", "item_is_locked")])
                .await
                .unwrap();
        }
    }
    // File-backend specific tests, as the DBus one require prompting
    #[tokio::test]
    async fn file_keyring_lock_unlock() {
        let temp_dir = tempdir().unwrap();
        let backends = all_backends(temp_dir).await;
        let keyring = &backends[0];
        assert!(!keyring.is_locked().await.unwrap());
        keyring.lock().await.unwrap();
        assert!(keyring.is_locked().await.unwrap());
        // Test edge case: locking an already locked keyring
        keyring.lock().await.unwrap();
        assert!(keyring.is_locked().await.unwrap());
        let result = keyring
            .create_item("test", &[("app", "test")], "secret", false)
            .await;
        assert!(matches!(
            result,
            Err(crate::Error::File(file::Error::Locked))
        ));
        if let Keyring::File(kg) = &keyring {
            let mut kg_guard = kg.write().await;
            if let Some(file::Keyring::Locked(locked)) = kg_guard.take() {
                let secret = Secret::from([1, 2].into_iter().cycle().take(64).collect::<Vec<_>>());
                let unlocked = unsafe { locked.unlock_unchecked(secret).await.unwrap() };
                *kg_guard = Some(file::Keyring::Unlocked(unlocked));
            }
        }
        assert!(!keyring.is_locked().await.unwrap());
    }
    #[tokio::test]
    async fn file_item_lock_unlock() {
        let temp_dir = tempdir().unwrap();
        let backends = all_backends(temp_dir).await;
        let keyring = &backends[0];
        keyring
            .create_item("Test Item", &[("app", "test")], "secret", false)
            .await
            .unwrap();
        let items = keyring.items().await.unwrap();
        let item = &items[0];
        assert!(!item.is_locked().await.unwrap());
        assert_eq!(item.secret().await.unwrap(), Secret::text("secret"));
        // Test edge case: unlocking an already unlocked item
        item.unlock().await.unwrap();
        assert!(!item.is_locked().await.unwrap());
        item.lock().await.unwrap();
        assert!(item.is_locked().await.unwrap());
        // Test edge case: locking an already locked item
        item.lock().await.unwrap();
        assert!(item.is_locked().await.unwrap());
        let result = item.secret().await;
        assert!(matches!(
            result,
            Err(crate::Error::File(file::Error::Locked))
        ));
        // Unlock the item
        item.unlock().await.unwrap();
        assert!(!item.is_locked().await.unwrap());
        assert_eq!(item.secret().await.unwrap(), Secret::text("secret"));
    }
    #[tokio::test]
    async fn file_locked_item_operations_fail() {
        let temp_dir = tempdir().unwrap();
        let backends = all_backends(temp_dir).await;
        let keyring = &backends[0];
        keyring
            .create_item("Test", &[("app", "test")], "secret", false)
            .await
            .unwrap();
        let items = keyring.items().await.unwrap();
        let item = &items[0];
        item.lock().await.unwrap();
        assert!(matches!(
            item.label().await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            item.attributes().await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            item.secret().await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            item.set_label("new").await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            item.set_attributes(&[("app", "test")]).await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            item.set_secret("new").await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            item.delete().await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            item.created().await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            item.modified().await,
            Err(crate::Error::File(file::Error::Locked))
        ));
    }
    #[tokio::test]
    async fn file_locked_keyring_operations_fail() {
        let temp_dir = tempdir().unwrap();
        let backends = all_backends(temp_dir).await;
        let keyring = &backends[0];
        keyring
            .create_item("Test", &[("app", "test")], "secret", false)
            .await
            .unwrap();
        let items = keyring.items().await.unwrap();
        let item = &items[0];
        keyring.lock().await.unwrap();
        assert!(matches!(
            keyring
                .create_item("test", &[("app", "test")], "secret", false)
                .await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            keyring.items().await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            keyring.search_items(&[("app", "test")]).await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            keyring.delete(&[("app", "test")]).await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            item.set_label("new label").await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            item.set_attributes(&[("app", "new")]).await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            item.set_secret("new secret").await,
            Err(crate::Error::File(file::Error::Locked))
        ));
        assert!(matches!(
            item.delete().await,
            Err(crate::Error::File(file::Error::Locked))
        ));
    }
    #[tokio::test]
    async fn file_item_lock_with_locked_keyring_fails() {
        let temp_dir = tempdir().unwrap();
        let backends = all_backends(temp_dir).await;
        let keyring = &backends[0];
        keyring
            .create_item("Test", &[("app", "test")], "secret", false)
            .await
            .unwrap();
        let items = keyring.items().await.unwrap();
        let item = &items[0];
        keyring.lock().await.unwrap();
        let result = item.lock().await;
        assert!(matches!(
            result,
            Err(crate::Error::File(file::Error::Locked))
        ));
    }
}