1
#[cfg(feature = "async-std")]
2
use std::io;
3
use std::{
4
    path::{Path, PathBuf},
5
    sync::Arc,
6
};
7

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

            
21
use super::{Error, LockedItem, UnlockedKeyring, api};
22
use crate::Secret;
23

            
24
/// A locked keyring that requires a secret to unlock.
25
#[derive(Debug)]
26
pub struct LockedKeyring {
27
    pub(super) keyring: Arc<RwLock<api::Keyring>>,
28
    pub(super) path: Option<PathBuf>,
29
    pub(super) mtime: Mutex<Option<std::time::SystemTime>>,
30
}
31

            
32
impl LockedKeyring {
33
    /// Validate that a secret can decrypt the items in this keyring.
34
    ///
35
    /// For empty keyrings, this always returns `true` since there are no items
36
    /// to validate against.
37
    ///
38
    /// # Arguments
39
    ///
40
    /// * `secret` - The secret to validate.
41
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, secret)))]
42
32
    pub async fn validate_secret(&self, secret: &Secret) -> Result<bool, Error> {
43
16
        let keyring = self.keyring.read().await;
44
16
        Ok(keyring.validate_secret(secret)?)
45
    }
46

            
47
    /// Return the associated file if any.
48
8
    pub fn path(&self) -> Option<&std::path::Path> {
49
8
        self.path.as_deref()
50
    }
51

            
52
    /// Get the modification timestamp
53
16
    pub async fn modified_time(&self) -> std::time::Duration {
54
8
        self.keyring.read().await.modified_time()
55
    }
56

            
57
    /// Retrieve the list of available [`LockedItem`]s without decrypting them.
58
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
59
16
    pub async fn items(&self) -> Result<Vec<LockedItem>, Error> {
60
8
        let keyring = self.keyring.read().await;
61

            
62
12
        Ok(keyring
63
            .items
64
4
            .iter()
65
12
            .map(|encrypted_item| LockedItem {
66
4
                inner: encrypted_item.clone(),
67
            })
68
4
            .collect())
69
    }
70

            
71
    /// Unlocks a keyring and validates it
72
60
    pub async fn unlock(self, secret: Secret) -> Result<UnlockedKeyring, Error> {
73
34
        self.unlock_inner(secret, true).await
74
    }
75

            
76
    /// Unlocks a keyring without validating it
77
    ///
78
    /// # Safety
79
    ///
80
    /// This method skips validation and doesn't verify that the secret can
81
    /// decrypt all items in the keyring. Use only for recovery scenarios where
82
    /// you need to access a partially corrupted keyring. The keyring may
83
    /// contain items that cannot be decrypted with the provided secret.
84
    #[allow(unsafe_code)]
85
8
    pub async unsafe fn unlock_unchecked(self, secret: Secret) -> Result<UnlockedKeyring, Error> {
86
4
        self.unlock_inner(secret, false).await
87
    }
88

            
89
13
    async fn unlock_inner(
90
        self,
91
        secret: Secret,
92
        validate_items: bool,
93
    ) -> Result<UnlockedKeyring, Error> {
94
30
        let key = if validate_items {
95
30
            let inner_keyring = self.keyring.read().await;
96

            
97
30
            let key = inner_keyring.derive_key(&secret)?;
98

            
99
15
            let mut n_broken_items = 0;
100
16
            let mut n_valid_items = 0;
101
29
            for encrypted_item in &inner_keyring.items {
102
36
                if encrypted_item.is_valid(&key) {
103
20
                    n_valid_items += 1;
104
                } else {
105
12
                    n_broken_items += 1;
106
                }
107
            }
108

            
109
15
            drop(inner_keyring);
110

            
111
32
            if n_valid_items == 0 && n_broken_items != 0 {
112
12
                #[cfg(feature = "tracing")]
113
                tracing::error!("Keyring cannot be decrypted. Invalid secret.");
114
6
                return Err(Error::IncorrectSecret);
115
16
            } else if n_broken_items > n_valid_items {
116
                #[cfg(feature = "tracing")]
117
                {
118
4
                    tracing::warn!(
119
                        "The file contains {n_broken_items} broken items and {n_valid_items} valid ones."
120
                    );
121
4
                    tracing::info!(
122
                        "Please switch to `UnlockedKeyring::load_unchecked` to load the keyring without the secret validation.
123
                        `Keyring::delete_broken_items` can be used to remove them or alternatively with `oo7-cli --repair`."
124
                    );
125
                }
126
2
                return Err(Error::PartiallyCorruptedKeyring {
127
2
                    valid_items: n_valid_items,
128
2
                    broken_items: n_broken_items,
129
                });
130
            }
131

            
132
30
            Some(Arc::new(key))
133
        } else {
134
2
            None
135
        };
136

            
137
18
        Ok(UnlockedKeyring {
138
15
            keyring: self.keyring,
139
15
            path: self.path,
140
16
            mtime: self.mtime,
141
16
            key: Mutex::new(key),
142
35
            secret: Mutex::new(Arc::new(secret)),
143
        })
144
    }
145

            
146
    /// Load a keyring from a file path.
147
131
    pub async fn load(path: impl AsRef<Path>) -> Result<Self, Error> {
148
54
        let path = path.as_ref();
149
107
        let (mtime, keyring) = match fs::File::open(&path).await {
150
37
            Err(err) if err.kind() == io::ErrorKind::NotFound => {
151
26
                #[cfg(feature = "tracing")]
152
                tracing::debug!("Keyring file not found, creating a new one");
153
26
                (None, api::Keyring::new()?)
154
            }
155
            Err(err) => return Err(err.into()),
156
14
            Ok(mut file) => {
157
30
                #[cfg(feature = "tracing")]
158
                tracing::debug!("Keyring file found, loading its content");
159
40
                let metadata = file.metadata().await?;
160
14
                let mtime = metadata.modified().ok();
161

            
162
14
                let mut content = Vec::with_capacity(metadata.len() as usize);
163
53
                file.read_to_end(&mut content).await?;
164

            
165
18
                let keyring = api::Keyring::try_from(content.as_slice())?;
166

            
167
14
                (mtime, keyring)
168
            }
169
        };
170

            
171
27
        Ok(Self {
172
50
            keyring: Arc::new(RwLock::new(keyring)),
173
50
            path: Some(path.to_path_buf()),
174
23
            mtime: Mutex::new(mtime),
175
        })
176
    }
177

            
178
    /// Open a named keyring.
179
16
    pub async fn open(name: &str) -> Result<Self, Error> {
180
8
        let v1_path = api::Keyring::path(name, api::MAJOR_VERSION)?;
181
12
        Self::load(v1_path).await
182
    }
183

            
184
    /// Open a locked keyring at a specific data directory.
185
    ///
186
    /// This is useful for tests and cases where you want explicit control over
187
    /// where keyrings are stored, avoiding the default XDG_DATA_HOME location.
188
    ///
189
    /// # Arguments
190
    ///
191
    /// * `data_dir` - Base data directory (keyrings stored in
192
    ///   `data_dir/keyrings/v1/`)
193
    /// * `name` - The name of the keyring.
194
    #[cfg_attr(feature = "tracing", tracing::instrument(fields(data_dir = ?data_dir.as_ref())))]
195
    pub async fn open_at(data_dir: impl AsRef<std::path::Path>, name: &str) -> Result<Self, Error> {
196
        let path = api::Keyring::path_at(data_dir, name, api::MAJOR_VERSION);
197
        Self::load(path).await
198
    }
199
}