oth_rvault 0.2.0

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

/// Encrypt all (or a filtered subset of) scalar 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 encrypt; empty means all encryptable values
pub fn encrypt(
    input: &str,
    output: &str,
    password: &str,
    keys: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
    let mut processor = make_encrypt_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_encrypt_processor(
    password: &str,
) -> impl FnMut(
    TypedValue,
    ValueType,
    &str,
) -> Result<(TypedValue, ValueType, ProcessHandling), Box<dyn std::error::Error>>
+ '_ {
    move |typed, vt, key_path| {
        if !is_encryptable(vt) {
            return Ok((typed, vt, ProcessHandling::Skip));
        }
        match vaultfunc::encrypt(&typed, password) {
            Ok(encrypted) => Ok((
                TypedValue::String(encrypted),
                ValueType::String,
                ProcessHandling::Process,
            )),
            Err(e) => {
                eprintln!("error encrypting key '{}': {}", key_path, e);
                Ok((typed, vt, ProcessHandling::Skip))
            }
        }
    }
}

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

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

    #[test]
    fn test_encrypt_decrypt_roundtrip_yaml() {
        let input = resources("example.yaml");
        let encrypted = tempfile::NamedTempFile::new().unwrap();
        let decrypted = tempfile::NamedTempFile::new().unwrap();

        encrypt(&input, encrypted.path().to_str().unwrap(), "test999", &[]).unwrap();
        decrypt(
            encrypted.path().to_str().unwrap(),
            decrypted.path().to_str().unwrap(),
            "test999",
            &[],
        )
        .unwrap();

        let got: serde_yaml::Value = serde_yaml::from_str(
            &std::fs::read_to_string(decrypted.path()).unwrap(),
        )
        .unwrap();
        let expected: serde_yaml::Value =
            serde_yaml::from_str(&std::fs::read_to_string(&input).unwrap()).unwrap();
        assert_eq!(got, expected, "YAML roundtrip: encrypt+decrypt doesn't match input");
    }

    #[test]
    fn test_encrypt_decrypt_roundtrip_yaml_filtered() {
        let input = resources("example.yaml");
        let encrypted = tempfile::NamedTempFile::new().unwrap();
        let decrypted = tempfile::NamedTempFile::new().unwrap();
        let filter = vec!["third.carrot".to_string()];

        encrypt(
            &input,
            encrypted.path().to_str().unwrap(),
            "test999",
            &filter,
        )
        .unwrap();
        decrypt(
            encrypted.path().to_str().unwrap(),
            decrypted.path().to_str().unwrap(),
            "test999",
            &filter,
        )
        .unwrap();

        let got: serde_yaml::Value = serde_yaml::from_str(
            &std::fs::read_to_string(decrypted.path()).unwrap(),
        )
        .unwrap();
        let expected: serde_yaml::Value =
            serde_yaml::from_str(&std::fs::read_to_string(&input).unwrap()).unwrap();
        assert_eq!(
            got, expected,
            "filtered YAML roundtrip: encrypt+decrypt doesn't match input"
        );
    }

    #[test]
    fn test_encrypt_decrypt_roundtrip_json() {
        let input = resources("example.json");
        let encrypted = tempfile::NamedTempFile::new().unwrap();
        let decrypted = tempfile::NamedTempFile::new().unwrap();

        encrypt(&input, encrypted.path().to_str().unwrap(), "test999", &[]).unwrap();
        decrypt(
            encrypted.path().to_str().unwrap(),
            decrypted.path().to_str().unwrap(),
            "test999",
            &[],
        )
        .unwrap();

        let got: serde_json::Value = serde_json::from_str(
            &std::fs::read_to_string(decrypted.path()).unwrap(),
        )
        .unwrap();
        let expected: serde_json::Value =
            serde_json::from_str(&std::fs::read_to_string(&input).unwrap()).unwrap();
        assert_eq!(got, expected, "JSON roundtrip: encrypt+decrypt doesn't match input");
    }
}