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
106pub 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
140pub 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
182pub 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 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 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}