1
use std::{
2
    collections::HashMap,
3
    path::{Path, PathBuf},
4
    sync::Arc,
5
};
6

            
7
#[cfg(feature = "async-std")]
8
use async_fs as fs;
9
#[cfg(feature = "async-std")]
10
use async_lock::{Mutex, RwLock};
11
#[cfg(feature = "async-std")]
12
use futures_lite::AsyncReadExt;
13
#[cfg(feature = "tokio")]
14
use tokio::{
15
    fs,
16
    io::AsyncReadExt,
17
    sync::{Mutex, RwLock},
18
};
19

            
20
use crate::{
21
    AsAttributes, Key, Secret,
22
    file::{Error, InvalidItemError, Item, LockedItem, LockedKeyring, UnlockedItem, api},
23
};
24

            
25
type ItemDefinition = (String, HashMap<String, String>, Secret, bool);
26

            
27
/// File backed keyring.
28
#[derive(Debug)]
29
pub struct UnlockedKeyring {
30
    pub(super) keyring: Arc<RwLock<api::Keyring>>,
31
    pub(super) path: Option<PathBuf>,
32
    /// Times are stored before reading the file to detect
33
    /// file changes before writing
34
    pub(super) mtime: Mutex<Option<std::time::SystemTime>>,
35
    pub(super) key: Mutex<Option<Arc<Key>>>,
36
    pub(super) secret: Mutex<Arc<Secret>>,
37
}
38

            
39
impl UnlockedKeyring {
40
    /// Load from a keyring file.
41
    ///
42
    /// # Arguments
43
    ///
44
    /// * `path` - The path to the file backend.
45
    /// * `secret` - The service key, usually retrieved from the Secrets portal.
46
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(secret), fields(path = ?path.as_ref())))]
47
16
    pub async fn load(path: impl AsRef<Path>, secret: Secret) -> Result<Self, Error> {
48
12
        Self::load_inner(path, secret, true).await
49
    }
50

            
51
    /// Load from a keyring file.
52
    ///
53
    /// # Arguments
54
    ///
55
    /// * `path` - The path to the file backend.
56
    /// * `secret` - The service key, usually retrieved from the Secrets portal.
57
    ///
58
    /// # Safety
59
    ///
60
    /// The secret is not validated to be the correct one to decrypt the keyring
61
    /// items. Allowing the API user to write new items with a different
62
    /// secret on top of previously added items with a different secret.
63
    ///
64
    /// As it is not a supported behaviour, this API is mostly meant for
65
    /// recovering broken keyrings.
66
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(secret), fields(path = ?path.as_ref())))]
67
2
    pub async unsafe fn load_unchecked(
68
        path: impl AsRef<Path>,
69
        secret: Secret,
70
    ) -> Result<Self, Error> {
71
6
        Self::load_inner(path, secret, false).await
72
    }
73

            
74
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(secret), fields(path = ?path.as_ref(), validate_items = validate_items)))]
75
4
    async fn load_inner(
76
        path: impl AsRef<Path>,
77
        secret: Secret,
78
        validate_items: bool,
79
    ) -> Result<Self, Error> {
80
8
        #[cfg(feature = "tracing")]
81
        tracing::debug!("Trying to load keyring file at {:?}", path.as_ref());
82
4
        if validate_items {
83
12
            LockedKeyring::load(path).await?.unlock(secret).await
84
        } else {
85
            unsafe {
86
12
                LockedKeyring::load(path)
87
8
                    .await?
88
2
                    .unlock_unchecked(secret)
89
6
                    .await
90
            }
91
        }
92
    }
93

            
94
    /// Creates a temporary backend, that is never stored on disk.
95
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(secret)))]
96
30
    pub async fn temporary(secret: Secret) -> Result<Self, Error> {
97
6
        let keyring = api::Keyring::new();
98
7
        Ok(Self {
99
7
            keyring: Arc::new(RwLock::new(keyring)),
100
5
            path: None,
101
7
            mtime: Default::default(),
102
5
            key: Default::default(),
103
12
            secret: Mutex::new(Arc::new(secret)),
104
        })
105
    }
106

            
107
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(file, secret), fields(path = ?path.as_ref())))]
108
4
    async fn migrate(
109
        file: &mut fs::File,
110
        path: impl AsRef<Path>,
111
        secret: Secret,
112
    ) -> Result<Self, Error> {
113
4
        let mut content = Vec::new();
114
16
        file.read_to_end(&mut content).await?;
115

            
116
12
        match api::Keyring::try_from(content.as_slice()) {
117
4
            Ok(keyring) => Ok(Self {
118
4
                keyring: Arc::new(RwLock::new(keyring)),
119
4
                path: Some(path.as_ref().to_path_buf()),
120
2
                mtime: Default::default(),
121
2
                key: Default::default(),
122
4
                secret: Mutex::new(Arc::new(secret)),
123
            }),
124
8
            Err(Error::VersionMismatch(Some(version)))
125
4
                if version[0] == api::LEGACY_MAJOR_VERSION =>
126
            {
127
8
                #[cfg(feature = "tracing")]
128
                tracing::debug!("Migrating from legacy keyring format");
129

            
130
8
                let legacy_keyring = api::LegacyKeyring::try_from(content.as_slice())?;
131
4
                let mut keyring = api::Keyring::new();
132
8
                let key = keyring.derive_key(&secret)?;
133

            
134
10
                let decrypted_items = legacy_keyring.decrypt_items(&secret)?;
135

            
136
                #[cfg(feature = "tracing")]
137
8
                let _migrate_span =
138
                    tracing::debug_span!("migrate_items", item_count = decrypted_items.len());
139

            
140
12
                for item in decrypted_items {
141
8
                    let encrypted_item = item.encrypt(&key)?;
142
4
                    keyring.items.push(encrypted_item);
143
                }
144

            
145
4
                Ok(Self {
146
4
                    keyring: Arc::new(RwLock::new(keyring)),
147
8
                    path: Some(path.as_ref().to_path_buf()),
148
4
                    mtime: Default::default(),
149
4
                    key: Default::default(),
150
8
                    secret: Mutex::new(Arc::new(secret)),
151
                })
152
            }
153
            Err(err) => Err(err),
154
        }
155
    }
156

            
157
    /// Open a keyring with given name from the default directory.
158
    ///
159
    /// This function will automatically migrate the keyring to the
160
    /// latest format.
161
    ///
162
    /// # Arguments
163
    ///
164
    /// * `name` - The name of the keyring.
165
    /// * `secret` - The service key, usually retrieved from the Secrets portal.
166
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(secret)))]
167
20
    pub async fn open(name: &str, secret: Secret) -> Result<Self, Error> {
168
8
        let v1_path = api::Keyring::path(name, api::MAJOR_VERSION)?;
169
8
        if v1_path.exists() {
170
4
            #[cfg(feature = "tracing")]
171
            tracing::debug!("Loading v1 keyring file");
172
6
            return Self::load(v1_path, secret).await;
173
        }
174

            
175
8
        let v0_path = api::Keyring::path(name, api::LEGACY_MAJOR_VERSION)?;
176
8
        if v0_path.exists() {
177
8
            #[cfg(feature = "tracing")]
178
            tracing::debug!("Trying to load keyring file at {:?}", v0_path);
179
12
            match fs::File::open(&v0_path).await {
180
                Err(err) => Err(err.into()),
181
8
                Ok(mut file) => Self::migrate(&mut file, v1_path, secret).await,
182
            }
183
        } else {
184
8
            #[cfg(feature = "tracing")]
185
            tracing::debug!("Creating new keyring");
186
4
            Ok(Self {
187
8
                keyring: Arc::new(RwLock::new(api::Keyring::new())),
188
4
                path: Some(v1_path),
189
4
                mtime: Default::default(),
190
4
                key: Default::default(),
191
8
                secret: Mutex::new(Arc::new(secret)),
192
            })
193
        }
194
    }
