Skip to content

Commit 8979274

Browse files
authored
Update hill_cipher.py
1 parent 8663333 commit 8979274

1 file changed

Lines changed: 49 additions & 147 deletions

File tree

ciphers/hill_cipher.py

Lines changed: 49 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -37,194 +37,96 @@
3737
import numpy as np
3838
from maths.greatest_common_divisor import greatest_common_divisor
3939

40-
4140
class HillCipher:
42-
key_string = string.ascii_uppercase + string.digits
43-
# This cipher takes alphanumerics into account
44-
# i.e. a total of 36 characters
45-
46-
# take x and return x % len(key_string)
47-
modulus = np.vectorize(lambda x: x % 36)
48-
49-
to_int = np.vectorize(round)
41+
key_string = string.ascii_uppercase + string.digits # 36 alphanumeric chars
42+
modulus = np.vectorize(lambda x: x % 36) # Mod 36 operation
43+
to_int = np.vectorize(round) # Round to nearest integer
5044

5145
def __init__(self, encrypt_key: np.ndarray) -> None:
52-
"""
53-
encrypt_key is an NxN numpy array
54-
"""
55-
self.encrypt_key = self.modulus(encrypt_key) # mod36 calc's on the encrypt key
56-
self.check_determinant() # validate the determinant of the encryption key
57-
self.break_key = encrypt_key.shape[0]
46+
self.encrypt_key = self.modulus(encrypt_key)
47+
self.check_determinant() # Validate key determinant
48+
self.break_key = encrypt_key.shape[0] # Matrix order
5849

5950
def replace_letters(self, letter: str) -> int:
60-
"""
61-
>>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
62-
>>> hill_cipher.replace_letters('T')
63-
19
64-
>>> hill_cipher.replace_letters('0')
65-
26
66-
"""
51+
"""Map char to index (A=0, Z=25, 0=26, 9=35)"""
6752
return self.key_string.index(letter)
6853

6954
def replace_digits(self, num: int) -> str:
70-
"""
71-
>>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
72-
>>> hill_cipher.replace_digits(19)
73-
'T'
74-
>>> hill_cipher.replace_digits(26)
75-
'0'
76-
"""
77-
# Directly use integer index without rounding
55+
"""Map index back to char"""
7856
return self.key_string[num]
7957

8058
def check_determinant(self) -> None:
81-
"""
82-
>>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
83-
>>> hill_cipher.check_determinant()
84-
"""
59+
"""Ensure det(key) is coprime with 36"""
8560
det = round(np.linalg.det(self.encrypt_key))
86-
8761
if det < 0:
88-
det = det % len(self.key_string)
89-
90-
req_l = len(self.key_string)
62+
det %= len(self.key_string)
63+
9164
if greatest_common_divisor(det, len(self.key_string)) != 1:
92-
msg = (
93-
f"determinant modular {req_l} of encryption key({det}) "
94-
f"is not co prime w.r.t {req_l}.\nTry another key."
95-
)
96-
raise ValueError(msg)
65+
raise ValueError(f"Det {det} not coprime with 36. Try another key.")
9766

9867
def process_text(self, text: str) -> str:
99-
"""
100-
>>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
101-
>>> hill_cipher.process_text('Testing Hill Cipher')
102-
'TESTINGHILLCIPHERR'
103-
>>> hill_cipher.process_text('hello')
104-
'HELLOO'
105-
"""
106-
# Filter valid characters and convert to uppercase
107-
chars = [char for char in text.upper() if char in self.key_string]
108-
109-
# Pad with last character to make length multiple of break_key
110-
last = chars[-1]
68+
"""Convert to uppercase, remove invalid chars, pad to multiple of break_key"""
69+
chars = [c for c in text.upper() if c in self.key_string]
70+
last = chars[-1] if chars else 'A'
11171
while len(chars) % self.break_key != 0:
11272
chars.append(last)
113-
11473
return "".join(chars)
11574

11675
def encrypt(self, text: str) -> str:
117-
"""
118-
>>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
119-
>>> hill_cipher.encrypt('testing hill cipher')
120-
'WHXYJOLM9C6XT085LL'
121-
>>> hill_cipher.encrypt('hello')
122-
'85FF00'
123-
"""
124-
# Preprocess text and initialize encrypted string
76+
"""Encrypt text using Hill cipher"""
12577
text = self.process_text(text.upper())
12678
encrypted = ""
127-
128-
# Process text in batches of size break_key
129-
for i in range(0, len(text) - self.break_key + 1, self.break_key):
130-
batch = text[i : i + self.break_key]
131-
# Convert characters to numerical values
132-
vec = [self.replace_letters(char) for char in batch]
79+
for i in range(0, len(text), self.break_key):
80+
batch = text[i:i+self.break_key]
81+
vec = [self.replace_letters(c) for c in batch]
13382
batch_vec = np.array([vec]).T
134-
135-
# Matrix multiplication with encryption key
136-
batch_encrypted = self.modulus(self.encrypt_key.dot(batch_vec)).T.tolist()[
137-
0
138-
]
139-
# Convert numerical results back to characters
140-
encrypted_batch = "".join(
141-
self.replace_digits(int(round(num))) for num in batch_encrypted
142-
)
143-
encrypted += encrypted_batch
144-
83+
batch_encrypted = self.modulus(self.encrypt_key.dot(batch_vec)).T.tolist()[0]
84+
encrypted += "".join(self.replace_digits(int(round(n))) for n in batch_encrypted)
14585
return encrypted
146-
def make_decrypt_key(self) -> np.ndarray:
147-
"""
148-
>>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
149-
>>> hill_cipher.make_decrypt_key()
150-
array([[ 6, 25],
151-
[ 5, 26]])
152-
"""
153-
# Calculate determinant of encryption key
154-
det = round(np.linalg.det(self.encrypt_key))
15586

