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 attribute_value;
44
mod encrypted_item;
45
mod legacy_keyring;
46

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

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

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

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

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

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

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

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

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

            
127
6
            let mut tmp_path = parent.to_path_buf();
128
12
            tmp_path.push(format!(".tmpkeyring{rnd}"));
129

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

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

            
150
6
        let mut tmpfile_builder = fs::OpenOptions::new();
151

            
152
6
        tmpfile_builder.write(true).create_new(true);
153
6
        tmpfile_builder.mode(0o600);
154
12
        let mut tmpfile = tmpfile_builder.open(&tmp_path).await?;
155

            
156
12
        self.modified_time = std::time::SystemTime::UNIX_EPOCH
157
6
            .elapsed()
158
6
            .unwrap()
159
6
            .as_secs();
160
6
        self.usage_count += 1;
161

            
162
12
        let blob = self.as_bytes()?;
163

            
164
18
        tmpfile.write_all(&blob).await?;
165
12
        tmpfile.sync_all().await?;
166

            
167
18
        let target_file = fs::File::open(path.as_ref()).await;
168

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

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

            
181
12
        fs::rename(tmp_path, path.as_ref()).await?;
182

            
183
6
        Ok(())
184
    }
185

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

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

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

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

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

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

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

            
235
12
        let (remove, keep): (Vec<EncryptedItem>, _) =
236
            self.items.clone().into_iter().partition(|e| {
237
10
                hashed_search
238
10
                    .iter()
239
50
                    .all(|(k, v)| v.as_ref().is_ok_and(|v| e.has_attribute(k, v)))
240
            });
241

            
242
        // check hashes for the ones to be removed
243
30
        for item in remove {
244
20
            item.decrypt(key)?;
245
        }
246

            
247
10
        self.items = keep;
248

            
249
10
        Ok(())
250
    }
251

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

            
255
4
        blob.push(MAJOR_VERSION);
256
4
        blob.push(MINOR_VERSION);
257
4
        blob.append(&mut zvariant::to_bytes(*GVARIANT_ENCODING, &self)?.to_vec());
258

            
259
4
        Ok(blob)
260
    }
261

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

            
275
    pub fn default_path() -> Result<PathBuf, Error> {
276
        Self::path("default", LEGACY_MAJOR_VERSION)
277
    }
278

            
279
5
    pub fn derive_key(&self, secret: &Secret) -> Result<Key, crypto::Error> {
280
        crypto::derive_key(
281
7
            &**secret,
282
5
            self.key_strength(secret),
283
            &self.salt,
284
7
            self.iteration_count.try_into().unwrap(),
285
        )
286
    }
287

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

            
295
        // If there are no items, we can't validate (empty keyrings are valid with any
296
        // password)
297
4
        if self.items.is_empty() {
298
2
            return Ok(true);
299
        }
300

            
301
        // Check if at least one item can be decrypted with this key
302
        // We only need to check one item to validate the password
303
8
        Ok(self.items.iter().any(|item| item.is_valid(&key)))
304
    }
305

            
306
    /// Get the modification timestamp
307
5
    pub fn modified_time(&self) -> std::time::Duration {
308
3
        std::time::Duration::from_secs(self.modified_time)
309
    }
310

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

            
322
impl TryFrom<&[u8]> for Keyring {
323
    type Error = Error;
324

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

            
333
4
        let version = value.get(FILE_HEADER_LEN..(FILE_HEADER_LEN + 2));
334
4
        if version != Some(&[MAJOR_VERSION, MINOR_VERSION]) {
335
12
            return Err(Error::VersionMismatch(version.map(|x| x.to_vec())));
336
        }
337

            
338
12
        if let Some(data) = value.get((FILE_HEADER_LEN + 2)..) {
339
8
            let keyring: Self = zvariant::serialized::Data::new(data, *GVARIANT_ENCODING)
340
4
                .deserialize()?
341
4
                .0;
342

            
343
8
            if keyring.salt.len() != keyring.salt_size as usize {
344
                Err(Error::SaltSizeMismatch(
345
                    keyring.salt.len(),
346
                    keyring.salt_size,
347
                ))
348
            } else {
349
4
                Ok(keyring)
350
            }
351
        } else {
352
            Err(Error::NoData)
353
        }
354
    }
355
}
356

            
357
#[cfg(test)]
358
#[cfg(feature = "tokio")]
359
mod tests {
360
    use super::*;
361
    use crate::secret::ContentType;
362

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

            
370
    #[tokio::test]
371
    async fn keyfile_add_remove() -> Result<(), Error> {
372
        let needle = &[("key", "value")];
373

            
374
        let mut keyring = Keyring::new();
375
        let key = keyring.derive_key(&SECRET.to_vec().into())?;
376

            
377
        keyring
378
            .items
379
            .push(UnlockedItem::new("Label", needle, Secret::blob("MyPassword")).encrypt(&key)?);
380

            
381
        assert_eq!(keyring.search_items(needle, &key)?.len(), 1);
382

            
383
        keyring.remove_items(needle, &key)?;
384

            
385
        assert_eq!(keyring.search_items(needle, &key)?.len(), 0);
386

            
387
        Ok(())
388
    }
389

            
390
    #[tokio::test]
391
    async fn keyfile_dump_load() -> Result<(), Error> {
392
        let _silent = std::fs::remove_file("/tmp/test.keyring");
393

            
394
        let mut new_keyring = Keyring::new();
395
        let key = new_keyring.derive_key(&SECRET.to_vec().into())?;
396

            
397
        new_keyring.items.push(
398
            UnlockedItem::new("My Label", &[("my-tag", "my tag value")], "A Password")
399
                .encrypt(&key)?,
400
        );
401
        new_keyring.dump("/tmp/test.keyring", None).await?;
402

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

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

            
408
        assert_eq!(loaded_items[0].secret(), Secret::text("A Password"));
409
        assert_eq!(loaded_items[0].secret().content_type(), ContentType::Text);
410

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

            
413
        Ok(())
414
    }
415

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

            
427
        let keyring = Keyring::new();
428
        let secret = Secret::from("ab");
429
        let result = keyring.key_strength(&secret);
430
        assert!(matches!(result, Err(WeakKeyError::PasswordTooShort(2))));
431

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