happy-cracking 0.5.0

A fast, comprehensive CTF toolkit for cryptographic encoding/decoding, classic ciphers, hash operations, and analysis tools
Documentation
use std::collections::HashSet;

use anyhow::{Context, Result};
use clap::Subcommand;

use aes::Aes128;
use aes::cipher::{Array, BlockCipherDecrypt, BlockCipherEncrypt, KeyInit, KeySizeUser};

#[derive(Subcommand)]
pub enum AesCipherAction {
    #[command(about = "AES-128 ECB encrypt (hex key, hex plaintext)")]
    EcbEncrypt {
        #[arg(help = "Plaintext (hex string, must be multiple of 16 bytes)")]
        input: String,
        #[arg(short, long, help = "Key (hex string, 16 bytes / 32 hex chars)")]
        key: String,
    },
    #[command(about = "AES-128 ECB decrypt (hex key, hex ciphertext)")]
    EcbDecrypt {
        #[arg(help = "Ciphertext (hex string, must be multiple of 16 bytes)")]
        input: String,
        #[arg(short, long, help = "Key (hex string, 16 bytes / 32 hex chars)")]
        key: String,
    },
    #[command(about = "AES-128 CBC encrypt (hex key, hex IV, hex plaintext)")]
    CbcEncrypt {
        #[arg(help = "Plaintext (hex string, must be multiple of 16 bytes)")]
        input: String,
        #[arg(short, long, help = "Key (hex string, 16 bytes)")]
        key: String,
        #[arg(long, help = "Initialization vector (hex string, 16 bytes)")]
        iv: String,
    },
    #[command(about = "AES-128 CBC decrypt (hex key, hex IV, hex ciphertext)")]
    CbcDecrypt {
        #[arg(help = "Ciphertext (hex string, must be multiple of 16 bytes)")]
        input: String,
        #[arg(short, long, help = "Key (hex string, 16 bytes)")]
        key: String,
        #[arg(long, help = "Initialization vector (hex string, 16 bytes)")]
        iv: String,
    },
    #[command(about = "Detect ECB mode by finding duplicate 16-byte blocks")]
    EcbDetect {
        #[arg(help = "Ciphertext (hex string)")]
        input: String,
    },
}

pub fn run(action: AesCipherAction) -> Result<()> {
    match action {
        AesCipherAction::EcbEncrypt { input, key } => {
            println!("{}", ecb_encrypt(&input, &key)?);
        }
        AesCipherAction::EcbDecrypt { input, key } => {
            println!("{}", ecb_decrypt(&input, &key)?);
        }
        AesCipherAction::CbcEncrypt { input, key, iv } => {
            println!("{}", cbc_encrypt(&input, &key, &iv)?);
        }
        AesCipherAction::CbcDecrypt { input, key, iv } => {
            println!("{}", cbc_decrypt(&input, &key, &iv)?);
        }
        AesCipherAction::EcbDetect { input } => {
            let (detected, duplicates) = ecb_detect(&input)?;
            if detected {
                println!(
                    "ECB mode detected! Found {} duplicate block(s):",
                    duplicates
                );
                println!("The ciphertext contains repeated 16-byte blocks, suggesting ECB mode.");
            } else {
                println!("No duplicate blocks found. Likely not ECB mode.");
            }
        }
    }
    Ok(())
}

fn parse_key(hex_key: &str) -> Result<Array<u8, <Aes128 as KeySizeUser>::KeySize>> {
    let key_bytes = hex::decode(hex_key.trim()).context("Failed to decode hex key")?;
    Array::try_from(key_bytes.as_slice()).map_err(|_| {
        anyhow::anyhow!(
            "AES-128 key must be exactly 16 bytes (32 hex chars), got {} bytes",
            key_bytes.len()
        )
    })
}

fn parse_blocks(hex_input: &str) -> Result<Vec<u8>> {
    let bytes = hex::decode(hex_input.trim()).context("Failed to decode hex input")?;
    if bytes.is_empty() {
        anyhow::bail!("Input must not be empty");
    }
    if bytes.len() % 16 != 0 {
        anyhow::bail!(
            "Input length must be a multiple of 16 bytes, got {} bytes",
            bytes.len()
        );
    }
    Ok(bytes)
}

