cipher_crypt/
hill.rs

1//! In classical cryptography, the Hill cipher is a polygraphic substitution cipher based on
2//! linear algebra.
3//!
4//! Invented by Lester S. Hill in 1929, it was the first polygraphic cipher in which it was
5//! practical (though barely) to operate on more than three symbols at once. The matrix used for
6//! encryption is the cipher key, and it should be chosen randomly from the set of invertible n ×
7//! n matrices (modulo 26).
8//!
9//! Please note that this cipher uses the external library
10//! [rulinalg](https://crates.io/crates/rulinalg) to perform the linear algebra calculations. If
11//! you want to create an instance of the `Hill` struct using the `new` function, then you will
12//! need to add a dependency on the `rulinalg` crate in `Cargo.toml`. Alternatively, you could
13//! avoid dealing with matrices altogether by creating an instance of `Hill` via the function
14//! `Hill::from_phrase(...)`.
15//!
16use crate::common::alphabet;
17use crate::common::alphabet::Alphabet;
18use crate::common::cipher::Cipher;
19use num::integer::gcd;
20use rulinalg::matrix::{BaseMatrix, BaseMatrixMut, Matrix};
21
22/// A Hill cipher.
23///
24/// This struct is created by the `new()` method. See its documentation for more.
25pub struct Hill {
26    key: Matrix<isize>,
27}
28
29impl Cipher for Hill {
30    type Key = Matrix<isize>;
31    type Algorithm = Hill;
32
33    /// Initialise a Hill cipher given a key matrix.
34    ///
35    /// # Panics
36    /// * The `key` matrix is not a square
37    /// * The `key` matrix is non-invertible
38    /// * The inverse determinant of the `key` matrix cannot be calculated such that
39    /// `d*d^-1 == 1 mod 26`
40    ///
41    /// # Examples
42    ///
43    /// ```
44    /// extern crate rulinalg;
45    /// extern crate cipher_crypt;
46    ///
47    /// use rulinalg::matrix::Matrix;
48    /// use cipher_crypt::{Cipher, Hill};
49    ///
50    /// fn main() {
51    ///     //Initialise a Hill cipher from a 3 x 3 matrix
52    ///     let m = Matrix::new(3, 3, vec![2, 4, 5, 9, 2, 1, 3, 17, 7]);
53    ///     let h = Hill::new(m);
54    /// }
55    /// ```
56    ///
57    fn new(key: Matrix<isize>) -> Hill {
58        if key.cols() != key.rows() {
59            panic!("The key is not a square matrix.");
60        }
61
62        //We want to restrict the caller to supplying matrices of type isize
63        //However, the majority of the matrix operations will be done with type f64
64        let m: Matrix<f64> = key
65            .clone()
66            .try_into()
67            .expect("Could not convert Matrix of type `isize` to `f64`.");
68
69        if m.clone().inverse().is_err() || Hill::calc_inverse_key(m.clone()).is_err() {
70            panic!("The inverse of this matrix cannot be calculated for decryption.");
71        }
72
73        if gcd(m.clone().det() as isize, 26) != 1 {
74            panic!("The inverse determinant of the key cannot be calculated.");
75        }
76
77        Hill { key }
78    }
79
80    /// Encrypt a message using a Hill cipher.
81    ///
82    /// It is expected that this message contains alphabetic characters only. Due to the nature of
83    /// the hill cipher it is very difficult to transpose whitespace or symbols during the
84    /// encryption process. It will reject with `Err` if the message contains any non-alphabetic
85    /// symbols.
86    ///
87    /// You may also notice that your encrypted message is longer than the original. This will
88    /// occur when the length of the message is not a multiple of the key matrix size. To
89    /// accommodate for this potential difference, the algorithm will add `n` amount of padding
90    /// characters so that encryption can occur. It is important that these extra padding
91    /// characters are not removed till *after* the decryption process, otherwise the message will
92    /// not be transposed properly.
93    ///
94    /// # Example
95    /// Basic usage:
96    ///
97    /// ```
98    /// extern crate rulinalg;
99    /// extern crate cipher_crypt;
100    ///
101    /// use rulinalg::matrix::Matrix;
102    /// use cipher_crypt::{Cipher, Hill};
103    ///
104    /// fn main() {
105    ///     let h = Hill::new(Matrix::new(3, 3, vec![2, 4, 5, 9, 2, 1, 3, 17, 7]));
106    ///     //Padding characters are added during the encryption process
107    ///     assert_eq!("PFOGOAUCIMpf", h.encrypt("ATTACKEAST").unwrap());
108    /// }
109    /// ```
110    ///
111    fn encrypt(&self, message: &str) -> Result<String, &'static str> {
112        //A small insight into the theory behind encrypting with the hill cipher will be explained
113        //thusly.
114        /*
115            The basic process is to break a message up into chunks (a set of character vectors),
116            whose individual length is equal to the length of the square matrix key.
117
118            Once we have obtained these chunks, we transform them so that the character is replaced
119            with its index within the alphabet. For example:
120            ['A', 'T', 'T'] = [0, 19, 19]
121
122            Once we have this list of indices, we perform a matrix multiplication of this
123            vector/matrix with the key matrix. For example say we had a key `k` ...
124
125                k * [0, 19, 19] mod 26 = [15, 5, 14] -> ['P', 'F', 'O'] encrypted characters
126
127                where k = [2, 4, 5; 9, 2, 1; 3, 17, 7]
128
129            This is repeated until all the 'chunks' of the message have been consumed/transformed.
130        */
131        Hill::transform_message(&self.key.clone().try_into().unwrap(), message)
132    }
133
134    /// Decrypt a message using a Hill cipher.
135    ///
136    /// It is expected that this message contains alphabetic characters only. Due to the nature of
137    /// the hill cipher it is very difficult to transpose whitespace or symbols during the
138    /// encryption process. It will reject with `Err` if the message contains any non-alphabetic
139    /// symbols.
140    ///
141    /// You may also notice that your encrypted message is longer than the original. This will
142    /// occur when the length of the message is not a multiple of the key matrix size. See encrypt
143    /// function for more information.
144    ///
145    /// # Examples
146    /// Example with stripping out padding:
147    ///
148    /// ```
149    /// extern crate rulinalg;
150    /// extern crate cipher_crypt;
151    ///
152    /// use rulinalg::matrix::Matrix;
153    /// use cipher_crypt::{Cipher, Hill};
154    ///
155    /// fn main() {
156    ///     let m = "ATTACKEAST";
157    ///     let h = Hill::new(Matrix::new(3, 3, vec![2, 4, 5, 9, 2, 1, 3, 17, 7]));;
158    ///
159    ///     let c = h.encrypt(m).unwrap();
160    ///     let padding = c.len() - m.len();
161    ///
162    ///     let p = h.decrypt(&c).unwrap();
163    ///     assert_eq!(m, p[0..(p.len() - padding)].to_string());
164    /// }
165    /// ```
166    ///
167    fn decrypt(&self, ciphertext: &str) -> Result<String, &'static str> {
168        /*
169        The decryption process is very similar to the encryption process as explained in
170        its function. However, the key is inverted in such way that performing a matrix
171        multiplication on the character vector will result in the original unencrypted chars.
172
173        For example, given the chunk [15, 5, 14] = ['P', 'F', 'O], and the inverse of the key
174        `k`, `k^-1`. Decryption occurs like so:
175
176            k^-1 * [15, 5, 14] mod 26 = [0, 19, 19] -> ['A', 'T', 'T'] decrypted characters
177
178        This is repeated until all the 'chunks' of the message have been consumed/transformed.
179        */
180        let inverse_key = Hill::calc_inverse_key(self.key.clone().try_into().unwrap())?;
181
182        Hill::transform_message(&inverse_key, ciphertext)
183    }
184}
185
186impl Hill {
187    /// Initialise a Hill cipher given a phrase.
188    ///
189    /// The position of each character within the alphabet is used to construct the
190    /// matrix key of the cipher. The variable `chunk_size` defines how many chars (or chunks)
191    /// of a message will be transposed during encryption/decryption.
192    ///
193    ///
194    /// # Panics
195    /// * The `chunk_size` is less than 2
196    /// * The square of `chunk_size` is not equal to the phrase length
197    /// * The phrase contains non-alphabetic symbols
198    /// * Any of the Err conditions as stipulated by the `new()` fn
199    ///
200    /// # Example
201    ///
202    /// ```
203    /// use cipher_crypt::{Cipher, Hill};
204    ///
205    /// let h = Hill::from_phrase("CEFJCBDRH", 3);
206    /// h.encrypt("thing");
207    /// ```
208    ///
209    pub fn from_phrase(phrase: &str, chunk_size: usize) -> Hill {
210        if chunk_size < 2 {
211            panic!("The chunk size must be greater than 1.");
212        }
213
214        if chunk_size * chunk_size != phrase.len() {
215            panic!("The square of the chunk size must equal the length of the phrase.");
216        }
217
218        if !alphabet::STANDARD.is_valid(phrase) {
219            panic!("Phrase cannot contain non-alphabetic symbols.");
220        }
221
222        let matrix: Vec<isize> = phrase
223            .chars()
224            .map(|c| alphabet::STANDARD.find_position(c).unwrap() as isize)
225            .collect();
226
227        Hill::new(Matrix::new(chunk_size, chunk_size, matrix))
228    }
229
230    /// Core logic of the hill cipher. Transposing messages with matrices
231    ///
232    fn transform_message(key: &Matrix<f64>, message: &str) -> Result<String, &'static str> {
233        //Only allow chars in the alphabet (no whitespace or symbols)
234        if !alphabet::STANDARD.is_valid(message) {
235            return Err("Message cannot contain non-alphabetic symbols.");
236        }
237
238        let mut transformed_message = String::new();
239        let mut buffer = message.to_string();
240        let chunk_size = key.rows();
241
242        //The message is processed/transposed in multiples of the matrix size, therefore
243        //the message length must be a multiple of this value. If not, add extra padding to make
244        //it so.
245        if buffer.len() % chunk_size > 0 {
246            let padding = chunk_size - (buffer.len() % chunk_size);
247            for _ in 0..padding {
248                buffer.push('a');
249            }
250        }
251
252        //For each set of chunks in the message, transform based on the key.
253        let mut i = 0;
254        while i < buffer.len() {
255            match Hill::transform_chunk(key, &buffer[i..(i + chunk_size)]) {
256                Ok(s) => transformed_message.push_str(&s),
257                Err(e) => return Err(e),
258            }
259
260            i += chunk_size;
261        }
262
263        //Return the transformed message - this may have extra padding appended
264        Ok(transformed_message)
265    }
266
267    /// Transforming a chunk of the message, whose length is determined by the size of the matrix
268    ///
269    fn transform_chunk(key: &Matrix<f64>, chunk: &str) -> Result<String, &'static str> {
270        let mut transformed = String::new();
271
272        if !alphabet::STANDARD.is_valid(chunk) {
273            panic!("Chunk contains a non-alphabetic symbol.");
274        }
275
276        if key.rows() != chunk.len() {
277            return Err("Cannot perform transformation on unequal vector lengths");
278        }
279
280        //Find the integer representation of the characters
281        //e.g. ['A', 'T', 'T'] -> [0, 19, 19]
282        let index_representation: Vec<f64> = chunk
283            .chars()
284            .map(|c| alphabet::STANDARD.find_position(c).unwrap() as f64)
285            .collect();
286
287        //Perform the transformation `k * [0, 19, 19] mod 26`
288        let mut product = key * Matrix::new(index_representation.len(), 1, index_representation);
289        product = product.apply(&|x| (x % 26.0).round());
290
291        //Convert the transformed indices back into characters of the alphabet
292        for (i, pos) in product.iter().enumerate() {
293            let orig = chunk
294                .chars()
295                .nth(i)
296                .expect("Expected to find char at index.");
297
298            transformed.push(alphabet::STANDARD.get_letter(*pos as usize, orig.is_uppercase()));
299        }
300
301        Ok(transformed)
302    }
303
304    /// Calculates the inverse key for decryption
305    ///
306    fn calc_inverse_key(key: Matrix<f64>) -> Result<Matrix<f64>, &'static str> {
307        let det = key.clone().det();
308
309        //Find the inverse determinant such that: d*d^-1 = 1 mod 26
310        if let Some(det_inv) = alphabet::STANDARD.multiplicative_inverse(det as isize) {
311            return Ok(key.inverse().unwrap().apply(&|x| {
312                let y = (x * det as f64).round() as isize;
313                (alphabet::STANDARD.modulo(y) as f64 * det_inv as f64) % 26.0
314            }));
315        }
316
317        Err("Inverse for determinant could not be found.")
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn keygen_from_phrase() {
327        Hill::from_phrase("CEFJCBDRH", 3);
328    }
329
330    #[test]
331    #[should_panic]
332    fn invalid_phrase() {
333        Hill::from_phrase("killer", 2);
334    }
335
336    #[test]
337    fn encrypt_no_padding_req() {
338        let h = Hill::new(Matrix::new(3, 3, vec![2, 4, 5, 9, 2, 1, 3, 17, 7]));
339
340        let m = "ATTACKatDAWN";
341        assert_eq!(m, h.decrypt(&h.encrypt(m).unwrap()).unwrap());
342    }
343
344    #[test]
345    fn encrypt_with_symbols() {
346        let h = Hill::from_phrase("CEFJCBDRH", 3);
347        assert!(h.encrypt("This won!t w@rk").is_err());
348    }
349
350    #[test]
351    fn decrypt_with_symbols() {
352        let h = Hill::from_phrase("CEFJCBDRH", 3);
353        assert!(h.decrypt("This won!t w@rk").is_err());
354    }
355
356    #[test]
357    fn encrypt_padding_req() {
358        let h = Hill::new(Matrix::new(3, 3, vec![2, 4, 5, 9, 2, 1, 3, 17, 7]));
359        let m = "ATTACKATDAWNz";
360
361        let e = h.encrypt(m).unwrap();
362        assert_eq!("PFOGOANPGXFXyrx", e);
363
364        let d = h.decrypt(&e).unwrap();
365        assert_eq!("ATTACKATDAWNzaa", d);
366    }
367
368    #[test]
369    fn valid_key() {
370        Hill::new(Matrix::new(3, 3, vec![2, 4, 5, 9, 2, 1, 3, 17, 7]));
371    }
372
373    #[test]
374    #[should_panic]
375    fn non_square_matrix() {
376        //A 3 x 2 matrix
377        Hill::new(Matrix::new(3, 2, vec![2, 4, 9, 2, 3, 17]));
378    }
379
380    #[test]
381    #[should_panic]
382    fn non_invertable_matrix() {
383        Hill::new(Matrix::new(3, 3, vec![2, 2, 3, 6, 6, 9, 1, 4, 8]));
384    }
385}