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, LockedItem, LockedKeyring, UnlockedItem, api},
23
};
24

            
25
/// Definition for batch item creation: (label, attributes, secret, replace)
26
pub type ItemDefinition = (String, HashMap<String, String>, Secret, bool);
27

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

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

            
53
    /// Load and unlock a keyring with an already-derived key.
54
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(key), fields(path = ?path.as_ref())))]
55
24
    pub async fn load_with_key(path: impl AsRef<Path>, key: Key) -> Result<Self, Error> {
56
12
        LockedKeyring::load(path).await?.unlock_with_key(key).await
57
    }
58

            
59
    /// Load from a keyring file without validating the secret.
60
    ///
61
    /// # Arguments
62
    ///
63
    /// * `path` - The path to the file backend.
64
    /// * `secret` - The service key, usually retrieved from the Secrets portal.
65
    ///
66
    /// # Safety
67
    ///
68
    /// This method skips validation and doesn't verify that the secret can
69
    /// decrypt all items in the keyring. Use only for recovery scenarios where
70
    /// you need to access a partially corrupted keyring. The keyring may
71
    /// contain items that cannot be decrypted with the provided secret, and
72
    /// writing new items may use a different secret than existing items.
73
    #[allow(unsafe_code)]
74
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(secret), fields(path = ?path.as_ref())))]
75
2
    pub async unsafe fn load_unchecked(
76
        path: impl AsRef<Path>,
77
        secret: Secret,
78
    ) -> Result<Self, Error> {
79
6
        Self::load_inner(path, Some(secret), false).await
80
    }
81

            
82
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(secret), fields(path = ?path.as_ref(), validate_items = validate_items)))]
83
17
    async fn load_inner(
84
        path: impl AsRef<Path>,
85
        secret: Option<Secret>,
86
        validate_items: bool,
87
    ) -> Result<Self, Error> {
88
        #[cfg(feature = "tracing")]
89
        tracing::debug!("Trying to load keyring file at {:?}", path.as_ref());
90
49
        let locked = LockedKeyring::load(path).await?;
91
19
        match secret {
92
46
            Some(secret) if validate_items => locked.unlock(secret).await,
93
2
            Some(secret) => {
94
                #[allow(unsafe_code)]
95
                unsafe {
96
4
                    locked.unlock_unchecked(secret).await
97
                }
98
            }
99
8
            None => locked.unlock_unencrypted().await,
100
        }
101
    }
102

            
103
    /// Creates a temporary backend, that is never stored on disk.
104
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(secret)))]
105
172
    pub async fn temporary(secret: Secret) -> Result<Self, Error> {
106
69
        let keyring = api::Keyring::new()?;
107
36
        Ok(Self {
108
72
            keyring: Arc::new(RwLock::new(keyring)),
109
35
            path: None,
110
35
            mtime: Default::default(),
111
33
            key: Default::default(),
112
67
            secret: Mutex::new(Some(Arc::new(secret))),
113
        })
114
    }
115

            
116
    /// Creates a temporary unencrypted backend, that is never stored on disk.
117
6
    pub async fn temporary_unencrypted() -> Result<Self, Error> {
118
4
        let keyring = api::Keyring::new()?;
119
2
        Ok(Self {
120
4
            keyring: Arc::new(RwLock::new(keyring)),
121
2
            path: None,
122
2
            mtime: Default::default(),
123
2
            key: Default::default(),
124
2
            secret: Mutex::new(None),
125
        })
126
    }
127

            
128
    /// Load a v0 (legacy gnome-keyring) file and migrate it to v1 format.
129
    ///
130
    /// The migrated keyring will be written to `target_path` when
131
    /// [`write()`](Self::write) is called.
132
    ///
133
    /// # Arguments
134
    ///
135
    /// * `source` - Path to the legacy v0 keyring file.
136
    /// * `target_path` - Where the v1 keyring should be stored.
137
    /// * `secret` - The encryption secret, or `None` for unencrypted keyrings.
