243 lines
7.8 KiB
Python
243 lines
7.8 KiB
Python
|
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())
|