extern crate hamming;
#[macro_use]
extern crate log;
use std::io::{Read, BufReader};
use std::fs::File;
use hamming::distance;
use std::collections::HashMap;
use std::ascii::AsciiExt;
pub trait Xor {
fn xor(&mut self, key_bytes : &Vec<u8>) -> Vec<u8>;
}
fn xor(reader: &mut Read, key_bytes : &Vec<u8>) -> Vec<u8> {
let mut key_idx = 0;
let mut warning_shown = false;
let mut encoded_bytes: Vec<u8> = Vec::new();
loop {
let mut data = [0; 1024];
let num_read = reader.read(&mut data[..]).unwrap();
if num_read == 0 {
break;
}
let data_bytes = &data[0 .. num_read];
for b in data_bytes {
let k = key_bytes[key_idx];
let e = b ^ k;
encoded_bytes.push(e);
key_idx += 1;
if key_idx >= key_bytes.len() {
key_idx = key_idx % key_bytes.len();
if !warning_shown {
warning_shown = true;
warn!("Key wasn't long enough and had to be re-used to fully encode data, use a longer key to be secure.");
}
}
}
}
encoded_bytes
}
impl<'a, R: Read> Xor for &'a mut R {
fn xor(&mut self, key_bytes : &Vec<u8>) -> Vec<u8> {
xor(self, key_bytes)
}
}
impl Xor for Read {
fn xor(&mut self, key_bytes : &Vec<u8>) -> Vec<u8> {
xor(self, key_bytes)
}
}
pub trait Score {
fn score(&self) -> f32;
}
pub trait ScoreAgainstDictionary {
fn score_with_words(&self, words_list : Vec<String>) -> f32;
}
impl Score for char {
fn score(&self) -> f32 {
score_character(*self)
}
}
impl Score for String {
fn score(&self) -> f32 {
let expected_char_frequency = get_char_score_map();
let ascii_only_vector : Vec<u8> = self.chars()
.filter(|c| c.is_ascii())
.map(|c| c.to_ascii_lowercase())
.filter(|c| expected_char_frequency.get(&c).is_some())
.map(|c| c as u8)
.collect();
let ascii_only = String::from_utf8(ascii_only_vector).unwrap();
debug!("Ascii only is: {}", ascii_only);
let mut actual_char_frequency = HashMap::new();
for c in ascii_only.chars() {
let count = actual_char_frequency.entry(c).or_insert(0.0);
*count += 1.0;
}
for count in actual_char_frequency.values_mut() {
*count = *count / ascii_only.len() as f32;
}
let mut sum = 0.0f32;
for (c, freq) in actual_char_frequency {
let expected = expected_char_frequency.get(&c).unwrap();
let diff = (*expected - freq).abs() * 10.0;
debug!("Diff for char '{}' is {}", c, diff);
sum += diff;
}
let proportion_of_ascii = ascii_only.len() as f32 / self.len() as f32;
sum = sum * proportion_of_ascii;
sum
}
}
impl ScoreAgainstDictionary for String {
fn score_with_words(&self, words_list : Vec<String>) -> f32 {
let mut sum = 0.0f32;
sum += self.score();
sum += score_words(self, words_list);
sum
}
}
pub fn load_words_list(path : &str) -> Vec<String> {
let mut dictionary_lines : Vec<String> = Vec::new();
match File::open(path) {
Ok(file) => {
let mut reader = BufReader::new(file);
let mut dictionary_data = String::new();
let _ = reader.read_to_string(&mut dictionary_data);
for line in dictionary_data.lines() {
let word = line.to_lowercase();
dictionary_lines.push(word);
}
dictionary_lines.sort_by(|a, b| {
let x = a.len();
let y = b.len();
y.cmp(&x)
});
},
Err(err) => {
println!("Failed to open dictionary file '{}' because: {:?}", path, err);
}
}
dictionary_lines
}
fn recursive_add_keys(length: u32, prefix : Vec<u8>, keys : &mut Vec<String>) {
if prefix.len() == (length as usize) {
let key = String::from_utf8(prefix).unwrap();
keys.push(key);
} else {
for idx in 0..128 {
let mut new_prefix = prefix.clone();
new_prefix.push(idx);
recursive_add_keys(length, new_prefix, keys);
}
}
}
pub fn gen_ascii_keys(length : u32) -> Vec<String> {
let mut keys : Vec<String> = Vec::new();
let prefix : Vec<u8> = Vec::new();
recursive_add_keys(length, prefix, &mut keys);
keys
}
pub fn avg_normalized_hamming_distance(input : &Vec<u8>, max_keysize : usize) -> HashMap<usize, f32> {
let mut keysize_to_avg_hamming_dist = HashMap::new();
for keysize in 1..(max_keysize+1) {
let mut chunks = input.chunks(keysize);
let mut num_chunks_compared = 0;
let mut average_hamming_dist = 0.0_f32;
for _ in 1..3 {
let left_chunk = chunks.next();
let right_chunk = chunks.next();
if left_chunk.is_none() {
break;
}
if right_chunk.is_none() {
break;
}
let left = left_chunk.unwrap();
let right = right_chunk.unwrap();
if left.len() != right.len() {
break;
}
let hamming_dist = distance(left, right);
let normalized_hamming = hamming_dist as f32 / keysize as f32;
average_hamming_dist += normalized_hamming;
debug!("{:4.3} is the normalized hamming distance for keysize {} and block {}", normalized_hamming, keysize, num_chunks_compared);
num_chunks_compared += 1;
}
if num_chunks_compared != 0 {
average_hamming_dist = average_hamming_dist / num_chunks_compared as f32;
keysize_to_avg_hamming_dist.insert(keysize, average_hamming_dist);
} else {
debug!("Not enough data in input file to check a keysize of '{}'", keysize);
}
}
keysize_to_avg_hamming_dist
}
fn score_words(words : &String, dictionary : Vec<String>) -> f32 {
let mut score : f32 = 0.0;
let mut cloned_input = words.clone();
for word in dictionary {
if cloned_input.contains(word.as_str()) {
let adjustment = 3.0 * (word.len() as f32).exp();
score = score + adjustment;
cloned_input = cloned_input.replacen(word.as_str(), "", 1);
}
}
score
}
fn score_character(c : char) -> f32 {
let character_scores = get_char_score_map();
if character_scores.contains_key(&c) {
let value = character_scores.get(&c).unwrap();
*value
} else {
0.00
}
}
fn get_char_score_map() -> HashMap<char, f32> {
let mut character_scores = HashMap::new();
character_scores.insert(' ', 15.000); character_scores.insert('e', 12.702);
character_scores.insert('t', 9.056);
character_scores.insert('a', 8.167);
character_scores.insert('o', 7.507);
character_scores.insert('i', 6.966);
character_scores.insert('n', 6.749);
character_scores.insert('s', 6.327);
character_scores.insert('h', 6.094);
character_scores.insert('r', 5.987);
character_scores.insert('d', 4.253);
character_scores.insert('l', 4.025);
character_scores.insert('c', 2.782);
character_scores.insert('u', 2.758);
character_scores.insert('m', 2.406);
character_scores.insert('w', 2.360);
character_scores.insert('f', 2.228);
character_scores.insert('g', 2.015);
character_scores.insert('y', 1.974);
character_scores.insert('p', 1.929);
character_scores.insert('b', 1.492);
character_scores.insert('v', 0.978);
character_scores.insert('k', 0.772);
character_scores.insert('j', 0.153);
character_scores.insert('x', 0.150);
character_scores.insert('q', 0.095);
character_scores.insert('z', 0.074);
character_scores
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn xor_works() {
let data : Vec<u8> = vec![0b11111111u8, 0b11111111u8, 0b00001111u8, 0b10101010u8, 0b11111111u8, 0b11111111u8, 0b00001111u8, 0b10101010u8];
let key : Vec<u8> = vec![0b11111111u8, 0b00000000u8, 0b11110000u8, 0b01010101u8];
let reader : &mut Read = &mut Cursor::new(data);
let cipher = reader.xor(key);
assert_eq!(0b00000000u8, cipher[0]);
assert_eq!(0b11111111u8, cipher[1]);
assert_eq!(0b11111111u8, cipher[2]);
assert_eq!(0b11111111u8, cipher[3]);
assert_eq!(0b00000000u8, cipher[4]);
assert_eq!(0b11111111u8, cipher[5]);
assert_eq!(0b11111111u8, cipher[6]);
assert_eq!(0b11111111u8, cipher[7]);
}
#[test]
fn scoring_strings_works() {
let a = String::from("hello world");
let b = String::from("9[;,1.23,45");
let c = String::from("$*(&^$@!as3");
let d = String::from("kj12asd89hh");
let score_a = a.score();
let score_b = b.score();
let score_c = c.score();
let score_d = d.score();
assert!(score_a > score_b);
assert!(score_a > score_c);
assert!(score_a > score_d);
}
}