ice 0.4.0

A simple CTF tool store
Documentation
use anyhow::{anyhow, Context, Result};

pub fn hex_x_hex(a: &str, b: &str) -> Result<String> {
    let a_decoded = hex::decode(a).context("first argument is not a valid hex string")?;
    let b_decoded = hex::decode(b).context("second argument is not a valid hex string")?;

    let result: Vec<u8> = a_decoded
        .iter()
        .zip(b_decoded.iter().cycle())
        .map(|(x1, x2)| x1 ^ x2)
        .collect();

    Ok(hex::encode(result))
}

pub fn str_x_str(a: &str, b: &str) -> Result<String> {
    let result: Vec<u8> = a
        .bytes()
        .zip(b.bytes().cycle())
        .map(|(i, j)| i ^ j)
        .collect();

    Ok(hex::encode(result))
}

pub fn str_x_byte(s: &str) -> Result<String> {
    hex_x_byte_inner(&hex::encode(s))
}

pub fn hex_x_byte(s: &str) -> Result<String> {
    hex_x_byte_inner(s)
}

fn hex_x_byte_inner(s: &str) -> Result<String> {
    let decoded = hex::decode(s).context("Invalid hex string")?;

    let results: Vec<String> = (0..=255)
        .filter_map(|i| {
            let bytes: Vec<u8> = decoded.iter().map(|b| b ^ i).collect();
            if bytes.iter().all(|&b| b.is_ascii_graphic() || b == b' ') {
                String::from_utf8(bytes).ok()
            } else {
                None
            }
        })
        .collect();

    Ok(results.join("\n"))
}

pub fn key_x_byte(s: &str) -> Result<String> {
    let trimmed: String = s.split_whitespace().collect();
    let bytes: Vec<u8> = if !trimmed.is_empty()
        && trimmed.len().is_multiple_of(2)
        && trimmed.chars().all(|c| c.is_ascii_hexdigit())
    {
        hex::decode(&trimmed).context("Invalid hex string")?
    } else {
        s.as_bytes().to_vec()
    };

    let max_ks = (40).min(bytes.len() / 4);
    if max_ks < 2 {
        return Err(anyhow!("Input too short for keysize analysis"));
    }

    let mut scored: Vec<(f64, usize)> = (2..=max_ks)
        .map(|ks| {
            let mut dist = 0.0f64;
            let mut pairs = 0usize;
            for chunk in bytes.chunks(ks).collect::<Vec<_>>().windows(2) {
                if chunk[0].len() == ks && chunk[1].len() == ks {
                    dist += hamming(chunk[0], chunk[1]) as f64 / ks as f64;
                    pairs += 1;
                }
            }
            (if pairs > 0 { dist / pairs as f64 } else { f64::MAX }, ks)
        })
        .collect();
    scored.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));

    let mut results: Vec<String> = Vec::new();
    for (_, ks) in scored.iter().take(5) {
        let ks = *ks;
        let mut key = Vec::with_capacity(ks);
        for pos in 0..ks {
            let column: Vec<u8> = bytes.iter().skip(pos).step_by(ks).copied().collect();
            let mut best = (f64::NEG_INFINITY, 0u8);
            for k in 0..=255u8 {
                let dec: Vec<u8> = column.iter().map(|&b| b ^ k).collect();
                let sc = english_score(&dec);
                if sc > best.0 {
                    best = (sc, k);
                }
            }
            key.push(best.1);
        }
        let plain: Vec<u8> = bytes
            .iter()
            .zip(key.iter().cycle())
            .map(|(&b, &k)| b ^ k)
            .collect();
        let key_str: String = key.iter().map(|&b| b as char).collect();
        let text = String::from_utf8_lossy(&plain);
        results.push(format!(
            "[score={:.1} {}] key=\"{}\"\n{}",
            english_score(&plain),
            crate::utils::color::cyan(&format!("keysize={}", ks)),
            key_str,
            text
        ));
    }
    Ok(results.join("\n\n"))
}

fn hamming(a: &[u8], b: &[u8]) -> u32 {
    a.iter()
        .zip(b.iter())
        .map(|(x, y)| (x ^ y).count_ones())
        .sum()
}

fn english_score(bytes: &[u8]) -> f64 {
    let mut score = 0.0f64;
    for &b in bytes {
        if b.is_ascii_alphabetic() {
            let c = b.to_ascii_lowercase();
            score += match c {
                b'e' => 12.0,
                b't' => 9.1,
                b'a' => 8.2,
                b'o' => 7.5,
                b'i' => 7.0,
                b'n' => 6.7,
                b's' => 6.3,
                b'h' => 6.1,
                b'r' => 6.0,
                b'd' => 4.3,
                b'l' => 4.0,
                b'u' => 2.8,
                b'c' => 2.8,
                b'm' => 2.4,
                b'w' => 2.4,
                b'f' => 2.2,
                b'g' => 2.0,
                b'y' => 2.0,
                b'p' => 1.9,
                b'b' => 1.5,
                b'v' => 0.98,
                b'k' => 0.77,
                b'j' => 0.15,
                b'x' => 0.15,
                b'q' => 0.095,
                b'z' => 0.074,
                _ => 0.0,
            };
        } else if b == b' ' {
            score += 2.5;
        } else if b.is_ascii_control() || b == 0x7f {
            score -= 10.0;
        } else if !b.is_ascii_alphanumeric() {
            score -= 2.0;
        }
    }
    score / bytes.len().max(1) as f64
}