From ad3a292e0ef3dd1f91103cddee9b0ca458f66d38 Mon Sep 17 00:00:00 2001 From: Felix Martin Date: Fri, 2 Sep 2022 12:28:57 -0400 Subject: [PATCH] Implement challenge 34 --- src/bytes.rs | 2 +- src/rsa.rs | 32 +++++--- src/set5.rs | 225 +++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 224 insertions(+), 35 deletions(-) diff --git a/src/bytes.rs b/src/bytes.rs index 96b522e..95a52b8 100644 --- a/src/bytes.rs +++ b/src/bytes.rs @@ -83,7 +83,7 @@ impl Bytes { pub fn pad_pkcs7(&mut self, block_size: usize) { let Bytes(v) = self; - let padding_value = (block_size - v.len() % block_size) + let padding_value = (block_size - (v.len() % block_size)) .try_into() .expect("Padding value has to be an u8"); for _ in 0..padding_value { diff --git a/src/rsa.rs b/src/rsa.rs index 95c336e..d482f4a 100644 --- a/src/rsa.rs +++ b/src/rsa.rs @@ -1,12 +1,26 @@ -pub fn expmod(base: u32, exp: u32, m: u32) -> u32 { - if exp == 0 { - return 1; - } +use num_bigint::BigUint; +use num_bigint::RandBigInt; - if exp % 2 == 0 { - let s = expmod(base, exp / 2, m); - (s * s) % m - } else { - (expmod(base, exp - 1, m) * base) % m +#[derive(Clone)] +pub struct PublicKey(pub BigUint); + +#[derive(Clone)] +pub struct PrivateKey(pub BigUint); + +#[derive(Clone)] +pub struct Keypair { + pub private: PrivateKey, + pub public: PublicKey, +} + +impl Keypair { + pub fn make(p: &BigUint, g: &BigUint) -> Self { + let mut rng = rand::thread_rng(); + let private = rng.gen_biguint_below(p); + let public = g.modpow(&private, p); + Self { + private: PrivateKey(private), + public: PublicKey(public), + } } } diff --git a/src/set5.rs b/src/set5.rs index 4b4ce21..470b715 100644 --- a/src/set5.rs +++ b/src/set5.rs @@ -1,12 +1,41 @@ +use crate::bytes::Bytes; +use crate::cbc; use crate::rsa; -use num_bigint::{BigUint, RandBigInt, ToBigUint}; +use crate::sha1::Sha1; +use num_bigint::ToBigUint; use rand::Rng; -use std::fs; + +mod challenge33 { + use num_bigint::BigUint; + use std::fs; + + pub fn load_large_prime() -> BigUint { + let s = fs::read_to_string("data/33.txt").expect("run from git root dir"); + let b: Vec = s + .chars() + .filter(|c| *c != '\n') + .map(|c| c.to_digit(16).unwrap().try_into().unwrap()) + .collect(); + BigUint::from_radix_be(&b, 16).unwrap() + } +} pub fn challenge33() { - #![allow(non_snake_case)] + fn expmod(base: u32, exp: u32, m: u32) -> u32 { + if exp == 0 { + return 1; + } + + if exp % 2 == 0 { + let s = expmod(base, exp / 2, m); + (s * s) % m + } else { + (expmod(base, exp - 1, m) * base) % m + } + } fn simple_diffie_hellman() { + #![allow(non_snake_case)] let mut rng = rand::thread_rng(); // Set a variable "p" to 37 and "g" to 5. @@ -16,18 +45,18 @@ pub fn challenge33() { // Generate "a", a random number mod 37. Now generate "A", which is "g" raised to the "a" // power mode 37 --- A = (g**a) % p. let a: u32 = rng.gen::() % p; - let A = rsa::expmod(g, a, p); + let A = expmod(g, a, p); // Do the same for "b" and "B". let b: u32 = rng.gen::() % p; - let B = rsa::expmod(g, b, p); + let B = expmod(g, b, p); // "A" and "B" are public keys. Generate a session key with them; set "s" to "B" raised to // the "a" power mod 37 --- s = (B**a) % p. - let s = rsa::expmod(B, a, p); + let s = expmod(B, a, p); // Do the same with A**b, check that you come up with the same "s". - let s_ = rsa::expmod(A, b, p); + let s_ = expmod(A, b, p); assert_eq!(s, s_, "crypto is broken"); // To turn "s" into a key, you can just hash it to create 128 bits of key material (or @@ -35,25 +64,14 @@ pub fn challenge33() { } fn serious_diffie_hellman() { - let mut rng = rand::thread_rng(); - - let s = fs::read_to_string("data/33.txt").expect("run from git root dir"); - let b: Vec = s - .chars() - .filter(|c| *c != '\n') - .map(|c| c.to_digit(16).unwrap().try_into().unwrap()) - .collect(); - let p = BigUint::from_radix_be(&b, 16).unwrap(); + let p = challenge33::load_large_prime(); let g = 2_u8.to_biguint().unwrap(); - let a = rng.gen_biguint_below(&p); - let A = g.modpow(&a, &p); + let a = rsa::Keypair::make(&p, &g); + let b = rsa::Keypair::make(&p, &g); - let b = rng.gen_biguint_below(&p); - let B = g.modpow(&b, &p); - - let s = B.modpow(&a, &p); - let s_ = A.modpow(&b, &p); + let s = b.public.0.modpow(&a.private.0, &p); + let s_ = a.public.0.modpow(&b.private.0, &p); assert_eq!(s, s_, "crypto is broken"); } @@ -62,6 +80,163 @@ pub fn challenge33() { println!("[okay] Challenge 33: implemented Diffie-Hellman"); } -pub fn challenge34() { - println!("[xxxx] Challenge 34: tbd"); +mod challenge34 { + use crate::bytes::Bytes; + use crate::cbc; + use crate::rsa; + use crate::sha1::Sha1; + use num_bigint::BigUint; + + pub struct Bot { + p: BigUint, + keypair: rsa::Keypair, + pub s: Option, + pub key: Option, + } + + impl Bot { + pub fn new(p: BigUint, g: &BigUint) -> Self { + let keypair = rsa::Keypair::make(&p, g); + Bot { + p: p, + keypair: keypair, + s: None, + key: None, + } + } + + pub fn get_public_key(&self) -> rsa::PublicKey { + self.keypair.public.clone() + } + + pub fn exchange_keys(&mut self, public: &rsa::PublicKey) -> rsa::PublicKey { + let s = public.0.modpow(&self.keypair.private.0, &self.p); + self.s = Some(s); + self.make_encryption_key(); + self.keypair.public.clone() + } + + fn make_encryption_key(&mut self) { + assert!(self.s.is_some(), "keys have not been exchanged"); + if self.key.is_some() { + return; + } + let mut sha1 = Sha1::default(); + let key = &sha1.hash(&Bytes(self.s.clone().unwrap().to_bytes_be())).0[0..16]; + self.key = Some(Bytes(key.to_vec())); + } + + pub fn encrypt(&mut self, message: &Bytes) -> Bytes { + assert!(self.key.is_some(), "keys have not been exchanged"); + + const BLOCK_SIZE: usize = 16; + let key = self.key.as_ref().unwrap(); + let mut iv = Bytes::random(16); + let mut message = Bytes(message.0.clone()); + message.pad_pkcs7(BLOCK_SIZE); + + // AES-CBC(SHA1(s)[0:16], iv=random(16), msg) + iv + let mut cipher = cbc::encrypt(&key, &iv, &message).0; + cipher.append(&mut iv.0); + Bytes(cipher) + } + + pub fn decrypt(&mut self, cipher: &Bytes) -> Bytes { + const BLOCK_SIZE: usize = 16; + assert!(self.key.is_some(), "keys have not been exchanged"); + + let key = self.key.as_ref().unwrap(); + let cipher_len = cipher.len(); + + let iv = Bytes(cipher.0[(cipher_len - BLOCK_SIZE)..cipher_len].to_vec()); + let cipher = Bytes(cipher.0[0..cipher_len - BLOCK_SIZE].to_vec()); + let mut message = cbc::decrypt(&key, &iv, &cipher); + message.remove_pkcs7(BLOCK_SIZE); + message + } + } +} + +pub fn challenge34() { + fn echo() { + let p = challenge33::load_large_prime(); + let g = 2_u8.to_biguint().unwrap(); + + let mut a = challenge34::Bot::new(p.clone(), &g); + + // A->B: Send "p", "g", "A" + let mut b = challenge34::Bot::new(p.clone(), &g); + + // B->A: Send "B" + a.exchange_keys(&b.exchange_keys(&a.get_public_key())); + assert_eq!(a.s, b.s, "crypto is broken"); + + // A->B: Send AES-CBC(SHA1(s)[0:16], iv=random(16), msg) + iv + let message_a = Bytes::from_utf8("Will this work? Let's make it longer."); + let cipher_a = a.encrypt(&message_a); + + // B->A: Send AES-CBC(SHA1(s)[0:16], iv=random(16), A's msg) + iv + let message_b = b.decrypt(&cipher_a); + let cipher_b = b.encrypt(&message_b); + let roundtrip = a.decrypt(&cipher_b); + assert_eq!(message_a, roundtrip, "we broke it fam"); + } + + fn echo_mitm() { + let p = challenge33::load_large_prime(); + let g = 2_u8.to_biguint().unwrap(); + let p_public = rsa::PublicKey(p.clone()); + + // A->M Send "p", "g", "A" + let mut a = challenge34::Bot::new(p.clone(), &g); + let mut m = challenge34::Bot::new(p.clone(), &g); + m.exchange_keys(&a.get_public_key()); + + // M->B Send "p", "g", "p" + // B->M Send "B" + let mut b = challenge34::Bot::new(p.clone(), &g); + b.exchange_keys(&p_public); + + // M->A Send "p" + a.exchange_keys(&p_public); + + // A->M Send AES-CBC(SHA1(s)[0:16], iv=random(16), msg) + iv + // M->B Relay that to B + let message_a = Bytes::from_utf8("Will this work again? I don't doubt it."); + let cipher_a = a.encrypt(&message_a); + + // B->M Send AES-CBC(SHA1(s)[0:16], iv=random(16), A's msg) + iv + let message_b = b.decrypt(&cipher_a); + let cipher_b = b.encrypt(&message_b); + + // M->A Relay that to A + let roundtrip = a.decrypt(&cipher_b); + assert_eq!(message_a, roundtrip, "we broke it fam"); + + let restored_a = attack(&cipher_a); + let restored_b = attack(&cipher_b); + assert_eq!(message_a, restored_a, "attack didn't work"); + assert_eq!(message_b, restored_b, "attack didn't work"); + } + + fn attack(cipher: &Bytes) -> Bytes { + const BLOCK_SIZE: usize = 16; + + // Do the DH math on this quickly to see what that does to the predictability of the key. + // Math: with p = A; s = (A ** b) % p becomes (p ** b) % p = 0 => s = 0 + let s = 0_u8.to_biguint().unwrap(); + let mut sha1 = Sha1::default(); + let key = Bytes(sha1.hash(&Bytes(s.to_bytes_be())).0[0..16].to_vec()); + + let cipher_len = cipher.len(); + let iv = Bytes(cipher.0[(cipher_len - BLOCK_SIZE)..cipher_len].to_vec()); + let cipher = Bytes(cipher.0[0..cipher_len - BLOCK_SIZE].to_vec()); + let mut message = cbc::decrypt(&key, &iv, &cipher); + message.remove_pkcs7(BLOCK_SIZE); + message + } + + echo(); + echo_mitm(); + println!("[okay] Challenge 34: implement MITM key-fixing attack on DH"); }