use log::{debug, trace}; use std::ascii::escape_default; use std::iter::repeat; pub fn escape(text: &[u8]) -> String { String::from_utf8( text.iter() .map(|ch| escape_default(ch.clone())) .flatten() .collect(), ) .unwrap() } pub fn score(plaintext: &[u8]) -> u32 { let ranking = [ 24, 7, 15, 17, 26, 11, 10, 19, 22, 4, 5, 16, 13, 21, 23, 8, 2, 18, 20, 25, 14, 6, 12, 3, 9, 1, ]; let mut score = 0; for ch in plaintext.iter() { if ch >= &0x20 && ch <= &0x7e { score += 1; if ch == &(' ' as u8) { score += 27; } else if ch >= &('a' as u8) && ch <= &('z' as u8) { score += ranking[(ch - 'a' as u8) as usize]; } else if ch >= &('A' as u8) && ch <= &('Z' as u8) { score += ranking[(ch - 'A' as u8) as usize]; } } } score } pub fn hamming(v1: &[u8], v2: &[u8]) -> u32 { v1.iter() .zip(v2.iter()) .map(|(a, b)| a ^ b) .map(|x| ((x & 0xaa) >> 1) + (x & 0x55)) .map(|x| ((x & 0xcc) >> 2) + (x & 0x33)) .map(|x| ((x & 0xf0) >> 4) + (x & 0x0f)) .map(|x| x as u32) .sum() } #[cfg(test)] mod tests { use super::*; #[test] fn challenge6_hamming() { let v1 = "this is a test"; let v2 = "wokka wokka!!!"; assert_eq!(hamming(v1.as_bytes(), v2.as_bytes()), 37); } } pub fn generic_xor_cipher(text: &[u8], keyiter: &mut dyn Iterator) -> Vec { text.iter().zip(keyiter).map(|(c, k)| c ^ k).collect() } pub fn fixed_xor_cipher(text: &[u8], key: &[u8]) -> Vec { generic_xor_cipher(text, &mut key.iter()) } pub fn xor_cipher(text: &[u8], key: u8) -> Vec { generic_xor_cipher(text, &mut repeat(&key)) } pub fn break_xor_cipher(ciphertext: &[u8]) -> (Vec, u32) { let mut maxscore = 0; let mut decrypted: Vec = vec![]; for key in 0..0xff { let plaintext = xor_cipher(ciphertext, key); let s = score(plaintext.as_slice()); trace!("score:{} \"{}\"", s, escape(plaintext.as_slice())); if s > maxscore { maxscore = s; decrypted = plaintext; } } debug!("score:{} \"{}\"", maxscore, escape(decrypted.as_slice())); (decrypted, maxscore) } pub fn repeating_xor_cipher(text: &[u8], key: &[u8]) -> Vec { generic_xor_cipher(text, &mut key.iter().cycle()) } pub fn break_repeating_xor_cipher(ciphertext: &[u8]) -> Vec { let mut mindistance = u32::MAX; let mut keylength = 0; for length in 1..40 { let mut distance = 0; let maxoffset = ciphertext.len() / length - 1; for offset in 0..maxoffset { distance += hamming( &ciphertext[offset * length..(offset + 1) * length], &ciphertext[(offset + 1) * length..(offset + 2) * length], ); } // scale and normalize distance *= 1024; distance /= (maxoffset * length) as u32; trace!("distance:{} keylength:{}", distance, length); if distance < mindistance { mindistance = distance; keylength = length; } } debug!("estimated keylength: {}", keylength); let mut decryptedcolumns: Vec> = vec![]; for offset in 0..keylength { let ciphercolumn: Vec = ciphertext[offset..] .iter() .step_by(keylength) .cloned() .collect(); let (decrypted, _) = break_xor_cipher(ciphercolumn.as_slice()); decryptedcolumns.push(decrypted); } let mut decrypted: Vec = vec![]; for i in 0..decryptedcolumns[0].len() { for column in decryptedcolumns.iter() { if column.len() <= i { break; } decrypted.push(column[i]); } } decrypted }