1- """
2- Hill Cipher:
3- The 'HillCipher' class below implements the Hill Cipher algorithm which uses
4- modern linear algebra techniques to encode and decode text using an encryption
5- key matrix.
6-
7- Algorithm:
8- Let the order of the encryption key be N (as it is a square matrix).
9- Your text is divided into batches of length N and converted to numerical vectors
10- by a simple mapping starting with A=0 and so on.
11-
12- The key is then multiplied with the newly created batch vector to obtain the
13- encoded vector. After each multiplication modular 36 calculations are performed
14- on the vectors so as to bring the numbers between 0 and 36 and then mapped with
15- their corresponding alphanumerics.
16-
17- While decrypting, the decrypting key is found which is the inverse of the
18- encrypting key modular 36. The same process is repeated for decrypting to get
19- the original message back.
20-
21- Constraints:
22- The determinant of the encryption key matrix must be relatively prime w.r.t 36.
23-
24- Note:
25- This implementation only considers alphanumerics in the text. If the length of
26- the text to be encrypted is not a multiple of the break key(the length of one
27- batch of letters), the last character of the text is added to the text until the
28- length of the text reaches a multiple of the break_key. So the text after
29- decrypting might be a little different than the original text.
30-
31- References:
32- https://apprendre-en-ligne.net/crypto/hill/Hillciph.pdf
33- https://www.youtube.com/watch?v=kfmNeskzs2o
34- https://www.youtube.com/watch?v=4RhLNDqcjpA
35- """
36-
371import string
2+
383import numpy as np
394from maths .greatest_common_divisor import greatest_common_divisor
405
41-
426class HillCipher :
437 key_string = string .ascii_uppercase + string .digits # 36 alphanumeric chars
448 modulus = np .vectorize (lambda x : x % 36 ) # Mod 36 operation
@@ -62,15 +26,15 @@ def check_determinant(self) -> None:
6226 det = round (np .linalg .det (self .encrypt_key ))
6327 if det < 0 :
6428 det %= len (self .key_string )
65-
29+
6630 error_msg = f"Det { det } not coprime with 36. Try another key."
6731 if greatest_common_divisor (det , len (self .key_string )) != 1 :
6832 raise ValueError (error_msg )
6933
7034 def process_text (self , text : str ) -> str :
7135 """Convert to uppercase, remove invalid chars, pad to multiple of break_key"""
7236 chars = [c for c in text .upper () if c in self .key_string ]
73- last = chars [- 1 ] if chars else "A"
37+ last = chars [- 1 ] if chars else 'A'
7438 while len (chars ) % self .break_key != 0 :
7539 chars .append (last )
7640 return "" .join (chars )
@@ -80,28 +44,24 @@ def encrypt(self, text: str) -> str:
8044 text = self .process_text (text .upper ())
8145 encrypted = ""
8246 for i in range (0 , len (text ), self .break_key ):
83- batch = text [i : i + self .break_key ]
47+ batch = text [i : i + self .break_key ]
8448 vec = [self .replace_letters (c ) for c in batch ]
8549 batch_vec = np .array ([vec ]).T
86- batch_encrypted = self .modulus (self .encrypt_key .dot (batch_vec )).T .tolist ()[
87- 0
88- ]
89- encrypted += "" .join (self .replace_digits (round (n )) for n in batch_encrypted )
50+ batch_encrypted = self .modulus (self .encrypt_key .dot (batch_vec )).T .tolist ()[0 ]
51+ encrypted += "" .join (self .replace_digits (round (n )) for n in batch_encrypted
9052 return encrypted
9153
9254 def make_decrypt_key (self ) -> np .ndarray :
9355 """Calculate decryption key matrix"""
9456 det = round (np .linalg .det (self .encrypt_key ))
9557 if det < 0 :
9658 det %= len (self .key_string )
97-
59+
9860 # Find modular inverse of det
9961 det_inv = next (i for i in range (36 ) if (det * i ) % 36 == 1 )
100-
62+
10163 # Calculate inverse key
102- inv_key = (
103- det_inv * np .linalg .det (self .encrypt_key ) * np .linalg .inv (self .encrypt_key )
104- )
64+ inv_key = det_inv * np .linalg .det (self .encrypt_key ) * np .linalg .inv (self .encrypt_key )
10565 return self .to_int (self .modulus (inv_key ))
10666
10767 def decrypt (self , text : str ) -> str :
@@ -110,33 +70,30 @@ def decrypt(self, text: str) -> str:
11070 text = self .process_text (text .upper ())
11171 decrypted = ""
11272 for i in range (0 , len (text ), self .break_key ):
113- batch = text [i : i + self .break_key ]
73+ batch = text [i : i + self .break_key ]
11474 vec = [self .replace_letters (c ) for c in batch ]
11575 batch_vec = np .array ([vec ]).T
11676 batch_decrypted = self .modulus (decrypt_key .dot (batch_vec )).T .tolist ()[0 ]
117- decrypted += "" .join (self .replace_digits (round (n )) for n in batch_decrypted )
77+ decrypted += "" .join (self .replace_digits (round (n )) for n in batch_decrypted
11878 return decrypted
11979
120-
12180def main () -> None :
12281 """CLI for Hill Cipher"""
12382 n = int (input ("Enter key order: " ))
12483 print (f"Enter { n } rows of space-separated integers:" )
12584 matrix = [list (map (int , input ().split ())) for _ in range (n )]
126-
85+
12786 hc = HillCipher (np .array (matrix ))
128-
87+
12988 option = input ("1. Encrypt\n 2. Decrypt\n Choose: " )
13089 text = input ("Enter text: " )
131-
90+
13291 if option == "1" :
13392 print ("Encrypted:" , hc .encrypt (text ))
13493 elif option == "2" :
13594 print ("Decrypted:" , hc .decrypt (text ))
13695
137-
13896if __name__ == "__main__" :
13997 import doctest
140-
14198 doctest .testmod ()
14299 main ()
0 commit comments