generated from noahgift/rust-new-project-template
-
Notifications
You must be signed in to change notification settings - Fork 110
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
216 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |