Skip to main content

xor_utils/
lib.rs

1
2//! Utility functions related to xor encryption / decryption.
3//!
4//! Contains a mix bag of functions related to xor encryption / decryption.
5
6extern crate hamming;
7
8#[macro_use]
9extern crate log;
10
11use std::io::{Read, BufReader};
12use std::fs::File;
13use hamming::distance;
14use std::collections::HashMap;
15use std::ascii::AsciiExt;
16
17pub trait Xor {
18    /// Creates xor encrypted copy of data using the provided key.
19    fn xor(&mut self, key_bytes : &Vec<u8>) -> Vec<u8>;
20}
21
22fn xor(reader: &mut Read, key_bytes : &Vec<u8>) -> Vec<u8> {
23    let mut key_idx = 0;
24    let mut warning_shown = false;
25    let mut encoded_bytes: Vec<u8> = Vec::new();
26
27    // Iterate each chunk of input data and XOR it against the provided key.
28    loop {
29        let mut data = [0; 1024];
30        let num_read = reader.read(&mut data[..]).unwrap();
31
32        if num_read == 0 {
33            break;
34        }
35
36        let data_bytes = &data[0 .. num_read];
37
38        for b in data_bytes {
39            let k = key_bytes[key_idx];
40            let e = b ^ k;
41
42            encoded_bytes.push(e);
43
44            key_idx += 1;
45
46            if key_idx >= key_bytes.len() {
47                key_idx = key_idx % key_bytes.len();
48
49                if !warning_shown {
50                    warning_shown = true;
51                    warn!("Key wasn't long enough and had to be re-used to fully encode data, use a longer key to be secure.");
52                }
53            }
54        }
55    }
56
57    encoded_bytes
58}
59
60impl<'a, R: Read> Xor for &'a mut R {
61    fn xor(&mut self, key_bytes : &Vec<u8>) -> Vec<u8> {
62        xor(self, key_bytes)
63    }
64}
65
66impl Xor for Read {
67    fn xor(&mut self, key_bytes : &Vec<u8>) -> Vec<u8> {
68        xor(self, key_bytes)
69    }
70}
71
72pub trait Score {
73    /// Calculates a relative value "score" for an item which relates to how likely it is the item
74    /// represents text.
75    ///
76    /// This value can be used to determine the likeliness of the item representing text.
77    fn score(&self) -> f32;
78}
79
80pub trait ScoreAgainstDictionary {
81    /// Calculates a relative value "score" for an item which relates to how likely it is the item
82    /// represents text.
83    ///
84    /// The provided words vector is used to increase the score of the item if it contained any of
85    /// the words in the vector.
86    fn score_with_words(&self, words_list : Vec<String>) -> f32;
87}
88
89impl Score for char {
90    fn score(&self) -> f32 {
91        score_character(*self)
92    }
93}
94
95impl Score for String {
96    fn score(&self) -> f32 {
97
98        let expected_char_frequency = get_char_score_map();
99
100        // Filter to only ascii characters that are contained in the expected char freq dict.
101        // Uppercase is mapped to lowercase.
102        let ascii_only_vector : Vec<u8> = self.chars()
103            .filter(|c| c.is_ascii())
104            .map(|c| c.to_ascii_lowercase())
105            .filter(|c| expected_char_frequency.get(&c).is_some())
106            .map(|c| c as u8)
107            .collect();
108
109        // String containing only the ascii parts of the input string.
110        let ascii_only = String::from_utf8(ascii_only_vector).unwrap();
111        debug!("Ascii only is: {}", ascii_only);
112
113        let mut actual_char_frequency = HashMap::new();
114
115        // Build the dict of actual char frequencies.
116        for c in ascii_only.chars() {
117            let count = actual_char_frequency.entry(c).or_insert(0.0);
118            *count += 1.0;
119        }
120        for count in actual_char_frequency.values_mut() {
121            *count = *count / ascii_only.len() as f32;
122        }
123
124        let mut sum = 0.0f32;
125
126        for (c, freq) in actual_char_frequency {
127            let expected = expected_char_frequency.get(&c).unwrap();
128            let diff = (*expected - freq).abs() * 10.0;
129
130            debug!("Diff for char '{}' is {}", c, diff);
131            sum += diff;
132        }
133
134        let proportion_of_ascii = ascii_only.len() as f32 / self.len() as f32;
135
136        sum = sum * proportion_of_ascii;
137
138        sum
139    }
140}
141
142impl ScoreAgainstDictionary for String {
143    fn score_with_words(&self, words_list : Vec<String>) -> f32 {
144        let mut sum = 0.0f32;
145
146        // Score each character.
147        sum += self.score();
148
149        // Score each word.
150        sum += score_words(self, words_list);
151
152        sum
153    }
154}
155
156/// Loads all lines in the given file and sorts them
157///
158/// Assumes the file is newline separated list of words.
159pub fn load_words_list(path : &str) -> Vec<String> {
160
161    // Will hold all the words in the dictionary file.
162    let mut dictionary_lines : Vec<String> = Vec::new();
163
164    match File::open(path) {
165        Ok(file) => {
166            // Read all the words and push them to the dictionary vector.
167            let mut reader = BufReader::new(file);
168            let mut dictionary_data = String::new();
169            let _ = reader.read_to_string(&mut dictionary_data);
170
171            for line in dictionary_data.lines() {
172                let word = line.to_lowercase();
173                dictionary_lines.push(word);
174            }
175
176            // Sort the dictionary in order of word length, largest words to smallest words.
177            dictionary_lines.sort_by(|a, b| {
178                let x = a.len();
179                let y = b.len();
180
181                y.cmp(&x)
182            });
183        },
184        Err(err) => {
185            println!("Failed to open dictionary file '{}' because: {:?}", path, err);
186        }
187    }
188
189    dictionary_lines
190}
191
192fn recursive_add_keys(length: u32, prefix : Vec<u8>, keys : &mut Vec<String>) {
193    if prefix.len() == (length as usize) {
194        // Key has been generated
195        let key = String::from_utf8(prefix).unwrap();
196        keys.push(key);
197    } else {
198        for idx in 0..128 {
199            let mut new_prefix = prefix.clone();
200            new_prefix.push(idx);
201
202            recursive_add_keys(length, new_prefix, keys);
203        }
204    }
205}
206
207
208/// Generate all combinations of ASCII up to the supplied character length.
209///
210/// This can be used to get all the possible ASCII keys of a certain character length.
211pub fn gen_ascii_keys(length : u32) -> Vec<String> {
212    let mut keys : Vec<String> = Vec::new();
213    let prefix : Vec<u8> = Vec::new();
214
215    recursive_add_keys(length, prefix, &mut keys);
216
217    keys
218}
219
220/// Calculates the average normalized hamming distance for the given input bytes
221///
222/// The average normalized hamming distance is calculated by
223///
224/// 1. Pick a keysize s
225/// 2. Take 2 chunks each of size s
226/// 3. Calculate the hamming distance between these 2 chunks
227/// 4. Normalize the hamming distance by dividing by s
228/// 5. Repeat 1-4 until there are no more chunks left
229/// 6. Calculate the mean average of the normalized hamming distances calculated from the above.
230///
231/// Returns a HashMap that maps keysize to average normalized hamming distance for that keysize.
232pub fn avg_normalized_hamming_distance(input : &Vec<u8>, max_keysize : usize) -> HashMap<usize, f32> {
233
234    let mut keysize_to_avg_hamming_dist = HashMap::new();
235
236    for keysize in 1..(max_keysize+1) {
237
238        let mut chunks = input.chunks(keysize);
239        let mut num_chunks_compared = 0;
240        let mut average_hamming_dist = 0.0_f32;
241
242        // Calculate the mean normalized hamming distance over a
243        // number of samples to try to improve accuracy.
244        for _ in 1..3 {
245
246            let left_chunk = chunks.next();
247            let right_chunk = chunks.next();
248
249            if left_chunk.is_none() {
250                break;
251            }
252            if right_chunk.is_none() {
253                break;
254            }
255
256            let left = left_chunk.unwrap();
257            let right = right_chunk.unwrap();
258
259            if left.len() != right.len() {
260                break;
261            }
262
263            let hamming_dist = distance(left, right);
264            let normalized_hamming = hamming_dist as f32 / keysize as f32;
265            average_hamming_dist += normalized_hamming;
266
267            debug!("{:4.3} is the normalized hamming distance for keysize {} and block {}", normalized_hamming, keysize, num_chunks_compared);
268
269            num_chunks_compared += 1;
270        }
271
272        if num_chunks_compared != 0 {
273            average_hamming_dist = average_hamming_dist / num_chunks_compared as f32;
274            keysize_to_avg_hamming_dist.insert(keysize, average_hamming_dist);
275        } else {
276            debug!("Not enough data in input file to check a keysize of '{}'", keysize);
277        }
278    }
279
280    keysize_to_avg_hamming_dist
281}
282
283fn score_words(words : &String, dictionary : Vec<String>) -> f32 {
284    let mut score : f32 = 0.0;
285
286    // Check if the input contains a word from the dictionary.
287    // Each time a word is matched it's removed from the input so there isn't double
288    // counting of words.
289    //
290    // For each word the score is increased by 3 * e ^ word_length.
291    // In this way large words contribute exponentially more to the overall score.
292    let mut cloned_input = words.clone();
293    for word in dictionary {
294        if cloned_input.contains(word.as_str()) {
295            let adjustment = 3.0 * (word.len() as f32).exp();
296            score = score + adjustment;
297
298            cloned_input = cloned_input.replacen(word.as_str(), "", 1);
299        }
300    }
301
302    score
303}
304
305
306fn score_character(c : char) -> f32 {
307    let character_scores = get_char_score_map();
308
309    if character_scores.contains_key(&c) {
310        let value = character_scores.get(&c).unwrap();
311        *value
312    } else {
313        0.00
314    }
315}
316
317// Creates a dictionary where:
318// key      - character
319// value    - frequency score
320fn get_char_score_map() -> HashMap<char, f32> {
321    let mut character_scores = HashMap::new();
322
323    character_scores.insert(' ', 15.000); // This is just guessed
324    character_scores.insert('e', 12.702);
325    character_scores.insert('t', 9.056);
326    character_scores.insert('a', 8.167);
327    character_scores.insert('o', 7.507);
328    character_scores.insert('i', 6.966);
329    character_scores.insert('n', 6.749);
330    character_scores.insert('s', 6.327);
331    character_scores.insert('h', 6.094);
332    character_scores.insert('r', 5.987);
333    character_scores.insert('d', 4.253);
334    character_scores.insert('l', 4.025);
335    character_scores.insert('c', 2.782);
336    character_scores.insert('u', 2.758);
337    character_scores.insert('m', 2.406);
338    character_scores.insert('w', 2.360);
339    character_scores.insert('f', 2.228);
340    character_scores.insert('g', 2.015);
341    character_scores.insert('y', 1.974);
342    character_scores.insert('p', 1.929);
343    character_scores.insert('b', 1.492);
344    character_scores.insert('v', 0.978);
345    character_scores.insert('k', 0.772);
346    character_scores.insert('j', 0.153);
347    character_scores.insert('x', 0.150);
348    character_scores.insert('q', 0.095);
349    character_scores.insert('z', 0.074);
350
351    character_scores
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use std::io::Cursor;
358
359    #[test]
360    fn xor_works() {
361
362        // Data is twice as long as the key.
363        let data : Vec<u8>  = vec![0b11111111u8, 0b11111111u8, 0b00001111u8, 0b10101010u8, 0b11111111u8, 0b11111111u8, 0b00001111u8, 0b10101010u8];
364        let key : Vec<u8>   = vec![0b11111111u8, 0b00000000u8, 0b11110000u8, 0b01010101u8];
365
366        let reader : &mut Read = &mut Cursor::new(data);
367
368        let cipher = reader.xor(key);
369
370        assert_eq!(0b00000000u8, cipher[0]);
371        assert_eq!(0b11111111u8, cipher[1]);
372        assert_eq!(0b11111111u8, cipher[2]);
373        assert_eq!(0b11111111u8, cipher[3]);
374        assert_eq!(0b00000000u8, cipher[4]);
375        assert_eq!(0b11111111u8, cipher[5]);
376        assert_eq!(0b11111111u8, cipher[6]);
377        assert_eq!(0b11111111u8, cipher[7]);
378    }
379
380    #[test]
381    fn scoring_strings_works() {
382        let a = String::from("hello world");
383        let b = String::from("9[;,1.23,45");
384        let c = String::from("$*(&^$@!as3");
385        let d = String::from("kj12asd89hh");
386
387        let score_a = a.score();
388        let score_b = b.score();
389        let score_c = c.score();
390        let score_d = d.score();
391
392        assert!(score_a > score_b);
393        assert!(score_a > score_c);
394        assert!(score_a > score_d);
395    }
396
397}