"""A nihilist cipher application written in Python.
HISTORICAL CONTEXT
The first nihilist movement began in 1860s Russian and was based in the
rejection of all totalitarianisms. Its name was derived from the Latin *nihil*,
meaning "nothing". Their influences can be found in many philosophical,
cultural, and aesthetic forms of life.
In the history of cryptography, the nihilist cipher is a manually operated
symmetric encryption cipher originally used by Russian nihilists in the 1880s
to communicate...
The encipherer uses a keyword to construct a mixed alphabet Polybius square:
. (Though, the code below
constructs a mixed alphanumeric Polybius square.) The square is then used to
convert both a second keyword and the plaintext message into a series of two
digit numbers. These numbers are then added together to obtain the ciphertext.
For information and instructions, you can read more about the nihilist cipher
here: .
REQUIREMENTS
- latest stable version of ``python``
USAGE
``python nihilist_cipher.py`` or ``python3 nihilist_cipher.py`` (where
`nihilist_cipher.py` is the path to the `nihilist_cipher.py` file) along with
the necessary arguments in the `--help` menu below.
```console
usage: nihilist_cipher.py [-h] -p POLYBIUS_KEY -k KEY
(-e MESSAGE [MESSAGE ...] | -d NUMBERS [NUMBERS ...])
Encrypt or decrypt a message using a nihilist cipher
optional arguments:
-h, --help show this help message and exit
-e MESSAGE [MESSAGE ...], --encrypt MESSAGE [MESSAGE ...]
use to encrypt a plaintext MESSAGE (only letters and
numbers considered)
-d NUMBERS [NUMBERS ...], --decrypt NUMBERS [NUMBERS ...]
use to decrypt cipher NUMBERS (numbers must be
separated with spaces)
keys:
necessary cipher keys
-p POLYBIUS_KEY, --polybius-key POLYBIUS_KEY
key to construct Polybius square (only letters and
numbers considered, no spaces allowed)
-k KEY, --key KEY key to encrypt or decrypt a message (only letters and
numbers considered, no spaces allowed)
WARNING: This nihilist cipher software is for historical purposes only and is
not secure since, for instance, it is likely any plaintext of keys and
messages are stored in your bash history, which can be a security risk in some
instances. Also, nowadays, the nihilist cipher itself is easily decipherable.
There is much better encryption software. Do not use this software for anything
but entertainment.
```
"""
import itertools
import argparse
import string
import re
class NihilistCipher:
"""All nihilist cipher methods needed to encrypt or decrypt a message."""
def __init__(self, polybius_key, key, message):
"""Assign user input values locally."""
self.polybius_key = polybius_key
self.key = key
self.message = message
# Initialize empty Polybius square and keyword cipher
self.polybius_square = {}
self.key_cipher = []
def create_polybius_square(self):
"""Create Polybius square with user-supplied polybius_key."""
# Add Polybius key characters to square without repeating any
square = []
for character in self.polybius_key:
if character not in square:
square.append(character)
# Add missing characters to square following Polybius key characters
alphanumeric = list(string.ascii_uppercase) + list(string.digits)
square += [
character for character in alphanumeric if character not in square
]
# Assign characters their respective numbers to complete square
square_numbers = []
square_numbers += itertools.chain(range(11, 17), range(21, 27),
range(31, 37), range(41, 47),
range(51, 57), range(61, 67))
self.polybius_square = dict(zip(square_numbers, square))
def key_encrypt(self):
"""Convert user-supplied key into numbers from Polybius square.
Key numbers need to then be repeated for the length of the plaintext or
cipher message so they can eventually be added or subtracted
number-by-number (letter-by-letter) from the numbers of the message.
"""
# Assign each key character its respective Polybius square number
key_numbers = [
key
for key, value in self.polybius_square.items()
for character in self.key
if value == character
]
# Repeat key numbers in case plaintext or cipher message is
# longer than key
self.key_cipher = itertools.cycle(key_numbers)
def encrypt(self):
"""Encrypt plaintext message."""
self.create_polybius_square()
self.key_encrypt()
# Assign each plaintext character its respective Polybius square number
self.message = [
key
for character in self.message
for key, value in self.polybius_square.items()
if value == character
]
# Create cipher message by adding plaintext numbers and key numbers
# Print cipher as a string
cipher_message = [x + y for x, y in zip(self.message, self.key_cipher)]
print('Cipher: ' + ' '.join(str(number) for number in cipher_message))
def decrypt(self):
"""Decrypt cipher message."""
self.create_polybius_square()
self.key_encrypt()
# Decrypt by first subtracting key numbers from cipher numbers
# Then find character values in Polybius square for each number result
# Print decrypted message as a string
decrypted_cipher = [
x - y for x, y in zip(self.message, self.key_cipher)
]
decrypted_result = []
# All cipher numbers have passed number validation during argument
# parsing but we now have to check if the numbers--after subtraction--
# also pass the same test of being one of all possible numbers
for number in decrypted_cipher:
if number not in self.polybius_square:
print(
'nihilist_cipher.py: error: at least one of your '
'cipher numbers is mathematically impossible: check again'
)
return
decrypted_result.append(self.polybius_square[number])
print('Message: ' + ''.join(decrypted_result))
def user_input():
"""Gather user input through parsed arguments."""
# Function to validate cipher numbers to be decrypted
def number_validation(numbers):
# Variables to only accept numbers from all possible numbers
included_set = set()
for entry in range(22, 133):
included_set.add(str(entry))
# First, check to see if user typed anything not a number
letters = re.search('[^0-9]+', numbers)
if letters is not None:
raise argparse.ArgumentTypeError(
'you are trying to decrypt: only numbers are allowed.'
)
# If there are only numbers make sure user has correct numbers and
# parsed them correctly
numbers = re.findall('[0-9]+', numbers)
for number in numbers:
if number not in included_set:
raise argparse.ArgumentTypeError(
'you either entered incorrect numbers or did not properly '
'separate them with spaces.'
)
return numbers
# Parse arguments
parser = argparse.ArgumentParser(
description='Encrypt or decrypt a message using a nihilist cipher',
epilog='WARNING: This nihilist cipher software is for historical '
'purposes only and is not secure since, for instance, it is '
'likely any plaintext of keys and messages are stored in your '
'bash history, which can be a security risk in some instances. '
'Also, nowadays, the nihilist cipher itself is easily '
'decipherable. There is much better encryption sofware. Do not '
'use this software for anything but entertainment.'
)
arguments = parser.add_argument_group('keys', 'necessary cipher keys')
# Obtain necessary Polybius key to construct Polybius square
arguments.add_argument(
'-p',
'--polybius-key',
required=True,
help='key to construct Polybius square (only letters and numbers '
'considered, no spaces allowed)'
)
# Obtain necessary key to encrypt or decrypt message
arguments.add_argument(
'-k',
'--key',
required=True,
help='key to encrypt or decrypt a message (only letters and numbers '
'considered, no spaces allowed)'
)
# Obtain message to encrypt or decrypt
# To encrypt or decrypt is a necessary choice
actions = parser.add_mutually_exclusive_group(required=True)
actions.add_argument(
'-e',
'--encrypt',
metavar='MESSAGE',
nargs='+',
help='use to encrypt a plaintext MESSAGE (only letters and numbers '
'considered)'
)
actions.add_argument(
'-d',
'--decrypt',
metavar='NUMBERS',
nargs='+',
type=number_validation,
help='use to decrypt cipher NUMBERS (numbers must be separated with '
'spaces)'
)
return parser
def main():
"""Call encryption or decryption methods based on user input."""
parser = user_input()
args = parser.parse_args()
args.polybius_key = args.polybius_key.upper()
args.key = args.key.upper()
if args.encrypt:
args.encrypt = str(args.encrypt)
args.encrypt = args.encrypt.upper()
call = NihilistCipher(args.polybius_key, args.key, args.encrypt)
call.encrypt()
elif args.decrypt:
cipher = [str(number) for entry in args.decrypt for number in entry]
args.decrypt = list(int(number) for number in cipher)
call = NihilistCipher(args.polybius_key, args.key, args.decrypt)
call.decrypt()
if __name__ == '__main__':
main()