Skip to main content

rvaultlib/vaultfunc/
mod.rs

1use std::io::Cursor;
2
3use ansible_vault::{decrypt_vault, encrypt_vault};
4use thiserror::Error;
5
6use crate::types::{TypedValue, ValueType};
7
8const ANSIBLE_VAULT_PREFIX: &str = "$ANSIBLE_VAULT;";
9
10#[derive(Debug, Error)]
11pub enum VaultError {
12    #[error("value type isn't supported for encryption: {0}")]
13    UnsupportedType(String),
14    #[error("error while encrypting value: {0}")]
15    Encrypt(String),
16    #[error("error while decrypting value: {0}")]
17    Decrypt(String),
18    #[error("value doesn't contain Ansible vault prefix")]
19    NoVaultPrefix,
20    #[error("decrypted value is not valid UTF-8: {0}")]
21    Utf8(#[from] std::string::FromUtf8Error),
22}
23
24/// Encrypt a typed value using Ansible Vault AES256.
25/// If the value is a String that is already vault-encrypted, it is returned unchanged.
26/// NUMBER values are formatted with 6 decimal places (matching Go's `%f`) to ensure
27/// they round-trip correctly through `input_type` without being mis-detected as INTEGER.
28pub fn encrypt(value: &TypedValue, password: &str) -> Result<String, VaultError> {
29    if let TypedValue::String(s) = value {
30        if s.contains("$ANSIBLE_VAULT") {
31            return Ok(s.clone());
32        }
33    }
34
35    let str_value = match value {
36        TypedValue::Bool(b) => format!("{}", b),
37        TypedValue::Number(f) => format!("{:.6}", f),
38        TypedValue::Integer(i) => format!("{}", i),
39        TypedValue::String(s) => s.clone(),
40        TypedValue::Null => return Err(VaultError::UnsupportedType("Null".to_string())),
41    };
42
43    encrypt_vault(Cursor::new(str_value.as_bytes()), password)
44        .map_err(|e| VaultError::Encrypt(e.to_string()))
45}
46
47/// Decrypt an Ansible Vault encrypted string, returning the raw decrypted content as a string.
48/// Unlike [`decrypt`], no type detection is applied — the raw bytes are returned as-is.
49/// Strips any content before the `$ANSIBLE_VAULT;` prefix.
50pub fn decrypt_raw(value: &str, password: &str) -> Result<String, VaultError> {
51    let index = value
52        .find(ANSIBLE_VAULT_PREFIX)
53        .ok_or(VaultError::NoVaultPrefix)?;
54    let vault_str = &value[index..];
55    let decrypted_bytes = decrypt_vault(Cursor::new(vault_str.as_bytes()), password)
56        .map_err(|e| VaultError::Decrypt(e.to_string()))?;
57    String::from_utf8(decrypted_bytes).map_err(Into::into)
58}
59
60/// Decrypt an Ansible Vault encrypted string.
61/// Strips any content before the `$ANSIBLE_VAULT;` prefix (e.g. leading whitespace from YAML).
62/// Returns the decrypted value with its detected type.
63pub fn decrypt(value: &str, password: &str) -> Result<(TypedValue, ValueType), VaultError> {
64    let index = value
65        .find(ANSIBLE_VAULT_PREFIX)
66        .ok_or(VaultError::NoVaultPrefix)?;
67    let vault_str = &value[index..];
68
69    let decrypted_bytes = decrypt_vault(Cursor::new(vault_str.as_bytes()), password)
70        .map_err(|e| VaultError::Decrypt(e.to_string()))?;
71
72    let decrypted = String::from_utf8(decrypted_bytes)?;
73    Ok(input_type(&decrypted))
74}
75
76/// Parse a string and detect its most specific type.
77/// Tries INTEGER, then BOOL, then NUMBER, then falls back to STRING.
78pub fn input_type(value: &str) -> (TypedValue, ValueType) {
79    if let Ok(i) = value.parse::<i64>() {
80        return (TypedValue::Integer(i), ValueType::Integer);
81    }
82    if let Ok(b) = value.parse::<bool>() {
83        return (TypedValue::Bool(b), ValueType::Bool);
84    }
85    if let Ok(f) = value.parse::<f64>() {
86        return (TypedValue::Number(f), ValueType::Number);
87    }
88    (TypedValue::String(value.to_string()), ValueType::String)
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    const PASSWORD: &str = "_strengGeheim_";
96
97    #[test]
98    fn test_encrypt_decrypt_int() {
99        let tests: &[i64] = &[42, -45, 0];
100        for &v in tests {
101            let encrypted = encrypt(&TypedValue::Integer(v), PASSWORD).expect("encrypt failed");
102            let (decrypted, vt) = decrypt(&encrypted, PASSWORD).expect("decrypt failed");
103            assert_eq!(vt, ValueType::Integer);
104            assert_eq!(decrypted, TypedValue::Integer(v));
105        }
106    }
107
108    #[test]
109    fn test_encrypt_decrypt_bool() {
110        for &v in &[true, false] {
111            let encrypted = encrypt(&TypedValue::Bool(v), PASSWORD).expect("encrypt failed");
112            let (decrypted, vt) = decrypt(&encrypted, PASSWORD).expect("decrypt failed");
113            assert_eq!(vt, ValueType::Bool);
114            assert_eq!(decrypted, TypedValue::Bool(v));
115        }
116    }
117
118    #[test]
119    fn test_encrypt_decrypt_float() {
120        let tests: &[f64] = &[42.78, -13.8, 0.0];
121        for &v in tests {
122            let encrypted = encrypt(&TypedValue::Number(v), PASSWORD).expect("encrypt failed");
123            let (decrypted, vt) = decrypt(&encrypted, PASSWORD).expect("decrypt failed");
124            assert_eq!(vt, ValueType::Number);
125            assert_eq!(decrypted, TypedValue::Number(v));
126        }
127    }
128
129    #[test]
130    fn test_encrypt_decrypt_string() {
131        let tests: &[&str] = &["Some words with äoß", "1234x", ""];
132        for &v in tests {
133            let encrypted =
134                encrypt(&TypedValue::String(v.to_string()), PASSWORD).expect("encrypt failed");
135            let (decrypted, vt) = decrypt(&encrypted, PASSWORD).expect("decrypt failed");
136            assert_eq!(vt, ValueType::String);
137            assert_eq!(decrypted, TypedValue::String(v.to_string()));
138        }
139    }
140
141    // Compatibility tests: decrypt values encrypted by the Go implementation.
142    // Source: rust/resources/tests/partial_encrypted_example.yaml, password: "test999"
143
144    #[test]
145    fn test_decrypt_go_encrypted_bool() {
146        // second.a.v = true
147        let vault_str = concat!(
148            "$ANSIBLE_VAULT;1.1;AES256\n",
149            "35643238653231643030326432343462386436643537343863643061386261613835353835336666\n",
150            "3731623866666563373637326266663963383266636465330a303437336534643232353963346535\n",
151            "35356238303435303932336234356337383663346235653831316330376666323739313964343162\n",
152            "6132343332646432300a643232643534323562353330643336626432666235623237643132326563\n",
153            "3832"
154        );
155        let (value, vt) = decrypt(vault_str, "test999").expect("decrypt failed");
156        assert_eq!(vt, ValueType::Bool);
157        assert_eq!(value, TypedValue::Bool(true));
158    }
159
160    #[test]
161    fn test_decrypt_go_encrypted_int() {
162        // second.a.w = 14
163        let vault_str = concat!(
164            "$ANSIBLE_VAULT;1.1;AES256\n",
165            "30613433363731303632376132343263336664313761613234346366653461613337323865393561\n",
166            "6534386436626431373864343862633533383734623731660a313433643032373166383164326534\n",
167            "63343264636166653865353965666639316361626232366266356337666162316234376232373930\n",
168            "3437306362383631310a663030373266373338383935303061653566613039326637666564353438\n",
169            "3461"
170        );
171        let (value, vt) = decrypt(vault_str, "test999").expect("decrypt failed");
172        assert_eq!(vt, ValueType::Integer);
173        assert_eq!(value, TypedValue::Integer(14));
174    }
175}