87+
def make_decrypt_key(self) -> np.ndarray:
88+
"""Calculate decryption key matrix"""
89+
det = round(np.linalg.det(self.encrypt_key))
15690
if det < 0:
157-
det = det % len(self.key_string)
158-
det_inv = None
159-
160-
# Find modular inverse of determinant
161-
for i in range(len(self.key_string)):
162-
if (det * i) % len(self.key_string) == 1:
163-
det_inv = i
164-
break
165-
166-
# Calculate inverse key matrix
167-
inv_key = (
168-
det_inv * np.linalg.det(self.encrypt_key) * np.linalg.inv(self.encrypt_key)
169-
)
170-
91+
det %= len(self.key_string)
92+
93+
# Find modular inverse of det
94+
det_inv = next(i for i in range(36) if (det * i) % 36 == 1)
95+
96+
# Calculate inverse key
97+
inv_key = det_inv * np.linalg.det(self.encrypt_key) * np.linalg.inv(self.encrypt_key)
17198
return self.to_int(self.modulus(inv_key))
17299

173100
def decrypt(self, text: str) -> str:
174-
"""
175-
>>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
176-
>>> hill_cipher.decrypt('WHXYJOLM9C6XT085LL')
177-
'TESTINGHILLCIPHERR'
178-
>>> hill_cipher.decrypt('85FF00')
179-
'HELLOO'
180-
"""
181-
# Get decryption key and preprocess text
101+
"""Decrypt text using Hill cipher"""
182102
decrypt_key = self.make_decrypt_key()
183103
text = self.process_text(text.upper())
184104
decrypted = ""
185-
186-
# Process text in batches of size break_key
187-
for i in range(0, len(text) - self.break_key + 1, self.break_key):
188-
batch = text[i : i + self.break_key]
189-
# Convert characters to numerical values
190-
vec = [self.replace_letters(char) for char in batch]
105+
for i in range(0, len(text), self.break_key):
106+
batch = text[i:i+self.break_key]
107+
vec = [self.replace_letters(c) for c in batch]
191108
batch_vec = np.array([vec]).T
192-
193-
# Matrix multiplication with decryption key
194109
batch_decrypted = self.modulus(decrypt_key.dot(batch_vec)).T.tolist()[0]
195-
# Convert numerical results back to characters
196-
decrypted_batch = "".join(
197-
self.replace_digits(int(round(num))) for num in batch_decrypted
198-
)
199-
decrypted += decrypted_batch
200-
110+
decrypted += "".join(self.replace_digits(int(round(n))) for n in batch_decrypted)
201111
return decrypted
202-
def main() -> None:
203-
"""Command-line interface for Hill Cipher"""
204-
n = int(input("Enter the order of the encryption key: "))
205-
hill_matrix = []
206-
207-
print("Enter each row of the encryption key with space separated integers")
208-
for _ in range(n):
209-
row = [int(x) for x in input().split()]
210-
hill_matrix.append(row)
211-
212-
hc = HillCipher(np.array(hill_matrix))
213112

214-
print("Would you like to encrypt or decrypt some text? (1 or 2)")
215-
option = input("\n1. Encrypt\n2. Decrypt\n")
113+
def main() -> None:
114+
"""CLI for Hill Cipher"""
115+
n = int(input("Enter key order: "))
116+
print(f"Enter {n} rows of space-separated integers:")
117+
matrix = [list(map(int, input().split())) for _ in range(n)]
118+
119+
hc = HillCipher(np.array(matrix))
120+
121+
option = input("1. Encrypt\n2. Decrypt\nChoose: ")
122+
text = input("Enter text: ")
123+
216124
if option == "1":
217-
text_e = input("What text would you like to encrypt?: ")
218-
print("Your encrypted text is:")
219-
print(hc.encrypt(text_e))
125+
print("Encrypted:", hc.encrypt(text))
220126
elif option == "2":
221-
text_d = input("What text would you like to decrypt?: ")
222-
print("Your decrypted text is:")
223-
print(hc.decrypt(text_d))
224-
127+
print("Decrypted:", hc.decrypt(text))
225128

226129
if __name__ == "__main__":
227130
import doctest
228-
229131
doctest.testmod()
230132
main()

0 commit comments

Comments
 (0)