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
74
    pub async fn load(path: impl AsRef<Path>, secret: Option<Secret>) -> Result<Self, Error> {
50
50
        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
14
    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
45
        let locked = LockedKeyring::load(path).await?;
91
14
        match secret {
92
47
            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
12
            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
168
    pub async fn temporary(secret: Secret) -> Result<Self, Error> {
106
68
        let keyring = api::Keyring::new()?;
107
31
        Ok(Self {
108
64
            keyring: Arc::new(RwLock::new(keyring)),
109
33
            path: None,
110
31
            mtime: Default::default(),
111
33
            key: Default::default(),
112
64
            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
18
        let metadata = file.metadata().await?;
154
6
        let mut content = Vec::with_capacity(metadata.len() as usize);
155
24
        file.read_to_end(&mut content).await?;
156

            
157
18
        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
            // Plain legacy keyrings don't have the binary file header
166
12
            Err(Error::FileHeaderMismatch(_)) => Self::migrate_legacy(&content, path, secret),
167
8
            Err(Error::VersionMismatch(Some(version)))
168
                if version[0] == api::LEGACY_MAJOR_VERSION =>
169
            {
170
12
                Self::migrate_legacy(&content, path, secret)
171
            }
172
            Err(err) => Err(err),
173
        }
174
    }
175

            
176
6
    fn migrate_legacy(
177
        content: &[u8],
178
        path: impl AsRef<Path>,
179
        secret: Option<Secret>,
180
    ) -> Result<Self, Error> {
181
        #[cfg(feature = "tracing")]
182
        tracing::debug!("Migrating from legacy keyring format");
183

            
184
16
        let legacy_keyring = api::LegacyKeyring::try_from(content)?;
185
12
        let mut keyring = api::Keyring::new()?;
186

            
187
24
        let key = secret.as_ref().map(|s| keyring.derive_key(s)).transpose()?;
188
16
        let decrypted_items = legacy_keyring
189
32
            .decrypt_items(&secret.clone().unwrap_or_else(|| Secret::from(vec![])))?;
190

            
191
        #[cfg(feature = "tracing")]
192
        let _migrate_span =
193
            tracing::debug_span!("migrate_items", item_count = decrypted_items.len());
194

            
195
18
        for item in decrypted_items {
196
12
            let encrypted_item = item.encrypt(key.as_ref())?;
197
6
            keyring.items.push(encrypted_item);
198
        }
199

            
200
6
        Ok(Self {
201
6
            keyring: Arc::new(RwLock::new(keyring)),
202
12
            path: Some(path.as_ref().to_path_buf()),
203
6
            mtime: Default::default(),
204
6
            key: Default::default(),
205
12
            secret: Mutex::new(secret.map(Arc::new)),
206
        })
207
    }
208

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

            
223
39
        if v0_path.exists() {
224
            #[cfg(feature = "tracing")]
225
            tracing::debug!("Trying to load keyring file at {:?}", v0_path);
226
18
            match fs::File::open(&v0_path).await {
227
                Err(err) => Err(err.into()),
228
12
                Ok(mut file) => Self::migrate(&mut file, v1_path, secret).await,
229
            }
230
        } else {
231
            #[cfg(feature = "tracing")]
232
            tracing::debug!("Creating new keyring");
233
15
            Ok(Self {
234
39
                keyring: Arc::new(RwLock::new(api::Keyring::new()?)),
235
20
                path: Some(v1_path),
236
15
                mtime: Default::default(),
237
20
                key: Default::default(),
238
35
                secret: Mutex::new(secret.map(Arc::new)),
239
            })
240
        }
241
    }
242

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

            
259
    /// Open a named current-format keyring with an already-derived key.
260
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(key)))]
261
    pub async fn open_with_key(name: &str, key: Key) -> Result<Self, Error> {
262
        let v1_path = api::Keyring::path(name, api::MAJOR_VERSION)?;
263
        Self::load_with_key(v1_path, key).await
264
    }
265

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

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

            
324
    /// Lock the keyring.
325
8
    pub fn lock(self) -> LockedKeyring {
326
        LockedKeyring {
327
8
            keyring: self.keyring,
328
8
            path: self.path,
329
8
            mtime: self.mtime,
330
        }
331
    }
332

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

            
340
    /// Unlock an item using the keyring's key.
341
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, item)))]
342
45
    pub async fn unlock_item(&self, item: LockedItem) -> Result<UnlockedItem, Error> {
343
18
        let key = self.derive_key().await?;
344
9
        item.unlock(key.as_deref())
345
    }
346

            
347
    /// Get the encryption key for this keyring.
348
    ///
349
    /// Returns `None` for unencrypted keyrings.
350
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
351
75
    pub async fn key(&self) -> Result<Option<Arc<Key>>, crate::crypto::Error> {
352
39
        self.derive_key().await
353
    }
354

            
355
    /// Return the associated file if any.
