Files
cryptopals/src/set3.rs

273 lines
9.9 KiB
Rust

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<Bytes> {
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<u8> = 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<Bytes> {
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<u8>) -> HashMap<u8, RefCell<HashSet<u8>>> {
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<u8> {
let mut letters: Vec<u8> = (0..255_u8).filter(u8::is_ascii_alphabetic).collect();
for b in additional.as_bytes() {
letters.push(*b);
}
letters
}
fn attack(ciphers: Vec<Bytes>) -> Vec<RefCell<Vec<u8>>> {
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<Option<u8>> = ciphers
.iter()
.map(|c| {
if c.len() > byte_index {
Some(c.0[byte_index])
} else {
None
}
})
.collect();
let mut possible_chars: Vec<HashSet<u8>> = 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<u8> = 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<RefCell<Vec<u8>>>) {
// 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<Bytes> = 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");
}