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 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<String>,
pub input_text: String,
pub output_format: OutputFormat,
}
@ -17,6 +18,7 @@ impl CipherContext {
algorithm: Algorithm,
operation: OperationMode,
key: String,
iv: Option<String>,
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<String> {
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<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 {
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<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)]
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<String>,
/// The text to encrypt/decrypt
#[arg(value_name = "TEXT", required = true)]
pub text: String,
@ -31,6 +35,7 @@ impl From<Args> 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(),
}