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
20
        let keyring = self.keyring.read().await;
44
16
        Ok(keyring.validate_secret(secret)?)
45
    }
46

            
47
    pub async fn validate_unencrypted(&self) -> Result<bool, Error> {
48
        let keyring = self.keyring.read().await;
49
        Ok(keyring.validate_unencrypted())
50
    }
51

            
52
    /// Return the associated file if any.
53
9
    pub fn path(&self) -> Option<&std::path::Path> {
54
9
        self.path.as_deref()
55
    }
56

            
57
    /// Get the modification timestamp
58
16
    pub async fn modified_time(&self) -> std::time::Duration {
59
10
        self.keyring.read().await.modified_time()
60
    }
61

            
62
    /// Retrieve the list of available [`LockedItem`]s without decrypting them.
63
    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
64
16
    pub async fn items(&self) -> Result<Vec<LockedItem>, Error> {
65
10
        let keyring = self.keyring.read().await;
66

            
67
12
        Ok(keyring
68
            .items
69
4
            .iter()
70
12
            .map(|encrypted_item| LockedItem {
71
4
                inner: encrypted_item.clone(),
72
            })
73
4
            .collect())
74
    }
75

            
76
    /// Unlocks a keyring and validates it
77
72
    pub async fn unlock(self, secret: Secret) -> Result<UnlockedKeyring, Error> {
78
41
        self.unlock_inner(secret, true).await
79
    }
80

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

            
94
20
    async fn unlock_inner(
95
        self,
96
        secret: Secret,
97
        validate_items: bool,
98
    ) -> Result<UnlockedKeyring, Error> {
99
38
        let key = if validate_items {
100
45
            let inner_keyring = self.keyring.read().await;
101

            
102
36
            let key = inner_keyring.derive_key(&secret)?;
103

            
104
18
            let mut n_broken_items = 0;
105
18
            let mut n_valid_items = 0;
106
41
            for encrypted_item in &inner_keyring.items {
107
43
                if encrypted_item.is_valid(Some(&key)) {
108
20
                    n_valid_items += 1;
109
                } else {
110
16
                    n_broken_items += 1;
111
                }
112
            }
113

            
114
20
            drop(inner_keyring);
115

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

            
137
33
            Some(Arc::new(key))
138
        } else {
139
2
            None
140
        };
141

            
142
15
        Ok(UnlockedKeyring {
143
14
            keyring: self.keyring,
144
16
            path: self.path,
145
15
            mtime: self.mtime,
146
17
            key: Mutex::new(key),
147
31
            secret: Mutex::new(Some(Arc::new(secret))),
148
        })
149
    }
150

            
151
    /// Unlocks a keyring without a secret, for unencrypted keyrings.
152
    ///
153
    /// Validates that existing items (if any) can be read without
154
    /// encryption. Returns [`Error::IncorrectSecret`] if encrypted items
155
    /// are found.
156
24
    pub async fn unlock_unencrypted(self) -> Result<UnlockedKeyring, Error> {
157
15
        let inner_keyring = self.keyring.read().await;
158
12
        for encrypted_item in &inner_keyring.items {
159
12
            if !encrypted_item.is_valid(None) {
160
2
                return Err(Error::IncorrectSecret);
161
            }
162
        }
163
6
        drop(inner_keyring);
164

            
165
7
        Ok(UnlockedKeyring {
166
6
            keyring: self.keyring,
167
7
            path: self.path,
168
7
            mtime: self.mtime,
169
7
            key: Mutex::new(None),
170
7
            secret: Mutex::new(None),
171
        })
172
    }
173

            
174
    /// Load a keyring from a file path.
175
142
    pub async fn load(path: impl AsRef<Path>) -> Result<Self, Error> {
176
54
        let path = path.as_ref();
177
116
        let (mtime, keyring) = match fs::File::open(&path).await {
178
38
            Err(err) if err.kind() == io::ErrorKind::NotFound => {
179
                #[cfg(feature = "tracing")]
180
                tracing::debug!("Keyring file not found, creating a new one");
181
25
                (None, api::Keyring::new()?)
182
            }
183
            Err(err) => return Err(err.into()),
184
16
            Ok(mut file) => {
185
                #[cfg(feature = "tracing")]
186
                tracing::debug!("Keyring file found, loading its content");
187
52
                let metadata = file.metadata().await?;
188
16
                let mtime = metadata.modified().ok();
189

            
190
16
                let mut content = Vec::with_capacity(metadata.len() as usize);
191
63
                file.read_to_end(&mut content).await?;
192

            
193
20
                let keyring = api::Keyring::try_from(content.as_slice())?;
194

            
195
16
                (mtime, keyring)
196
            }
197
        };
198

            
199
30
        Ok(Self {
200
54
            keyring: Arc::new(RwLock::new(keyring)),
201
54
            path: Some(path.to_path_buf()),
202
26
            mtime: Mutex::new(mtime),
203
        })
204
    }
205

            
206
    /// Open a named keyring.
207
16
    pub async fn open(name: &str) -> Result<Self, Error> {
208
8
        let v1_path = api::Keyring::path(name, api::MAJOR_VERSION)?;
209
14
        Self::load(v1_path).await
210
    }
211

            
212
    /// Open a locked keyring at a specific data directory.
213
    ///
214
    /// This is useful for tests and cases where you want explicit control over
215
    /// where keyrings are stored, avoiding the default XDG_DATA_HOME location.
216
    ///
217
    /// # Arguments
218
    ///
219
    /// * `data_dir` - Base data directory (keyrings stored in
220
    ///   `data_dir/keyrings/v1/`)
221
    /// * `name` - The name of the keyring.
222
    #[cfg_attr(feature = "tracing", tracing::instrument(fields(data_dir = ?data_dir.as_ref())))]
223
    pub async fn open_at(data_dir: impl AsRef<std::path::Path>, name: &str) -> Result<Self, Error> {
224
        let path = api::Keyring::path_at(data_dir, name, api::MAJOR_VERSION);
225
        Self::load(path).await
226
    }
227
}