revault_cli 0.0.3

CLI for reVault encrypted lockboxes, store files, variables and forms with integration to your platform key storage
use super::context::{cli_error, CliResult};
use clap::ArgMatches;
use revault_lockbox_api::{Error, Lockbox, LockboxFileInspection, LockboxKeySlotProtection};
use revault_vault_api::{
    agent_log_destination, agent_sleep_support, default_vault_path, get_platform_vault_password,
    is_running, list, local_vault, platform_secret_store_disabled, platform_secret_store_status,
    verify_agent_transport_security, SecretString, VaultDirectory,
};
use std::fs::OpenOptions;
use std::path::Path;

pub(crate) fn run_matches(matches: &ArgMatches) -> CliResult<()> {
    match matches.get_one::<String>("lockbox") {
        Some(lockbox) => run_lockbox(lockbox),
        None => run_global(),
    }
}

fn run_global() -> CliResult<()> {
    let vault_path = default_vault_path()?;
    println!("reVault");
    println!("  version: {}", env!("CARGO_PKG_VERSION"));
    println!();
    println!("Local vault");
    println!("  path: {}", vault_path.display());
    println!("  exists: {}", yes_no(vault_path.exists()));
    println!(
        "  readable: {}",
        yes_no(std::fs::File::open(&vault_path).is_ok())
    );
    println!(
        "  writable: {}",
        yes_no(if vault_path.exists() {
            OpenOptions::new().append(true).open(&vault_path).is_ok()
        } else {
            vault_path
                .parent()
                .and_then(|parent| parent.metadata().ok())
                .map(|metadata| !metadata.permissions().readonly())
                .unwrap_or(false)
        })
    );
    println!();
    let auto_open = platform_secret_store_status()?;
    println!("Auto-open");
    println!("  supported: {}", yes_no(auto_open.supported));
    println!("  scope: {}", auto_open.scope.as_str());
    println!("  backend: {}", auto_open.backend);
    println!();
    println!("Session agent");
    let sleep_support = agent_sleep_support();
    println!(
        "  transport security: {}",
        if verify_agent_transport_security().is_ok() {
            "ok"
        } else {
            "unsupported"
        }
    );
    println!(
        "  suspend management: {}",
        yes_no(sleep_support.supported())
    );
    println!(
        "  suspend notifications: {}",
        yes_no(sleep_support.suspend_notifications)
    );
    println!(
        "  sleep prevention: {}",
        yes_no(sleep_support.sleep_inhibition)
    );
    println!("  running: {}", yes_no(is_running()));
    match list() {
        Ok(lockboxes) => println!("  open lockboxes: {}", lockboxes.len()),
        Err(err) => println!("  open lockboxes: unknown: {err}"),
    }
    println!("  log: {}", agent_log_destination());
    println!();
    println!("Known lockboxes");
    match default_vault_noninteractive() {
        Ok(Some(vault)) => {
            let known = vault.list_known_lockboxes()?;
            let mut present = 0usize;
            let mut missing = Vec::new();
            let mut inaccessible = Vec::new();
            for lockbox in known {
                match std::fs::metadata(&lockbox.path) {
                    Ok(_) => present += 1,
                    Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
                        missing.push(lockbox.path)
                    }
                    Err(_) => inaccessible.push(lockbox.path),
                }
            }
            println!("  present: {present}");
            println!("  missing: {}", missing.len());
            println!("  inaccessible: {}", inaccessible.len());
            if !missing.is_empty() {
                println!("  missing paths:");
                for path in missing {
                    println!("    {path}");
                    println!("      run: lockbox vault lockbox forget {path}");
                }
            }
        }
        Ok(None) => {
            println!("  not checked: vault is closed");
        }
        Err(err) => {
            println!("  not checked: {err}");
        }
    }
    Ok(())
}

fn run_lockbox(lockbox_path: &str) -> CliResult<()> {
    let path = Path::new(lockbox_path);
    let metadata = std::fs::metadata(path).map_err(|err| {
        if err.kind() == std::io::ErrorKind::NotFound {
            cli_error(format!("lockbox not found: {lockbox_path}"))
        } else if err.kind() == std::io::ErrorKind::PermissionDenied {
            cli_error(format!("permission denied reading lockbox: {lockbox_path}"))
        } else {
            cli_error(format!("cannot access lockbox {lockbox_path}: {err}"))
        }
    })?;
    if metadata.is_dir() {
        return Err(cli_error(format!(
            "lockbox path is a directory: {lockbox_path}"
        )));
    }

    let inspection = Lockbox::inspect_file(path)?;
    println!("Lockbox");
    println!("  path: {lockbox_path}");
    println!("  readable: {}", yes_no(std::fs::File::open(path).is_ok()));
    println!("  size: {} bytes", metadata.len());
    println!("  id: {}", inspection.lockbox_id);
    println!("  header: {}", header_status(&inspection));
    println!();
    print_access_methods(&inspection);
    println!();
    print_lockbox_session(&inspection);
    println!();
    print_revault_vault_api(&inspection);
    println!();
    print_open_checks(path, lockbox_path);
    Ok(())
}

