use crate::bytes::Bytes; use crate::bytes_base64::BytesBase64; use crate::cbc; use crate::ctr; use crate::mt19937; use crate::mtcipher; use crate::utils; use rand::Rng; use std::cell::RefCell; pub fn challenge17() { let key = Bytes::random(16); let encrypt = || -> (Bytes, Bytes, usize) { // The first function should select at random one of the ten strings let cleartexts = utils::read_base64_lines("data/17.txt"); let index: usize = rand::thread_rng().gen_range(0..cleartexts.len()); let mut cleartext = Bytes(cleartexts[index].0.clone()); // 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: u8 = cipher_block .len() .try_into() .expect("block size should be less than 255"); let mut attack_vector = Bytes::random(block_size.into()); let mut intermittent_result = vec![]; for pad_byte in 1_u8..=block_size { // preset attack vector so that paddinig is [1], [2, 2], [3, 3, 3], and so on. let pad_byte_index: usize = (block_size - pad_byte).into(); attack_vector.0[pad_byte_index] = pad_byte; for (i, intermittent_byte) in intermittent_result .iter() .enumerate() .take(pad_byte as usize - 1) { attack_vector.0[block_size as usize - 1 - i] = (pad_byte as u8) ^ intermittent_byte; } // 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 as usize] = 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.into()); 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 = utils::read_base64_lines("data/17.txt"); let cleartext = Bytes(cleartexts[cleartext_index].0.clone()); assert_eq!(roundtrip, cleartext); println!("[okay] Challenge 17: {}", roundtrip.to_utf8()); } pub fn challenge18() { let key = Bytes::from_utf8("YELLOW SUBMARINE"); let nonce = 1337; let cleartext = Bytes::from_utf8("Let's see if we can get the party started hard my friends."); let cipher = ctr::encrypt(&key, nonce, &cleartext); let roundtrip = ctr::decrypt(&key, nonce, &cipher); assert_eq!(cleartext, roundtrip); let cipher = BytesBase64::from_base64( "L77na/nrFsKvynd6HzOoG7GHTLXsTVu9qvY/2syLXzhPweyyMTJULu/6/kXX0KSvoOLSFQ==", ) .unwrap() .to_bytes(); let cleartext = ctr::decrypt(&key, 0, &cipher).to_utf8(); println!("[okay] Challenge 18: {cleartext}"); } mod challenge19 { use crate::bytes::Bytes; use std::cell::RefCell; use std::collections::HashMap; use std::collections::HashSet; 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 (b'A'..=b'Z').contains(&s) { 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 } pub fn attack(ciphers: &[Bytes]) -> Vec>> { let ciphers_len = ciphers.len(); let deciphered = vec![RefCell::new(vec![]); ciphers_len]; let max_cipher_len = ciphers.iter().map(Bytes::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(|_| letters.iter().copied().collect()) .collect(); for i in 0..ciphers_len { for j in i..ciphers_len { if target_bytes[i].is_none() || target_bytes[j].is_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).copied().collect(); possible_chars[j] = possible_chars[j].intersection(&chars).copied().collect(); } } for cipher_index in 0..ciphers_len { if ciphers[cipher_index].len() <= byte_index { continue; } let chars: Vec = possible_chars[cipher_index].iter().copied().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 } } pub fn challenge19() { fn manual(decrypts: &[RefCell>]) { // 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 = utils::read_base64_lines("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(encrypt).collect(); let decrypts = challenge19::attack(&ciphers); manual(&decrypts); let first_line = Bytes(decrypts[0].borrow().to_vec()).to_utf8(); println!("[okay] Challenge 19: {first_line}"); } pub fn challenge20() { fn attack(ciphers: &[Bytes]) -> Vec { let min_cipher_len = ciphers.iter().map(Bytes::len).min().unwrap_or(0); let mut key: Vec = vec![]; for byte_index in 0..min_cipher_len { let bytes = Bytes(ciphers.iter().map(|c| c.0[byte_index]).collect()); let key_char = Bytes::guess_key(&bytes); key.push(key_char); } let key = Bytes(key); ciphers .iter() .map(|cipher| Bytes::xor(&key, cipher)) .collect() } let plaintexts = utils::read_base64_lines("data/20.txt"); let key = Bytes::from_utf8("YELLOW SUBMARINE"); let encrypt = |plaintext: &Bytes| -> Bytes { ctr::encrypt(&key, 0, plaintext) }; let ciphers: Vec = plaintexts.iter().map(encrypt).collect(); let plaintexts = attack(&ciphers); println!("[okay] Challenge 20: {}", plaintexts[0].to_utf8()); } pub fn challenge21() { // Implement the MT19937 Mersenne Twister RNG let expected: Vec = vec![ 0xD091_BB5C, 0x22AE_9EF6, 0xE7E1_FAEE, 0xD5C3_1F79, 0x2082_352C, 0xF807_B7DF, 0xE9D3_0005, 0x3895_AFE1, 0xA1E2_4BBA, 0x4EE4_092B, ]; let mut mt = mt19937::MT19937::new(); mt.seed(5489); for e in expected { assert_eq!(mt.extract_number(), e); } println!("[okay] Challenge 21: implemented MT19937"); } pub fn challenge22() { fn find_seed(rngout: u32) -> Option { let mut mt = mt19937::MT19937::new(); let start = utils::unix_timestamp() - 2000; for seed in start..(start + 4000) { mt.seed(seed); if rngout == mt.extract_number() { return Some(seed); } } None } // Wait a random number of seconds between, I don't know, 40 and 1000. let now = utils::unix_timestamp(); let wait_time: u32 = rand::thread_rng().gen_range(40..1000); let seed = now + wait_time; // Seeds the RNG with the current Unix timestamp. let mut mt = mt19937::MT19937::new(); mt.seed(seed); // Returns the first 32 bit output of the RNG. let rngout = mt.extract_number(); // From the 32 bit RNG output, discover the seed. let found_seed = find_seed(rngout); assert_eq!(seed, found_seed.unwrap()); println!("[okay] Challenge 22: cracked MT19937 seed"); } pub fn challenge23() { const fn _temper(x: u32) -> u32 { const S: u32 = 7; const T: u32 = 15; const U: u32 = 11; const B: u32 = 0x9D2C_5680; const C: u32 = 0xEFC6_0000; const L: u32 = 18; let mut y = x; y = y ^ (y >> U); y = y ^ ((y << S) & B); y = y ^ ((y << T) & C); y = y ^ (y >> L); y } fn untemper(x: u32) -> u32 { const B: u32 = 0x9D2C_5680; const C: u32 = 0xEFC6_0000; let mut y = x; // reverse y = y ^ (y >> L); L = 18; const UPPER_18_BITS: u32 = u32::MAX << 14; const LOWER_14_BITS: u32 = u32::MAX >> 18; let mut o = y & UPPER_18_BITS; // upper 18 bits are correct o |= ((o & UPPER_18_BITS) >> 18) ^ (y & LOWER_14_BITS); // all 32 bits are correct y = o; // reverse y = y ^ ((y << T) & C); T = 15; const LOWER_15_BITS: u32 = u32::MAX >> 17; const MID_15_BITS: u32 = LOWER_15_BITS << 15; const UPPER_2_BITS: u32 = u32::MAX << 30; let mut o = y & LOWER_15_BITS; // lower 15 bits are correct o |= ((o << 15) & C) ^ (y & MID_15_BITS); // lower 30 bits are correct o |= (((o << 15) & C) & UPPER_2_BITS) ^ (y & UPPER_2_BITS); // all 32 bits are correct y = o; // reverse y = y ^ ((y << S) & B); S = 7 const LOWER_7_BITS: u32 = u32::MAX >> 25; const SECOND_7_BITS: u32 = LOWER_7_BITS << 7; const THIRD_7_BITS: u32 = SECOND_7_BITS << 7; const FOURTH_7_BITS: u32 = THIRD_7_BITS << 7; const UPPER_4_BITS: u32 = u32::MAX << 28; let mut o = y & LOWER_7_BITS; // lower 7 bits are correct o |= ((o << 7) & B) ^ (y & SECOND_7_BITS); // lower 14 bits are correct o |= (((o << 7) & B) & THIRD_7_BITS) ^ (y & THIRD_7_BITS); // lower 21 bits are correct o |= (((o << 7) & B) & FOURTH_7_BITS) ^ (y & FOURTH_7_BITS); // lower 28 bits are correct o |= (((o << 7) & B) & UPPER_4_BITS) ^ (y & UPPER_4_BITS); // all 32 bits are correct y = o; // reverse y = y ^ (y >> U); U = 11; const UPPER_11_BITS: u32 = u32::MAX << 21; const SECOND_11_BITS: u32 = UPPER_11_BITS >> 11; const LOWER_10_BITS: u32 = u32::MAX >> 22; let mut o = y & UPPER_11_BITS; // upper 11 bits are correct o |= ((o & UPPER_11_BITS) >> 11) ^ (y & SECOND_11_BITS); // upper 22 bits are correct o |= ((o & SECOND_11_BITS) >> 11) ^ (y & LOWER_10_BITS); // all 32 bits are correct y = o; y } // untemper test code // let a: u32 = 0x12345678; // let b = _temper(a); // let c = untemper(b); // println!("{:#010x} -> {:#010x} -> {:#010x}", a, b, c); // Once you have "untemper" working, create a new MT19937 generator, tap it for 624 outputs, let mut mt = mt19937::MT19937::new(); let seed: u32 = rand::thread_rng().gen::(); mt.seed(seed); let outputs: Vec = (0..624).map(|_| mt.extract_number()).collect(); // untemper each of them to recreate the state of the generator, let outputs = outputs.iter().map(|o| untemper(*o)).collect(); // and splice that state into a new instance of the MT19937 generator. let mut spliced_mt = mt19937::MT19937::new(); spliced_mt.splice(outputs); // The new "spliced" generator should predict the values of the original. for _ in 0..2000 { assert_eq!(mt.extract_number(), spliced_mt.extract_number()); } println!("[okay] Challenge 23: MT19937 RNG successfully cloned from output"); } pub fn challenge24() { // Verify that you can encrypt and decrypt properly. This code should look // similar to your CTR code. let key: u16 = 111; let cleartext = Bytes::from_utf8("Let's see if we can get the party started hard my friends."); let cipher = mtcipher::encrypt(key, &cleartext); let roundtrip = mtcipher::decrypt(key, &cipher); assert_eq!(cleartext, roundtrip); // Use your function to encrypt a known plaintext (say, 14 consecutive 'A' // characters) prefixed by a random number of random characters. fn get_plaintext() -> Bytes { let length: usize = rand::thread_rng().gen_range(30..100); let mut data = Bytes::random(length); data.0.append(&mut Bytes(vec![b'A'; 14]).0); data } let key: u16 = rand::thread_rng().gen::(); let plaintext = get_plaintext(); let cipher = mtcipher::encrypt(key, &plaintext); // From the ciphertext, recover the "key" (the 16 bit seed). fn recover_key(cipher: &Bytes) -> u16 { let cipher_len = cipher.len(); // brute force bb! for key in 0..u16::MAX { let mut found_key = true; let roundtrip = mtcipher::decrypt(key, cipher); // check if the last 14 chars are 'A' - if yes, we found the key for i in (cipher_len - 14)..cipher_len { if roundtrip.0[i] != b'A' { found_key = false; break; } } if found_key { return key; } } 0 } let recovered_key = recover_key(&cipher); assert_eq!(key, recovered_key); // Use the same idea to generate a random "password reset token" using // MT19937 seeded from the current time. fn get_reset_token(time: Option) -> Bytes { const TOKEN_LENGTH: usize = 16; let time = match time { Some(time) => time, None => utils::unix_timestamp(), }; let mut token = vec![]; let mut mt = mt19937::MT19937::new(); mt.seed(time); while token.len() < (TOKEN_LENGTH - 1) { for b in mt.extract_bytes() { if token.len() >= TOKEN_LENGTH { break; } if b.is_ascii_alphanumeric() { token.push(b); } } } Bytes(token) } let token = get_reset_token(None); // println!("{}", token.to_utf8()); // Write a function to check if any given password token is actually the // product of an MT19937 PRNG seeded with the current time. fn is_time_token(token: &Bytes) -> bool { let current_time = utils::unix_timestamp(); for time in (current_time - 10)..(current_time + 10) { let time_token = get_reset_token(Some(time)); if *token == time_token { return true; } } false } assert!(is_time_token(&token)); let non_token = Bytes(vec![b'z', 16]); assert!(!is_time_token(&non_token)); println!("[okay] Challenge 24: MT19937 stream cipher implemented and cracked"); }