spec and Python implementation upload

This commit is contained in:
Luxferre
2025-02-23 17:15:25 +02:00
commit 5eb44b10d6
2 changed files with 363 additions and 0 deletions
+225
View File
@@ -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.
Executable
+138
View File
@@ -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()