use super::crack_results::CrackResult;
use super::interface::{Crack, Decoder};
use crate::checkers::CheckerTypes;
use crate::decoders::interface::check_string_success;
use crate::storage::ENGLISH_FREQS;
use gibberish_or_not::Sensitivity;
use log::{debug, info, trace};
const EXPECTED_IOC: f64 = 0.0667;
pub struct VigenereDecoder;
impl Crack for Decoder<VigenereDecoder> {
fn new() -> Decoder<VigenereDecoder> {
Decoder {
name: "Vigenere",
description: "A polyalphabetic substitution cipher using a keyword to shift each letter. This implementation automatically detects the key length and breaks the cipher. Uses Medium sensitivity for gibberish detection.",
link: "https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher",
tags: vec!["substitution", "classical"],
popularity: 0.8,
phantom: std::marker::PhantomData,
}
}
fn crack(&self, text: &str, checker: &CheckerTypes) -> CrackResult {
trace!("Attempting Vigenère decryption on text: {:?}", text);
let mut results = CrackResult::new(self, text.to_string());
let clean_text: String = text.chars().filter(|c| c.is_ascii_alphabetic()).collect();
if clean_text.is_empty() {
debug!("No valid characters found in input text");
return results;
}
let mut best_key_length = 0;
let mut best_ioc = 0.0;
for key_length in 1..=20 {
let ioc = calculate_average_ioc(&clean_text, key_length);
if (ioc - EXPECTED_IOC).abs() < (best_ioc - EXPECTED_IOC).abs() {
best_ioc = ioc;
best_key_length = key_length;
}
}
if best_key_length == 0 {
debug!("Failed to determine key length");
return results;
}
let key = find_key(&clean_text, best_key_length);
let decrypted = decrypt(&clean_text, &key);
let final_text = reconstruct_formatting(text, &decrypted);
if !check_string_success(&final_text, text) {
info!("Failed Vigenère decoding validation");
return results;
}
let checker_with_sensitivity = checker.with_sensitivity(Sensitivity::Medium);
let checker_result = checker_with_sensitivity.check(&final_text);
results.unencrypted_text = Some(vec![final_text]);
results.update_checker(&checker_result);
results
}
fn get_tags(&self) -> &Vec<&str> {
&self.tags
}
fn get_name(&self) -> &str {
self.name
}
}
fn calculate_average_ioc(text: &str, key_length: usize) -> f64 {
let mut total_ioc = 0.0;
let text_bytes: Vec<u8> = text.bytes().collect();
for i in 0..key_length {
let mut freqs = [0; 26];
let mut count = 0;
for j in (i..text_bytes.len()).step_by(key_length) {
if text_bytes[j].is_ascii_uppercase() {
freqs[(text_bytes[j] - b'A') as usize] += 1;
count += 1;
} else if text_bytes[j].is_ascii_lowercase() {
freqs[(text_bytes[j] - b'a') as usize] += 1;
count += 1;
}
}
if count > 1 {
let mut column_ioc = 0.0;
for freq in freqs.iter() {
column_ioc += (*freq as f64) * ((*freq - 1) as f64);
}
column_ioc /= (count * (count - 1)) as f64;
total_ioc += column_ioc;
}
}
total_ioc / key_length as f64
}
fn find_key(text: &str, key_length: usize) -> String {
let mut key = String::with_capacity(key_length);
let text_bytes: Vec<u8> = text.bytes().collect();
for i in 0..key_length {
let mut freqs = [0.0; 26];
let mut count = 0;
for j in (i..text_bytes.len()).step_by(key_length) {
if text_bytes[j].is_ascii_alphabetic() {
let idx = (text_bytes[j].to_ascii_uppercase() - b'A') as usize;
freqs[idx] += 1.0;
count += 1;
}
}
if count > 0 {
for freq in freqs.iter_mut() {
*freq /= count as f64;
}
}
let mut best_shift = 0;
let mut best_chi_squared = f64::MAX;
for shift in 0..26 {
let mut chi_squared = 0.0;
for j in 0..26 {
let expected = ENGLISH_FREQS[j];
let observed = freqs[(j + shift) % 26];
let diff = observed - expected;
chi_squared += diff * diff / expected;
}
if chi_squared < best_chi_squared {
best_chi_squared = chi_squared;
best_shift = shift;
}
}
key.push((b'A' + best_shift as u8) as char);
}
key
}
fn decrypt(text: &str, key: &str) -> String {
let key_bytes: Vec<u8> = key.bytes().collect();
let mut result = String::with_capacity(text.len());
let mut key_idx = 0;
for c in text.chars() {
if c.is_ascii_alphabetic() {
let shift = (key_bytes[key_idx % key_bytes.len()] - b'A') as i8;
let base = if c.is_ascii_uppercase() { b'A' } else { b'a' };
let pos = ((c as u8) - base) as i8;
let new_pos = ((pos - shift + 26) % 26) as u8;
result.push((base + new_pos) as char);
key_idx += 1;
} else {
result.push(c);
}
}
result
}
fn reconstruct_formatting(original: &str, decrypted: &str) -> String {
let mut result = String::with_capacity(original.len());
let mut dec_iter = decrypted.chars().filter(|c| c.is_ascii_alphabetic());
for c in original.chars() {
if c.is_ascii_alphabetic() {
if let Some(dec_char) = dec_iter.next() {
result.push(dec_char);
}
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::checkers::{
athena::Athena,
checker_type::{Check, Checker},
CheckerTypes,
};
fn get_athena_checker() -> CheckerTypes {
let athena_checker = Checker::<Athena>::new();
CheckerTypes::CheckAthena(athena_checker)
}
#[test]
fn test_vigenere_decoding() {
let vigenere_decoder = Decoder::<VigenereDecoder>::new();
let result = vigenere_decoder
.crack(
"Vyc fnqkm spdpv nqo hjfxa qmcg 13 eiha umvl.",
&get_athena_checker(),
)
.unencrypted_text;
assert!(result.is_some());
let _decoded_text = &result.as_ref().unwrap()[0];
}
#[test]
fn test_vigenere_with_special_chars() {
let vigenere_decoder = Decoder::<VigenereDecoder>::new();
let result = vigenere_decoder
.crack(
"Jvjah Asgccihva! Vycgx'a i ffe xg ug ecmhxb",
&get_athena_checker(),
)
.unencrypted_text;
assert!(result.is_some());
let _decoded_text = &result.as_ref().unwrap()[0];
}
#[test]
fn test_empty_input() {
let vigenere_decoder = Decoder::<VigenereDecoder>::new();
let result = vigenere_decoder
.crack("", &get_athena_checker())
.unencrypted_text;
assert!(result.is_none());
}
#[test]
fn test_non_alphabetic_input() {
let vigenere_decoder = Decoder::<VigenereDecoder>::new();
let result = vigenere_decoder
.crack("12345!@#$%", &get_athena_checker())
.unencrypted_text;
assert!(result.is_none());
}
}