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::{Key, 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
    /// Validate that an already-derived key can decrypt at least one item in
48
    /// this keyring.
49
    ///
50
    /// Empty keyrings return `true` because they contain no item with which to
51
    /// authenticate the key. Callers that persist keys for empty keyrings must
52
    /// bind them to the exact keyring file separately.
53
    ///
54
    /// A partially corrupted keyring may return `true` here but still fail
55
    /// [`Self::unlock_with_key`] when broken items outnumber valid items.
56
8
    pub async fn validate_key(&self, key: &Key) -> Result<bool, Error> {
57
6
        key.validate_file_key()?;
58
2
        let keyring = self.keyring.read().await;
59
4
        Ok(keyring.validate_key(key))
60
    }
61

            
62
    pub async fn validate_unencrypted(&self) -> Result<bool, Error> {
63
        let keyring = self.keyring.read().await;
64
        Ok(keyring.validate_unencrypted())
65
    }
66

            
67
    /// Return the associated file if any.
68
8
    pub fn path(&self) -> Option<&std::path::Path> {
69
8
        self.path.as_deref()
70
    }
71

            
72
    /// Get the modification timestamp
73
16
    pub async fn modified_time(&self) -> std::time::Duration {
74
8
        self.keyring.read().await.modified_time()
75
    }
76

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

            
82
12
        Ok(keyring
83
            .items
84
4
            .iter()
85
12
            .map(|encrypted_item| LockedItem {
86
4
                inner: encrypted_item.clone(),
87
            })
88
4
            .collect())
89
    }
90

            
91
    /// Unlocks a keyring and validates it
92
79
    pub async fn unlock(self, secret: Secret) -> Result<UnlockedKeyring, Error> {
93
42
        self.unlock_inner(secret, true).await
94
    }
95

            
96
    /// Unlocks a keyring with an already-derived key and validates it.
97
    ///
98
    /// An exact-length [`Key::new`] value is treated as direct key material and
99
    /// may be used for subsequent writes. The caller is responsible for
100
    /// supplying a key with sufficient entropy.
101
    ///
102
    /// Empty keyrings cannot authenticate the key and therefore accept any key
103
    /// of the required length, matching [`Self::validate_key`].
104
8
    pub async fn unlock_with_key(self, key: Key) -> Result<UnlockedKeyring, Error> {
105
6
        let key = key.into_file_key()?;
106
        let validation = {
107
4
            let inner_keyring = self.keyring.read().await;
108
4
            inner_keyring.validate_items(&key)
109
        };
110
        #[cfg(feature = "tracing")]
111
        Self::log_validation_error(&validation, false);
112
2
        validation?;
113

            
114
2
        Ok(self.into_unlocked(Some(Arc::new(key)), None))
115
    }
116

            
117
    /// Unlocks a keyring without validating it
118
    ///
119
    /// # Safety
120
    ///
121
    /// This method skips validation and doesn't verify that the secret can
122
    /// decrypt all items in the keyring. Use only for recovery scenarios where
123
    /// you need to access a partially corrupted keyring. The keyring may
124
    /// contain items that cannot be decrypted with the provided secret.
125
    #[allow(unsafe_code)]
126
8
    pub async unsafe fn unlock_unchecked(self, secret: Secret) -> Result<UnlockedKeyring, Error> {
127
4
        self.unlock_inner(secret, false).await
128
    }
129

            
130
21
    async fn unlock_inner(
131
        self,
132
        secret: Secret,
133
        validate_items: bool,
134
    ) -> Result<UnlockedKeyring, Error> {
135
23
        let key = if validate_items {
136
42
            let inner_keyring = self.keyring.read().await;
137

            
138
42
            let key = inner_keyring.derive_key(&secret)?;
139
39
            let validation = inner_keyring.validate_items(&key);
140
            #[cfg(feature = "tracing")]
141
            Self::log_validation_error(&validation, true);
142
21
            validation?;
143

            
144
16
            Some(Arc::new(key))
145
        } else {
146
2
            None
147
        };
148

            
149
19
        Ok(self.into_unlocked(key, Some(Arc::new(secret))))
150
    }
151

            
152
    #[cfg(feature = "tracing")]