356
31
    pub fn path(&self) -> Option<&std::path::Path> {
357
29
        self.path.as_deref()
358
    }
359

            
360
    /// Get the modification timestamp
361
112
    pub async fn modified_time(&self) -> std::time::Duration {
362
56
        self.keyring.read().await.modified_time()
363
    }
364

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

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

            
387
        #[cfg(feature = "tracing")]
388
        let _span = tracing::debug_span!("decrypt_all", total_items = keyring.items.len());
389

            
390
100
        Ok(keyring
391
            .items
392
33
            .iter()
393
43
            .map(|e| {
394
10
                (*e).clone().decrypt(key.as_deref()).map_err(|err| {
395
2
                    InvalidItemError::new(
396
2
                        err,
397
8
                        e.hashed_attributes.keys().map(|x| x.to_string()).collect(),
398
                    )
399
                })
400
            })
401
32
            .collect())
402
    }
403

            
404
    /// Retrieve the list of available [`UnlockedItem`]s.
405
    ///
406
    /// Items that cannot be decrypted are silently skipped. Use
407
    /// [`all_items()`](Self::all_items) if you need access to decryption
408
    /// errors.
409
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
410
145
    pub async fn items(&self) -> Result<Vec<UnlockedItem>, Error> {
411
91
        Ok(self.all_items().await?.into_iter().flatten().collect())
412
    }
413

            
414
    /// Search items matching the attributes.
415
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, attributes)))]
416
24
    pub async fn search_items(
417
        &self,
418
        attributes: &impl AsAttributes,
419
    ) -> Result<Vec<UnlockedItem>, Error> {
420
48
        let key = self.derive_key().await?;
421
48
        let keyring = self.keyring.read().await;
422
48
        let results = keyring.search_items(attributes, key.as_deref())?;
423

            
424
        #[cfg(feature = "tracing")]
425
        tracing::debug!("Found {} matching items", results.len());
426

            
427
24
        Ok(results)
428
    }
429

            
430
    /// Find the first item matching the attributes.
431
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, attributes)))]
432
2
    pub async fn lookup_item(
433
        &self,
434
        attributes: &impl AsAttributes,
435
    ) -> Result<Option<UnlockedItem>, Error> {
436
4
        let key = self.derive_key().await?;
437
4
        let keyring = self.keyring.read().await;
438

            
439
4
        keyring.lookup_item(attributes, key.as_deref())
440
    }
441

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

            
452
8
        Ok(keyring.lookup_item_index(attributes, key.as_deref()))
453
    }
454

            
455
    /// Delete an item.
456
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, attributes)))]
457
120
    pub async fn delete(&self, attributes: &impl AsAttributes) -> Result<(), Error> {
458
        #[cfg(feature = "tracing")]
459
        let items_before = { self.keyring.read().await.items.len() };
460

            
461
        {
462
27
            let key = self.derive_key().await?;
463
54
            let mut keyring = self.keyring.write().await;
464
55
            keyring.remove_items(attributes, key.as_deref())?;
465
        };
466

            
467
39
        self.write().await?;
468

            
469
        #[cfg(feature = "tracing")]
470
        {
471
            let items_after = self.keyring.read().await.items.len();
472
            let deleted_count = items_before.saturating_sub(items_after);
473
            tracing::info!("Deleted {} items", deleted_count);
474
        }
475

            
476
28
        Ok(())
477
    }
478

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

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

            
533
8
            if let Some(item_store) = keyring.items.get_mut(index) {
534
8
                *item_store = item.encrypt(key.as_deref())?;
535
            } else {
536
2
                return Err(Error::InvalidItemIndex(index));
537
            }
538
        }
539
8
        self.write().await
540
    }
541

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

            
552
4
            if index < keyring.items.len() {
553
4
                keyring.items.remove(index);
554
            } else {
555
2
                return Err(Error::InvalidItemIndex(index));
556
            }
557
        }
558
4
        self.write().await
559
    }
560

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

            
571
        #[cfg(feature = "tracing")]
572
        let _span = tracing::debug_span!("bulk_create", items_to_create = items.len());
573

            
574
12
        for (label, attributes, secret, replace) in items {
575
4
            let item = UnlockedItem::new(label, &attributes, secret);
576
6
            if replace {
577
4
                keyring.remove_items_exact(item.attributes(), key.as_deref())?;
578
            }
579
8
            let encrypted_item = item.encrypt(key.as_deref())?;
580
8
            keyring.items.push(encrypted_item);
581
        }
582

            
583
        #[cfg(feature = "tracing")]
584
        tracing::debug!("Writing keyring back to the file");
585
8
        if let Some(ref path) = self.path {
586
12
            keyring.dump(path, *mtime).await?;
587
            // Update mtime after successful write
588
12
            if let Ok(modified) = fs::metadata(path).await?.modified() {
589
8
                *mtime = Some(modified);
590
            }
591
        }
592
4
        Ok(())
593
    }
