use crate::bytes::Bytes; use crate::bytes_base64::BytesBase64; use crate::cbc; use crate::ctr; use rand::Rng; use std::cell::RefCell; use std::collections::HashMap; use std::collections::HashSet; use std::io::{BufRead, BufReader}; pub fn challenge17() { fn read(path: &str) -> Vec { let file = std::fs::File::open(path).unwrap(); let br = BufReader::new(file); br.lines() .map(|line| BytesBase64::from_base64(&line.unwrap()).to_bytes()) .collect() } let key = Bytes::random(16); let encrypt = || -> (Bytes, Bytes, usize) { // The first function should select at random one of the ten strings let cleartexts = read("data/17.txt"); let index: usize = rand::thread_rng().gen_range(0..cleartexts.len()); let mut cleartext = Bytes(cleartexts[index].0.to_vec()); // pad the string out to the 16-byte AES block size and cleartext.pad_pkcs7(16); // CBC-encrypt it under that key, providing the caller the ciphertext // and IV and cleartext index for check. let iv = Bytes::random(16); (cbc::encrypt(&key, &iv, &cleartext), iv, index) }; // generate a random AES key (which it should save for all future encryptions) let (cipher, iv, cleartext_index) = encrypt(); let decryption_oracle = |iv: &Bytes, cipher: &Bytes| -> bool { // The second function should consume the ciphertext produced by the // first function, decrypt it, check its padding, and return true or // false depending on whether the padding is valid. let cleartext = cbc::decrypt(&key, iv, cipher); cleartext.has_valid_pkcs7(16) }; let attack_block = |previous_block: &Bytes, cipher_block: &Bytes| -> Bytes { // Good explanation: https://robertheaton.com/2013/07/29/padding-oracle-attack/ let block_size = cipher_block.len(); let mut attack_vector = Bytes::random(block_size); let mut intermittent_result = vec![]; for pad_byte in 1..=block_size { // preset attack vector so that paddinig is [1], [2, 2], [3, 3, 3], and so on. attack_vector.0[block_size - pad_byte] = pad_byte as u8; for i in 0..(pad_byte - 1) { attack_vector.0[block_size - 1 - i] = (pad_byte as u8) ^ intermittent_result[i]; } // guess attack vector so that padding is valid let guess_index = block_size - pad_byte; for guess in 0..=255 { attack_vector.0[guess_index] = guess; if decryption_oracle(&attack_vector, cipher_block) { // println!("{guess:#016b}"); let c = (guess as u8) ^ (pad_byte as u8); intermittent_result.push(c); } } } // transform intermittent result by xoring it with previous block intermittent_result.reverse(); let xored: Vec = Iterator::zip(previous_block.0.iter(), intermittent_result) .map(|z| z.0 ^ z.1) .collect(); assert_eq!(xored.len(), block_size); Bytes(xored) }; // Attack block by block. let mut roundtrip = Bytes(vec![]); let block_count = cipher.len() / 16; for block in 0..block_count { let mut clear_block = if block == 0 { attack_block(&iv, &cipher.get_block(0, 16)) } else { attack_block( &cipher.get_block(block - 1, 16), &cipher.get_block(block, 16), ) }; roundtrip.0.append(&mut clear_block.0); } roundtrip.remove_pkcs7(16); let cleartexts = read("data/17.txt"); let cleartext = Bytes(cleartexts[cleartext_index].0.to_vec()); assert_eq!(roundtrip, cleartext); println!("[okay] Challenge 17: {}", roundtrip.to_utf8()); } pub fn challenge18() { let key = Bytes::from_utf8("YELLOW SUBMARINE"); let cleartext = Bytes::from_utf8("Let's see if we can get the party started hard my friends."); let cipher = ctr::encrypt(&key, 42351234, &cleartext); let roundtrip = ctr::decrypt(&key, 42351234, &cipher); assert_eq!(cleartext, roundtrip); let cipher = BytesBase64::from_base64( "L77na/nrFsKvynd6HzOoG7GHTLXsTVu9qvY/2syLXzhPweyyMTJULu/6/kXX0KSvoOLSFQ==", ) .to_bytes(); let cleartext = ctr::decrypt(&key, 0, &cipher).to_utf8(); println!("[okay] Challenge 18: {cleartext}"); } pub fn challenge19() { fn read(path: &str) -> Vec { let file = std::fs::File::open(path).unwrap(); let br = BufReader::new(file); br.lines() .map(|line| BytesBase64::from_base64(&line.unwrap()).to_bytes()) .collect() } fn xor_to_char_set(letters: &Vec) -> HashMap>> { let mut h = HashMap::new(); for i in 0..255_u8 { h.insert(i, RefCell::new(HashSet::new())); } for c1 in letters { for c2 in letters { let xored = c1 ^ c2; if let Some(h) = h.get(&xored) { let mut h_mut = h.borrow_mut(); h_mut.insert(*c1); h_mut.insert(*c2); }; } } h } fn u8_lower(s: u8) -> u8 { if s >= b'A' && s <= b'Z' { return s + 32; } s } fn ascii_letters(additional: &str) -> Vec { let mut letters: Vec = (0..255_u8).filter(u8::is_ascii_alphabetic).collect(); for b in additional.as_bytes() { letters.push(*b); } letters } fn attack(ciphers: Vec) -> Vec>> { let ciphers_len = ciphers.len(); let deciphered = vec![RefCell::new(vec![]); ciphers_len]; let max_cipher_len = ciphers.iter().map(|c| c.len()).max().unwrap_or(0); for byte_index in 0..max_cipher_len { let letters = match byte_index { // chars that work for 10 and 20 found via trial and error 10 => ascii_letters(" _-.,;:'"), 20 => ascii_letters(" _-.,;:?"), _ => ascii_letters(" _-.,;:"), }; let lookup = xor_to_char_set(&letters); let target_bytes: Vec> = ciphers .iter() .map(|c| { if c.len() > byte_index { Some(c.0[byte_index]) } else { None } }) .collect(); let mut possible_chars: Vec> = ciphers .iter() .map(|_| HashSet::from_iter(letters.iter().cloned())) .collect(); for i in 0..ciphers_len { for j in i..ciphers_len { if target_bytes[i] == None || target_bytes[j] == None { continue; } let xored = target_bytes[i].unwrap() ^ target_bytes[j].unwrap(); let chars = lookup.get(&xored).unwrap().borrow(); possible_chars[i] = possible_chars[i].intersection(&chars).cloned().collect(); possible_chars[j] = possible_chars[j].intersection(&chars).cloned().collect(); } } for cipher_index in 0..ciphers_len { if ciphers[cipher_index].len() <= byte_index { continue; } let chars: Vec = possible_chars[cipher_index].iter().cloned().collect(); match chars.len() { 0 => { // println!("No chars for {cipher_index} {byte_index}"); deciphered[cipher_index].borrow_mut().push(b'?'); } 1 => { deciphered[cipher_index] .borrow_mut() .push(u8_lower(chars[0])); } 2 => { if u8_lower(chars[0]) == u8_lower(chars[1]) { deciphered[cipher_index] .borrow_mut() .push(u8_lower(chars[0])); } else { // println!("Two {chars:?} {cipher_index} {byte_index}"); deciphered[cipher_index].borrow_mut().push(b'^'); } } _ => { // println!("Two {chars:?} {cipher_index} {byte_index}"); deciphered[cipher_index].borrow_mut().push(b'^'); } } } } deciphered } fn manual(decrypts: &Vec>>) { // Add manually guessed letters decrypts[0].borrow_mut()[30] = b'y'; decrypts[2].borrow_mut()[30] = b'y'; let mut d4 = decrypts[4].borrow_mut(); d4[30] = b'e'; d4[32] = b'h'; d4[33] = b'e'; d4[34] = b'a'; d4[35] = b'd'; decrypts[6].borrow_mut()[30] = b'i'; decrypts[13].borrow_mut()[30] = b' '; decrypts[20].borrow_mut()[30] = b' '; decrypts[25].borrow_mut()[30] = b'n'; decrypts[28].borrow_mut()[30] = b' '; decrypts[29].borrow_mut()[30] = b't'; decrypts[37].borrow_mut()[30] = b'i'; } let plaintexts = read("data/19.txt"); let key = Bytes::from_utf8("YELLOW SUBMARINE"); let encrypt = |plaintext: &Bytes| -> Bytes { ctr::encrypt(&key, 0, plaintext) }; let ciphers: Vec = plaintexts.iter().map(|ct| encrypt(&ct)).collect(); let decrypts = attack(ciphers); manual(&decrypts); for row in decrypts { println!( "[okay] Challenge 19: {}", Bytes(row.borrow().to_vec()).to_utf8() ); break; } } pub fn challenge20() { println!("[xxxx] Challenge 20: TBD"); }