2025-02-23 17:15:25 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# InterPlay-36: reference Python 3 implementation for the
|
|
|
|
|
# Interleaved 6x6 Playfair crypto algorithm
|
|
|
|
|
#
|
|
|
|
|
# Features:
|
|
|
|
|
# * fixed 6x6 alphabet (whitespace, 1-9, a-z; 0 is replaced with o)
|
|
|
|
|
# * normal Playfair rules for table keying and encryption/decryption
|
|
|
|
|
# * the plaintext is interleaved with random chars
|
|
|
|
|
# before encryption and they are removed after decryption
|
|
|
|
|
# * since the cryptogram may begin with a space, a random prefix is prepended
|
|
|
|
|
# * (its length depends on the key length)
|
|
|
|
|
#
|
|
|
|
|
# Created by Luxferre in 2025, released into public domain
|
|
|
|
|
|
|
|
|
|
import sys, math, secrets
|
|
|
|
|
from collections import OrderedDict
|
|
|
|
|
|
|
|
|
|
pfgrid = ' 123456789abcdefghijklmnopqrstuvwxyz'
|
|
|
|
|
pfsize = int(math.sqrt(len(pfgrid)))
|
|
|
|
|
|
|
|
|
|
# message/keystring preparation step: replace 0 with o then filter out all invalid chars
|
|
|
|
|
def filtermsg(msg:str):
|
2025-03-04 19:15:06 +02:00
|
|
|
return ''.join(list(filter(lambda i: i in pfgrid, msg.lower().replace('0', 'o').replace('\n', ' '))))
|
2025-02-23 17:15:25 +02:00
|
|
|
|
|
|
|
|
# grid keying algorithm
|
|
|
|
|
def keygrid(key:str):
|
|
|
|
|
# start with the standard fill-in
|
|
|
|
|
startgrid = list(OrderedDict.fromkeys(filtermsg(key) + pfgrid).keys())
|
|
|
|
|
# extract the transposition keys
|
|
|
|
|
tkey1 = []
|
|
|
|
|
tkey2 = []
|
|
|
|
|
rkey = list(map(lambda x: pfgrid.index(x), startgrid[0:pfsize])) # first row
|
|
|
|
|
srkey = sorted(rkey)
|
|
|
|
|
for c in rkey:
|
|
|
|
|
tkey1.append(srkey.index(c))
|
|
|
|
|
rkey = list(map(lambda x: pfgrid.index(x), startgrid[pfsize:pfsize*2])) # second row
|
|
|
|
|
srkey = sorted(rkey)
|
|
|
|
|
for c in rkey:
|
|
|
|
|
tkey2.append(srkey.index(c))
|
|
|
|
|
# perform the zigzag row transposition into the intermediate grid
|
|
|
|
|
igrid = []
|
|
|
|
|
for row in tkey1:
|
|
|
|
|
for i in range(0, pfsize):
|
|
|
|
|
igrid.append(startgrid[((row + (i&1)) % pfsize) * pfsize + i])
|
|
|
|
|
# perform the straight columnar transposition into the final key grid
|
|
|
|
|
keygrid = []
|
|
|
|
|
for col in tkey2:
|
|
|
|
|
for i in range(0, pfsize):
|
|
|
|
|
keygrid.append(igrid[i * pfsize + col])
|
|
|
|
|
return keygrid
|
|
|
|
|
|
|
|
|
|
# get character coordinates in the grid
|
|
|
|
|
def getcoords(grid:list, char:str):
|
|
|
|
|
pos = grid.index(char)
|
|
|
|
|
return (pos % pfsize, int(pos // pfsize))
|
|
|
|
|
|
|
|
|
|
# get grid character by coordinates
|
|
|
|
|
def getgridchar(grid:list, x:int, y:int):
|
|
|
|
|
return grid[y * pfsize + x]
|
|
|
|
|
|
|
|
|
|
# Interleaved Playfair encryption method
|
|
|
|
|
def ipfencrypt(msg:str, key:str):
|
|
|
|
|
kg = keygrid(key)
|
|
|
|
|
# prepare the message
|
|
|
|
|
msg = filtermsg(msg).strip()
|
|
|
|
|
# perform the encryption
|
|
|
|
|
enc = []
|
|
|
|
|
for c in msg:
|
|
|
|
|
# shape the digraphs by interleaving random characters
|
|
|
|
|
nc = c
|
|
|
|
|
while nc == c: # the second char in digraph must differ
|
|
|
|
|
nc = secrets.choice(pfgrid)
|
|
|
|
|
# run the algo
|
|
|
|
|
x1, y1 = getcoords(kg, c)
|
|
|
|
|
x2, y2 = getcoords(kg, nc)
|
|
|
|
|
if x1 != x2 and y1 != y2: # no coordinates are equal
|
|
|
|
|
enc.append(getgridchar(kg, x2, y1))
|
|
|
|
|
enc.append(getgridchar(kg, x1, y2))
|
|
|
|
|
else:
|
|
|
|
|
if x1 == x2: # same column: get the coords below
|
|
|
|
|
y1 = (y1 + 1) % pfsize
|
|
|
|
|
y2 = (y2 + 1) % pfsize
|
|
|
|
|
elif y1 == y2: # same row: get the coords to the right
|
|
|
|
|
x1 = (x1 + 1) % pfsize
|
|
|
|
|
x2 = (x2 + 1) % pfsize
|
2025-02-24 11:29:07 +02:00
|
|
|
# swap the ciphertext character
|
2025-02-23 17:15:25 +02:00
|
|
|
enc.append(getgridchar(kg, x2, y2))
|
2025-02-24 11:29:07 +02:00
|
|
|
enc.append(getgridchar(kg, x1, y1))
|
2025-02-23 17:15:25 +02:00
|
|
|
# generate a random prefix
|
|
|
|
|
preflen = (len(key) % 10) + 1
|
|
|
|
|
prefix = ''
|
|
|
|
|
for i in range(preflen):
|
|
|
|
|
prefix += secrets.choice(pfgrid[1:])
|
2025-03-04 21:47:28 +02:00
|
|
|
return prefix + ''.join(enc).replace(' ', '__')
|
2025-02-23 17:15:25 +02:00
|
|
|
|
|
|
|
|
# Interleaved Playfair decryption method
|
|
|
|
|
def ipfdecrypt(enc:str, key:str):
|
|
|
|
|
kg = keygrid(key)
|
|
|
|
|
# remove the prefix, then add a space if the cryptogram
|
|
|
|
|
# had been stripped and ended with a space
|
|
|
|
|
preflen = (len(key) % 10) + 1
|
2025-03-04 21:47:28 +02:00
|
|
|
enc = enc[preflen:].rstrip().replace('__', ' ')
|
2025-02-23 17:15:25 +02:00
|
|
|
if len(enc) & 1 == 1:
|
|
|
|
|
enc += ' '
|
|
|
|
|
# split into digraphs
|
|
|
|
|
digraphs = [enc[i:i+2] for i in range(0, len(enc), 2)]
|
|
|
|
|
# perform the decryption
|
|
|
|
|
msg = []
|
|
|
|
|
for dg in digraphs: # for IPF, we only decrypt the first digraph char
|
|
|
|
|
x1, y1 = getcoords(kg, dg[0])
|
|
|
|
|
x2, y2 = getcoords(kg, dg[1])
|
|
|
|
|
if x1 != x2 and y1 != y2: # no coordinates are equal
|
|
|
|
|
msg.append(getgridchar(kg, x2, y1))
|
|
|
|
|
else:
|
|
|
|
|
if x1 == x2: # same column: get the coords above
|
2025-02-24 11:29:07 +02:00
|
|
|
y2 -= 1
|
|
|
|
|
if y2 < 0:
|
|
|
|
|
y2 += pfsize
|
2025-02-23 17:15:25 +02:00
|
|
|
elif y1 == y2: # same row: get the coords to the left
|
2025-02-24 11:29:07 +02:00
|
|
|
x2 -= 1
|
|
|
|
|
if x2 < 0:
|
|
|
|
|
x2 += pfsize
|
|
|
|
|
msg.append(getgridchar(kg, x2, y2))
|
2025-02-23 17:15:25 +02:00
|
|
|
# finalize the decrypted message
|
2025-03-04 19:15:06 +02:00
|
|
|
return ''.join(msg).replace(' ', '\n').strip()
|
2025-02-23 17:15:25 +02:00
|
|
|
|
|
|
|
|
# entrypoint parameters: [mode] [keystring]
|
|
|
|
|
# modes: e - encrypt with 6x6 IPF, d - decrypt with 6x6 IPF
|
|
|
|
|
def main():
|
|
|
|
|
mode = sys.argv[1].lower()
|
|
|
|
|
key = sys.argv[2]
|
|
|
|
|
msg = sys.stdin.read()
|
|
|
|
|
if mode.startswith('e'):
|
|
|
|
|
print(ipfencrypt(msg, key))
|
|
|
|
|
else:
|
|
|
|
|
print(ipfdecrypt(msg, key))
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
main()
|