594

            
595
    /// Write the changes to the keyring file.
596
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
597
122
    pub async fn write(&self) -> Result<(), Error> {
598
54
        let mut mtime = self.mtime.lock().await;
599
        {
600
53
            let mut keyring = self.keyring.write().await;
601

            
602
49
            if let Some(ref path) = self.path {
603
57
                keyring.dump(path, *mtime).await?;
604
            }
605
        };
606
28
        let Some(ref path) = self.path else {
607
23
            return Ok(());
608
        };
609

            
610
83
        if let Ok(modified) = fs::metadata(path).await?.modified() {
611
42
            *mtime = Some(modified);
612
        }
613
24
        Ok(())
614
    }
615

            
616
    /// Return key, derive and store it first if not initialized.
617
    ///
618
    /// Returns `None` when no secret is set (unencrypted keyring).
619
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
620
225
    async fn derive_key(&self) -> Result<Option<Arc<Key>>, crate::crypto::Error> {
621
        {
622
69
            let key_lock = self.key.lock().await;
623
68
            if key_lock.is_some() {
624
52
                return Ok(key_lock.clone());
625
            }
626
        }
627

            
628
66
        let keyring = Arc::clone(&self.keyring);
629
65
        let secret_lock = self.secret.lock().await;
630
69
        let secret = match secret_lock.as_ref() {
631
62
            Some(secret) => Arc::clone(secret),
632
11
            None => return Ok(None),
633
        };
634
34
        drop(secret_lock);
635

            
636
31
        let mut key_lock = self.key.lock().await;
637
104
        if key_lock.is_none() {
638
            #[cfg(feature = "async-std")]
639
            let key = blocking::unblock(move || {
640
                async_io::block_on(async { keyring.read().await.derive_key(&secret) })
641
            })
642
            .await?;
643
            #[cfg(feature = "tokio")]
644
            let key = {
645
                tokio::task::spawn_blocking(move || keyring.blocking_read().derive_key(&secret))
646
                    .await
647
                    .unwrap()?
648
            };
649

            
650
75
            *key_lock = Some(Arc::new(key));
651
        }
652

            
653
75
        Ok(key_lock.clone())
654
    }
655

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

            
667
        #[cfg(feature = "tracing")]
668
        let _decrypt_span =
669
            tracing::debug_span!("decrypt_for_reencrypt", total_items = keyring.items.len());
670

            
671
12
        for item in &keyring.items {
672
6
            items.push(item.clone().decrypt(key.as_deref())?);
673
        }
674
6
        drop(keyring);
675

            
676
        #[cfg(feature = "tracing")]
677
        tracing::debug!("Updating secret and resetting key");
678

            
679
12
        let mut secret_lock = self.secret.lock().await;
680
6
        *secret_lock = Some(Arc::new(secret));
681
6
        drop(secret_lock);
682

            
683
12
        let mut key_lock = self.key.lock().await;
684
        // Unset the old key
685
6
        *key_lock = None;
686
6
        drop(key_lock);
687

            
688
        // Reset Keyring content before setting the new key
689
12
        let mut keyring = self.keyring.write().await;
690
12
        keyring.reset()?;
691
6
        drop(keyring);
692

            
693
        // Set new key
694
12
        let key = self.derive_key().await?;
695

            
696
        #[cfg(feature = "tracing")]
697
        let _reencrypt_span = tracing::debug_span!("reencrypt", total_items = items.len());
698

            
699
12
        let mut keyring = self.keyring.write().await;
700
18
        for item in items {
701
12
            let encrypted_item = item.encrypt(key.as_deref())?;
702
12
            keyring.items.push(encrypted_item);
703
        }
704
6
        drop(keyring);
705

            
706
18
        self.write().await
707
    }
708

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

            
723
8
    pub async fn validate_unencrypted(&self) -> Result<bool, Error> {
724
4
        let keyring = self.keyring.read().await;
725
4
        Ok(keyring.validate_unencrypted())
726
    }
727

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

            
740
        #[cfg(feature = "tracing")]
741
        let _span = tracing::debug_span!("identify_broken", total_items = keyring.items.len());
742

            
743
4
        for (index, encrypted_item) in keyring.items.iter().enumerate() {
744
4
            if !encrypted_item.is_valid(key.as_deref()) {
745
2
                broken_items.push(index);
746
            }
747
        }
748
2
        let n_broken_items = broken_items.len();
749

            
750
        #[cfg(feature = "tracing")]
751
        tracing::info!("Found {} broken items to delete", n_broken_items);
752

            
753
        #[cfg(feature = "tracing")]
754
        let _remove_span = tracing::debug_span!("remove_broken", broken_count = n_broken_items);
755

            
756
6
        for index in broken_items.into_iter().rev() {
757
4
            keyring.items.remove(index);
758
        }
759
2
        drop(keyring);
760

            
761
4
        self.write().await?;
762
2
        Ok(n_broken_items)
763
    }
764
}