Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions submissions/challenge1/mikolaj_kot/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "cryptopals"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0" # Great for simple error handling
base64 = "0.21"
clap = { version = "4.4", features = ["derive"] }
hex = "0.4"

38 changes: 38 additions & 0 deletions submissions/challenge1/mikolaj_kot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Cryptopals Solutions (Rust)

A "roll-your-own-crypto" library and CLI tool implementing solutions to the [Cryptopals Crypto Challenges](https://cryptopals.com/). This project demonstrates low-level cryptographic concepts including XOR ciphers, frequency analysis, and SHA-1 implementation from scratch.

## Getting Started

Ensure you have Rust and Cargo installed.

```bash
# Build the project
cargo build --release

# View help menu
cargo run -- --help
```

## Assignment Demos

To run specific challenge solutions, use the `demo` subcommand. The ID format is sXcY (set X, challenge Y).

### Recommended Demos for Evaluation

| Difficulty | ID | Description | Command |
| :--- | :--- | :--- | :--- |
| **Easy** | `s1c1` | Hex to Base64 Conversion | `cargo run -- demo s1c1` |
| **Medium** | `s1c4` | Detect Single-Byte XOR | `cargo run -- demo s1c4` |
| **Hard** | `s4c29`| SHA-1 Length Extension Attack | `cargo run -- demo s4c29` |

### Listing all demos

To see all implemented challenges:
```bash
cargo run -- demo --help
```

### Notes

I tried to make this code extendible so that I can easily update it with solutions to other challenges in the future. The code is still not properly commented, some of the values used should be randomised etc. But the challenges are solved and some functionality for custom usage has been added.
327 changes: 327 additions & 0 deletions submissions/challenge1/mikolaj_kot/data/4.txt

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions submissions/challenge1/mikolaj_kot/src/cracker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use crate::utils::{ hamming_distance, score_english_text, transpose_bytes, };
use crate::crypto::{ repeating_xor };
use std::cmp::min;

const MAX_KEY_SIZE: usize = 40;

pub struct CrackResult {
pub key: Vec<u8>,
pub plaintext: Vec<u8>,
pub score: f64,
}

#[derive(Copy, Clone, Debug)]
pub enum Cracker {
SingleByteXor,
RepeatingKeyXor,
}

impl Cracker {
pub fn crack(&self, ciphertext: &[u8], num_of_results: usize) -> Vec<CrackResult> {
let results: Vec<CrackResult> = match self {
Self::SingleByteXor => self.solve_single_byte(ciphertext, num_of_results),
Self::RepeatingKeyXor => self.solve_repeating(ciphertext, num_of_results),
};

results
}

pub fn print_results(&self, results: Vec<CrackResult>) {
println!("--- TOP {} CANDIDATES ---", results.len());
for result in results {
println!("Key: {:?} | Message: {}\n | Score: {:.2}",
result.key, String::from_utf8_lossy(&result.plaintext), result.score);
};
}

fn solve_single_byte(&self, ciphertext: &[u8], num_of_results: usize) -> Vec<CrackResult> {
let mut results: Vec<CrackResult> = Vec::new();

for key in u8::MIN..=u8::MAX {
let decrypted_bytes: Vec<u8> = ciphertext.iter().map(|&b| b ^ key).collect();
let score = score_english_text(&decrypted_bytes);
results.push(CrackResult { key: vec![key], plaintext: decrypted_bytes, score });
}

results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());

results.into_iter().take(num_of_results as usize).collect()
}

fn solve_repeating(&self, ciphertext: &[u8], num_of_results: usize) -> Vec<CrackResult> {
let mut results: Vec<CrackResult> = Vec::new();

let mut distances: Vec<(usize, f32)> = Vec::with_capacity(MAX_KEY_SIZE - 2);

for keysize in 2..min(MAX_KEY_SIZE, ciphertext.len() / 4) {
let block1 = &ciphertext[0..keysize];
let block2 = &ciphertext[keysize..keysize * 2];
let block3 = &ciphertext[keysize * 2..keysize * 3];
let block4 = &ciphertext[keysize * 3..keysize * 4];

let d12 = hamming_distance(block1, block2);
let d13 = hamming_distance(block1, block3);
let d14 = hamming_distance(block1, block4);
let d23 = hamming_distance(block2, block3);
let d24 = hamming_distance(block2, block4);
let d34 = hamming_distance(block3, block4);

let total_dist = d12 + d13 + d14 + d23 + d24 + d34;
let normalised = (total_dist as f32 / 6.0) / keysize as f32;

distances.push((keysize, normalised));
}

distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());

for (keysize, _dist) in distances.iter().take(num_of_results) {
let mut key_candidate: Vec<u8> = Vec::with_capacity(*keysize);

let transposed_blocks = transpose_bytes(ciphertext, *keysize);
for block in transposed_blocks {
let byte = self.solve_single_byte(&block, 1)[0].key[0];
key_candidate.push(byte);
}

let cracked_plaintext = repeating_xor(ciphertext, &key_candidate);
let score = score_english_text(&cracked_plaintext);
results.push(CrackResult {
key: (key_candidate),
plaintext: (cracked_plaintext),
score,
})
}

results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());

