cipher_crypt/
columnar_transposition.rs

1//! The Columnar cipher is a transposition cipher. In columnar transposition the message is
2//! written out in rows of a fixed length, and then transcribed to a message via the columns.
3//! The columns are scrambled based on a secret key.
4//!
5//! Columnar transposition continued to be used as a component of more complex ciphers up
6//! until the 1950s.
7//!
8use crate::common::alphabet::Alphabet;
9use crate::common::cipher::Cipher;
10use crate::common::{alphabet, keygen};
11
12/// A Columnar Transposition cipher.
13/// This struct is created by the `new()` method. See its documentation for more.
14pub struct ColumnarTransposition {
15    keystream: String,
16    null_char: Option<char>,
17    derived_key: Vec<(char, Vec<char>)>,
18}
19
20impl Cipher for ColumnarTransposition {
21    type Key = (String, Option<char>);
22    type Algorithm = ColumnarTransposition;
23
24    /// Initialize a Columnar Transposition cipher.
25    ///
26    /// Where...
27    ///
28    /// * Elements of `keystream` are used as the column identifiers.
29    /// * The optional `null_char` is used to pad messages of uneven length.
30    /// * The `derived_key` is used to initialise the column structures in the cipher.
31    ///
32    /// # Panics
33    /// * The `keystream` length is 0.
34    /// * The `keystream` contains non-alphanumeric symbols.
35    /// * The `keystream` contains duplicate characters.
36    /// * The `null_char` is a character within the `keystream`
37    ///
38    fn new(key: (String, Option<char>)) -> ColumnarTransposition {
39        if let Some(null_char) = key.1 {
40            if key.0.contains(null_char) {
41                panic!("The `keystream` contains a `null_char`.");
42            }
43        }
44
45        ColumnarTransposition {
46            derived_key: keygen::columnar_key(&key.0),
47            keystream: key.0,
48            null_char: key.1,
49        }
50    }
51
52    /// Encrypt a message with a Columnar Transposition cipher.
53    ///
54    /// All characters (including utf8) can be encrypted during the transposition process.
55    /// However, it is important to note that if padding characters are being used (`null_char`),
56    /// the user must ensure that the message does not contain these padding characters, otherwise
57    /// problems will occur during decryption. For this reason, the function will `Err` if it
58    /// detects padding characters in the message to be encrypted.
59    ///
60    /// # Examples
61    /// Basic usage:
62    ///
63    /// ```
64    /// use cipher_crypt::{Cipher, ColumnarTransposition};
65    ///
66    /// let key_word = String::from("zebras");
67    /// let null_char = None;
68    ///
69    /// let ct = ColumnarTransposition::new((key_word, null_char));;
70    ///
71    /// assert_eq!("respce!uemeers-taSs g", ct.encrypt("Super-secret message!").unwrap());
72    /// ```
73    ///
74    fn encrypt(&self, message: &str) -> Result<String, &'static str> {
75        if let Some(null_char) = self.null_char {
76            if message.contains(null_char) {
77                return Err("Message contains null characters.");
78            }
79        }
80
81        let mut key = self.derived_key.clone();
82
83        //Construct the column
84        let mut i = 0;
85        let mut chars = message.trim_end().chars(); //Any trailing spaces will be stripped
86        loop {
87            if let Some(c) = chars.next() {
88                key[i].1.push(c);
89            } else if i > 0 {
90                if let Some(null_char) = self.null_char {
91                    key[i].1.push(null_char)
92                }
93            } else {
94                break;
95            }
96
97            i = (i + 1) % key.len();
98        }
99
100        //Sort the key based on it's alphabet positions
101        key.sort_by(|a, b| {
102            alphabet::STANDARD
103                .find_position(a.0)
104                .unwrap()
105                .cmp(&alphabet::STANDARD.find_position(b.0).unwrap())
106        });
107
108        //Construct the cipher text
109        let ciphertext: String = key
110            .iter()
111            .map(|column| column.1.iter().collect::<String>())
112            .collect();
113
114        Ok(ciphertext)
115    }
116
117    /// Decrypt a ciphertext with a Columnar Transposition cipher.
118    ///
119    /// # Examples
120    /// Basic usage:
121    ///
122    /// ```
123    /// use cipher_crypt::{Cipher, ColumnarTransposition};
124    ///
125    /// let key_word = String::from("zebras");
126    /// let null_char = None;
127    ///
128    /// let ct = ColumnarTransposition::new((key_word, null_char));;
129    /// assert_eq!("Super-secret message!", ct.decrypt("respce!uemeers-taSs g").unwrap());
130    /// ```
131    /// Using whitespace as null (special case):
132    ///  This will strip only trailing whitespace in message during decryption
133    ///
134    /// ```
135    /// use cipher_crypt::{Cipher, ColumnarTransposition};
136    ///
137    /// let key_word = String::from("zebras");
138    /// let null_char = None;
139    /// let message = "we are discovered  "; // Only trailing spaces will be stripped
140    ///
141    /// let ct = ColumnarTransposition::new((key_word, null_char));;
142    ///
143    /// assert_eq!(ct.decrypt(&ct.encrypt(message).unwrap()).unwrap(),"we are discovered");
144    /// ```
145    ///
146    fn decrypt(&self, ciphertext: &str) -> Result<String, &'static str> {
147        let mut key = self.derived_key.clone();
148
149        // Transcribe the ciphertext along each column
150        let mut chars = ciphertext.chars();
151        // We only know the maximum length, as there may be null spaces
152        let max_col_size: usize =
153            (ciphertext.chars().count() as f32 / self.keystream.len() as f32).ceil() as usize;
154
155        // Once we know the max col size, we need to fill the columns according to order of the
156        // keyword. So, if the keyword is 'zebras' then the largest column is 'z' according to
157        // offset size. If keyword_length is 6 and cipher_text is 31 there are 5 columns that are
158        // offset.
159        let offset = key.len() - (ciphertext.chars().count() % key.len());
160
161        // Now we need to know which columns are offset
162        let offset_cols = if self.null_char.is_none() && offset != key.len() {
163            key.iter()
164                .map(|e| e.0)
165                .rev()
166                .take(offset)
167                .collect::<String>()
168        } else {
169            String::from("")
170        };
171
172        //Sort the key so that it's in its encryption order
173        key.sort_by(|a, b| {
174            alphabet::STANDARD
175                .find_position(a.0)
176                .unwrap()
177                .cmp(&alphabet::STANDARD.find_position(b.0).unwrap())
178        });
179
180        'outer: for column in &mut key {
181            loop {
182                let offset_num = if offset_cols.contains(column.0) { 1 } else { 0 };
183                // This will test for offset size
184                if column.1.len() >= max_col_size - offset_num {
185                    break;
186                } else if let Some(c) = chars.next() {
187                    column.1.push(c);
188                } else {
189                    break 'outer; //No more characters left in ciphertext
190                }
191            }
192        }
193
194        let mut plaintext = String::new();
195        for i in 0..max_col_size {
196            for chr in self.keystream.chars() {
197                // Outer getting the key char
198                if let Some(column) = key.iter().find(|x| x.0 == chr) {
199                    if i < column.1.len() {
200                        let c = column.1[i];
201                        // Special case for whitespace as the nulls can be trimmed
202                        if let Some(null_char) = self.null_char {
203                            if c == null_char && !c.is_whitespace() {
204                                break;
205                            }
206                        }
207                        plaintext.push(c);
208                    }
209                } else {
210                    return Err("Could not find column during decryption.");
211                }
212            }
213        }
214
215        Ok(plaintext.trim_end().to_string())
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn simple() {
225        let message = "wearediscovered";
226
227        let key_word = String::from("zebras");
228        let null_char = Some('\u{0}');
229        let ct = ColumnarTransposition::new((key_word, null_char));
230
231        assert_eq!(ct.decrypt(&ct.encrypt(message).unwrap()).unwrap(), message);
232    }
233
234    #[test]
235    fn simple_no_padding() {
236        let message = "wearediscovered";
237
238        let key_word = String::from("zebras");
239        let null_char = None;
240        let ct = ColumnarTransposition::new((key_word, null_char));
241
242        assert_eq!(ct.decrypt(&ct.encrypt(message).unwrap()).unwrap(), message);
243    }
244
245    #[test]
246    fn with_utf8() {
247        let message = "Peace, Freedom 🗡️ and Liberty!";
248
249        let key_word = String::from("zebras");
250        let null_char = Some('\u{0}');
251        let ct = ColumnarTransposition::new((key_word, null_char));
252        let encrypted = ct.encrypt(message).unwrap();
253        assert_eq!(ct.decrypt(&encrypted).unwrap(), message);
254    }
255
256    #[test]
257    fn with_utf8_no_padding() {
258        let message = "Peace, Freedom 🗡️ and Liberty!";
259
260        let key_word = String::from("zebras");
261        let null_char = None;
262        let ct = ColumnarTransposition::new((key_word, null_char));
263        let encrypted = ct.encrypt(message).unwrap();
264        assert_eq!(ct.decrypt(&encrypted).unwrap(), message);
265    }
266
267    #[test]
268    fn single_column() {
269        let message = "we are discovered";
270
271        let key_word = String::from("z");
272        let null_char = Some('\u{0}');
273        let ct = ColumnarTransposition::new((key_word, null_char));
274        assert_eq!(ct.decrypt(&ct.encrypt(message).unwrap()).unwrap(), message);
275    }
276
277    #[test]
278    fn single_column_no_padding() {
279        let message = "we are discovered";
280
281        let key_word = String::from("z");
282        let null_char = None;
283        let ct = ColumnarTransposition::new((key_word, null_char));
284        assert_eq!(ct.decrypt(&ct.encrypt(message).unwrap()).unwrap(), message);
285    }
286
287    #[test]
288    fn trailing_spaces() {
289        let message = "we are discovered  "; //The trailing spaces will be stripped
290
291        let key_word = String::from("z");
292        let null_char = None;
293        let ct = ColumnarTransposition::new((key_word, null_char));
294
295        assert_eq!(
296            ct.decrypt(&ct.encrypt(message).unwrap()).unwrap(),
297            "we are discovered"
298        );
299    }
300
301    #[test]
302    fn plaintext_containing_padding() {
303        let key_word = String::from("zebras");
304        let null_char = Some(' ');
305        let ct = ColumnarTransposition::new((key_word, null_char));
306
307        let plain_text = "This will fail because of spaces.";
308        assert!(ct.encrypt(plain_text).is_err());
309    }
310
311    #[test]
312    fn trailing_spaces_no_padding() {
313        let message = "we are discovered  "; //The trailing spaces will be stripped
314
315        let key_word = String::from("z");
316        let null_char = None;
317        let ct = ColumnarTransposition::new((key_word, null_char));
318
319        assert_eq!(
320            ct.decrypt(&ct.encrypt(message).unwrap()).unwrap(),
321            "we are discovered"
322        );
323    }
324
325    #[test]
326    #[should_panic]
327    fn padding_in_key() {
328        ColumnarTransposition::new((String::from("zebras"), Some('z')));
329    }
330}