use crate::{bytes::Bytes, cbc, ctr, ecb, md4, parser, sha1, utils}; use std::path::Path; pub fn challenge25() { // Now, write the code that allows you to "seek" into the ciphertext, // decrypt, and re-encrypt with different plaintext. Expose this as a // function, like, "edit(ciphertext, key, offset, newtext)". fn edit(ciphertext: &Bytes, key: &Bytes, offset: usize, newtext: &Vec) -> Bytes { let mut plaintext = ctr::decrypt(key, 0, ciphertext); assert!( offset + newtext.len() <= plaintext.len(), "challenge25 - edit - out of bounds" ); plaintext.0[offset..(newtext.len() + offset)].copy_from_slice(&newtext[..]); ctr::encrypt(key, 0, &plaintext) } let cipher = utils::read_base64("data/25.txt"); let key = Bytes::from_utf8("YELLOW SUBMARINE"); let plaintext = ecb::decrypt(&key, &cipher); let key = Bytes::random(16); let nonce: u64 = 0; // otherwise edit would require the nonce too? // Imagine the "edit" function was exposed to attackers by means of an API // call that didn't reveal the key or the original plaintext; the attacker // has the ciphertext and controls the offset and "new text". Recover the // original plaintext. let ciphertext = ctr::encrypt(&key, nonce, &plaintext); let newtext = vec![b'a'; ciphertext.len()]; let cipher_newtext = edit(&ciphertext, &key, 0, &newtext); let keystream = utils::xor(&newtext, &cipher_newtext.0); let recovered_plaintext = Bytes(utils::xor(&keystream, &ciphertext.0)); assert_eq!(plaintext, recovered_plaintext); println!("[okay] Challenge 25: recovered AES CTR plaintext via edit"); // A folkloric supposed benefit of CTR mode is the ability to easily "seek forward" into the // ciphertext; to access byte N of the ciphertext, all you need to be able to do is generate // byte N of the keystream. Imagine if you'd relied on that advice to, say, encrypt a disk. // Answer: you would have to run through the whole cipher stream till you reach the point where // you want to decrypt. } pub fn challenge26() { fn encrypt(input: &str, key: &Bytes) -> Bytes { let mut r = String::new(); for c in input.chars() { assert!(!(c == ';' || c == '='), "encrypt: invalid char {}", c); } r.push_str("comment1=cooking%20MCs;userdata="); r.push_str(input); r.push_str(";comment2=%20like%20a%20pound%20of%20bacon"); let data = Bytes(r.as_bytes().to_vec()); ctr::encrypt(key, 0, &data) } let key = Bytes::random(16); // 0 16 32 48 64 // 0..34..78..bc..f0..34..78..bc..f0..34..78..bc..f0..34..78..bc..f0..34..78..bc..f // comment1=cooking%20MCs;userdata=aaaaaaaaaaaaaaaa;comment2=%20like%20a%20pound%20of%20bacon // comment1=cooking%20MCs;userdata=fobar;admin=true;comment2=%20like%20a%20pound%20of%20bacon let input = "aaaaaaaaaaaaaaaa"; let cipher = encrypt(input, &key); let keystream = utils::xor(&cipher.0[32..48], &Bytes::from_utf8(input).0); let input = "fobar;admin=true"; let mut flipped_cipher = cipher.0[0..32].to_vec(); flipped_cipher.append(&mut utils::xor(&keystream, input.as_bytes())); flipped_cipher.append(&mut cipher.0[48..cipher.len()].to_vec()); let cleartext = ctr::decrypt(&key, 0, &Bytes(flipped_cipher)); let dict = parser::parse_key_value(&cleartext.to_utf8()); let admin_status = dict.get("admin").unwrap(); assert_eq!(admin_status, "true"); println!("[okay] Challenge 26: admin={}", admin_status); } pub fn challenge27() { fn encrypt(key: &Bytes) -> Bytes { // AES-CBC(P_1, P_2, P_3) -> C_1, C_2, C_3 let mut ct = Bytes::from_utf8("comment1=cooking%20MCs;userdata=secretsaucenouse"); ct.pad_pkcs7(16); cbc::encrypt(key, key, &ct) } fn decrypt(key: &Bytes, cipher: &Bytes) -> Result { // The CBC code from exercise 16 encrypts a URL string. Verify each byte // of the plaintext for ASCII compliance (ie, look for high-ASCII // values). Noncompliant messages should raise an exception or return an // error that includes the decrypted plaintext (this happens all the // time in real systems, for what it's worth). let mut cleartext = cbc::decrypt(key, key, cipher); cleartext.remove_pkcs7(16); match std::str::from_utf8(&cleartext.0) { Ok(_) => Ok(cleartext), Err(_) => Err(cleartext), } } fn modify(cipher: &Bytes) -> Bytes { // C_1, C_2, C_3 -> C_1, 0, C_1 let mut c1 = cipher.get_block(0, 16).0; let mut modified = c1.clone(); modified.append(&mut vec![0; 16]); modified.append(&mut c1); Bytes(modified) } fn recover_key(plaintext: &Bytes) -> Bytes { // P'_1 XOR P'_3 let block_size = 16; Bytes(utils::xor( &plaintext.get_block(0, block_size).0, &plaintext.get_block(2, block_size).0, )) } let key = Bytes::random(16); // Use your code to encrypt a message that is at least 3 blocks long: let cipher = encrypt(&key); // Modify the message (you are now the attacker): let cipher = modify(&cipher); let plaintext = decrypt(&key, &cipher); // As the attacker, recovering the plaintext from the error, extract the key: let recovered_key = match plaintext { Ok(plaintext) => panic!("Ok: {:?}", plaintext.to_utf8()), Err(plaintext) => recover_key(&plaintext), }; assert_eq!(key, recovered_key); println!("[okay] Challenge 27: recovered key successfully"); } pub fn challenge28() { let mut sha1 = sha1::Sha1::default(); assert_eq!( Bytes::from_hex("0098ba824b5c16427bd7a1122a5a442a25ec644d"), sha1.hash(&Bytes(vec![b'a'; 64])) ); sha1.reset(); assert_eq!( Bytes::from_hex("ad5b3fdbcb526778c2839d2f151ea753995e26a0"), sha1.hash(&Bytes(vec![b'a'; 128])) ); sha1.reset(); assert_eq!( Bytes::from_hex("7e240de74fb1ed08fa08d38063f6a6a91462a815"), sha1.hash(&Bytes(vec![b'a'; 3])), ); sha1.reset(); assert_eq!( Bytes::from_hex("da39a3ee5e6b4b0d3255bfef95601890afd80709"), sha1.hash(&Bytes(vec![])), ); // Verify that you cannot tamper with the message without breaking the MAC // you've produced, and that you can't produce a new MAC without knowing the // secret key. let mut message = Bytes::from_utf8("love, love, love"); let key = Bytes::from_utf8("kisses!"); let mac = sha1::authenticate(&message, &key); message.flip_bit(2, 3); assert!(!sha1::verify(&message, &key, &mac)); println!("[okay] Challenge 28: implemented SHA-1"); } pub fn challenge29() { fn hash_fixated(bytes: &Bytes, fixture: &Bytes, byte_len: u64) -> Bytes { // Now, take the SHA-1 secret-prefix MAC of the message you want to forge --- this is just // a SHA-1 hash --- and break it into 32 bit SHA-1 registers (SHA-1 calls them "a", "b", // "c", &c). let mut s = sha1::Sha1::default(); let fixate: Vec = fixture .0 .chunks(4) .map(|c| u32::from_be_bytes(c.try_into().unwrap())) .collect(); // Modify your SHA-1 implementation so that callers can pass in new // values for "a", "b", "c" &c (they normally start at magic numbers). // With the registers "fixated", hash the additional data you want to // forge. s.fix(fixate.try_into().unwrap(), byte_len); s.hash(bytes) } // use random let key = Bytes::random_range(2, 64); let message = Bytes::from_utf8( "comment1=cooking%20MCs;userdata=foo;comment2=%20like%20a%20pound%20of%20bacon", ); let mac = sha1::authenticate(&message, &key); assert!(sha1::verify(&message, &key, &mac)); let mut forged_message = vec![]; let mut mac_forged = Bytes(vec![]); for key_len in 1..128 { // get padding for key || orig-message let key_guessed = vec![b'z'; key_len]; // key-guessed let mut bytes = key_guessed.clone(); bytes.append(&mut message.0.clone()); // original-message let s1 = sha1::Sha1::default(); let glue_padding = s1.get_padding(&bytes); // glue-padding // forget MAC via fixture: make sure to fix sha1.h *and* sha1.byte_length let byte_length = (key_guessed.len() + message.len() + glue_padding.len()) as u64; let new_message = b"admin=true".to_vec(); // new-message mac_forged = hash_fixated(&Bytes(new_message.clone()), &mac, byte_length); // forge message: original-message || glue-padding || new-message forged_message = message.0.clone(); forged_message.append(&mut glue_padding.clone()); forged_message.append(&mut new_message.clone()); let r = sha1::verify(&Bytes(forged_message.clone()), &key, &mac_forged); if r { break; } } assert!(sha1::verify(&Bytes(forged_message), &key, &mac_forged)); println!("[okay] Challenge 29: extended SHA-1 keyed message successfully"); } pub fn challenge30() { fn test_md4() { assert_eq!( md4::hash(&Bytes::from_utf8("")), Bytes::from_hex("31d6cfe0d16ae931b73c59d7e0c089c0"), ); assert_eq!( md4::hash(&Bytes::from_utf8("a")), Bytes::from_hex("bde52cb31de33e46245e05fbdbd6fb24"), ); assert_eq!( md4::hash(&Bytes::from_utf8("abc")), Bytes::from_hex("a448017aaf21d8525fc10ae87aa6729d"), ); assert_eq!( md4::hash(&Bytes::from_utf8("abcdefghijklmnopqrstuvwxyz")), Bytes::from_hex("d79e1c308aa5bbcdeea8ed63df412da9"), ); assert_eq!( md4::hash(&Bytes(vec![b'a'; 1337])), Bytes::from_hex("9a4bceae0ae389c4653ad92cfd7bfc3e"), ); } // extend MD4 copy and pasted from SHA-1 #allow!(dont-repeat-yourself) fn hash_fixated(bytes: &Bytes, fixture: &Bytes, byte_len: u64) -> Bytes { let mut m = md4::Md4Core::default(); let fixate: Vec = fixture .0 .chunks(4) .map(|c| u32::from_le_bytes(c.try_into().unwrap())) .collect(); m.fix(fixate.try_into().unwrap(), byte_len); m.hash(bytes) } test_md4(); let key = Bytes::random_range(2, 64); let message = Bytes::from_utf8( "comment1=cooking%20MCs;userdata=foo;comment2=%20like%20a%20pound%20of%20bacon", ); let mac = md4::authenticate(&message, &key); assert!(md4::verify(&message, &key, &mac)); let mut forged_message = vec![]; let mut mac_forged = Bytes(vec![]); for key_len in 1..128 { // get padding for key || orig-message let key_guessed = vec![b'z'; key_len]; // key-guessed let mut bytes = key_guessed.clone(); bytes.append(&mut message.0.clone()); // original-message let md4 = md4::Md4Core::default(); let glue_padding = md4.get_padding(&bytes); // glue-padding // forget MAC via fixture: make sure to fix md4.state *and* md4.byte_length let byte_length = (key_guessed.len() + message.len() + glue_padding.len()) as u64; let new_message = b"admin=true".to_vec(); // new-message mac_forged = hash_fixated(&Bytes(new_message.clone()), &mac, byte_length); // forge message: original-message || glue-padding || new-message forged_message = message.0.clone(); forged_message.append(&mut glue_padding.clone()); forged_message.append(&mut new_message.clone()); let r = md4::verify(&Bytes(forged_message.clone()), &key, &mac_forged); if r { break; } } assert!(md4::verify(&Bytes(forged_message), &key, &mac_forged)); println!("[okay] Challenge 30: implemented and extended MD4 successfully"); } mod challenge31 { use crate::{bytes::Bytes, sha1}; use std::fs; use std::path::Path; use std::{thread, time}; pub fn verify(file: &Path, signature: &[u8], delay: u64) -> bool { // Have the server generate an HMAC key, and then verify that the "signature" on incoming // requests is valid for "file", using the "==" operator to compare the valid MAC for a // file with the "signature" parameter (in other words, verify the HMAC the way any normal // programmer would verify it). let key = Bytes::from_utf8("sosecretbb"); let contents = fs::read_to_string(file); assert!(contents.is_ok(), "Could not read: {}", file.display()); let contents = Bytes(contents.unwrap().as_bytes().to_vec()); insecure_compare(&sha1::hmac_sha1(&key, &contents).0, signature, delay) } fn insecure_compare(a: &[u8], b: &[u8], delay: u64) -> bool { // Write a function, call it "insecure_compare", that implements the == operation by doing // byte-at-a-time comparisons with early exit (ie, return false at the first non-matching // byte). let delay = time::Duration::from_millis(delay); if a.len() != b.len() { return false; } for (a, b) in a.iter().zip(b.iter()) { if a != b { return false; } // In the loop for "insecure_compare", add a 50ms sleep after each byte. thread::sleep(delay); } true } pub fn _attack(file: &Path, delay: u64) -> Bytes { const BLOCK_SIZE: usize = 20; let mut sig = vec![0x0; BLOCK_SIZE]; for i in 0..BLOCK_SIZE { let mut max_tuple: (u128, u8) = (u128::MIN, 0); for c in 0_u8..=255_u8 { let now = time::Instant::now(); sig[i] = c; verify(file, &sig, delay); let elapsed = now.elapsed().as_micros(); if elapsed > max_tuple.0 { max_tuple = (elapsed, c); } } sig[i] = max_tuple.1; } Bytes(sig) } } pub fn challenge31() { let key = Bytes::from_utf8("YELLOW SUBMARINE"); let message = Bytes::from_utf8("Attact at dawn after tomorrow when it's cold inside."); assert_eq!( sha1::hmac_sha1(&key, &message), Bytes::from_hex("8232f3d05afb6bce7e09fe764885cc158e435e36") ); let path = Path::new("data/12.txt"); let expected_sig = Bytes::from_hex("62f4527ea6cb716d0ad1ca0fc69135a49bc2d138"); assert!(challenge31::verify(path, &expected_sig.0, 0), "Invalid"); // Don't attack because it interrupts the flow of the other challenges by taking long. // const DELAY: u64 = 20; // let signature = challenge31::_attack(path, DELAY); // assert_eq!(expected_sig, signature, "Recovery of HMAC-SHA1 failed"); println!("[okay] Challenge 31: recovered HMAC-SHA1 via timing attack"); } pub fn challenge32() { use std::time; pub fn _attack(file: &Path, delay: u64) -> Bytes { const CYCLES_PER_BYTE: usize = 30; const BLOCK_SIZE: usize = 20; let mut sig = vec![0x0; BLOCK_SIZE]; for i in 0..BLOCK_SIZE { let mut max_tuple: (u128, u8) = (u128::MIN, 0); for c in 0_u8..=255_u8 { let mut cummulated_us = 0; for _ in 0..CYCLES_PER_BYTE { let now = time::Instant::now(); sig[i] = c; challenge31::verify(file, &sig, delay); cummulated_us += now.elapsed().as_micros(); } if cummulated_us > max_tuple.0 { max_tuple = (cummulated_us, c); } } sig[i] = max_tuple.1; } Bytes(sig) } // Don't attack because it interrupts the flow of the other challenges by taking long. // const DELAY: u64 = 1; // let path = Path::new("data/12.txt"); // let expected_sig = Bytes::from_hex("62f4527ea6cb716d0ad1ca0fc69135a49bc2d138"); // assert!( // challenge31::_attack(path, DELAY) != expected_sig, // "Recovery was successful" // ); // assert!( // _attack(path, DELAY) == expected_sig, // "Recovery was not successful" // ); println!("[okay] Challenge 32: recovered HMAC-SHA1 with slightly less artificial timing leak"); }