2
\$\begingroup\$

I've recently started learning rust and doing cryptopals as exercise. I coded a program that decrypts single xor cypher. The algorithm I came up with is very simple and probably not efficient, but I would like more emphasis on rust philosophy and syntax

extern crate hex;
use std::collections::HashMap;

fn main() {
    let ciphertext = "1b37373331363f78151b7f2b783431333d78397828372d363c78373e783a393b3736";
    let ciphertext_bytes = hex::decode(ciphertext).unwrap();
    let xor_key = find_xor_key(&ciphertext_bytes);
    println!("{}", ciphertext_bytes.iter().map(|x|(x^xor_key) as char ).collect::<String>());
}

fn find_xor_key(ciphertext: &Vec<u8>) -> u8{

    let mut max_score = 0;
    let mut best_key = 0;
    for xor_key in 1..255{
        let xored_vec = ciphertext.iter().map(|x|x^xor_key).collect(); 
        let score = get_score(xored_vec);
        if score > max_score{
            max_score = score;
            best_key = xor_key;
        } 
    }
    println!("{}", max_score);
    
    best_key
}

fn get_score(text: Vec<u8>) -> u8
{
    let ideal_frequencies: HashMap<char, f32> = [
    
('E', 11.1607),
('M', 3.0129),
('A', 8.4966),
('H', 3.0034),
('R', 7.5809),
('G', 2.4705),
('I', 7.5448),
('B', 2.0720),
('O', 7.1635),
('F', 1.8121),
('T', 6.9509),
('Y', 1.7779),
('N', 6.6544),
('W', 1.2899),
('S', 5.7351),
('K', 1.1016),
('L', 5.4893),
('V', 1.0074),
('C', 4.5388),
('X', 0.2902),
('U', 3.6308),
('Z', 0.2722),
('D', 3.3844),
('J', 0.1965),
('P', 3.1671),
('Q', 0.1962),
    
    ].iter().cloned().collect();

    for byte in text.clone(){
        let char_rep: char =  byte as char;
        if !char_rep.is_ascii(){
            return 0;
        }
    }
    let mut score = 1;
    let chars: Vec<char> = text.iter().cloned().map(|x|x as char).collect();
    let frequencies: HashMap<char, f32> = calculate_frequencies(&chars.into_iter().collect::<String>()); 

    for (letter, frequency) in &frequencies{
        if ideal_frequencies.contains_key(&letter) && //already checked here that it exists in the hashmap
        f32::abs(frequency - ideal_frequencies.get(&letter).unwrap()) <= 10.0{
            score += 1;
        } 
    }
    score
}


fn calculate_frequencies(text: &str) ->HashMap<char, f32>{

    let mut frequencies: HashMap<char, f32> = HashMap::new();
    let mut letter_counts:HashMap<char, i32> = HashMap::new();
    let text_upper = text.to_ascii_uppercase();
    for c in text_upper.chars(){
        let count = letter_counts.entry(c).or_insert(0);
        *count += 1;        
    }
    for (c, count) in letter_counts{
        frequencies.entry(c).or_insert(count as f32 / text_upper.len() as f32 * 100.0);
    }
    frequencies
}
```
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$
extern crate hex;

extern crate is generally not necessary for programs since the 2018 (currently latest) edition of Rust. You should be able to delete this line, provided that edition = "2018" is in your Cargo.toml file.


let ciphertext_bytes = hex::decode(ciphertext).unwrap();

If you change .unwrap to .expect("failed to decode ciphertext") then there'll be a little more information in the error message. Of course, for a well-made command line program, you'd want to print your own better-formatted error message, instead of a Rust "panic" error.


fn find_xor_key(ciphertext: &Vec<u8>) -> u8{

Accepting &Vec<u8> is usually overly restrictive because it requires the caller to have constructed a Vec, whereas if you change the type to &[u8] they can pass any slice of bytes. This will work without needing to change main, because Vec derefs to a byte slice.

The same applies to get_score's parameter, but you'll need to tweak a couple things to make that work, like giving xored_vec the explicit type Vec<u8> and changing byte as char to *byte as char.


    let ideal_frequencies: HashMap<char, f32> = [
...
    ].iter().cloned().collect();

This is basically a constant, so it'd be nice if we can compute this hash map only once rather than every time we check a score. We can do that with the once_cell crate, which will hopefully soon become part of the standard library. Like this:

static IDEAL_FREQUENCIES: Lazy<HashMap<char, f32>> = Lazy::new(|| [
    ('E', 11.1607),
    ...
].iter().cloned().collect());

for byte in text.clone(){

Use text.iter() instead of text.clone(); it avoids allocating a copy. In general, where both are available, .into_iter() consumes the structure and iterates over its elements by value (moves them), and .iter() does not consume the structure but iterates over references to its elements instead.


let char_rep: char =  byte as char;
if !char_rep.is_ascii(){

You can just use if !byte.is_ascii() { here.


let chars: Vec<char> = text.iter().cloned().map(|x|x as char).collect();
let frequencies: HashMap<char, f32> = calculate_frequencies(&chars.into_iter().collect::<String>()); 

.cloned().map(|x|x as char) can be written as .map(|x| *x as char).

You're collecting into chars and then immediately iterate to convert it to a String; that isn't necessary because you can just write

calculate_frequencies(&text.iter().map(|x| *x as char).collect::<String>())

But, there's a bigger point: text was already an &[u8], and if we want a &str out of that, that's just std::str::from_utf8.

calculate_frequencies(std::str::from_utf8(text).unwrap())

(.unwrap() will never fail here because we already checked that every byte is ASCII, so it cannot be invalid UTF-8.)


Your loop over the frequencies has several unnecessary &s, and also looks up in the hash map twice instead of once; here's both fixed:

for (letter, frequency) in frequencies.iter() {
    if let Some(ideal_frequency) = IDEAL_FREQUENCIES.get(letter) {
        if (frequency - ideal_frequency).abs() <= 10.0 {
            score += 1;
        }
    } 
}

fn calculate_frequencies(text: &str) ->HashMap<char, f32>{

If we want to optimize for performance, which a real code-breaking tool would want to do, then this function should be accepting &[u8], since the only thing it does with the str is split back to characters, and we know all the characters are ASCII at this point. But for more general idiomatic programming, taking &str is fine.


let mut frequencies: HashMap<char, f32> = HashMap::new();
let mut letter_counts:HashMap<char, i32> = HashMap::new();

You can leave out the type declarations here and they will be inferred.


let text_upper = text.to_ascii_uppercase();
for c in text_upper.chars(){

This allocates a String of uppercase unnecessarily; we might as well do it per-character instead:

for c in text.chars() {
    let c = c.to_ascii_uppercase();

(Note, however, that this strategy does not work for Unicode-aware uppercasing, where one char may occasionally become several.)


This is not a matter of Rust idiom at all, but I notice that frequencies is identical to letter_counts except for the values getting scaled. So, we can reuse a single HashMap instead of duplicating it:

fn calculate_frequencies(text: &str) ->HashMap<char, f32>{
    let count_to_frequency_scale = 100.0 / text.len() as f32;
    
    let mut counts = HashMap::new();
    for c in text.chars() {
        *counts.entry(c.to_ascii_uppercase()).or_insert(0.0) += 1.0;
    }
    
    // Rescale counts to frequencies
    for count in counts.values_mut() {
        *count *= count_to_frequency_scale;
    }
    counts
}

Note: Using f32 instead of i32 for counting does not introduce any floating-point error, for reasonable string lengths: f32 exactly represents all integers up to 224 - 1 = 16,777,216. (However, there would be accumulated error if we tried to apply count_to_frequency_scale while counting instead of after.)

If it didn't happen to be possible to reuse the same map, I'd be suggesting using iterators to do the transformation instead:

letter_counts
    .into_iter()
    .map(|(key, count)| (key, count as f32 * count_to_frequency_scale))
    .collect()

Here's your code with all of my suggestions, and also formatted with rustfmt. I haven't tested it because I was working in the Rust Playground which doesn't include the hex crate in its available libraries.

extern crate hex;
use once_cell::sync::Lazy;
use std::collections::HashMap;

#[allow(dead_code)]
fn main() {
    let ciphertext = "1b37373331363f78151b7f2b783431333d78397828372d363c78373e783a393b3736";
    let ciphertext_bytes = hex::decode(ciphertext).unwrap();
    let xor_key = find_xor_key(&ciphertext_bytes);
    println!(
        "{}",
        ciphertext_bytes
            .iter()
            .map(|x| (x ^ xor_key) as char)
            .collect::<String>()
    );
}

fn find_xor_key(ciphertext: &[u8]) -> u8 {
    let mut max_score = 0;
    let mut best_key = 0;
    for xor_key in 1..255 {
        let xored_vec: Vec<u8> = ciphertext.iter().map(|x| x ^ xor_key).collect();
        let score = get_score(&xored_vec);
        if score > max_score {
            max_score = score;
            best_key = xor_key;
        }
    }
    println!("{}", max_score);

    best_key
}

fn get_score(text: &[u8]) -> u8 {
    for byte in text.iter() {
        if !byte.is_ascii() {
            return 0;
        }
    }
    let mut score = 1;
    let frequencies = calculate_frequencies(std::str::from_utf8(text).unwrap());

    for (letter, frequency) in frequencies.iter() {
        if let Some(ideal_frequency) = IDEAL_FREQUENCIES.get(letter) {
            if (frequency - ideal_frequency).abs() <= 10.0 {
                score += 1;
            }
        }
    }
    score
}

fn calculate_frequencies(text: &str) -> HashMap<char, f32> {
    let count_to_frequency_scale = 100.0 / text.len() as f32;

    let mut counts = HashMap::new();
    for c in text.chars() {
        *counts.entry(c.to_ascii_uppercase()).or_insert(0.0) += 1.0;
    }

    // Rescale counts to frequencies
    for count in counts.values_mut() {
        *count *= count_to_frequency_scale;
    }
    counts
}

static IDEAL_FREQUENCIES: Lazy<HashMap<char, f32>> = Lazy::new(|| {
    [
        ('E', 11.1607),
        ('M', 3.0129),
        ('A', 8.4966),
        ('H', 3.0034),
        ('R', 7.5809),
        ('G', 2.4705),
        ('I', 7.5448),
        ('B', 2.0720),
        ('O', 7.1635),
        ('F', 1.8121),
        ('T', 6.9509),
        ('Y', 1.7779),
        ('N', 6.6544),
        ('W', 1.2899),
        ('S', 5.7351),
        ('K', 1.1016),
        ('L', 5.4893),
        ('V', 1.0074),
        ('C', 4.5388),
        ('X', 0.2902),
        ('U', 3.6308),
        ('Z', 0.2722),
        ('D', 3.3844),
        ('J', 0.1965),
        ('P', 3.1671),
        ('Q', 0.1962),
    ]
    .iter()
    .cloned()
    .collect()
});
```
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.