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 serde::{Deserialize, Serialize};
22
#[cfg(feature = "tokio")]
23
use tokio::{fs, io, io::AsyncWriteExt};
24
use zbus::zvariant::{Endian, Type, serialized::Context};
25

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

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

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

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

            
42
mod encrypted_item;
43
mod legacy_keyring;
44

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

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

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

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

            
70
/// Logical contents of a keyring file
71
#[derive(Deserialize, Serialize, Type, Debug, Zeroize, ZeroizeOnDrop)]
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
    #[zeroize(skip)]
80
    pub(in crate::file) items: Vec<EncryptedItem>,
81
}
82

            
83
impl Keyring {
84
    #[allow(clippy::new_without_default)]
85
36
    pub(crate) fn new() -> Result<Self, Error> {
86
42
        let mut salt = [0u8; DEFAULT_SALT_SIZE];
87
74
        getrandom::fill(&mut salt)
88
39
            .map_err(|e| Error::Crypto(crate::crypto::Error::Getrandom(e)))?;
89

            
90
35
        Ok(Self {
91
            salt_size: salt.len() as u32,
92
36
            salt: salt.to_vec(),
93
            iteration_count: DEFAULT_ITERATION_COUNT,
94
            // TODO: UTC?
95
35
            modified_time: std::time::SystemTime::UNIX_EPOCH
96
34
                .elapsed()
97
37
                .unwrap()
98
37
                .as_secs(),
99
            usage_count: 0,
100
38
            items: Vec::new(),
101
        })
102
    }
103

            
104
31
    pub fn key_strength(&self, secret: &[u8]) -> Result<(), WeakKeyError> {
105
42
        if self.iteration_count < MIN_ITERATION_COUNT {
106
2
            Err(WeakKeyError::IterationCountTooLow(self.iteration_count))
107
32
        } else if self.salt.len() < MIN_SALT_SIZE {
108
2
            Err(WeakKeyError::SaltTooShort(self.salt.len()))
109
76
        } else if secret.len() < MIN_PASSWORD_LENGTH {
110
4
            Err(WeakKeyError::PasswordTooShort(secret.len()))
111
        } else {
112
30
            Ok(())
113
        }
114
    }
115

            
116
    /// Write to a keyring file
117
24
    pub async fn dump(
118
        &mut self,
119
        path: impl AsRef<Path>,
120
        mtime: Option<std::time::SystemTime>,
121
    ) -> Result<(), Error> {
122
84
        let tmp_path = if let Some(parent) = path.as_ref().parent() {
123
22
            let mut rnd_bytes = [0u8; 8];
124
34
            getrandom::fill(&mut rnd_bytes)
125
23
                .map_err(|e| Error::Crypto(crate::crypto::Error::Getrandom(e)))?;
126
40
            let rnd = rnd_bytes.iter().fold(String::new(), |mut acc, b| {
127
40
                acc.push_str(&format!("{:02x}", b));
128
23
                acc
129
            });
130

            
131
23
            let mut tmp_path = parent.to_path_buf();
132
40
            tmp_path.push(format!(".tmpkeyring{rnd}"));
133

            
134
32
            if !parent.exists() {
135
20
                #[cfg(feature = "tracing")]
136
                tracing::debug!("Parent directory {:?} doesn't exists, creating it", parent);
137
46
                fs::DirBuilder::new()
138
                    .recursive(true)
139
                    .mode(0o700)
140
8
                    .create(parent)
141
41
                    .await?;
142
            }
143

            
144
21
            Ok(tmp_path)
145
        } else {
146
            Err(Error::NoParentDir(path.as_ref().display().to_string()))
147
        }?;
148
42
        #[cfg(feature = "tracing")]
149
        tracing::debug!(
150
            "Created a temporary file to store the keyring on {:?}",
151
            tmp_path
152
        );
153

            
154
21
        let mut tmpfile_builder = fs::OpenOptions::new();
155

            
156
21
        tmpfile_builder.write(true).create_new(true);
157
21
        tmpfile_builder.mode(0o600);
158
37
        let mut tmpfile = tmpfile_builder.open(&tmp_path).await?;
159

            
160
41
        self.modified_time = std::time::SystemTime::UNIX_EPOCH
161
19
            .elapsed()
162
19
            .unwrap()
163
19
            .as_secs();
164
19
        self.usage_count += 1;
165

            
166
46
        let blob = self.as_bytes()?;
167

            
168
71
        tmpfile.write_all(&blob).await?;
169
47
        tmpfile.sync_all().await?;
170

            
171
66
        let target_file = fs::File::open(path.as_ref()).await;
172

            
173
18
        let target_mtime = match target_file {
174
69
            Err(err) if err.kind() == io::ErrorKind::NotFound => None,
175
            Err(err) => return Err(err.into()),
176
18
            Ok(file) => file.metadata().await?.modified().ok(),
177
        };
178

            
179
48
        if mtime != target_mtime {
180
            return Err(Error::TargetFileChanged(
181
                path.as_ref().display().to_string(),
182
            ));
183
        }
184

            
185
43
        fs::rename(tmp_path, path.as_ref()).await?;
186

            
187
20
        Ok(())
188
    }
189

            
190
15
    pub fn search_items(
191
        &self,
192
        attributes: &impl AsAttributes,
193
        key: &Key,
194
    ) -> Result<Vec<UnlockedItem>, Error> {
195
15
        let hashed_search = attributes.hash(key);
196

            
197
15
        self.items
198
            .iter()
199
30
            .filter(|e| {
200
15
                hashed_search
201
15
                    .iter()
202
75
                    .all(|(k, v)| v.as_ref().is_ok_and(|v| e.has_attribute(k.as_str(), v)))
203
            })
204
45
            .map(|e| (*e).clone().decrypt(key))
205
            .collect()
206
    }
207

            
208
2
    pub fn lookup_item(
209
        &self,
210
        attributes: &impl AsAttributes,
211
        key: &Key,
212
    ) -> Result<Option<UnlockedItem>, Error> {
213
2
        let hashed_search = attributes.hash(key);
214

            
215
4
        self.items
216
            .iter()
217
4
            .find(|e| {
218
2
                hashed_search
219
2
                    .iter()
220
10
                    .all(|(k, v)| v.as_ref().is_ok_and(|v| e.has_attribute(k.as_str(), v)))
221
            })
222
6
            .map(|e| (*e).clone().decrypt(key))
223
            .transpose()
224
    }
225

            
226
4
    pub fn lookup_item_index(&self, attributes: &impl AsAttributes, key: &Key) -> Option<usize> {
227
4
        let hashed_search = attributes.hash(key);
228

            
229
12
        self.items.iter().position(|e| {
230
4
            hashed_search
231
4
                .iter()
232
20
                .all(|(k, v)| v.as_ref().is_ok_and(|v| e.has_attribute(k.as_str(), v)))
233
        })
234
    }
235

            
236
30
    pub fn remove_items(&mut self, attributes: &impl AsAttributes, key: &Key) -> Result<(), Error> {
237
34
        let hashed_search = attributes.hash(key);
238

            
239
        // Validate items to be removed before actually removing them
240
67
        for item in &self.items {
241
93
            if hashed_search
242
                .iter()
243
155
                .all(|(k, v)| v.as_ref().is_ok_and(|v| item.has_attribute(k.as_str(), v)))
244
            {
245
                // Validate by checking if it can be decrypted
246
33
                if !item.is_valid(key) {
247
                    return Err(Error::MacError);
248
                }
249
            }
250
        }
251

            
252
        // Remove matching items
253
63
        self.items.retain(|e| {
254
63
            !hashed_search
255
31
                .iter()
256
158
                .all(|(k, v)| v.as_ref().is_ok_and(|v| e.has_attribute(k.as_str(), v)))
257
        });
258

            
259
35
        Ok(())
260
    }
261

            
262
17
    fn as_bytes(&self) -> Result<Vec<u8>, Error> {
263
17
        let mut blob = FILE_HEADER.to_vec();
264

            
265
17
        blob.push(MAJOR_VERSION);
266
20
        blob.push(MINOR_VERSION);
267
17
        blob.append(&mut zvariant::to_bytes(*GVARIANT_ENCODING, &self)?.to_vec());
268

            
269
21
        Ok(blob)
270
    }
271

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

            
285
    /// Construct a keyring path within a specific data directory.
286
    ///
287
    /// This is useful for tests and cases where you want explicit control over
288
    /// where keyrings are stored, avoiding the default XDG_DATA_HOME location.
289
15
    pub(crate) fn path_at(
290
        data_dir: impl AsRef<std::path::Path>,
291
        name: &str,
292
        version: u8,
293
    ) -> PathBuf {
294
32
        let mut path = data_dir.as_ref().to_path_buf();
295
17
        path.push("keyrings");
296
15
        if version > 0 {
297
17
            path.push(format!("v{version}"));
298
        }
299
32
        path.push(format!("{name}.keyring"));
300
15
        path
301
    }
302

            
303
    pub fn default_path() -> Result<PathBuf, Error> {
304
        Self::path("default", LEGACY_MAJOR_VERSION)
305
    }
306

            
307
39
    pub fn derive_key(&self, secret: &Secret) -> Result<Key, crypto::Error> {
308
        crypto::derive_key(
309
33
            &**secret,
310
38
            self.key_strength(secret),
311
            &self.salt,
312
30
            self.iteration_count.try_into().unwrap(),
313
        )
314
    }
315

            
316
    /// Validate that a secret can decrypt the items in this keyring.
317
    ///
318
    /// This is useful for checking if a password is correct without having to
319
    /// re-open the keyring file.
320
8
    pub fn validate_secret(&self, secret: &Secret) -> Result<bool, crypto::Error> {
321
8
        let key = self.derive_key(secret)?;
322

            
323
        // If there are no items, we can't validate (empty keyrings are valid with any
324
        // password)
325
16
        if self.items.is_empty() {
326
8
            return Ok(true);
327
        }
328

            
329
        // Check if at least one item can be decrypted with this key
330
        // We only need to check one item to validate the password
331
24
        Ok(self.items.iter().any(|item| item.is_valid(&key)))
332
    }
333

            
334
    /// Get the modification timestamp
335
30
    pub fn modified_time(&self) -> std::time::Duration {
336
25
        std::time::Duration::from_secs(self.modified_time)
337
    }
338

            
339
    // Reset Keyring content
340
6
    pub(crate) fn reset(&mut self) -> Result<(), Error> {
341
6
        let mut salt = [0u8; DEFAULT_SALT_SIZE];
342
12
        getrandom::fill(&mut salt)
343
6
            .map_err(|e| Error::Crypto(crate::crypto::Error::Getrandom(e)))?;
344
6
        self.salt_size = salt.len() as u32;
345
6
        self.salt = salt.to_vec();
346
6
        self.iteration_count = DEFAULT_ITERATION_COUNT;
347
6
        self.usage_count = 0;
348
6
        self.items = Vec::new();
349
6
        Ok(())
350
    }
351
}
352

            
353
impl TryFrom<&[u8]> for Keyring {
354
    type Error = Error;
355

            
356
8
    fn try_from(value: &[u8]) -> Result<Self, Error> {
357
8
        let header = value.get(..FILE_HEADER.len());
358
8
        if header != Some(FILE_HEADER) {
359
            return Err(Error::FileHeaderMismatch(
360
                header.map(|x| String::from_utf8_lossy(x).to_string()),
361
            ));
362
        }
363

            
364
8
        let version = value.get(FILE_HEADER_LEN..(FILE_HEADER_LEN + 2));
365
8
        if version != Some(&[MAJOR_VERSION, MINOR_VERSION]) {
366
18
            return Err(Error::VersionMismatch(version.map(|x| x.to_vec())));
367
        }
368

            
369
24
        if let Some(data) = value.get((FILE_HEADER_LEN + 2)..) {
370
16
            let keyring: Self = zvariant::serialized::Data::new(data, *GVARIANT_ENCODING)
371
8
                .deserialize()?
372
8
                .0;
373

            
374
16
            if keyring.salt.len() != keyring.salt_size as usize {
375
                Err(Error::SaltSizeMismatch(
376
                    keyring.salt.len(),
377
                    keyring.salt_size,
378
                ))
379
            } else {
380
8
                Ok(keyring)
381
            }
382
        } else {
383
            Err(Error::NoData)
384
        }
385
    }
386
}
387

            
388
#[cfg(test)]
389
#[cfg(feature = "tokio")]
390
mod tests {
391
    use super::*;
392
    use crate::secret::ContentType;
393

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

            
401
    #[tokio::test]
402
    async fn keyfile_add_remove() -> Result<(), Error> {
403
        let needle = &[("key", "value")];
404

            
405
        let mut keyring = Keyring::new()?;
406
        let key = keyring.derive_key(&SECRET.to_vec().into())?;
407

            
408
        keyring
409
            .items
410
            .push(UnlockedItem::new("Label", needle, Secret::blob("MyPassword")).encrypt(&key)?);
411

            
412
        assert_eq!(keyring.search_items(needle, &key)?.len(), 1);
413

            
414
        keyring.remove_items(needle, &key)?;
415

            
416
        assert_eq!(keyring.search_items(needle, &key)?.len(), 0);
417

            
418
        Ok(())
419
    }
420

            
421
    #[tokio::test]
422
    async fn keyfile_dump_load() -> Result<(), Error> {
423
        let _silent = std::fs::remove_file("/tmp/test.keyring");
424

            
425
        let mut new_keyring = Keyring::new()?;
426
        let key = new_keyring.derive_key(&SECRET.to_vec().into())?;
427

            
428
        new_keyring.items.push(
429
            UnlockedItem::new("My Label", &[("my-tag", "my tag value")], "A Password")
430
                .encrypt(&key)?,
431
        );
432
        new_keyring.dump("/tmp/test.keyring", None).await?;
433

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

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

            
439
        assert_eq!(loaded_items[0].secret(), Secret::text("A Password"));
440
        assert_eq!(loaded_items[0].secret().content_type(), ContentType::Text);
441

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

            
444
        Ok(())
445
    }
446

            
447
    #[tokio::test]
448
    async fn key_strength() -> Result<(), Error> {
449
        let mut keyring = Keyring::new()?;
450
        keyring.iteration_count = 50000; // Less than MIN_ITERATION_COUNT (100000)
451
        let secret = Secret::from("test-password-that-is-long-enough");
452
        let result = keyring.key_strength(&secret);
453
        assert!(matches!(
454
            result,
455
            Err(WeakKeyError::IterationCountTooLow(50000))
456
        ));
457

            
458
        let keyring = Keyring::new()?;
459
        let secret = Secret::from("ab");
460
        let result = keyring.key_strength(&secret);
461
        assert!(matches!(result, Err(WeakKeyError::PasswordTooShort(2))));
462

            
463
        let mut keyring = Keyring::new()?;
464
        keyring.salt = vec![1, 2, 3, 4]; // Less than MIN_SALT_SIZE (32)
465
        let secret = Secret::from("test-password-that-is-long-enough");
466
        let result = keyring.key_strength(&secret);
467
        assert!(matches!(result, Err(WeakKeyError::SaltTooShort(4))));
468

            
469
        Ok(())
470
    }
471
}