diff --git a/module2/ciphers/Cargo.toml b/module2/ciphers/Cargo.toml new file mode 100644 index 0000000..8737114 --- /dev/null +++ b/module2/ciphers/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ciphers" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.3.17", features = ["derive"] } +rand = "0.8.5" + +[lib] +name = "cipher_makers" +path = "src/lib.rs" \ No newline at end of file diff --git a/module2/ciphers/Makefile b/module2/ciphers/Makefile new file mode 100644 index 0000000..4daa6f8 --- /dev/null +++ b/module2/ciphers/Makefile @@ -0,0 +1,38 @@ +SHELL := /bin/bash +.PHONY: help + +help: + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +clean: ## Clean the project using cargo + cargo clean + +build: ## Build the project using cargo + cargo build + +run: ## Run the project using cargo + cargo run + +test: ## Run the tests using cargo + cargo test + +lint: ## Run the linter using cargo + @rustup component add clippy 2> /dev/null + cargo clippy + +format: ## Format the code using cargo + @rustup component add rustfmt 2> /dev/null + cargo fmt + +release: + cargo build --release + +all: format lint test run + +bump: ## Bump the version of the project + @echo "Current version is $(shell cargo pkgid | cut -d# -f2)" + @read -p "Enter the new version: " version; \ + updated_version=$$(cargo pkgid | cut -d# -f2 | sed "s/$(shell cargo pkgid | cut -d# -f2)/$$version/"); \ + sed -i -E "s/^version = .*/version = \"$$updated_version\"/" Cargo.toml + @echo "Version bumped to $$(cargo pkgid | cut -d# -f2)" + rm Cargo.toml-e \ No newline at end of file diff --git a/module2/ciphers/src/decoder_ring.rs b/module2/ciphers/src/decoder_ring.rs new file mode 100644 index 0000000..87a8c76 --- /dev/null +++ b/module2/ciphers/src/decoder_ring.rs @@ -0,0 +1,115 @@ +use std::collections::HashMap; + +fn gen_counts() -> HashMap { + // Reference letter frequencies in English + let mut eng_freq: HashMap = HashMap::new(); + + // Accounts for 80% of all letters in English + eng_freq.insert('e', 12.7); + eng_freq.insert('t', 9.1); + eng_freq.insert('a', 8.2); + eng_freq.insert('o', 7.5); + eng_freq.insert('i', 7.0); + eng_freq.insert('n', 6.7); + eng_freq.insert('s', 6.3); + eng_freq.insert('h', 6.1); + eng_freq.insert('r', 6.0); + eng_freq.insert('d', 4.3); + + eng_freq +} + +fn stats_analysis(text: &str) -> Vec<(char, u32, f32, Option, f32)> { + let mut counts: HashMap = HashMap::new(); + + for c in text.chars() { + *counts.entry(c).or_insert(0) += 1; + } + + let total: u32 = counts.values().sum(); + + let eng_freq_map = gen_counts(); + let eng_freq_map: HashMap = eng_freq_map.iter().map(|(k, v)| (*k, *v)).collect(); + + let mut results = Vec::new(); + + for (letter, count) in &counts { + let freq = (*count as f32 / total as f32) * 100.0; + let eng_freq = eng_freq_map.get(&letter.to_ascii_lowercase()).cloned(); + + let eng_freq_diff = eng_freq.map_or(0.0, |f| (freq - f).abs()); + + results.push((*letter, *count, freq, eng_freq, eng_freq_diff)); + } + results +} + +pub fn print_stats_analysis(text: &str) { + let stats = stats_analysis(text); + for (letter, count, freq, eng_freq, eng_freq_diff) in stats { + println!( + "{}: {} ({}%), English Freq: {} ({}%)", + letter, + count, + freq, + eng_freq.unwrap_or(0.0), + eng_freq_diff + ); + } +} + +pub fn decrypt(text: &str, shift: u8) -> String { + let mut result = String::new(); + + for c in text.chars() { + if c.is_ascii_alphabetic() { + let base = if c.is_ascii_lowercase() { b'a' } else { b'A' }; + let offset = (c as u8 - base + shift) % 26; + result.push((base + offset) as char); + } else { + result.push(c); + } + } + + result +} + +/* +Guess Shift: + +First, uses statistical analysis to determine the most likely shift. +Then, uses the most likely shift to decrypt the message. +Accepts: + * text: the message to decrypt + * depth: the number of shifts to try +Returns: + * depth: the number of shifts to tried + * shift: the most likely shift + * decrypted: the decrypted message +*/ + +pub fn guess_shift(text: &str, depth: u8) -> (u8, u8, String, f32) { + let mut max_score = 0.0; + let mut best_shift = 0; + let mut decrypted = String::new(); + + for shift in 0..depth { + let decrypted_text = decrypt(text, shift); + let stats = stats_analysis(&decrypted_text); + + let mut score = 0.0; + for (_, _, freq, eng_freq, eng_freq_diff) in stats { + if let Some(eng_freq) = eng_freq { + score += (1.0 - eng_freq_diff / eng_freq) * freq; + } + } + println!("Shift: {}, Score: {}", shift, score); + if score > max_score { + max_score = score; + best_shift = shift; + decrypted = decrypted_text; + } + } + + (depth, best_shift, decrypted, max_score) +} \ No newline at end of file diff --git a/module2/ciphers/src/lib.rs b/module2/ciphers/src/lib.rs new file mode 100644 index 0000000..c322fdd --- /dev/null +++ b/module2/ciphers/src/lib.rs @@ -0,0 +1,134 @@ +use rand::Rng; +use std::collections::HashMap; + + +/* +This code defines two functions: encrypt and decrypt. +The encrypt function takes a plaintext string and a shift value, and returns the ciphertext string. The decrypt function takes a ciphertext string and a shift value, +and returns the plaintext string. + +*/ + +pub fn encrypt(text: &str, shift: u8) -> String { + let mut result = String::new(); + for c in text.chars() { + if c.is_ascii_alphabetic() { + let base = if c.is_ascii_lowercase() { b'a' } else { b'A' }; + let offset = (c as u8 - base + shift) % 26; + result.push((base + offset) as char); + } else { + result.push(c); + } + } + result +} + +pub fn decrypt(text: &str, shift: u8) -> String { + encrypt(text, 26 - shift) +} + +/* + This code defines two functions: get_homophones and homophonic_cipher. + The get_homophones function generates a random number of random lowercase characters. + The homophonic_cipher function takes a plaintext string and generates a homophonic cipher based + on the homophones mapping. + + * Generates a list of random homophones for each lowercase letter in the + * English alphabet. Maps each character in the plaintext to one of its + * random homophones to create the cipher text. Prints the plaintext, + * cipher text, and homophonic mapping. Returns the cipher text and + * homophonic mapping. + * + * + * Here is an example: + * Plaintext: the quick brown fox jumps over the lazy dog + * Ciphertext: acrsalgzuwxsgpeqqrjrnekrwvnnwdgfuqn + * Mapping: { + * 't': ['q', 'a', 'v'], 'y': ['f', 's'], 'q': ['s', 'u'], + * 'l': ['w', 'z', 'o'], 's': ['i', 'w', 'n'], 'b': ['u', 'f'], + * 'h': ['n', 'n', 'c'], 'k': ['z', 'r'], 'j': ['s', 'w', 'q'], + * 'x': ['g', 'g', 'q'], 'i': ['l', 'k'], 'g': ['n', 'g'], + * 'm': ['s', 'j', 'w'], 'p': ['k', 'r'], 'a': ['d', 'm', 'w'], + * 'r': ['w', 'o', 'o'], 'o': ['q', 'x', 'e'], 'e': ['n', 'r'], + * 'f': ['i', 'p', 'e'], 'c': ['g', 'z'], 'u': ['a', 'd', 'r'], + * 'v': ['h', 'f', 'k'], 'd': ['s', 'r', 'u'], 'n': ['d', 'g', 'l'], + * 'w': ['s', 'c'], 'z': ['g', 'b'] + * } + * The mapping {'t': ['q', 'a', 'v'], ...} is a part of the homophonic + * cipher mapping from plaintext characters to their cipher characters. + * + + * In this specific example, the plaintext character 't' can be represented + * in the ciphertext by either 'q', 'a', or 'v'. This introduces ambiguity + * into the encryption, which makes the homophonic cipher harder to break + * compared to simple substitution ciphers. + * + * The homophonic cipher is more secure than a simple substitution cipher, + * it is still not secure for serious cryptographic uses. + * + * Given the ciphertext and the mapping, you can reverse-engineer the + * plaintext. Let's start with the first three characters of the + * ciphertext: 'a', 'c', and 'r'. + * + * 'a': Looking at the mapping, you can see that 'a' can be a cipher for + * 'u' or 't', as 'u' and 't' have 'a' in their list of homophones. So + * the possible plaintext letters for 'a' are 'u' and 't'. + * + * 'c': Looking at the mapping again, 'c' can be a cipher for 'h' or 'w' + * since 'h' and 'w' have 'c' in their list of homophones. So the + * possible plaintext letters for 'c' are 'h' and 'w'. + * + * 'r': 'r' can be a cipher for 'e', 'o', or 't' since 'e', 'o', and 't' + * have 'r' in their list of homophones. So the possible plaintext + * letters for 'r' are 'e', 'o', and 't'. + * + * So the first three characters of the plaintext could be any combination + * of the possible plaintext letters for 'a', 'c', and 'r'. For example, + * it could be 'u', 'h', 'e', or 't', 'w', 'o', etc. + * + * Remember, homophonic ciphers are designed to provide many possible + * plaintexts for a single ciphertext, which makes it much harder to crack + * the code without having more information. One possible approach to + * decode the message is using a frequency analysis or a known-plaintext + * attack if you have a part of the original message. Another way is to + * use the context of the message if it's known. +*/ +fn get_homophones() -> Vec { + let mut rng = rand::thread_rng(); + let homophones: Vec = (0..rng.gen_range(2..4)) + .map(|_| rng.gen_range('a'..='z')) + .collect(); + homophones +} + +pub fn homophonic_cipher(plaintext: &str) -> (String, HashMap>) { + let mut rng = rand::thread_rng(); + let alphabet: Vec = ('a'..='z').collect(); + let mut ciphertext = String::new(); + let mut mapping: HashMap> = HashMap::new(); + + for c in &alphabet { + let homophones: Vec = get_homophones(); + mapping.insert(*c, homophones); + } + + for c in plaintext.chars() { + if let Some(c) = c.to_lowercase().next() { + if let Some(homophones) = mapping.get(&c) { + if let Some(&homophone) = homophones.get(rng.gen_range(0..homophones.len())) { + ciphertext.push(homophone); + } else { + eprintln!("Error: No homophones for character {}", c); + } + } + } else { + ciphertext.push(c); + } + } + + println!("Plaintext: {}", plaintext); + println!("Ciphertext: {}", ciphertext); + println!("Mapping: {:?}", mapping); + + (ciphertext, mapping) +} \ No newline at end of file diff --git a/module2/ciphers/src/main.rs b/module2/ciphers/src/main.rs new file mode 100644 index 0000000..074bfff --- /dev/null +++ b/module2/ciphers/src/main.rs @@ -0,0 +1,84 @@ +/* + +To run homophone cipher: + +cargo run -- --message "Off to the bunker. Every person for themselves" --ahomophone --shift 0 + +To run: + +cargo run -- --message "Off to the bunker. Every person for themselves" --encrypt --shift 10 + +To decrypt: + +cargo run -- --message "Ypp dy dro lexuob. Ofobi zobcyx pyb drowcovfoc" --decrypt --shift 10 + +To Guess: + +cargo run -- --message "Ypp dy dro lexuob. Ofobi zobcyx pyb drowcovfoc" --guess + +*/ + +use cipher_makers::{decrypt, encrypt, homophonic_cipher}; +use clap::Parser; + +mod decoder_ring; + +/// CLI tool to encrypt and decrypt messages using the caeser cipher +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + + /// Encrypt the message using a homophonic cipher + #[arg(short, long)] + ahomophone: bool, + + /// Encrypt the message + #[arg(short, long)] + encrypt: bool, + + /// decrypt the message + #[arg(short, long)] + decrypt: bool, + + /// Guess the shift for the message + #[arg(short, long)] + guess: bool, + + /// The message to encrypt or decrypt + #[arg(short, long)] + message: String, + + //statistical information about the message + #[arg(short, long)] + printstats: bool, + + /// The shift to use for the cipher + /// Must be between 1 and 25, the default is 3 + #[arg(short, long, default_value = "3")] + shift: u8, +} + + fn main() { + + let args = Args::parse(); + if args.ahomophone { + println!("Encrypting the plaintext with homophonic cipher: {}", &args.message); + homophonic_cipher(&args.message); + } else if args.encrypt { + println!("{}", encrypt(&args.message, args.shift)); + } else if args.decrypt { + println!("{}", decrypt(&args.message, args.shift)); + } else if args.guess { + if args.printstats { + decoder_ring::print_stats_analysis(&args.message); + } + let (depth, best_shift, decrypted, max_score) = decoder_ring::guess_shift(&args.message, 26); + println!( + "Best shift: {} (out of {}), score: {}", + best_shift, depth, max_score + ); + println!("Decrypted message: {}", decrypted); + } else { + println!("Please specify mode: --ahomophone, --encrypt, --decrypt, or --guess"); + } + } \ No newline at end of file