diff --git a/ciphers/des_ecb.py b/ciphers/des_ecb.py new file mode 100644 index 000000000000..4c904294a2d4 --- /dev/null +++ b/ciphers/des_ecb.py @@ -0,0 +1,557 @@ +""" +Python program for DES (Data Encryption Standard) using Electronic Codebook (ECB) mode. + +DES is a symmetric-key block cipher that encrypts data in fixed-size blocks (64 bits). +In ECB mode, the plaintext is divided into 64-bit blocks, and each block is encrypted +independently using the same key. This makes ECB the simplest block cipher mode, but +also one of the least secure, as identical plaintext blocks will produce identical +ciphertext blocks. + +This implementation of DES includes key scheduling, encryption, and decryption. +It uses standard DES operations such as initial and final permutations, expansion, +permutation, and S-box lookups. Padding is applied to ensure the plaintext length is +a multiple of 64 bits. + +Warning: ECB mode is not secure for most use cases due to its vulnerability to block +repetition analysis. Consider using a more secure mode of operation, such as CBC +(Cipher Block Chaining), for sensitive data encryption. + +References: +- Handbook of Applied Cryptography (Algorithm 7.82) +- Handbook of Applied Cryptography (Algorithm 7.83) +- Handbook of Applied Cryptography (Algorithm 9.29) +- https://en.wikipedia.org/wiki/Data_Encryption_Standard +""" + +import random + +# fmt: off + +# Initial Permutation Table +IP = [58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, + 62, 54, 46, 38, 30, 22, 14, 6, + 64, 56, 48, 40, 32, 24, 16, 8, + 57, 49, 41, 33, 25, 17, 9, 1, + 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, + 63, 55, 47, 39, 31, 23, 15, 7] + +# Final Permutation Table +IP_INV = [40, 8, 48, 16, 56, 24, 64, 32, + 39, 7, 47, 15, 55, 23, 63, 31, + 38, 6, 46, 14, 54, 22, 62, 30, + 37, 5, 45, 13, 53, 21, 61, 29, + 36, 4, 44, 12, 52, 20, 60, 28, + 35, 3, 43, 11, 51, 19, 59, 27, + 34, 2, 42, 10, 50, 18, 58, 26, + 33, 1, 41, 9, 49, 17, 57, 25] + +# Expansion Table +E = [32, 1, 2, 3, 4, 5, + 4, 5, 6, 7, 8, 9, + 8, 9, 10, 11, 12, 13, + 12, 13, 14, 15, 16, 17, + 16, 17, 18, 19, 20, 21, + 20, 21, 22, 23, 24, 25, + 24, 25, 26, 27, 28, 29, + 28, 29, 30, 31, 32, 1] + +# Permutation Table +P = [16, 7, 20, 21, 29, 12, 28, 17, + 1, 15, 23, 26, 5, 18, 31, 10, + 2, 8, 24, 14, 32, 27, 3, 9, + 19, 13, 30, 6, 22, 11, 4, 25] + +# S-boxes (Substitution Boxes) +S_BOXES = { + "S1": [[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7], + [0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8], + [4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0], + [15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13]], + + "S2": [[15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10], + [3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5], + [0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15], + [13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9]], + + "S3": [[10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8], + [13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1], + [13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7], + [1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12]], + + "S4": [[7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15], + [13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9], + [10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4], + [3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14]], + + "S5": [[2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9], + [14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6], + [4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14], + [11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3]], + + "S6": [[12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11], + [10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8], + [9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6], + [4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13]], + + "S7": [[4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1], + [13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6], + [1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2], + [6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12]], + + "S8": [[13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7], + [1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2], + [7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8], + [2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11]] +} + +# Permuted Choice 1 Table +PC1 = [57, 49, 41, 33, 25, 17, 9, + 1, 58, 50, 42, 34, 26, 18, + 10, 2, 59, 51, 43, 35, 27, + 19, 11, 3, 60, 52, 44, 36, + 63, 55, 47, 39, 31, 23, 15, + 7, 62, 54, 46, 38, 30, 22, + 14, 6, 61, 53, 45, 37, 29, + 21, 13, 5, 28, 20, 12, 4] + +# Permuted Choice 2 Table +PC2 = [14, 17, 11, 24, 1, 5, + 3, 28, 15, 6, 21, 10, + 23, 19, 12, 4, 26, 8, + 16, 7, 27, 20, 13, 2, + 41, 52, 31, 37, 47, 55, + 30, 40, 51, 45, 33, 48, + 44, 49, 39, 56, 34, 53, + 46, 42, 50, 36, 29, 32] +# fmt: on + + +class Operations: + @staticmethod + def string_to_bitset(string: str) -> list: + """ + Converts a string into a list of binary digits (bitset). + + Args: + string (str): The input string to be converted. + + Returns: + list: A list of binary digits representing the string. + + Examples: + >>> Operations.string_to_bitset('A') + ['0', '1', '0', '0', '0', '0', '0', '1'] + >>> len(Operations.string_to_bitset('ab')) + 16 + >>> Operations.string_to_bitset(' ') + ['0', '0', '1', '0', '0', '0', '0', '0'] + """ + return list("".join(format(ord(char), "08b") for char in string)) + + @staticmethod + def pad_right_to_multiple_of_n(bitset: list, length: int) -> list: + """ + Pads the bitset with zeros on the right until + its length is a multiple of `length`. + + Args: + bitset (list): A list of binary digits (as strings) to be padded. + + Returns: + list: The padded bitset, with a length that is a multiple of n. + + Examples: + >>> Operations.pad_right_to_multiple_of_n(['1', '0', '1'], 4) + ['1', '0', '1', '0'] + >>> len(Operations.pad_right_to_multiple_of_n(['1'] * 64, 64)) % 64 + 0 + >>> len(Operations.pad_right_to_multiple_of_n(['0'] * 63, 64)) % 64 + 0 + """ + if len(bitset) % length != 0: + bitset += list("0" * (length - len(bitset) % length)) + return bitset + + @staticmethod + def pad_left_to_multiple_of_n(bitset: list, length: int) -> list: + """ + Pads the bitset with zeros on the left until + its length is a multiple of length. + + Args: + bitset (list): A list of binary digits (as strings) to be padded. + + Returns: + list: The padded bitset, with a length that is a multiple of n. + + Examples: + >>> Operations.pad_left_to_multiple_of_n(['1', '0', '1'], 4) + ['0', '1', '0', '1'] + >>> len(Operations.pad_left_to_multiple_of_n(['1'] * 64, 64)) % 64 + 0 + >>> len(Operations.pad_left_to_multiple_of_n(['0'] * 63, 64)) % 64 + 0 + """ + if len(bitset) % length != 0: + bitset = list("0" * (length - len(bitset) % length)) + bitset + return bitset + + @staticmethod + def bitset_to_hex(bitset: list) -> str: + """ + Converts a list of binary digits + into its 16 digit hexadecimal representation. + + Args: + bitset (list): A list of binary digits (as strings)\ + representing a binary number. + + Returns: + str: The 16 digit hexadecimal representation of the binary number. + + Examples: + >>> Operations.bitset_to_hex(['1', '0', '1', '0', '1', '1', '1', '0']) + '00000000000000ae' + >>> Operations.bitset_to_hex(['1', '1', '1', '1'] * 16) + 'ffffffffffffffff' + >>> Operations.bitset_to_hex(['0'] * 64) + '0000000000000000' + """ + return format(int("".join(bitset), 2), "016x") + + @staticmethod + def hex_to_bitset(hex_string: str, left_pad: int) -> list: + """ + Converts a hexadecimal string to a bitset + and pads the bitset to a specified length. + + Args: + hex_string (str): The hexadecimal string to convert. + left_pad (int): The length to pad the bitset on the left. + + Returns: + list: The padded bitset. + + Examples: + >>> Operations.hex_to_bitset('ae', 8) + ['1', '0', '1', '0', '1', '1', '1', '0'] + >>> Operations.hex_to_bitset('1f', 10) + ['0', '0', '0', '0', '0', '1', '1', '1', '1', '1'] + """ + return Operations.pad_left_to_multiple_of_n( + list(format(int(hex_string, 16), f"0{left_pad}b")), left_pad + ) + + @staticmethod + def bitset_to_string(bitset: list) -> str: + """ + Converts a bitset into a string by interpreting every 8 bits as a character. + + Args: + bitset (list): The list of binary digits (bitset). + + Returns: + str: The decoded string. + + Examples: + >>> Operations.bitset_to_string(['0', '1', '0', '0', '0', '0', '0', '1']) + 'A' + >>> Operations.bitset_to_string(['0', '1', '0', '0', '0', '1',\ +'0', '0', '0', '1', '0', '1', '0', '1', '1', '0']) + 'DV' + """ + return "".join( + chr(int("".join(bitset[i : i + 8]), 2)) for i in range(0, len(bitset), 8) + ) + + @staticmethod + def xor(bitset1: list, bitset2: list) -> list: + """ + Applies a bitwise XOR operation between two bitsets of the same length. + + Args: + bitset1 (list): The first bitset. + bitset2 (list): The second bitset. + + Returns: + list: The result of the XOR operation as a new bitset. + + Examples: + >>> Operations.xor(['0', '1', '0', '1'], ['1', '0', '1', '1']) + ['1', '1', '1', '0'] + >>> Operations.xor(['1', '0', '1', '0'], ['0', '0', '0', '1']) + ['1', '0', '1', '1'] + >>> Operations.xor(['1', '0', '1', '0', '1'], ['0', '0', '0', '1']) + Traceback (most recent call last): + ... + ValueError: Bitsets must be of the same length + >>> Operations.xor(['1', '0', '1', '0'], ['0', '0', '0', '1', '1']) + Traceback (most recent call last): + ... + ValueError: Bitsets must be of the same length + """ + if len(bitset1) != len(bitset2): + raise ValueError("Bitsets must be of the same length") + return [str(int(bitset1[i]) ^ int(bitset2[i])) for i in range(len(bitset1))] + + @staticmethod + def shift_left(bitset: list, position: int) -> list: + """ + Performs a rotated left shift on a bitset by `position` positions. + + Args: + bitset (list): The bitset to be shifted. + n (int): The number of positions to shift. + + Returns: + list: The left-shifted bitset. + + Examples: + >>> Operations.shift_left(['1', '0', '0', '1'], 2) + ['0', '1', '1', '0'] + >>> Operations.shift_left(['0', '1', '1', '1'], 1) + ['1', '1', '1', '0'] + >>> Operations.shift_left(['0', '1', '1', '1'], 7) + ['1', '0', '1', '1'] + """ + position = position % len(bitset) + return bitset[position:] + bitset[:position] + + @staticmethod + def permute(bitset: list, permutation: list) -> list: + """ + Permutes a bitset according to a given permutation table. + + Args: + bitset (list): The bitset to be permuted. + permutation (list): The permutation table specifying the new order. + + Returns: + list: The permuted bitset. + + Examples: + >>> Operations.permute(['1', '0', '1', '0', '1', '1'], [6, 5, 4, 3, 2, 1]) + ['1', '1', '0', '1', '0', '1'] + >>> Operations.permute(['0', '1', '1', '0', '1', '0'], [3, 1, 6, 5, 4, 2]) + ['1', '0', '0', '1', '0', '1'] + >>> Operations.permute(['0', '1', '1', '0', '1', '0'], \ +[3, 1, 6, 5, 4, 2, 7]) + Traceback (most recent call last): + ... + ValueError: Permutation values must be within the range of the bitset + """ + if not all(0 < i <= len(bitset) for i in permutation): + raise ValueError( + "Permutation values must be within the range of the bitset" + ) + return [bitset[i - 1] for i in permutation] + + +class Des: + @staticmethod + def generate_key() -> str: + """ + Generates a random hexadecimal key of 16 characters (64 bits). + + Returns: + str: A random hexadecimal key. + + Examples: + >>> key = Des.generate_key() + >>> len(key) # Check if the key length is correct + 16 + >>> # Ensure key only contains valid hex characters + >>> all(c in "0123456789abcdef" for c in key) + True + """ + return "".join(random.choice("0123456789abcdef") for i in range(16)) + + @staticmethod + def key_schedule(key: str) -> list: + """ + Generates 16 subkeys (round keys) from a given + 64-bit hexadecimal key using the DES key schedule. + + Args: + key (str): A 16-character hexadecimal string representing a 64-bit key. + + Returns: + list: A list of 16 subkeys, each of which is a permuted bitset. + + Examples: + >>> subkeys = Des.key_schedule('133457799BBCDFF1') + >>> len(subkeys) # Check that 16 subkeys are generated + 16 + >>> # Ensure each subkey is 48 bits long + >>> all(len(subkey) == 48 for subkey in subkeys) + True + """ + key_bitset = Operations.hex_to_bitset(key, 64) + key_permuted = Operations.permute(key_bitset, PC1) + + left_key = key_permuted[:28] + right_key = key_permuted[28:] + + keys = [] + + shift_list = [1, 2, 9, 16] + + for i in range(1, 17): + if i in shift_list: + left_key = Operations.shift_left(left_key, 1) + right_key = Operations.shift_left(right_key, 1) + else: + left_key = Operations.shift_left(left_key, 2) + right_key = Operations.shift_left(right_key, 2) + keys.append(Operations.permute(left_key + right_key, PC2)) + + return keys + + @staticmethod + def des(keys: list, plain_bitset: list) -> list: + """ + Encrypts a plain bitset using the provided + subkeys with DES encryption algorithm. + + Args: + keys (list): A list of 16 subkeys, each 48 bits long. + plain_bitset (list): A bitset representing the plain text,\ + which should be divisible into 64-bit blocks. + + Returns: + list: The encrypted bitset. + + Examples: + >>> plain_bitset = Operations.string_to_bitset("Test string") + >>> plain_bitset = Operations.pad_right_to_multiple_of_n(plain_bitset, 64) + >>> keys = Des.key_schedule(Des.generate_key()) + >>> cipher_bitset = Des.des(keys, plain_bitset) + >>> # The output length should be the same as the input + >>> len(cipher_bitset) == len(plain_bitset) + True + >>> # Decryption should be the inverse of encryption + >>> plain_bitset == Des.des(keys[::-1], cipher_bitset) + True + """ + + no_of_blocks = len(plain_bitset) // 64 + + cipher_bitset = [] + + for i in range(no_of_blocks): + block = plain_bitset[i * 64 : (i + 1) * 64] + initial_permutation = Operations.permute(block, IP) + + left_bits = initial_permutation[:32] + right_bits = initial_permutation[32:] + + for i in range(16): + right_expanded = Operations.permute(right_bits, E) + right_xor_key = Operations.xor(right_expanded, keys[i]) + right_s_box = [] + for j in range(8): + s_box_input = right_xor_key[j * 6 : (j + 1) * 6] + row = 2 * int(s_box_input[0]) + int(s_box_input[5]) + col = int("".join(s_box_input[1:5]), 2) + s_box_output = format(S_BOXES[f"S{j + 1}"][row][col], "04b") + right_s_box += list(s_box_output) + + right_permuted = Operations.permute(right_s_box, P) + + left_bits, right_bits = ( + right_bits, + Operations.xor(left_bits, right_permuted), + ) + + left_bits, right_bits = right_bits, left_bits + cipher_bitset += Operations.permute(left_bits + right_bits, IP_INV) + return cipher_bitset + + @staticmethod + def encrypt(key: str, input_string: str) -> str: + """ + Encrypts a given input string using the DES encryption algorithm. + + Args: + key (str): A 16-character hexadecimal string representing a 64-bit key. + input_string (str): The plain text string to be encrypted. + + Returns: + str: A hexadecimal string representing the encrypted data. + + Examples: + >>> key = '133457799BBCDFF1' + >>> input_string = 'Test string' + >>> encrypted = Des.encrypt(key, input_string) + >>> encrypted # Checking the cipher to ensure consistency + 'c84e3c8fb646872720b224896db4f60' + """ + plain_bitset = Operations.string_to_bitset(input_string) + padded_bitset = Operations.pad_right_to_multiple_of_n(plain_bitset, 64) + keys = Des.key_schedule(key) + cipher_bitset = Des.des(keys, padded_bitset) + return Operations.bitset_to_hex(cipher_bitset) + + @staticmethod + def decrypt(key: str, cipher_text: str) -> str: + """ + Decrypts the given cipher text using DES decryption. + + Args: + key (str): A 16-character hexadecimal string representing a 64-bit key. + cipher_text (str): A hexadecimal string representing the encrypted data. + + Returns: + str: The decrypted plain text string. + + Examples: + >>> key = '133457799BBCDFF1' + >>> encrypted_string = 'c84e3c8fb646872720b224896db4f60' + >>> decrypted = Des.decrypt(key, encrypted_string) + >>> decrypted # Checking the cipher to ensure consistency + 'Test string' + """ + cipher_bitset = Operations.hex_to_bitset(cipher_text, 64) + keys = Des.key_schedule(key)[::-1] + plain_bitset = Des.des(keys, cipher_bitset) + return Operations.bitset_to_string(plain_bitset).replace("\x00", "") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + while True: + print() + print("################ DES Algorithm ################") + print() + print("Select an option:") + print("1. To encrypt a string, enter 'e'") + print("2. To decrypt a string, enter 'd'") + print("3. To generate a key, enter 'k'") + print("4. To Quit, enter 'q'") + mode = input("Enter the option: ").strip().lower() + if mode not in ["e", "d", "k", "q"]: + print("Invalid option. Please try again.") + continue + if mode == "q": + break + elif mode == "k": + print(f"Generated key: {Des.generate_key()}") + elif mode == "e": + key = input("Enter the key: ").strip() + if len(key) != 16 and not all(char in "0123456789abcdef" for char in key): + print("Invalid key. Please try again.") + continue + input_string = input("Enter the string to encrypt: ").strip() + print(f"Encrypted string: {Des.encrypt(key, input_string)}") + elif mode == "d": + key = input("Enter the key: ").strip() + if len(key) != 16 and not all(char in "0123456789abcdef" for char in key): + print("Invalid key. Please try again.") + continue + cipher_text = input("Enter the cipher text to decrypt: ").strip() + print(f"Decrypted string: {Des.decrypt(key, cipher_text)}")