fn parse_iv(hex_iv: &str) -> Result<[u8; 16]> {
    let iv_bytes = hex::decode(hex_iv.trim()).context("Failed to decode hex IV")?;
    if iv_bytes.len() != 16 {
        anyhow::bail!(
            "IV must be exactly 16 bytes (32 hex chars), got {} bytes",
            iv_bytes.len()
        );
    }
    let mut iv = [0u8; 16];
    iv.copy_from_slice(&iv_bytes);
    Ok(iv)
}

pub fn ecb_encrypt(hex_input: &str, hex_key: &str) -> Result<String> {
    let key = parse_key(hex_key)?;
    let plaintext = parse_blocks(hex_input)?;
    let cipher = Aes128::new(&key);

    let mut output = Vec::with_capacity(plaintext.len());
    for block in plaintext.chunks(16) {
        let mut block_array = Array::try_from(block).context("AES block must be 16 bytes")?;
        cipher.encrypt_block(&mut block_array);
        output.extend_from_slice(&block_array);
    }
    Ok(hex::encode(output))
}

pub fn ecb_decrypt(hex_input: &str, hex_key: &str) -> Result<String> {
    let key = parse_key(hex_key)?;
    let ciphertext = parse_blocks(hex_input)?;
    let cipher = Aes128::new(&key);

    let mut output = Vec::with_capacity(ciphertext.len());
    for block in ciphertext.chunks(16) {
        let mut block_array = Array::try_from(block).context("AES block must be 16 bytes")?;
        cipher.decrypt_block(&mut block_array);
        output.extend_from_slice(&block_array);
    }
    Ok(hex::encode(output))
}

pub fn cbc_encrypt(hex_input: &str, hex_key: &str, hex_iv: &str) -> Result<String> {
    let key = parse_key(hex_key)?;
    let plaintext = parse_blocks(hex_input)?;
    let mut prev = parse_iv(hex_iv)?;
    let cipher = Aes128::new(&key);

    let mut output = Vec::with_capacity(plaintext.len());
    for block in plaintext.chunks(16) {
        let mut xored = [0u8; 16];
        for i in 0..16 {
            xored[i] = block[i] ^ prev[i];
        }
        let mut block_array = Array::from(xored);
        cipher.encrypt_block(&mut block_array);
        prev.copy_from_slice(&block_array);
        output.extend_from_slice(&block_array);
    }
    Ok(hex::encode(output))
}

pub fn cbc_decrypt(hex_input: &str, hex_key: &str, hex_iv: &str) -> Result<String> {
    let key = parse_key(hex_key)?;
    let ciphertext = parse_blocks(hex_input)?;
    let mut prev = parse_iv(hex_iv)?;
    let cipher = Aes128::new(&key);

    let mut output = Vec::with_capacity(ciphertext.len());
    for block in ciphertext.chunks(16) {
        let mut block_array = Array::try_from(block).context("AES block must be 16 bytes")?;
        cipher.decrypt_block(&mut block_array);
        for i in 0..16 {
            block_array[i] ^= prev[i];
        }
        prev.copy_from_slice(block);
        output.extend_from_slice(&block_array);
    }
    Ok(hex::encode(output))
}

pub fn ecb_detect(hex_input: &str) -> Result<(bool, usize)> {
    let bytes = hex::decode(hex_input.trim()).context("Failed to decode hex input")?;
    if bytes.len() < 16 {
        return Ok((false, 0));
    }

    // Perf: Use HashSet for O(n) duplicate detection instead of clone + sort + dedup (O(n log n)),
    // also avoiding the unnecessary vector clone and its extra allocation.
    let blocks: Vec<&[u8]> = bytes.chunks(16).collect();
    let unique: HashSet<&[u8]> = blocks.iter().copied().collect();

    let duplicates = blocks.len() - unique.len();
    Ok((duplicates > 0, duplicates))
}