initial import of legacy cipher code
This commit is contained in:
parent
233e9956b7
commit
ae12261a1e
242
cipher/cipher.py
Normal file
242
cipher/cipher.py
Normal file
|
@ -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())
|
15
pyproject.toml
Normal file
15
pyproject.toml
Normal file
|
@ -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 <evilchili@gmail.com>"]
|
||||
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"
|
Loading…
Reference in New Issue
Block a user