1use 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
15pub const TOMB_AUTO_CLOSE_SEC: u32 = 5 * 60;
17
18pub const TOMB_FILE_SUFFIX: &str = ".tomb";
20
21pub const TOMB_KEY_FILE_SUFFIX: &str = ".tomb.key";
23
24pub const SSH_PROCESS_NAME: &str = "ssh";
26
27pub struct Tomb<'a> {
29 store: &'a Store,
31
32 pub settings: TombSettings,
34}
35
36impl<'a> Tomb<'a> {
37 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 pub fn find_tomb_path(&self) -> Result<PathBuf> {
53 find_tomb_path(&self.store.root).ok_or_else(|| Err::CannotFindTomb.into())
54 }
55
56 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 pub fn open(&self) -> Result<Vec<Err>> {
69 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 let mut errs = vec![];
77
78 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 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 pub fn close(&self) -> Result<()> {
100 let tomb = self.find_tomb_path()?;
101
102 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 pub fn prepare(&self) -> Result<()> {
113 if !self.is_tomb() {
117 return Ok(());
118 }
119
120 if self.is_open()? {
122 return Ok(());
123 }
124
125 if !self.settings.quiet {
126 eprintln!("Opening password store Tomb...");
127 }
128
129 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 pub fn start_timer(&self, sec: u32, force: bool) -> Result<()> {
146 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 if !force && systemd_bin::systemd_has_timer(&unit).map_err(Err::AutoCloseTimer)? {
153 return Ok(());
154 }
155
156 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 pub fn has_timer(&self) -> Result<bool> {
186 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 pub fn stop_timer(&self) -> Result<()> {
196 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 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 pub fn finalize(&self) -> Result<()> {
212 Ok(())
214 }
215
216 pub fn init(&self, key: &Key, mbs: u32) -> Result<()> {
226 assert_eq!(key.proto(), Proto::Gpg, "key for Tomb is not a GPG key");
228
229 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 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 util::fs::sudo_chown_current_user(&store_tmp_dir, true).map_err(Err::Chown)?;
252
253 util::fs::copy_dir_contents(&self.store.root, &store_tmp_dir).map_err(Err::Init)?;
255
256 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 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 self.open()?;
267
268 Ok(())
269 }
270
271 pub fn is_tomb(&self) -> bool {
276 find_tomb_path(&self.store.root).is_some()
277 }
278
279 pub fn is_open(&self) -> Result<bool> {
283 if !self.store.root.is_dir() {
285 return Ok(false);
286 }
287
288 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 Ok(false)
298 }
299
300 pub fn fetch_size_stats(&self) -> Result<TombSize> {
307 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
327pub fn slam(settings: TombSettings) -> Result<()> {
332 tomb_bin::tomb_slam(settings).map_err(Err::Slam)?;
333 Ok(())
334}
335
336#[derive(Debug, Copy, Clone)]
338pub struct TombSize {
339 pub store: Option<u64>,
341
342 pub tomb_file: Option<u64>,
344}
345
346impl TombSize {
347 pub fn tomb_file_size_mbs(&self) -> Option<u32> {
349 self.tomb_file.map(|s| (s / 1024 / 1024) as u32)
350 }
351
352 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 pub fn should_resize(&self) -> bool {
363 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
407fn tomb_paths(root: &Path) -> Vec<PathBuf> {
409 let mut paths = Vec::with_capacity(4);
410
411 let parent = root.parent();
413 let file_name = root.file_name().and_then(|n| n.to_str());
414
415 if let (Some(parent), Some(file_name)) = (parent, file_name) {
417 paths.push(parent.join(format!("{file_name}{TOMB_FILE_SUFFIX}")));
418 }
419
420 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
429fn find_tomb_path(root: &Path) -> Option<PathBuf> {
435 if let Ok(path) = env::var("PASSWORD_STORE_TOMB_FILE") {
437 return Some(path.into());
438 }
439
440 tomb_paths(root).into_iter().find(|p| p.is_file())
442}
443
444fn tomb_key_paths(root: &Path) -> Vec<PathBuf> {
446 let mut paths = Vec::with_capacity(4);
447
448 let parent = root.parent();
450 let file_name = root.file_name().and_then(|n| n.to_str());
451
452 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 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
466fn find_tomb_key_path(root: &Path) -> Option<PathBuf> {
472 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}