Skip to content

Commit dc559b8

Browse files
authored
Merge pull request #7 from JadeCara/jade/ciphers
Jade/ciphers
2 parents 22754c3 + 752b19d commit dc559b8

File tree

5 files changed

+383
-0
lines changed

5 files changed

+383
-0
lines changed

module2/ciphers/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "ciphers"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
clap = { version = "4.3.17", features = ["derive"] }
8+
rand = "0.8.5"
9+
10+
[lib]
11+
name = "cipher_makers"
12+
path = "src/lib.rs"

module2/ciphers/Makefile

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
SHELL := /bin/bash
2+
.PHONY: help
3+
4+
help:
5+
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
6+
7+
clean: ## Clean the project using cargo
8+
cargo clean
9+
10+
build: ## Build the project using cargo
11+
cargo build
12+
13+
run: ## Run the project using cargo
14+
cargo run
15+
16+
test: ## Run the tests using cargo
17+
cargo test
18+
19+
lint: ## Run the linter using cargo
20+
@rustup component add clippy 2> /dev/null
21+
cargo clippy
22+
23+
format: ## Format the code using cargo
24+
@rustup component add rustfmt 2> /dev/null
25+
cargo fmt
26+
27+
release:
28+
cargo build --release
29+
30+
all: format lint test run
31+
32+
bump: ## Bump the version of the project
33+
@echo "Current version is $(shell cargo pkgid | cut -d# -f2)"
34+
@read -p "Enter the new version: " version; \
35+
updated_version=$$(cargo pkgid | cut -d# -f2 | sed "s/$(shell cargo pkgid | cut -d# -f2)/$$version/"); \
36+
sed -i -E "s/^version = .*/version = \"$$updated_version\"/" Cargo.toml
37+
@echo "Version bumped to $$(cargo pkgid | cut -d# -f2)"
38+
rm Cargo.toml-e
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use std::collections::HashMap;
2+
3+
fn gen_counts() -> HashMap<char, f32> {
4+
// Reference letter frequencies in English
5+
let mut eng_freq: HashMap<char, f32> = HashMap::new();
6+
7+
// Accounts for 80% of all letters in English
8+
eng_freq.insert('e', 12.7);
9+
eng_freq.insert('t', 9.1);
10+
eng_freq.insert('a', 8.2);
11+
eng_freq.insert('o', 7.5);
12+
eng_freq.insert('i', 7.0);
13+
eng_freq.insert('n', 6.7);
14+
eng_freq.insert('s', 6.3);
15+
eng_freq.insert('h', 6.1);
16+
eng_freq.insert('r', 6.0);
17+
eng_freq.insert('d', 4.3);
18+
19+
eng_freq
20+
}
21+
22+
fn stats_analysis(text: &str) -> Vec<(char, u32, f32, Option<f32>, f32)> {
23+
let mut counts: HashMap<char, u32> = HashMap::new();
24+
25+
for c in text.chars() {
26+
*counts.entry(c).or_insert(0) += 1;
27+
}
28+
29+
let total: u32 = counts.values().sum();
30+
31+
let eng_freq_map = gen_counts();
32+
let eng_freq_map: HashMap<char, f32> = eng_freq_map.iter().map(|(k, v)| (*k, *v)).collect();
33+
34+
let mut results = Vec::new();
35+
36+
for (letter, count) in &counts {
37+
let freq = (*count as f32 / total as f32) * 100.0;
38+
let eng_freq = eng_freq_map.get(&letter.to_ascii_lowercase()).cloned();
39+
40+
let eng_freq_diff = eng_freq.map_or(0.0, |f| (freq - f).abs());
41+
42+
results.push((*letter, *count, freq, eng_freq, eng_freq_diff));
43+
}
44+
results
45+
}
46+
47+
pub fn print_stats_analysis(text: &str) {
48+
let stats = stats_analysis(text);
49+
for (letter, count, freq, eng_freq, eng_freq_diff) in stats {
50+
println!(
51+
"{}: {} ({}%), English Freq: {} ({}%)",
52+
letter,
53+
count,
54+
freq,
55+
eng_freq.unwrap_or(0.0),
56+
eng_freq_diff
57+
);
58+
}
59+
}
60+
61+
pub fn decrypt(text: &str, shift: u8) -> String {
62+
let mut result = String::new();
63+
64+
for c in text.chars() {
65+
if c.is_ascii_alphabetic() {
66+
let base = if c.is_ascii_lowercase() { b'a' } else { b'A' };
67+
let offset = (c as u8 - base + shift) % 26;
68+
result.push((base + offset) as char);
69+
} else {
70+
result.push(c);
71+
}
72+
}
73+
74+
result
75+
}
76+
77+
/*
78+
Guess Shift:
79+
80+
First, uses statistical analysis to determine the most likely shift.
81+
Then, uses the most likely shift to decrypt the message.
82+
Accepts:
83+
* text: the message to decrypt
84+
* depth: the number of shifts to try
85+
Returns:
86+
* depth: the number of shifts to tried
87+
* shift: the most likely shift
88+
* decrypted: the decrypted message
89+
*/
90+
91+
pub fn guess_shift(text: &str, depth: u8) -> (u8, u8, String, f32) {
92+
let mut max_score = 0.0;
93+
let mut best_shift = 0;
94+
let mut decrypted = String::new();
95+
96+
for shift in 0..depth {
97+
let decrypted_text = decrypt(text, shift);
98+
let stats = stats_analysis(&decrypted_text);
99+
100+
let mut score = 0.0;
101+
for (_, _, freq, eng_freq, eng_freq_diff) in stats {
102+
if let Some(eng_freq) = eng_freq {
103+
score += (1.0 - eng_freq_diff / eng_freq) * freq;
104+
}
105+
}
106+
println!("Shift: {}, Score: {}", shift, score);
107+
if score > max_score {
108+
max_score = score;
109+
best_shift = shift;
110+
decrypted = decrypted_text;
111+
}
112+
}
113+
114+
(depth, best_shift, decrypted, max_score)
115+
}

module2/ciphers/src/lib.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use rand::Rng;
2+
use std::collections::HashMap;
3+
4+
5+
/*
6+
This code defines two functions: encrypt and decrypt.
7+
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,
8+
and returns the plaintext string.
9+
10+
*/
11+
12+
pub fn encrypt(text: &str, shift: u8) -> String {
13+
let mut result = String::new();
14+
for c in text.chars() {
15+
if c.is_ascii_alphabetic() {
16+
let base = if c.is_ascii_lowercase() { b'a' } else { b'A' };
17+
let offset = (c as u8 - base + shift) % 26;
18+
result.push((base + offset) as char);
19+
} else {
20+
result.push(c);
21+
}
22+
}
23+
result
24+
}
25+
26+
pub fn decrypt(text: &str, shift: u8) -> String {
27+
encrypt(text, 26 - shift)
28+
}
29+
30+
/*
31+
This code defines two functions: get_homophones and homophonic_cipher.
32+
The get_homophones function generates a random number of random lowercase characters.
33+
The homophonic_cipher function takes a plaintext string and generates a homophonic cipher based
34+
on the homophones mapping.
35+
36+
* Generates a list of random homophones for each lowercase letter in the
37+
* English alphabet. Maps each character in the plaintext to one of its
38+
* random homophones to create the cipher text. Prints the plaintext,
39+
* cipher text, and homophonic mapping. Returns the cipher text and
40+
* homophonic mapping.
41+
*
42+
*
43+
* Here is an example:
44+
* Plaintext: the quick brown fox jumps over the lazy dog
45+
* Ciphertext: acrsalgzuwxsgpeqqrjrnekrwvnnwdgfuqn
46+
* Mapping: {
47+
* 't': ['q', 'a', 'v'], 'y': ['f', 's'], 'q': ['s', 'u'],
48+
* 'l': ['w', 'z', 'o'], 's': ['i', 'w', 'n'], 'b': ['u', 'f'],
49+
* 'h': ['n', 'n', 'c'], 'k': ['z', 'r'], 'j': ['s', 'w', 'q'],
50+
* 'x': ['g', 'g', 'q'], 'i': ['l', 'k'], 'g': ['n', 'g'],
51+
* 'm': ['s', 'j', 'w'], 'p': ['k', 'r'], 'a': ['d', 'm', 'w'],
52+
* 'r': ['w', 'o', 'o'], 'o': ['q', 'x', 'e'], 'e': ['n', 'r'],
53+
* 'f': ['i', 'p', 'e'], 'c': ['g', 'z'], 'u': ['a', 'd', 'r'],
54+
* 'v': ['h', 'f', 'k'], 'd': ['s', 'r', 'u'], 'n': ['d', 'g', 'l'],
55+
* 'w': ['s', 'c'], 'z': ['g', 'b']
56+
* }
57+
* The mapping {'t': ['q', 'a', 'v'], ...} is a part of the homophonic
58+
* cipher mapping from plaintext characters to their cipher characters.
59+
*
60+
61+
* In this specific example, the plaintext character 't' can be represented
62+
* in the ciphertext by either 'q', 'a', or 'v'. This introduces ambiguity
63+
* into the encryption, which makes the homophonic cipher harder to break
64+
* compared to simple substitution ciphers.
65+
*
66+
* The homophonic cipher is more secure than a simple substitution cipher,
67+
* it is still not secure for serious cryptographic uses.
68+
*
69+
* Given the ciphertext and the mapping, you can reverse-engineer the
70+
* plaintext. Let's start with the first three characters of the
71+
* ciphertext: 'a', 'c', and 'r'.
72+
*
73+
* 'a': Looking at the mapping, you can see that 'a' can be a cipher for
74+
* 'u' or 't', as 'u' and 't' have 'a' in their list of homophones. So
75+
* the possible plaintext letters for 'a' are 'u' and 't'.
76+
*
77+
* 'c': Looking at the mapping again, 'c' can be a cipher for 'h' or 'w'
78+
* since 'h' and 'w' have 'c' in their list of homophones. So the
79+
* possible plaintext letters for 'c' are 'h' and 'w'.
80+
*
81+
* 'r': 'r' can be a cipher for 'e', 'o', or 't' since 'e', 'o', and 't'
82+
* have 'r' in their list of homophones. So the possible plaintext
83+
* letters for 'r' are 'e', 'o', and 't'.
84+
*
85+
* So the first three characters of the plaintext could be any combination
86+
* of the possible plaintext letters for 'a', 'c', and 'r'. For example,
87+
* it could be 'u', 'h', 'e', or 't', 'w', 'o', etc.
88+
*
89+
* Remember, homophonic ciphers are designed to provide many possible
90+
* plaintexts for a single ciphertext, which makes it much harder to crack
91+
* the code without having more information. One possible approach to
92+
* decode the message is using a frequency analysis or a known-plaintext
93+
* attack if you have a part of the original message. Another way is to
94+
* use the context of the message if it's known.
95+
*/
96+
fn get_homophones() -> Vec<char> {
97+
let mut rng = rand::thread_rng();
98+
let homophones: Vec<char> = (0..rng.gen_range(2..4))
99+
.map(|_| rng.gen_range('a'..='z'))
100+
.collect();
101+
homophones
102+
}
103+
104+
pub fn homophonic_cipher(plaintext: &str) -> (String, HashMap<char, Vec<char>>) {
105+
let mut rng = rand::thread_rng();
106+
let alphabet: Vec<char> = ('a'..='z').collect();
107+
let mut ciphertext = String::new();
108+
let mut mapping: HashMap<char, Vec<char>> = HashMap::new();
109+
110+
for c in &alphabet {
111+
let homophones: Vec<char> = get_homophones();
112+
mapping.insert(*c, homophones);
113+
}
114+
115+
for c in plaintext.chars() {
116+
if let Some(c) = c.to_lowercase().next() {
117+
if let Some(homophones) = mapping.get(&c) {
118+
if let Some(&homophone) = homophones.get(rng.gen_range(0..homophones.len())) {
119+
ciphertext.push(homophone);
120+
} else {
121+
eprintln!("Error: No homophones for character {}", c);
122+
}
123+
}
124+
} else {
125+
ciphertext.push(c);
126+
}
127+
}
128+
129+
println!("Plaintext: {}", plaintext);
130+
println!("Ciphertext: {}", ciphertext);
131+
println!("Mapping: {:?}", mapping);
132+
133+
(ciphertext, mapping)
134+
}

module2/ciphers/src/main.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
3+
To run homophone cipher:
4+
5+
cargo run -- --message "Off to the bunker. Every person for themselves" --ahomophone --shift 0
6+
7+
To run:
8+
9+
cargo run -- --message "Off to the bunker. Every person for themselves" --encrypt --shift 10
10+
11+
To decrypt:
12+
13+
cargo run -- --message "Ypp dy dro lexuob. Ofobi zobcyx pyb drowcovfoc" --decrypt --shift 10
14+
15+
To Guess:
16+
17+
cargo run -- --message "Ypp dy dro lexuob. Ofobi zobcyx pyb drowcovfoc" --guess
18+
19+
*/
20+
21+
use cipher_makers::{decrypt, encrypt, homophonic_cipher};
22+
use clap::Parser;
23+
24+
mod decoder_ring;
25+
26+
/// CLI tool to encrypt and decrypt messages using the caeser cipher
27+
#[derive(Parser, Debug)]
28+
#[command(author, version, about, long_about = None)]
29+
struct Args {
30+
31+
/// Encrypt the message using a homophonic cipher
32+
#[arg(short, long)]
33+
ahomophone: bool,
34+
35+
/// Encrypt the message
36+
#[arg(short, long)]
37+
encrypt: bool,
38+
39+
/// decrypt the message
40+
#[arg(short, long)]
41+
decrypt: bool,
42+
43+
/// Guess the shift for the message
44+
#[arg(short, long)]
45+
guess: bool,
46+
47+
/// The message to encrypt or decrypt
48+
#[arg(short, long)]
49+
message: String,
50+
51+
//statistical information about the message
52+
#[arg(short, long)]
53+
printstats: bool,
54+
55+
/// The shift to use for the cipher
56+
/// Must be between 1 and 25, the default is 3
57+
#[arg(short, long, default_value = "3")]
58+
shift: u8,
59+
}
60+
61+
fn main() {
62+
63+
let args = Args::parse();
64+
if args.ahomophone {
65+
println!("Encrypting the plaintext with homophonic cipher: {}", &args.message);
66+
homophonic_cipher(&args.message);
67+
} else if args.encrypt {
68+
println!("{}", encrypt(&args.message, args.shift));
69+
} else if args.decrypt {
70+
println!("{}", decrypt(&args.message, args.shift));
71+
} else if args.guess {
72+
if args.printstats {
73+
decoder_ring::print_stats_analysis(&args.message);
74+
}
75+
let (depth, best_shift, decrypted, max_score) = decoder_ring::guess_shift(&args.message, 26);
76+
println!(
77+
"Best shift: {} (out of {}), score: {}",
78+
best_shift, depth, max_score
79+
);
80+
println!("Decrypted message: {}", decrypted);
81+
} else {
82+
println!("Please specify mode: --ahomophone, --encrypt, --decrypt, or --guess");
83+
}
84+
}

0 commit comments

Comments
 (0)