|
37 | 37 | import numpy as np |
38 | 38 | from maths.greatest_common_divisor import greatest_common_divisor |
39 | 39 |
|
40 | | - |
41 | 40 | 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 |
50 | 44 |
|
51 | 45 | 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 |
58 | 49 |
|
59 | 50 | 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)""" |
67 | 52 | return self.key_string.index(letter) |
68 | 53 |
|
69 | 54 | 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""" |
78 | 56 | return self.key_string[num] |
79 | 57 |
|
80 | 58 | 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""" |
85 | 60 | det = round(np.linalg.det(self.encrypt_key)) |
86 | | - |
87 | 61 | 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 | + |
91 | 64 | 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.") |
97 | 66 |
|
98 | 67 | 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' |
111 | 71 | while len(chars) % self.break_key != 0: |
112 | 72 | chars.append(last) |
113 | | - |
114 | 73 | return "".join(chars) |
115 | 74 |
|
116 | 75 | 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""" |
125 | 77 | text = self.process_text(text.upper()) |
126 | 78 | 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] |
133 | 82 | 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) |
145 | 85 | 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)) |
155 | 86 |
|
| 87 | + def make_decrypt_key(self) -> np.ndarray: |
| 88 | + """Calculate decryption key matrix""" |
| 89 | + det = round(np.linalg.det(self.encrypt_key)) |
156 | 90 | 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) |
171 | 98 | return self.to_int(self.modulus(inv_key)) |
172 | 99 |
|
173 | 100 | 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""" |
182 | 102 | decrypt_key = self.make_decrypt_key() |
183 | 103 | text = self.process_text(text.upper()) |
184 | 104 | 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] |
191 | 108 | batch_vec = np.array([vec]).T |
192 | | - |
193 | | - # Matrix multiplication with decryption key |
194 | 109 | 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) |
201 | 111 | 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)) |
213 | 112 |
|
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 | + |
216 | 124 | 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)) |
220 | 126 | 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)) |
225 | 128 |
|
226 | 129 | if __name__ == "__main__": |
227 | 130 | import doctest |
228 | | - |
229 | 131 | doctest.testmod() |
230 | 132 | main() |
0 commit comments