From 48ab599d66c28cf8214a088b2c4e88c188264a8e Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Wed, 31 Dec 2025 04:24:51 +0200 Subject: [PATCH] feat(cli): add file encryption/decryption support Add --input-file/-i and --output-file/-o options: - Encrypt files with AES-CBC: reads binary, writes binary/hex - Decrypt files with AES-CBC: reads binary, writes binary - Falls back to text-based processing for non-file operations --- cli/src/args.rs | 35 +++++++++++------- cli/src/main.rs | 94 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 112 insertions(+), 17 deletions(-) diff --git a/cli/src/args.rs b/cli/src/args.rs index 38c3616..8870d00 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -1,5 +1,6 @@ use cipher_factory::{Algorithm, CipherContext, OperationMode, OutputFormat}; use clap::Parser; +use std::path::PathBuf; #[derive(Debug, Clone, Parser)] #[command(version, about, long_about = None)] @@ -20,24 +21,34 @@ pub struct Args { #[arg(long)] pub iv: Option, - /// The text to encrypt/decrypt - #[arg(value_name = "TEXT", required = true)] - pub text: String, + /// The text to encrypt/decrypt (use --input-file for file input) + #[arg(value_name = "TEXT", required_unless_present = "input_file")] + pub text: Option, + + /// Input file to encrypt/decrypt + #[arg(short, long, value_name = "FILE")] + pub input_file: Option, + + /// Output file (defaults to stdout) + #[arg(short, long, value_name = "FILE")] + pub output_file: Option, /// Output format for decrypted data #[arg(short = 'f', long)] pub output_format: Option, } -impl From for CipherContext { - fn from(args: Args) -> Self { - Self { - algorithm: args.algorithm, - operation: args.operation, - key: args.key, - iv: args.iv, - input_text: args.text, - output_format: args.output_format.unwrap_or_default(), +impl Args { + /// Creates a [`CipherContext`] for text-based operations. + #[must_use] + pub fn into_context(self, input_text: String) -> CipherContext { + CipherContext { + algorithm: self.algorithm, + operation: self.operation, + key: self.key, + iv: self.iv, + input_text, + output_format: self.output_format.unwrap_or_default(), } } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 9acc950..b51b69c 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,17 +1,101 @@ mod args; use crate::args::Args; -use cipher_factory::CipherContext; +use aes::{AesCbc, Block128, Iv}; +use cipher_factory::{Algorithm, OperationMode}; use clap::Parser; -use color_eyre::eyre::Result; +use color_eyre::eyre::{Result, eyre}; +use std::fs::{self, File}; +use std::io::{Write, stdout}; +use std::str::FromStr; fn main() -> Result<()> { color_eyre::install()?; let args = Args::parse(); - let context = CipherContext::from(args); - let output = context.process()?; - println!("{output}"); + // Check if we're doing file-based CBC operation + if args.input_file.is_some() && args.algorithm == Algorithm::AesCbc { + process_cbc_file(&args)?; + } else { + process_text(&args)?; + } Ok(()) } + +fn process_text(args: &Args) -> Result<()> { + let input_text = match (&args.text, &args.input_file) { + (Some(text), None) => text.clone(), + (None, Some(path)) => fs::read_to_string(path)?, + (Some(_), Some(_)) => return Err(eyre!("Cannot specify both TEXT and --input-file")), + (None, None) => return Err(eyre!("Must specify TEXT or --input-file")), + }; + + let context = args.clone().into_context(input_text); + let output = context.process()?; + + write_output(args, output.as_bytes())?; + Ok(()) +} + +fn process_cbc_file(args: &Args) -> Result<()> { + let input_path = args + .input_file + .as_ref() + .ok_or_else(|| eyre!("No input file"))?; + let iv_str = args + .iv + .as_ref() + .ok_or_else(|| eyre!("CBC mode requires --iv"))?; + + let key = Block128::from_str(&args.key).map_err(|e| eyre!("Invalid key: {e}"))?; + let iv = Iv::from_str(iv_str).map_err(|e| eyre!("Invalid IV: {e}"))?; + + let cipher = AesCbc::new(key, iv); + + match args.operation { + OperationMode::Encrypt => { + let plaintext = fs::read(input_path)?; + let ciphertext = cipher + .encrypt(&plaintext) + .map_err(|e| eyre!("Encryption failed: {e}"))?; + + if args.output_file.is_some() { + // Write raw binary to file + write_output(args, &ciphertext)?; + } else { + // Write hex to stdout + let hex = ciphertext.iter().fold(String::new(), |mut acc, b| { + use std::fmt::Write; + let _ = write!(acc, "{b:02X}"); + acc + }); + println!("{hex}"); + } + } + OperationMode::Decrypt => { + let ciphertext = fs::read(input_path)?; + let plaintext = cipher + .decrypt(&ciphertext) + .map_err(|e| eyre!("Decryption failed: {e}"))?; + + write_output(args, &plaintext)?; + } + } + + Ok(()) +} + +fn write_output(args: &Args, data: &[u8]) -> Result<()> { + if let Some(path) = &args.output_file { + let mut file = File::create(path)?; + file.write_all(data)?; + } else { + stdout().write_all(data)?; + // Add newline if output doesn't end with one + if !data.ends_with(b"\n") { + println!(); + } + } + Ok(()) +}