prs_lib/
tomb.rs

1//! Password store Tomb functionality.
2
3use std::env;
4use std::os::linux::fs::MetadataExt;
5use std::path::{Path, PathBuf};
6
7use anyhow::{Result, anyhow};
8use thiserror::Error;
9
10use crate::crypto::Proto;
11pub use crate::tomb_bin::TombSettings;
12use crate::util;
13use crate::{Key, Store, systemd_bin, tomb_bin};
14
15/// Default time after which to automatically close the password tomb.
16pub const TOMB_AUTO_CLOSE_SEC: u32 = 5 * 60;
17
18/// Common tomb file suffix.
19pub const TOMB_FILE_SUFFIX: &str = ".tomb";
20
21/// Common tomb key file suffix.
22pub const TOMB_KEY_FILE_SUFFIX: &str = ".tomb.key";
23
24/// Name of SSH client process.
25pub const SSH_PROCESS_NAME: &str = "ssh";
26
27/// Tomb helper for given store.
28pub struct Tomb<'a> {
29    /// The store.
30    store: &'a Store,
31
32    /// Tomb settings.
33    pub settings: TombSettings,
34}
35
36impl<'a> Tomb<'a> {
37    /// Construct new Tomb helper for given store.
38    pub fn new(store: &'a Store, quiet: bool, verbose: bool, force: bool) -> Tomb<'a> {
39        Self {
40            store,
41            settings: TombSettings {
42                quiet,
43                verbose,
44                force,
45            },
46        }
47    }
48
49    /// Find the tomb path.
50    ///
51    /// Errors if it cannot be found.
52    pub fn find_tomb_path(&self) -> Result<PathBuf> {
53        find_tomb_path(&self.store.root).ok_or_else(|| Err::CannotFindTomb.into())
54    }
55
56    /// Find the tomb key path.
57    ///
58    /// Errors if it cannot be found.
59    pub fn find_tomb_key_path(&self) -> Result<PathBuf> {
60        find_tomb_key_path(&self.store.root).ok_or_else(|| Err::CannotFindTombKey.into())
61    }
62
63    /// Open the tomb.
64    ///
65    /// This will keep the tomb open until it is manually closed. See `start_timer()`.
66    ///
67    /// On success this may return a list with soft-fail errors.
68    pub fn open(&self) -> Result<Vec<Err>> {
69        // Open tomb
70        let tomb = self.find_tomb_path()?;
71        let key = self.find_tomb_key_path()?;
72        tomb_bin::tomb_open(&tomb, &key, &self.store.root, None, self.settings)
73            .map_err(Err::Open)?;
74
75        // Soft fail on following errors, collect them
76        let mut errs = vec![];
77
78        // Change mountpoint directory permissions to current user
79        if let Err(err) =
80            util::fs::sudo_chown_current_user(&self.store.root, false).map_err(Err::Chown)
81        {
82            errs.push(err);
83        }
84
85        Ok(errs)
86    }
87
88    /// Resize the tomb.
89    ///
90    /// The Tomb must not be mounted and the size must be larger than the current.
91    pub fn resize(&self, mbs: u32) -> Result<()> {
92        let tomb = self.find_tomb_path()?;
93        let key = self.find_tomb_key_path()?;
94        tomb_bin::tomb_resize(&tomb, &key, mbs, self.settings).map_err(Err::Resize)?;
95        Ok(())
96    }
97
98    /// Close the tomb.
99    pub fn close(&self) -> Result<()> {
100        let tomb = self.find_tomb_path()?;
101
102        // Kill SSH clients that still have a persistent session open for this store
103        util::git::kill_ssh_by_session(self.store);
104
105        tomb_bin::tomb_close(&tomb, self.settings).map_err(Err::Close)?;
106        Ok(())
107    }
108
109    /// Prepare a Tomb store for usage.
110    ///
111    /// - If this store is a Tomb, the tomb is opened.
112    pub fn prepare(&self) -> Result<()> {
113        // TODO: return error if dirty?
114
115        // Skip if not a tomb
116        if !self.is_tomb() {
117            return Ok(());
118        }
119
120        // Skip if already open
121        if self.is_open()? {
122            return Ok(());
123        }
124
125        if !self.settings.quiet {
126            eprintln!("Opening password store Tomb...");
127        }
128
129        // Open tomb, set up auto close timer
130        self.open().map_err(Err::Prepare)?;
131        self.start_timer(TOMB_AUTO_CLOSE_SEC, false)
132            .map_err(Err::Prepare)?;
133
134        eprintln!();
135        if self.settings.verbose {
136            eprintln!("Opened password store, automatically closing in 5 seconds");
137        }
138
139        Ok(())
140    }
141
142    /// Set up a timer to automatically close password store tomb.
143    ///
144    /// TODO: add support for non-systemd systems
145    pub fn start_timer(&self, sec: u32, force: bool) -> Result<()> {
146        // Figure out tomb path and name
147        let tomb_path = self.find_tomb_path()?;
148        let name = tomb_bin::name(&tomb_path).unwrap_or(".unwrap");
149        let unit = format!("prs-tomb-close@{name}.service");
150
151        // Skip if already running
152        if !force && systemd_bin::systemd_has_timer(&unit).map_err(Err::AutoCloseTimer)? {
153            return Ok(());
154        }
155
156        // Spawn timer to automatically close tomb
157        // TODO: better method to find current exe path
158        // TODO: do not hardcode exe, command and store path
159        systemd_bin::systemd_cmd_timer(
160            sec,
161            "prs tomb close timer",
162            &unit,
163            &[
164                std::env::current_exe()
165                    .expect("failed to determine current exe")
166                    .to_str()
167                    .expect("current exe contains invalid UTF-8"),
168                "tomb",
169                "--store",
170                self.store
171                    .root
172                    .to_str()
173                    .expect("password store path contains invalid UTF-8"),
174                "close",
175                "--try",
176                "--verbose",
177            ],
178        )
179        .map_err(Err::AutoCloseTimer)?;
180
181        Ok(())
182    }
183
184    /// Check whether the timer is running.
185    pub fn has_timer(&self) -> Result<bool> {
186        // Figure out tomb path and name
187        let tomb_path = self.find_tomb_path()?;
188        let name = tomb_bin::name(&tomb_path).unwrap_or(".unwrap");
189        let unit = format!("prs-tomb-close@{name}.service");
190
191        systemd_bin::systemd_has_timer(&unit).map_err(|err| Err::AutoCloseTimer(err).into())
192    }
193
194    /// Stop automatic close timer if any is running.
195    pub fn stop_timer(&self) -> Result<()> {
196        // Figure out tomb path and name
197        let tomb_path = self.find_tomb_path()?;
198        let name = tomb_bin::name(&tomb_path).unwrap_or(".unwrap");
199        let unit = format!("prs-tomb-close@{name}.service");
200
201        // We're done if none is running
202        if !systemd_bin::systemd_has_timer(&unit).map_err(Err::AutoCloseTimer)? {
203            return Ok(());
204        }
205
206        systemd_bin::systemd_remove_timer(&unit).map_err(Err::AutoCloseTimer)?;
207        Ok(())
208    }
209
210    /// Finalize the Tomb.
211    pub fn finalize(&self) -> Result<()> {
212        // This is currently just a placeholder for special closing functionality in the future
213        Ok(())
214    }
215
216    /// Initialize tomb.
217    ///
218    /// `mbs` is the size in megabytes.
219    ///
220    /// The given GPG key is used to encrypt the Tomb key with.
221    ///
222    /// # Panics
223    ///
224    /// Panics if given key is not a GPG key.
225    pub fn init(&self, key: &Key, mbs: u32) -> Result<()> {
226        // Assert key is GPG
227        assert_eq!(key.proto(), Proto::Gpg, "key for Tomb is not a GPG key");
228
229        // TODO: map errors
230
231        // TODO: we need these paths even though tomb does not exist yet
232        let tomb_file = tomb_paths(&self.store.root).first().unwrap().to_owned();
233        let key_file = tomb_key_paths(&self.store.root).first().unwrap().to_owned();
234        let store_tmp_dir =
235            util::fs::append_file_name(&self.store.root, ".tomb-init").map_err(Err::Init)?;
236
237        // Dig tomb, forge key, lock tomb with key, open tomb
238        tomb_bin::tomb_dig(&tomb_file, mbs, self.settings).map_err(Err::Init)?;
239        tomb_bin::tomb_forge(&key_file, key, self.settings).map_err(Err::Init)?;
240        tomb_bin::tomb_lock(&tomb_file, &key_file, key, self.settings).map_err(Err::Init)?;
241        tomb_bin::tomb_open(
242            &tomb_file,
243            &key_file,
244            &store_tmp_dir,
245            Some(key),
246            self.settings,
247        )
248        .map_err(Err::Init)?;
249
250        // Change temporary mountpoint directory permissions to current user
251        util::fs::sudo_chown_current_user(&store_tmp_dir, true).map_err(Err::Chown)?;
252
253        // Copy password store contents
254        util::fs::copy_dir_contents(&self.store.root, &store_tmp_dir).map_err(Err::Init)?;
255
256        // Close tomb
257        tomb_bin::tomb_close(&tomb_file, self.settings).map_err(Err::Init)?;
258        util::fs::sudo_chown_current_user(&store_tmp_dir, true).map_err(Err::Chown)?;
259
260        // Remove both main and temporary store
261        fs_extra::dir::remove(&self.store.root).map_err(|err| Err::Init(anyhow!(err)))?;
262        fs_extra::dir::remove(&store_tmp_dir).map_err(|err| Err::Init(anyhow!(err)))?;
263
264        // Open tomb as regular
265        // TODO: do something with Ok(errors)?
266        self.open()?;
267
268        Ok(())
269    }
270
271    /// Check whether the password store is a tomb.
272    ///
273    /// This guesses based on existence of some files.
274    /// If this returns false you may assume this password store doesn't use a tomb.
275    pub fn is_tomb(&self) -> bool {
276        find_tomb_path(&self.store.root).is_some()
277    }
278
279    /// Check whether the password store is currently opened.
280    ///
281    /// This guesses based on mount information for the password store directory.
282    pub fn is_open(&self) -> Result<bool> {
283        // Password store directory must exist
284        if !self.store.root.is_dir() {
285            return Ok(false);
286        }
287
288        // If device ID of store dir and it's parent differ we can assume it is mounted
289        if let Some(parent) = self.store.root.parent() {
290            let meta_root = self.store.root.metadata().map_err(Err::OpenCheck)?;
291            let meta_parent = parent.metadata().map_err(Err::OpenCheck)?;
292            return Ok(meta_root.st_dev() != meta_parent.st_dev());
293        }
294
295        // TODO: do extensive mount check here
296
297        Ok(false)
298    }
299
300    /// Fetch Tomb size statistics.
301    ///
302    /// This attempts to gather password store and tomb size statistics, whether this store is a
303    /// tomb or not.
304    ///
305    /// This is expensive.
306    pub fn fetch_size_stats(&self) -> Result<TombSize> {
307        // Get sizes depending on whether this store uses a tomb
308        match self.find_tomb_path() {
309            Ok(tomb_path) => {
310                let store = if self.is_open().unwrap_or(false) {
311                    util::fs::dir_size(&self.store.root).ok()
312                } else {
313                    None
314                };
315                let tomb_file = tomb_path.metadata().map(|m| m.len()).ok();
316
317                Ok(TombSize { store, tomb_file })
318            }
319            Err(_) => Ok(TombSize {
320                store: util::fs::dir_size(&self.store.root).ok(),
321                tomb_file: None,
322            }),
323        }
324    }
325}
326
327/// Slam all open tombs.
328///
329/// Warning: this may be dangerous and could have unwanted side effects. This also closes
330/// non-password Tombs and kills all programs using it.
331pub fn slam(settings: TombSettings) -> Result<()> {
332    tomb_bin::tomb_slam(settings).map_err(Err::Slam)?;
333    Ok(())
334}
335
336/// Holds information for password store Tomb sizes.
337#[derive(Debug, Copy, Clone)]
338pub struct TombSize {
339    /// Store directory.
340    pub store: Option<u64>,
341
342    /// Tomb file size.
343    pub tomb_file: Option<u64>,
344}
345
346impl TombSize {
347    /// Get Tomb file size in MBs.
348    pub fn tomb_file_size_mbs(&self) -> Option<u32> {
349        self.tomb_file.map(|s| (s / 1024 / 1024) as u32)
350    }
351
352    /// Get the desired Tomb size in megabytes based on the current state.
353    ///
354    /// Currently twice the password store size, defaults to minimum of 10.
355    pub fn desired_tomb_size(&self) -> u32 {
356        self.store
357            .map(|bytes| ((bytes * 3) / 1024 / 1024).max(10) as u32)
358            .unwrap_or(10)
359    }
360
361    /// Determine whether the password store should be resized.
362    pub fn should_resize(&self) -> bool {
363        // TODO: determine this based on 'tomb list' output
364        self.store
365            .zip(self.tomb_file)
366            .map(|(store, tomb_file)| store * 2 > tomb_file)
367            .unwrap_or(false)
368    }
369}
370
371#[derive(Debug, Error)]
372pub enum Err {
373    #[error("failed to find tomb file for password store")]
374    CannotFindTomb,
375
376    #[error("failed to find tomb key file to unlock password store tomb")]
377    CannotFindTombKey,
378
379    #[error("failed to prepare password store tomb for usage")]
380    Prepare(#[source] anyhow::Error),
381
382    #[error("failed to initialize new password store tomb")]
383    Init(#[source] anyhow::Error),
384
385    #[error("failed to open password store tomb through tomb CLI")]
386    Open(#[source] anyhow::Error),
387
388    #[error("failed to close password store tomb through tomb CLI")]
389    Close(#[source] anyhow::Error),
390
391    #[error("failed to resize password store tomb through tomb CLI")]
392    Resize(#[source] anyhow::Error),
393
394    #[error("failed to slam all open tombs through tomb CLI")]
395    Slam(#[source] anyhow::Error),
396
397    #[error("failed to change permissions to current user for tomb mountpoint")]
398    Chown(#[source] anyhow::Error),
399
400    #[error("failed to check if password store tomb is opened")]
401    OpenCheck(#[source] std::io::Error),
402
403    #[error("failed to set up systemd timer to auto close password store tomb")]
404    AutoCloseTimer(#[source] anyhow::Error),
405}
406
407/// Build list of probable tomb paths for given store root.
408fn tomb_paths(root: &Path) -> Vec<PathBuf> {
409    let mut paths = Vec::with_capacity(4);
410
411    // Get parent directory and file name
412    let parent = root.parent();
413    let file_name = root.file_name().and_then(|n| n.to_str());
414
415    // Same path as store root with .tomb suffix
416    if let (Some(parent), Some(file_name)) = (parent, file_name) {
417        paths.push(parent.join(format!("{file_name}{TOMB_FILE_SUFFIX}")));
418    }
419
420    // Path from pass-tomb in store parent and in home
421    if let Some(parent) = parent {
422        paths.push(parent.join(format!(".password{TOMB_FILE_SUFFIX}")));
423    }
424    paths.push(format!("~/.password{TOMB_FILE_SUFFIX}").into());
425
426    paths
427}
428
429/// Find tomb path for given store root.
430///
431/// Uses `PASSWORD_STORE_TOMB_FILE` if set.
432/// This does not guarantee that the returned path is an actual tomb file.
433/// This is a best effort search.
434fn find_tomb_path(root: &Path) -> Option<PathBuf> {
435    // Take path from environment variable
436    if let Ok(path) = env::var("PASSWORD_STORE_TOMB_FILE") {
437        return Some(path.into());
438    }
439
440    // TODO: ensure file is large enough to be a tomb (tomb be at least 10 MB)
441    tomb_paths(root).into_iter().find(|p| p.is_file())
442}
443
444/// Build list of probable tomb key paths for given store root.
445fn tomb_key_paths(root: &Path) -> Vec<PathBuf> {
446    let mut paths = Vec::with_capacity(4);
447
448    // Get parent directory and file name
449    let parent = root.parent();
450    let file_name = root.file_name().and_then(|n| n.to_str());
451
452    // Same path as store root with .tomb suffix
453    if let (Some(parent), Some(file_name)) = (parent, file_name) {
454        paths.push(parent.join(format!("{file_name}{TOMB_KEY_FILE_SUFFIX}")));
455    }
456
457    // Path from pass-tomb in store parent and in home
458    if let Some(parent) = parent {
459        paths.push(parent.join(format!(".password{TOMB_KEY_FILE_SUFFIX}")));
460    }
461    paths.push(format!("~/.password{TOMB_KEY_FILE_SUFFIX}").into());
462
463    paths
464}
465
466/// Find tomb key path for given store root.
467///
468/// Uses `PASSWORD_STORE_TOMB_KEY` if set.
469/// This does not guarantee that the returned path is an actual tomb key file.
470/// This is a best effort search.
471fn find_tomb_key_path(root: &Path) -> Option<PathBuf> {
472    // Take path from environment variable
473    if let Ok(path) = env::var("PASSWORD_STORE_TOMB_KEY") {
474        return Some(path.into());
475    }
476
477    tomb_key_paths(root).into_iter().find(|p| p.is_file())
478}