Skip to content
Merged
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
12 changes: 12 additions & 0 deletions module2/ciphers/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
38 changes: 38 additions & 0 deletions module2/ciphers/Makefile
Original file line number Diff line number Diff line change
@@ -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
115 changes: 115 additions & 0 deletions module2/ciphers/src/decoder_ring.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use std::collections::HashMap;

fn gen_counts() -> HashMap<char, f32> {
// Reference letter frequencies in English
let mut eng_freq: HashMap<char, f32> = 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>, f32)> {
let mut counts: HashMap<char, u32> = 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<char, f32> = 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)
}
134 changes: 134 additions & 0 deletions module2/ciphers/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<char> {
let mut rng = rand::thread_rng();
let homophones: Vec<char> = (0..rng.gen_range(2..4))
.map(|_| rng.gen_range('a'..='z'))
.collect();
homophones
}

pub fn homophonic_cipher(plaintext: &str) -> (String, HashMap<char, Vec<char>>) {
let mut rng = rand::thread_rng();
let alphabet: Vec<char> = ('a'..='z').collect();
let mut ciphertext = String::new();
let mut mapping: HashMap<char, Vec<char>> = HashMap::new();

for c in &alphabet {
let homophones: Vec<char> = 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)
}
84 changes: 84 additions & 0 deletions module2/ciphers/src/main.rs
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading