passlane 3.1.0

A password manager and authenticator for the command line
pub mod add;
pub mod change_password;
pub mod completions;
pub mod delete;
pub mod edit;
pub mod export;
pub mod generate;
pub mod help;
pub mod import;
pub mod init;
pub mod list;
pub mod lock;
pub mod show;
pub mod unlock;

use crate::keychain;
use crate::store;

use crate::ui::input::{ask_master_password, ask_totp_master_password};
use crate::vault::entities::Error;
use crate::vault::keepass_vault::KeepassVault;
use crate::vault::vault_trait::Vault;
use clap::ArgMatches;
use clipboard::ClipboardContext;
use clipboard::ClipboardProvider;

pub(crate) trait MatchHandlerTemplate
where
    Self::ItemType: Clone,
{
    type ItemType;

    fn pre_handle_matches(&self, matches: &Vec<Self::ItemType>);
    fn handle_one_match(&mut self, the_match: Self::ItemType) -> Result<Option<String>, Error>;
    fn handle_many_matches(
        &mut self,
        matches: Vec<Self::ItemType>,
    ) -> Result<Option<String>, Error>;
}

pub(crate) fn handle_matches<H>(
    matches: Vec<H::ItemType>,
    handler: &mut Box<H>,
) -> Result<Option<String>, Error>
where
    H: MatchHandlerTemplate,
    H::ItemType: Clone,
{
    if matches.is_empty() {
        Ok(Some("No matches found".to_string()))
    } else {
        handler.pre_handle_matches(&matches.clone());

        if matches.len() == 1 {
            handler.handle_one_match(matches[0].clone())
        } else {
            handler.handle_many_matches(matches)
        }
    }
}

pub trait Action {
    fn run(&self) -> Result<String, Error> {
        Ok("Success".to_string())
    }
}

fn get_vault_properties() -> (String, String, Option<String>) {
    let stored_password = keychain::get_master_password();
    let master_pwd = stored_password.unwrap_or_else(|_| ask_master_password(None));
    let filepath = store::get_vault_path();
    let keyfile_path = store::get_keyfile_path();
    (master_pwd, filepath, keyfile_path)
}

fn unlock() -> Result<Box<dyn Vault>, Error> {
    let (master_pwd, filepath, keyfile_path) = get_vault_properties();
    println!("Unlocking vault...");
    get_vault(&master_pwd, &filepath, keyfile_path)
}

fn unlock_totp_vault() -> Result<Box<dyn Vault>, Error> {
    let stored_password = keychain::get_totp_master_password();
    let master_pwd = stored_password.unwrap_or_else(|_| ask_totp_master_password());
    let filepath = store::get_totp_vault_path();
    let keyfile_path = store::get_totp_keyfile_path();
    println!("Unlocking TOTP vault...");
    get_vault(&master_pwd, &filepath, keyfile_path)
}

fn get_vault(
    password: &str,
    filepath: &str,
    keyfile_path: Option<String>,
) -> Result<Box<dyn Vault>, Error> {
    // we could return some other Vault implementation here
    let vault = KeepassVault::open(password, filepath, keyfile_path)?;
    Ok(Box::new(vault))
}

pub fn copy_to_clipboard(value: &str) {
    let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
    ctx.set_contents(String::from(value)).unwrap();
}

/// Copies `value` to the clipboard, then waits `timeout_secs` seconds (or until
/// Ctrl+C) and clears the clipboard if its content still matches.
/// This function **blocks** for the timeout duration.
pub fn copy_to_clipboard_timed(value: &str, timeout_secs: u64) {
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::Arc;

    let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
    ctx.set_contents(String::from(value)).unwrap();

    let interrupted = Arc::new(AtomicBool::new(false));
    let interrupted_clone = interrupted.clone();
    let original = String::from(value);

    // Register Ctrl+C handler — sets the flag so the wait loop exits early.
    ctrlc::set_handler(move || {
        interrupted_clone.store(true, Ordering::SeqCst);
    })
    .ok(); // ignore error if handler already set

    // Wait in 100ms increments so we notice the interrupt quickly.
    let total_ms = timeout_secs * 1000;
    let mut elapsed = 0u64;
    while elapsed < total_ms && !interrupted.load(Ordering::SeqCst) {
        std::thread::sleep(std::time::Duration::from_millis(100));
        elapsed += 100;
    }

    // Clear clipboard if it still holds the password we put there.
    let result: Result<(), ()> = (|| {
        let mut ctx: ClipboardContext = ClipboardProvider::new().map_err(|_| ())?;
        let current = ctx.get_contents().map_err(|_| ())?;
        if current == original {
            ctx.set_contents(String::new()).map_err(|_| ())?;
        }
        Ok(())
    })();
    if result.is_err() {
        log::debug!("Failed to clear clipboard after timeout");
    }

    // If we were interrupted, exit after cleanup.
    if interrupted.load(Ordering::SeqCst) {
        std::process::exit(0);
    }
}

pub trait UnlockingAction {
    fn execute(&self) -> Result<Option<String>, Error> {
        if self.is_totp_vault() {
            self.run_with_vault(&mut unlock_totp_vault()?)
        } else {
            let mut vault = unlock()?;
            // Ensure completion cache exists when vault is open
            crate::completion_cache::ensure_cache_from_vault(&vault);
            self.run_with_vault(&mut vault)
        }
    }

    fn is_totp_vault(&self) -> bool {
        false
    }

    fn run_with_vault(&self, _: &mut Box<dyn Vault>) -> Result<Option<String>, Error> {
        Ok(Some("Success".to_string()))
    }
}

#[derive(Debug, PartialEq)]
pub enum ItemType {
    Credential,
    Payment,
    Note,
    Totp,
}

impl ItemType {
    pub fn new_from_args(matches: &ArgMatches) -> ItemType {
        if matches.get_one::<bool>("payments").map_or(false, |v| *v) {
            ItemType::Payment
        } else if matches.get_one("notes").map_or(false, |v| *v) {
            ItemType::Note
        } else if matches.get_one("otp").map_or(false, |v| *v) {
            ItemType::Totp
        } else {
            ItemType::Credential
        }
    }
}