cryptopals/src/set4.rs

422 lines
16 KiB
Rust

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<u8>) -> 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<Bytes, Bytes> {
// 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<u32> = 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<u32> = 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");
}