playfair/
lib.rs

1//! Playfair cipher implementation in Rust
2
3/// Bigram type. Used in the Playfair cipher by grouping characters and performing operations on
4/// those pairs.
5pub type Bigram = (char, char);
6
7/// Position type. Used to store an X, Y value for use in a matrix.
8pub type Position = (usize, usize);
9
10/// The Matrix type is a 5 by 5 character array.
11pub type Matrix = [[char; 5]; 5];
12
13/// Cipher trait, enforces `encrypt` and `decrypt` methods.
14pub trait Cipher {
15    /// Encryption logic for a given plaintext
16    fn encrypt(&self, plaintext: &str) -> String;
17    /// Decryption logic for a given ciphertext
18    fn decrypt(&self, ciphertext: &str) -> String;
19}
20
21/// Keyword structure, used in constructing the matrix in which the encryption is performed over.
22#[derive(Debug, PartialEq)]
23pub struct Keyword(String);
24
25impl Keyword {
26    /// Create a keyword from an initial input. This will have a size of 25 and will not have any
27    /// duplicate letters, and equate the letter 'i' to the letter 'j'. This is to conform to the 5x5 matrix that
28    /// the Playfair cipher is used on. The letter 'j' was chosen arbitrarily due to its low use in
29    /// the English language. This will mean that upon decryption, you'll notice anything that once
30    /// was a 'j' in the initial plain text is now an 'i'.
31    ///
32    /// # Example
33    /// Key: `playfair`
34    /// Message: `Jane`
35    /// Encrypted message: `bpun`
36    /// Decrypted of encryption: `iane`
37    ///
38    /// Two things to note with this, it turns everything lowercase for easier searching and
39    /// complexity, and j's are now converted to i's.
40    ///
41    /// ## TODO
42    /// According to
43    /// [this](https://users.rust-lang.org/t/fast-removing-chars-from-string/24554) post, using
44    /// `.retain()` on the initial filitering we do may be faster in release builds. Investigate
45    /// more.
46    pub fn new(initial: &str) -> Self {
47        // Create a string with the capacity of 25 since we know how big this will be. This will
48        // eliminate the need for a reallocation, if Rust defaults the capacity to less than 25.
49        let mut buffer = String::with_capacity(25);
50
51        // Ensure we only take the alphabetic parts of the input string and
52        // remove any instance of 'j'.
53        let mut parsed: String = initial
54            .to_lowercase()
55            .chars()
56            .filter(|c| c.is_alphabetic() && *c != 'j')
57            .collect();
58
59        // Append the alphabet (equating 'i' = 'j', thus omitting 'j') to the initial input, to fill in the rest of the possible letters
60        // that the initial input might not cover.
61        parsed.push_str("abcdefghiklmnopqrstuvwxyz");
62
63        // We only need 25 letters, so keep pushing to the buffer while we have less than 25
64        // characters.
65        while buffer.len() < 25 {
66            // Loop over each character in the input and alphabet string, checking that the
67            // character is alphabetic since we can't use numbers of symbols in our Matrix.
68            for c in parsed.chars() {
69                // Check that the character does not exist in the buffer
70                if buffer.find(c).is_none() {
71                    // If so, push to the buffer
72                    buffer.push(c);
73                }
74            }
75        }
76
77        // Return the generated keyword
78        Self(buffer)
79    }
80
81    /// Convert the keyword into a 5x5 [Matrix] array type in.
82    /// TODO: This can be converted to a 1-d array
83    pub fn to_matrix(&self) -> Matrix {
84        // Initialize a matrix to null-bytes to start. They will all be overwritten
85        let mut mtx: Matrix = [['\0'; 5]; 5];
86
87        for (idx, chr) in self.0.char_indices() {
88            // Perform the x-value calcuation by using modular arithmetic
89            let x = idx % 5;
90            // Perform the y-value calculation by using integer division
91            let y = idx / 5;
92
93            // Set the char at the given x, y value
94            mtx[x][y] = chr;
95        }
96
97        // Return the matrix
98        mtx
99    }
100}
101
102/// Playfair cipher structure, stores data needed during the encryption/decryption
103pub struct Playfair {
104    /// The keyword in which we generate the matrix from.
105    keyword: Keyword,
106    /// The matrix which encryption/decryption is operated over
107    matrix: Matrix,
108}
109
110impl Cipher for Playfair {
111    /// Encryption logic for a given plaintext
112    fn encrypt(&self, plaintext: &str) -> String {
113        let mut buffer = String::new();
114        let bigrams: Vec<Bigram> = Playfair::bigramify(plaintext);
115
116        // Loop over each bigram
117        for bigram in bigrams {
118            // Get the positions of the characters, needed in performing the operations on swapping
119            // or incrementing x & y values.
120            let a_pos: Position = self.get_position_in_matrix(&bigram.0);
121            let b_pos: Position = self.get_position_in_matrix(&bigram.1);
122
123            if a_pos.0 == b_pos.0 {
124                // Case 1: They are in the same column. In this case, we increment (with wrapping)
125                // their y-values by 1.
126                buffer.push(self.matrix[a_pos.0][(a_pos.1 + 1) % 5]);
127                buffer.push(self.matrix[b_pos.0][(b_pos.1 + 1) % 5]);
128            } else if a_pos.1 == b_pos.1 {
129                // Case 2: They are in the same row. In this case, we increment (with wrapping)
130                // their x-values by 1.
131                buffer.push(self.matrix[(a_pos.0 + 1) % 5][a_pos.1]);
132                buffer.push(self.matrix[(b_pos.0 + 1) % 5][b_pos.1]);
133            } else {
134                // Case 3: They are in different rows and columns, In this case, we swap the
135                // x-values of each position and keep the same y-values.
136                buffer.push(self.matrix[b_pos.0][a_pos.1]);
137                buffer.push(self.matrix[a_pos.0][b_pos.1]);
138            }
139        }
140
141        buffer
142    }
143
144    /// Decryption logic for a given ciphertext
145    fn decrypt(&self, ciphertext: &str) -> String {
146        let mut buffer = String::new();
147        let bigrams: Vec<Bigram> = Playfair::bigramify(ciphertext);
148
149        // Loop over the bigrams
150        for bigram in bigrams {
151            // Get the positions of the characters, needed in performing the operations on swapping
152            // or decrementing x & y values.
153            let a_pos: Position = self.get_position_in_matrix(&bigram.0);
154            let b_pos: Position = self.get_position_in_matrix(&bigram.1);
155
156            if a_pos.0 == b_pos.0 {
157                // Case 1: They are in the same column. In this case, we increment (with wrapping)
158                // their y-values by 1.
159
160                // Subtract 1, producing an optional with the value from the operation. If we try
161                // to subtract 1 from 0, .checked_sub() would result in a None being returned, in
162                // which case .unwrap_or() will give us a 4, effectively giving us this 'reverse'
163                // modular arithmetic
164                let a_y = a_pos.1.checked_sub(1).unwrap_or(4);
165                let b_y = b_pos.1.checked_sub(1).unwrap_or(4);
166
167                buffer.push(self.matrix[a_pos.0][a_y]);
168                buffer.push(self.matrix[b_pos.0][b_y]);
169            } else if a_pos.1 == b_pos.1 {
170                // Case 2: They are in the same row. In this case, we increment (with wrapping)
171                // their x-values by 1.
172
173                // Subtract 1, producing an optional with the value from the operation. If we try
174                // to subtract 1 from 0, .checked_sub() would result in a None being returned, in
175                // which case .unwrap_or() will give us a 4, effectively giving us this 'reverse'
176                // modular arithmetic
177                let a_x = a_pos.0.checked_sub(1).unwrap_or(4);
178                let b_x = b_pos.0.checked_sub(1).unwrap_or(4);
179
180                buffer.push(self.matrix[a_x][a_pos.1]);
181                buffer.push(self.matrix[b_x][b_pos.1]);
182            } else {
183                // Case 3: They are in different rows and columns, In this case, we swap the
184                // x-values of each position and keep the same y-values.
185                buffer.push(self.matrix[b_pos.0][a_pos.1]);
186                buffer.push(self.matrix[a_pos.0][b_pos.1]);
187            }
188        }
189
190        buffer
191    }
192}
193
194impl Playfair {
195    /// Generates a new Playfair cipher structure with the keyword and appropriate alphabet padding to
196    /// ensure it can fit into the matrix.
197    pub fn new(kw: &str) -> Self {
198        // Generate the keyword from the given input
199        let keyword = Keyword::new(kw);
200        // Construct a matrix from the keyword.
201        let matrix = keyword.to_matrix();
202
203        // Return the playfair cipher
204        Self { keyword, matrix }
205    }
206
207    /// Bigramify takes in a string input, converts it to an even length, and splits the input into
208    /// groups of 2-tuples of characters. This is then used in the encryption/decryption
209    /// algorithms.
210    fn bigramify(input: &str) -> Vec<Bigram> {
211        let mut buffer: Vec<Bigram> = vec![];
212        // Ensure the input is only alphabetic
213        let mut input: String = input
214            .to_lowercase()
215            .chars()
216            .filter(|c| c.is_alphabetic())
217            .collect();
218
219        // Loop over the characters of the input 2 at a time, checking that there is a next one.
220        // If there are duplicates insert an 'x' to seperate the duplicates.
221        for idx in (0..input.len()).step_by(2) {
222            let a = input.chars().nth(idx).unwrap();
223
224            if let Some(b) = input.chars().nth(idx + 1) {
225                if a == b {
226                    input.insert(idx + 1, 'x');
227                }
228            }
229        }
230
231        // If we are still at an odd length, append a 0 at the end of the input.
232        if input.len() % 2 != 0 {
233            input.push('x');
234        }
235
236        // Again loop over the pairs, this time we are guarenteed that it is an even length so we
237        // don't need the `if let Some(_)` check.
238        for idx in (0..input.len()).step_by(2) {
239            let a = input.chars().nth(idx).unwrap();
240            let b = input.chars().nth(idx + 1).unwrap();
241
242            buffer.push((a, b));
243        }
244
245        // Return the buffer
246        buffer
247    }
248
249    /// Get the position of a given character withing the matrix. Returns a [Position] type, which is an
250    /// (x, y) pair of where the character is in the function. Since i = j in this implementation,
251    /// whenever the letter 'j' is searched for, just search for 'i' instead.
252    fn get_position_in_matrix(&self, to_search: &char) -> Position {
253        // Loop over each column and item.
254        for (idx, column) in self.matrix.iter().enumerate() {
255            // Check if what we are searching for is in the Matrix. This seems to be marginally
256            // faster than just another for loop and comparison.
257            if let Some(jdx) = column.iter().position(|&chr| chr == *to_search) {
258                // Return the position we found.
259                return (idx, jdx);
260            }
261        }
262
263        // If no position was found, we were probably searching for a 'j', which in our current
264        // implementation, i = j, so  return the result for searching for 'i'.
265        self.get_position_in_matrix(&'i')
266    }
267
268    /// Get a copy of the keyword of the Playfair structure
269    pub fn keyword(&self) -> &str {
270        self.keyword.0.as_str()
271    }
272
273    /// Allow updating the current keyword of the Playfair object. This may be useful if you are
274    /// encrypting and decrypting amonst multiple parties at once, and have numerous different
275    /// keywords / matricies to operate over.
276    pub fn update_keyword(&mut self, kw: &str) {
277        // Generate the new keyword from the input
278        let kw = Keyword::new(kw);
279        // Generate a new matrix from the keyword
280        let mx = kw.to_matrix();
281
282        // Update the current keyword
283        self.keyword = kw;
284        // Update the current matrix to the new matrix
285        self.matrix = mx;
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn test_keyword_no_dups() {
295        let initial = "abcdefg";
296        let kw = Keyword::new(initial);
297
298        assert_eq!(kw.0.len(), 25);
299        assert_eq!(kw.0, "abcdefghiklmnopqrstuvwxyz");
300    }
301
302    #[test]
303    fn test_keyword_with_dups() {
304        let initial = "aabbccddee";
305        let kw = Keyword::new(initial);
306
307        assert_eq!(kw.0.len(), 25);
308        assert_eq!(kw.0, "abcdefghiklmnopqrstuvwxyz");
309    }
310
311    #[test]
312    fn test_keyword_wiki_example() {
313        let initial = "playfair example";
314        let kw = Keyword::new(initial);
315
316        assert_eq!(kw.0.len(), 25);
317        assert_eq!(kw.0, "playfirexmbcdghknoqstuvwz");
318    }
319
320    #[test]
321    fn test_keyword_weird_input() {
322        let initial = "play!!!fa123ir ex^&*ample";
323        let kw = Keyword::new(initial);
324
325        assert_eq!(kw.0.len(), 25);
326        assert_eq!(kw.0, "playfirexmbcdghknoqstuvwz");
327    }
328
329    #[test]
330    fn test_keyword_one_letter_input() {
331        let initial = "iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii";
332        let kw = Keyword::new(initial);
333
334        assert_eq!(kw.0.len(), 25);
335        assert_eq!(kw.0, "iabcdefghklmnopqrstuvwxyz");
336    }
337
338    #[test]
339    fn test_getting_keyword_pf_struct() {
340        let initial = "playfair example";
341        let pf = Playfair::new(initial);
342
343        assert_eq!(pf.keyword.0, "playfirexmbcdghknoqstuvwz");
344    }
345
346    #[test]
347    fn test_bigraming_even_length() {
348        let initial = "abcd";
349        let big = Playfair::bigramify(initial);
350
351        assert_eq!(big, vec![('a', 'b'), ('c', 'd')]);
352    }
353
354    #[test]
355    fn test_bigraming_odd_length() {
356        let initial = "abc";
357        let big = Playfair::bigramify(initial);
358
359        assert_eq!(big, vec![('a', 'b'), ('c', 'x')]);
360    }
361
362    #[test]
363    fn test_bigramming_wiki() {
364        let initial = "hide the gold in the tree stump";
365        let big = Playfair::bigramify(initial);
366
367        assert_eq!(
368            big,
369            vec![
370                ('h', 'i'),
371                ('d', 'e'),
372                ('t', 'h'),
373                ('e', 'g'),
374                ('o', 'l'),
375                ('d', 'i'),
376                ('n', 't'),
377                ('h', 'e'),
378                ('t', 'r'),
379                ('e', 'x'),
380                ('e', 's'),
381                ('t', 'u'),
382                ('m', 'p'),
383            ]
384        );
385    }
386
387    #[test]
388    fn test_matrix_generation() {
389        let initial = "playfair example";
390        let kw = Keyword::new(initial);
391
392        assert_eq!(kw.0.len(), 25);
393        assert_eq!(kw.0, "playfirexmbcdghknoqstuvwz");
394
395        let mx = kw.to_matrix();
396        assert_eq!(
397            mx,
398            [
399                ['p', 'i', 'b', 'k', 't'],
400                ['l', 'r', 'c', 'n', 'u'],
401                ['a', 'e', 'd', 'o', 'v'],
402                ['y', 'x', 'g', 'q', 'w'],
403                ['f', 'm', 'h', 's', 'z']
404            ]
405        );
406    }
407
408    #[test]
409    fn test_finding_in_matrix() {
410        let initial = "playfair example";
411        let pf = Playfair::new(initial);
412
413        let pos = pf.get_position_in_matrix(&'a');
414
415        assert_eq!(pos, (2, 0));
416    }
417
418    #[test]
419    fn test_finding_i_j_equivalence() {
420        let initial = "playfair example";
421        let pf = Playfair::new(initial);
422
423        let pos_1 = pf.get_position_in_matrix(&'i');
424        let pos_2 = pf.get_position_in_matrix(&'j');
425
426        assert_eq!(pos_1, pos_2);
427    }
428
429    #[test]
430    fn test_updating_keyword() {
431        let initial = "init";
432        let mut pf = Playfair::new(initial);
433
434        assert_eq!(pf.keyword(), "intabcdefghklmopqrsuvwxyz");
435
436        let new = "playfair example";
437        pf.update_keyword(new);
438
439        assert_eq!(pf.keyword(), "playfirexmbcdghknoqstuvwz");
440    }
441}