diff --git a/Cargo.lock b/Cargo.lock index 407f824..a5320d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,9 +17,22 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "break_repeating_xor" +version = "0.1.0" +dependencies = [ + "common", + "env_logger", + "log", + "rustc-serialize", +] + [[package]] name = "common" version = "0.1.0" +dependencies = [ + "log", +] [[package]] name = "detect_xor" diff --git a/Cargo.toml b/Cargo.toml index f4424ae..800e077 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,6 @@ members = [ "set1/challenge3_xor_cipher", "set1/challenge4_detect_xor", "set1/challenge5_repeating_xor", + "set1/challenge6_break_repeating_xor", ] resolver = "2" diff --git a/common/Cargo.toml b/common/Cargo.toml index 2894bbe..eb4ec24 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -5,3 +5,4 @@ edition = "2021" authors = ["Thomas Lindner "] [dependencies] +log = "0.4" diff --git a/common/src/lib.rs b/common/src/lib.rs index ecee692..c3dac5f 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,6 +1,12 @@ +use log::{debug, trace}; +use std::ascii::escape_default; use std::iter::repeat; -pub fn score(plaintext: &Vec) -> u32 { +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; @@ -8,7 +14,9 @@ pub fn score(plaintext: &Vec) -> u32 { for ch in plaintext.iter() { if ch >= &0x20 && ch <= &0x7e { score += 1; - if ch >= &('a' as u8) && ch <= &('z' as u8) { + 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]; @@ -18,6 +26,26 @@ pub fn score(plaintext: &Vec) -> u32 { 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() } @@ -35,16 +63,58 @@ pub fn break_xor_cipher(ciphertext: &[u8]) -> (Vec, u32) { let mut decrypted: Vec = vec![]; for key in 0..0xff { - let plaintext: Vec = xor_cipher(&ciphertext, key); - let s = score(&plaintext); + 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 +} diff --git a/set1/challenge6_break_repeating_xor/6.txt b/set1/challenge6_break_repeating_xor/6.txt new file mode 100644 index 0000000..cecdb81 --- /dev/null +++ b/set1/challenge6_break_repeating_xor/6.txt @@ -0,0 +1,64 @@ +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 diff --git a/set1/challenge6_break_repeating_xor/6_plaintext.txt b/set1/challenge6_break_repeating_xor/6_plaintext.txt new file mode 100644 index 0000000..14283df --- /dev/null +++ b/set1/challenge6_break_repeating_xor/6_plaintext.txt @@ -0,0 +1,79 @@ +I'm back and I'm ringin' the bell +A rockin' on the mike while the fly girls yell +In ecstasy in the back of me +Well that's my DJ Deshay cuttin' all them Z's +Hittin' hard and the girlies goin' crazy +Vanilla's on the mike, man I'm not lazy. + +I'm lettin' my drug kick in +It controls my mouth and I begin +To just let it flow, let my concepts go +My posse's to the side yellin', Go Vanilla Go! + +Smooth 'cause that's the way I will be +And if you don't give a damn, then +Why you starin' at me +So get off 'cause I control the stage +There's no dissin' allowed +I'm in my own phase +The girlies sa y they love me and that is ok +And I can dance better than any kid n' play + +Stage 2 -- Yea the one ya' wanna listen to +It's off my head so let the beat play through +So I can funk it up and make it sound good +1-2-3 Yo -- Knock on some wood +For good luck, I like my rhymes atrocious +Supercalafragilisticexpialidocious +I'm an effect and that you can bet +I can take a fly girl and make her wet. + +I'm like Samson -- Samson to Delilah +There's no denyin', You can try to hang +But you'll keep tryin' to get my style +Over and over, practice makes perfect +But not if you're a loafer. + +You'll get nowhere, no place, no time, no girls +Soon -- Oh my God, homebody, you probably eat +Spaghetti with a spoon! Come on and say it! + +VIP. Vanilla Ice yep, yep, I'm comin' hard like a rhino +Intoxicating so you stagger like a wino +So punks stop trying and girl stop cryin' +Vanilla Ice is sellin' and you people are buyin' +'Cause why the freaks are jockin' like Crazy Glue +Movin' and groovin' trying to sing along +All through the ghetto groovin' this here song +Now you're amazed by the VIP posse. + +Steppin' so hard like a German Nazi +Startled by the bases hittin' ground +There's no trippin' on mine, I'm just gettin' down +Sparkamatic, I'm hangin' tight like a fanatic +You trapped me once and I thought that +You might have it +So step down and lend me your ear +'89 in my time! You, '90 is my year. + +You're weakenin' fast, YO! and I can tell it +Your body's gettin' hot, so, so I can smell it +So don't be mad and don't be sad +'Cause the lyrics belong to ICE, You can call me Dad +You're pitchin' a fit, so step back and endure +Let the witch doctor, Ice, do the dance to cure +So come up close and don't be square +You wanna battle me -- Anytime, anywhere + +You thought that I was weak, Boy, you're dead wrong +So come on, everybody and sing this song + +Say -- Play that funky music Say, go white boy, go white boy go +play that funky music Go white boy, go white boy, go +Lay down and boogie and play that funky music till you die. + +Play that funky music Come on, Come on, let me hear +Play that funky music white boy you say it, say it +Play that funky music A little louder now +Play that funky music, white boy Come on, Come on, Come on +Play that funky music diff --git a/set1/challenge6_break_repeating_xor/Cargo.toml b/set1/challenge6_break_repeating_xor/Cargo.toml new file mode 100644 index 0000000..86f1eb5 --- /dev/null +++ b/set1/challenge6_break_repeating_xor/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "break_repeating_xor" +version = "0.1.0" +edition = "2021" +authors = ["Thomas Lindner "] + +[dependencies] +common = { path = "../../common" } +log = "0.4" +env_logger = "0.10" +rustc-serialize = "0.3" diff --git a/set1/challenge6_break_repeating_xor/src/main.rs b/set1/challenge6_break_repeating_xor/src/main.rs new file mode 100644 index 0000000..2c95615 --- /dev/null +++ b/set1/challenge6_break_repeating_xor/src/main.rs @@ -0,0 +1,37 @@ +use common::break_repeating_xor_cipher; +use log::error; +use rustc_serialize::base64::FromBase64; +use std::error::Error; +use std::io::{Read, read_to_string, stdin}; + +fn break_xor(input: &mut dyn Read) -> Result> { + let ciphertext = read_to_string(input)?.from_base64()?; + let decrypted = break_repeating_xor_cipher(ciphertext.as_slice()); + Ok(String::from_utf8(decrypted)?) +} + +fn main() { + env_logger::init(); + + match break_xor(&mut stdin().lock()) { + Ok(s) => println!("{}", s), + Err(e) => error!("{}", e) + } +} + +#[cfg(test)] +mod tests { + use std::fs::File; + use std::io::BufReader; + use std::path::Path; + use super::*; + + #[test] + fn challenge6_break_repeating_xor() { + let cargo_manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let testinput = File::open(cargo_manifest_dir.join("6.txt")).unwrap(); + let decrypted = break_xor(&mut BufReader::new(testinput)).unwrap(); + let plaintext = read_to_string(File::open(cargo_manifest_dir.join("6_plaintext.txt")).unwrap()).unwrap(); + assert_eq!(decrypted, plaintext); + } +}