use clap::Parser;
use colored::Colorize;
use prettytable::{Cell, Row, Table};
use regex::Regex;
use std::error::Error;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use crate::identify_cipher;
use crate::models::cipher_type::{load_cipher_types, get_cipher_primary_type};
use crate::statistical_tests::{binary_random, ioc, shannon_entropy};
pub struct CipherAnalyzer;
#[derive(Parser, Debug)]
#[command(
name = "cipher_analyzer",
about = "Analyzes ciphertext and identifies the most likely cipher types",
long_about = "Cipher Analyzer will analyze your ciphertext and run advanced algorithms on it to determine the correct encryption."
)]
pub struct CliArgs {
#[arg(short, long)]
text: Option<String>,
#[arg(short, long, default_value = "5")]
number: usize,
#[arg(short, long)]
cipher: Option<String>,
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
#[arg(short, long)]
file: Option<PathBuf>,
}
impl CipherAnalyzer {
pub fn new() -> Self {
CipherAnalyzer
}
pub fn run(&self, args: CliArgs) -> Result<(), Box<dyn Error>> {
let text = if let Some(file_path) = args.file {
let mut file = File::open(file_path)?;
let mut text = String::new();
file.read_to_string(&mut text)?;
text
} else if let Some(text) = args.text {
text
} else {
return Err("Text input expected. Run with --help for usage information.".into());
};
let text = Regex::new(r"\s+")?.replace_all(&text, "").to_string().to_uppercase();
self.display_basic_stats(&text);
self.identify_cipher(&text, args.number, args.cipher.as_deref())?;
Ok(())
}
pub fn display_basic_stats(&self, text: &str) {
let text_length = text.len();
let text_ioc = ioc::get_ioc(text);
let text_entropy = shannon_entropy::get_shannon_entropy(text);
let binary_random_test = binary_random::get_binary_random(text);
let mut table = Table::new();
table.set_titles(Row::new(vec![
Cell::new("Stat").style_spec("Fc"),
Cell::new("Value").style_spec("Fb"),
]));
table.add_row(Row::new(vec![
Cell::new("Length"),
Cell::new(&text_length.to_string()),
]));
table.add_row(Row::new(vec![
Cell::new("Number of unique characters"),
Cell::new(&text.chars().collect::<std::collections::HashSet<_>>().len().to_string()),
]));
table.add_row(Row::new(vec![
Cell::new("Missing letters"),
Cell::new(&self.find_missing_letters(text)),
]));
table.add_row(Row::new(vec![
Cell::new("IoC"),
Cell::new(&format!("{:.6}", text_ioc)),
]));
table.add_row(Row::new(vec![
Cell::new("Shannon entropy"),
Cell::new(&format!("{:.6}", text_entropy)),
]));
table.add_row(Row::new(vec![
Cell::new("Binary random test"),
Cell::new(&binary_random_test),
]));
println!("\n{}", "Basic stats".bold());
table.printstd();
}
pub fn identify_cipher(&self, text: &str, number: usize, highlight: Option<&str>) -> Result<(), Box<dyn Error>> {
let scores = identify_cipher::identify_cipher(text, number, highlight);
let mut table = Table::new();
table.set_titles(Row::new(vec![
Cell::new("Cipher").style_spec("Fc"),
Cell::new("Score").style_spec("Fb"),
Cell::new("Cipher type").style_spec("Fg"),
]));
let cipher_types = load_cipher_types("resources/cipher_types.json").ok();
for (cipher, score) in scores {
let cipher_type = if let Some(ref types) = cipher_types {
get_cipher_primary_type(types, &cipher)
} else {
"unknown".to_string()
};
if Some(cipher.as_str()) == highlight {
table.add_row(Row::new(vec![
Cell::new(&cipher).style_spec("Fm"),
Cell::new(&format!("{:.3}", score)).style_spec("Fm"),
Cell::new(&cipher_type).style_spec("Fm"),
]));
} else {
table.add_row(Row::new(vec![
Cell::new(&cipher),
Cell::new(&format!("{:.3}", score)),
Cell::new(&cipher_type),
]));
}
}
println!("\n{} (lower is better)", format!("Top {} most likely ciphers", number).bold());
table.printstd();
Ok(())
}
pub fn find_missing_letters(&self, text: &str) -> String {
let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let text_chars: std::collections::HashSet<char> = text.chars().collect();
alphabet
.chars()
.filter(|&c| !text_chars.contains(&c))
.collect()
}
}
pub fn main() -> Result<(), Box<dyn Error>> {
let args = CliArgs::parse();
let analyzer = CipherAnalyzer::new();
analyzer.run(args)
}