1
//! GNOME Keyring file format low level API.
2

            
3
// TODO:
4
// - Order user calls
5
// - Keep proxis around
6
// - Make more things async
7

            
8
#[cfg(feature = "async-std")]
9
use std::io;
10
use std::{
11
    path::{Path, PathBuf},
12
    sync::LazyLock,
13
};
14

            
15
#[cfg(feature = "async-std")]
16
use async_fs as fs;
17
#[cfg(feature = "async-std")]
18
use async_fs::unix::{DirBuilderExt, OpenOptionsExt};
19
#[cfg(feature = "async-std")]
20
use futures_lite::AsyncWriteExt;
21
use rand::Rng;
22
use serde::{Deserialize, Serialize};
23
#[cfg(feature = "tokio")]
24
use tokio::{fs, io, io::AsyncWriteExt};
25
use zbus::zvariant::{Endian, Type, serialized::Context};
26

            
27
/// Used for newly created [`Keyring`]s
28
const DEFAULT_ITERATION_COUNT: u32 = 100000;
29
/// Used for newly created [`Keyring`]s
30
const DEFAULT_SALT_SIZE: usize = 32;
31

            
32
const MIN_ITERATION_COUNT: u32 = 100000;
33
const MIN_SALT_SIZE: usize = 32;
34
// FIXME: choose a reasonable value
35
const MIN_PASSWORD_LENGTH: usize = 4;
36

            
37
const FILE_HEADER: &[u8] = b"GnomeKeyring\n\r\0\n";
38
const FILE_HEADER_LEN: usize = FILE_HEADER.len();
39

            
40
pub(super) const MAJOR_VERSION: u8 = 1;
41
const MINOR_VERSION: u8 = 0;
42

            
43
mod encrypted_item;
44
mod legacy_keyring;
45

            
46
pub(super) use encrypted_item::EncryptedItem;
47
pub(super) use legacy_keyring::{Keyring as LegacyKeyring, MAJOR_VERSION as LEGACY_MAJOR_VERSION};
48

            
49
use crate::{
50
    AsAttributes, Key, Secret, crypto,
51
    file::{Error, UnlockedItem, WeakKeyError},
52
};
53

            
54
2
pub(crate) fn data_dir() -> Option<PathBuf> {
55
2
    std::env::var_os("XDG_DATA_HOME")
56
6
        .and_then(|h| if h.is_empty() { None } else { Some(h) })
57
2
        .map(PathBuf::from)
58
6
        .and_then(|p| if p.is_absolute() { Some(p) } else { None })
59
4
        .or_else(|| {
60
2
            std::env::var_os("HOME")
61
6
                .and_then(|h| if h.is_empty() { None } else { Some(h) })
62
2
                .map(PathBuf::from)
63
6
                .map(|p| p.join(".local/share"))
64
        })
65
}
66

            
67
pub(crate) static GVARIANT_ENCODING: LazyLock<Context> =
68
8
    LazyLock::new(|| Context::new_gvariant(Endian::Little, 0));
