From ae12261a1eb3b9375542f7788af0bc1e2122309e Mon Sep 17 00:00:00 2001 From: evilchili Date: Wed, 3 Aug 2022 00:32:29 -0700 Subject: [PATCH] initial import of legacy cipher code --- cipher/cipher.py | 242 +++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 15 +++ 2 files changed, 257 insertions(+) create mode 100644 cipher/cipher.py create mode 100644 pyproject.toml diff --git a/cipher/cipher.py b/cipher/cipher.py new file mode 100644 index 0000000..2b140c8 --- /dev/null +++ b/cipher/cipher.py @@ -0,0 +1,242 @@ +from collections import defaultdict +import re + + +class ElethisCipher: + """ + An implementation of Elethi's Cipher, a cipher created by Madame Elethi and + used in her ledger. + + This is an autokey cipher that uses a variant on a tabula recta. Rather + than a typical 26x26 matrix with A-Z, Elethi's Cipher uses a 10x10 matrix + that encodes A-Z0-9: + + 0 1 2 3 4 5 6 7 8 9 + +-------------------- + 0 | a b c d e f g h i j + 1 | k l m n o p q r s t + 2 | u v w x y z 0 1 2 3 + 3 | 4 5 6 7 8 9 a b c d + 4 | e f g h i j k l m n + 5 | o p q r s t u v w x + 6 | y z 0 1 2 3 4 5 6 7 + 7 | 8 9 a b c d e f g h + 8 | i j k l m n o p q r + 9 | s t u v w x y z 0 1 + + + Encrypting Messages + ------------------- + + The encryption algorithm is as follows. Consider the plaintext message + + KILL KEEN FOR HANDSOME HENRY SMALLS + + and the pre-shared key + + SABETHA + + 1. Remove all non alpha-numeric characters from both the message and the + pre-shared key. + + 2. Create an encryption key by prefixing the plaintext message with a short + pre-shared key: + + S A B E T H A K I L L K E E N F O ... + + 3. Locate each character of the key in the tabula recta and replace it with + a two-digit number comprised of the row coordinate followed by the column + coordinate: + + S A B E T H A K I L L K E E N F O + 18 00 01 04 19 07 00 10 08 11 11 10 04 04 13 05 14 ... + + 4. Convert the characters of the plaintext message in the same way: + + K I L L K E E N F O + 10 08 11 11 10 04 04 13 05 14 ... + + 5. Sum the value of each character in the plaintext message with the key: + + K I L L K E E N F O + 10 08 11 11 10 04 04 13 05 14 + + 18 00 01 04 19 07 00 10 08 11 + ----------------------------- + 28 08 12 15 29 11 04 23 13 25 + + Note that tabula recta has multiple entries for each plaintext character; + any valid set of coordinates may be used to encode a given character, which + can be helpful when encoding repeating characters. From the above example, + We might choose to encode KEEN as follows: + + K E E N + 10 76 04 13 + + + Decrypting Messages + ------------------- + + To decrypt the message, the recipient constructs the decryption key one + character at a time and subtracts the values from the encrypted characters: + + 1. Generate the start of the decryption key by converting the pre-shared + key into numbers using the tabula recta: + + S A B E T H A + 18 00 01 04 19 07 00 + + 2. Subtract the first number of the key from the first number in the + message: + + 28 + - 18 + -- + 10 + + 3. This number is appended to the decryption key: + + S A B E T H A K + 18 00 01 04 19 07 00 10 + + 4. To decrypt the first character of the message, look up the number from + Step 2 in the tabula recta: + + K + + 5. Repeat Steps 2-4 until the message has been decrypted, subtracting the + next value in the key from the next value in the encrypted message: + + 28 08 12 15 29 11 04 23 13 25 + - 18 00 01 04 19 07 00 10 08 11 + ----------------------------- + 10 08 11 11 10 04 04 13 05 14 + K I L L K E E N F O + + """ + + alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789' + non_alphanum = re.compile('[^A-Za-z0-9]') + + def __init__(self): + self._table = [] + self._reversed = defaultdict(list) + self._reversed_index = defaultdict(list) + self._generate_tabula_recta() + + def _generate_tabula_recta(self): + """ + Generate the tabula recta as a two-dimensional array, and a reverse lookup table as a dictionary. + """ + index = 0 + for offset_y in range(0, 10): + row = '' + for offset_x in range(0, 10): + letter = self.alphabet[index] + row = row + letter + self._reversed[letter].append(f'{offset_y}{offset_x}') + index = index + 1 + if index == len(self.alphabet): + index = 0 + self._table.append(row) + + @property + def tabula_recta(self): + """ + Return a formatted representation of the tabula recta. + """ + t = self._table.copy() + for (i, line) in enumerate(t): + t[i] = f'{i}|{line}' + t = '\n'.join([' '.join([col for col in row]) for row in t]) + return f' 0 1 2 3 4 5 6 7 8 9\n +--------------------\n{t}' + + def forward(self, n): + """ + Perform a forward lookup of a two digit number in the tabula recta and return the corresponding letter. + """ + n = int(n) + if n < 10: + x = 0 + y = n + else: + y = n % 10 + x = int((n - y)/10) + return self._table[x][y] + + def reverse(self, letter): + """ + Perform a reverse lookup of a letter in the tabula recta and return a two-digit number corresponding to it. + """ + return int(self._reversed[letter][0]) + + def normalize(self, text): + """ + Normalize a string so that each character can be encrypted using the tabula recta. + """ + return self.non_alphanum.sub('', text).lower() + + def encrypt(self, psk, message): + """ + Encrypt a message using the specified pre-shared key (psk). Returns a list of numbers. + """ + psk = self.normalize(psk) + plaintext = self.normalize(message) + + key = [self.reverse(x) for x in psk + plaintext] + encrypted = [] + for i in range(len(plaintext)): + a = self.reverse(plaintext[i]) + x = int(a) + key[i] + encrypted.append(x) + return encrypted + + def pretty_encrypt(self, psk, message, block_length=5, blocks_per_line=3): + """ + Encrypt a message using the specified pre-shared key (psk). Returns a formatted string consisting of the + encrypted message split into blocks of 3-digit numbers, 5 numbers to a block, 3 blocks to a line. + """ + psk = self.normalize(psk) + encrypted = [f'{x:03d}' for x in self.encrypt(psk, message)] + blocks = [encrypted[i:i + block_length] for i in range(0, len(encrypted), block_length)] + return '\n'.join([ + ' '.join([' '.join(inner) for inner in blocks[outer:outer + blocks_per_line]]) + for outer in range(0, len(blocks), blocks_per_line) + ]) + + def decrypt(self, psk, message): + """ + Decrypte a message using the specified pre-shared key (psk). Returns a list of characters. + """ + psk = self.normalize(psk) + plaintext = [] + + numbers = [int(x) for x in message.split()] + key = [self.reverse(x) for x in psk] + + for (i, n) in enumerate(numbers): + k = key[0] + a = n - k + plaintext.append(self.forward(a).upper()) + key = key[1:] + [a] + + return plaintext + + def pretty_decrypt(self, psk, message, block_length=5, blocks_per_line=3): + """ + Decryptes a message using the specified pre-shared key (psk). Returns a formatted string consisting of the + plaintext message split into blocks of single characters, 5 characters to a block, 3 blocks to a line. + """ + plaintext = self.decrypt(psk, message) + blocks = [plaintext[i:i + block_length] for i in range(0, len(plaintext), block_length)] + return '\n'.join([ + ' '.join([' '.join(inner) for inner in blocks[outer:outer + blocks_per_line]]) + for outer in range(0, len(blocks), blocks_per_line) + ]) + + def encrypt_file(self, psk, infile): + with open(infile) as f: + return self.pretty_encrypt(psk, f.read()) + + def decrypt_file(self, psk, infile): + with open(infile) as f: + return self.pretty_decrypt(psk, f.read()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..32b7fac --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "elethis-cipher" +version = "0.1.0" +description = "A tabula recta variant autokey cipher, implementation in python" +authors = ["evilchili "] +license = "The Unlicense" + +[tool.poetry.dependencies] +python = "^3.10" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api"