138
    pub async fn load_from_v0(
139
        source: impl AsRef<Path>,
140
        target_path: impl Into<PathBuf>,
141
        secret: Option<Secret>,
142
    ) -> Result<Self, Error> {
143
        let mut file = fs::File::open(source.as_ref()).await?;
144
        Self::migrate(&mut file, target_path.into(), secret).await
145
    }
146

            
147
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(file, secret), fields(path = ?path.as_ref())))]
148
6
    async fn migrate(
149
        file: &mut fs::File,
150
        path: impl AsRef<Path>,
151
        secret: Option<Secret>,
152
    ) -> Result<Self, Error> {
153
21
        let metadata = file.metadata().await?;
154
7
        let mut content = Vec::with_capacity(metadata.len() as usize);
155
28
        file.read_to_end(&mut content).await?;
156

            
157
20
        match api::Keyring::try_from(content.as_slice()) {
158
4
            Ok(keyring) => Ok(Self {
159
4
                keyring: Arc::new(RwLock::new(keyring)),
160
4
                path: Some(path.as_ref().to_path_buf()),
161
2
                mtime: Default::default(),
162
2
                key: Default::default(),
163
4
                secret: Mutex::new(secret.map(Arc::new)),
164
            }),
165
9
            Err(Error::VersionMismatch(Some(version)))
166
                if version[0] == api::LEGACY_MAJOR_VERSION =>
167
            {
168
                #[cfg(feature = "tracing")]
169
                tracing::debug!("Migrating from legacy keyring format");
170

            
171
12
                let legacy_keyring = api::LegacyKeyring::try_from(content.as_slice())?;
172
12
                let mut keyring = api::Keyring::new()?;
173

            
174
6
                let key = secret
175
                    .as_ref()
176
18
                    .map(|s| keyring.derive_key_unchecked(s))
177
                    .transpose()?;
178
16
                let decrypted_items = legacy_keyring
179
28
                    .decrypt_items(&secret.clone().unwrap_or_else(|| Secret::from(vec![])))?;
180

            
181
                #[cfg(feature = "tracing")]
182
                let _migrate_span =
183
                    tracing::debug_span!("migrate_items", item_count = decrypted_items.len());
184

            
185
18
                for item in decrypted_items {
186
12
                    let encrypted_item = item.encrypt(key.as_ref())?;
187
6
                    keyring.items.push(encrypted_item);
188
                }
189

            
190
6
                Ok(Self {
191
6
                    keyring: Arc::new(RwLock::new(keyring)),
192
12
                    path: Some(path.as_ref().to_path_buf()),
193
6
                    mtime: Default::default(),
194
6
                    key: Default::default(),
195
12
                    secret: Mutex::new(secret.map(Arc::new)),
196
                })
197
            }
198
            Err(err) => Err(err),
199
        }
200
    }
201

            
202
    /// Helper for opening/creating keyrings with explicit paths.
203
    ///
204
    /// Handles v0 -> v1 migration automatically.
205
18
    async fn open_with_paths(
206
        v1_path: PathBuf,
207
        v0_path: PathBuf,
208
        secret: Option<Secret>,
209
    ) -> Result<Self, Error> {
210
34
        if v1_path.exists() {
211
            #[cfg(feature = "tracing")]
212
            tracing::debug!("Loading v1 keyring file");
213
6
            return Self::load(v1_path, secret).await;
214
        }
215

            
216
38
        if v0_path.exists() {
217
            #[cfg(feature = "tracing")]
218
            tracing::debug!("Trying to load keyring file at {:?}", v0_path);
219
18
            match fs::File::open(&v0_path).await {
220
                Err(err) => Err(err.into()),
221
12
                Ok(mut file) => Self::migrate(&mut file, v1_path, secret).await,
222
            }
223
        } else {
224
            #[cfg(feature = "tracing")]
225
            tracing::debug!("Creating new keyring");
226
18
            Ok(Self {
227
38
                keyring: Arc::new(RwLock::new(api::Keyring::new()?)),
228
19
                path: Some(v1_path),
229
17
                mtime: Default::default(),
230
18
                key: Default::default(),
231
34
                secret: Mutex::new(secret.map(Arc::new)),
232
            })
233
        }
234
    }
235

            
236
    /// Open a keyring with given name from the default directory.
