hill cipher the hillcipher class below implements the hill cipher algorithm which uses modern linear algebra techniques to encode and decode text using an encryption key matrix algorithm let the order of the encryption key be n as it is a square matrix your text is divided into batches of length n and converted to numerical vectors by a simple mapping starting with a0 and so on the key is then multiplied with the newly created batch vector to obtain the encoded vector after each multiplication modular 36 calculations are performed on the vectors so as to bring the numbers between 0 and 36 and then mapped with their corresponding alphanumerics while decrypting the decrypting key is found which is the inverse of the encrypting key modular 36 the same process is repeated for decrypting to get the original message back constraints the determinant of the encryption key matrix must be relatively prime w r t 36 note this implementation only considers alphanumerics in the text if the length of the text to be encrypted is not a multiple of the break keythe length of one batch of letters the last character of the text is added to the text until the length of the text reaches a multiple of the breakkey so the text after decrypting might be a little different than the original text references https apprendreenligne netcryptohillhillciph pdf https www youtube comwatch vkfmneskzs2o https www youtube comwatch v4rhlndqcjpa this cipher takes alphanumerics into account i e a total of 36 characters take x and return x lenkeystring encryptkey is an nxn numpy array hillcipher hillciphernumpy array2 5 1 6 hillcipher replaceletters t 19 hillcipher replaceletters 0 26 hillcipher hillciphernumpy array2 5 1 6 hillcipher replacedigits19 t hillcipher replacedigits26 0 hillcipher hillciphernumpy array2 5 1 6 hillcipher checkdeterminant hillcipher hillciphernumpy array2 5 1 6 hillcipher processtext testing hill cipher testinghillcipherr hillcipher processtext hello helloo hillcipher hillciphernumpy array2 5 1 6 hillcipher encrypt testing hill cipher whxyjolm9c6xt085ll hillcipher encrypt hello 85ff00 hillcipher hillciphernumpy array2 5 1 6 hillcipher makedecryptkey array 6 25 5 26 hillcipher hillciphernumpy array2 5 1 6 hillcipher decrypt whxyjolm9c6xt085ll testinghillcipherr hillcipher decrypt 85ff00 helloo this cipher takes alphanumerics into account i e a total of 36 characters take x and return x len key_string encrypt_key is an nxn numpy array mod36 calc s on the encrypt key validate the determinant of the encryption key hill_cipher hillcipher numpy array 2 5 1 6 hill_cipher replace_letters t 19 hill_cipher replace_letters 0 26 hill_cipher hillcipher numpy array 2 5 1 6 hill_cipher replace_digits 19 t hill_cipher replace_digits 26 0 hill_cipher hillcipher numpy array 2 5 1 6 hill_cipher check_determinant hill_cipher hillcipher numpy array 2 5 1 6 hill_cipher process_text testing hill cipher testinghillcipherr hill_cipher process_text hello helloo hill_cipher hillcipher numpy array 2 5 1 6 hill_cipher encrypt testing hill cipher whxyjolm9c6xt085ll hill_cipher encrypt hello 85ff00 hill_cipher hillcipher numpy array 2 5 1 6 hill_cipher make_decrypt_key array 6 25 5 26 hill_cipher hillcipher numpy array 2 5 1 6 hill_cipher decrypt whxyjolm9c6xt085ll testinghillcipherr hill_cipher decrypt 85ff00 helloo
import string

import numpy

from maths.greatest_common_divisor import greatest_common_divisor


class HillCipher:
    key_string = string.ascii_uppercase + string.digits

    modulus = numpy.vectorize(lambda x: x % 36)

    to_int = numpy.vectorize(round)

    def __init__(self, encrypt_key: numpy.ndarray) -> None:
        
        self.encrypt_key = self.modulus(encrypt_key)  
        self.check_determinant()  
        self.break_key = encrypt_key.shape[0]

    def replace_letters(self, letter: str) -> int:
        
        return self.key_string.index(letter)

    def replace_digits(self, num: int) -> str:
        
        return self.key_string[round(num)]

    def check_determinant(self) -> None:
        
        det = round(numpy.linalg.det(self.encrypt_key))

        if det < 0:
            det = det % len(self.key_string)

        req_l = len(self.key_string)
        if greatest_common_divisor(det, len(self.key_string)) != 1:
            msg = (
                f"determinant modular {req_l} of encryption key({det}) "
                f"is not co prime w.r.t {req_l}.\nTry another key."
            )
            raise ValueError(msg)

    def process_text(self, text: str) -> str:
        
        chars = [char for char in text.upper() if char in self.key_string]

        last = chars[-1]
        while len(chars) % self.break_key != 0:
            chars.append(last)

        return "".join(chars)

    def encrypt(self, text: str) -> str:
        
        text = self.process_text(text.upper())
        encrypted = ""

        for i in range(0, len(text) - self.break_key + 1, self.break_key):
            batch = text[i : i + self.break_key]
            vec = [self.replace_letters(char) for char in batch]
            batch_vec = numpy.array([vec]).T
            batch_encrypted = self.modulus(self.encrypt_key.dot(batch_vec)).T.tolist()[
                0
            ]
            encrypted_batch = "".join(
                self.replace_digits(num) for num in batch_encrypted
            )
            encrypted += encrypted_batch

        return encrypted

    def make_decrypt_key(self) -> numpy.ndarray:
        
        det = round(numpy.linalg.det(self.encrypt_key))

        if det < 0:
            det = det % len(self.key_string)
        det_inv = None
        for i in range(len(self.key_string)):
            if (det * i) % len(self.key_string) == 1:
                det_inv = i
                break

        inv_key = (
            det_inv
            * numpy.linalg.det(self.encrypt_key)
            * numpy.linalg.inv(self.encrypt_key)
        )

        return self.to_int(self.modulus(inv_key))

    def decrypt(self, text: str) -> str:
        
        decrypt_key = self.make_decrypt_key()
        text = self.process_text(text.upper())
        decrypted = ""

        for i in range(0, len(text) - self.break_key + 1, self.break_key):
            batch = text[i : i + self.break_key]
            vec = [self.replace_letters(char) for char in batch]
            batch_vec = numpy.array([vec]).T
            batch_decrypted = self.modulus(decrypt_key.dot(batch_vec)).T.tolist()[0]
            decrypted_batch = "".join(
                self.replace_digits(num) for num in batch_decrypted
            )
            decrypted += decrypted_batch

        return decrypted


def main() -> None:
    n = int(input("Enter the order of the encryption key: "))
    hill_matrix = []

    print("Enter each row of the encryption key with space separated integers")
    for _ in range(n):
        row = [int(x) for x in input().split()]
        hill_matrix.append(row)

    hc = HillCipher(numpy.array(hill_matrix))

    print("Would you like to encrypt or decrypt some text? (1 or 2)")
    option = input("\n1. Encrypt\n2. Decrypt\n")
    if option == "1":
        text_e = input("What text would you like to encrypt?: ")
        print("Your encrypted text is:")
        print(hc.encrypt(text_e))
    elif option == "2":
        text_d = input("What text would you like to decrypt?: ")
        print("Your decrypted text is:")
        print(hc.decrypt(text_d))


if __name__ == "__main__":
    import doctest

    doctest.testmod()

    main()