happy-cracking 0.5.0

A fast, comprehensive CTF toolkit for cryptographic encoding/decoding, classic ciphers, hash operations, and analysis tools
Documentation
use anyhow::{Context, Result};
use clap::Subcommand;

#[derive(Subcommand)]
pub enum QpAction {
    #[command(about = "Encode to Quoted-Printable (RFC 2045)")]
    Encode {
        #[arg(help = "Input text")]
        input: String,
    },
    #[command(about = "Decode from Quoted-Printable (RFC 2045)")]
    Decode {
        #[arg(help = "Quoted-Printable encoded string")]
        input: String,
    },
}

pub fn run(action: QpAction) -> Result<()> {
    match action {
        QpAction::Encode { input } => {
            println!("{}", encode(input.as_bytes()));
        }
        QpAction::Decode { input } => {
            let bytes = decode(&input)?;
            match String::from_utf8(bytes.clone()) {
                Ok(s) => println!("{s}"),
                Err(_) => println!("{}", String::from_utf8_lossy(&bytes)),
            }
        }
    }
    Ok(())
}

const HEX: &[u8; 16] = b"0123456789ABCDEF";

fn push_encoded(out: &mut String, line_len: &mut usize, token: &str) {
    if *line_len + token.len() > 75 {
        out.push_str("=\n");
        *line_len = 0;
    }
    out.push_str(token);
    *line_len += token.len();
}

pub fn encode(data: &[u8]) -> String {
    let mut out = String::new();
    let mut line_len = 0usize;

    for (i, &b) in data.iter().enumerate() {
        match b {
            b'\n' => {
                out.push('\n');
                line_len = 0;
            }
            b' ' | b'\t' => {
                let at_line_end = match data.get(i + 1) {
                    None => true,
                    Some(&next) => next == b'\n',
                };
                if at_line_end {
                    let token = format!(
                        "={}{}",
                        HEX[(b >> 4) as usize] as char,
                        HEX[(b & 0xF) as usize] as char
                    );
                    push_encoded(&mut out, &mut line_len, &token);
                } else {
                    push_encoded(&mut out, &mut line_len, &(b as char).to_string());
                }
            }
            0x21..=0x7E if b != b'=' => {
                push_encoded(&mut out, &mut line_len, &(b as char).to_string());
            }
            _ => {
                let token = format!(
                    "={}{}",
                    HEX[(b >> 4) as usize] as char,
                    HEX[(b & 0xF) as usize] as char
                );
                push_encoded(&mut out, &mut line_len, &token);
            }
        }
    }

    out
}

fn hex_digit(c: u8) -> Result<u8> {
    match c {
        b'0'..=b'9' => Ok(c - b'0'),
        b'a'..=b'f' => Ok(c - b'a' + 10),
        b'A'..=b'F' => Ok(c - b'A' + 10),
        _ => anyhow::bail!("Invalid hex digit in '=' escape: {:?}", c as char),
    }
}

pub fn decode(s: &str) -> Result<Vec<u8>> {
    let bytes = s.as_bytes();
    let mut result = Vec::with_capacity(bytes.len());
    let mut i = 0;

    while i < bytes.len() {
        let b = bytes[i];
        if b == b'=' {
            if i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
                i += 2;
                continue;
            }
            if i + 2 < bytes.len() && bytes[i + 1] == b'\r' && bytes[i + 2] == b'\n' {
                i += 3;
                continue;
            }

            if i + 2 >= bytes.len() {
                anyhow::bail!("Truncated '=' escape sequence");
            }
            let hi = hex_digit(bytes[i + 1]).context("Failed to decode Quoted-Printable")?;
            let lo = hex_digit(bytes[i + 2]).context("Failed to decode Quoted-Printable")?;
            result.push((hi << 4) | lo);
            i += 3;
        } else {
            result.push(b);
            i += 1;
        }
    }

    Ok(result)
}