fn print_access_methods(inspection: &LockboxFileInspection) {
    let password_count = inspection
        .key_slots
        .iter()
        .filter(|slot| slot.protection == LockboxKeySlotProtection::Password)
        .count();
    let contact_count = inspection
        .key_slots
        .iter()
        .filter(|slot| slot.protection == LockboxKeySlotProtection::Contact)
        .count();
    println!("Access methods");
    println!("  pass phrase slots: {password_count}");
    println!("  contact-key slots: {contact_count}");
    println!(
        "  key directory: {}",
        if inspection.key_directory_copy_count == 0 {
            "not found".to_string()
        } else {
            format!(
                "generation {}, {} readable copy/copies",
                inspection.key_directory_generation, inspection.key_directory_copy_count
            )
        }
    );
    if !inspection.key_slots.is_empty() {
        println!("  slots:");
        for slot in &inspection.key_slots {
            println!("    {}: {}", slot.id, slot_protection(slot.protection));
        }
    }
}

fn print_lockbox_session(inspection: &LockboxFileInspection) {
    println!("Session");
    match list() {
        Ok(lockboxes) => {
            let cached = lockboxes
                .iter()
                .find(|lockbox| lockbox.id == inspection.lockbox_id.to_string());
            println!("  open: {}", yes_no(cached.is_some()));
            if let Some(cached) = cached.and_then(|lockbox| lockbox.path.as_deref()) {
                println!("  cached path: {cached}");
            }
        }
        Err(err) => {
            println!("  open: unknown");
            println!("  session check: {err}");
        }
    }
}

fn print_revault_vault_api(inspection: &LockboxFileInspection) {
    println!("Local vault");
    match default_vault_noninteractive() {
        Ok(Some(vault)) => {
            println!("  open: yes");
            println!(
                "  key-directory backup: {}",
                yes_no(
                    vault
                        .load_key_directory_backup(inspection.lockbox_id)
                        .is_ok()
                )
            );
            match vault.list_private_keys() {
                Ok(keys) => println!("  profiles: {}", keys.len()),
                Err(err) => println!("  profiles: not checked: {err}"),
            }
        }
        Ok(None) => {
            println!("  open: no");
            println!("  key-directory backup: not checked");
        }
        Err(err) => {
            println!("  open: no");
            println!("  key-directory backup: not checked: {err}");
        }
    }
}

fn print_open_checks(path: &Path, lockbox_path: &str) {
    println!("Open checks");
    match local_vault().open_lockbox(path) {
        Ok(lockbox) => {
            let inspector = lockbox.inspector();
            println!("  open: yes");
            match inspector.storage_len() {
                Ok(len) => println!("  storage length: {len} bytes"),
                Err(err) => println!("  storage length: not checked: {err}"),
            }
            match inspector.inspect_pages() {
                Ok(pages) => println!("  pages: {}", pages.len()),
                Err(err) => println!("  pages: not checked: {err}"),
            }
            let report = inspector.recovery_report();
            println!("  intact files: {}", report.intact_file_count);
            println!("  partial files: {}", report.partial_files);
        }
        Err(Error::VaultUnavailable(message)) if message.contains("no cached content key") => {
            println!("  open: no");
            println!("  additional checks require an open lockbox.");
            println!("  run: lockbox open {lockbox_path}");
            println!("  then: lockbox doctor {lockbox_path}");
        }
        Err(err) => {
            println!("  open: no");
            println!("  additional checks unavailable: {err}");
            println!("  run after opening: lockbox doctor {lockbox_path}");
        }
    }
}

fn header_status(inspection: &LockboxFileInspection) -> &'static str {
    if inspection.header_readable {
        "ok"
    } else {
        "corrupt; recovered key-directory metadata"
    }
}

fn slot_protection(protection: LockboxKeySlotProtection) -> &'static str {
    match protection {
        LockboxKeySlotProtection::Password => "pass phrase",
        LockboxKeySlotProtection::Contact => "contact key",
        _ => "unknown",
    }
}

fn yes_no(value: bool) -> &'static str {
    if value {
        "yes"
    } else {
        "no"
    }
}

fn default_vault_noninteractive() -> Result<Option<VaultDirectory>, Box<dyn std::error::Error>> {
    if let Some(password) = SecretString::try_from_env("LOCKBOX_VAULT_PASSWORD")? {
        return Ok(Some(VaultDirectory::open_or_create_default(&password)?));
    }
    if !platform_secret_store_disabled()? {
        if let Ok(Some(password)) = get_platform_vault_password() {
            return Ok(Some(VaultDirectory::open_or_create_default(&password)?));
        }
    }
    Ok(None)
}