1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
//! Password store Tomb functionality.

use std::env;
use std::os::linux::fs::MetadataExt;
use std::path::{Path, PathBuf};

use anyhow::{anyhow, Result};
use thiserror::Error;

use crate::crypto::Proto;
use crate::tomb_bin::TombSettings;
use crate::util;
use crate::{systemd_bin, tomb_bin, Key, Store};

/// Default time after which to automatically close the password tomb.
pub const TOMB_AUTO_CLOSE_SEC: u32 = 5 * 60;

/// Common tomb file suffix.
pub const TOMB_FILE_SUFFIX: &str = ".tomb";

/// Common tomb key file suffix.
pub const TOMB_KEY_FILE_SUFFIX: &str = ".tomb.key";

/// Tomb helper for given store.
pub struct Tomb<'a> {
    /// The store.
    store: &'a Store,

    /// Tomb settings.
    pub settings: TombSettings,
}

impl<'a> Tomb<'a> {
    /// Construct new Tomb helper for given store.
    pub fn new(store: &'a Store, quiet: bool, verbose: bool, force: bool) -> Tomb<'a> {
        Self {
            store,
            settings: TombSettings {
                quiet,
                verbose,
                force,
            },
        }
    }

    /// Find the tomb path.
    ///
    /// Errors if it cannot be found.
    pub fn find_tomb_path(&self) -> Result<PathBuf> {
        find_tomb_path(&self.store.root).ok_or_else(|| Err::CannotFindTomb.into())
    }

    /// Find the tomb key path.
    ///
    /// Errors if it cannot be found.
    pub fn find_tomb_key_path(&self) -> Result<PathBuf> {
        find_tomb_key_path(&self.store.root).ok_or_else(|| Err::CannotFindTombKey.into())
    }

    /// Open the tomb.
    ///
    /// This will keep the tomb open until it is manually closed. See `start_timer()`.
    ///
    /// On success this may return a list with soft-fail errors.
    pub fn open(&self) -> Result<Vec<Err>> {
        // Open tomb
        let tomb = self.find_tomb_path()?;
        let key = self.find_tomb_key_path()?;
        tomb_bin::tomb_open(&tomb, &key, &self.store.root, None, self.settings)
            .map_err(Err::Open)?;

        // Soft fail on following errors, collect them
        let mut errs = vec![];

        // Change mountpoint directory permissions to current user
        if let Err(err) =
            util::fs::sudo_chown_current_user(&self.store.root, false).map_err(Err::Chown)
        {
            errs.push(err);
        }

        Ok(errs)
    }

    /// Resize the tomb.
    ///
    /// The Tomb must not be mounted and the size must be larger than the current.
    pub fn resize(&self, mbs: u32) -> Result<()> {
        let tomb = self.find_tomb_path()?;
        let key = self.find_tomb_key_path()?;
        tomb_bin::tomb_resize(&tomb, &key, mbs, self.settings).map_err(Err::Resize)?;
        Ok(())
    }

    /// Close the tomb.
    pub fn close(&self) -> Result<()> {
        let tomb = self.find_tomb_path()?;
        tomb_bin::tomb_close(&tomb, self.settings)
    }

    /// Prepare a Tomb store for usage.
    ///
    /// - If this store is a Tomb, the tomb is opened.
    pub fn prepare(&self) -> Result<()> {
        // TODO: return error if dirty?

        // Skip if not a tomb
        if !self.is_tomb() {
            return Ok(());
        }

        // Skip if already open
        if self.is_open()? {
            return Ok(());
        }

        if !self.settings.quiet {
            eprintln!("Opening password store Tomb...");
        }

        // Open tomb, set up auto close timer
        self.open().map_err(Err::Prepare)?;
        self.start_timer(TOMB_AUTO_CLOSE_SEC, false)
            .map_err(Err::Prepare)?;

        eprintln!();
        if self.settings.verbose {
            eprintln!("Opened password store, automatically closing in 5 seconds");
        }

        Ok(())
    }

    /// Set up a timer to automatically close password store tomb.
    ///
    /// TODO: add support for non-systemd systems
    pub fn start_timer(&self, sec: u32, force: bool) -> Result<()> {
        // Figure out tomb path and name
        let tomb_path = self.find_tomb_path()?;
        let name = tomb_bin::name(&tomb_path).unwrap_or(".unwrap");
        let unit = format!("prs-tomb-close@{}.service", name);

        // Skip if already running
        if !force && systemd_bin::systemd_has_timer(&unit).map_err(Err::AutoCloseTimer)? {
            return Ok(());
        }

        // Spawn timer to automatically close tomb
        // TODO: better method to find current exe path
        // TODO: do not hardcode exe, command and store path
        systemd_bin::systemd_cmd_timer(
            sec,
            "prs tomb close timer",
            &unit,
            &[
                std::env::current_exe()
                    .expect("failed to determine current exe")
                    .to_str()
                    .expect("current exe contains invalid UTF-8"),
                "tomb",
                "--store",
                self.store
                    .root
                    .to_str()
                    .expect("password store path contains invalid UTF-8"),
                "close",
                "--try",
                "--verbose",
            ],
        )
        .map_err(Err::AutoCloseTimer)?;

        Ok(())
    }

    /// Check whether the timer is running.
    pub fn has_timer(&self) -> Result<bool> {
        // Figure out tomb path and name
        let tomb_path = self.find_tomb_path()?;
        let name = tomb_bin::name(&tomb_path).unwrap_or(".unwrap");
        let unit = format!("prs-tomb-close@{}.service", name);

        systemd_bin::systemd_has_timer(&unit).map_err(|err| Err::AutoCloseTimer(err).into())
    }

    /// Stop automatic close timer if any is running.
    pub fn stop_timer(&self) -> Result<()> {
        // Figure out tomb path and name
        let tomb_path = self.find_tomb_path()?;
        let name = tomb_bin::name(&tomb_path).unwrap_or(".unwrap");
        let unit = format!("prs-tomb-close@{}.service", name);

        // We're done if none is running
        if !systemd_bin::systemd_has_timer(&unit).map_err(Err::AutoCloseTimer)? {
            return Ok(());
        }

        systemd_bin::systemd_remove_timer(&unit).map_err(Err::AutoCloseTimer)?;
        Ok(())
    }

    /// Finalize the Tomb.
    pub fn finalize(&self) -> Result<()> {
        // This is currently just a placeholder for special closing functionality in the future
        Ok(())
    }

    /// Initialize tomb.
    ///
    /// `mbs` is the size in megabytes.
    ///
    /// The given GPG key is used to encrypt the Tomb key with.
    ///
    /// # Panics
    ///
    /// Panics if given key is not a GPG key.
    pub fn init(&self, key: &Key, mbs: u32) -> Result<()> {
        // Assert key is GPG
        assert_eq!(key.proto(), Proto::Gpg, "key for Tomb is not a GPG key");

        // TODO: map errors

        // TODO: we need these paths even though tomb does not exist yet
        let tomb_file = tomb_paths(&self.store.root).first().unwrap().to_owned();
        let key_file = tomb_key_paths(&self.store.root).first().unwrap().to_owned();
        let store_tmp_dir =
            util::fs::append_file_name(&self.store.root, ".tomb-init").map_err(Err::Init)?;

        // Dig tomb, forge key, lock tomb with key, open tomb
        tomb_bin::tomb_dig(&tomb_file, mbs, self.settings).map_err(Err::Init)?;
        tomb_bin::tomb_forge(&key_file, key, self.settings).map_err(Err::Init)?;
        tomb_bin::tomb_lock(&tomb_file, &key_file, key, self.settings).map_err(Err::Init)?;
        tomb_bin::tomb_open(
            &tomb_file,
            &key_file,
            &store_tmp_dir,
            Some(key),
            self.settings,
        )
        .map_err(Err::Init)?;

        // Change temporary mountpoint directory permissions to current user
        util::fs::sudo_chown_current_user(&store_tmp_dir, true).map_err(Err::Chown)?;

        // Copy password store contents
        util::fs::copy_dir_contents(&self.store.root, &store_tmp_dir).map_err(Err::Init)?;

        // Close tomb
        tomb_bin::tomb_close(&tomb_file, self.settings).map_err(Err::Init)?;

        // Remove both main and temporary store
        fs_extra::dir::remove(&self.store.root).map_err(|err| Err::Init(anyhow!(err)))?;
        fs_extra::dir::remove(&store_tmp_dir).map_err(|err| Err::Init(anyhow!(err)))?;

        // Open tomb as regular
        // TODO: do something with Ok(errors)?
        self.open()?;

        // Change mountpoint directory permissions to current user
        util::fs::sudo_chown_current_user(&self.store.root, false).map_err(Err::Chown)?;

        Ok(())
    }

    /// Check whether the password store is a tomb.
    ///
    /// This guesses based on existence of some files.
    /// If this returns false you may assume this password store doesn't use a tomb.
    pub fn is_tomb(&self) -> bool {
        find_tomb_path(&self.store.root).is_some()
    }

    /// Check whether the password store is currently opened.
    ///
    /// This guesses based on mount information for the password store directory.
    pub fn is_open(&self) -> Result<bool> {
        // Password store directory must exist
        if !self.store.root.is_dir() {
            return Ok(false);
        }

        // If device ID of store dir and it's parent differ we can assume it is mounted
        if let Some(parent) = self.store.root.parent() {
            let meta_root = self.store.root.metadata().map_err(Err::OpenCheck)?;
            let meta_parent = parent.metadata().map_err(Err::OpenCheck)?;
            return Ok(meta_root.st_dev() != meta_parent.st_dev());
        }

        // TODO: do extensive mount check here

        Ok(false)
    }

    /// Fetch Tomb size statistics.
    ///
    /// This is expensive.
    pub fn fetch_size_stats(&self) -> Result<TombSize> {
        let tomb_path = self.find_tomb_path()?;

        // Get sizes
        let store = if self.is_open().unwrap_or(false) {
            util::fs::dir_size(&self.store.root).ok()
        } else {
            None
        };
        let tomb_file = tomb_path.metadata().map(|m| m.len()).ok();

        Ok(TombSize { store, tomb_file })
    }
}

/// Holds information for password store Tomb sizes.
#[derive(Debug, Copy, Clone)]
pub struct TombSize {
    /// Store directory.
    pub store: Option<u64>,

