mirror of
https://github.com/kristoferssolo/cipher-workshop.git
synced 2025-12-31 13:52:29 +00:00
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:
parent
ae33c596ef
commit
48ab599d66
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user