cipher_crypt/playfair.rs
1//! The Playfair cipher is the first bigram substitution cipher.
2//! Invented in 1854 by Charles Wheatstone, its name honors Lord
3//! Lyon Playfair for promoting its use.
4//!
5//! [Reference](https://en.wikipedia.org/wiki/Playfair_cipher)
6//!
7//! The Playfair cipher operates on a 5x5 table. The key, omitting repeated
8//! characters, is written from left to right starting on the first row
9//! of the table. Other key layout patterns in the table can be used
10//! but are less common. Note that a letter must either be omitted
11//! (typically 'Q') or two letters can occupy the same space (I=J).
12//! This implementation uses the *latter* design, replacing all
13//! encountered 'J' characters with 'I'.
14//!
15use crate::common::{alphabet, alphabet::Alphabet, cipher::Cipher, keygen::playfair_table};
16
17type Bigram = (char, char);
18
19/// A Playfair cipher.
20///
21/// This struct is created by the `new()` method. See its documentation for more.
22pub struct Playfair {
23 /// The Playfair key table (5x5)
24 rows: [String; 5],
25 cols: [String; 5],
26 null_char: char,
27}
28
29impl Cipher for Playfair {
30 type Key = (String, Option<char>);
31 type Algorithm = Playfair;
32
33 /// Initialize a Playfair cipher.
34 ///
35 /// The `key` tuple maps to the following `(String, Option<char>) = (keystream, null_char)`.
36 /// Where ...
37 ///
38 /// * The `keystream` is used to generate a playfair table.
39 /// * The `null_char` is the character that is used to pad uneven messages
40 /// during the encryption process. This value will default to 'X'.
41 ///
42 /// # Panics
43 /// * The `keystream` must not be empty.
44 /// * The `keystream` must not exceed the length of the playfair alphabet (25 characters).
45 /// * The `keystream` must not contain non-alphabetic symbols or the letter 'J'.
46 ///
47 fn new(key: (String, Option<char>)) -> Playfair {
48 let null_char = key.1.unwrap_or_else(|| 'X').to_ascii_uppercase();
49 let (rows, cols) = playfair_table(&key.0);
50
51 Playfair {
52 rows,
53 cols,
54 null_char,
55 }
56 }
57
58 /// Encrypt a message with the Playfair cipher.
59 ///
60 /// # Warning
61 /// * The 5x5 key table requires any 'J' characters in the message
62 /// to be substituted with 'I' characters (i.e. I = J).
63 /// * The resulting ciphertext will be fully uppercase with no whitespace.
64 ///
65 /// # Errors
66 /// * Message contains a non-alphabetic character.
67 /// * Message contains the null character.
68 /// * Message contains whitespace.
69 ///
70 /// # Examples
71 ///
72 /// Basic Usage:
73 ///
74 /// ```
75 /// use cipher_crypt::{Cipher, Playfair};
76 ///
77 /// let c = Playfair::new(("playfairexample".to_string(), None));
78 /// assert_eq!(
79 /// c.encrypt("Hidethegoldinthetreestump").unwrap(),
80 /// "BMODZBXDNABEKUDMUIXMKZZRYI"
81 /// );
82 /// ```
83 ///
84 fn encrypt(&self, message: &str) -> Result<String, &'static str> {
85 if !alphabet::PLAYFAIR.is_valid(&message) {
86 return Err("Message must only consist of alphabetic characters.");
87 } else if message.to_uppercase().contains(self.null_char) {
88 return Err("Message cannot contain the null character.");
89 }
90
91 // Handles Rule 1 (Bigrams)
92 let bmsg = self.bigram(&message.to_uppercase());
93
94 self.apply_rules(bmsg, |v, first, second| {
95 (v[(first + 1) % 5], v[(second + 1) % 5])
96 })
97 }
98
99 /// Decrypt a message with the Playfair cipher.
100 ///
101 /// # Warning
102 /// * The 5x5 key table requires any 'J' characters in the message
103 /// to be substituted with 'I' characters (i.e. I = J).
104 /// * The resulting plaintext will be fully uppercase with no whitespace.
105 /// * The resulting plaintext may contain added null characters.
106 ///
107 /// # Errors
108 /// * Message contains a non-alphabetic character.
109 /// * Message contains whitespace.
110 ///
111 /// # Examples
112 ///
113 /// Basic Usage:
114 ///
115 /// ```
116 /// use cipher_crypt::{Cipher, Playfair};
117 ///
118 /// let c = Playfair::new(("playfairexample".to_string(), None));
119 /// assert_eq!(
120 /// c.decrypt("BMODZBXDNABEKUDMUIXMKZZRYI").unwrap(),
121 /// "HIDETHEGOLDINTHETREXSTUMPX"
122 /// );
123 ///
124 /// ```
125 ///
126 fn decrypt(&self, message: &str) -> Result<String, &'static str> {
127 if !alphabet::PLAYFAIR.is_valid(&message) {
128 return Err("Message must only consist of alphabetic characters.");
129 }
130 // Handles Rule 1
131 let bmsg = self.bigram(&message.to_uppercase());
132
133 //Must be wary of negative wrap-around in modulo
134 self.apply_rules(bmsg, |v, first, second| {
135 (
136 v[first.checked_sub(1).unwrap_or(v.len() - 1)],
137 v[second.checked_sub(1).unwrap_or(v.len() - 1)],
138 )
139 })
140 }
141}
142
143impl Playfair {
144 /// Apply the PlayFair cipher algorithm.
145 ///
146 /// The operations for encrypt and decrypt are identical
147 /// except for the direction of the substitution choice.
148 ///
149 fn apply_rules<F>(&self, bigrams: Vec<Bigram>, shift: F) -> Result<String, &'static str>
150 where
151 F: Fn(Vec<char>, usize, usize) -> Bigram,
152 {
153 let mut text = String::new();
154 for bigram in bigrams {
155 let chars: Bigram;
156 if let Some(b) = self.apply_slice(bigram, &self.rows, &shift) {
157 // Rule 2 (Row)
158 chars = b;
159 } else if let Some(b) = self.apply_slice(bigram, &self.cols, &shift) {
160 // Rule 3 (Column)
161 chars = b;
162 } else {
163 // Rule 4 (Rectangle)
164 chars = self.apply_rectangle(bigram);
165 }
166
167 text.push(chars.0);
168 text.push(chars.1);
169 }
170 Ok(text)
171 }
172
173 /// Apply rule 1 (bigrams).
174 ///
175 /// # Rule 1
176 ///
177 /// If both letters are the same (or only one letter is left), add the null_char
178 /// after the first letter. Encrypt the new pair and continue.
179 ///
180 /// [Reference](https://en.wikipedia.org/wiki/Playfair_cipher#Description)
181 ///
182 fn bigram(&self, message: &str) -> Vec<Bigram> {
183 if message.contains(char::is_whitespace) {
184 panic!("Message contains whitespace.");
185 }
186 if !alphabet::PLAYFAIR.is_valid(&message) {
187 panic!("Message must only consist of alphabetic characters.");
188 }
189
190 let mut bigrams: Vec<Bigram> = Vec::new();
191 let mut msg_iter = message.chars().peekable();
192 let mut skip = false;
193
194 while let Some(current) = msg_iter.next() {
195 if skip {
196 skip = false;
197 continue;
198 }
199
200 if let Some(next) = msg_iter.peek() {
201 if next == ¤t {
202 bigrams.push((current, self.null_char)); // Add the null character for repeating chars
203 skip = true;
204 } else {
205 bigrams.push((current, *next)); // Add the next two letters
206 skip = true;
207 }
208 } else {
209 bigrams.push((current, self.null_char)); //It's uneven - add the null char
210 }
211 }
212
213 bigrams
214 }
215
216 /// Apply rule 2 (Row) or rule 3 (Column).
217 ///
218 /// # Rule 2
219 ///
220 /// If the letters appear on the same row of your table, replace them
221 /// with the letters to their immediate right respectively (wrapping
222 /// around to the left side of the row if a letter in the original pair
223 /// was on the right side of the row).
224 ///
225 /// # Rule 3
226 ///
227 /// If the letters appear on the same column of your table, replace them
228 /// with the letters immediately below respectively (wrapping around to the
229 /// top side of the column if a letter in the original pair was on the
230 /// bottom side of the column).
231 ///
232 /// [Reference](https://en.wikipedia.org/wiki/Playfair_cipher#Description)
233 ///
234 fn apply_slice<F>(&self, b: Bigram, slices: &[String; 5], shift: &F) -> Option<Bigram>
235 where
236 F: Fn(Vec<char>, usize, usize) -> Bigram,
237 {
238 for slice in slices.iter() {
239 if let Some(first) = slice.find(b.0) {
240 if let Some(second) = slice.find(b.1) {
241 return Some(shift(slice.chars().collect(), first, second));
242 }
243 }
244 }
245 None
246 }
247
248 /// Apply rule 4 (Rectangle).
249 ///
250 /// # Rule 4
251 ///
252 /// If the letters are not on the same row or column, replace them with
253 /// the letters on the same row respectively but at the other pair of
254 /// corners of the rectangle defined by the original pair. The order is
255 /// important – the first letter of the encrypted pair is the one that
256 /// lies on the same row as the first letter of the plaintext pair.
257 ///
258 /// [Reference](https://en.wikipedia.org/wiki/Playfair_cipher#Description)
259 ///
260 fn apply_rectangle(&self, b: Bigram) -> Bigram {
261 let row_indices = find_corners(b, &self.cols);
262 let col_indices = find_corners(b, &self.rows);
263
264 let row0: Vec<char> = self.rows[row_indices.0].chars().collect();
265 let row1: Vec<char> = self.rows[row_indices.1].chars().collect();
266
267 (row0[col_indices.1], row1[col_indices.0])
268 }
269}
270
271/// Identifies 2 corners of the rectangle.
272fn find_corners(b: Bigram, slices: &[String; 5]) -> (usize, usize) {
273 let mut indices = (0, 0);
274 for slice in slices.iter() {
275 if let Some(pos) = slice.find(b.0) {
276 indices.0 = pos;
277 } else if let Some(pos) = slice.find(b.1) {
278 indices.1 = pos;
279 }
280 }
281 indices
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn bigram_handles_repeats() {
290 let pf = Playfair::new(("test".to_string(), Some('X')));
291 let message = "FIZZBAR";
292 assert_eq!(
293 vec![('F', 'I'), ('Z', 'X'), ('B', 'A'), ('R', 'X'),],
294 pf.bigram(message)
295 );
296 }
297
298 #[test]
299 fn bigram_handles_odd_length() {
300 let pf = Playfair::new(("test".to_string(), Some('Z')));
301 let message = "WORLD";
302 assert_eq!(
303 vec![('W', 'O'), ('R', 'L'), ('D', 'Z'),],
304 pf.bigram(message)
305 );
306 }
307
308 #[test]
309 fn invalid_encrypt_message_whitespace() {
310 let pf = Playfair::new(("playfairexample".to_string(), None));
311 assert!(pf.encrypt("This contains whitespace").is_err());
312 }
313
314 #[test]
315 fn invalid_encrypt_message_null_char() {
316 let pf = Playfair::new(("playfairexample".to_string(), Some('Z')));
317 assert!(pf.encrypt("Thiscontainsthenullcharz").is_err());
318 }
319
320 #[test]
321 fn invalid_decrypt_message_symbols() {
322 let pf = Playfair::new(("playfairexample".to_string(), None));
323 assert!(pf.decrypt("This!contains!whitespace").is_err());
324 }
325
326 #[test]
327 fn simple_encrypt() {
328 let pf = Playfair::new(("playfairexample".to_string(), None));
329 assert_eq!(
330 "BMODZBXDNABEKUDMUIXMKZZRYI",
331 pf.encrypt("Hidethegoldinthetreestump").unwrap(),
332 );
333 }
334
335 #[test]
336 fn simple_decrypt() {
337 let pf = Playfair::new(("playfairexample".to_string(), None));
338 assert_eq!(
339 "HIDETHEGOLDINTHETREXSTUMPX",
340 pf.decrypt("BMODZBXDNABEKUDMUIXMKZZRYI").unwrap(),
341 );
342 }
343
344 #[test]
345 fn negative_wrap_around() {
346 let pf = Playfair::new(("apt".to_string(), None));
347 let msg = "HELLOWORLD";
348 assert_eq!("HELXOWORLD", pf.decrypt(&pf.encrypt(msg).unwrap()).unwrap());
349 }
350}