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}