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

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

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

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

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

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

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

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

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

            
129
4
            let mut tmp_path = parent.to_path_buf();
130
8
            tmp_path.push(format!(".tmpkeyring{rnd}"));
131

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

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

            
152
4
        let mut tmpfile_builder = fs::OpenOptions::new();
153

            
154
4
        tmpfile_builder.write(true).create_new(true);
155
4
        tmpfile_builder.mode(0o600);
156
8
        let mut tmpfile = tmpfile_builder.open(&tmp_path).await?;
157

            
158
8
        self.modified_time = std::time::SystemTime::UNIX_EPOCH
159
4
            .elapsed()
160
4
            .unwrap()
161
4
            .as_secs();
162
4
        self.usage_count += 1;
163

            
164
8
        let blob = self.as_bytes()?;
165

            
166
12
        tmpfile.write_all(&blob).await?;
167
8
        tmpfile.sync_all().await?;
168

            
169
12
        let target_file = fs::File::open(path.as_ref()).await;
170

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

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

            
183
8
        fs::rename(tmp_path, path.as_ref()).await?;
184

            
185
4
        Ok(())
186
    }
187

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

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

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

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

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

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

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

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

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

            
257
4
        Ok(())
258
    }
259

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

            
263
4
        blob.push(MAJOR_VERSION);
264
4
        blob.push(MINOR_VERSION);
265
4
        blob.append(&mut zvariant::to_bytes(*GVARIANT_ENCODING, &self)?.to_vec());
266

            
267
4
        Ok(blob)
268
    }
269

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

            
283
    pub fn default_path() -> Result<PathBuf, Error> {
284
        Self::path("default", LEGACY_MAJOR_VERSION)
285
    }
286

            
287
7
    pub fn derive_key(&self, secret: &Secret) -> Result<Key, crypto::Error> {
288
        crypto::derive_key(
289
5
            &**secret,
290
7
            self.key_strength(secret),
291
            &self.salt,
292
5
            self.iteration_count.try_into().unwrap(),
293
        )
294
    }
295

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

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

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

            
314
    /// Get the modification timestamp
315
4
    pub fn modified_time(&self) -> std::time::Duration {
316
6
        std::time::Duration::from_secs(self.modified_time)
317
    }
318

            
319
    // Reset Keyring content
320
2
    pub(crate) fn reset(&mut self) -> Result<(), Error> {
321
2
        let mut salt = [0u8; DEFAULT_SALT_SIZE];
322
4
        getrandom::fill(&mut salt)
323
2
            .map_err(|e| Error::Crypto(crate::crypto::Error::Getrandom(e)))?;
324
2
        self.salt_size = salt.len() as u32;
325
2
        self.salt = salt.to_vec();
326
2
        self.iteration_count = DEFAULT_ITERATION_COUNT;
327
2
        self.usage_count = 0;
328
2
        self.items = Vec::new();
329
2
        Ok(())
330
    }
331
}
332

            
333
impl TryFrom<&[u8]> for Keyring {
334
    type Error = Error;
335

            
336
4
    fn try_from(value: &[u8]) -> Result<Self, Error> {
337
4
        let header = value.get(..FILE_HEADER.len());
338
4
        if header != Some(FILE_HEADER) {
339
            return Err(Error::FileHeaderMismatch(
340
                header.map(|x| String::from_utf8_lossy(x).to_string()),
341
            ));
342
        }
343

            
344
4
        let version = value.get(FILE_HEADER_LEN..(FILE_HEADER_LEN + 2));
345
4
        if version != Some(&[MAJOR_VERSION, MINOR_VERSION]) {
346
6
            return Err(Error::VersionMismatch(version.map(|x| x.to_vec())));
347
        }
348

            
349
12
        if let Some(data) = value.get((FILE_HEADER_LEN + 2)..) {
350
8
            let keyring: Self = zvariant::serialized::Data::new(data, *GVARIANT_ENCODING)
351
4
                .deserialize()?
352
4
                .0;
353

            
354
8
            if keyring.salt.len() != keyring.salt_size as usize {
355
                Err(Error::SaltSizeMismatch(
356
                    keyring.salt.len(),
357
                    keyring.salt_size,
358
                ))
359
            } else {
360
4
                Ok(keyring)
361
            }
362
        } else {
363
            Err(Error::NoData)
364
        }
365
    }
366
}
367

            
368
#[cfg(test)]
369
#[cfg(feature = "tokio")]
370
mod tests {
371
    use super::*;
372
    use crate::secret::ContentType;
373

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

            
381
    #[tokio::test]
382
    async fn keyfile_add_remove() -> Result<(), Error> {
383
        let needle = &[("key", "value")];
384

            
385
        let mut keyring = Keyring::new()?;
386
        let key = keyring.derive_key(&SECRET.to_vec().into())?;
387

            
388
        keyring
389
            .items
390
            .push(UnlockedItem::new("Label", needle, Secret::blob("MyPassword")).encrypt(&key)?);
391

            
392
        assert_eq!(keyring.search_items(needle, &key)?.len(), 1);
393

            
394
        keyring.remove_items(needle, &key)?;
395

            
396
        assert_eq!(keyring.search_items(needle, &key)?.len(), 0);
397

            
398
        Ok(())
399
    }
400

            
401
    #[tokio::test]
402
    async fn keyfile_dump_load() -> Result<(), Error> {
403
        let _silent = std::fs::remove_file("/tmp/test.keyring");
404

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

            
408
        new_keyring.items.push(
409
            UnlockedItem::new("My Label", &[("my-tag", "my tag value")], "A Password")
410
                .encrypt(&key)?,
411
        );
412
        new_keyring.dump("/tmp/test.keyring", None).await?;
413

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

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

            
419
        assert_eq!(loaded_items[0].secret(), Secret::text("A Password"));
420
        assert_eq!(loaded_items[0].secret().content_type(), ContentType::Text);
421

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

            
424
        Ok(())
425
    }
426

            
427
    #[tokio::test]
428
    async fn key_strength() -> Result<(), Error> {
429
        let mut keyring = Keyring::new()?;
430
        keyring.iteration_count = 50000; // Less than MIN_ITERATION_COUNT (100000)
431
        let secret = Secret::from("test-password-that-is-long-enough");
432
        let result = keyring.key_strength(&secret);
433
        assert!(matches!(
434
            result,
435
            Err(WeakKeyError::IterationCountTooLow(50000))
436
        ));
437

            
438
        let keyring = Keyring::new()?;
439
        let secret = Secret::from("ab");
440
        let result = keyring.key_strength(&secret);
441
        assert!(matches!(result, Err(WeakKeyError::PasswordTooShort(2))));
442

            
443
        let mut keyring = Keyring::new()?;
444
        keyring.salt = vec![1, 2, 3, 4]; // Less than MIN_SALT_SIZE (32)
445
        let secret = Secret::from("test-password-that-is-long-enough");
446
        let result = keyring.key_strength(&secret);
447
        assert!(matches!(result, Err(WeakKeyError::SaltTooShort(4))));
448

            
449
        Ok(())
450
    }
451
}