oth_rvault 0.2.0

Partial Ansible Vault encoder and decoder
Documentation
use std::io::{self, Write};

use crate::{
    keys::is_encrypted,
    typedetect::{FileFormat, detect_format},
    types::{ProcessHandling, TypedValue, ValueType},
    vaultfunc,
};

/// Interactively edit vault-encrypted values in a JSON or YAML file.
///
/// For each encrypted value that matches `keys` (or all encrypted values when `keys` is empty),
/// the current decrypted value is shown and the user is prompted for a replacement.
/// An empty input keeps the existing encrypted value; `q` aborts processing.
///
/// - `input`:    path to the input file
/// - `output`:   path to write the result, or `"stdout"` to print to stdout
/// - `password`: vault password
/// - `keys`:     dot-notation key paths to edit; empty means all encrypted values
pub fn edit(
    input: &str,
    output: &str,
    password: &str,
    keys: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
    println!("Editing: {input}");
    println!();
    let mut processor = make_edit_processor(password);
    match detect_format(input)? {
        FileFormat::Json => crate::json::process_file(input, output, keys, &mut processor),
        FileFormat::Yaml => crate::yaml::process_file(input, output, keys, &mut processor),
    }
}

fn display_value(v: &TypedValue) -> String {
    match v {
        TypedValue::String(s) => s.clone(),
        TypedValue::Integer(i) => i.to_string(),
        TypedValue::Bool(b) => b.to_string(),
        TypedValue::Number(f) => f.to_string(),
        TypedValue::Null => "null".to_string(),
    }
}

pub(crate) fn make_edit_processor(
    password: &str,
) -> impl FnMut(
    TypedValue,
    ValueType,
    &str,
) -> Result<(TypedValue, ValueType, ProcessHandling), Box<dyn std::error::Error>>
+ '_ {
    move |typed, vt, key_path| {
        let (encrypted, vault_str) = is_encrypted(&typed);
        if !encrypted {
            return Ok((typed, vt, ProcessHandling::Skip));
        }
        let vault_str = vault_str.unwrap();

        let (decrypted_val, _) = match vaultfunc::decrypt(&vault_str, password) {
            Ok(result) => result,
            Err(e) => {
                eprintln!("[!] could not decrypt '{key_path}': {e}");
                return Ok((typed, vt, ProcessHandling::Skip));
            }
        };

        println!("key:     {key_path}");
        println!("current: {}", display_value(&decrypted_val));
        print!("new value (empty = keep, q = quit): ");
        io::stdout().flush()?;

        let mut line = String::new();
        io::stdin().read_line(&mut line)?;
        let trimmed = line.trim();

        println!();

        if trimmed.is_empty() {
            return Ok((typed, vt, ProcessHandling::Skip));
        }
        if trimmed.eq_ignore_ascii_case("q") {
            println!("Cancelled.");
            return Ok((typed, vt, ProcessHandling::Cancel));
        }

        let (new_typed, _) = vaultfunc::input_type(trimmed);
        match vaultfunc::encrypt(&new_typed, password) {
            Ok(enc) => Ok((TypedValue::String(enc), ValueType::String, ProcessHandling::Process)),
            Err(e) => {
                eprintln!("[!] could not encrypt new value for '{key_path}': {e}");
                Ok((typed, vt, ProcessHandling::Skip))
            }
        }
    }
}