cipher_crypt/polybius.rs
1//! The Polybius square, also known as the Polybius checkerboard, is a device invented by the
2//! Ancient Greek historian and scholar Polybius, for fractionating plaintext characters so that
3//! they can be represented by a smaller set of symbols.
4//!
5use crate::common::alphabet::Alphabet;
6use crate::common::cipher::Cipher;
7use crate::common::{alphabet, keygen};
8use std::collections::HashMap;
9
10/// A Polybius square cipher.
11///
12/// This struct is created by the `new()` method. See its documentation for more.
13pub struct Polybius {
14 square: HashMap<String, char>,
15}
16
17impl Cipher for Polybius {
18 type Key = (String, [char; 6], [char; 6]);
19 type Algorithm = Polybius;
20
21 /// Initialise a Polybius square cipher.
22 ///
23 /// In this implementation each part of the `key` is used to initialise a 6x6 polybius square.
24 /// The `key` tuple maps to the following `(String, [char; 6], [char; 6]) = (phase,
25 /// column_ids, row_ids)`.
26 ///
27 /// Where ...
28 ///
29 /// * `phrase` is used to generate an alphanumeric keyed alphabet. It can contain characters
30 /// `a-z 0-9`.
31 /// * `column_ids` are unique identifiers used for each column of the polybius square. Valid
32 /// characters are alphabetic only (`a-z`).
33 /// * `row_ids` are unique identifiers used for each row of the polybius square. Valid
34 /// characters can be alphabetic only (`a-z`).
35 ///
36 /// # Panics
37 /// * If a non-alphanumeric symbol is part of the `key`.
38 /// * The `key` must have a length of 36.
39 /// * The `key` must contain each character of the alphanumeric alphabet `a-z`, `0-9`.
40 /// * The `column` and `row_ids` must contain alphabetic characters only.
41 /// * The `column` or `row_ids` contain repeated characters.
42 ///
43 /// # Example
44 /// Lets say the phrase was `or0an3ge` the column_ids were `['A','Z','C','D','E','F']`
45 /// and the row_ids were `['A','B','G','D','E','F']`. Then the polybius square would look like
46 /// ...
47 ///
48 /// ```md,no_run
49 /// __ A Z C D E F
50 /// A| o r 0 a n 3
51 /// B| g e b c d f
52 /// G| h i j k l m
53 /// D| p q s t u v
54 /// E| w x y z 1 2
55 /// F| 4 5 6 7 8 9
56 /// ```
57 /// Basic usage:
58 ///
59 /// ```
60 /// use cipher_crypt::{Cipher, Polybius};
61 ///
62 /// let p = Polybius::new((String::from("or0an3ge"), ['A','Z','C','D','E','F'],
63 /// ['A','B','G','D','E','F']));;
64 ///
65 /// assert_eq!("EEAC AAazadaebabzdc adaebe EF ADdadagebzdc!",
66 /// p.encrypt("10 Oranges and 2 Apples!").unwrap());
67 /// ```
68 ///
69 fn new(key: (String, [char; 6], [char; 6])) -> Polybius {
70 let alphabet_key = keygen::keyed_alphabet(&key.0, &alphabet::ALPHANUMERIC, false);
71 let square = keygen::polybius_square(&alphabet_key, &key.1, &key.2);
72
73 Polybius { square }
74 }
75
76 /// Encrypt a message using a Polybius square cipher.
77 ///
78 /// # Examples
79 /// Basic usage:
80 ///
81 /// ```
82 /// use cipher_crypt::{Cipher, Polybius};
83 ///
84 /// let p = Polybius::new((String::from("p0lyb1us"), ['A','Z','C','D','E','F'],
85 /// ['A','B','G','D','E','F']));;
86 ///
87 /// assert_eq!("BCdfdfbcbdgf 🗡️ dfgcbf bfbcbzdf ezbcacac",
88 /// p.encrypt("Attack 🗡️ the east wall").unwrap());
89 /// ```
90 ///
91 fn encrypt(&self, message: &str) -> Result<String, &'static str> {
92 Ok(message
93 .chars()
94 .map(|c| {
95 if let Some((key, _)) = self.square.iter().find(|e| e.1 == &c) {
96 key.clone()
97 } else {
98 c.to_string()
99 }
100 })
101 .collect())
102 }
103
104 /// Decrypt a message using a Polybius square cipher.
105 ///
106 /// # Examples
107 /// Basic usage:
108 ///
109 /// ```
110 /// use cipher_crypt::{Cipher, Polybius};
111 ///
112 /// let p = Polybius::new((String::from("p0lyb1us"), ['A','Z','C','D','E','F'],
113 /// ['A','B','G','D','E','F']));;
114 ///
115 /// assert_eq!("Attack 🗡️ the east wall",
116 /// p.decrypt("BCdfdfbcbdgf 🗡️ dfgcbf bfbcbzdf ezbcacac").unwrap());
117 /// ```
118 ///
119 fn decrypt(&self, ciphertext: &str) -> Result<String, &'static str> {
120 //We read the ciphertext two bytes at a time and transpose the original message using the
121 //polybius square
122 let mut message = String::new();
123 let mut buffer = String::new();
124
125 for c in ciphertext.chars() {
126 //Determine if the character could potentially be part of a 'polybius sequence' to
127 //be decrypted. Only standard alphabetic characters can be part of a valid sequence.
128 match alphabet::STANDARD.find_position(c) {
129 Some(_) => buffer.push(c),
130 None => message.push(c),
131 }
132
133 if buffer.len() == 2 {
134 match self.square.get(&buffer) {
135 Some(&val) => message.push(val),
136 None => return Err("Unknown sequence in the ciphertext."),
137 }
138
139 buffer.clear();
140 }
141 }
142
143 Ok(message)
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn encrypt_message() {
153 // A B C D E F
154 // A| o r 0 a n g
155 // B| e 1 b c d f
156 // C| 2 h i j k 3
157 // D| l m p 4 q s
158 // E| 5 t u 6 v w
159 // F| 7 x 8 y 9 z
160 let p = Polybius::new((
161 "or0ange1bcdf2hijk3lmp4qs5tu6vw7x8y9z".to_string(),
162 ['A', 'B', 'C', 'D', 'E', 'F'],
163 ['A', 'B', 'C', 'D', 'E', 'F'],
164 ));
165
166 assert_eq!(
167 "BBAC AAabadaeafbadf adaebe CA ADdcdcdabadf!",
168 p.encrypt("10 Oranges and 2 Apples!").unwrap()
169 );
170 }
171
172 #[test]
173 fn decrypt_message() {
174 let p = Polybius::new((
175 "or0ange1bcdf2hijk3lmp4qs5tu6vw7x8y9z".to_string(),
176 ['A', 'B', 'C', 'D', 'E', 'F'],
177 ['A', 'B', 'C', 'D', 'E', 'F'],
178 ));
179
180 assert_eq!(
181 "10 Oranges and 2 Apples!",
182 p.decrypt("BBAC AAabadaeafbadf adaebe CA ADdcdcdabadf!")
183 .unwrap()
184 );
185 }
186
187 #[test]
188 fn invalid_decrypt_sequence() {
189 let p = Polybius::new((
190 "or0ange1bcdf2hijk3lmp4qs5tu6vw7x8y9z".to_string(),
191 ['A', 'B', 'C', 'D', 'E', 'F'],
192 ['A', 'B', 'C', 'D', 'E', 'F'],
193 ));
194
195 //The sequnce 'AZ' is unknown to the polybius square
196 assert!(p
197 .decrypt("BBAC AZabadaeazbadf adaebe CA ADdcdcdabadf!")
198 .is_err());
199 }
200
201 #[test]
202 fn with_utf8() {
203 let m = "Attack 🗡️ the east wall";
204 let p = Polybius::new((
205 "or0ange1bcdf2hijk3lmp4qs5tu6vw7x8y9z".to_string(),
206 ['A', 'B', 'C', 'D', 'E', 'F'],
207 ['A', 'B', 'C', 'D', 'E', 'F'],
208 ));
209
210 assert_eq!(m, p.decrypt(&p.encrypt(m).unwrap()).unwrap());
211 }
212
213 #[test]
214 #[should_panic]
215 fn invalid_key_phrase() {
216 Polybius::new((
217 "F@IL".to_string(),
218 ['A', 'B', 'C', 'D', 'E', 'F'],
219 ['A', 'B', 'C', 'D', 'E', 'F'],
220 ));
221 }
222
223 #[test]
224 #[should_panic]
225 fn invalid_ids() {
226 Polybius::new((
227 "oranges".to_string(),
228 ['A', '!', 'C', 'D', 'E', 'F'],
229 ['A', 'B', '@', 'D', 'E', 'F'],
230 ));
231 }
232
233 #[test]
234 #[should_panic]
235 fn repeated_ids() {
236 Polybius::new((
237 "oranges".to_string(),
238 ['A', 'A', 'C', 'D', 'E', 'F'],
239 ['A', 'C', 'C', 'D', 'E', 'F'],
240 ));
241 }
242}