195

            
196
    /// Lock the keyring.
197
4
    pub fn lock(self) -> LockedKeyring {
198
        LockedKeyring {
199
4
            keyring: self.keyring,
200
4
            path: self.path,
201
4
            mtime: self.mtime,
202
        }
203
    }
204

            
205
    /// Lock an item using the keyring's key.
206
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, item)))]
207
20
    pub async fn lock_item(&self, item: UnlockedItem) -> Result<LockedItem, Error> {
208
8
        let key = self.derive_key().await?;
209
4
        item.lock(&key)
210
    }
211

            
212
    /// Unlock an item using the keyring's key.
213
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, item)))]
214
20
    pub async fn unlock_item(&self, item: LockedItem) -> Result<UnlockedItem, Error> {
215
8
        let key = self.derive_key().await?;
216
4
        item.unlock(&key)
217
    }
218

            
219
    /// Get the encryption key for this keyring.
220
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
221
8
    pub async fn key(&self) -> Result<Arc<Key>, crate::crypto::Error> {
222
6
        self.derive_key().await
223
    }
224

            
225
    /// Return the associated file if any.
226
5
    pub fn path(&self) -> Option<&std::path::Path> {
227
3
        self.path.as_deref()
228
    }
229

            
230
    /// Get the modification timestamp
231
15
    pub async fn modified_time(&self) -> std::time::Duration {
232
8
        self.keyring.read().await.modified_time()
233
    }
234

            
235
    /// Retrieve the number of items
236
    ///
237
    /// This function will not trigger a key derivation and can therefore be
238
    /// faster than [`items().len()`](Self::items).
239
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
240
8
    pub async fn n_items(&self) -> usize {
241
6
        self.keyring.read().await.items.len()
242
    }
243

            
244
    /// Retrieve the list of available [`UnlockedItem`]s.
245
    ///
246
    /// If items cannot be decrypted, [`InvalidItemError`]s are returned for
247
    /// them instead of [`UnlockedItem`]s.
248
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
249
24
    pub async fn items(&self) -> Result<Vec<Result<Item, InvalidItemError>>, Error> {
250
18
        let key = self.derive_key().await?;
251
12
        let keyring = self.keyring.read().await;
252

            
253
        #[cfg(feature = "tracing")]
254
12
        let _span = tracing::debug_span!("decrypt", total_items = keyring.items.len());
255

            
256
18
        Ok(keyring
257
            .items
258
6
            .iter()
259
10
            .map(|e| {
260
8
                (*e).clone()
261
8
                    .decrypt(&key)
262
6
                    .map_err(|err| {
263
2
                        InvalidItemError::new(
264
2
                            err,
265
8
                            e.hashed_attributes.keys().map(|x| x.to_string()).collect(),
266
                        )
267
                    })
268
4
                    .map(Item::Unlocked)
269
            })
270
6
            .collect())
271
    }
272

            
273
    /// Search items matching the attributes.
274
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, attributes)))]
275
24
    pub async fn search_items(&self, attributes: &impl AsAttributes) -> Result<Vec<Item>, Error> {
276
12
        let key = self.derive_key().await?;
277
12
        let keyring = self.keyring.read().await;
278
12
        let results = keyring
279
6
            .search_items(attributes, &key)?
280
            .into_iter()
281
6
            .map(Item::Unlocked)
282
            .collect::<Vec<Item>>();
283

            
284
12
        #[cfg(feature = "tracing")]
285
        tracing::debug!("Found {} matching items", results.len());
286

            
287
6
        Ok(results)
288
    }
289

            
290
    /// Find the first item matching the attributes.
291
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, attributes)))]
292
8
    pub async fn lookup_item(&self, attributes: &impl AsAttributes) -> Result<Option<Item>, Error> {
293
4
        let key = self.derive_key().await?;
294
4
        let keyring = self.keyring.read().await;
295

            
296
2
        keyring
297
4
            .lookup_item(attributes, &key)
298
6
            .map(|maybe_item| maybe_item.map(Item::Unlocked))
299
    }
300

            
301
    /// Find the index in the list of items of the first item matching the
302
    /// attributes.
303
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, attributes)))]
304
4
    pub async fn lookup_item_index(
305
        &self,
306
        attributes: &impl AsAttributes,
307
    ) -> Result<Option<usize>, Error> {
308
8
        let key = self.derive_key().await?;
309
8
        let keyring = self.keyring.read().await;
310

            
311
12
        Ok(keyring.lookup_item_index(attributes, &key))
312
    }
313

            
314
    /// Delete an item.
315
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, attributes)))]
316
32
    pub async fn delete(&self, attributes: &impl AsAttributes) -> Result<(), Error> {
317
        #[cfg(feature = "tracing")]
318
16
        let items_before = { self.keyring.read().await.items.len() };
319

            
320
        {
321
8
            let key = self.derive_key().await?;
322
16
            let mut keyring = self.keyring.write().await;
323
16
            keyring.remove_items(attributes, &key)?;
324
        };
325

            
326
14
        self.write().await?;
327

            
328
        #[cfg(feature = "tracing")]
329
        {
330
8
            let items_after = self.keyring.read().await.items.len();
331
8
            let deleted_count = items_before.saturating_sub(items_after);
332
8
            tracing::info!("Deleted {} items", deleted_count);
333
        }
334

            
335
8
        Ok(())
336
    }
337

            
338
    /// Create a new item
339
    ///
340
    /// # Arguments
341
    ///
342
    /// * `label` - A user visible label of the item.
343
    /// * `attributes` - A map of key/value attributes, used to find the item
344
    ///   later.
345
    /// * `secret` - The secret to store.
346
    /// * `replace` - Whether to replace the value if the `attributes` matches
347
    ///   an existing `secret`.
348
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, secret, attributes), fields(replace = replace)))]
349
24
    pub async fn create_item(
350
        &self,
351
        label: &str,
352
        attributes: &impl AsAttributes,
353
        secret: impl Into<Secret>,
354
        replace: bool,
355
    ) -> Result<Item, Error> {
356
        let item = {
357
58
            let key = self.derive_key().await?;
358
48
            let mut keyring = self.keyring.write().await;
359
34
            if replace {
360
20
                keyring.remove_items(attributes, &key)?;
361
            }
362
24
            let item = UnlockedItem::new(label, attributes, secret);
363
48
            let encrypted_item = item.encrypt(&key)?;
364
48
            keyring.items.push(encrypted_item);
365
24
            item
366
        };
367
96
        match self.write().await {
368
            Err(e) => {
369
                #[cfg(feature = "tracing")]
370
                tracing::error!("Failed to write keyring after item creation");
371
                Err(e)
372
            }
373
            Ok(_) => {
374
48
                #[cfg(feature = "tracing")]
375
                tracing::info!("Successfully created item");
376
24
                Ok(Item::Unlocked(item))
377
            }
378
        }
379
    }
