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
This commit is contained in:
Kristofers Solo 2025-12-31 01:08:40 +02:00
parent 6c5cd9b78a
commit 1ffc0327b3
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
2 changed files with 74 additions and 9 deletions

View File

@ -1,11 +1,12 @@
use crate::{Algorithm, OperationMode, OutputFormat}; use crate::{Algorithm, OperationMode, OutputFormat};
use cipher_core::{BlockCipher, CipherResult}; use cipher_core::{BlockCipher, CipherError, CipherResult, Output};
#[derive(Clone)] #[derive(Clone)]
pub struct CipherContext { pub struct CipherContext {
pub algorithm: Algorithm, pub algorithm: Algorithm,
pub operation: OperationMode, pub operation: OperationMode,
pub key: String, pub key: String,
pub iv: Option<String>,
pub input_text: String, pub input_text: String,
pub output_format: OutputFormat, pub output_format: OutputFormat,
} }
@ -17,6 +18,7 @@ impl CipherContext {
algorithm: Algorithm, algorithm: Algorithm,
operation: OperationMode, operation: OperationMode,
key: String, key: String,
iv: Option<String>,
input_text: String, input_text: String,
output_format: OutputFormat, output_format: OutputFormat,
) -> Self { ) -> Self {
@ -24,22 +26,57 @@ impl CipherContext {
algorithm, algorithm,
operation, operation,
key, key,
iv,
input_text, input_text,
output_format, output_format,
} }
} }
/// Processes the input text using the configured cipher algorithm and operation.
///
/// # Errors /// # Errors
/// ///
/// Returns `Err` if parsing the input text or creating the cipher fails, /// Returns `Err` if:
/// or if the encryption/decryption process encounters an error. /// - 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<String> { pub fn process(&self) -> CipherResult<String> {
let text_bytes = self.algorithm.parse_text(&self.input_text)?; if self.algorithm.requires_iv() {
let cipher = self.algorithm.new_cipher(&self.key)?; self.process_cbc()
self.execute(cipher.as_ref(), &text_bytes) } else {
self.process_ecb()
}
} }
fn execute(&self, cipher: &dyn BlockCipher, text_bytes: &[u8]) -> CipherResult<String> { fn process_ecb(&self) -> CipherResult<String> {
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<String> {
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<String> {
match self.operation { match self.operation {
OperationMode::Encrypt => { OperationMode::Encrypt => {
let ciphertext = cipher.encrypt(text_bytes)?; let ciphertext = cipher.encrypt(text_bytes)?;
@ -53,3 +90,26 @@ impl CipherContext {
} }
} }
} }
/// Parses a hex string into bytes.
fn parse_hex(s: &str) -> CipherResult<Vec<u8>> {
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()
}

View File

@ -12,11 +12,15 @@ pub struct Args {
#[arg(short, long)] #[arg(short, long)]
pub algorithm: Algorithm, 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)] #[arg(short, long, required = true)]
pub key: String, 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<String>,
/// The text to encrypt/decrypt
#[arg(value_name = "TEXT", required = true)] #[arg(value_name = "TEXT", required = true)]
pub text: String, pub text: String,
@ -31,6 +35,7 @@ impl From<Args> for CipherContext {
algorithm: args.algorithm, algorithm: args.algorithm,
operation: args.operation, operation: args.operation,
key: args.key, key: args.key,
iv: args.iv,
input_text: args.text, input_text: args.text,
output_format: args.output_format.unwrap_or_default(), output_format: args.output_format.unwrap_or_default(),
} }