237
    ///
238
    /// This function will automatically migrate the keyring to the
239
    /// latest format.
240
    ///
241
    /// # Arguments
242
    ///
243
    /// * `name` - The name of the keyring.
244
    /// * `secret` - The service key, usually retrieved from the Secrets portal.
245
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(secret)))]
246
    pub async fn open(name: &str, secret: Option<Secret>) -> Result<Self, Error> {
247
        let v1_path = api::Keyring::path(name, api::MAJOR_VERSION)?;
248
        let v0_path = api::Keyring::path(name, api::LEGACY_MAJOR_VERSION)?;
249
        Self::open_with_paths(v1_path, v0_path, secret).await
250
    }
251

            
252
    /// Open a named current-format keyring with an already-derived key.
253
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(key)))]
254
    pub async fn open_with_key(name: &str, key: Key) -> Result<Self, Error> {
255
        let v1_path = api::Keyring::path(name, api::MAJOR_VERSION)?;
256
        Self::load_with_key(v1_path, key).await
257
    }
258

            
259
    /// Open or create a keyring at a specific data directory.
260
    ///
261
    /// This is useful for tests and cases where you want explicit control over
262
    /// where keyrings are stored, avoiding the default XDG_DATA_HOME location.
263
    ///
264
    /// This function will automatically migrate the keyring to the latest
265
    /// format.
266
    ///
267
    /// # Arguments
268
    ///
269
    /// * `data_dir` - Base data directory (keyrings stored in
270
    ///   `data_dir/keyrings/v1/`)
271
    /// * `name` - The name of the keyring.
272
    /// * `secret` - The service key, usually retrieved from the Secrets portal.
273
    ///
274
    /// # Example
275
    ///
276
    /// ```no_run
277
    /// # use oo7::{Secret, file::UnlockedKeyring};
278
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
279
    /// let temp_dir = tempfile::tempdir()?;
280
    /// let keyring = UnlockedKeyring::open_at(
281
    ///     temp_dir.path(),
282
    ///     "test-keyring",
283
    ///     Some(Secret::from("password")),
284
    /// )
285
    /// .await?;
286
    /// keyring
287
    ///     .create_item("item", &[("attr", "value")], Secret::text("secret"), false)
288
    ///     .await?;
289
    /// keyring.write().await?; // Writes to temp_dir/keyrings/v1/test-keyring.keyring
290
    /// //
291
    /// # Ok(())
292
    /// # }
293
    /// ```
294
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(secret), fields(data_dir = ?data_dir.as_ref())))]
295
24
    pub async fn open_at(
296
        data_dir: impl AsRef<Path>,
297
        name: &str,
298
        secret: Option<Secret>,
299
    ) -> Result<Self, Error> {
300
23
        let v1_path = api::Keyring::path_at(&data_dir, name, api::MAJOR_VERSION);
301
22
        let v0_path = api::Keyring::path_at(&data_dir, name, api::LEGACY_MAJOR_VERSION);
302
26
        Self::open_with_paths(v1_path, v0_path, secret).await
303
    }
304

            
305
    /// Open a named current-format keyring at a specific data directory with
306
    /// an already-derived key.
307
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(key), fields(data_dir = ?data_dir.as_ref())))]
308
2
    pub async fn open_at_with_key(
309
        data_dir: impl AsRef<Path>,
310
        name: &str,
311
        key: Key,
312
    ) -> Result<Self, Error> {
313
2
        let v1_path = api::Keyring::path_at(&data_dir, name, api::MAJOR_VERSION);
314
4
        Self::load_with_key(v1_path, key).await
315
    }
316

            
317
    /// Lock the keyring.
318
8
    pub fn lock(self) -> LockedKeyring {
319
        LockedKeyring {
320
8
            keyring: self.keyring,
321
8
            path: self.path,
322
8
            mtime: self.mtime,
323
        }
324
    }
325

            
326
    /// Lock an item using the keyring's key.
327
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, item)))]
328
40
    pub async fn lock_item(&self, item: UnlockedItem) -> Result<LockedItem, Error> {
329
16
        let key = self.derive_key().await?;
330
8
        item.lock(key.as_deref())
331
    }
