use inquire::InquireError;
use crate::{
keys::{check_key_conflicts, is_encrypted},
typedetect::{FileFormat, detect_format},
types::{ProcessHandling, TypedValue, ValueType},
vaultfunc,
};
fn get_at_path_json<'a>(
val: &'a serde_json::Value,
path: &str,
) -> Option<&'a serde_json::Value> {
let mut current = val;
for part in path.split('.') {
match current {
serde_json::Value::Object(map) => current = map.get(part)?,
_ => return None,
}
}
Some(current)
}
fn set_at_path_json(target: &mut serde_json::Value, path: &str, value: serde_json::Value) {
let parts: Vec<&str> = path.split('.').collect();
let mut current = target;
for (i, &part) in parts.iter().enumerate() {
if i == parts.len() - 1 {
if let serde_json::Value::Object(map) = current {
map.insert(part.to_string(), value);
}
return;
}
if let serde_json::Value::Object(map) = current {
map.entry(part.to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
current = map.get_mut(part).unwrap();
}
}
}
fn extract_keys_json(root: &serde_json::Value, keys: &[String]) -> serde_json::Value {
let mut result = serde_json::Value::Object(serde_json::Map::new());
for key_path in keys {
if let Some(val) = get_at_path_json(root, key_path) {
set_at_path_json(&mut result, key_path, val.clone());
}
}
result
}
fn get_at_path_yaml<'a>(
val: &'a serde_yaml::Value,
path: &str,
) -> Option<&'a serde_yaml::Value> {
let mut current = val;
for part in path.split('.') {
match current {
serde_yaml::Value::Mapping(map) => {
let str_key = serde_yaml::Value::String(part.to_string());
if let Some(v) = map.get(&str_key) {
current = v;
} else if let Ok(n) = part.parse::<i64>() {
let num_key = serde_yaml::Value::Number(n.into());
current = map.get(&num_key)?;
} else {
return None;
}
}
_ => return None,
}
}
Some(current)
}
fn set_at_path_yaml(target: &mut serde_yaml::Value, path: &str, value: serde_yaml::Value) {
let parts: Vec<&str> = path.split('.').collect();
let mut current = target;
for (i, &part) in parts.iter().enumerate() {
let key = serde_yaml::Value::String(part.to_string());
if i == parts.len() - 1 {
if let serde_yaml::Value::Mapping(map) = current {
map.insert(key, value);
}
return;
}
if let serde_yaml::Value::Mapping(map) = current {
if !map.contains_key(&key) {
map.insert(key.clone(), serde_yaml::Value::Mapping(serde_yaml::Mapping::new()));
}
current = map.get_mut(&key).unwrap();
}
}
}
fn extract_keys_yaml(root: &serde_yaml::Value, keys: &[String]) -> serde_yaml::Value {
let mut result = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
for key_path in keys {
if let Some(val) = get_at_path_yaml(root, key_path) {
set_at_path_yaml(&mut result, key_path, val.clone());
}
}
result
}
pub fn decrypt(
input: &str,
output: &str,
password: &str,
keys: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
check_key_conflicts(keys)?;
let mut processor = make_decrypt_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 fn decrypt_value_only(
input: &str,
output: &str,
password: &str,
keys: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
check_key_conflicts(keys)?;
let mut processor = make_decrypt_processor(password);
match detect_format(input)? {
FileFormat::Json => {
let tree = crate::json::process_in_memory(input, keys, &mut processor)?;
let minimal = extract_keys_json(&tree, keys);
let output_str = serde_json::to_string_pretty(&minimal)?;
if output == "stdout" {
println!("{}", output_str);
} else {
std::fs::write(output, &output_str)
.map_err(|e| format!("error writing '{}': {}", output, e))?;
}
}
FileFormat::Yaml => {
let tree = crate::yaml::process_in_memory(input, keys, &mut processor)?;
let minimal = extract_keys_yaml(&tree, keys);
let yaml_str = serde_yaml::to_string(&minimal)?;
if output == "stdout" {
print!("{}", yaml_str);
} else {
std::fs::write(output, &yaml_str)
.map_err(|e| format!("error writing '{}': {}", output, e))?;
}
}
}
Ok(())
}
pub fn decrypt_fuzzy(
input: &str,
output: &str,
password: &str,
value_only: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let keys = match detect_format(input)? {
FileFormat::Json => crate::json::get_encrypted_keys(input)?,
FileFormat::Yaml => crate::yaml::get_encrypted_keys(input)?,
};
if keys.is_empty() {
println!("No encrypted values found in '{}'.", input);
return Ok(());
}
let selected = match inquire::MultiSelect::new(
"Select keys to decrypt (Space to select, Enter to confirm):",
keys,
)
.prompt()
{
Ok(s) => s,
Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => {
println!("Cancelled.");
return Ok(());
}
Err(e) => return Err(e.into()),
};
if selected.is_empty() {
println!("No keys selected.");
return Ok(());
}
if value_only {
decrypt_value_only(input, output, password, &selected)
} else {
decrypt(input, output, password, &selected)
}
}
pub(crate) fn make_decrypt_processor(
password: &str,
) -> impl FnMut(
TypedValue,
ValueType,
&str,
) -> Result<(TypedValue, ValueType, ProcessHandling), Box<dyn std::error::Error>>
+ '_ {
move |typed, vt, key_path| {
let (encrypted, vault_str) = is_encrypted(&typed);
if !encrypted {
return Ok((typed, vt, ProcessHandling::Skip));
}
let vault_str = vault_str.unwrap();
match vaultfunc::decrypt(&vault_str, password) {
Ok((decrypted, new_vt)) => Ok((decrypted, new_vt, ProcessHandling::Process)),
Err(e) => {
eprintln!("error decrypting key '{}': {}", key_path, e);
Ok((typed, vt, ProcessHandling::Skip))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn resources(filename: &str) -> String {
format!(
"{}/resources/tests/{}",
env!("CARGO_MANIFEST_DIR"),
filename
)
}
struct YamlDecryptCase {
input: &'static str,
reference: &'static str,
keys: &'static [&'static str],
}
fn run_yaml_decrypt(case: &YamlDecryptCase) {
let input = resources(case.input);
let reference = resources(case.reference);
let keys: Vec<String> = case.keys.iter().map(|s| s.to_string()).collect();
let output = tempfile::NamedTempFile::new().unwrap();
let output_path = output.path().to_str().unwrap().to_string();
decrypt(&input, &output_path, "test999", &keys).unwrap();
let got: serde_yaml::Value =
serde_yaml::from_str(&std::fs::read_to_string(&output_path).unwrap()).unwrap();
let expected: serde_yaml::Value =
serde_yaml::from_str(&std::fs::read_to_string(&reference).unwrap()).unwrap();
assert_eq!(got, expected, "YAML decrypt '{}' failed", case.input);
}
#[test]
fn test_decrypt_yaml_all_keys() {
run_yaml_decrypt(&YamlDecryptCase {
input: "partial_encrypted_example.yaml",
reference: "partial_encrypted_example_decrypted_01.yaml",
keys: &[],
});
}
#[test]
fn test_decrypt_yaml_filtered_key() {
run_yaml_decrypt(&YamlDecryptCase {
input: "partial_encrypted_example.yaml",
reference: "partial_encrypted_example_decrypted_03.yaml",
keys: &["third.carrot"],
});
}
#[test]
fn test_decrypt_yaml_multiple_keys() {
run_yaml_decrypt(&YamlDecryptCase {
input: "partial_encrypted_example.yaml",
reference: "partial_encrypted_example_decrypted_04.yaml",
keys: &["first.a", "first.z", "second.b.2", "fourth.list"],
});
}
#[test]
fn test_decrypt_yaml_02() {
run_yaml_decrypt(&YamlDecryptCase {
input: "partial_encrypted_example_02.yaml",
reference: "partial_encrypted_example_decrypted_01.yaml",
keys: &[],
});
}
#[test]
fn test_decrypt_yaml_03() {
run_yaml_decrypt(&YamlDecryptCase {
input: "partial_encrypted_example_03.yaml",
reference: "partial_encrypted_example_decrypted_01.yaml",
keys: &[],
});
}
struct JsonDecryptCase {
input: &'static str,
reference: &'static str,
keys: &'static [&'static str],
}
fn run_json_decrypt(case: &JsonDecryptCase) {
let input = resources(case.input);
let reference = resources(case.reference);
let keys: Vec<String> = case.keys.iter().map(|s| s.to_string()).collect();
let output = tempfile::NamedTempFile::new().unwrap();
let output_path = output.path().to_str().unwrap().to_string();
decrypt(&input, &output_path, "test999", &keys).unwrap();
let got: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&output_path).unwrap()).unwrap();
let expected: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&reference).unwrap()).unwrap();
assert_eq!(got, expected, "JSON decrypt '{}' failed", case.input);
}
#[test]
fn test_decrypt_json_all_keys() {
run_json_decrypt(&JsonDecryptCase {
input: "partial_encrypted_example.json",
reference: "partial_encrypted_example_decrypted_01.json",
keys: &[],
});
}
#[test]
fn test_decrypt_json_filtered_key() {
run_json_decrypt(&JsonDecryptCase {
input: "partial_encrypted_example.json",
reference: "partial_encrypted_example_decrypted_03.json",
keys: &["third.carrot"],
});
}
#[test]
fn test_decrypt_json_multiple_keys() {
run_json_decrypt(&JsonDecryptCase {
input: "partial_encrypted_example.json",
reference: "partial_encrypted_example_decrypted_04.json",
keys: &["first.a", "first.z", "second.b.2", "fourth.list"],
});
}
#[test]
fn test_decrypt_json_02() {
run_json_decrypt(&JsonDecryptCase {
input: "partial_encrypted_example_02.json",
reference: "partial_encrypted_example_decrypted_01.json",
keys: &[],
});
}
#[test]
fn test_decrypt_json_03() {
run_json_decrypt(&JsonDecryptCase {
input: "partial_encrypted_example_03.json",
reference: "partial_encrypted_example_decrypted_01.json",
keys: &[],
});
}
}