I have written a Python 3 program that can encrypt and decrypt Playfair cipher, though I know it has been implemented countless times, none of the scripts I have seen so far(e.g. https://codegolf.stackexchange.com/a/23363) is as performant as mine, and many of them only do the encryption part and not the decryption part, not to mention most of them are written in Python 2.
My code is self documentary, without any code obfuscation and nested for loops and nested dict
s/list
s and any other kinds of nonsense, it uses a simple flat list
and divmod
as basis of the transformations.
My code implements all the rules of Playfair cipher, along with some additional rules: there are a total of 600 bigrams defined by the rules of Playfair cipher, and 'XX'
isn't one of them, however in practice 'XX'
can be a very real possibility;
I have pulled a total of 105230 words from GCIDE, although only one of them contains 'xx'
(vioxx), 144 of them starts with x and 400 of them ends with x, there are three cases where xx may occur:
case 1: a word not found in dictionaries contains xx
case 2: a word that ends with x followed by a word that starts with x
case 3: the last letter in the message is x and the message contains odd number of letters
When one of the above is true, standard Playfair cipher can't encrypt xx, but in my algorithm this is just a special case of rule 2.
Additionally, because of rule 1, there is a need to remove every x in the second position in a bigram, however bigram with x as its second letter can very possibly occur in a word, and when this happens an additional x should be inserted between the letters to break the bigram, for example, 'axe'
after preparation is 'AX XE'
.
The code:
import re
from enum import Enum
LETTERS = 'ABCDEFGHIKLMNOPQRSTUVWXYZ'
class Playfair:
@staticmethod
def check_input(arg):
if not isinstance(arg, str):
raise TypeError(
'The argument of the function should be an instance of `str`')
if not all(i.isascii() & i.isprintable() for i in arg):
raise ValueError(
'The argument of the function should be a `str` containing only printable ASCII characters')
@staticmethod
def prepare(msg):
msg = msg.upper().replace('J', 'I')
Playfair.check_input(msg)
msg = ''.join([i for i in msg if i.isalpha()])
while msg:
s = msg[:2]
if len(set(s)) == 2 and s[-1] != 'X':
msg = msg[2:]
else:
s = s[0] + 'X'
msg = msg[1:]
yield s
def __init__(self, key):
key = key.upper().replace('J', 'I')
Playfair.check_input(key)
key = ''.join(i for i in key if i.isalpha())
self.square = list(dict.fromkeys(key + LETTERS))
def locate(self, letter):
letter = letter.upper()
return list(divmod(self.square.index(letter), 5))
def get_letter(self, pos):
index = pos[0] * 5 + pos[1]
return self.square[index]
def transform(self, pair, move):
p0, p1 = map(self.locate, pair)
def roll(x): return (x + move) % 5
for i in range(3):
if i < 2 and p0[i] == p1[i]:
j = 1 - i
p0[j], p1[j] = map(roll, (p0[j], p1[j]))
break
if i == 2:
p0[1], p1[1] = p1[1], p0[1]
return ''.join(map(self.get_letter, (p0, p1)))
def encrypt(self, msg):
pairs = Playfair.prepare(msg)
return ' '.join(self.transform(pair, 1) for pair in pairs)
def decrypt(self, msg):
msg = msg.upper()
Playfair.check_input(msg)
if not re.match(r'^([A-Z]{2}(\s)?)+$', msg):
raise ValueError(
"The inputted message isn't a valid Playfair cipher message")
msg = re.findall('[A-Z]{2}', msg)
def rule1(x): return x if x[1] != 'X' else x[0]
return ' '.join(rule1(self.transform(s, -1)) for s in msg)
def test():
p = Playfair("Imagine there's no heaven")
plain = [
'I am a new soul, I came to this strange world',
'Starry starry night, paint your palette blue and grey',
'I crown me king of the sweet cold north',
'If I die young, bury me in satin',
"Come gather 'round people, wherever you roam",
'Somewhere over the rainbow'
]
encrypted = [
'MG AG IS ZT FI FA BG IT EV HR NE EH HG IN TU CE QB',
'EH GH CG EH GH CG IM AR RK GM MS UC YE LG FH HW HT LX IO GI CN ST ZY',
'GO EC ZM IT FM IN FU HR TE UT TH DV QB ID SH BA',
'EU NO EO UC ZI AC YE WG OE SD MH MI',
'DV IT NG HR TS EC ZI CQ OF QP TU RT ST OT CG FI EC GA',
'ED IT XT TS OF OT SH RT HG MI CV XY'
]
decrypted = [
'IA MA NE WS OU LI CA ME TO TH IS ST RA NG EW OR LD',
'ST AR RY ST AR RY NI GH TP AI NT YO UR PA LE T TE BL UE AN DG RE Y',
'IC RO WN ME KI NG OF TH ES WE ET CO LD NO RT H',
'IF ID IE YO UN GB UR YM EI NS AT IN',
'CO ME GA TH ER RO UN DP EO PL EW HE RE VE RY OU RO AM',
'SO ME WH ER EO VE RT HE RA IN BO W'
]
assert [p.encrypt(i) for i in plain] == encrypted
assert [p.decrypt(i) for i in encrypted] == decrypted
if __name__ == '__main__':
test()
How can it be improved?
P.S. I have tried to pregenerate the bigrams using itertools.permutations
(naturally xx
isn't supported by this method) and corresponding encrypted bigrams, to generate two lookup tables for encryption and decryption.
I have written two versions of this, one uses two dict
s, the other uses two pandas.Series
, and with the rest of the code unchanged, and ran the test function.
Surprisingly, the one without pregeneration is the fastest, %timeit
states it takes around 1 ms on average to complete, and the one using the dict
s is in the middle, uses 2.5 ms on average to complete, and the one using vectorized code is the slowest, it takes around 8.2 ms on average to complete...