vios_app 0.1.1

Small JSON vaults: Argon2id + AES-GCM, with optional AAD binding.
Documentation
// src/bin/vaultviewer.rs
#![cfg_attr(not(feature = "pdf"), allow(unused_variables))]

use std::env;
use std::error::Error;
use std::fmt::{self, Display};
use std::io::{self, Read, Write};

use clap::Parser;
use rpassword::read_password;

use vios_core::vault::vault_crypto::decrypt_vault;

#[derive(Parser, Debug)]
#[command(
    name = "vaultviewer",
    about = "View a VIOS vault (and optionally export to PDF)",
    version
)]
struct Args {
    /// Path to .vault file
    path: String,

    /// Export a PDF to this path (requires --features pdf)
    #[arg(long)]
    pdf_out: Option<String>,

    /// Read password from env var VIOS_PASSWORD
    #[arg(long)]
    password_from_env: bool,

    /// Read password from stdin (no prompt)
    #[arg(long)]
    password_stdin: bool,

    /// Print only the decrypted plaintext content to stdout
    #[arg(long)]
    dump_plaintext: bool,
}

#[derive(Debug)]
struct Msg(&'static str);
impl Display for Msg {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.0)
    }
}
impl Error for Msg {}

fn read_password_interactive() -> Result<String, Box<dyn Error>> {
    print!("Enter password: ");
    io::stdout().flush().ok();
    let pw = read_password().map_err(|_| Msg("failed to read password"))?;
    Ok(pw)
}

fn read_password_from_stdin() -> Result<String, Box<dyn Error>> {
    let mut buf = String::new();
    io::stdin().read_to_string(&mut buf)?;
    Ok(buf.trim_end().to_string())
}

fn resolve_password(args: &Args) -> Result<String, Box<dyn Error>> {
    if args.password_from_env && args.password_stdin {
        return Err(Box::new(Msg(
            "use only one of --password-from-env or --password-stdin",
        )));
    }
    if args.password_from_env {
        Ok(env::var("VIOS_PASSWORD").map_err(|_| Msg("VIOS_PASSWORD not set"))?)
    } else if args.password_stdin {
        read_password_from_stdin()
    } else {
        read_password_interactive()
    }
}

fn main() -> Result<(), Box<dyn Error>> {
    let args = Args::parse();

    let password = resolve_password(&args)?;
    let payload = decrypt_vault(&args.path, &password)
        .map_err(|e| format!("failed to decrypt '{}': {e}", &args.path))?;

    if args.dump_plaintext {
        println!("{}", payload.content);
    } else {
        println!("✅ Decrypted");
        println!("creator : {}", payload.metadata.creator_name);
        println!("created : {}", payload.metadata.created_at);
        println!("content : {}", payload.content);
    }

    if let Some(out) = args.pdf_out.as_deref() {
        #[cfg(feature = "pdf")]
        {
            vios_core::vault::pdf_exporter::export_vault_to_pdf(&args.path, &password, out)
                .map_err(|e| format!("PDF export failed to '{}': {e}", out))?;
            eprintln!("📝 PDF written: {out}");
        }
        #[cfg(not(feature = "pdf"))]
        {
            return Err(Box::new(Msg(
                "pdf feature not enabled. Re-run with `--features pdf`.",
            )));
        }
    }

    Ok(())
}