enigma_cracker/
lib.rs

1use std::io::Write;
2
3use enigma_simulator::{EnigmaBuilder as _, EnigmaMachine, EnigmaResult};
4
5pub fn decrypt_enigma(ciphertext: &str) -> EnigmaResult<()> {
6    let plugboard = "BY EW FZ GI MQ RV UX";
7    let reflector = "B";
8
9    let (rotors, offsets) = best_rotors(plugboard, reflector, ciphertext)?;
10    println!("Best rotors: {}, {}, {}", rotors.0, rotors.1, rotors.2);
11    println!("Best offsets: {}, {}, {}", offsets.0, offsets.1, offsets.2);
12
13    let ring_settings = best_ring_settings(reflector, plugboard, rotors, offsets, ciphertext)?;
14    println!("Best ring settings: {}, {}, {}", ring_settings.0, ring_settings.1, ring_settings.2);
15
16    let plaintext = &EnigmaMachine::new()
17        .reflector(reflector)
18        .plugboard(plugboard)
19        .rotors(rotors.0, rotors.1, rotors.2)
20        .ring_positions(offsets.0, offsets.1, offsets.2)
21        .ring_settings(ring_settings.0, ring_settings.1, ring_settings.2)?
22        .decrypt(ciphertext);
23
24    println!("Plaintext: {plaintext}");
25
26    Ok(())
27}
28
29#[allow(clippy::type_complexity)]
30fn best_rotors(plugboard: &str, reflector: &str, ciphertext: &str) -> EnigmaResult<((u8, u8, u8), (u8, u8, u8))> {
31    let mut plaintexts = Vec::new();
32    let total = 8 * 8 * 8 * 26 * 26 * 26;
33    let mut iteration = 0;
34
35    println!("\n");
36
37    for rotor_1 in 1..=8 {
38        for rotor_2 in 1..=8 {
39            for rotor_3 in 1..=8 {
40                for offset_1 in 1..=26 {
41                    for offset_2 in 1..=26 {
42                        for offset_3 in 1..=26 {
43                            let machine = EnigmaMachine::new()
44                                .plugboard(plugboard)
45                                .reflector(reflector)
46                                .rotors(rotor_1, rotor_2, rotor_3)
47                                .ring_positions(offset_1, offset_2, offset_3)
48                                .ring_settings(1, 1, 1)?;
49                            let plaintext = machine.decrypt(ciphertext);
50                            let distance = (index_of_coincidence(&plaintext) - 0.0667).abs();
51                            plaintexts.push((distance, ((rotor_1, rotor_2, rotor_3), (offset_1, offset_2, offset_3))));
52
53                            iteration += 1;
54                            let progress = 100f64 * (iteration as f64 / total as f64);
55                            print!("\x1B[A");
56                            println!("Finding best rotor settings... ({:.2}%)", progress);
57                            std::io::stdout().flush().unwrap();
58                        }
59                    }
60                }
61            }
62        }
63    }
64
65    Ok(plaintexts.iter().min_by(|first, second| first.0.total_cmp(&second.0)).unwrap().1)
66}
67
68fn best_ring_settings(reflector: &str, plugboard: &str, rotors: (u8, u8, u8), ring_positions: (u8, u8, u8), ciphertext: &str) -> EnigmaResult<(u8, u8, u8)> {
69    let mut plaintexts = Vec::new();
70    let mut iteration = 1;
71    let total = 26 * 26 * 26;
72
73    println!();
74
75    for offset_1 in 1..=26 {
76        for offset_2 in 1..=26 {
77            for offset_3 in 1..=26 {
78                let machine = EnigmaMachine::new()
79                    .plugboard(plugboard)
80                    .reflector(reflector)
81                    .rotors(rotors.0, rotors.1, rotors.2)
82                    .ring_positions(ring_positions.0, ring_positions.1, ring_positions.2)
83                    .ring_settings(ring_positions.0, ring_positions.1, ring_positions.2)?;
84                let plaintext = machine.decrypt(ciphertext);
85                let distance = (index_of_coincidence(&plaintext) - 0.0667).abs();
86                plaintexts.push((distance, (offset_1, offset_2, offset_3)));
87
88                iteration += 1;
89                let progress = 100f64 * (iteration as f64 / total as f64);
90                print!("\x1B[A");
91                println!("Finding best ring settings... ({:.2}%)", progress);
92                std::io::stdout().flush().unwrap();
93            }
94        }
95    }
96
97    Ok(plaintexts.iter().min_by(|first, second| first.0.total_cmp(&second.0)).unwrap().1)
98}
99
100fn index_of_coincidence(text: &str) -> f64 {
101    let mut frequency = [0u32; 26];
102    let mut total_letters = 0;
103
104    for c in text.chars() {
105        if c.is_alphabetic() {
106            let idx = c.to_ascii_lowercase() as usize - 'a' as usize;
107            frequency[idx] += 1;
108            total_letters += 1;
109        }
110    }
111
112    if total_letters < 2 {
113        return 0.0;
114    }
115
116    let mut numerator = 0u32;
117
118    for &count in &frequency {
119        numerator += count * (count - 1);
120    }
121
122    let denominator = total_letters * (total_letters - 1);
123
124    numerator as f64 / denominator as f64
125}
126
127#[cfg(test)]
128mod tests {
129    use enigma_simulator::{EnigmaBuilder, EnigmaMachine, EnigmaResult};
130
131    use crate::{best_ring_settings, best_rotors};
132
133    #[test]
134    #[ignore]
135    fn rotors() -> EnigmaResult<()> {
136        let plugboard = "BY EW FZ GI MQ RV UX";
137        let reflector = "B";
138        let ciphertext = include_str!("../tests/encrypted_letter.txt");
139
140        let (rotors, offsets) = best_rotors(plugboard, reflector, ciphertext)?;
141
142        assert_eq!(rotors, (5, 8, 3));
143        assert_eq!(offsets, (5, 22, 3));
144
145        Ok(())
146    }
147
148    #[test]
149    #[ignore]
150    fn ring_settings() -> EnigmaResult<()> {
151        let plugboard = "BY EW FZ GI MQ RV UX";
152        let reflector = "B";
153        let ciphertext = include_str!("../tests/encrypted_letter.txt");
154
155        let (rotors, offsets) = best_rotors(plugboard, reflector, ciphertext)?;
156        println!("Best rotors: {}, {}, {}", rotors.0, rotors.1, rotors.2);
157        println!("Best offsets: {}, {}, {}", offsets.0, offsets.1, offsets.2);
158
159        let ring_settings = best_ring_settings(reflector, plugboard, rotors, offsets, ciphertext)?;
160        println!("Best ring settings: {}, {}, {}", ring_settings.0, ring_settings.1, ring_settings.2);
161
162        let plaintext = &EnigmaMachine::new()
163            .reflector(reflector)
164            .plugboard(plugboard)
165            .rotors(rotors.0, rotors.1, rotors.2)
166            .ring_positions(offsets.0, offsets.1, offsets.2)
167            .ring_settings(ring_settings.0, ring_settings.1, ring_settings.2)?
168            .decrypt(ciphertext);
169
170        println!("Plaintext: {plaintext}");
171
172        Ok(())
173    }
174}