diff --git a/src/main.rs b/src/main.rs index 17d1a16..21fc69c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod model; +mod templating; use std::io::{Read, Seek, Write}; use std::path::{Path, PathBuf}; @@ -120,7 +121,12 @@ fn process_instructions( instructions: Instructions, values: Value, ) -> Result<(), ProcessInstructionsError> { - let tera = Tera::new(&format!("{}/**", templates))?; + let mut tera = Tera::new(&format!("{}/**", templates))?; + tera.register_filter( + templating::decrypt::NAME, + templating::decrypt::DecryptFilter, + ); + let ctx = Context::from_serialize(&values)?; for i in instructions.render { let out = match tera.render(&i.template, &ctx) { diff --git a/src/templating/decrypt.rs b/src/templating/decrypt.rs new file mode 100644 index 0000000..ead97a9 --- /dev/null +++ b/src/templating/decrypt.rs @@ -0,0 +1,87 @@ +use std::collections::HashMap; +use std::io::{Read, Write}; +use std::path::Path; +use std::process::{Command, Stdio}; + +use tera::Value; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum DecryptError { + #[error("Decryption failed: {0}")] + Io(#[from] std::io::Error), + #[error("Decryption failed: {0}")] + DecryptFailed(String), +} + +pub(crate) static NAME: &str = "decrypt"; + +pub(crate) struct DecryptFilter; + +impl tera::Filter for DecryptFilter { + fn filter( + &self, + value: &Value, + args: &HashMap, + ) -> tera::Result { + let data = match value { + Value::String(s) => s.clone().into_bytes(), + _ => { + return Err(tera::Error::msg( + "Can only decrypt string values", + )); + } + }; + let identity = match args.get("identity") { + Some(Value::String(s)) => s, + Some(_) => { + return Err(tera::Error::msg("identity must be string")); + } + None => "keys.txt", + }; + let decrypted = Self::decrypt(&data, identity) + .map_err(|e| tera::Error::chain("Decryption failed", e))?; + Ok(Value::String( + String::from_utf8(decrypted) + .map_err(|_| tera::Error::msg("Invalid UTF-8 string"))?, + )) + } + + fn is_safe(&self) -> bool { + false + } +} + +impl DecryptFilter { + fn decrypt( + data: &[u8], + identity: impl AsRef, + ) -> Result, DecryptError> { + let mut cmd = Command::new("age") + .arg("-d") + .arg("-i") + .arg(identity.as_ref()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + if let Some(mut stdin) = cmd.stdin.take() { + stdin.write_all(data)?; + drop(stdin); + } + let mut decrypted = vec![]; + if let Some(stdout) = cmd.stdout.as_mut() { + stdout.read_to_end(&mut decrypted)?; + } + let status = cmd.wait()?; + if status.success() { + Ok(decrypted) + } else { + let mut error = vec![]; + if let Some(stderr) = cmd.stderr.as_mut() { + let _ = stderr.read_to_end(&mut error); + } + let msg = String::from_utf8_lossy(&error[..]).trim_end().into(); + Err(DecryptError::DecryptFailed(msg)) + } + } +} diff --git a/src/templating/mod.rs b/src/templating/mod.rs new file mode 100644 index 0000000..fb74fb8 --- /dev/null +++ b/src/templating/mod.rs @@ -0,0 +1 @@ +pub mod decrypt;