use std::env;
use std::os::linux::fs::MetadataExt;
use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow};
use thiserror::Error;
use crate::crypto::Proto;
pub use crate::tomb_bin::TombSettings;
use crate::util;
use crate::{Key, Store, systemd_bin, tomb_bin};
pub const TOMB_AUTO_CLOSE_SEC: u32 = 5 * 60;
pub const TOMB_FILE_SUFFIX: &str = ".tomb";
pub const TOMB_KEY_FILE_SUFFIX: &str = ".tomb.key";
pub const SSH_PROCESS_NAME: &str = "ssh";
pub struct Tomb<'a> {
store: &'a Store,
pub settings: TombSettings,
}
impl<'a> Tomb<'a> {
pub fn new(store: &'a Store, quiet: bool, verbose: bool, force: bool) -> Tomb<'a> {
Self {
store,
settings: TombSettings {
quiet,
verbose,
force,
},
}
}
pub fn find_tomb_path(&self) -> Result<PathBuf> {
find_tomb_path(&self.store.root).ok_or_else(|| Err::CannotFindTomb.into())
}
pub fn find_tomb_key_path(&self) -> Result<PathBuf> {
find_tomb_key_path(&self.store.root).ok_or_else(|| Err::CannotFindTombKey.into())
}
pub fn open(&self) -> Result<Vec<Err>> {
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)?;
let mut errs = vec![];
if let Err(err) =
util::fs::sudo_chown_current_user(&self.store.root, false).map_err(Err::Chown)
{
errs.push(err);
}
Ok(errs)
}
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(())
}
pub fn close(&self) -> Result<()> {
let tomb = self.find_tomb_path()?;
util::git::kill_ssh_by_session(self.store);
tomb_bin::tomb_close(&tomb, self.settings).map_err(Err::Close)?;
Ok(())
}
pub fn prepare(&self) -> Result<()> {
if !self.is_tomb() {
return Ok(());
}
if self.is_open()? {
return Ok(());
}
if !self.settings.quiet {
eprintln!("Opening password store Tomb...");
}
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(())
}
pub fn start_timer(&self, sec: u32, force: bool) -> Result<()> {
let tomb_path = self.find_tomb_path()?;
let name = tomb_bin::name(&tomb_path).unwrap_or(".unwrap");
let unit = format!("prs-tomb-close@{name}.service");
if !force && systemd_bin::systemd_has_timer(&unit).map_err(Err::AutoCloseTimer)? {
return Ok(());
}
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(())
}
pub fn has_timer(&self) -> Result<bool> {
let tomb_path = self.find_tomb_path()?;
let name = tomb_bin::name(&tomb_path).unwrap_or(".unwrap");
let unit = format!("prs-tomb-close@{name}.service");
systemd_bin::systemd_has_timer(&unit).map_err(|err| Err::AutoCloseTimer(err).into())
}
pub fn stop_timer(&self) -> Result<()> {
let tomb_path = self.find_tomb_path()?;
let name = tomb_bin::name(&tomb_path).unwrap_or(".unwrap");
let unit = format!("prs-tomb-close@{name}.service");
if !systemd_bin::systemd_has_timer(&unit).map_err(Err::AutoCloseTimer)? {
return Ok(());
}
systemd_bin::systemd_remove_timer(&unit).map_err(Err::AutoCloseTimer)?;
Ok(())
}
pub fn finalize(&self) -> Result<()> {
Ok(())
}
pub fn init(&self, key: &Key, mbs: u32) -> Result<()> {
assert_eq!(key.proto(), Proto::Gpg, "key for Tomb is not a GPG key");
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)?;
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)?;
util::fs::sudo_chown_current_user(&store_tmp_dir, true).map_err(Err::Chown)?;
util::fs::copy_dir_contents(&self.store.root, &store_tmp_dir).map_err(Err::Init)?;
tomb_bin::tomb_close(&tomb_file, self.settings).map_err(Err::Init)?;
util::fs::sudo_chown_current_user(&store_tmp_dir, true).map_err(Err::Chown)?;
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)))?;
self.open()?;
Ok(())
}
pub fn is_tomb(&self) -> bool {
find_tomb_path(&self.store.root).is_some()
}
pub fn is_open(&self) -> Result<bool> {
if !self.store.root.is_dir() {
return Ok(false);
}
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());
}
Ok(false)
}
pub fn fetch_size_stats(&self) -> Result<TombSize> {
match self.find_tomb_path() {
Ok(tomb_path) => {
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 })
}
Err(_) => Ok(TombSize {
store: util::fs::dir_size(&self.store.root).ok(),
tomb_file: None,
}),
}
}
}
pub fn slam(settings: TombSettings) -> Result<()> {
tomb_bin::tomb_slam(settings).map_err(Err::Slam)?;
Ok(())
}
#[derive(Debug, Copy, Clone)]
pub struct TombSize {
pub store: Option<u64>,
pub tomb_file: Option<u64>,
}
impl TombSize {
pub fn tomb_file_size_mbs(&self) -> Option<u32> {
self.tomb_file.map(|s| (s / 1024 / 1024) as u32)
}
pub fn desired_tomb_size(&self) -> u32 {
self.store
.map(|bytes| ((bytes * 3) / 1024 / 1024).max(10) as u32)
.unwrap_or(10)
}
pub fn should_resize(&self) -> bool {
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 close password store tomb through tomb CLI")]
Close(#[source] anyhow::Error),
#[error("failed to resize password store tomb through tomb CLI")]
Resize(#[source] anyhow::Error),
#[error("failed to slam all open tombs through tomb CLI")]
Slam(#[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),
}
fn tomb_paths(root: &Path) -> Vec<PathBuf> {
let mut paths = Vec::with_capacity(4);
let parent = root.parent();
let file_name = root.file_name().and_then(|n| n.to_str());
if let (Some(parent), Some(file_name)) = (parent, file_name) {
paths.push(parent.join(format!("{file_name}{TOMB_FILE_SUFFIX}")));
}
if let Some(parent) = parent {
paths.push(parent.join(format!(".password{TOMB_FILE_SUFFIX}")));
}
paths.push(format!("~/.password{TOMB_FILE_SUFFIX}").into());
paths
}
fn find_tomb_path(root: &Path) -> Option<PathBuf> {
if let Ok(path) = env::var("PASSWORD_STORE_TOMB_FILE") {
return Some(path.into());
}
tomb_paths(root).into_iter().find(|p| p.is_file())
}
fn tomb_key_paths(root: &Path) -> Vec<PathBuf> {
let mut paths = Vec::with_capacity(4);
let parent = root.parent();
let file_name = root.file_name().and_then(|n| n.to_str());
if let (Some(parent), Some(file_name)) = (parent, file_name) {
paths.push(parent.join(format!("{file_name}{TOMB_KEY_FILE_SUFFIX}")));
}
if let Some(parent) = parent {
paths.push(parent.join(format!(".password{TOMB_KEY_FILE_SUFFIX}")));
}
paths.push(format!("~/.password{TOMB_KEY_FILE_SUFFIX}").into());
paths
}
fn find_tomb_key_path(root: &Path) -> Option<PathBuf> {
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())
}