332

            
333
    /// Unlock an item using the keyring's key.
334
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, item)))]
335
45
    pub async fn unlock_item(&self, item: LockedItem) -> Result<UnlockedItem, Error> {
336
18
        let key = self.derive_key().await?;
337
9
        item.unlock(key.as_deref())
338
    }
339

            
340
    /// Get the encryption key for this keyring.
341
    ///
342
    /// Returns `None` for unencrypted keyrings.
343
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
344
72
    pub async fn key(&self) -> Result<Option<Arc<Key>>, crate::crypto::Error> {
345
37
        self.derive_key().await
346
    }
347

            
348
    /// Return the associated file if any.
349
29
    pub fn path(&self) -> Option<&std::path::Path> {
350
27
        self.path.as_deref()
351
    }
352

            
353
    /// Get the modification timestamp
354
108
    pub async fn modified_time(&self) -> std::time::Duration {
355
53
        self.keyring.read().await.modified_time()
356
    }
357

            
358
    /// Retrieve the number of items
359
    ///
360
    /// This function will not trigger a key derivation and can therefore be
361
    /// faster than [`items().len()`](Self::items).
362
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
363
16
    pub async fn n_items(&self) -> usize {
364
12
        self.keyring.read().await.items.len()
365
    }
366

            
367
    /// Retrieve all items including those that cannot be decrypted.
368
    ///
369
    /// Returns a [`Vec`] where each element is either an [`UnlockedItem`] or an
370
    /// [`InvalidItemError`] for items that failed to decrypt.
371
    ///
372
    /// Use this method when you need to know about or handle decryption
373
    /// failures. For most use cases, [`items()`](Self::items) is more
374
    /// convenient as it only returns successfully decrypted items.
375
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
376
147
    pub async fn all_items(&self) -> Result<Vec<Result<UnlockedItem, InvalidItemError>>, Error> {
377
92
        let key = self.derive_key().await?;
378
76
        let keyring = self.keyring.read().await;
379

            
380
        #[cfg(feature = "tracing")]
381
        let _span = tracing::debug_span!("decrypt_all", total_items = keyring.items.len());
382

            
383
114
        Ok(keyring
384
            .items
385
38
            .iter()
386
46
            .map(|e| {
387
10
                (*e).clone().decrypt(key.as_deref()).map_err(|err| {
388
2
                    InvalidItemError::new(
389
2
                        err,
390
8
                        e.hashed_attributes.keys().map(|x| x.to_string()).collect(),
391
                    )
392
                })
393
            })
394
38
            .collect())
395
    }
396

            
397
    /// Retrieve the list of available [`UnlockedItem`]s.
398
    ///
399
    /// Items that cannot be decrypted are silently skipped. Use
400
    /// [`all_items()`](Self::all_items) if you need access to decryption
401
    /// errors.
402
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
403
155
    pub async fn items(&self) -> Result<Vec<UnlockedItem>, Error> {
404
96
        Ok(self.all_items().await?.into_iter().flatten().collect())
405
    }
406

            
407
    /// Search items matching the attributes.
408
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, attributes)))]
409
24
    pub async fn search_items(
410
        &self,
411
        attributes: &impl AsAttributes,
412
    ) -> Result<Vec<UnlockedItem>, Error> {
413
48
        let key = self.derive_key().await?;
414
48
        let keyring = self.keyring.read().await;
415
48
        let results = keyring.search_items(attributes, key.as_deref())?;
416

            
417
        #[cfg(feature = "tracing")]
418
        tracing::debug!("Found {} matching items", results.len());
419

            
420
24
        Ok(results)
421
    }
422

            
423
    /// Find the first item matching the attributes.
424
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, attributes)))]
425
2
    pub async fn lookup_item(
426
        &self,
427
        attributes: &impl AsAttributes,
428
    ) -> Result<Option<UnlockedItem>, Error> {
429
4
        let key = self.derive_key().await?;
430
4
        let keyring = self.keyring.read().await;
431

            
432
4
        keyring.lookup_item(attributes, key.as_deref())
433
    }
