""" The permutation cipher, also called the transposition cipher, is a simple encryption technique that rearranges the characters in a message based on a secret key. It divides the message into blocks and applies a permutation to the characters within each block according to the key. The key is a sequence of unique integers that determine the order of character rearrangement. For more info: https://www.nku.edu/~christensen/1402%20permutation%20ciphers.pdf """ import random def generate_valid_block_size(message_length: int) -> int: """ Generate a valid block size that is a factor of the message length. Args: message_length (int): The length of the message. Returns: int: A valid block size. Example: >>> random.seed(1) >>> generate_valid_block_size(12) 3 """ block_sizes = [ block_size for block_size in range(2, message_length + 1) if message_length % block_size == 0 ] return random.choice(block_sizes) def generate_permutation_key(block_size: int) -> list[int]: """ Generate a random permutation key of a specified block size. Args: block_size (int): The size of each permutation block. Returns: list[int]: A list containing a random permutation of digits. Example: >>> random.seed(0) >>> generate_permutation_key(4) [2, 0, 1, 3] """ digits = list(range(block_size)) random.shuffle(digits) return digits def encrypt( message: str, key: list[int] | None = None, block_size: int | None = None ) -> tuple[str, list[int]]: """ Encrypt a message using a permutation cipher with block rearrangement using a key. Args: message (str): The plaintext message to be encrypted. key (list[int]): The permutation key for decryption. block_size (int): The size of each permutation block. Returns: tuple: A tuple containing the encrypted message and the encryption key. Example: >>> encrypted_message, key = encrypt("HELLO WORLD") >>> decrypted_message = decrypt(encrypted_message, key) >>> decrypted_message 'HELLO WORLD' """ message = message.upper() message_length = len(message) if key is None or block_size is None: block_size = generate_valid_block_size(message_length) key = generate_permutation_key(block_size) encrypted_message = "" for i in range(0, message_length, block_size): block = message[i : i + block_size] rearranged_block = [block[digit] for digit in key] encrypted_message += "".join(rearranged_block) return encrypted_message, key def decrypt(encrypted_message: str, key: list[int]) -> str: """ Decrypt an encrypted message using a permutation cipher with block rearrangement. Args: encrypted_message (str): The encrypted message. key (list[int]): The permutation key for decryption. Returns: str: The decrypted plaintext message. Example: >>> encrypted_message, key = encrypt("HELLO WORLD") >>> decrypted_message = decrypt(encrypted_message, key) >>> decrypted_message 'HELLO WORLD' """ key_length = len(key) decrypted_message = "" for i in range(0, len(encrypted_message), key_length): block = encrypted_message[i : i + key_length] original_block = [""] * key_length for j, digit in enumerate(key): original_block[digit] = block[j] decrypted_message += "".join(original_block) return decrypted_message def main() -> None: """ Driver function to pass message to get encrypted, then decrypted. Example: >>> main() Decrypted message: HELLO WORLD """ message = "HELLO WORLD" encrypted_message, key = encrypt(message) decrypted_message = decrypt(encrypted_message, key) print(f"Decrypted message: {decrypted_message}") if __name__ == "__main__": import doctest doctest.testmod() main()