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}