use std::path::Path;
use clap::{Parser, Subcommand, ValueEnum};
use vios_app::vault::vault_crypto::{
read_vault_auto_file_with_aadmode, write_vault_v2_file_with_aadmode, AadMode, VaultCryptoError,
};
#[derive(Copy, Clone, Debug, ValueEnum)]
enum AadChoice {
None,
Path,
Custom,
}
fn get_pw(opt: Option<String>, env_name: &str, prompt: &str) -> String {
if let Some(s) = opt {
return s;
}
if let Ok(s) = std::env::var(env_name) {
return s;
}
rpassword::prompt_password(prompt).expect("failed to read password")
}
fn parse_hex_or_die(s: &str) -> Vec<u8> {
let s = s.trim();
if s.len() % 2 != 0 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
eprintln!("ERROR: --aad-hex must be even-length ASCII hex");
std::process::exit(2);
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
fn make_mode(aad: AadChoice, aad_hex: &Option<String>) -> AadMode<'static> {
match aad {
AadChoice::None => AadMode::None,
AadChoice::Path => AadMode::Path,
AadChoice::Custom => {
let hex = aad_hex.as_deref().unwrap_or_else(|| {
eprintln!("ERROR: --aad custom requires --aad-hex <HEX>");
std::process::exit(2);
});
let bytes = parse_hex_or_die(hex);
let leaked: &'static [u8] = Box::leak(bytes.into_boxed_slice());
AadMode::Custom(leaked)
}
}
}
#[derive(Parser)]
#[command(name = "vaultkit")]
#[command(about = "Seal / open small JSON vaults (Argon2id + AES-GCM)")]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
New {
#[arg(long)]
input: String,
#[arg(long)]
output: String,
#[arg(long)]
password: Option<String>,
#[arg(long, value_enum, default_value_t = AadChoice::None)]
aad: AadChoice,
#[arg(long = "aad-hex")]
aad_hex: Option<String>,
},
View {
#[arg(long)]
file: String,
#[arg(long)]
password: Option<String>,
#[arg(long, value_enum, default_value_t = AadChoice::None)]
aad: AadChoice,
#[arg(long = "aad-hex")]
aad_hex: Option<String>,
},
Verify {
#[arg(long)]
file: String,
#[arg(long)]
password: Option<String>,
#[arg(long, value_enum, default_value_t = AadChoice::None)]
aad: AadChoice,
#[arg(long = "aad-hex")]
aad_hex: Option<String>,
},
SetExpiry {
#[arg(long)]
file: String,
#[arg(long = "expires-at")]
expires_at: String,
#[arg(long)]
password: Option<String>,
#[arg(long, value_enum, default_value_t = AadChoice::None)]
aad: AadChoice,
#[arg(long = "aad-hex")]
aad_hex: Option<String>,
},
SetBurnFlag {
#[arg(long)]
file: String,
#[arg(long = "value")]
value: bool,
#[arg(long)]
password: Option<String>,
#[arg(long, value_enum, default_value_t = AadChoice::None)]
aad: AadChoice,
#[arg(long = "aad-hex")]
aad_hex: Option<String>,
},
RotateKey {
#[arg(long)]
file: String,
#[arg(long, value_enum, default_value_t = AadChoice::None)]
aad: AadChoice,
#[arg(long = "aad-hex")]
aad_hex: Option<String>,
},
Burn {
#[arg(long)]
file: String,
},
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.cmd {
Cmd::New {
input,
output,
password,
aad,
aad_hex,
} => {
let pw = get_pw(password, "VIOS_VAULT_PASSWORD", "Enter password: ");
let pt = std::fs::read(&input)?;
let mode = make_mode(aad, &aad_hex);
write_vault_v2_file_with_aadmode(Path::new(&output), &pw, &pt, mode)
.map_err(|e| anyhow::anyhow!("encrypt+save -> {}: {}", output, err(&e)))?;
println!("OK • New vault -> {}", output);
}
Cmd::View {
file,
password,
aad,
aad_hex,
} => {
let pw = get_pw(password, "VIOS_VAULT_PASSWORD", "Enter password: ");
let mode = make_mode(aad, &aad_hex);
let pt = read_vault_auto_file_with_aadmode(Path::new(&file), &pw, mode)
.map_err(|e| anyhow::anyhow!("decrypt -> {}: {}", file, err(&e)))?;
println!("{}", String::from_utf8_lossy(&pt));
}
Cmd::Verify {
file,
password,
aad,
aad_hex,
} => {
let pw = get_pw(password, "VIOS_VAULT_PASSWORD", "Enter password: ");
let mode = make_mode(aad, &aad_hex);
let _ = read_vault_auto_file_with_aadmode(Path::new(&file), &pw, mode)
.map_err(|e| anyhow::anyhow!("decrypt -> {}: {}", file, err(&e)))?;
println!("OK • decryptable");
}
Cmd::SetExpiry {
file,
expires_at,
password,
aad,
aad_hex,
} => {
let pw = get_pw(password, "VIOS_VAULT_PASSWORD", "Enter password: ");
let mode = make_mode(aad, &aad_hex);
let pt = read_vault_auto_file_with_aadmode(Path::new(&file), &pw, mode)
.map_err(|e| anyhow::anyhow!("decrypt -> {}: {}", file, err(&e)))?;
let mut v: serde_json::Value = serde_json::from_slice(&pt)?;
v["expires_at"] = serde_json::Value::String(expires_at);
let out = serde_json::to_vec_pretty(&v)?;
write_vault_v2_file_with_aadmode(Path::new(&file), &pw, &out, mode)
.map_err(|e| anyhow::anyhow!("encrypt+save -> {}: {}", file, err(&e)))?;
println!("OK • expires_at updated");
}
Cmd::SetBurnFlag {
file,
value,
password,
aad,
aad_hex,
} => {
let pw = get_pw(password, "VIOS_VAULT_PASSWORD", "Enter password: ");
let mode = make_mode(aad, &aad_hex);
let pt = read_vault_auto_file_with_aadmode(Path::new(&file), &pw, mode)
.map_err(|e| anyhow::anyhow!("decrypt -> {}: {}", file, err(&e)))?;
let mut v: serde_json::Value = serde_json::from_slice(&pt)?;
v["burn_on_view"] = serde_json::Value::Bool(value);
let out = serde_json::to_vec_pretty(&v)?;
write_vault_v2_file_with_aadmode(Path::new(&file), &pw, &out, mode)
.map_err(|e| anyhow::anyhow!("encrypt+save -> {}: {}", file, err(&e)))?;
println!("OK • burn_on_view = {}", value);
}
Cmd::RotateKey { file, aad, aad_hex } => {
let old_pw = std::env::var("VIOS_OLD_PASSWORD")
.map_err(|_| anyhow::anyhow!("set VIOS_OLD_PASSWORD"))?;
let new_pw = std::env::var("VIOS_NEW_PASSWORD")
.map_err(|_| anyhow::anyhow!("set VIOS_NEW_PASSWORD"))?;
let mode = make_mode(aad, &aad_hex);
let pt = read_vault_auto_file_with_aadmode(Path::new(&file), &old_pw, mode)
.map_err(|e| anyhow::anyhow!("decrypt(old) -> {}: {}", file, err(&e)))?;
write_vault_v2_file_with_aadmode(Path::new(&file), &new_pw, &pt, mode)
.map_err(|e| anyhow::anyhow!("encrypt+save(new) -> {}: {}", file, err(&e)))?;
println!("OK • password rotated");
}
Cmd::Burn { file } => {
std::fs::remove_file(&file)?;
println!("BURN • {}", file);
}
}
Ok(())
}
fn err(e: &VaultCryptoError) -> String {
use VaultCryptoError::*;
match e {
InvalidHeader => "invalid header".into(),
UnsupportedVersion => "unsupported version".into(),
UnsupportedKdf => "unsupported kdf".into(),
CryptoFailure => "crypto failure".into(),
Io(inner) => format!("io error: {}", inner),
Format(s) => format!("format error: {}", s),
}
}