spec and Python implementation upload
This commit is contained in:
@@ -0,0 +1,225 @@
|
|||||||
|
InterPlay-36 cipher
|
||||||
|
===================
|
||||||
|
**InterPlay-36** is a symmetrical cipher that uses a 6x6 key grid for all
|
||||||
|
encryption and decryption operations. It is based upon a historical cipher
|
||||||
|
called Playfair, with several modifications applied to the plaintext and
|
||||||
|
ciphertext.
|
||||||
|
|
||||||
|
InterPlay-36 is easy to implement both in software and manually. Just like
|
||||||
|
the classic Playfair, it is optimized for pen and paper operation, but
|
||||||
|
offers significantly more security at the same time.
|
||||||
|
|
||||||
|
The cipher itself and its reference implementations are public domain.
|
||||||
|
|
||||||
|
Basic alphabet grid
|
||||||
|
-------------------
|
||||||
|
All operations start with the following basic 6x6 grid (Polybius square):
|
||||||
|
|
||||||
|
```
|
||||||
|
_ 1 2 3 4 5
|
||||||
|
6 7 8 9 a b
|
||||||
|
c d e f g h
|
||||||
|
i j k l m n
|
||||||
|
o p q r s t
|
||||||
|
u v w x y z
|
||||||
|
```
|
||||||
|
|
||||||
|
*Note*: The `_` character actually is the whitespace character.
|
||||||
|
|
||||||
|
All incoming plaintext and keys are converted to this alphabet according to
|
||||||
|
the following rules:
|
||||||
|
|
||||||
|
1. The text is converted into lowercase.
|
||||||
|
2. Every occurrence of the `0` (zero) character is replaced with the letter `o`.
|
||||||
|
3. Every occurrence of either of these characters `_-` is replaced with a space.
|
||||||
|
4. Any other characters not belonging to the alphabet are deleted from the text.
|
||||||
|
|
||||||
|
Prior to step 4, the user may apply additional conversions to preserve special
|
||||||
|
characters, such as replacing `%` with `cto`, `$` with `dlr` and so on.
|
||||||
|
|
||||||
|
Key grid preparation
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
### Step 1
|
||||||
|
|
||||||
|
The first step for preparing the key grid is the same as for the Playfair and
|
||||||
|
other classic ciphers based on Polybius squares:
|
||||||
|
|
||||||
|
1. Write the basic alphabet grid into a single string (`_1...9a...z`).
|
||||||
|
2. Prepend the key phrase in front of the basic grid string.
|
||||||
|
3. Strike out all duplicates starting from left to right.
|
||||||
|
4. Rewrite the resulting key grid into the 6x6 square.
|
||||||
|
|
||||||
|
**Example**: Suppose the key phrase is `si vis pacem para bellum`. We can start
|
||||||
|
by eliminating all duplicates in the phrase itself: `si vpacemrblu`. Then we can
|
||||||
|
write the rest of the alphabet: `si vpacemrblu123456789dfghjknoqtwxyz`. After
|
||||||
|
rewriting this string into a 6x6 grid, we can verify that we still have 36
|
||||||
|
unique characters:
|
||||||
|
```
|
||||||
|
s i _ v p a
|
||||||
|
c e m r b l
|
||||||
|
u 1 2 3 4 5
|
||||||
|
6 7 8 9 d f
|
||||||
|
g h j k n o
|
||||||
|
q t w x y z
|
||||||
|
```
|
||||||
|
|
||||||
|
You will also need to note the length KL of the initial keyphrase (the one used
|
||||||
|
before converting it into the key grid) including all internal whitespaces.
|
||||||
|
|
||||||
|
### Step 2
|
||||||
|
|
||||||
|
To finalize the key grid, perform the following steps.
|
||||||
|
|
||||||
|
1. Convert the first **row** of the grid obtained in step 1 into the numeric
|
||||||
|
indices (0-based) from the **base** alphabet.
|
||||||
|
In our example: `si_vpa` => `25, 18, 0, 31, 24, 10`
|
||||||
|
2. Normalize the index list so that it ranges from 0 to 5 according to the
|
||||||
|
order. This will get you the K1 transposition key.
|
||||||
|
In our example: `25, 18, 0, 31, 24, 10` => `4, 2, 0, 5, 3, 1`
|
||||||
|
3. Repeat steps 1 and 2 for the second row of the starting grid. This will get
|
||||||
|
you the K2 transposition key.
|
||||||
|
In our example: `c e m r b l` is the second grid row, further converted to
|
||||||
|
`12, 14, 22, 27, 11, 21` => ` 1, 2, 4, 5, 0, 3`
|
||||||
|
4. Perform a **row** zigzag transposition of the grid according to the K1 key:
|
||||||
|
For each row i taken from the transposition key, take the following values
|
||||||
|
from the key grid G (if i+1 > 5, then use 0 as the value):
|
||||||
|
G(i, 0), G(i+1, 1), G(i, 2), G(i+1, 3), G(i, 4), G(i+1, 5).
|
||||||
|
Write these values as a new row of the intermediate grid.
|
||||||
|
|
||||||
|
In our example, the key is `4, 2, 0, 5, 3, 1` and the grid is:
|
||||||
|
|
||||||
|
```
|
||||||
|
s i _ v p a
|
||||||
|
c e m r b l
|
||||||
|
u 1 2 3 4 5
|
||||||
|
6 7 8 9 d f
|
||||||
|
g h j k n o
|
||||||
|
q t w x y z
|
||||||
|
```
|
||||||
|
|
||||||
|
After the transposition, the grid becomes:
|
||||||
|
|
||||||
|
```
|
||||||
|
g t j x n z
|
||||||
|
u 7 2 9 4 f
|
||||||
|
s e _ r p l
|
||||||
|
q i w v y a
|
||||||
|
6 h 8 k d o
|
||||||
|
c 1 m 3 b 5
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Perform a straight columnar transposition of the step 3 result grid
|
||||||
|
according to the K2 key:
|
||||||
|
For each column i taken from the transposition key, write its values as a
|
||||||
|
new **row** of the final grid.
|
||||||
|
|
||||||
|
In our example, the key is `1, 2, 4, 5, 0, 3` and the grid is:
|
||||||
|
|
||||||
|
```
|
||||||
|
g t j x n z
|
||||||
|
u 7 2 9 4 f
|
||||||
|
s e _ r p l
|
||||||
|
q i w v y a
|
||||||
|
6 h 8 k d o
|
||||||
|
c 1 m 3 b 5
|
||||||
|
```
|
||||||
|
|
||||||
|
After the transposition, the final key grid becomes:
|
||||||
|
|
||||||
|
```
|
||||||
|
t 7 e i h 1
|
||||||
|
j 2 _ w 8 m
|
||||||
|
n 4 p y d b
|
||||||
|
z f l a o 5
|
||||||
|
g u s q 6 c
|
||||||
|
x 9 r v k 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we are ready to perform encryption and decryption steps.
|
||||||
|
|
||||||
|
Encryption
|
||||||
|
----------
|
||||||
|
For each plaintext character P1, do the following:
|
||||||
|
|
||||||
|
1. Select a **random** character P2 on the key grid such as P2 != P1.
|
||||||
|
2. Find the positions of P1 and P2 within the key grid.
|
||||||
|
3. If P1 and P2 are not on the same row and not on the same column, find the
|
||||||
|
opposite corners of the imaginary rectangle on the grid. The ciphertext
|
||||||
|
character C1 will be on the same row as P1, and the ciphertext character C2
|
||||||
|
will be on the same row as P2.
|
||||||
|
4. If P1 and P2 are on the same row, C1 and C2 will be to the immediate right
|
||||||
|
of P1 and P2 respectively. If a character is in the rightmost column, the
|
||||||
|
resulting character will be on the first one of the same row.
|
||||||
|
5. If P1 and P2 are on the same column, C1 and C2 will be to the immediate down
|
||||||
|
of P1 and P2 respectively. If a character is in the bottommost row, the
|
||||||
|
resulting character will be on the first one of the same column.
|
||||||
|
6. Write down C1 and C2 as the ciphertext digraph.
|
||||||
|
|
||||||
|
After all plaintext characters are processed, add PL random characters in front
|
||||||
|
of the ciphertext, where PL = KL mod 10 + 1, where KL is the initial key phrase
|
||||||
|
length. None of the prepended characters must be a whitespace.
|
||||||
|
|
||||||
|
Decryption
|
||||||
|
----------
|
||||||
|
Remove first PL characters from the ciphertext, where PL = KL mod 10 + 1, where
|
||||||
|
KL is the initial key phrase length.
|
||||||
|
|
||||||
|
Then, for each ciphertext digraph C1C2, do the following:
|
||||||
|
|
||||||
|
1. Find the first and the second character C1 and C2 within the key grid.
|
||||||
|
2. If C1 and C2 are not on the same row and not on the same column, find the
|
||||||
|
opposite corners of the imaginary rectangle on the grid. The plaintext
|
||||||
|
character P1 will be on the same row as C1.
|
||||||
|
3. If C1 and C2 are on the same row, P1 will be to the immediate left of C1.
|
||||||
|
If C1 is in the leftmost column, P1 will be on the last one of the same row.
|
||||||
|
4. If C1 and C2 are on the same column, P1 will be to the immediate up of C1.
|
||||||
|
If C1 is in the topmost row, P1 will be on the last one of the same column.
|
||||||
|
5. Write down P1 as the next plaintext character.
|
||||||
|
|
||||||
|
Design rationale
|
||||||
|
----------------
|
||||||
|
Playfair cipher had been chosen as the basis for InterPlay-36 because:
|
||||||
|
|
||||||
|
1. It allows to easily reconstruct the basic alphabet and the key grid from
|
||||||
|
the memory.
|
||||||
|
2. In case of manual encryption, the only thing that needs to be written down
|
||||||
|
besides the final ciphertext is the key grid. Obviously, any notes of the
|
||||||
|
key grid preparation must be destroyed.
|
||||||
|
|
||||||
|
InterPlay-36 improves over the classic Playfair in several ways:
|
||||||
|
|
||||||
|
1. Increases the keyspace by increasing the grid size: the theoretical amount
|
||||||
|
of permutations is equivalent to |log2(36!)| = 132 bits of key material, as
|
||||||
|
opposed to 79 key bits in case of the 5x5 grid.
|
||||||
|
2. Adds a plaintext interleaving step into the encryption phase (see below).
|
||||||
|
3. Allows to preserve whitespace in the plaintext while making sure the end
|
||||||
|
ciphertext will never start with whitespace.
|
||||||
|
|
||||||
|
The interleaving step is important to break several negative properties of the
|
||||||
|
Playfair algorithm that make its cryptanalysis easier, like its susceptibility
|
||||||
|
to digraph frequency analysis or the fact that reverse plaintext digraphs are
|
||||||
|
always encrypted to reverse ciphertext digraphs. Interleaving also makes sure
|
||||||
|
that no digraph contains a repeated letter, without complicating the logic.
|
||||||
|
|
||||||
|
### Strengthening
|
||||||
|
|
||||||
|
InterPlay-36 was designed to be used "as is", however one can easily combine it
|
||||||
|
with other popular encryption methods such as transposition ciphers (using the
|
||||||
|
key grid, a transposed key grid and/or the keyphrase length as the key sources).
|
||||||
|
In any case, it is advised to apply InterPlay-36 first in the chain when you are
|
||||||
|
encrypting the messages, and last when decrypting. The reference implementation
|
||||||
|
of InterPlay-36 in HTML5/JS also contains a flag to apply a DCT (double columnar
|
||||||
|
transposition) to the InterPlay-36 ciphertext, where two transposition keys with
|
||||||
|
coprime lengths are derived from the transposed key grid. This feature is now
|
||||||
|
considered experimental and should not be relied upon.
|
||||||
|
|
||||||
|
Reference implementations of InterPlay-36
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
* [Simple CLI implementation in Python 3](ip36.py)
|
||||||
|
* [Web version in HTML5](https://pf.hoi.st), fully client-side
|
||||||
|
|
||||||
|
Credits
|
||||||
|
-------
|
||||||
|
Created by Luxferre in 2025. Released into public domain with no warranties.
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#!/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):
|
||||||
|
return ''.join(list(filter(lambda i: i in pfgrid, msg.lower().replace('0', 'o'))))
|
||||||
|
|
||||||
|
# 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
|
||||||
|
enc.append(getgridchar(kg, x1, y1))
|
||||||
|
enc.append(getgridchar(kg, x2, y2))
|
||||||
|
# generate a random prefix
|
||||||
|
preflen = (len(key) % 10) + 1
|
||||||
|
prefix = ''
|
||||||
|
for i in range(preflen):
|
||||||
|
prefix += secrets.choice(pfgrid[1:])
|
||||||
|
return prefix + ''.join(enc)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
enc = enc[preflen:].rstrip()
|
||||||
|
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
|
||||||
|
y1 -= 1
|
||||||
|
if y1 < 0:
|
||||||
|
y1 += pfsize
|
||||||
|
elif y1 == y2: # same row: get the coords to the left
|
||||||
|
x1 -= 1
|
||||||
|
if x1 < 0:
|
||||||
|
x1 += pfsize
|
||||||
|
msg.append(getgridchar(kg, x1, y1))
|
||||||
|
# finalize the decrypted message
|
||||||
|
return ''.join(msg).strip()
|
||||||
|
|
||||||
|
# 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()
|
||||||
Reference in New Issue
Block a user