diff --git a/chapter_two/Homophone_Chiffre.py b/chapter_two/Homophone_Chiffre.py new file mode 100644 index 0000000..331c071 --- /dev/null +++ b/chapter_two/Homophone_Chiffre.py @@ -0,0 +1,131 @@ +import math +import random + +from utils import AlphabetUtils as au +from utils import CipherUtils as cu + + +def encrypt_text(cleartext: str, key: {}) -> str: + """ + Encrypts the given text with the given key + :param cleartext: The text to encrypt + :param key: The key to use. Has to be generated with the function below in order to work properly. + :return: The encrypted text + """ + cleartext = cu.transform_invalid_chars(cleartext) + + letter_length = key['config']['letter_length'] + + resulting = '' + + for char in cleartext: + possible_ciphers = key['data'][char.lower()] + chosen_cipher = random.choice(possible_ciphers) + resulting += str(chosen_cipher).zfill(letter_length) + + return resulting + + +def decrypt_text(ciphertext: str, key: {}) -> str: + """ + Decrypts the given ciphertext with the given key + :param ciphertext: The text to decrypt + :param key: The key to use. Has to be generated with the function below in order to work properly. + :return: The decrypted text + """ + letter_length = key['config']['letter_length'] + + letter_split = [ciphertext[i:i + letter_length] for i in range(0, len(ciphertext), letter_length)] + + transformed_key = _transform_key(key) + + resulting = '' + + for cipher_letter in letter_split: + cleartext = transformed_key[int(cipher_letter)] + resulting += str(cleartext) + + return resulting + + +def _transform_key(key: {}) -> []: + """ + Transforms the given key into a format that can be easier used for decryption + :param key: The key to transform + :return: The transformed key. Contains a list where the index is the ciphertext and the value is the cleartext + """ + transformed_key = [None] * int(key['config']['max_cipher']) + + for letter in key['data'].keys(): + possible_ciphers = key['data'][letter] + for cipher in possible_ciphers: + transformed_key[int(cipher)] = letter + + return transformed_key + + +def generate_key(key_distribution: [int]) -> {}: + """ + Generates a key that can be used for this cipher by using the given key distribution profile + :param key_distribution: The key distribution, e.g. number of possibilities per letter + :return: The key as an object which contains the letter as key and the list of possible ciphers as value + """ + key = { + 'config': {}, + 'data': {} + } + + # Generate the key possibilities + amount_of_numbers = sum(key_distribution) + key_possibilities = [i for i in range(amount_of_numbers)] + random.shuffle(key_possibilities) + + # Because the ciphertext does not contain of letters but of numbers instead and these are dependent on the key, + # we need to include the fixed length of each 'letter' here + key['config']['letter_length'] = len(str(amount_of_numbers)) + key['config']['max_cipher'] = str(amount_of_numbers) + + # Distribute the keys to the letters + for letter_index in range(len(key_distribution)): + letter = au.get_letter_at_index(letter_index) + + keys_for_this_letter = [] + + for key_possibility in range(key_distribution[letter_index]): + keys_for_this_letter.append(key_possibilities.pop()) + + key['data'][letter] = keys_for_this_letter + + return key + + +def generate_key_distribution(base: str, multiplier: int) -> [int]: + """ + Generates a key distribution profile based on the letter frequencies of the given input text. + Larger inputs generally result in better key distribution profiles. + :param base: The text to use as a sort of seed + :param multiplier: A multiplier for the possibilities per letter + :return: The key distribution profile as a list of integers + """ + letter_distribution = cu.calculate_frequency(base) + + return [math.ceil(i * 100 * multiplier) for i in letter_distribution] + + +def get_german_key_distribution(multiplier: int) -> [int]: + """ + Returns the 'optimal' key distribution profile for german texts + :param multiplier: A multiplier for the possibilities per letter + :return: The key distribution profile as a list of integers + """ + return [math.ceil(i * 100 * multiplier) for i in cu.GERMAN_FREQUENCY_PROFILE] + + +if __name__ == '__main__': + dist = get_german_key_distribution(10) + print(dist) + key = generate_key(key_distribution=dist) + print(key) + ciphertext = encrypt_text('BonkRocks', key) + print(ciphertext) + print(decrypt_text(ciphertext, key))