434

            
435
    /// Find the index in the list of items of the first item matching the
436
    /// attributes.
437
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, attributes)))]
438
4
    pub async fn lookup_item_index(
439
        &self,
440
        attributes: &impl AsAttributes,
441
    ) -> Result<Option<usize>, Error> {
442
8
        let key = self.derive_key().await?;
443
8
        let keyring = self.keyring.read().await;
444

            
445
8
        Ok(keyring.lookup_item_index(attributes, key.as_deref()))
446
    }
447

            
448
    /// Delete an item.
449
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, attributes)))]
450
114
    pub async fn delete(&self, attributes: &impl AsAttributes) -> Result<(), Error> {
451
        #[cfg(feature = "tracing")]
452
        let items_before = { self.keyring.read().await.items.len() };
453

            
454
        {
455
25
            let key = self.derive_key().await?;
456
52
            let mut keyring = self.keyring.write().await;
457
54
            keyring.remove_items(attributes, key.as_deref())?;
458
        };
459

            
460
42
        self.write().await?;
461

            
462
        #[cfg(feature = "tracing")]
463
        {
464
            let items_after = self.keyring.read().await.items.len();
465
            let deleted_count = items_before.saturating_sub(items_after);
466
            tracing::info!("Deleted {} items", deleted_count);
467
        }
468

            
469
27
        Ok(())
470
    }
471

            
472
    /// Create a new item
473
    ///
474
    /// # Arguments
475
    ///
476
    /// * `label` - A user visible label of the item.
477
    /// * `attributes` - A map of key/value attributes, used to find the item
478
    ///   later.
479
    /// * `secret` - The secret to store.
480
    /// * `replace` - Whether to replace the value if the `attributes` matches
481
    ///   an existing `secret`.
482
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, secret, attributes), fields(replace = replace)))]
483
52
    pub async fn create_item(
484
        &self,
485
        label: &str,
486
        attributes: &impl AsAttributes,
487
        secret: impl Into<Secret>,
488
        replace: bool,
489
    ) -> Result<UnlockedItem, Error> {
490
        let item = {
491
126
            let key = self.derive_key().await?;
492
107
            let mut keyring = self.keyring.write().await;
493
53
            let item = UnlockedItem::new(label, attributes, secret);
494
80
            if replace {
495
53
                keyring.remove_items_exact(item.attributes(), key.as_deref())?;
496
            }
497
105
            let encrypted_item = item.encrypt(key.as_deref())?;
498
106
            keyring.items.push(encrypted_item);
499
53
            item
500
        };
501
206
        match self.write().await {
502
            Err(e) => {
503
                #[cfg(feature = "tracing")]
504
                tracing::error!("Failed to write keyring after item creation");
505
                Err(e)
506
            }
507
            Ok(_) => {
508
                #[cfg(feature = "tracing")]
509
                tracing::info!("Successfully created item");
510
57
                Ok(item)
511
            }
512
        }
513
    }
514

            
515
    /// Replaces item at the given index.
516
    ///
517
    /// The `index` refers to the index of the [`Vec`] returned by
518
    /// [`items()`](Self::items). If the index does not exist, the functions
519
    /// returns an error.
520
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, item), fields(index = index)))]
521
20
    pub async fn replace_item_index(&self, index: usize, item: &UnlockedItem) -> Result<(), Error> {
522
        {
523
8
            let key = self.derive_key().await?;
524
8
            let mut keyring = self.keyring.write().await;
525

            
526
8
            if let Some(item_store) = keyring.items.get_mut(index) {
527
8
                *item_store = item.encrypt(key.as_deref())?;
528
            } else {
529
2
                return Err(Error::InvalidItemIndex(index));
530
            }
531
        }
532
8
        self.write().await
533
    }
534

            
535
    /// Deletes item at the given index.
536
    ///
537
    /// The `index` refers to the index of the [`Vec`] returned by
538
    /// [`items()`](Self::items). If the index does not exist, the functions
539
    /// returns an error.
