From 1ffc0327b37dc4531d27e4482a599caf152c8aa6 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Wed, 31 Dec 2025 01:08:40 +0200 Subject: [PATCH] feat(cipher-factory,cli): add CBC mode support to CipherContext and CLI Update CipherContext: - Add optional iv field for CBC mode - Add process_cbc() for CBC-specific handling - Add parse_hex() helper for decryption input - Separate ECB and CBC processing paths Update CLI: - Add --iv argument for initialization vector - Pass IV through to CipherContext --- cipher-factory/src/context.rs | 74 +++++++++++++++++++++++++++++++---- cli/src/args.rs | 9 ++++- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/cipher-factory/src/context.rs b/cipher-factory/src/context.rs index 93822f6..b71ebb4 100644 --- a/cipher-factory/src/context.rs +++ b/cipher-factory/src/context.rs @@ -1,11 +1,12 @@ use crate::{Algorithm, OperationMode, OutputFormat}; -use cipher_core::{BlockCipher, CipherResult}; +use cipher_core::{BlockCipher, CipherError, CipherResult, Output}; #[derive(Clone)] pub struct CipherContext { pub algorithm: Algorithm, pub operation: OperationMode, pub key: String, + pub iv: Option, pub input_text: String, pub output_format: OutputFormat, } @@ -17,6 +18,7 @@ impl CipherContext { algorithm: Algorithm, operation: OperationMode, key: String, + iv: Option, input_text: String, output_format: OutputFormat, ) -> Self { @@ -24,22 +26,57 @@ impl CipherContext { algorithm, operation, key, + iv, input_text, output_format, } } + /// Processes the input text using the configured cipher algorithm and operation. + /// /// # Errors /// - /// Returns `Err` if parsing the input text or creating the cipher fails, - /// or if the encryption/decryption process encounters an error. + /// Returns `Err` if: + /// - Parsing the input text or creating the cipher fails + /// - The encryption/decryption process encounters an error + /// - CBC mode is used without providing an IV pub fn process(&self) -> CipherResult { - let text_bytes = self.algorithm.parse_text(&self.input_text)?; - let cipher = self.algorithm.new_cipher(&self.key)?; - self.execute(cipher.as_ref(), &text_bytes) + if self.algorithm.requires_iv() { + self.process_cbc() + } else { + self.process_ecb() + } } - fn execute(&self, cipher: &dyn BlockCipher, text_bytes: &[u8]) -> CipherResult { + fn process_ecb(&self) -> CipherResult { + let text_bytes = self.algorithm.parse_text(&self.input_text)?; + let cipher = self.algorithm.new_cipher(&self.key)?; + self.execute_ecb(cipher.as_ref(), &text_bytes) + } + + fn process_cbc(&self) -> CipherResult { + let iv = self.iv.as_ref().ok_or_else(|| { + CipherError::InvalidPadding("CBC mode requires an IV".into()) + })?; + + let cipher = self.algorithm.new_cbc_cipher(&self.key, iv)?; + + match self.operation { + OperationMode::Encrypt => { + let ciphertext = cipher.encrypt(self.input_text.as_bytes())?; + Ok(format!("{:X}", Output::from(ciphertext))) + } + OperationMode::Decrypt => { + // Parse hex input for decryption + let ciphertext = parse_hex(&self.input_text)?; + let plaintext = cipher.decrypt(&ciphertext)?; + let output = self.output_format.format(&Output::from(plaintext)); + Ok(output) + } + } + } + + fn execute_ecb(&self, cipher: &dyn BlockCipher, text_bytes: &[u8]) -> CipherResult { match self.operation { OperationMode::Encrypt => { let ciphertext = cipher.encrypt(text_bytes)?; @@ -53,3 +90,26 @@ impl CipherContext { } } } + +/// Parses a hex string into bytes. +fn parse_hex(s: &str) -> CipherResult> { + let trimmed = s.trim(); + let s = trimmed + .strip_prefix("0x") + .or_else(|| trimmed.strip_prefix("0X")) + .unwrap_or(trimmed); + + if !s.len().is_multiple_of(2) { + return Err(CipherError::InvalidPadding( + "hex string must have even length".into(), + )); + } + + (0..s.len()) + .step_by(2) + .map(|i| { + u8::from_str_radix(&s[i..i + 2], 16) + .map_err(|_| CipherError::InvalidPadding(format!("invalid hex at position {i}"))) + }) + .collect() +} diff --git a/cli/src/args.rs b/cli/src/args.rs index f5608fd..38c3616 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -12,11 +12,15 @@ pub struct Args { #[arg(short, long)] pub algorithm: Algorithm, - /// Key used for encryption/decryption. Can be a string or a path to a file + /// Key used for encryption/decryption (hex string, e.g., 0x2b7e...) #[arg(short, long, required = true)] pub key: String, - /// The text to encrypt/decrypt. Can be a string or a path to a file + /// Initialization vector for CBC mode (hex string, e.g., 0x0001...) + #[arg(long)] + pub iv: Option, + + /// The text to encrypt/decrypt #[arg(value_name = "TEXT", required = true)] pub text: String, @@ -31,6 +35,7 @@ impl From for CipherContext { algorithm: args.algorithm, operation: args.operation, key: args.key, + iv: args.iv, input_text: args.text, output_format: args.output_format.unwrap_or_default(), }