Skip to main content

rvaultlib/decrypt/
mod.rs

1use inquire::InquireError;
2
3use crate::{
4    keys::{check_key_conflicts, is_encrypted},
5    typedetect::{FileFormat, detect_format},
6    types::{ProcessHandling, TypedValue, ValueType},
7    vaultfunc,
8};
9
10fn get_at_path_json<'a>(
11    val: &'a serde_json::Value,
12    path: &str,
13) -> Option<&'a serde_json::Value> {
14    let mut current = val;
15    for part in path.split('.') {
16        match current {
17            serde_json::Value::Object(map) => current = map.get(part)?,
18            _ => return None,
19        }
20    }
21    Some(current)
22}
23
24fn set_at_path_json(target: &mut serde_json::Value, path: &str, value: serde_json::Value) {
25    let parts: Vec<&str> = path.split('.').collect();
26    let mut current = target;
27    for (i, &part) in parts.iter().enumerate() {
28        if i == parts.len() - 1 {
29            if let serde_json::Value::Object(map) = current {
30                map.insert(part.to_string(), value);
31            }
32            return;
33        }
34        if let serde_json::Value::Object(map) = current {
35            map.entry(part.to_string())
36                .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
37            current = map.get_mut(part).unwrap();
38        }
39    }
40}
41
42fn extract_keys_json(root: &serde_json::Value, keys: &[String]) -> serde_json::Value {
43    let mut result = serde_json::Value::Object(serde_json::Map::new());
44    for key_path in keys {
45        if let Some(val) = get_at_path_json(root, key_path) {
46            set_at_path_json(&mut result, key_path, val.clone());
47        }
48    }
49    result
50}
51
52fn get_at_path_yaml<'a>(
53    val: &'a serde_yaml::Value,
54    path: &str,
55) -> Option<&'a serde_yaml::Value> {
56    let mut current = val;
57    for part in path.split('.') {
58        match current {
59            serde_yaml::Value::Mapping(map) => {
60                let str_key = serde_yaml::Value::String(part.to_string());
61                if let Some(v) = map.get(&str_key) {
62                    current = v;
63                } else if let Ok(n) = part.parse::<i64>() {
64                    let num_key = serde_yaml::Value::Number(n.into());
65                    current = map.get(&num_key)?;
66                } else {
67                    return None;
68                }
69            }
70            _ => return None,
71        }
72    }
73    Some(current)
74}
75
76fn set_at_path_yaml(target: &mut serde_yaml::Value, path: &str, value: serde_yaml::Value) {
77    let parts: Vec<&str> = path.split('.').collect();
78    let mut current = target;
79    for (i, &part) in parts.iter().enumerate() {
80        let key = serde_yaml::Value::String(part.to_string());
81        if i == parts.len() - 1 {
82            if let serde_yaml::Value::Mapping(map) = current {
83                map.insert(key, value);
84            }
85            return;
86        }
87        if let serde_yaml::Value::Mapping(map) = current {
88            if !map.contains_key(&key) {
89                map.insert(key.clone(), serde_yaml::Value::Mapping(serde_yaml::Mapping::new()));
90            }
91            current = map.get_mut(&key).unwrap();
92        }
93    }
94}
95
96fn extract_keys_yaml(root: &serde_yaml::Value, keys: &[String]) -> serde_yaml::Value {
97    let mut result = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
98    for key_path in keys {
99        if let Some(val) = get_at_path_yaml(root, key_path) {
100            set_at_path_yaml(&mut result, key_path, val.clone());
101        }
102    }
103    result
104}
105
106/// Decrypt all (or a filtered subset of) vault-encrypted values in a JSON or YAML file.
107///
108/// - `input`:    path to the input file
109/// - `output`:   path to write the result, or `"stdout"` to print to stdout
110/// - `password`: vault password
111/// - `keys`:     dot-notation key paths to decrypt; empty means all encrypted values
112pub fn decrypt(
113    input: &str,
114    output: &str,
115    password: &str,
116    keys: &[String],
117) -> Result<(), Box<dyn std::error::Error>> {
118    check_key_conflicts(keys)?;
119    let format = detect_format(input)?;
120    if format == FileFormat::Vault {
121        let content = std::fs::read_to_string(input)
122            .map_err(|e| format!("error reading '{}': {}", input, e))?;
123        let decrypted = vaultfunc::decrypt_raw(&content, password)?;
124        if output == "stdout" {
125            print!("{}", decrypted);
126        } else {
127            std::fs::write(output, &decrypted)
128                .map_err(|e| format!("error writing '{}': {}", output, e))?;
129        }
130        return Ok(());
131    }
132    let mut processor = make_decrypt_processor(password);
133    match format {
134        FileFormat::Json => crate::json::process_file(input, output, keys, &mut processor),
135        FileFormat::Yaml => crate::yaml::process_file(input, output, keys, &mut processor),
136        FileFormat::Vault => unreachable!(),
137    }
138}
139
140/// Like [`decrypt`] but outputs only the specified keys as a minimal nested JSON/YAML document.
141/// Requires at least one key to be specified.
142pub fn decrypt_value_only(
143    input: &str,
144    output: &str,
145    password: &str,
146    keys: &[String],
147) -> Result<(), Box<dyn std::error::Error>> {
148    check_key_conflicts(keys)?;
149    let format = detect_format(input)?;
150    if format == FileFormat::Vault {
151        return Err("--value-only is not applicable to a raw vault-encrypted file".into());
152    }
153    let mut processor = make_decrypt_processor(password);
154    match format {
155        FileFormat::Json => {
156            let tree = crate::json::process_in_memory(input, keys, &mut processor)?;
157            let minimal = extract_keys_json(&tree, keys);
158            let output_str = serde_json::to_string_pretty(&minimal)?;
159            if output == "stdout" {
160                println!("{}", output_str);
161            } else {
162                std::fs::write(output, &output_str)
163                    .map_err(|e| format!("error writing '{}': {}", output, e))?;
164            }
165        }
166        FileFormat::Yaml => {
167            let tree = crate::yaml::process_in_memory(input, keys, &mut processor)?;
168            let minimal = extract_keys_yaml(&tree, keys);
169            let yaml_str = serde_yaml::to_string(&minimal)?;
170            if output == "stdout" {
171                print!("{}", yaml_str);
172            } else {
173                std::fs::write(output, &yaml_str)
174                    .map_err(|e| format!("error writing '{}': {}", output, e))?;
175            }
176        }
177        FileFormat::Vault => unreachable!(),
178    }
179    Ok(())
180}
181
182/// Fuzzy-select which encrypted keys to decrypt, then run [`decrypt`] or [`decrypt_value_only`].
183///
184/// Shows a `MultiSelect` over all encrypted key paths in the file.
185/// If `value_only` is true, only the selected keys are emitted as a minimal document.
186pub fn decrypt_fuzzy(
187    input: &str,
188    output: &str,
189    password: &str,
190    value_only: bool,
191    color: bool,
192) -> Result<(), Box<dyn std::error::Error>> {
193    if !color {
194        inquire::set_global_render_config(inquire::ui::RenderConfig::empty());
195    }
196    let keys = match detect_format(input)? {
197        FileFormat::Json => crate::json::get_encrypted_keys(input)?,
198        FileFormat::Yaml => crate::yaml::get_encrypted_keys(input)?,
199        FileFormat::Vault => {
200            return Err("--interactive is not applicable to a raw vault-encrypted file".into())
201        }
202    };
203
204    if keys.is_empty() {
205        println!("No encrypted values found in '{}'.", input);
206        return Ok(());
207    }
208
209    let selected = match inquire::MultiSelect::new(
210        "Select keys to decrypt (Space to select, Enter to confirm):",
211        keys,
212    )
213    .prompt()
214    {
215        Ok(s) => s,
216        Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => {
217            println!("Cancelled.");
218            return Ok(());
219        }
220        Err(e) => return Err(e.into()),
221    };
222
223    if selected.is_empty() {
224        println!("No keys selected.");
225        return Ok(());
226    }
227
228    if value_only {
229        decrypt_value_only(input, output, password, &selected)
230    } else {
231        decrypt(input, output, password, &selected)
232    }
233}
234
235pub(crate) fn make_decrypt_processor(
236    password: &str,
237) -> impl FnMut(
238    TypedValue,
239    ValueType,
240    &str,
241) -> Result<(TypedValue, ValueType, ProcessHandling), Box<dyn std::error::Error>>
242+ '_ {
243    move |typed, vt, key_path| {
244        let (encrypted, vault_str) = is_encrypted(&typed);
245        if !encrypted {
246            return Ok((typed, vt, ProcessHandling::Skip));
247        }
248        let vault_str = vault_str.unwrap();
249        match vaultfunc::decrypt(&vault_str, password) {
250            Ok((decrypted, new_vt)) => Ok((decrypted, new_vt, ProcessHandling::Process)),
251            Err(e) => {
252                eprintln!("error decrypting key '{}': {}", key_path, e);
253                Ok((typed, vt, ProcessHandling::Skip))
254            }
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    fn resources(filename: &str) -> String {
264        format!(
265            "{}/resources/tests/{}",
266            env!("CARGO_MANIFEST_DIR"),
267            filename
268        )
269    }
270
271    // --- YAML decrypt tests ---
272
273    struct YamlDecryptCase {
274        input: &'static str,
275        reference: &'static str,
276        keys: &'static [&'static str],
277    }
278
279    fn run_yaml_decrypt(case: &YamlDecryptCase) {
280        let input = resources(case.input);
281        let reference = resources(case.reference);
282        let keys: Vec<String> = case.keys.iter().map(|s| s.to_string()).collect();
283
284        let output = tempfile::NamedTempFile::new().unwrap();
285        let output_path = output.path().to_str().unwrap().to_string();
286
287        decrypt(&input, &output_path, "test999", &keys).unwrap();
288
289        let got: serde_yaml::Value =
290            serde_yaml::from_str(&std::fs::read_to_string(&output_path).unwrap()).unwrap();
291        let expected: serde_yaml::Value =
292            serde_yaml::from_str(&std::fs::read_to_string(&reference).unwrap()).unwrap();
293        assert_eq!(got, expected, "YAML decrypt '{}' failed", case.input);
294    }
295
296    #[test]
297    fn test_decrypt_yaml_all_keys() {
298        run_yaml_decrypt(&YamlDecryptCase {
299            input: "partial_encrypted_example.yaml",
300            reference: "partial_encrypted_example_decrypted_01.yaml",
301            keys: &[],
302        });
303    }
304
305    #[test]
306    fn test_decrypt_yaml_filtered_key() {
307        run_yaml_decrypt(&YamlDecryptCase {
308            input: "partial_encrypted_example.yaml",
309            reference: "partial_encrypted_example_decrypted_03.yaml",
310            keys: &["third.carrot"],
311        });
312    }
313
314    #[test]
315    fn test_decrypt_yaml_multiple_keys() {
316        run_yaml_decrypt(&YamlDecryptCase {
317            input: "partial_encrypted_example.yaml",
318            reference: "partial_encrypted_example_decrypted_04.yaml",
319            keys: &["first.a", "first.z", "second.b.2", "fourth.list"],
320        });
321    }
322
323    #[test]
324    fn test_decrypt_yaml_02() {
325        run_yaml_decrypt(&YamlDecryptCase {
326            input: "partial_encrypted_example_02.yaml",
327            reference: "partial_encrypted_example_decrypted_01.yaml",
328            keys: &[],
329        });
330    }
331
332    #[test]
333    fn test_decrypt_yaml_03() {
334        run_yaml_decrypt(&YamlDecryptCase {
335            input: "partial_encrypted_example_03.yaml",
336            reference: "partial_encrypted_example_decrypted_01.yaml",
337            keys: &[],
338        });
339    }
340
341    // --- JSON decrypt tests ---
342
343    struct JsonDecryptCase {
344        input: &'static str,
345        reference: &'static str,
346        keys: &'static [&'static str],
347    }
348
349    fn run_json_decrypt(case: &JsonDecryptCase) {
350        let input = resources(case.input);
351        let reference = resources(case.reference);
352        let keys: Vec<String> = case.keys.iter().map(|s| s.to_string()).collect();
353
354        let output = tempfile::NamedTempFile::new().unwrap();
355        let output_path = output.path().to_str().unwrap().to_string();
356
357        decrypt(&input, &output_path, "test999", &keys).unwrap();
358
359        let got: serde_json::Value =
360            serde_json::from_str(&std::fs::read_to_string(&output_path).unwrap()).unwrap();
361        let expected: serde_json::Value =
362            serde_json::from_str(&std::fs::read_to_string(&reference).unwrap()).unwrap();
363        assert_eq!(got, expected, "JSON decrypt '{}' failed", case.input);
364    }
365
366    #[test]
367    fn test_decrypt_json_all_keys() {
368        run_json_decrypt(&JsonDecryptCase {
369            input: "partial_encrypted_example.json",
370            reference: "partial_encrypted_example_decrypted_01.json",
371            keys: &[],
372        });
373    }
374
375    #[test]
376    fn test_decrypt_json_filtered_key() {
377        run_json_decrypt(&JsonDecryptCase {
378            input: "partial_encrypted_example.json",
379            reference: "partial_encrypted_example_decrypted_03.json",
380            keys: &["third.carrot"],
381        });
382    }
383
384    #[test]
385    fn test_decrypt_json_multiple_keys() {
386        run_json_decrypt(&JsonDecryptCase {
387            input: "partial_encrypted_example.json",
388            reference: "partial_encrypted_example_decrypted_04.json",
389            keys: &["first.a", "first.z", "second.b.2", "fourth.list"],
390        });
391    }
392
393    #[test]
394    fn test_decrypt_json_02() {
395        run_json_decrypt(&JsonDecryptCase {
396            input: "partial_encrypted_example_02.json",
397            reference: "partial_encrypted_example_decrypted_01.json",
398            keys: &[],
399        });
400    }
401
402    #[test]
403    fn test_decrypt_json_03() {
404        run_json_decrypt(&JsonDecryptCase {
405            input: "partial_encrypted_example_03.json",
406            reference: "partial_encrypted_example_decrypted_01.json",
407            keys: &[],
408        });
409    }
410}