results
}
}
228 changes: 228 additions & 0 deletions submissions/challenge1/mikolaj_kot/src/crypto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
use anyhow::{Result, bail};
use clap::{ValueEnum};

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum XorType {
Single,
Fixed,
Repeating,
Otp,
}

pub fn single_xor(c_bytes: &[u8], k: u8) -> Vec<u8> {
c_bytes.iter().map(|&b| b ^ k).collect()
}

pub fn fixed_xor(c_bytes: &[u8], k_bytes: &[u8]) -> Result<Vec<u8>> {
if c_bytes.len() != k_bytes.len() {
bail!("Fixed XOR requires buffers of equal length")
}
Ok(c_bytes.iter().zip(k_bytes).map(|(&b, &k)| b ^ k).collect())
}

pub fn otp(c_bytes: &[u8], k_bytes: &[u8]) -> Result<Vec<u8>> {
if c_bytes.len() > k_bytes.len() {
bail!("OTP Error: Key is shorter than message. Insecure.")
}
Ok(c_bytes.iter().zip(k_bytes).map(|(&b, &k)| b ^ k).collect())
}

pub fn repeating_xor(c_bytes: &[u8], k_bytes: &[u8]) -> Vec<u8> {
c_bytes.iter().zip(k_bytes.iter().cycle()).map(|(b, k)| b ^ k).collect()
}

pub fn secret_prefix_mac(key: &[u8], message: &[u8]) -> [u8; 20] {
let mut input = Vec::with_capacity(key.len() + message.len());
input.extend_from_slice(key);
input.extend_from_slice(message);

sha1(&input)
}

pub fn verify_mac(key: &[u8], message: &[u8], provided_mac: &[u8; 20]) -> bool {
let calculated_mac = secret_prefix_mac(key, message);

// INSECURE: In production, use constant time compariosn
calculated_mac == *provided_mac
}

pub fn sha1_modified(message: &[u8], registers: &[u32; 5], total_bytes_processed: u64) -> [u8; 20] {
let mut buffer = message.to_vec();

// Initialise variables:
let mut h: [u32; 5] = registers.clone();
let ml: u64 = (total_bytes_processed + message.len() as u64) * 8;

// Pre-processing
buffer.push(0x80);
let rem = buffer.len() % 64;
let pad_len = if rem <= 56 {
56 - rem
} else {
64 + 56 - rem
};

// append pad_len zero bytes
buffer.extend(std::iter::repeat(0).take(pad_len));
buffer.extend_from_slice(&ml.to_be_bytes());

let mut w = [0u32; 80];
for chunk in buffer.chunks(64) {
for i in 0..16 {
let j = i * 4;
w[i] = (chunk[j] as u32) << 24 |
(chunk[j + 1] as u32) << 16 |
(chunk[j + 2] as u32) << 8 |
(chunk[j + 3] as u32);
}

for i in 16..80 {
w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
}

let (mut a, mut b, mut c, mut d, mut e) = (h[0], h[1], h[2], h[3], h[4]);
for i in 0..80 {
let (f, k) = match i {
0..20 => { ((b & c) | ((!b) & d), 0x5A827999) },
20..40 => { (b ^ c ^ d, 0x6ED9EBA1) },
40..60 => { ((b & c) | (b & d) | (c & d), 0x8F1BBCDC) },
60..80 => { (b ^ c ^ d, 0xCA62C1D6) },
_ => unreachable!(),
};

let temp = a.rotate_left(5)
.wrapping_add(f)
.wrapping_add(e)
.wrapping_add(k)
.wrapping_add(w[i]);

(e, d, c, b, a) = (d, c, b.rotate_left(30), a, temp);
}

let t: [u32; 5] = [a, b, c, d, e];
for i in 0..5 {
h[i] = h[i].wrapping_add(t[i]);
}

}

let mut digest = [0u8; 20];
for i in 0..5 {
digest[i * 4..(i + 1) * 4].copy_from_slice(&h[i].to_be_bytes());
}

digest
}

