From ad2c29248be6d95e37c714f71c06780875252b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Thu, 1 Feb 2024 11:57:15 +0100 Subject: [PATCH] implement decrypt --- Cargo.lock | 10 ++++++ Cargo.toml | 1 + src/cli.rs | 16 ++++++--- src/decrypt.rs | 89 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/error.rs | 8 +++++ 5 files changed, 118 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a0a500..b85c4e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1071,6 +1071,15 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "substring" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +dependencies = [ + "autocfg", +] + [[package]] name = "subtle" version = "2.5.0" @@ -1445,6 +1454,7 @@ dependencies = [ "log", "ocli", "serde_yaml", + "substring", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 31ccf78..ca0e629 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,4 +22,5 @@ clap-verbosity-flag = "2.1.2" log = "0.4.20" ocli = "0.1.1" serde_yaml = "0.9.31" +substring = "1.4.5" thiserror = "1.0.56" diff --git a/src/cli.rs b/src/cli.rs index adbb261..b596b3a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -76,16 +76,24 @@ pub struct EncryptArgs { #[derive(Args, Debug)] pub struct DecryptArgs { /// Decrypt with the specified key - #[clap(short, long, env = "YAGE_KEY")] - pub key: Option, + #[clap(short, long = "key", env = "YAGE_KEY")] + pub keys: Vec, /// Decrypt with the key at PATH - #[clap(short = 'K', long, name = "PATH", env = "YAGE_KEY_FILE")] - pub key_file: Option, + #[clap(short = 'K', long = "key-file", name = "PATH", env = "YAGE_KEY_FILE")] + pub key_files: Vec, /// Decrypt in place #[clap(short, long)] pub inplace: bool, + + /// The output path to the decrypted YAML file + #[clap(short, long, default_value = "-")] + pub output: PathBuf, + + /// The YAML file to decrypt + #[arg()] + pub file: PathBuf, } /// Execute a command with decrypted values inserted into the environment diff --git a/src/decrypt.rs b/src/decrypt.rs index 1822463..c48ee95 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -1,7 +1,92 @@ +use std::io::Read; +use std::str::FromStr; + +use age::x25519; +use base64::prelude::*; +use substring::Substring; + use crate::cli::DecryptArgs; -use crate::error::Result; +use crate::error::{AppError, IOResultExt, Result}; +use crate::util::{stdin_or_file, stdout_or_file}; pub fn decrypt(args: &DecryptArgs) -> Result<()> { - info!("Decrypting a file {args:?}"); + let mut identities: Vec = Vec::new(); + for key in args.keys.iter() { + debug!("loading key: {key}"); + let key = x25519::Identity::from_str(key) + .map_err(|e| AppError::KeyParseError { message: e.into() })?; + identities.push(key); + } + for key_file in args.key_files.iter() { + debug!("loading key file: {key_file:?}"); + let input = stdin_or_file(key_file)?; + let keys = age::IdentityFile::from_buffer(input).path_ctx(key_file)?; + for key in keys.into_identities() { + let age::IdentityFileEntry::Native(key) = key; + identities.push(key); + } + } + debug!("loading yaml file: {:?}", args.file); + let input_data: serde_yaml::Value = serde_yaml::from_reader(stdin_or_file(&args.file)?)?; + let output_data = decrypt_yaml(&input_data, &identities)?; + let output = stdout_or_file(if args.inplace { + &args.file + } else { + &args.output + })?; + serde_yaml::to_writer(output, &output_data)?; Ok(()) } + +fn decrypt_yaml( + value: &serde_yaml::Value, + identities: &[x25519::Identity], +) -> Result { + match value { + serde_yaml::Value::Mapping(mapping) => { + let mut output = serde_yaml::Mapping::new(); + for (key, value) in mapping { + let key = key.clone(); + let value = decrypt_yaml(value, identities)?; + output.insert(key, value); + } + Ok(serde_yaml::Value::Mapping(output)) + } + serde_yaml::Value::Sequence(sequence) => { + let mut output = Vec::new(); + for value in sequence { + let value = decrypt_yaml(value, identities)?; + output.push(value); + } + Ok(serde_yaml::Value::Sequence(output)) + } + serde_yaml::Value::String(encrypted) => { + let decrypted = decrypt_value(encrypted, identities)?; + Ok(decrypted) + } + _ => Ok(value.clone()), + } +} + +fn decrypt_value(s: &str, identities: &[x25519::Identity]) -> Result { + if is_yage_encoded(s) { + // remove the yage[…] prefix and suffix + let encoded = s.substring(5, s.len() - 1); + let encrypted = BASE64_STANDARD.decode(encoded)?; + let decryptor = match age::Decryptor::new(&encrypted[..])? { + age::Decryptor::Recipients(d) => Ok(d), + _ => Err(AppError::PassphraseUnsupportedError), + }?; + let mut decrypted = vec![]; + let mut reader = decryptor.decrypt(identities.iter().map(|i| i as &dyn age::Identity))?; + reader.read_to_end(&mut decrypted)?; + let value: serde_yaml::Value = serde_yaml::from_slice(&decrypted)?; + Ok(value) + } else { + Ok(serde_yaml::Value::String(s.to_owned())) + } +} + +fn is_yage_encoded(s: &str) -> bool { + s.starts_with("yage[") && s.ends_with("]") +} diff --git a/src/error.rs b/src/error.rs index c983f86..42bb399 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,12 +16,20 @@ pub enum AppError { YamlError(#[from] serde_yaml::Error), #[error("can't parse recipient {recipient}: {message}")] RecipientParseError { recipient: String, message: String }, + #[error("can't parse key: {message}")] + KeyParseError { message: String }, + #[error(transparent)] + DecryptError(#[from] age::DecryptError), #[error(transparent)] EncryptError(#[from] age::EncryptError), #[error(transparent)] Utf8Error(#[from] std::string::FromUtf8Error), + #[error(transparent)] + Base64DecodeError(#[from] base64::DecodeError), #[error("no recipients provided")] NoRecipientsError, + #[error("passphrase not supported")] + PassphraseUnsupportedError, } /// Alias for a `Result` with the error type `AppError`.