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 mut processor = make_decrypt_processor(password);
120    match detect_format(input)? {
121        FileFormat::Json => crate::json::process_file(input, output, keys, &mut processor),
122        FileFormat::Yaml => crate::yaml::process_file(input, output, keys, &mut processor),
123    }
124}
125
126/// Like [`decrypt`] but outputs only the specified keys as a minimal nested JSON/YAML document.
127/// Requires at least one key to be specified.
128pub fn decrypt_value_only(
129    input: &str,
130    output: &str,
131    password: &str,
132    keys: &[String],
133) -> Result<(), Box<dyn std::error::Error>> {
134    check_key_conflicts(keys)?;
135    let mut processor = make_decrypt_processor(password);
136    match detect_format(input)? {
137        FileFormat::Json => {
138            let tree = crate::json::process_in_memory(input, keys, &mut processor)?;
139            let minimal = extract_keys_json(&tree, keys);
140            let output_str = serde_json::to_string_pretty(&minimal)?;
141            if output == "stdout" {
142                println!("{}", output_str);
143            } else {
144                std::fs::write(output, &output_str)
145                    .map_err(|e| format!("error writing '{}': {}", output, e))?;
146            }
147        }
148        FileFormat::Yaml => {
149            let tree = crate::yaml::process_in_memory(input, keys, &mut processor)?;
150            let minimal = extract_keys_yaml(&tree, keys);
151            let yaml_str = serde_yaml::to_string(&minimal)?;
152            if output == "stdout" {
153                print!("{}", yaml_str);
154            } else {
155                std::fs::write(output, &yaml_str)
156                    .map_err(|e| format!("error writing '{}': {}", output, e))?;
157            }
158        }
159    }
160    Ok(())
161}
162
163/// Fuzzy-select which encrypted keys to decrypt, then run [`decrypt`] or [`decrypt_value_only`].
164///
165/// Shows a `MultiSelect` over all encrypted key paths in the file.
166/// If `value_only` is true, only the selected keys are emitted as a minimal document.
167pub fn decrypt_fuzzy(
168    input: &str,
169    output: &str,
170    password: &str,
171    value_only: bool,
172) -> Result<(), Box<dyn std::error::Error>> {
173    let keys = match detect_format(input)? {
174        FileFormat::Json => crate::json::get_encrypted_keys(input)?,
175        FileFormat::Yaml => crate::yaml::get_encrypted_keys(input)?,
176    };
177
178    if keys.is_empty() {
179        println!("No encrypted values found in '{}'.", input);
180        return Ok(());
181    }
182
183    let selected = match inquire::MultiSelect::new(
184        "Select keys to decrypt (Space to select, Enter to confirm):",
185        keys,
186    )
187    .prompt()
188    {
189        Ok(s) => s,
190        Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => {
191            println!("Cancelled.");
192            return Ok(());
193        }
194        Err(e) => return Err(e.into()),
195    };
196
197    if selected.is_empty() {
198        println!("No keys selected.");
199        return Ok(());
200    }
201
202    if value_only {
203        decrypt_value_only(input, output, password, &selected)
204    } else {
205        decrypt(input, output, password, &selected)
206    }
207}
208
209pub(crate) fn make_decrypt_processor(
210    password: &str,
211) -> impl FnMut(
212    TypedValue,
213    ValueType,
214    &str,
215) -> Result<(TypedValue, ValueType, ProcessHandling), Box<dyn std::error::Error>>
216+ '_ {
217    move |typed, vt, key_path| {
218        let (encrypted, vault_str) = is_encrypted(&typed);
219        if !encrypted {
220            return Ok((typed, vt, ProcessHandling::Skip));
221        }
222        let vault_str = vault_str.unwrap();
223        match vaultfunc::decrypt(&vault_str, password) {
224            Ok((decrypted, new_vt)) => Ok((decrypted, new_vt, ProcessHandling::Process)),
225            Err(e) => {
226                eprintln!("error decrypting key '{}': {}", key_path, e);
227                Ok((typed, vt, ProcessHandling::Skip))
228            }
229        }
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    fn resources(filename: &str) -> String {
238        format!(
239            "{}/resources/tests/{}",
240            env!("CARGO_MANIFEST_DIR"),
241            filename
242        )
243    }
244
245    // --- YAML decrypt tests ---
246
247    struct YamlDecryptCase {
248        input: &'static str,
249        reference: &'static str,
250        keys: &'static [&'static str],
251    }
252
253    fn run_yaml_decrypt(case: &YamlDecryptCase) {
254        let input = resources(case.input);
255        let reference = resources(case.reference);
256        let keys: Vec<String> = case.keys.iter().map(|s| s.to_string()).collect();
257
258        let output = tempfile::NamedTempFile::new().unwrap();
259        let output_path = output.path().to_str().unwrap().to_string();
260
261        decrypt(&input, &output_path, "test999", &keys).unwrap();
262
263        let got: serde_yaml::Value =
264            serde_yaml::from_str(&std::fs::read_to_string(&output_path).unwrap()).unwrap();
265        let expected: serde_yaml::Value =
266            serde_yaml::from_str(&std::fs::read_to_string(&reference).unwrap()).unwrap();
267        assert_eq!(got, expected, "YAML decrypt '{}' failed", case.input);
268    }
269
270    #[test]
271    fn test_decrypt_yaml_all_keys() {
272        run_yaml_decrypt(&YamlDecryptCase {
273            input: "partial_encrypted_example.yaml",
274            reference: "partial_encrypted_example_decrypted_01.yaml",
275            keys: &[],
276        });
277    }
278
279    #[test]
280    fn test_decrypt_yaml_filtered_key() {
281        run_yaml_decrypt(&YamlDecryptCase {
282            input: "partial_encrypted_example.yaml",
283            reference: "partial_encrypted_example_decrypted_03.yaml",
284            keys: &["third.carrot"],
285        });
286    }
287
288    #[test]
289    fn test_decrypt_yaml_multiple_keys() {
290        run_yaml_decrypt(&YamlDecryptCase {
291            input: "partial_encrypted_example.yaml",
292            reference: "partial_encrypted_example_decrypted_04.yaml",
293            keys: &["first.a", "first.z", "second.b.2", "fourth.list"],
294        });
295    }
296
297    #[test]
298    fn test_decrypt_yaml_02() {
299        run_yaml_decrypt(&YamlDecryptCase {
300            input: "partial_encrypted_example_02.yaml",
301            reference: "partial_encrypted_example_decrypted_01.yaml",
302            keys: &[],
303        });
304    }
305
306    #[test]
307    fn test_decrypt_yaml_03() {
308        run_yaml_decrypt(&YamlDecryptCase {
309            input: "partial_encrypted_example_03.yaml",
310            reference: "partial_encrypted_example_decrypted_01.yaml",
311            keys: &[],
312        });
313    }
314
315    // --- JSON decrypt tests ---
316
317    struct JsonDecryptCase {
318        input: &'static str,
319        reference: &'static str,
320        keys: &'static [&'static str],
321    }
322
323    fn run_json_decrypt(case: &JsonDecryptCase) {
324        let input = resources(case.input);
325        let reference = resources(case.reference);
326        let keys: Vec<String> = case.keys.iter().map(|s| s.to_string()).collect();
327
328        let output = tempfile::NamedTempFile::new().unwrap();
329        let output_path = output.path().to_str().unwrap().to_string();
330
331        decrypt(&input, &output_path, "test999", &keys).unwrap();
332
333        let got: serde_json::Value =
334            serde_json::from_str(&std::fs::read_to_string(&output_path).unwrap()).unwrap();
335        let expected: serde_json::Value =
336            serde_json::from_str(&std::fs::read_to_string(&reference).unwrap()).unwrap();
337        assert_eq!(got, expected, "JSON decrypt '{}' failed", case.input);
338    }
339
340    #[test]
341    fn test_decrypt_json_all_keys() {
342        run_json_decrypt(&JsonDecryptCase {
343            input: "partial_encrypted_example.json",
344            reference: "partial_encrypted_example_decrypted_01.json",
345            keys: &[],
346        });
347    }
348
349    #[test]
350    fn test_decrypt_json_filtered_key() {
351        run_json_decrypt(&JsonDecryptCase {
352            input: "partial_encrypted_example.json",
353            reference: "partial_encrypted_example_decrypted_03.json",
354            keys: &["third.carrot"],
355        });
356    }
357
358    #[test]
359    fn test_decrypt_json_multiple_keys() {
360        run_json_decrypt(&JsonDecryptCase {
361            input: "partial_encrypted_example.json",
362            reference: "partial_encrypted_example_decrypted_04.json",
363            keys: &["first.a", "first.z", "second.b.2", "fourth.list"],
364        });
365    }
366
367    #[test]
368    fn test_decrypt_json_02() {
369        run_json_decrypt(&JsonDecryptCase {
370            input: "partial_encrypted_example_02.json",
371            reference: "partial_encrypted_example_decrypted_01.json",
372            keys: &[],
373        });
374    }
375
376    #[test]
377    fn test_decrypt_json_03() {
378        run_json_decrypt(&JsonDecryptCase {
379            input: "partial_encrypted_example_03.json",
380            reference: "partial_encrypted_example_decrypted_01.json",
381            keys: &[],
382        });
383    }
384}