pub fn sha1(message: &[u8]) -> [u8; 20] {
let mut buffer = message.to_vec();

// Initialise variables:
let mut h: [u32; 5] = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0];
let ml: u64 = (message.len() as u64) * 8; // message length in bits

/*Pre-processing:
append the bit '1' to the message.
append 0 ≤ k < 512 bits '0', such that the resulting message length in bits
is congruent to −64 ≡ 448 (mod 512)
append ml, the original message length in bits, as a 64-bit big-endian integer.*/
buffer.push(0x80);
let rem = buffer.len() % 64;
let pad_len = if rem <= 56 {
56 - rem
} else {
64 + 56 - rem
};

// append pad_len zero bytes
buffer.extend(std::iter::repeat(0).take(pad_len));
buffer.extend_from_slice(&ml.to_be_bytes());

let mut w = [0u32; 80];
for chunk in buffer.chunks(64) {
for i in 0..16 {
let j = i * 4;
w[i] = (chunk[j] as u32) << 24 |
(chunk[j + 1] as u32) << 16 |
(chunk[j + 2] as u32) << 8 |
(chunk[j + 3] as u32);
}

for i in 16..80 {
w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
}

let (mut a, mut b, mut c, mut d, mut e) = (h[0], h[1], h[2], h[3], h[4]);
for i in 0..80 {
let (f, k) = match i {
0..20 => { ((b & c) | ((!b) & d), 0x5A827999) },
20..40 => { (b ^ c ^ d, 0x6ED9EBA1) },
40..60 => { ((b & c) | (b & d) | (c & d), 0x8F1BBCDC) },
60..80 => { (b ^ c ^ d, 0xCA62C1D6) },
_ => unreachable!(),
};

let temp = a.rotate_left(5)
.wrapping_add(f)
.wrapping_add(e)
.wrapping_add(k)
.wrapping_add(w[i]);

(e, d, c, b, a) = (d, c, b.rotate_left(30), a, temp);
}

let t: [u32; 5] = [a, b, c, d, e];
for i in 0..5 {
h[i] = h[i].wrapping_add(t[i]);
}

}

let mut digest = [0u8; 20];
for i in 0..5 {
digest[i * 4..(i + 1) * 4].copy_from_slice(&h[i].to_be_bytes());
}

digest
}

pub fn md_padding(message: &[u8]) -> Vec<u8> {
let mut padding = Vec::new();
let ml = (message.len() as u64) * 8;

// Append the '1' bit (0x80 byte)
padding.push(0x80);

// Calculate zero padding
let current_len = message.len() + 1;
let rem = current_len % 64;

let pad_len = if rem <= 56 {
56 - rem
} else {
64 + 56 - rem
};

// Append zeros
padding.extend(std::iter::repeat(0).take(pad_len));

// Append length (Big Endian)
padding.extend_from_slice(&ml.to_be_bytes());

padding
}

#[test]
fn test_md_padding() {
let msg = b"john frusciante";
let padding = md_padding(msg);

assert_eq!(padding[0], 0x80);

let total_len = msg.len() + padding.len();
assert_eq!(total_len % 64, 0);

let suffix = &padding[padding.len()-8..];
let len_val = u64::from_be_bytes(suffix.try_into().unwrap());
assert_eq!(len_val, 48);
}
Loading