380

            
381
    /// Replaces item at the given index.
382
    ///
383
    /// The `index` refers to the index of the [`Vec`] returned by
384
    /// [`items()`](Self::items). If the index does not exist, the functions
385
    /// returns an error.
386
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, item), fields(index = index)))]
387
8
    pub async fn replace_item_index(&self, index: usize, item: &UnlockedItem) -> Result<(), Error> {
388
        {
389
4
            let key = self.derive_key().await?;
390
4
            let mut keyring = self.keyring.write().await;
391

            
392
4
            if let Some(item_store) = keyring.items.get_mut(index) {
393
4
                *item_store = item.encrypt(&key)?;
394
            } else {
395
2
                return Err(Error::InvalidItemIndex(index));
396
            }
397
        }
398
4
        self.write().await
399
    }
400

            
401
    /// Deletes item at the given index.
402
    ///
403
    /// The `index` refers to the index of the [`Vec`] returned by
404
    /// [`items()`](Self::items). If the index does not exist, the functions
405
    /// returns an error.
406
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(index = index)))]
407
8
    pub async fn delete_item_index(&self, index: usize) -> Result<(), Error> {
408
        {
409
4
            let mut keyring = self.keyring.write().await;
410

            
411
4
            if index < keyring.items.len() {
412
4
                keyring.items.remove(index);
413
            } else {
414
2
                return Err(Error::InvalidItemIndex(index));
415
            }
416
        }
417
4
        self.write().await
418
    }
419

            
420
    /// Helper used for migration to avoid re-writing the file multiple times
421
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, items), fields(item_count = items.len())))]
422
10
    pub(crate) async fn create_items(&self, items: Vec<ItemDefinition>) -> Result<(), Error> {
423
4
        let key = self.derive_key().await?;
424
4
        let mut mtime = self.mtime.lock().await;
425
4
        let mut keyring = self.keyring.write().await;
426

            
427
        #[cfg(feature = "tracing")]
428
4
        let _span = tracing::debug_span!("bulk_create", items_to_create = items.len());
429

            
430
8
        for (label, attributes, secret, replace) in items {
431
4
            if replace {
432
4
                keyring.remove_items(&attributes, &key)?;
433
            }
434
2
            let item = UnlockedItem::new(label, &attributes, secret);
435
4
            let encrypted_item = item.encrypt(&key)?;
436
4
            keyring.items.push(encrypted_item);
437
        }
438

            
439
2
        #[cfg(feature = "tracing")]
440
        tracing::debug!("Writing keyring back to the file");
441
4
        if let Some(ref path) = self.path {
442
6
            keyring.dump(path, *mtime).await?;
443
            // Update mtime after successful write
444
6
            if let Ok(modified) = fs::metadata(path).await?.modified() {
445
4
                *mtime = Some(modified);
446
            }
447
        }
448
2
        Ok(())
449
    }
450

            
451
    /// Write the changes to the keyring file.
452
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
453
16
    pub async fn write(&self) -> Result<(), Error> {
454
10
        let mut mtime = self.mtime.lock().await;
455
        {
456
8
            let mut keyring = self.keyring.write().await;
457

            
458
8
            if let Some(ref path) = self.path {
459
12
                keyring.dump(path, *mtime).await?;
460
            }
461
        };
462
4
        let Some(ref path) = self.path else {
463
4
            return Ok(());
464
        };
465

            
466
16
        if let Ok(modified) = fs::metadata(path).await?.modified() {
467
8
            *mtime = Some(modified);
468
        }
469
4
        Ok(())
470
    }
471

            
472
    /// Return key, derive and store it first if not initialized
473
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
474
28
    async fn derive_key(&self) -> Result<Arc<Key>, crate::crypto::Error> {
475
12
        let keyring = Arc::clone(&self.keyring);
476
12
        let secret_lock = self.secret.lock().await;
477
12
        let secret = Arc::clone(&secret_lock);
478
8
        drop(secret_lock);
479

            
480
4
        let mut key_lock = self.key.lock().await;
481
18
        if key_lock.is_none() {
482
            #[cfg(feature = "async-std")]
483
            let key = blocking::unblock(move || {
484
                async_io::block_on(async { keyring.read().await.derive_key(&secret) })
485
            })
486
            .await?;
487
            #[cfg(feature = "tokio")]
488
            let key = {
489
37
                tokio::task::spawn_blocking(move || keyring.blocking_read().derive_key(&secret))
490
22
                    .await
491
                    .unwrap()?
492
            };
493

            
494
12
            *key_lock = Some(Arc::new(key));
495
        }
496

            
497
12
        Ok(Arc::clone(key_lock.as_ref().unwrap()))
498
    }
499

            
500
    /// Change keyring secret
501
    ///
502
    /// # Arguments
503
    ///
504
    /// * `secret` - The new secret to store.
505
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, secret)))]
506
20
    pub async fn change_secret(&self, secret: Secret) -> Result<(), Error> {
507
8
        let keyring = self.keyring.read().await;
508
8
        let key = self.derive_key().await?;
509
8
        let mut items = Vec::with_capacity(keyring.items.len());
510

            
511
        #[cfg(feature = "tracing")]
512
8
        let _decrypt_span =
513
            tracing::debug_span!("decrypt_for_reencrypt", total_items = keyring.items.len());
514

            
515
8
        for item in &keyring.items {
516
4
            items.push(item.clone().decrypt(&key)?);
517
        }
518
4
        drop(keyring);
519

            
520
4
        #[cfg(feature = "tracing")]
521
        tracing::debug!("Updating secret and resetting key");
522

            
523
8
        let mut secret_lock = self.secret.lock().await;
524
4
        *secret_lock = Arc::new(secret);
525
4
        drop(secret_lock);
526

            
527
8
        let mut key_lock = self.key.lock().await;
528
        // Unset the old key
529
4
        *key_lock = None;
530
4
        drop(key_lock);
531

            
532
        // Reset Keyring content before setting the new key
533
8
        let mut keyring = self.keyring.write().await;
534
8
        keyring.reset();
535
4
        drop(keyring);
536

            
537
        // Set new key
538
8
        let key = self.derive_key().await?;
539

            
540
        #[cfg(feature = "tracing")]
541
8
        let _reencrypt_span = tracing::debug_span!("reencrypt", total_items = items.len());
542

            
543
8
        let mut keyring = self.keyring.write().await;
544
12
        for item in items {
545
8
            let encrypted_item = item.encrypt(&key)?;
546
8
            keyring.items.push(encrypted_item);
547
        }
548
4
        drop(keyring);
549

            
550
12
        self.write().await
551
    }
552

            
553
    /// Validate that a secret can decrypt the items in this keyring.
554
    ///
555
    /// For empty keyrings, this always returns `true` since there are no items
556
    /// to validate against.
557
    ///
558
    /// # Arguments
559
    ///
560
    /// * `secret` - The secret to validate.
561
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, secret)))]
562
8
    pub async fn validate_secret(&self, secret: &Secret) -> Result<bool, Error> {
563
4
        let keyring = self.keyring.read().await;
564
4
        Ok(keyring.validate_secret(secret)?)
565
    }
566

            
567
    /// Delete any item that cannot be decrypted with the key associated to the
