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}