540
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(index = index)))]
541
10
    pub async fn delete_item_index(&self, index: usize) -> Result<(), Error> {
542
        {
543
4
            let mut keyring = self.keyring.write().await;
544

            
545
4
            if index < keyring.items.len() {
546
4
                keyring.items.remove(index);
547
            } else {
548
2
                return Err(Error::InvalidItemIndex(index));
549
            }
550
        }
551
4
        self.write().await
552
    }
553

            
554
    /// Create multiple items in a single operation to avoid re-writing the file
555
    /// multiple times.
556
    ///
557
    /// This is more efficient than calling `create_item()` multiple times.
558
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, items), fields(item_count = items.len())))]
559
24
    pub async fn create_items(&self, items: Vec<ItemDefinition>) -> Result<(), Error> {
560
8
        let key = self.derive_key().await?;
561
8
        let mut mtime = self.mtime.lock().await;
562
8
        let mut keyring = self.keyring.write().await;
563

            
564
        #[cfg(feature = "tracing")]
565
        let _span = tracing::debug_span!("bulk_create", items_to_create = items.len());
566

            
567
12
        for (label, attributes, secret, replace) in items {
568
4
            let item = UnlockedItem::new(label, &attributes, secret);
569
6
            if replace {
570
4
                keyring.remove_items_exact(item.attributes(), key.as_deref())?;
571
            }
572
8
            let encrypted_item = item.encrypt(key.as_deref())?;
573
8
            keyring.items.push(encrypted_item);
574
        }
575

            
576
        #[cfg(feature = "tracing")]
577
        tracing::debug!("Writing keyring back to the file");
578
8
        if let Some(ref path) = self.path {
579
12
            keyring.dump(path, *mtime).await?;
580
            // Update mtime after successful write
581
12
            if let Ok(modified) = fs::metadata(path).await?.modified() {
582
8
                *mtime = Some(modified);
583
            }
584
        }
585
4
        Ok(())
586
    }
587

            
588
    /// Write the changes to the keyring file.
589
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
590
116
    pub async fn write(&self) -> Result<(), Error> {
591
56
        let mut mtime = self.mtime.lock().await;
592
        {
593
54
            let mut keyring = self.keyring.write().await;
594

            
595
41
            if let Some(ref path) = self.path {
596
60
                keyring.dump(path, *mtime).await?;
597
            }
598
        };
599
27
        let Some(ref path) = self.path else {
600
20
            return Ok(());
601
        };
602

            
603
85
        if let Ok(modified) = fs::metadata(path).await?.modified() {
604
47
            *mtime = Some(modified);
605
        }
606
22
        Ok(())
607
    }
608

            
609
    /// Return key, derive and store it first if not initialized.
610
    ///
611
    /// Returns `None` when no secret is set (unencrypted keyring).
612
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
613
229
    async fn derive_key(&self) -> Result<Option<Arc<Key>>, crate::crypto::Error> {
614
        {
615
71
            let key_lock = self.key.lock().await;
616
71
            if key_lock.is_some() {
617
39
                return Ok(key_lock.clone());
618
            }
619
        }
620

            
621
67
        let keyring = Arc::clone(&self.keyring);
622
68
        let secret_lock = self.secret.lock().await;
623
68
        let secret = match secret_lock.as_ref() {
624
64
            Some(secret) => Arc::clone(secret),
625
9
            None => return Ok(None),
626
        };
627
29
        drop(secret_lock);
628

            
629
34
        let mut key_lock = self.key.lock().await;
630
108
        if key_lock.is_none() {
631
            #[cfg(feature = "async-std")]
632
            let key = blocking::unblock(move || {
633
                async_io::block_on(async { keyring.read().await.derive_key(&secret) })
634
            })
635
            .await?;
636
            #[cfg(feature = "tokio")]
637
            let key = {
638
                tokio::task::spawn_blocking(move || keyring.blocking_read().derive_key(&secret))
639
                    .await
640
                    .unwrap()?
641
            };
642

            
643
84
            *key_lock = Some(Arc::new(key));
644
        }
645

            
646
84
        Ok(key_lock.clone())
647
    }
648

            
649
    /// Change keyring secret
650
    ///
651
    /// # Arguments
652
    ///
653
    /// * `secret` - The new secret to store.
