oth_rvault 0.2.0

Partial Ansible Vault encoder and decoder
Documentation
use crate::{
    keys::is_encrypted,
    typedetect::{FileFormat, detect_format},
    types::{ProcessHandling, TypedValue, ValueType},
    vaultfunc,
};

/// Decrypt all (or a filtered subset of) vault-encrypted values in a JSON or YAML file.
///
/// - `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 decrypt; empty means all encrypted values
pub fn decrypt(
    input: &str,
    output: &str,
    password: &str,
    keys: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
    let mut processor = make_decrypt_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),
    }
}

pub(crate) fn make_decrypt_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();
        match vaultfunc::decrypt(&vault_str, password) {
            Ok((decrypted, new_vt)) => Ok((decrypted, new_vt, ProcessHandling::Process)),
            Err(e) => {
                eprintln!("error decrypting key '{}': {}", key_path, e);
                Ok((typed, vt, ProcessHandling::Skip))
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn resources(filename: &str) -> String {
        format!(
            "{}/resources/tests/{}",
            env!("CARGO_MANIFEST_DIR"),
            filename
        )
    }

    // --- YAML decrypt tests ---

    struct YamlDecryptCase {
        input: &'static str,
        reference: &'static str,
        keys: &'static [&'static str],
    }

    fn run_yaml_decrypt(case: &YamlDecryptCase) {
        let input = resources(case.input);
        let reference = resources(case.reference);
        let keys: Vec<String> = case.keys.iter().map(|s| s.to_string()).collect();

        let output = tempfile::NamedTempFile::new().unwrap();
        let output_path = output.path().to_str().unwrap().to_string();

        decrypt(&input, &output_path, "test999", &keys).unwrap();

        let got: serde_yaml::Value =
            serde_yaml::from_str(&std::fs::read_to_string(&output_path).unwrap()).unwrap();
        let expected: serde_yaml::Value =
            serde_yaml::from_str(&std::fs::read_to_string(&reference).unwrap()).unwrap();
        assert_eq!(got, expected, "YAML decrypt '{}' failed", case.input);
    }

    #[test]
    fn test_decrypt_yaml_all_keys() {
        run_yaml_decrypt(&YamlDecryptCase {
            input: "partial_encrypted_example.yaml",
            reference: "partial_encrypted_example_decrypted_01.yaml",
            keys: &[],
        });
    }

    #[test]
    fn test_decrypt_yaml_filtered_key() {
        run_yaml_decrypt(&YamlDecryptCase {
            input: "partial_encrypted_example.yaml",
            reference: "partial_encrypted_example_decrypted_03.yaml",
            keys: &["third.carrot"],
        });
    }

    #[test]
    fn test_decrypt_yaml_multiple_keys() {
        run_yaml_decrypt(&YamlDecryptCase {
            input: "partial_encrypted_example.yaml",
            reference: "partial_encrypted_example_decrypted_04.yaml",
            keys: &["first.a", "first.z", "second.b.2", "fourth.list"],
        });
    }

    #[test]
    fn test_decrypt_yaml_02() {
        run_yaml_decrypt(&YamlDecryptCase {
            input: "partial_encrypted_example_02.yaml",
            reference: "partial_encrypted_example_decrypted_01.yaml",
            keys: &[],
        });
    }

    #[test]
    fn test_decrypt_yaml_03() {
        run_yaml_decrypt(&YamlDecryptCase {
            input: "partial_encrypted_example_03.yaml",
            reference: "partial_encrypted_example_decrypted_01.yaml",
            keys: &[],
        });
    }

    // --- JSON decrypt tests ---

    struct JsonDecryptCase {
        input: &'static str,
        reference: &'static str,
        keys: &'static [&'static str],
    }

    fn run_json_decrypt(case: &JsonDecryptCase) {
        let input = resources(case.input);
        let reference = resources(case.reference);
        let keys: Vec<String> = case.keys.iter().map(|s| s.to_string()).collect();

        let output = tempfile::NamedTempFile::new().unwrap();
        let output_path = output.path().to_str().unwrap().to_string();

        decrypt(&input, &output_path, "test999", &keys).unwrap();

        let got: serde_json::Value =
            serde_json::from_str(&std::fs::read_to_string(&output_path).unwrap()).unwrap();
        let expected: serde_json::Value =
            serde_json::from_str(&std::fs::read_to_string(&reference).unwrap()).unwrap();
        assert_eq!(got, expected, "JSON decrypt '{}' failed", case.input);
    }

    #[test]
    fn test_decrypt_json_all_keys() {
        run_json_decrypt(&JsonDecryptCase {
            input: "partial_encrypted_example.json",
            reference: "partial_encrypted_example_decrypted_01.json",
            keys: &[],
        });
    }

    #[test]
    fn test_decrypt_json_filtered_key() {
        run_json_decrypt(&JsonDecryptCase {
            input: "partial_encrypted_example.json",
            reference: "partial_encrypted_example_decrypted_03.json",
            keys: &["third.carrot"],
        });
    }

    #[test]
    fn test_decrypt_json_multiple_keys() {
        run_json_decrypt(&JsonDecryptCase {
            input: "partial_encrypted_example.json",
            reference: "partial_encrypted_example_decrypted_04.json",
            keys: &["first.a", "first.z", "second.b.2", "fourth.list"],
        });
    }

    #[test]
    fn test_decrypt_json_02() {
        run_json_decrypt(&JsonDecryptCase {
            input: "partial_encrypted_example_02.json",
            reference: "partial_encrypted_example_decrypted_01.json",
            keys: &[],
        });
    }

    #[test]
    fn test_decrypt_json_03() {
        run_json_decrypt(&JsonDecryptCase {
            input: "partial_encrypted_example_03.json",
            reference: "partial_encrypted_example_decrypted_01.json",
            keys: &[],
        });
    }
}