"""
This module provides simple usage of functions related to a list of ciphers
"""
import string
import re
import random
from math import gcd
from unidecode import unidecode
[docs]class PolybiusSquare:
"""
`PolybiusSquare` represents a Polybius Square cipher manipulator
Args:
width (int): The square's width. Must be at least 1. Width times height must be greater than the alphabet length
height (int): The square's height. Must be at least 1. Height times width must be greater than the alphabet length
abc (str): The alphabet used in the square. Defaults to ``string.ascii_uppercase``
ij (bool): Whether 'i' and 'j' are treated as the same letter. Defaults to ``True``
Raises:
ValueError: When `width` is smaller than 1
ValueError: When ``width * height`` is smaller than ``len(abc)``
"""
def __init__(self, width, height, abc=string.ascii_uppercase, ij=True):
self.abc = abc.replace('J', '') if ij else self.abc
self.width = width
self.height = height
self.mount_square()
self.not_abc_pattern = re.compile('[^{}]+'.format(abc), re.UNICODE)
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value < 1:
raise ValueError('This square is not large enough to comprehend the whole alphabet.\nPlease increase width or height.')
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value * self.width < len(self.abc):
raise ValueError('This square is not large enough to comprehend the whole alphabet.\nPlease increase width or height.')
self._height = value
def mount_square(self):
self.square_area = self.width * self.height
self.abc_square = (self.abc * (self.square_area // len(self.abc) + 1))[:self.square_area]
self.square = [self.abc_square[i:i+self.width] for i in range(0, len(self.abc_square), self.width)]
self.abc_to_pos = {letter:[] for letter in self.abc}
for line_index, line in enumerate(self.square):
for col_index, letter in enumerate(line):
self.abc_to_pos[letter].append('{}-{}'.format(col_index + 1, line_index + 1))
self.pos_to_abc = {}
for letter in self.abc:
for pos in self.abc_to_pos[letter]:
self.pos_to_abc[pos] = letter
[docs] def encrypt(self, text):
"""
Returns encrypted text (str)
Args:
text (str): The text to be encrypted
Examples:
>>> from crypyto.ciphers import PolybiusSquare
>>> ps = PolybiusSquare(5, 5)
>>> ps.encrypt('EncryptedMessage')
'5x5#5-1;3-3;3-1;2-4;4-5;5-3;4-4;5-1;4-1;2-3;5-1;3-4;3-4;1-1;2-2;5-1'
"""
text = unidecode(text).upper()
text = text.replace('J', 'I') if len(self.abc) == 25 else text
text = self.not_abc_pattern.sub('', text)
cipher = '{}x{}#'.format(self.width, self.height)
positions = [random.choice(self.abc_to_pos[letter]) for letter in text]
cipher += ';'.join(positions)
return cipher
[docs] def decrypt(self, cipher):
"""
Returns decrypted cipher (str)
Args:
cipher (str): The cipher to be decrypted. May or may not contain the square size at the beggining (e.g. '5x5#')
Raises:
ValueError: When ``cipher`` doesn't match the Polybius Square pattern
Examples:
>>> from crypyto.ciphers import PolybiusSquare
>>> ps = PolybiusSquare(5, 5)
>>> ps.decrypt('5x5#5-1;3-3;3-1;2-4;4-5;5-3;4-4;5-1;4-1;2-3;5-1;3-4;3-4;1-1;2-2;5-1')
'ENCRYPTEDMESSAGE'
"""
cipher = cipher.lower()
match = re.search(r'(?:\d+x\d+#)?((?:\d+-\d+;?)+)', cipher)
if match:
positions = match.group(1).split(';')
text = ''.join([self.pos_to_abc[pos] for pos in positions])
return text
else:
raise ValueError('Cipher doesn\'t match the Polybius Square pattern.')
[docs]class Atbash:
"""
`Atbash` represents an Atbash cipher manipulator
Args:
abc (str): The alphabet used in the cipher. Defaults to ``string.ascii_uppercase``
"""
def __init__(self, abc=string.ascii_uppercase):
self.abc = abc
self.cba = abc[::-1]
self.convertion_dict = dict(zip(self.abc, self.cba))
[docs] def encrypt(self, text, decode_unicode=True):
"""
Returns encrypted text (str)
Args:
text (str): The text to be encrypted
decode_unicode (bool): Whether the text should have unicode characters converted to ascii before encrypting. Defaults to ``True``
Examples:
>>> from crypyto.ciphers import Atbash
>>> atbash = Atbash()
>>> atbash.encrypt('Hello, world!')
'SVOOL, DLIOW!'
"""
text = unidecode(text).upper() if decode_unicode else text.upper()
cipher = ''.join([self.convertion_dict.get(character, character) for character in text])
return cipher
[docs] def decrypt(self, cipher, decode_unicode=True):
"""
Returns decrypted text (str)
Args:
cipher (str): The cipher to be decrypted
decode_unicode (bool): Whether the cipher should have unicode characters converted to ascii before decrypting. Defaults to ``True``
Examples:
>>> from crypyto.ciphers import Atbash
>>> atbash = Atbash()
>>> atbash.decrypt('SVOOL, DLIOW!')
'HELLO, WORLD!'
"""
return self.encrypt(cipher, decode_unicode)
[docs]class Caesar:
"""
`Caesar` represents a Caesar cipher manipulator
Args:
abc (str): The alphabet used in the cipher. Defaults to ``string.ascii_uppercase``
key (int): The key to initialize the cipher manipulator. Defaults to ``1``
"""
def __init__(self, abc=string.ascii_uppercase, key=1):
self.abc = abc
self.max_value = len(abc) - 1
self.key = abs(key)
self.caesar_dict = dict(enumerate(abc, 1))
@property
def abc(self):
return self._abc
@abc.setter
def abc(self, value):
self._abc = value
self.max_value = len(value) - 1
@property
def max_value(self):
return self._max_value
@max_value.setter
def max_value(self, value):
if value != len(self.abc) - 1:
raise ValueError('max_value is automatically set')
self._max_value = value
[docs] def encrypt(self, text, decode_unicode=True, key=None):
"""
Returns encrypted text (str)
Args:
text (str): The text to be encrypted
decode_unicode (bool): Whether the text should have unicode characters converted to ascii before encrypting. Defaults to ``True``
key (int|None): The key used to encrypt. Defaults to ``None``, which uses the value from ``self.key``
Examples:
>>> from crypyto.ciphers import Caesar
>>> caesar = Caesar(key=5)
>>> caesar.encrypt('Hello, world!')
'MJQQT, BTWQI!'
"""
key = self.key if key == None else key
text = unidecode(text).upper() if decode_unicode else text.upper()
cipher = ''
for letter in text:
if letter in self.abc:
letter_index = self.abc.index(letter) + key
if letter_index > self.max_value:
letter_index = letter_index - self.max_value - 1
if letter_index < 0:
letter_index = letter_index + self.max_value + 1
cipher += self.abc[letter_index]
else:
cipher += letter
return cipher
[docs] def decrypt(self, cipher, decode_unicode=True, key=None):
"""
Returns decrypted cipher (str)
Args:
cipher (str): The cipher to be decrypted
decode_unicode (bool): Whether the cipher should have unicode characters converted to ascii before decrypting. Defaults to ``True``
key (int|None): The key used to decrypt. Defaults to ``None``, which uses the value from ``self.key``
Examples:
>>> from crypyto.ciphers import Caesar
>>> caesar = Caesar(key=5)
>>> caesar.decrypt('MJQQT, BTWQI!')
'HELLO, WORLD!'
"""
key = key if key else self.key
text = self.encrypt(cipher, decode_unicode, -key)
return text
[docs] def brute_force(self, cipher, decode_unicode=True, output_file=None):
"""
Prints (to stdout or specified file) all possible results
Args:
cipher (str): The cipher to be decrypted
decode_unicode (bool): Whether the cipher should have unicode characters converted to ascii before decrypting. Defaults to ``True``
output_file (str|None): The filename of the file the results are gonna be printed. Defaults to ``None``, which indicated printing on stdout
Examples:
>>> from crypyto.ciphers import Caesar
>>> caesar = Caesar()
>>> caesar.brute_force('MJQQT, BTWQI!')
NKRRU, CUXRJ!
OLSSV, DVYSK!
...
HELLO, WORLD!
IFMMP, XPSME!
...
"""
if self.max_value > 30 and not output_file:
print('There are {} possible results. You can specify an output file in the parameter output_file'.format(self.max_value))
print('Are you sure you want to print them all (Y/N)?')
if not input().upper().startswith('Y'):
return
results = ''
for try_number in range(1, self.max_value + 1):
text = self.encrypt(cipher, decode_unicode, try_number)
results += text + '\n'
if output_file:
with open(output_file, 'w') as out:
out.write(results.strip())
else:
print(results.strip())
ROT13 = Caesar(key=13)
[docs]class Affine:
"""
`Affine` represents an Affine cipher manipulator
Args:
a (int): Value of ``a``. Must be coprime to ``len(abc)``
b (int): Value of ``b``
abc (str): The alphabet used in the cipher. Defaults to ``string.ascii_uppercase``
Raises:
ValueError: If ``a`` is not coprime to ``len(abc)``
"""
def __init__(self, a, b, abc=string.ascii_uppercase):
self.abc = abc
self.a = a
self.b = b
self.pos_to_abc = dict(enumerate(abc))
self.abc_to_pos = {v:k for k, v in self.pos_to_abc.items()}
@property
def a(self):
return self._a
@a.setter
def a(self, value):
if gcd(value, len(self.abc)) != 1:
raise ValueError('Parameter a must be coprime to {}'.format(len(self.abc)))
self._a = value
[docs] def encrypt(self, text):
"""
Returns encrypted text (str)
Args:
text (str): Text to be encrypted
Examples:
>>> from crypyto.cipher import Affine
>>> af = Affine(a=5, b=8)
>>> af.encrypt('Hello, world!')
'RCLLA, OAPLX!'
"""
text = unidecode(text).upper()
cipher = ''
for character in text:
if character in self.abc:
x = self.abc_to_pos[character]
cipher_pos = (self.a * x + self.b) % len(self.abc)
character = self.pos_to_abc[cipher_pos]
cipher += character
return cipher
[docs] def decrypt(self, cipher):
"""
Returns decrypted cipher (str)
Args:
cipher (str): Cipher to be decrypted
Examples:
>>> from crypyto.cipher import Affine
>>> af = Affine(a=5, b=8)
>>> af.decrypt('RCLLA, OAPLX!')
'HELLO, WORLD!'
"""
cipher = cipher.upper()
text = ''
for character in cipher:
if character in self.abc:
mod_inv = -self.a % len(self.abc)
cipher_pos = self.abc_to_pos[character]
x = mod_inv * (cipher_pos - self.b) % 26
character = self.pos_to_abc[x]
text += character
return text
[docs]class RailFence:
"""
`RailFence` represents a Rail Fence cipher manipulator
Args:
n_rails (int): Number of rails
only_alnum (bool): Whether the manipulator will only encrypt alphanumerical characters. Defaults to ``False``
direction (str): Default direction to start zigzagging. Must be ``'D'`` (Downwards) or ``'U'`` (Upwards). Defaults to ``'D'``
Raises:
ValueError: When ``direction`` doesn't start with ``'U'`` or ``'D'``
"""
def __init__(self, n_rails, only_alnum=False, direction='D'):
self.direction = direction
self.n_rails = n_rails
self.only_alnum = only_alnum
self.not_alnum_pattern = re.compile(r'[\W_]+', re.UNICODE)
self.direction = direction
@property
def n_rails(self):
return self._n_rails
@n_rails.setter
def n_rails(self, value):
self._n_rails = abs(value) if abs(value) > 1 else 2
self.cycle = self.n_rails * 2 - 2
@property
def cycle(self):
return self._cycle
@cycle.setter
def cycle(self, value):
if value != self.n_rails * 2 - 2:
raise ValueError('Cycle number is automatically calculated')
self._cycle = value
@property
def direction(self):
return self._direction
@direction.setter
def direction(self, value):
if value[0].upper() not in ['D', 'U']:
raise ValueError('direction must be (U)p or (D)own')
self._direction = value[0].upper()
def _zig_zag_for(self, iterate_over, action):
direction = self.direction
rail_index = 0 if direction == 'D' else self.n_rails - 1
for n in iterate_over:
action(rail_index, n)
if rail_index == 0:
direction = 'U'
elif rail_index == self.n_rails - 1:
direction = 'D'
rail_index = rail_index + 1 if direction == 'U' else rail_index - 1
[docs] def encrypt(self, text):
"""
Returns encrypted text (str)
Args:
text (str): The text to be encrypted
Examples:
>>> from crypyto.cipher import RailFence
>>> rf = RailFence(n_rails=3, only_alnum=True)
>>> rf.encrypt('WE ARE DISCOVERED. FLEE AT ONCE')
'WECRLTEERDSOEEFEAOCAIVDEN'
"""
text = self.not_alnum_pattern.sub('', text) if self.only_alnum else text
rails = [''] * self.n_rails
def add_char(rail_index, *params):
nonlocal rails
rails[rail_index] += params[0]
self._zig_zag_for(text, add_char)
cipher = ''.join(rails)
return cipher
[docs] def decrypt(self, cipher):
"""
Returns decrypted cipher
Args:
cipher (str): The cipher to be decrypted
Examples:
>>> from crypyto.cipher import RailFence
>>> rf = RailFence(n_rails=3, only_alnum=True)
>>> rf.decrypt('WECRLTEERDSOEEFEAOCAIVDEN')
'WEAREDISCOVEREDFLEEATONCE'
"""
base_width, n_extra = divmod(len(cipher), self.cycle)
n_characters_rails = [base_width * 2 if 0 < n_rail < self.n_rails - 1 else base_width for n_rail in range(self.n_rails)]
def add_n_characters(rail_index, *trash):
nonlocal n_characters_rails
n_characters_rails[rail_index] += 1
self._zig_zag_for(range(n_extra), add_n_characters)
rails = [[] for n in range(self.n_rails)]
cipher_index = 0
for rail_index, n_characters in enumerate(n_characters_rails):
rails[rail_index].extend(list(cipher[cipher_index:cipher_index + n_characters]))
cipher_index += n_characters
text = ''
def add_decrypted_char(rail_index, *trash):
nonlocal text
text += rails[rail_index].pop(0)
self._zig_zag_for(range(len(cipher)), add_decrypted_char)
return text
[docs] def brute_force(self, cipher, output_file=None):
"""
Prints (to stdout or specified file) all possible decrypted results
Args:
cipher (str): The cipher to be decrypted
output_file (str|None): The filename of the file the results are gonna be printed. Defaults to ``None``, which indicated printing on stdout
Examples:
>>> from crypyto.ciphers import RailFence
>>> rf = RailFence(n_rails=1, only_alnum=True)
>>> rf.decrypt('WECRLTEERDSOEEFEAOCAIVDEN')
There are 46 possible results. You can specify an output file in the parameter output_file
Are you sure you want to print them all (Y/N)?
Y
WEEFCERALOTCEAEIRVDDSEONE
WEAREDISCOVEREDFLEEATONCE
...
NEDVIACOAEFEEOSDREETREWCL
NEDVIACOAEFEEOSDREETLREWC
"""
n_possibilities = len(cipher) * 2 - 4
if n_possibilities > 30 and not output_file:
print('There are {} possible results. You can specify an output file in the parameter output_file'.format(n_possibilities))
print('Are you sure you want to print them all (Y/N)?')
if not input().upper().startswith('Y'):
return
initial_direction = self.direction
results = ''
for direction in ['D', 'U']:
self.direction = direction
initial_n_rails = self.n_rails
for n in range(2, len(cipher)):
self.n_rails = n
results += self.decrypt(cipher) + '\n'
self.n_rails = initial_n_rails
self.direction = initial_direction
if output_file:
with open(output_file, 'w') as out:
out.write(results.strip())
else:
print(results.strip())
[docs]class Keyword:
"""
`Keyword` represents a Keyword Cipher manipulator
Args:
key (str): The keyword to encrypt/decrypt
abc (str): The alphabet used in the cipher. Defaults to ``string.ascii_uppercase``
"""
def __init__(self, key, abc=string.ascii_uppercase):
self.abc = abc.upper()
self.key = key
@property
def key(self):
return self._key
@key.setter
def key(self, value):
value = unidecode(value).upper()
self._key = ''
for letter in value:
if letter not in self._key:
self._key += letter
key_abc = self._key + ''.join(letter for letter in self.abc if letter not in self._key)
self._abc_to_key = dict(zip(self.abc, key_abc))
self._key_to_abc = dict(zip(key_abc, self.abc))
[docs] def encrypt(self, text):
"""
Returns encrypted text (str)
Args:
text (str): Text to be encrypted
Examples:
>>> from crypyto.ciphers import Keyword
>>> kw = Keyword('secret')
>>> kw.encrypt('Hello, world!')
'BEHHK, VKNHR!'
"""
text = unidecode(text).upper()
cipher = ''.join(self._abc_to_key.get(char, char) for char in text)
return cipher
[docs] def decrypt(self, cipher):
"""
Returns decrypted cipher (str)
Args:
cipher (str): Cipher to be decrypted
Examples:
>>> from crypyto.ciphers import Keyword
>>> kw = Keyword('secret')
>>> kw.decrypt('BEHHK, VKNHR!')
'HELLO, WORLD!'
"""
cipher = cipher.upper()
text = ''.join(self._key_to_abc.get(char, char) for char in cipher)
return text
[docs]class Vigenere:
"""
`Vigenere` represents a Vigenère Cipher manipulator
Args:
key (str): The key to encode/decode
abc (str): The alphabet the cipher will be based upon. Defauts to ``string.ascii.uppercase``
decode_unicode_key (bool): Whether the key should have unicode characters converted to ascii. Defaults to ``True``
"""
def __init__(self, key, abc=string.ascii_uppercase, decode_unicode_key=True):
self.abc = abc
self._decode_unicode_key = decode_unicode_key
self.key = key
@property
def key(self):
return self._key
@key.setter
def key(self, value):
value = unidecode(value.upper()) if self._decode_unicode_key else value.upper()
self._key = self._not_abc_pattern.sub('', value)
@property
def abc(self):
return self._abc
@abc.setter
def abc(self, value):
self._abc = value.upper()
self._not_abc_pattern = re.compile('[^{}]+'.format(self._abc), re.UNICODE)
def _prepare_encryption(self, text, decode_unicode):
text = unidecode(text).upper() if decode_unicode else text.upper()
text_only_abc = self._not_abc_pattern.sub('', text)
rpt_times, extra_letters = divmod(len(text_only_abc), len(self.key))
key = self.key * rpt_times + self.key[:extra_letters]
return text, key
def _encrypt(self, text, decode_unicode=True, decrypt=False):
text, key = self._prepare_encryption(text, decode_unicode)
abc_index = 0
cipher = ''
for char in text:
if char in self.abc:
mul_factor = -1 if decrypt else 1
cipher_index = self.abc.index(char) + mul_factor * self.abc.index(key[abc_index])
cipher_index = cipher_index + 26 if cipher_index < 0 else cipher_index % 26
cipher += self.abc[cipher_index]
abc_index += 1
else:
cipher += char
return cipher
[docs] def encrypt(self, text, decode_unicode=True):
"""
Returns encrypted text (str)
Args:
text (str): Text to be encrypted
decode_unicode (bool): Whether the text should have unicode characters converted to ascii before encrypting. Defaults to ``True``
Examples:
>>> from crypyto.ciphers import Vigenere
>>> v = Vigenere('secret')
>>> v.encrypt('Hello, world!')
'ZINCS, PGVNU!'
"""
return self._encrypt(text, decode_unicode, False)
[docs] def decrypt(self, cipher, decode_unicode=True):
"""
Returns decrypted cipher
Args:
cipher (str): Cipher to be decrypted
decode_unicode (bool): Whether the cipher should have unicode characters converted to ascii before decrypting. Defaults to ``True``
Examples:
>>> from crypyto.ciphers import Vigenere
>>> v = Vigenere('secret')
>>> v.decrypt('ZINCS, PGVNU!')
'HELLO, WORLD!'
"""
return self._encrypt(cipher, decode_unicode, True)
[docs]class Beaufort(Vigenere):
"""
`Beaufort` represents a Beaufort Cipher manipulator
Args:
key (str): The key to encode/decode
abc (str): The alphabet the cipher will be based upon. Defauts to ``string.ascii.uppercase``
decode_unicode_key (bool): Whether the key should have unicode characters converted to ascii. Defaults to ``True``
"""
def __init__(self, key, abc=string.ascii_uppercase, decode_unicode_key=True):
self._atbash = Atbash()
super().__init__(key, abc, decode_unicode_key)
@property
def key(self):
return self._key
@key.setter
def key(self, value):
value = unidecode(value.upper()) if self._decode_unicode_key else value.upper()
self._key = self._atbash.encrypt(self._not_abc_pattern.sub('', value))
[docs] def encrypt(self, text, decode_unicode=True):
"""
Returns encrypted text (str)
Args:
text (str): The text to be encrypted
decode_unicode (bool): Whether the text should have unicode characters converted to ascii before encrypting. Defaults to ``True``
Examples:
>>> from crypyto.ciphers import Beaufort
>>> b = Beaufort('secret')
>>> b.encrypt('Hello, world!')
'LARGQ, XENRO!'
"""
return self._encrypt(self._atbash.encrypt(text), decode_unicode, True)
[docs] def decrypt(self, cipher, decode_unicode=True):
"""
Returns decrypted cipher (str)
Args:
cipher (str): The cipher to be decrypted
decode_unicode (bool): Whether the cipher should have unicode characters converted to ascii before decrypting. Defaults to ``True``
Examples:
>>> from crypyto.ciphers import Beaufort
>>> b = Beaufort('secret')
>>> b.decrypt('LARGQ, XENRO!')
'HELLO, WORLD!'
"""
return self.encrypt(cipher, decode_unicode)
[docs]class Gronsfeld(Vigenere):
"""
`Gronsfeld` represents a Gronsfeld Cipher manipulator
Args:
key (str): The key to encode/decode. Must contain only numerical characters (0-9)
abc (str): The alphabet the cipher will be based upon. Defauts to ``string.ascii.uppercase``
"""
def __init__(self, key, abc=string.ascii_uppercase):
self.abc = abc
self.only_num_pattern = re.compile(r'[^\d]+', re.UNICODE)
self.key = key
@property
def abc(self):
return self._abc
@abc.setter
def abc(self, value):
self._abc = value.upper()
self._not_abc_pattern = re.compile('[^{}]+'.format(self._abc), re.UNICODE)
self._caesar = Caesar(value)
@property
def key(self):
return self._key
@key.setter
def key(self, value):
self._key = self.only_num_pattern.sub('', value)
def _encrypt(self, text, decode_unicode=True, decrypt=False):
text, key = self._prepare_encryption(text, decode_unicode)
abc_index = 0
cipher = ''
for char in text:
if char in self.abc:
caesar_key = int(key[abc_index])
cipher_char = self._caesar.decrypt(char, key=caesar_key) if decrypt else self._caesar.encrypt(char, key=caesar_key)
cipher += cipher_char
abc_index += 1
else:
cipher += char
return cipher
[docs] def encrypt(self, text, decode_unicode=True):
"""
Returns encrypted text (str)
Args:
text (str): The text to be encrypted
decode_unicode (bool): Whether the text should have unicode characters converted to ascii before encrypting. Defaults to ``True``
Examples:
>>> from crypyto.ciphers import Gronsfeld
>>> g = Gronsfeld('2317')
>>> g.encrypt('Hello, world!')
'JHMSQ, ZPYNG!'
"""
return self._encrypt(text, decode_unicode, False)
[docs] def decrypt(self, cipher, decode_unicode=True):
"""
Returns decrypted cipher (str)
Args:
cipher (str): The cipher to be decrypted
decode_unicode (bool): Whether the text should have unicode characters converted to ascii before encrypting. Defaults to ``True``
Examples:
>>> from crypyto.ciphers import Gronsfeld
>>> g = Gronsfeld('2317')
>>> g.decrypt('JHMSQ, ZPYNG!')
'HELLO, WORLD!'
"""
return self._encrypt(cipher, decode_unicode, True)