Skip to content

Commit 1655a82

Browse files
authored
Update hill_cipher.py
1 parent e60ccc9 commit 1655a82

1 file changed

Lines changed: 14 additions & 57 deletions

File tree

ciphers/hill_cipher.py

Lines changed: 14 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,8 @@
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-
371
import string
2+
383
import numpy as np
394
from maths.greatest_common_divisor import greatest_common_divisor
405

41-
426
class 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-
12180
def 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\n2. Decrypt\nChoose: ")
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-
13896
if __name__ == "__main__":
13997
import doctest
140-
14198
doctest.testmod()
14299
main()

0 commit comments

Comments
 (0)