568
    /// keyring.
569
    ///
570
    /// This can only happen if an item was created using
571
    /// [`Self::load_unchecked`] or prior to 0.4 where we didn't validate
572
    /// the secret when using [`Self::load`] or modified externally.
573
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
574
10
    pub async fn delete_broken_items(&self) -> Result<usize, Error> {
575
4
        let key = self.derive_key().await?;
576
4
        let mut keyring = self.keyring.write().await;
577
2
        let mut broken_items = vec![];
578

            
579
        #[cfg(feature = "tracing")]
580
4
        let _span = tracing::debug_span!("identify_broken", total_items = keyring.items.len());
581

            
582
4
        for (index, encrypted_item) in keyring.items.iter().enumerate() {
583
4
            if !encrypted_item.is_valid(&key) {
584
2
                broken_items.push(index);
585
            }
586
        }
587
2
        let n_broken_items = broken_items.len();
588

            
589
2
        #[cfg(feature = "tracing")]
590
        tracing::info!("Found {} broken items to delete", n_broken_items);
591

            
592
        #[cfg(feature = "tracing")]
593
4
        let _remove_span = tracing::debug_span!("remove_broken", broken_count = n_broken_items);
594

            
595
6
        for index in broken_items.into_iter().rev() {
596
4
            keyring.items.remove(index);
597
        }
598
2
        drop(keyring);
599

            
600
4
        self.write().await?;
601
2
        Ok(n_broken_items)
602
    }