153
20
    fn log_validation_error(validation: &Result<(), Error>, source_secret: bool) {
154
18
        match validation {
155
8
            Err(Error::IncorrectSecret) if source_secret => {
156
16
                tracing::error!("Keyring cannot be decrypted. Invalid secret.");
157
            }
158
            Err(Error::IncorrectSecret) => {
159
4
                tracing::error!("Keyring cannot be decrypted. Invalid key material.");
160
            }
161
2
            Err(Error::PartiallyCorruptedKeyring {
162
                valid_items,
163
                broken_items,
164
            }) => {
165
4
                tracing::warn!(
166
                    "The file contains {broken_items} broken items and {valid_items} valid ones."
167
                );
168
2
                if source_secret {
169
4
                    tracing::info!(
170
                        "Please switch to `UnlockedKeyring::load_unchecked` to load the keyring without the secret validation.
171
                        `Keyring::delete_broken_items` can be used to remove them or alternatively with `oo7-cli --repair`."
172
                    );
173
                } else {
174
4
                    tracing::info!(
175
                        "Recover the keyring with its source secret; key-based unlock does not bypass validation."
176
                    );
177
                }
178
            }
179
            _ => {}
180
        }
181
    }
182

            
183
19
    fn into_unlocked(self, key: Option<Arc<Key>>, secret: Option<Arc<Secret>>) -> UnlockedKeyring {
184
        UnlockedKeyring {
185
22
            keyring: self.keyring,
186
19
            path: self.path,
187
22
            mtime: self.mtime,
188
19
            key: Mutex::new(key),
189
21
            secret: Mutex::new(secret),
190
        }
191
    }
192

            
193
    /// Unlocks a keyring without a secret, for unencrypted keyrings.
194
    ///
195
    /// Validates that existing items (if any) can be read without
196
    /// encryption. Returns [`Error::IncorrectSecret`] if encrypted items
197
    /// are found.
198
32
    pub async fn unlock_unencrypted(self) -> Result<UnlockedKeyring, Error> {
199
16
        let inner_keyring = self.keyring.read().await;
200
16
        for encrypted_item in &inner_keyring.items {
201
16
            if !encrypted_item.is_valid(None) {
202
2
                return Err(Error::IncorrectSecret);
203
            }
204
        }
205
8
        drop(inner_keyring);
206

            
207
8
        Ok(self.into_unlocked(None, None))
208
    }
209

            
210
    /// Load a keyring from a file path.
211
144
    pub async fn load(path: impl AsRef<Path>) -> Result<Self, Error> {
212
55
        let path = path.as_ref();
213
112
        let (mtime, keyring) = match fs::File::open(&path).await {
214
55
            Err(err) if err.kind() == io::ErrorKind::NotFound => {
215
                #[cfg(feature = "tracing")]
216
                tracing::debug!("Keyring file not found, creating a new one");
217
36
                (None, api::Keyring::new()?)
218
            }
219
            Err(err) => return Err(err.into()),
220
16
            Ok(mut file) => {
221
                #[cfg(feature = "tracing")]
222
                tracing::debug!("Keyring file found, loading its content");
223
47
                let metadata = file.metadata().await?;
224
16
                let mtime = metadata.modified().ok();
225

            
226
16
                let mut content = Vec::with_capacity(metadata.len() as usize);
227
63
                file.read_to_end(&mut content).await?;
228

            
229
20
                let keyring = api::Keyring::try_from(content.as_slice())?;
230

            
231
16
                (mtime, keyring)
232
            }
233
        };
234

            
235
29
        Ok(Self {
236
60
            keyring: Arc::new(RwLock::new(keyring)),
237
60
            path: Some(path.to_path_buf()),
238
31
            mtime: Mutex::new(mtime),
239
        })
240
    }
241

            
242
    /// Open a named keyring.
243
16
    pub async fn open(name: &str) -> Result<Self, Error> {
244
8
        let v1_path = api::Keyring::path(name, api::MAJOR_VERSION)?;
245
12
        Self::load(v1_path).await
246
    }
247

            
248
    /// Open a locked keyring at a specific data directory.
249
    ///
250
    /// This is useful for tests and cases where you want explicit control over
251
    /// where keyrings are stored, avoiding the default XDG_DATA_HOME location.
252
    ///
253
    /// # Arguments
254
    ///
255
    /// * `data_dir` - Base data directory (keyrings stored in
256
    ///   `data_dir/keyrings/v1/`)
257
    /// * `name` - The name of the keyring.
258
    #[cfg_attr(feature = "tracing", tracing::instrument(fields(data_dir = ?data_dir.as_ref())))]
259
    pub async fn open_at(data_dir: impl AsRef<std::path::Path>, name: &str) -> Result<Self, Error> {
260
        let path = api::Keyring::path_at(data_dir, name, api::MAJOR_VERSION);
261
        Self::load(path).await
262
    }
263
}