69

            
70
/// Logical contents of a keyring file
71
#[derive(Deserialize, Serialize, Type, Debug)]
72
pub struct Keyring {
73
    salt_size: u32,
74
    #[serde(with = "serde_bytes")]
75
    salt: Vec<u8>,
76
    iteration_count: u32,
77
    modified_time: u64,
78
    usage_count: u32,
79
    pub(in crate::file) items: Vec<EncryptedItem>,
80
}
81

            
82
impl Keyring {
83
    #[allow(clippy::new_without_default)]
84
7
    pub(crate) fn new() -> Self {
85
5
        let salt = rand::rng().random::<[u8; DEFAULT_SALT_SIZE]>().to_vec();
86

            
87
        Self {
88
7
            salt_size: salt.len() as u32,
89
            salt,
90
            iteration_count: DEFAULT_ITERATION_COUNT,
91
            // TODO: UTC?
92
4
            modified_time: std::time::SystemTime::UNIX_EPOCH
93
                .elapsed()
94
                .unwrap()
95
                .as_secs(),
96
            usage_count: 0,
97
7
            items: Vec::new(),
98
        }
99
    }
100

            
101
8
    pub fn key_strength(&self, secret: &[u8]) -> Result<(), WeakKeyError> {
102
6
        if self.iteration_count < MIN_ITERATION_COUNT {
103
2
            Err(WeakKeyError::IterationCountTooLow(self.iteration_count))
104
10
        } else if self.salt.len() < MIN_SALT_SIZE {
105
2
            Err(WeakKeyError::SaltTooShort(self.salt.len()))
106
8
        } else if secret.len() < MIN_PASSWORD_LENGTH {
107
2
            Err(WeakKeyError::PasswordTooShort(secret.len()))
108
        } else {
109
8
            Ok(())
110
        }
111
    }
112

            
113
    /// Write to a keyring file
114
4
    pub async fn dump(
115
        &mut self,
116
        path: impl AsRef<Path>,
117
        mtime: Option<std::time::SystemTime>,
118
    ) -> Result<(), Error> {
119
16
        let tmp_path = if let Some(parent) = path.as_ref().parent() {
120
8
            let rnd: String = rand::rng()
121
4
                .sample_iter(&rand::distr::Alphanumeric)
122
                .take(16)
123
4
                .map(char::from)
124
                .collect();
125

            
126
4
            let mut tmp_path = parent.to_path_buf();
127
8
            tmp_path.push(format!(".tmpkeyring{rnd}"));
128

            
129
6
            if !parent.exists() {
130
4
                #[cfg(feature = "tracing")]
131
                tracing::debug!("Parent directory {:?} doesn't exists, creating it", parent);
132
10
                fs::DirBuilder::new()
133
                    .recursive(true)
134
                    .mode(0o700)
135
2
                    .create(parent)
136
8
                    .await?;
137
            }
138

            
139
4
            Ok(tmp_path)
140
        } else {
141
            Err(Error::NoParentDir(path.as_ref().display().to_string()))
142
        }?;
143
8
        #[cfg(feature = "tracing")]
144
        tracing::debug!(
145
            "Created a temporary file to store the keyring on {:?}",
146
            tmp_path
147
        );
148

            
149
4
        let mut tmpfile_builder = fs::OpenOptions::new();
150

            
151
4
        tmpfile_builder.write(true).create_new(true);
152
4
        tmpfile_builder.mode(0o600);
153
9
        let mut tmpfile = tmpfile_builder.open(&tmp_path).await?;
154

            
155
8
        self.modified_time = std::time::SystemTime::UNIX_EPOCH
156
4
            .elapsed()
157
4
            .unwrap()
158
4
            .as_secs();
159
4
        self.usage_count += 1;
160

            
161
8
        let blob = self.as_bytes()?;
162

            
163
12
        tmpfile.write_all(&blob).await?;
164
9
        tmpfile.sync_all().await?;
165

            
166
13
        let target_file = fs::File::open(path.as_ref()).await;
167

            
168
4
        let target_mtime = match target_file {
169
12
            Err(err) if err.kind() == io::ErrorKind::NotFound => None,
170
            Err(err) => return Err(err.into()),
171
4
            Ok(file) => file.metadata().await?.modified().ok(),
172
        };
173

            
174
8
        if mtime != target_mtime {
175
            return Err(Error::TargetFileChanged(
176
                path.as_ref().display().to_string(),
177
            ));
178
        }
179

            
180
9
        fs::rename(tmp_path, path.as_ref()).await?;
181

            
182
4
        Ok(())
183
    }
184

            
185
2
    pub fn search_items(
186
        &self,
187
        attributes: &impl AsAttributes,
188
        key: &Key,
189
    ) -> Result<Vec<UnlockedItem>, Error> {
190
2
        let hashed_search = attributes.hash(key);
191

            
192
2
        self.items
193
            .iter()
194
4
            .filter(|e| {
195
2
                hashed_search
196
2
                    .iter()
197
10
                    .all(|(k, v)| v.as_ref().is_ok_and(|v| e.has_attribute(k.as_str(), v)))
198
            })
199
6
            .map(|e| (*e).clone().decrypt(key))
200
            .collect()
201
    }
202

            
203
    pub fn lookup_item(
204
        &self,
205
        attributes: &impl AsAttributes,
206
        key: &Key,
207
    ) -> Result<Option<UnlockedItem>, Error> {
208
        let hashed_search = attributes.hash(key);
209

            
210
        self.items
211
            .iter()
212
            .find(|e| {
213
                hashed_search
214
                    .iter()
215
                    .all(|(k, v)| v.as_ref().is_ok_and(|v| e.has_attribute(k.as_str(), v)))
216
            })
217
            .map(|e| (*e).clone().decrypt(key))
218
            .transpose()
219
    }
220

            
221
    pub fn lookup_item_index(&self, attributes: &impl AsAttributes, key: &Key) -> Option<usize> {
222
        let hashed_search = attributes.hash(key);
223

            
224
        self.items.iter().position(|e| {
225
            hashed_search
226
                .iter()
227
                .all(|(k, v)| v.as_ref().is_ok_and(|v| e.has_attribute(k.as_str(), v)))
228
        })
229
    }
230

            
231
4
    pub fn remove_items(&mut self, attributes: &impl AsAttributes, key: &Key) -> Result<(), Error> {
232
4
        let hashed_search = attributes.hash(key);
233

            
234
        // Validate items to be removed before actually removing them
235
8
        for item in &self.items {
236
12
            if hashed_search
237
                .iter()
238
20
                .all(|(k, v)| v.as_ref().is_ok_and(|v| item.has_attribute(k.as_str(), v)))
239
            {
240
                // Validate by checking if it can be decrypted
241
4
                if !item.is_valid(key) {
242
                    return Err(Error::MacError);
243
                }
244
            }
245
        }
246

            
247
        // Remove matching items
248
8
        self.items.retain(|e| {
249
8
            !hashed_search
250
4
                .iter()
251
20
                .all(|(k, v)| v.as_ref().is_ok_and(|v| e.has_attribute(k.as_str(), v)))
252
        });
253

            
254
4
        Ok(())
255
    }
256

            
257
4
    fn as_bytes(&self) -> Result<Vec<u8>, Error> {
258
4
        let mut blob = FILE_HEADER.to_vec();
259

            
260
4
        blob.push(MAJOR_VERSION);
261
4
        blob.push(MINOR_VERSION);
262
4
        blob.append(&mut zvariant::to_bytes(*GVARIANT_ENCODING, &self)?.to_vec());
263

            
264
4
        Ok(blob)
265
    }
266

            
267
2
    pub(crate) fn path(name: &str, version: u8) -> Result<PathBuf, Error> {
268
2
        if let Some(mut path) = data_dir() {
269
2
            path.push("keyrings");
270
2
            if version > 0 {
271
2
                path.push(format!("v{version}"));
272
            }
273
4
            path.push(format!("{name}.keyring"));
274
2
            Ok(path)
275
        } else {
276
            Err(Error::NoDataDir)
277
        }
278
    }
279

            
280
    pub fn default_path() -> Result<PathBuf, Error> {
281
        Self::path("default", LEGACY_MAJOR_VERSION)
282
    }
283

            
284
4
    pub fn derive_key(&self, secret: &Secret) -> Result<Key, crypto::Error> {
285
        crypto::derive_key(
286
8
            &**secret,
287
4
            self.key_strength(secret),
288
            &self.salt,
289
8
            self.iteration_count.try_into().unwrap(),
290
        )
291
    }
292

            
293
    /// Validate that a secret can decrypt the items in this keyring.
294
    ///
295
    /// This is useful for checking if a password is correct without having to
296
    /// re-open the keyring file.
297
2
    pub fn validate_secret(&self, secret: &Secret) -> Result<bool, crypto::Error> {
298
2
        let key = self.derive_key(secret)?;
299

            
300
        // If there are no items, we can't validate (empty keyrings are valid with any
301
        // password)
302
4
        if self.items.is_empty() {
303
2
            return Ok(true);
304
        }
305

            
306
        // Check if at least one item can be decrypted with this key
307
        // We only need to check one item to validate the password
308
8
        Ok(self.items.iter().any(|item| item.is_valid(&key)))
309
    }
310

            
311
    /// Get the modification timestamp
312
4
    pub fn modified_time(&self) -> std::time::Duration {
313
4
        std::time::Duration::from_secs(self.modified_time)
314
    }
315

            
316
    // Reset Keyring content
317
2
    pub(crate) fn reset(&mut self) {
318
2
        let salt = rand::rng().random::<[u8; DEFAULT_SALT_SIZE]>().to_vec();
319
2
        self.salt_size = salt.len() as u32;
320
2
        self.salt = salt;
321
2
        self.iteration_count = DEFAULT_ITERATION_COUNT;
322
2
        self.usage_count = 0;
323
2
        self.items = Vec::new();
324
    }
325
}
326

            
327
impl TryFrom<&[u8]> for Keyring {
328
    type Error = Error;
329

            
330
4
    fn try_from(value: &[u8]) -> Result<Self, Error> {
331
4
        let header = value.get(..FILE_HEADER.len());
332
4
        if header != Some(FILE_HEADER) {
333
            return Err(Error::FileHeaderMismatch(
334
                header.map(|x| String::from_utf8_lossy(x).to_string()),
335
            ));
336
        }
337

            
338
4
        let version = value.get(FILE_HEADER_LEN..(FILE_HEADER_LEN + 2));
339
4
        if version != Some(&[MAJOR_VERSION, MINOR_VERSION]) {
340
6
            return Err(Error::VersionMismatch(version.map(|x| x.to_vec())));
341
        }
342

            
343
12
        if let Some(data) = value.get((FILE_HEADER_LEN + 2)..) {
344
8
            let keyring: Self = zvariant::serialized::Data::new(data, *GVARIANT_ENCODING)
345
4
                .deserialize()?
346
4
                .0;
347

            
348
8
            if keyring.salt.len() != keyring.salt_size as usize {
349
                Err(Error::SaltSizeMismatch(
350
                    keyring.salt.len(),
351
                    keyring.salt_size,
352
                ))
353
            } else {
354
4
                Ok(keyring)
355
            }
356
        } else {
357
            Err(Error::NoData)
358
        }
359
    }
360
}
361

            
362
#[cfg(test)]
363
#[cfg(feature = "tokio")]
364
mod tests {
365
    use super::*;
366
    use crate::secret::ContentType;
367

            
368
    const SECRET: [u8; 64] = [
369
        44, 173, 251, 20, 203, 56, 241, 169, 91, 54, 51, 244, 40, 40, 202, 92, 71, 233, 174, 17,
370
        145, 58, 7, 107, 31, 204, 175, 245, 112, 174, 31, 198, 162, 149, 13, 127, 119, 113, 13, 3,
371
        191, 143, 162, 153, 183, 7, 21, 116, 81, 45, 51, 198, 73, 127, 147, 40, 52, 25, 181, 188,
372
        48, 159, 0, 146,
373
    ];
374

            
375
    #[tokio::test]
376
    async fn keyfile_add_remove() -> Result<(), Error> {
377
        let needle = &[("key", "value")];
378

            
379
        let mut keyring = Keyring::new();
380
        let key = keyring.derive_key(&SECRET.to_vec().into())?;
381

            
382
        keyring
383
            .items
384
            .push(UnlockedItem::new("Label", needle, Secret::blob("MyPassword")).encrypt(&key)?);
385

            
386
        assert_eq!(keyring.search_items(needle, &key)?.len(), 1);
387

            
388
        keyring.remove_items(needle, &key)?;
389

            
390
        assert_eq!(keyring.search_items(needle, &key)?.len(), 0);
391

            
392
        Ok(())
393
    }
394

            
395
    #[tokio::test]
396
    async fn keyfile_dump_load() -> Result<(), Error> {
397
        let _silent = std::fs::remove_file("/tmp/test.keyring");
398

            
399
        let mut new_keyring = Keyring::new();
400
        let key = new_keyring.derive_key(&SECRET.to_vec().into())?;
401

            
402
        new_keyring.items.push(
403
            UnlockedItem::new("My Label", &[("my-tag", "my tag value")], "A Password")
404
                .encrypt(&key)?,
405
        );
406
        new_keyring.dump("/tmp/test.keyring", None).await?;
407

            
408
        let blob = tokio::fs::read("/tmp/test.keyring").await?;
409

            
410
        let loaded_keyring = Keyring::try_from(blob.as_slice())?;
411
        let loaded_items = loaded_keyring.search_items(&[("my-tag", "my tag value")], &key)?;
412

            
413
        assert_eq!(loaded_items[0].secret(), Secret::text("A Password"));
414
        assert_eq!(loaded_items[0].secret().content_type(), ContentType::Text);
415

            
416
        let _silent = std::fs::remove_file("/tmp/test.keyring");
417

            
418
        Ok(())
419
    }
420

            
421
    #[tokio::test]
422
    async fn key_strength() {
423
        let mut keyring = Keyring::new();
424
        keyring.iteration_count = 50000; // Less than MIN_ITERATION_COUNT (100000)
425
        let secret = Secret::from("test-password-that-is-long-enough");
426
        let result = keyring.key_strength(&secret);
427
        assert!(matches!(
428
            result,
429
            Err(WeakKeyError::IterationCountTooLow(50000))
430
        ));
431

            
432
        let keyring = Keyring::new();
433
        let secret = Secret::from("ab");
434
        let result = keyring.key_strength(&secret);
435
        assert!(matches!(result, Err(WeakKeyError::PasswordTooShort(2))));
436

            
437
        let mut keyring = Keyring::new();
438
        keyring.salt = vec![1, 2, 3, 4]; // Less than MIN_SALT_SIZE (32)
439
        let secret = Secret::from("test-password-that-is-long-enough");
440
        let result = keyring.key_strength(&secret);
441
        assert!(matches!(result, Err(WeakKeyError::SaltTooShort(4))));
442
    }
443
}