|
4 | 4 | from maths.greatest_common_divisor import greatest_common_divisor |
5 | 5 |
|
6 | 6 | class HillCipher: |
7 | | - key_string = string.ascii_uppercase + string.digits # 36 alphanumeric chars |
8 | | - modulus = np.vectorize(lambda x: x % 36) # Mod 36 operation |
9 | | - to_int = np.vectorize(round) # Round to nearest integer |
| 7 | + key_string = string.ascii_uppercase + string.digits # 36 chars |
| 8 | + modulus = np.vectorize(lambda x: x % 36) # Mod 36 |
| 9 | + to_int = np.vectorize(round) # Round numbers |
10 | 10 |
|
11 | 11 | def __init__(self, encrypt_key: np.ndarray) -> None: |
12 | 12 | self.encrypt_key = self.modulus(encrypt_key) |
13 | | - self.check_determinant() # Validate key determinant |
14 | | - self.break_key = encrypt_key.shape[0] # Matrix order |
| 13 | + self.check_determinant() # Validate key |
| 14 | + self.break_key = encrypt_key.shape[0] # Matrix size |
15 | 15 |
|
16 | 16 | def replace_letters(self, letter: str) -> int: |
17 | | - """Map char to index (A=0, Z=25, 0=26, 9=35)""" |
| 17 | + """Char to index (A=0, 0=26)""" |
18 | 18 | return self.key_string.index(letter) |
19 | 19 |
|
20 | 20 | def replace_digits(self, num: int) -> str: |
21 | | - """Map index back to char""" |
| 21 | + """Index to char""" |
22 | 22 | return self.key_string[num] |
23 | 23 |
|
24 | 24 | def check_determinant(self) -> None: |
25 | | - """Ensure det(key) is coprime with 36""" |
| 25 | + """Ensure det(key) coprime with 36""" |
26 | 26 | det = round(np.linalg.det(self.encrypt_key)) |
27 | 27 | if det < 0: |
28 | 28 | det %= len(self.key_string) |
29 | | - |
| 29 | + |
30 | 30 | error_msg = f"Det {det} not coprime with 36. Try another key." |
31 | 31 | if greatest_common_divisor(det, len(self.key_string)) != 1: |
32 | 32 | raise ValueError(error_msg) |
33 | 33 |
|
34 | 34 | def process_text(self, text: str) -> str: |
35 | | - """Convert to uppercase, remove invalid chars, pad to multiple of break_key""" |
| 35 | + """Uppercase, filter, pad text""" |
36 | 36 | chars = [c for c in text.upper() if c in self.key_string] |
37 | 37 | last = chars[-1] if chars else 'A' |
38 | 38 | while len(chars) % self.break_key != 0: |
39 | 39 | chars.append(last) |
40 | 40 | return "".join(chars) |
41 | 41 |
|
42 | 42 | def encrypt(self, text: str) -> str: |
43 | | - """Encrypt text using Hill cipher""" |
| 43 | + """Encrypt with Hill cipher""" |
44 | 44 | text = self.process_text(text.upper()) |
45 | 45 | encrypted = "" |
46 | 46 | for i in range(0, len(text), self.break_key): |
47 | 47 | batch = text[i:i+self.break_key] |
48 | 48 | vec = [self.replace_letters(c) for c in batch] |
49 | 49 | batch_vec = np.array([vec]).T |
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 |
| 50 | + product = self.encrypt_key.dot(batch_vec) |
| 51 | + modulated = self.modulus(product) |
| 52 | + batch_encrypted = modulated.T.tolist()[0] |
| 53 | + encrypted_batch = "".join( |
| 54 | + self.replace_digits(round(n)) for n in batch_encrypted |
| 55 | + ) |
| 56 | + encrypted += encrypted_batch |
52 | 57 | return encrypted |
53 | 58 |
|
54 | 59 | def make_decrypt_key(self) -> np.ndarray: |
55 | | - """Calculate decryption key matrix""" |
| 60 | + """Create decryption key""" |
56 | 61 | det = round(np.linalg.det(self.encrypt_key)) |
57 | 62 | if det < 0: |
58 | 63 | det %= len(self.key_string) |
59 | | - |
60 | | - # Find modular inverse of det |
| 64 | + |
| 65 | + # Find det modular inverse |
61 | 66 | det_inv = next(i for i in range(36) if (det * i) % 36 == 1) |
62 | | - |
63 | | - # Calculate inverse key |
64 | | - inv_key = det_inv * np.linalg.det(self.encrypt_key) * np.linalg.inv(self.encrypt_key) |
| 67 | + |
| 68 | + # Compute inverse key |
| 69 | + inv_key = ( |
| 70 | + det_inv * |
| 71 | + np.linalg.det(self.encrypt_key) * |
| 72 | + np.linalg.inv(self.encrypt_key) |
| 73 | + ) |
65 | 74 | return self.to_int(self.modulus(inv_key)) |
66 | 75 |
|
67 | 76 | def decrypt(self, text: str) -> str: |
68 | | - """Decrypt text using Hill cipher""" |
| 77 | + """Decrypt with Hill cipher""" |
69 | 78 | decrypt_key = self.make_decrypt_key() |
70 | 79 | text = self.process_text(text.upper()) |
71 | 80 | decrypted = "" |
72 | 81 | for i in range(0, len(text), self.break_key): |
73 | 82 | batch = text[i:i+self.break_key] |
74 | 83 | vec = [self.replace_letters(c) for c in batch] |
75 | 84 | batch_vec = np.array([vec]).T |
76 | | - batch_decrypted = self.modulus(decrypt_key.dot(batch_vec)).T.tolist()[0] |
77 | | - decrypted += "".join(self.replace_digits(round(n)) for n in batch_decrypted |
| 85 | + product = decrypt_key.dot(batch_vec) |
| 86 | + modulated = self.modulus(product) |
| 87 | + batch_decrypted = modulated.T.tolist()[0] |
| 88 | + decrypted_batch = "".join( |
| 89 | + self.replace_digits(round(n)) for n in batch_decrypted |
| 90 | + ) |
| 91 | + decrypted += decrypted_batch |
78 | 92 | return decrypted |
79 | 93 |
|
80 | 94 | def main() -> None: |
81 | | - """CLI for Hill Cipher""" |
| 95 | + """Hill Cipher CLI""" |
82 | 96 | n = int(input("Enter key order: ")) |
83 | 97 | print(f"Enter {n} rows of space-separated integers:") |
84 | 98 | matrix = [list(map(int, input().split())) for _ in range(n)] |
85 | | - |
| 99 | + |
86 | 100 | hc = HillCipher(np.array(matrix)) |
87 | | - |
| 101 | + |
88 | 102 | option = input("1. Encrypt\n2. Decrypt\nChoose: ") |
89 | 103 | text = input("Enter text: ") |
90 | | - |
| 104 | + |
91 | 105 | if option == "1": |
92 | 106 | print("Encrypted:", hc.encrypt(text)) |
93 | 107 | elif option == "2": |
|
0 commit comments