Tags

, , , ,

Earlier this month, on my other blog, I wrote about the Playfair Cipher, a polygraphic substitution cipher invented by Sir Charles Wheatstone in 1854.

At the time I mused about writing some Python to automate using the cipher, and now I’ve done that, so here it is:

You’ll need either to be already familiar with the Playfair cipher or to have read my post or the wiki article. Here I’m assuming you know how it works and won’t describe it.

Suffice to say both encoding and decoding share three basic tasks:

  1. Conditioning the input.
  2. Finding letter pairs in the grid.
  3. Encoding/Decoding the grid pairs.

Only the third step differs (slightly) due to how decoding is different from encoding.

§

The code is reasonably simple. As usual, I created a class.

Here’s the class and its constructor (and its string appearance):

class Playfair (object):
    A2Z = 'ABCDEFGHIKLMNOPQRSTUVWXYZ'

    def __init__ (self, keyword):
        rs = range(5)
        kw = []
        for ch in keyword.upper():
            if (ch in self.A2Z) and (ch not in kw):
                kw.append(ch)
        az = [a for a in self.A2Z if a not in kw]
        cs = list(reversed(kw+az))
        self.grid = [[[cs.pop(),r,c] for c in rs] for r in rs]

    def __str__ (self):
        a = [str(row) for row in self.grid]
        return '\n'.join(a)

#

So a new instance takes the keyword (or phrase) used to encrypt or decrypt messages. Using that, it creates the Playfair grid.

Note that the key is forced to upper case and uses only unique allowed chars (lines 7–9). The next two lines (10 and 11) append the unused alpha chars to the key, which creates a string 25-characters long (note that “J” is ignored here). The last line (12) creates the actual 5×5 Playfair grid.

The string appearance of an instance is a string version of the grid.

§

To encode a string, call the encode method:

def encode (self, plaintext):
    self.msg = self._condition(plaintext)
    self._ps = [self._find_pair(m) for m in self.msg]
    self._gs = [self._encode(p0,p1) for p0,p1 in self._ps]
    self.enc = [(g[0][0], g[1][0]) for g in self._gs]
    self.txt = ''.join([a[0]+a[1] for a in self.enc])
    return self.txt
#

The method takes a plaintext message which it first conditions (see below). Then it generates a list of grid pairs, which it encodes (lines 2–4).

The enc instance member (line 5) is a list of encoded pair tuples, and the txt member (line 6), which is returned, is a string of those pairs.

The output is a continuous string of encoded characters:

VOPATIQCPAYHKTAGHQYFNAWAQZCQLOCQFW

The user breaks this up into random groups and adds spurious punctuation:

VOP AT IQCPAY, HKTAG. HQYF: NAW A QZCQLO CQFW!

To create the final cipher version. (I thought about writing some code to automate that, but been there done that, wrote the post.)

§

The decode method is very similar (with an added bit):

def decode (self, ciphertext):
    self.msg = self._condition(ciphertext)
    self._ps = [self._find_pair(m) for m in self.msg]
    self._gs = [self._decode(p0,p1) for p0,p1 in self._ps]
    self.enc = [(g[0][0], g[1][0]) for g in self._gs]
    self.txt = ''.join([a[0]+a[1] for a in self.enc])
    self.txt = self.txt.rstrip('Q')
    ix = self.txt.find('Q')
    while 0 < ix:
        if self.txt[ix-1] == self.txt[ix+1]:
            self.txt = self.txt[0:ix]+self.txt[ix+1:]
        ix = self.txt.find('Q', ix+1)
    return self.txt
#

The first two lines are exactly the same as in the encode method. The third line differs only in calling the _decode method rather than the _encode method.

The enc instance member is still a list of encoded pair tuples, and the txt member is still returned.

Lines 7–12 first remove any trailing ‘Q’ characters and then remove instances where a ‘Q’ comes between matching letters. (This is necessary to remove ‘Q’ characters inserted to break up those matching pairs, so BALLPIT becomes BALQLPIT.)

§

The real meat is in the helper methods that condition the input, find pairs, and encode or decode.

Here’s the _condition method:

def _condition (self, message):
    mesg = []
    state = 0
    tempc = ''
    for ch in message.upper():
        if ch.isalpha():
            if ch == 'J':
                ch = 'I'
            if state:
                if ch == tempc:
                    t = (tempc, 'Q')
                    mesg.append(t)
                    continue
                t = (tempc, ch)
                mesg.append(t)
                state = 0
            else:
                tempc = ch
                state = 1
    if state:
        t = (tempc, 'Q')
        mesg.append(t)
    return mesg
#

Essentially, it forces the message to upper case, ignores anything not an alpha char, converts the letter ‘J’ to ‘I’, and breaks the message into two-character pairs (while inserting ‘Q’ characters to break up matching pairs as described just above).

It returns a list of two-character tuples.

§

Here’s the _find_pair method:

def _find_pair (self, pair):
    c0,c1 = pair
    rc0,rc1 = (0,0), (0,0)
    for row in self.grid:
        for col in row:
            if col[0] == c0:
                rc0 = (col[1],col[2])
            if col[0] == c1:
                rc1 = (col[1],col[2])
    return rc0,rc1
#

It takes a two-character tuple and returns a tuple of two tuples, each a row-column coordinate of the input character.

§

Lastly, the _encode method:

def _encode (self, rc0, rc1):
    if (rc0[0]==rc1[0]) and (rc0[1]==rc1[1]):
        # Same row and column...
        raise RuntimeError('Matching pair!')
    if rc0[0] == rc1[0]:
        # Same row...
        c0 = self.grid[rc0[0]][(rc0[1]+1)%5]
        c1 = self.grid[rc1[0]][(rc1[1]+1)%5]
        return c0,c1
    if rc0[1] == rc1[1]:
        # Same column...
        c0 = self.grid[(rc0[0]+1)%5][rc0[1]]
        c1 = self.grid[(rc1[0]+1)%5][rc1[1]]
        return c0,c1
    # Different row and column...
    c0 = self.grid[rc0[0]][rc1[1]]
    c1 = self.grid[rc1[0]][rc0[1]]
    return c0,c1
#

And the _decode method, which is nearly the same:

def _decode (self, rc0, rc1):
    if (rc0[0]==rc1[0]) and (rc0[1]==rc1[1]):
        # Same row and column...
        raise RuntimeError('Matching pair!')
    if rc0[0] == rc1[0]:
        # Same row...
        c0 = self.grid[rc0[0]][(rc0[1]-1)%5]
        c1 = self.grid[rc1[0]][(rc1[1]-1)%5]
        return c0,c1
    if rc0[1] == rc1[1]:
        # Same column...
        c0 = self.grid[(rc0[0]-1)%5][rc0[1]]
        c1 = self.grid[(rc1[0]-1)%5][rc1[1]]
        return c0,c1
    # Different row and column...
    c0 = self.grid[rc0[0]][rc1[1]]
    c1 = self.grid[rc1[0]][rc0[1]]
    return c0,c1
#

The decode process uses slightly different rules than the encode process.

§

And that’s the Playfair class. To use it:

def encode_message (keyword, message):
    coder = Playfair(keyword)
    return coder.encode(message)
#

Just create an instance and call the encode method. The same process for decoding, just call the decode method instead.