    /// Tomb file size.
    pub tomb_file: Option<u64>,
}

impl TombSize {
    /// Get Tomb file size in MBs.
    pub fn tomb_file_size_mbs(&self) -> Option<u32> {
        self.tomb_file.map(|s| (s / 1024 / 1024) as u32)
    }

    /// Get the desired Tomb size in megabytes based on the current state.
    ///
    /// Currently twice the password store size, defaults to minimum of 10.
    pub fn desired_tomb_size(&self) -> u32 {
        self.store
            .map(|bytes| ((bytes * 3) / 1024 / 1024).max(10) as u32)
            .unwrap_or(10)
    }

    /// Determine whether the password store should be resized.
    pub fn should_resize(&self) -> bool {
        // TODO: determine this based on 'tomb list' output
        self.store
            .zip(self.tomb_file)
            .map(|(store, tomb_file)| store * 2 > tomb_file)
            .unwrap_or(false)
    }
}

#[derive(Debug, Error)]
pub enum Err {
    #[error("failed to find tomb file for password store")]
    CannotFindTomb,

    #[error("failed to find tomb key file to unlock password store tomb")]
    CannotFindTombKey,

    #[error("failed to prepare password store tomb for usage")]
    Prepare(#[source] anyhow::Error),

    #[error("failed to initialize new password store tomb")]
    Init(#[source] anyhow::Error),

    #[error("failed to open password store tomb through tomb CLI")]
    Open(#[source] anyhow::Error),

    #[error("failed to resize password store tomb through tomb CLI")]
    Resize(#[source] anyhow::Error),

    #[error("failed to change permissions to current user for tomb mountpoint")]
    Chown(#[source] anyhow::Error),

    #[error("failed to check if password store tomb is opened")]
    OpenCheck(#[source] std::io::Error),

    #[error("failed to set up systemd timer to auto close password store tomb")]
    AutoCloseTimer(#[source] anyhow::Error),
}

/// Build list of probable tomb paths for given store root.
fn tomb_paths(root: &Path) -> Vec<PathBuf> {
    let mut paths = Vec::with_capacity(4);

    // Get parent directory and file name
    let parent = root.parent();
    let file_name = root.file_name().and_then(|n| n.to_str());

    // Same path as store root with .tomb suffix
    if let (Some(parent), Some(file_name)) = (parent, file_name) {
        paths.push(parent.join(format!("{}{}", file_name, TOMB_FILE_SUFFIX)));
    }

    // Path from pass-tomb in store parent and in home
    if let Some(parent) = parent {
        paths.push(parent.join(format!(".password{}", TOMB_FILE_SUFFIX)).into());
    }
    paths.push(format!("~/.password{}", TOMB_FILE_SUFFIX).into());

    paths
}

/// Find tomb path for given store root.
///
/// Uses `PASSWORD_STORE_TOMB_FILE` if set.
/// This does not guarantee that the returned path is an actual tomb file.
/// This is a best effort search.
fn find_tomb_path(root: &Path) -> Option<PathBuf> {
    // Take path from environment variable
    if let Ok(path) = env::var("PASSWORD_STORE_TOMB_FILE") {
        return Some(path.into());
    }

    // TODO: ensure file is large enough to be a tomb (tomb be at least 10 MB)
    tomb_paths(root).into_iter().find(|p| p.is_file())
}

/// Build list of probable tomb key paths for given store root.
fn tomb_key_paths(root: &Path) -> Vec<PathBuf> {
    let mut paths = Vec::with_capacity(4);

    // Get parent directory and file name
    let parent = root.parent();
    let file_name = root.file_name().and_then(|n| n.to_str());

    // Same path as store root with .tomb suffix
    if let (Some(parent), Some(file_name)) = (parent, file_name) {
        paths.push(parent.join(format!("{}{}", file_name, TOMB_KEY_FILE_SUFFIX)));
    }

    // Path from pass-tomb in store parent and in home
    if let Some(parent) = parent {
        paths.push(
            parent
                .join(format!(".password{}", TOMB_KEY_FILE_SUFFIX))
                .into(),
        );
    }
    paths.push(format!("~/.password{}", TOMB_KEY_FILE_SUFFIX).into());

    paths
}

/// Find tomb key path for given store root.
///
/// Uses `PASSWORD_STORE_TOMB_KEY` if set.
/// This does not guarantee that the returned path is an actual tomb key file.
/// This is a best effort search.
fn find_tomb_key_path(root: &Path) -> Option<PathBuf> {
    // Take path from environment variable
    if let Ok(path) = env::var("PASSWORD_STORE_TOMB_KEY") {
        return Some(path.into());
    }

    tomb_key_paths(root).into_iter().find(|p| p.is_file())
}