From 5eb44b10d6262f35c6820a5c3230db1647b75809 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Sun, 23 Feb 2025 17:15:25 +0200 Subject: [PATCH] spec and Python implementation upload --- README.md | 225 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ ip36.py | 138 +++++++++++++++++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 README.md create mode 100755 ip36.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..10ea1d7 --- /dev/null +++ b/README.md @@ -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. diff --git a/ip36.py b/ip36.py new file mode 100755 index 0000000..56540a4 --- /dev/null +++ b/ip36.py @@ -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()