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 == &current {
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}