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
This commit is contained in:
Kristofers Solo 2025-12-31 04:24:51 +02:00
parent ae33c596ef
commit 48ab599d66
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
2 changed files with 112 additions and 17 deletions

View File

@ -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<String>,
/// 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<String>,
/// Input file to encrypt/decrypt
#[arg(short, long, value_name = "FILE")]
pub input_file: Option<PathBuf>,
/// Output file (defaults to stdout)
#[arg(short, long, value_name = "FILE")]
pub output_file: Option<PathBuf>,
/// Output format for decrypted data
#[arg(short = 'f', long)]
pub output_format: Option<OutputFormat>,
}
impl From<Args> 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(),
}
}
}

View File

@ -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(())
}