From dba9037230f69a202e5cef8c42135801b06af94f Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Thu, 11 Jan 2024 17:16:58 -0600 Subject: [PATCH] templating: Add decrypt filter The `decrypt` filter can be used in Tera templates to decrypt a string using `age`. This will allow the template context to contain encrypted vaules that are only decrypted if they are actually used to render a template. I considered using the [age] crate to implement this filter, rather than using the `age` command, but I decided against it for two reasons: 1. The way I intend for the `keys.txt` file to be populated is by fetching the keys from *keyserv.pyrocufflink.blue*, which returns an encrypted file that has to be decrypted with the `age` command before it can be used. Since that means `age` will always be available, it makes less sense to increase the size of the `tmpl` binary with what is effectively duplicate code. 2. The Rust API for *age* is rather un-ergonomic, particularly with respect to loading identites from a file. It's much easier to [age]: https://crates.io/crates/age --- src/main.rs | 8 +++- src/templating/decrypt.rs | 87 +++++++++++++++++++++++++++++++++++++++ src/templating/mod.rs | 1 + 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/templating/decrypt.rs create mode 100644 src/templating/mod.rs 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;