Open in CoCalc

Serial Encryption

Peter Francis 6/7/2019
Contact: [email protected]

This notebook shows a method for encrypting and decrypting stings based on the Playfair cypher. Specifically, this cypher uses a password that is time-dependent, so the time of encryption must be near the time of decryption.

The following modules are imported for a Python implementation.

In [1]:
import numpy as np import time from collections import OrderedDict import random from datetime import date

original is a string consisting of the 81 characters allowed to be used in the serial to be encrypted.

In [2]:
original = 'xE$kjL*n!&FtMG:ZHwpQre^d1?qz0+)#|I4Rs,2VuvmOW.([email protected] lXJNy6T-/PYoa'

make_enc_grid() takes a string ticket and forms a string with an encoding of the current date and time, followed by the characters in ticket, followed by the characters in original. Then, only the first occurance of each character in original is added to an array (so the array is always exactly 81 characters). The array is reshaped to be 9-by-9 and is returned. This original (and time-dependent) arrangement of the 81 original characters will be used for the encryption and decryption.

In [3]:
def make_encr_grid(ticket = '', mode = 'dateTime'): password = str(ticket) + original if mode == 'dateTime': password = date.fromtimestamp(time.time()).strftime("%a%d%b%Y") + str(int(((time.time() % 86400) % 3600) * 100 // 60)) + password return np.array(list(OrderedDict.fromkeys(password))).reshape(9,9)

plyf_encrypt() uses the playfair algorythm to encrypt serial with the grid, make_encr_grid(ticket).

In [4]:
def plyf_encrypt(serial = ' ', ticket = ''): if len(serial) % 2 != 0: print('Serial must have even length!') return None grid = make_encr_grid(ticket) find = lambda char : np.array(np.where(grid == char)).reshape(1,-1)[0] mixed = '' for i in range(0,len(serial),2): loc1, loc2 = find(serial[i]), find(serial[i+1]) if loc1[1] == loc2[1]: # Same column newChar1 = grid[(loc1[0] + 1) % 9][loc1[1]] newChar2 = grid[(loc2[0] + 1) % 9][loc2[1]] if loc1[0] == loc2[0]: # Same row newChar1 = grid[loc1[0]][(loc1[1] + 1) % 9] newChar2 = grid[loc2[0]][(loc2[1] + 1) % 9] else: # Otherwise newChar1 = grid[loc1[0]][loc2[1]] newChar2 = grid[loc2[0]][loc1[1]] mixed += newChar1 mixed += newChar2 return mixed

Similarly, plyf_decrypt decrypts an encoded message using make_encr_grid(ticket).

In [5]:
def plyf_decrypt(mixed = ' ', ticket = ''): grid = make_encr_grid(ticket) find = lambda char : np.array(np.where(grid == char)).reshape(1,-1)[0] serial = '' for i in range(0,len(mixed),2): loc1, loc2 = find(mixed[i]), find(mixed[i+1]) if loc1[1] == loc2[1]: # Same column newChar1 = grid[(loc1[0] - 1) % 9][loc1[1]] newChar2 = grid[(loc2[0] - 1) % 9][loc2[1]] if loc1[0] == loc2[0]: # Same row newChar1 = grid[loc1[0]][(loc1[1] - 1) % 9] newChar2 = grid[loc2[0]][(loc2[1] - 1) % 9] else: # Otherwise newChar1 = grid[loc1[0]][loc2[1]] newChar2 = grid[loc2[0]][loc1[1]] serial += newChar1 serial += newChar2 return serial

Since the password for the encryption and decryption is time-dependent, it is important to make sure that on average, the encryption and decryption process does not get separated by the time-accuracy difference. That is, the encryption and decryption must be fast enough so the time does not change between encryption and decryption. The following script shows the percent of encryptions/decryptions that do not get disturbed by a change in time.

In [6]:
good = 0 n = 10000 for _ in range(n): serial = ''.join(random.choice(original) for _ in range(100)) if serial == plyf_decrypt(plyf_encrypt(serial)): good += 1 print(f'{good*100/n}%')
99.97%

The following script demosntrates how this method works.

In [7]:
serial = 'Serial String Test ABCD 12345 [email protected]#$' mixed = plyf_encrypt(serial) grid = make_encr_grid(ticket = 'sample Ticket') ## this step done in between the encryption and decryption makes it likely to display the actual grid that is being used decryption = plyf_decrypt(mixed) print(f'The non-encrypted serial is:\n\n {serial}') print(f'\nChoose a ticket, say \'sample Ticket\', so the encryption grid is \n\n{grid}') print(f'\nThe encrpted string is\n\n {mixed}') print(f'\nWe immediately decrypt the string:\n\n {decryption}') print('\nIf we attempt to wait and try to decrypt the same string:\n\n ...Waiting 15 seconds...') time.sleep(15) print(f'\nAttempted decryption after 15 seconds:\n\n {plyf_decrypt(mixed)}') print('\n ...Waiting 105 seconds...') time.sleep(105) print(f'\nAttempted decryption after 2 minutes:\n\n {plyf_decrypt(mixed)}')
The non-encrypted serial is: Serial String Test ABCD 12345 [email protected]#$ Choose a ticket, say 'sample Ticket', so the encryption grid is [['S' 'a' 't' '0' '8' 'J' 'u' 'n' '2'] ['1' '9' '3' 's' 'm' 'p' 'l' 'e' ' '] ['T' 'i' 'c' 'k' 'x' 'E' '$' 'j' 'L'] ['*' '!' '&' 'F' 'M' 'G' ':' 'Z' 'H'] ['w' 'Q' 'r' '^' 'd' '?' 'q' 'z' '+'] [')' '#' '|' 'I' '4' 'R' ',' 'V' 'v'] ['O' 'W' '.' '(' '_' 'C' '7' 'A' 'b'] ['U' '5' 'B' 'g' 'K' '@' 'h' 'f' 'D'] ['X' 'N' 'y' '6' '-' '/' 'P' 'Y' 'o']] The encrpted string is [email protected])2h5g7 lLSk)5 [email protected] We immediately decrypt the string: Serial String Test ABCD 12345 [email protected]#$ If we attempt to wait and try to decrypt the same string: ...Waiting 15 seconds... Attempted decryption after 15 seconds: arrial S0Qbnjc-rR0c3BCD LS54$c*h#$ ...Waiting 105 seconds... Attempted decryption after 2 minutes: SeQUSlB2trUuK3Te,t3ABK3 124RB3*h#$

The following script shows how the devolution of decryptions progresses over time.

In [8]:
serial = 'Testing abcd 1234 ' mixed = plyf_encrypt(serial) for i in range(100): print(i, '--', plyf_decrypt(mixed)) time.sleep(1)
0 -- Testing abcd 1234 1 -- Testing abcd 1234 2 -- Testing abcd 1234 3 -- -rR0b2g tAcd 9S/6 4 -- -rR0ing abf^l1234l 5 -- Testing abcd 1234 6 -- Testing abcd 1234 7 -- Testing abcd 1234 8 -- Testing abcd 1234 9 -- -rR0b2g tAcd 9S/6 10 -- -rR0ing abf^l1234l 11 -- Testing abcd 1234 12 -- Testing abcd 1234 13 -- Testing abcd 1234 14 -- Testing abcd 1234 15 -- -rR0b2g tAcd 9S/6 16 -- -rR0ing abf^l1234l 17 -- Testing abcd 1234 18 -- -rR0b2BltAc^l92D3l 19 -- -rR0b2BltAc^l92D3l 20 -- -rR0b2BltAc^l92D3l 21 -- /QI8hSBl07c^l4S/6l 22 -- /QI8b2BltAfeX92D3X 23 -- -rR0b2BltAc^l92D3l 24 -- Testing abcd 1234 25 -- -rR0b2BltAc^l92D3l 26 -- Testing abcd 1234 27 -- -rR0b2g tAcd 9S/6 28 -- Testing abcd 1234 29 -- Testing abcd 1234 30 -- -rR0bng tAf^l1234l 31 -- /QI8A2Bl07feX92D5X 32 -- -rR0bng tAf^l1234l 33 -- /QI8A2g 07f^l9S/5l 34 -- -rR0bng tAf^l1234l 35 -- -rR0bng tAf^l1234l 36 -- -rR0b2g tAcd 9S/6 37 -- /QI8hSBl07c^l4S/3l 38 -- -rR0b2g tAcd 9S/6 39 -- -rR0b2g tAcd 9S/6 40 -- -rR0b2g tAcd 9S/6 41 -- -rR0b2g tAcd 9S/6 42 -- -rR0ing abf^l1234l 43 -- /QI8b2BltAfeX92D7X 44 -- -rR0ing abf^l1234l 45 -- /QI8b2g tAf^l9S/7l 46 -- -rR0ing abf^l1234l 47 -- -rR0ing abf^l1234l 48 -- Testing abcd 1234 49 -- -rR0b2BltAc^l92D3l 50 -- Testing abcd 1234 51 -- -rR0b2g tAcd 9S/6 52 -- Testing abcd 1234 53 -- Testing abcd 1234 54 -- Testing abcd 1234 55 -- -rR0b2BltAc^l92D3l 56 -- Testing abcd 1234 57 -- -rR0b2g tAcd 9S/6 58 -- Testing abcd 1234 59 -- Testing abcd 1234 60 -- Testing abcd 1234 61 -- -rR0b2BltAc^l92D3l 62 -- Testing abcd 1234 63 -- -rR0b2g tAcd 9S/6 64 -- Testing abcd 1234 65 -- Testing abcd 1234 66 -- Testing abcd 1234 67 -- -rR0b2BltAc^l92D3l 68 -- -rR0bng tAf^l1234l 69 -- -rR0b2g tAcd 9S/6 70 -- Testing abcd 1234 71 -- Testing abcd 1234 72 -- Testing abcd 1234 73 -- -rR0b2BltAc^l92D3l 74 -- -rR0bng tAf^l1234l 75 -- -rR0b2g tAcd 9S/6 76 -- Testing abcd 1234 77 -- -rR0b2BltAc^l92D3l 78 -- -rR0b2BltAc^l92D3l 79 -- -rR0b2BltAc^l92D3l 80 -- /QI8A2Bl07feX92D3X 81 -- /QI8hSBl07c^l4S/6l 82 -- -rR0b2BltAc^l92D3l 83 -- Testing abcd 1234 84 -- Testing abcd 1234 85 -- -rR0b2BltAc^l92D3l 86 -- -rR0bng tAf^l1234l 87 -- -rR0b2g tAcd 9S/6 88 -- Testing abcd 1234 89 -- -rR0bng tAf^l1234l 90 -- -rR0bng tAf^l1234l 91 -- /QI8A2Bl07feX92D5X 92 -- -rR0bng tAf^l1234l 93 -- /QI8A2g 07f^l9S/5l 94 -- -rR0bng tAf^l1234l 95 -- -rR0b2g tAcd 9S/6 96 -- -rR0b2g tAcd 9S/6 97 -- /QI8hSBl07c^l4S/3l 98 -- /QI8A2g 07f^l9S/6l 99 -- /QI8b2g tAf^l9S/6l

As can be seen from the sparce occurance of the original serial being decrypted, it is still possible (although seemingly unlikely) that the decryption will yield the original serial. The longer the initial serial is, the less likely that this will happen. The script below counts when the delayed decryption matches the original but this time uses a randomly generated serial of length 500.

In [9]:
serial = ''.join(random.choice(original) for _ in range(500)) mixed = plyf_encrypt(serial) start = time.time() time.sleep(10) count = 0 for i in range(1000): if serial == plyf_decrypt(mixed): count += 1 time.sleep(.1) print(f'{count/100}% successful delayed decryptions.')
0.06% successful delayed decryptions.

If more security is wanted, time_dep_serial creates a serial number that expires with respect to time, as checked by check_time_dep_serial.

In [10]:
def time_dep_serial(expiretime, ticket = 'default'): return str(int(time.time()) + expiretime) + str(ticket)
In [11]:
def check_time_dep_serial(serial, ticket = 'default'): serial = str(serial) if serial[10:] != ticket: return False tag = serial[:10] if time.time() <= int(tag): return True else: return False
In [12]:
serial = time_dep_serial(5) print('serial: ' + serial) time.sleep(3) print(f'Check after 3 seconds: {check_time_dep_serial(serial)}') time.sleep(3) print(f'Check after 6 seconds: {check_time_dep_serial(serial)}')
serial: 1559961720default Check after 3 seconds: True Check after 6 seconds: False

Extra

Simple Swap

In [13]:
def make_encr_dict(grid = make_encr_grid('')): array = grid.reshape(81,1) dict = {} for i in range(81): dict[original[i]] = mixed[i] return dict def simple_encrypt(string = ' ', ticket = ''): secret = '' dict = make_encr_dict(make_encr_array(ticket)) for char in string: secret += dict[char] return secret def simple_decrypt(secret = ' ', ticket = ''): dict = make_encr_dict(make_encr_array(ticket)) inverseDict = {} for char in dict: inverseDict[dict[char]] = char string ='' for char in secret: string += inverseDict[char] return string