Skip to content

Commit

Permalink
adding a decoder ring
Browse files Browse the repository at this point in the history
  • Loading branch information
noahgift committed Jul 21, 2023
1 parent 4c7858b commit e778605
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 0 deletions.
9 changes: 9 additions & 0 deletions decoder-ring/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "decoder-ring"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
clap = { version = "4.3.17", features = ["derive"] }
13 changes: 13 additions & 0 deletions decoder-ring/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
format:
cargo fmt --quiet

lint:
cargo clippy --quiet

test:
cargo test --quiet

run:
cargo run

all: format lint test run
115 changes: 115 additions & 0 deletions decoder-ring/src/lib.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)
}
79 changes: 79 additions & 0 deletions decoder-ring/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
Attempts to statistically decode a Caesar cipher.
Here's an example of how to use it:
This is a shift 10 message: "Off to the bunker. Every person for themselves"
"Ypp dy dro lexuob. Ofobi zobcyx pyb drowcovfoc"
cargo run -- --message "Ypp dy dro lexuob. Ofobi zobcyx pyb drowcovfoc" --guess
Here is an example of it in action:
Shift: 0, Score: 7.538945
Shift: 1, Score: 10.078025
Shift: 2, Score: 20.755177
Shift: 3, Score: 11.284368
Shift: 4, Score: 7.5232525
Shift: 5, Score: 23.558884
Shift: 6, Score: 21.086407
Shift: 7, Score: 9.926911
Shift: 8, Score: 5.5866623
Shift: 9, Score: 15.310673
Shift: 10, Score: 17.950832
Shift: 11, Score: 21.200842
Shift: 12, Score: 23.447578
Shift: 13, Score: 14.035946
Shift: 14, Score: 13.314641
Shift: 15, Score: 2.055822
Shift: 16, Score: 40.54977
Shift: 17, Score: 15.98934
Shift: 18, Score: 18.178614
Shift: 19, Score: 8.523561
Shift: 20, Score: 9.371011
Shift: 21, Score: 12.385875
Shift: 22, Score: 8.159725
Shift: 23, Score: 10.439689
Shift: 24, Score: 17.104122
Shift: 25, Score: 14.300304
Best shift: 16 (out of 26), score: 40.54977
Decrypted message: Off to the bunker. Every person for themselves
*/

use clap::Parser;
use decoder_ring::print_stats_analysis;

/// CLI tool to reverse engineer a Caesar cipher
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// The message to decrypt
#[arg(short, long)]
message: String,

//statistical information about the message
#[arg(short, long)]
stats: bool,

//guess the shift
#[arg(short, long)]
guess: bool,
}

// run it
fn main() {
let args = Args::parse();
//stats
if args.stats {
print_stats_analysis(&args.message);
}
//guess
if args.guess {
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);
}
}

0 comments on commit e778605

Please sign in to comment.