603
}
604

            
605
#[cfg(test)]
606
#[cfg(feature = "tokio")]
607
mod tests {
608
    use std::{collections::HashMap, path::PathBuf};
609

            
610
    use tempfile::tempdir;
611

            
612
    use super::*;
613
    use crate::file::{InvalidItemError, WeakKeyError};
614

            
615
    #[tokio::test]
616
    async fn repeated_write() -> Result<(), Error> {
617
        let path = PathBuf::from("../../tests/test.keyring");
618

            
619
        let secret = Secret::from(vec![1, 2]);
620
        let keyring = UnlockedKeyring::load(&path, secret).await?;
621

            
622
        keyring.write().await?;
623
        keyring.write().await?;
624

            
625
        Ok(())
626
    }
627

            
628
    #[tokio::test]
629
    async fn delete() -> Result<(), Error> {
630
        let path = PathBuf::from("../../tests/test-delete.keyring");
631

            
632
        let keyring = UnlockedKeyring::load(&path, strong_key()).await?;
633
        let attributes: HashMap<&str, &str> = HashMap::default();
634
        keyring
635
            .create_item("Label", &attributes, "secret", false)
636
            .await?;
637

            
638
        keyring.delete_item_index(0).await?;
639

            
640
        let result = keyring.delete_item_index(100).await;
641

            
642
        assert!(matches!(result, Err(Error::InvalidItemIndex(100))));
643

            
644
        Ok(())
645
    }
646

            
647
    #[tokio::test]
648
    async fn write_with_weak_key() -> Result<(), Error> {
649
        let path = PathBuf::from("../../tests/write_with_weak_key.keyring");
650

            
651
        let secret = Secret::from(vec![1, 2]);
652
        let keyring = UnlockedKeyring::load(&path, secret).await?;
653
        let attributes: HashMap<&str, &str> = HashMap::default();
654

            
655
        let result = keyring
656
            .create_item("label", &attributes, "my-password", false)
657
            .await;
658

            
659
        assert!(matches!(
660
            result,
661
            Err(Error::WeakKey(WeakKeyError::PasswordTooShort(2)))
662
        ));
663

            
664
        Ok(())
665
    }
666

            
667
    #[tokio::test]
668
    async fn write_with_strong_key() -> Result<(), Error> {
669
        let path = PathBuf::from("../../tests/write_with_strong_key.keyring");
670

            
671
        let keyring = UnlockedKeyring::load(&path, strong_key()).await?;
672
        let attributes: HashMap<&str, &str> = HashMap::default();
673

            
674
        keyring
675
            .create_item("label", &attributes, "my-password", false)
676
            .await?;
677

            
678
        Ok(())
679
    }
680

            
681
    fn strong_key() -> Secret {
682
        Secret::from([1, 2].into_iter().cycle().take(64).collect::<Vec<_>>())
683
    }
684

            
685
    #[tokio::test]
686
    async fn concurrent_writes() -> Result<(), Error> {
687
        let path = PathBuf::from("../../tests/concurrent_writes.keyring");
688

            
689
        let keyring = Arc::new(UnlockedKeyring::load(&path, strong_key()).await?);
690

            
691
        let keyring_clone = keyring.clone();
692
        let handle_1 = tokio::task::spawn(async move { keyring_clone.write().await });
693
        let handle_2 = tokio::task::spawn(async move { keyring.write().await });
694

            
695
        let (res_1, res_2) = futures_util::future::join(handle_1, handle_2).await;
696
        res_1.unwrap()?;
697
        res_2.unwrap()?;
698

            
699
        Ok(())
700
    }
701

            
702
    async fn check_items(keyring: &UnlockedKeyring) -> Result<(), Error> {
703
        assert_eq!(keyring.n_items().await, 1);
704
        let items: Result<Vec<_>, _> = keyring.items().await?.into_iter().collect();
705
        let items = items.expect("unable to retrieve items");
706
        assert_eq!(items.len(), 1);
707
        assert_eq!(items[0].as_unlocked().label(), "foo");
708
        assert_eq!(items[0].as_unlocked().secret(), Secret::blob("foo"));
709
        let attributes = items[0].as_unlocked().attributes();
710
        assert_eq!(attributes.len(), 2);
711
        assert_eq!(
712
            attributes
713
                .get(crate::XDG_SCHEMA_ATTRIBUTE)
714
                .map(|v| v.as_ref()),
715
            Some("org.gnome.keyring.Note")
716
        );
717

            
718
        Ok(())
719
    }
720

            
721
    #[tokio::test]
722
    #[serial_test::serial]
723
    async fn migrate_from_legacy() -> Result<(), Error> {
724
        let data_dir = tempdir()?;
725
        let v0_dir = data_dir.path().join("keyrings");
726
        let v1_dir = v0_dir.join("v1");
727
        fs::create_dir_all(&v1_dir).await?;
728

            
729
        let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
730
            .join("fixtures")
731
            .join("legacy.keyring");
732
        fs::copy(&fixture_path, &v0_dir.join("default.keyring")).await?;
733

            
734
        unsafe {
735
            std::env::set_var("XDG_DATA_HOME", data_dir.path());
736
        }
737

            
738
        assert!(!v1_dir.join("default.keyring").exists());
739

            
740
        let secret = Secret::blob("test");
741
        let keyring = UnlockedKeyring::open("default", secret).await?;
742

            
743
        check_items(&keyring).await?;
744

            
745
        keyring.write().await?;
746
        assert!(v1_dir.join("default.keyring").exists());
747

            
748
        Ok(())
749
    }
750

            
751
    #[tokio::test]
752
    #[serial_test::serial]
753
    async fn migrate() -> Result<(), Error> {
754
        let data_dir = tempdir()?;
755
        let v0_dir = data_dir.path().join("keyrings");
756
        let v1_dir = v0_dir.join("v1");
757
        fs::create_dir_all(&v1_dir).await?;
758

            
759
        let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
760
            .join("fixtures")
761
            .join("default.keyring");
762
        fs::copy(&fixture_path, &v0_dir.join("default.keyring")).await?;
763

            
764
        unsafe {
765
            std::env::set_var("XDG_DATA_HOME", data_dir.path());
766
        }
767

            
768
        let secret = Secret::blob("test");
769
        let keyring = UnlockedKeyring::open("default", secret).await?;
770

            
771
        assert!(!v1_dir.join("default.keyring").exists());
772

            
773
        check_items(&keyring).await?;
774

            
775
        keyring.write().await?;
776
        assert!(v1_dir.join("default.keyring").exists());
777

            
778
        Ok(())
779
    }
780

            
781
    #[tokio::test]
782
    #[serial_test::serial]
783
    async fn open_wrong_password() -> Result<(), Error> {
784
        let data_dir = tempdir()?;
785
        let v0_dir = data_dir.path().join("keyrings");
786
        let v1_dir = v0_dir.join("v1");
787
        fs::create_dir_all(&v1_dir).await?;
788

            
789
        let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
790
            .join("fixtures")
791
            .join("default.keyring");
792
        fs::copy(&fixture_path, &v1_dir.join("default.keyring")).await?;
793

            
794
        unsafe {
795
            std::env::set_var("XDG_DATA_HOME", data_dir.path());
796
        }
797

            
798
        let secret = Secret::blob("wrong");
799
        let keyring = UnlockedKeyring::open("default", secret).await;
800

            
801
        assert!(keyring.is_err());
802
        assert!(matches!(keyring.unwrap_err(), Error::IncorrectSecret));
803

            
804
        let secret = Secret::blob("test");
805
        let keyring = UnlockedKeyring::open("default", secret).await;
806

            
807
        assert!(keyring.is_ok());
808

            
809
        Ok(())
810
    }
811

            
812
    #[tokio::test]
813
    #[serial_test::serial]
814
    async fn open() -> Result<(), Error> {
815
        let data_dir = tempdir()?;
816
        let v0_dir = data_dir.path().join("keyrings");
817
        let v1_dir = v0_dir.join("v1");
818
        fs::create_dir_all(&v1_dir).await?;
819

            
820
        let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
821
            .join("fixtures")
822
            .join("default.keyring");
823
        fs::copy(&fixture_path, &v1_dir.join("default.keyring")).await?;
824

            
825
        unsafe {
826
            std::env::set_var("XDG_DATA_HOME", data_dir.path());
827
        }
828

            
829
        let secret = Secret::blob("test");
830
        let keyring = UnlockedKeyring::open("default", secret).await?;
831

            
832
        assert!(v1_dir.join("default.keyring").exists());
833

            
834
        check_items(&keyring).await?;
835

            
836
        keyring.write().await?;
837
        assert!(v1_dir.join("default.keyring").exists());
838

            
839
        Ok(())
840
    }
841

            
842
    #[tokio::test]
843
    #[serial_test::serial]
844
    async fn open_nonexistent() -> Result<(), Error> {
845
        let data_dir = tempdir()?;
846
        let v0_dir = data_dir.path().join("keyrings");
847
        let v1_dir = v0_dir.join("v1");
848
        fs::create_dir_all(&v1_dir).await?;
849

            
850
        unsafe {
851
            std::env::set_var("XDG_DATA_HOME", data_dir.path());
852
        }
853

            
854
        let secret = Secret::blob("test");
855
        let keyring = UnlockedKeyring::open("default", secret).await?;
856

            
857
        assert!(!v1_dir.join("default.keyring").exists());
858

            
859
        keyring
860
            .create_item(
861
                "foo",
862
                &[(crate::XDG_SCHEMA_ATTRIBUTE, "org.gnome.keyring.Note")],
863
                "foo",
864
                false,
865
            )
866
            .await?;
867
        keyring.write().await?;
868

            
869
        assert!(v1_dir.join("default.keyring").exists());
870

            
871
        Ok(())
872
    }
873

            
874
    #[tokio::test]
875
    async fn delete_broken_items() -> Result<(), Error> {
876
        const VALID_TO_ADD: usize = 5;
877
        const BROKEN_TO_ADD: usize = 3;
878

            
879
        let data_dir = tempdir()?;
880
        let v0_dir = data_dir.path().join("keyrings");
881
        let v1_dir = v0_dir.join("v1");
882
        fs::create_dir_all(&v1_dir).await?;
883

            
884
        let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
885
            .join("fixtures")
886
            .join("default.keyring");
887
        let keyring_path = v1_dir.join("default.keyring");
888
        fs::copy(&fixture_path, &keyring_path).await?;
889

            
890
        // 1) Load with the correct password and add several valid items. This ensures
891
        //    valid_items > broken_items that we'll add later.
892
        let keyring = UnlockedKeyring::load(&keyring_path, Secret::blob("test")).await?;
893
        for i in 0..VALID_TO_ADD {
894
            keyring
895
                .create_item(
896
                    &format!("valid {}", i),
897
                    &[("attr_valid", "value")],
898
                    format!("password_valid_{}", i),
899
                    false,
900
                )
901
                .await?;
902
        }
903
        drop(keyring);
904

            
905
        // 2) Load_unchecked with the wrong password and add a few "broken" items.
906
        let keyring = unsafe {
907
            UnlockedKeyring::load_unchecked(&keyring_path, Secret::blob("wrong_password")).await?
908
        };
909
        for i in 0..BROKEN_TO_ADD {
910
            keyring
911
                .create_item(
912
                    &format!("bad{}", i),
913
                    &[("attr_bad", "value_bad")],
914
                    format!("pw_bad{}", i),
915
                    false,
916
                )
917
                .await?;
918
        }
919
        drop(keyring);
920

            
921
        // 3) Load with the correct password and run the deletion.
922
        let keyring = UnlockedKeyring::load(&keyring_path, Secret::blob("test")).await?;
923
        let removed = keyring.delete_broken_items().await?;
924
        assert!(
925
            removed >= BROKEN_TO_ADD,
926
            "expected at least {} broken items removed, got {}",
927
            BROKEN_TO_ADD,
928
            removed
929
        );
930

            
931
        // Second call should find nothing left to clean up.
932
        assert_eq!(keyring.delete_broken_items().await?, 0);
933

            
934
        fs::remove_file(keyring_path).await?;
935
        Ok(())
936
    }
937

            
938
    #[tokio::test]
939
    async fn change_secret() -> Result<(), Error> {
940
        let data_dir = tempdir()?;
941
        let v0_dir = data_dir.path().join("keyrings");
942
        let v1_dir = v0_dir.join("v1");
943
        fs::create_dir_all(&v1_dir).await?;
944

            
945
        let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
946
            .join("fixtures")
947
            .join("default.keyring");
948
        let keyring_path = v1_dir.join("default.keyring");
949
        fs::copy(&fixture_path, &keyring_path).await?;
950

            
951
        let keyring = UnlockedKeyring::load(&keyring_path, Secret::blob("test")).await?;
952
        let attributes = &[("attr", "value")];
953
        let item_before = keyring
954
            .create_item("test", attributes, "password", false)
955
            .await?;
956
        let item_before = item_before.as_unlocked();
957

            
958
        let secret = Secret::blob("new_secret");
959
        keyring.change_secret(secret).await?;
960

            
961
        let secret = Secret::blob("new_secret");
962
        let keyring = UnlockedKeyring::load(&keyring_path, secret).await?;
963
        let item_now = keyring.lookup_item(attributes).await?.unwrap();
964
        let item_now = item_now.as_unlocked();
965

            
966
        assert_eq!(item_before.label(), item_now.label());
967
        assert_eq!(item_before.secret(), item_now.secret());
968
        assert_eq!(item_before.attributes(), item_now.attributes());
969

            
970
        // No items were broken during the secret change
971
        assert_eq!(keyring.delete_broken_items().await?, 0);
972

            
973
        fs::remove_file(keyring_path).await?;
974

            
975
        Ok(())
976
    }
977

            
978
    #[tokio::test]
979
    async fn content_type() -> Result<(), Error> {
980
        use crate::secret::ContentType;
981

            
982
        let keyring = UnlockedKeyring::temporary(Secret::blob("test_password")).await?;
983

            
984
        // Add items with different MIME types
985
        keyring
986
            .create_item(
987
                "Text",
988
                &[("type", "text")],
989
                Secret::text("Hello, World!"),
990
                false,
991
            )
992
            .await?;
993

            
994
        keyring
995
            .create_item(
996
                "Password",
997
                &[("type", "password")],
998
                Secret::blob("super_secret_password"),
999
                false,
            )
            .await?;
        let items = keyring.search_items(&[("type", "text")]).await?;
        assert_eq!(items.len(), 1);
        assert_eq!(
            items[0].as_unlocked().secret().content_type(),
            ContentType::Text
        );
        let items = keyring.search_items(&[("type", "password")]).await?;
        assert_eq!(items.len(), 1);
        assert_eq!(
            items[0].as_unlocked().secret().content_type(),
            ContentType::Blob
        );
        Ok(())
    }
    #[tokio::test]
    async fn wrong_password_error_type() -> Result<(), Error> {
        let temp_dir = tempdir().unwrap();
        let keyring_path = temp_dir.path().join("wrong_password_test.keyring");
        let correct_secret = Secret::from("correct-password-that-is-long-enough".as_bytes());
        let wrong_secret = Secret::from("wrong-password-that-is-long-enough".as_bytes());
        // Create a keyring with the correct password
        let keyring = UnlockedKeyring::load(&keyring_path, correct_secret).await?;
        keyring
            .create_item("Test Item", &[("app", "test")], "my-secret", false)
            .await?;
        // Try to load with wrong password
        let result = UnlockedKeyring::load(&keyring_path, wrong_secret).await;
        // Verify this returns IncorrectSecret, not ChecksumMismatch
        assert!(matches!(result, Err(Error::IncorrectSecret)));
        Ok(())
    }
    #[tokio::test]
    async fn comprehensive_search_patterns() -> Result<(), Error> {
        let temp_dir = tempdir().unwrap();
        let keyring_path = temp_dir.path().join("search_test.keyring");
        let keyring = UnlockedKeyring::load(&keyring_path, strong_key()).await?;
        // Create diverse test data
        let test_items = vec![
            (
                "Email Password",
                vec![
                    ("app", "email"),
                    ("user", "alice@example.com"),
                    ("type", "password"),
                ],
            ),
            (
                "Email Token",
                vec![
                    ("app", "email"),
                    ("user", "alice@example.com"),
                    ("type", "token"),
                ],
            ),
            (
                "SSH Key",
                vec![("app", "ssh"), ("user", "alice"), ("type", "key")],
            ),
            (
                "Database Password",
                vec![
                    ("app", "database"),
                    ("env", "production"),
                    ("type", "password"),
                ],
            ),
            (
                "API Key",
                vec![("app", "api"), ("service", "github"), ("type", "key")],
            ),
        ];
        for (i, (label, attrs)) in test_items.iter().enumerate() {
            let attrs_map: HashMap<&str, &str> = attrs.iter().cloned().collect();
            keyring
                .create_item(label, &attrs_map, format!("secret{}", i), false)
                .await?;
        }
        // Test exact match
        let exact = keyring
            .search_items(&[
                ("app", "email"),
                ("user", "alice@example.com"),
                ("type", "password"),
            ])
            .await?;
        assert_eq!(exact.len(), 1);
        assert_eq!(exact[0].as_unlocked().label(), "Email Password");
        // Test partial match - by app
        let email_items = keyring.search_items(&[("app", "email")]).await?;
        assert_eq!(email_items.len(), 2);
        // Test partial match - by type
        let passwords = keyring.search_items(&[("type", "password")]).await?;
        assert_eq!(passwords.len(), 2);
        let keys = keyring.search_items(&[("type", "key")]).await?;
        assert_eq!(keys.len(), 2);
        // Test no match
        let nonexistent = keyring.search_items(&[("app", "nonexistent")]).await?;
        assert_eq!(nonexistent.len(), 0);
        Ok(())
    }
    #[tokio::test]
    async fn item_replacement_behavior() -> Result<(), Error> {
        let temp_dir = tempdir().unwrap();
        let keyring_path = temp_dir.path().join("replace_test.keyring");
        let keyring = UnlockedKeyring::load(&keyring_path, strong_key()).await?;
        let attrs = &[("app", "test"), ("user", "alice")];
        // Create initial item
        keyring
            .create_item("Original", attrs, "secret1", false)
            .await?;
        // Verify initial state
        let items = keyring.search_items(attrs).await?;
        assert_eq!(items.len(), 1);
        assert_eq!(items[0].as_unlocked().label(), "Original");
        assert_eq!(items[0].as_unlocked().secret(), Secret::text("secret1"));
        // With replace=false, allows duplicates (discovered behavior)
        keyring
            .create_item("Duplicate", attrs, "secret2", false)
            .await?;
        // Verify we now have 2 items with same attributes
        let items = keyring.search_items(attrs).await?;
        assert_eq!(items.len(), 2);
        // Verify both items exist with different content
        let labels: Vec<_> = items.iter().map(|i| i.as_unlocked().label()).collect();
        assert!(labels.contains(&"Original"));
        assert!(labels.contains(&"Duplicate"));
        // Now test replace=true behavior - should remove existing items with same
        // attributes
        keyring
            .create_item("Replacement", attrs, "secret3", true)
            .await?;
        // After replace=true, should only have the new item
        let items = keyring.search_items(attrs).await?;
        assert_eq!(items.len(), 1);
        assert_eq!(items[0].as_unlocked().label(), "Replacement");
        assert_eq!(items[0].as_unlocked().secret(), Secret::text("secret3"));
        // Test replace=true on empty attributes (should just add)
        let unique_attrs = &[("app", "unique"), ("user", "bob")];
        keyring
            .create_item("Unique Item", unique_attrs, "unique_secret", true)
            .await?;
        let unique_items = keyring.search_items(unique_attrs).await?;
        assert_eq!(unique_items.len(), 1);
        assert_eq!(unique_items[0].as_unlocked().label(), "Unique Item");
        // Test replace=true again on the unique item - should replace it
        keyring
            .create_item("Updated Unique", unique_attrs, "updated_secret", true)
            .await?;
        let unique_items = keyring.search_items(unique_attrs).await?;
        assert_eq!(unique_items.len(), 1);
        assert_eq!(unique_items[0].as_unlocked().label(), "Updated Unique");
        assert_eq!(
            unique_items[0].as_unlocked().secret(),
            Secret::text("updated_secret")
        );
        Ok(())
    }
    #[tokio::test]
    async fn empty_keyring_operations() -> Result<(), Error> {
        let temp_dir = tempdir().unwrap();
        let keyring_path = temp_dir.path().join("empty_test.keyring");
        let keyring = UnlockedKeyring::load(&keyring_path, strong_key()).await?;
        // Test operations on empty keyring
        let items = keyring.items().await?;
        assert_eq!(items.len(), 0);
        let search_results = keyring.search_items(&[("any", "thing")]).await?;
        assert_eq!(search_results.len(), 0);
        // Delete on empty keyring should succeed
        keyring.delete(&[("nonexistent", "key")]).await?;
        // Verify still empty after delete
        assert_eq!(keyring.n_items().await, 0);
        // Test lookup on empty keyring
        let lookup_result = keyring.lookup_item(&[("test", "value")]).await?;
        assert!(lookup_result.is_none());
        Ok(())
    }
    #[tokio::test]
    async fn secret_types_handling() -> Result<(), Error> {
        let temp_dir = tempdir().unwrap();
        let keyring_path = temp_dir.path().join("secret_types_test.keyring");
        let keyring = UnlockedKeyring::load(&keyring_path, strong_key()).await?;
        // Test text secret
        keyring
            .create_item(
                "Text Secret",
                &[("type", "text")],
                Secret::text("Hello, World!"),
                false,
            )
            .await?;
        // Test binary secret
        keyring
            .create_item(
                "Binary Secret",
                &[("type", "binary")],
                Secret::blob(&[0x00, 0x01, 0x02, 0xFF]),
                false,
            )
            .await?;
        // Test large secret
        let large_data = vec![42u8; 10000];
        keyring
            .create_item(
                "Large Secret",
                &[("type", "large")],
                Secret::blob(&large_data),
                false,
            )
            .await?;
        // Test empty secret
        keyring
            .create_item(
                "Empty Secret",
                &[("type", "empty")],
                Secret::text(""),
                false,
            )
            .await?;
        // Verify all secrets can be retrieved correctly
        let text_items = keyring.search_items(&[("type", "text")]).await?;
        assert_eq!(text_items.len(), 1);
        assert_eq!(
            text_items[0].as_unlocked().secret(),
            Secret::text("Hello, World!")
        );
        assert_eq!(
            text_items[0].as_unlocked().secret().content_type(),
            crate::secret::ContentType::Text
        );
        let binary_items = keyring.search_items(&[("type", "binary")]).await?;
        assert_eq!(binary_items.len(), 1);
        assert_eq!(
            &*binary_items[0].as_unlocked().secret(),
            &[0x00, 0x01, 0x02, 0xFF]
        );
        assert_eq!(
            binary_items[0].as_unlocked().secret().content_type(),
            crate::secret::ContentType::Blob
        );
        let large_items = keyring.search_items(&[("type", "large")]).await?;
        assert_eq!(large_items.len(), 1);
        assert_eq!(&*large_items[0].as_unlocked().secret(), &large_data);
        let empty_items = keyring.search_items(&[("type", "empty")]).await?;
        assert_eq!(empty_items.len(), 1);
        assert_eq!(empty_items[0].as_unlocked().secret(), Secret::text(""));
        Ok(())
    }
    #[tokio::test]
    async fn item_lifecycle_operations() -> Result<(), Error> {
        let temp_dir = tempdir().unwrap();
        let keyring_path = temp_dir.path().join("lifecycle_test.keyring");
        let keyring = UnlockedKeyring::load(&keyring_path, strong_key()).await?;
        // Test creating multiple items
        keyring
            .create_item(
                "Test Item 1",
                &[("app", "myapp"), ("user", "alice")],
                "secret1",
                false,
            )
            .await?;
        keyring
            .create_item(
                "Test Item 2",
                &[("app", "myapp"), ("user", "bob")],
                "secret2",
                false,
            )
            .await?;
        // Test retrieving all items
        let items = keyring.items().await?;
        let valid_items: Vec<_> = items.into_iter().map(|r| r.unwrap()).collect();
        assert_eq!(valid_items.len(), 2);
        // Test searching by user
        let alice_items = keyring.search_items(&[("user", "alice")]).await?;
        assert_eq!(alice_items.len(), 1);
        assert_eq!(alice_items[0].as_unlocked().label(), "Test Item 1");
        assert_eq!(
            alice_items[0].as_unlocked().secret(),
            Secret::text("secret1")
        );
        // Test searching by app (should find both)
        let app_items = keyring.search_items(&[("app", "myapp")]).await?;
        assert_eq!(app_items.len(), 2);
        // Test deleting items
        keyring.delete(&[("user", "alice")]).await?;
        let remaining_items = keyring.items().await?;
        let valid_remaining: Vec<_> = remaining_items.into_iter().map(|r| r.unwrap()).collect();
        assert_eq!(valid_remaining.len(), 1);
        assert_eq!(valid_remaining[0].as_unlocked().label(), "Test Item 2");
        Ok(())
    }
    #[tokio::test]
    async fn item_attribute_operations() -> Result<(), Error> {
        let temp_dir = tempdir().unwrap();
        let keyring_path = temp_dir.path().join("attr_test.keyring");
        let keyring = UnlockedKeyring::load(&keyring_path, strong_key()).await?;
        // Create item with initial attributes
        keyring
            .create_item(
                "Attribute Test",
                &[("app", "testapp"), ("version", "1.0"), ("env", "test")],
                "test-secret",
                false,
            )
            .await?;
        let items = keyring.search_items(&[("app", "testapp")]).await?;
        assert_eq!(items.len(), 1);
        let item = &items[0].as_unlocked();
        // Test reading attributes
        let attrs = item.attributes();
        assert_eq!(attrs.len(), 4); // 3 + xdg:schema
        assert_eq!(attrs.get("app").unwrap().to_string(), "testapp");
        assert_eq!(attrs.get("version").unwrap().to_string(), "1.0");
        assert_eq!(attrs.get("env").unwrap().to_string(), "test");
        // Test updating attributes - need to get item from keyring after update
        let index = keyring
            .lookup_item_index(&[("app", "testapp")])
            .await?
            .unwrap();
        keyring
            .replace_item_index(
                index,
                &crate::file::UnlockedItem::new(
                    "Attribute Test",
                    &[
                        ("app", "testapp"),
                        ("version", "2.0"),        // updated
                        ("env", "production"),     // updated
                        ("new_attr", "new_value"), // added
                    ],
                    item.secret(),
                ),
            )
            .await?;
        let updated_items = keyring.search_items(&[("app", "testapp")]).await?;
        assert_eq!(updated_items.len(), 1);
        let updated_attrs = updated_items[0].as_unlocked().attributes();
        assert_eq!(updated_attrs.get("version").unwrap().to_string(), "2.0");
        assert_eq!(updated_attrs.get("env").unwrap().to_string(), "production");
        assert_eq!(
            updated_attrs.get("new_attr").unwrap().to_string(),
            "new_value"
        );
        Ok(())
    }
    #[tokio::test]
    async fn bulk_create_items() -> Result<(), Error> {
        let temp_dir = tempdir().unwrap();
        let keyring_path = temp_dir.path().join("bulk_create_test.keyring");
        let keyring = UnlockedKeyring::load(&keyring_path, strong_key()).await?;
        // Prepare multiple items to create at once
        let items_to_create = vec![
            (
                "Bulk Item 1".to_string(),
                HashMap::from([
                    ("app".to_string(), "bulk-app".to_string()),
                    ("user".to_string(), "user1".to_string()),
                ]),
                Secret::text("secret1"),
                false,
            ),
            (
                "Bulk Item 2".to_string(),
                HashMap::from([
                    ("app".to_string(), "bulk-app".to_string()),
                    ("user".to_string(), "user2".to_string()),
                ]),
                Secret::text("secret2"),
                false,
            ),
            (
                "Bulk Item 3".to_string(),
                HashMap::from([
                    ("app".to_string(), "bulk-app".to_string()),
                    ("user".to_string(), "user3".to_string()),
                ]),
                Secret::text("secret3"),
                false,
            ),
        ];
        // Create all items in bulk
        keyring.create_items(items_to_create).await?;
        // Verify all items were created
        let all_items = keyring.search_items(&[("app", "bulk-app")]).await?;
        assert_eq!(all_items.len(), 3);
        // Test replace=true in bulk create
        let replace_items = vec![(
            "Replaced Item".to_string(),
            HashMap::from([
                ("app".to_string(), "bulk-app".to_string()),
                ("user".to_string(), "user1".to_string()),
            ]),
            Secret::text("new_secret1"),
            true, // replace=true should remove existing item with same attributes
        )];
        keyring.create_items(replace_items).await?;
        // Verify the item was replaced - should still have 3 items total
        let all_items_after = keyring.search_items(&[("app", "bulk-app")]).await?;
        assert_eq!(all_items_after.len(), 3);
        Ok(())
    }
    #[tokio::test]
    async fn partially_corrupted_keyring_error() -> Result<(), Error> {
        let temp_dir = tempdir().unwrap();
        let keyring_path = temp_dir.path().join("partially_corrupted.keyring");
        // Create keyring with correct password and add 2 valid items
        let correct_secret = Secret::from("correct-password-long-enough".as_bytes());
        let keyring = UnlockedKeyring::load(&keyring_path, correct_secret.clone()).await?;
        keyring
            .create_item("valid1", &[("attr", "value1")], "password1", false)
            .await?;
        keyring
            .create_item("valid2", &[("attr", "value2")], "password2", false)
            .await?;
        drop(keyring);
        // Load_unchecked with wrong password and add 3 broken items (more than valid)
        let wrong_secret = Secret::from("wrong-password-long-enough".as_bytes());
        let keyring =
            unsafe { UnlockedKeyring::load_unchecked(&keyring_path, wrong_secret).await? };
        keyring
            .create_item("broken1", &[("bad", "value1")], "bad_password1", false)
            .await?;
        keyring
            .create_item("broken2", &[("bad", "value2")], "bad_password2", false)
            .await?;
        keyring
            .create_item("broken3", &[("bad", "value3")], "bad_password3", false)
            .await?;
        drop(keyring);
        let result = UnlockedKeyring::load(&keyring_path, correct_secret).await;
        assert!(result.is_err());
        match result.unwrap_err() {
            Error::PartiallyCorruptedKeyring {
                valid_items,
                broken_items,
            } => {
                assert_eq!(valid_items, 2);
                assert_eq!(broken_items, 3);
                assert!(broken_items > valid_items);
            }
            other => panic!("Expected PartiallyCorruptedKeyring, got: {:?}", other),
        }
        Ok(())
    }
    #[tokio::test]
    async fn invalid_item_error_on_decrypt_failure() -> Result<(), Error> {
        let temp_dir = tempdir().unwrap();
        let keyring_path = temp_dir.path().join("invalid_item_test.keyring");
        // 1) Create keyring with correct password and add 2 items
        let correct_secret = Secret::from("correct-password-long-enough".as_bytes());
        let keyring = UnlockedKeyring::load(&keyring_path, correct_secret).await?;
        keyring
            .create_item("item1", &[("app", "test1")], "password1", false)
            .await?;
        keyring
            .create_item("item2", &[("app", "test2")], "password2", false)
            .await?;
        drop(keyring);
        // 2) Load_unchecked with wrong password - items won't decrypt
        let wrong_secret = Secret::from("wrong-password-long-enough".as_bytes());
        let keyring =
            unsafe { UnlockedKeyring::load_unchecked(&keyring_path, wrong_secret).await? };
        let items_result = keyring.items().await?;
        assert_eq!(items_result.len(), 2);
        assert!(matches!(
            items_result[0].as_ref().unwrap_err(),
            InvalidItemError { .. }
        ));
        assert!(matches!(
            items_result[1].as_ref().unwrap_err(),
            InvalidItemError { .. }
        ));
        Ok(())
    }
    #[tokio::test]
    async fn replace_item_index_invalid() -> Result<(), Error> {
        let temp_dir = tempdir().unwrap();
        let keyring_path = temp_dir.path().join("replace_invalid_index.keyring");
        let keyring = UnlockedKeyring::load(&keyring_path, strong_key()).await?;
        // Create one item
        keyring
            .create_item("Test Item", &[("app", "test")], "secret", false)
            .await?;
        // Try to replace at invalid index
        let new_item = UnlockedItem::new("Replacement", &[("app", "test2")], "new_secret");
        let result = keyring.replace_item_index(100, &new_item).await;
        assert!(matches!(result, Err(Error::InvalidItemIndex(100))));
        Ok(())
    }
    #[tokio::test]
    async fn set_attributes() -> Result<(), Error> {
        let data_dir = tempdir().unwrap();
        let dir = data_dir.path().join("keyrings");
        fs::create_dir_all(&dir).await.unwrap();
        let path = dir.join("default.keyring");
        let keyring = UnlockedKeyring::load(&path, strong_key()).await?;
        let items = keyring.items().await?;
        assert_eq!(items.len(), 0);
        keyring
            .create_item("my item", &vec![("key", "value")], "my_secret", false)
            .await?;
        let mut items = keyring.items().await?;
        assert_eq!(items.len(), 1);
        let mut item = items.remove(0).unwrap();
        let item = item.as_mut_unlocked();
        assert_eq!(item.label(), "my item");
        assert_eq!(item.secret(), Secret::text("my_secret"));
        let attrs = item.attributes();
        assert_eq!(attrs.len(), 2);
        assert_eq!(attrs.get("key").unwrap(), "value");
        // Update attributes on the item
        item.set_attributes(&vec![("key", "changed_value"), ("new_key", "new_value")]);
        // Write the updated item back to the keyring at index 0
        keyring.replace_item_index(0, &item).await?;
        // Now retrieve the item again from the keyring to verify the changes persisted
        let mut items = keyring.items().await?;
        assert_eq!(items.len(), 1);
        let item = items.remove(0).unwrap();
        let item = item.as_unlocked();
        assert_eq!(item.label(), "my item");
        assert_eq!(item.secret(), Secret::text("my_secret"));
        let attrs = item.attributes();
        assert_eq!(attrs.len(), 3);
        assert_eq!(attrs.get("key").unwrap(), "changed_value");
        assert_eq!(attrs.get("new_key").unwrap(), "new_value");
        Ok(())
    }
}