654
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, secret)))]
655
36
    pub async fn change_secret(&self, secret: Secret) -> Result<(), Error> {
656
12
        let keyring = self.keyring.read().await;
657
12
        let key = self.derive_key().await?;
658
12
        let mut items = Vec::with_capacity(keyring.items.len());
659

            
660
        #[cfg(feature = "tracing")]
661
        let _decrypt_span =
662
            tracing::debug_span!("decrypt_for_reencrypt", total_items = keyring.items.len());
663

            
664
12
        for item in &keyring.items {
665
6
            items.push(item.clone().decrypt(key.as_deref())?);
666
        }
667
6
        drop(keyring);
668

            
669
        #[cfg(feature = "tracing")]
670
        tracing::debug!("Updating secret and resetting key");
671

            
672
12
        let mut secret_lock = self.secret.lock().await;
673
6
        *secret_lock = Some(Arc::new(secret));
674
6
        drop(secret_lock);
675

            
676
12
        let mut key_lock = self.key.lock().await;
677
        // Unset the old key
678
6
        *key_lock = None;
679
6
        drop(key_lock);
680

            
681
        // Reset Keyring content before setting the new key
682
12
        let mut keyring = self.keyring.write().await;
683
12
        keyring.reset()?;
684
6
        drop(keyring);
685

            
686
        // Set new key
687
13
        let key = self.derive_key().await?;
688

            
689
        #[cfg(feature = "tracing")]
690
        let _reencrypt_span = tracing::debug_span!("reencrypt", total_items = items.len());
691

            
692
12
        let mut keyring = self.keyring.write().await;
693
18
        for item in items {
694
12
            let encrypted_item = item.encrypt(key.as_deref())?;
695
12
            keyring.items.push(encrypted_item);
696
        }
697
6
        drop(keyring);
698

            
699
18
        self.write().await
700
    }
701

            
702
    /// Validate that a secret can decrypt the items in this keyring.
703
    ///
704
    /// For empty keyrings, this always returns `true` since there are no items
705
    /// to validate against.
706
    ///
707
    /// # Arguments
708
    ///
709
    /// * `secret` - The secret to validate.
710
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, secret)))]
711
16
    pub async fn validate_secret(&self, secret: &Secret) -> Result<bool, Error> {
712
8
        let keyring = self.keyring.read().await;
713
8
        Ok(keyring.validate_secret(secret)?)
714
    }
715

            
716
8
    pub async fn validate_unencrypted(&self) -> Result<bool, Error> {
717
4
        let keyring = self.keyring.read().await;
718
4
        Ok(keyring.validate_unencrypted())
719
    }
720

            
721
    /// Delete any item that cannot be decrypted with the key associated to the
722
    /// keyring.
723
    ///
724
    /// This can only happen if an item was created using
725
    /// [`Self::load_unchecked`] or prior to 0.4 where we didn't validate
726
    /// the secret when using [`Self::load`] or modified externally.
727
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
728
12
    pub async fn delete_broken_items(&self) -> Result<usize, Error> {
729
4
        let key = self.derive_key().await?;
730
4
        let mut keyring = self.keyring.write().await;
731
2
        let mut broken_items = vec![];
732

            
733
        #[cfg(feature = "tracing")]
734
        let _span = tracing::debug_span!("identify_broken", total_items = keyring.items.len());
735

            
736
4
        for (index, encrypted_item) in keyring.items.iter().enumerate() {
737
4
            if !encrypted_item.is_valid(key.as_deref()) {
738
2
                broken_items.push(index);
739
            }
740
        }
741
2
        let n_broken_items = broken_items.len();
742

            
743
        #[cfg(feature = "tracing")]
744
        tracing::info!("Found {} broken items to delete", n_broken_items);
745

            
746
        #[cfg(feature = "tracing")]
747
        let _remove_span = tracing::debug_span!("remove_broken", broken_count = n_broken_items);
748

            
749
6
        for index in broken_items.into_iter().rev() {
750
4
            keyring.items.remove(index);
751
        }
752
2
        drop(keyring);
753

            
754
4
        self.write().await?;
755
2
        Ok(n_broken_items)
756
    }
757
}