Custom Cipher Implementation

This is the cipher used in this puzzling post, if you would like to crack it before looking at the programs use to make it.

Several weeks ago, I created a custom cipher for a friend of mine to crack, and I also posted it on Puzzling. Here are the programs I used to create it - I threw them together one morning, and I didn't comment them. I decided to leave them as is - they are self-explanatory to some degree, and I would also like to have my rush-job instinct in variable naming and what not critiqued. I know it's not good per say, but since it was a personal project I didn't care. How the cipher works is explained the in answer to the puzzling question, if my code doesn't show you well enough.

I have an encoder and a decoder. They currently assume that plaintext.txt, code.txt, and decoded.txt are in the current directory. The decoder assumes that the code is formatted as the encoder would format it.

encoder.py

import random
import string

#chars = "abcdefghijklmnopqrstuvwxyz*.,?!()- \n"
chars = "(stephn)a*dowjzr.b\nm,uvklc ?qig!fyx-"

def base_10_to_base_6(n):
s = ""
while n:
s = str(n % 6) + s
n //= 6

if len(s) == 1:
s = "0" + s

if not s:
return "00"

return s

def base_10_to_base_26(n):
s = ""
while n:
s = string.ascii_lowercase[n % 26] + s
n //= 26

if len(s) == 1:
s = "a" + s

if not s:
return "aa"

return s

print(text)

result = ""

for letter in text:
letter = letter.lower()

if letter not in chars:
letter = "*"

coded_char = base_10_to_base_6(chars.index(letter.lower()))

print(letter + ": " + coded_char + " : " + chars[int(coded_char, 6)])

result += coded_char

len_diff = len(result) % 3

if len_diff != 0:
result += (3 - len_diff) * str(random.randint(0, 5))

print(str(len(result)) + " " + result)

sets_of_3 = []

index = 3

while index <= len(result):
sets_of_3.append(result[index - 3:index])
index += 3

print(sets_of_3)

sets_of_letters = []

for set_of_3 in sets_of_3:
sets_of_letters.append(base_10_to_base_26(int(set_of_3)))

print(sets_of_letters)

with open("code.txt", "w") as text_file:
print(" ".join(sets_of_letters), file=text_file, end="")


decoder.py

import random
import string

#chars = "abcdefghijklmnopqrstuvwxyz*.,?!()- \n"
chars = "(stephn)a*dowjzr.b\nm,uvklc ?qig!fyx-"

def base_10_to_base_6(n):
s = ""
while n:
s = str(n % 6) + s
n //= 6

if len(s) == 1:
s = "0" + s

if not s:
return "00"

return s

def base_10_to_base_26(n):
s = ""
while n:
s = string.ascii_lowercase[n % 26] + s
n //= 26

if len(s) == 1:
s = "a" + s

if not s:
return "aa"

return s

def base_26_to_base_10(n):
result = string.ascii_lowercase.index(n[0]) * 26 + string.ascii_lowercase.index(n[1])

result = str(result)

if len(result) == 2:
result = "0" + result
elif len(result) == 1:
result = "00" + result
elif not result:
result = "000"

return result

def base_6_to_base_10(n):
return int(n[0]) * 6 + int(n[1])

sets_of_chars = text.split(" ")

print(sets_of_chars)

triplets = []

for each in sets_of_chars:
triplets.append(base_26_to_base_10(each))

code = "".join(triplets)

print(code)

index = 2

output = ""

while index <= len(code):
output += chars[base_6_to_base_10(code[index - 2:index])]
index += 2

with open("decoded.txt", "w") as text_file:
print(output, file=text_file, end="")

• And your question is ...? – MarianD Aug 3 '17 at 21:53
• @MarianD what I did bad in my code, what I can do better, optimizations, etc. I haven't coded in Python too extensively. – Stephen S Aug 3 '17 at 22:07

You may replace the lines as

text = open("plaintext.txt").read()


with so called context manager approach (see PEP 343 -- The "with" Statement):

with open("plaintext.txt") as pt:


which has the advantage of automatically closing the open file, even if an exception is raised on the way.

sets_of_chars = text.split(" ")


is somehow different from

sets_of_chars = text.split()


in two ways - if there are more consecutive spaces, and if it is applied to the empty string:

'abc  def'.split(' ')        # returns ['abc', '', 'def']
'abc  def'.split()           # returns ['abc', 'def']

''.split(' ')                # returns ['']
''.split()                   # returns []


Consider which one is more appropriate for your code.

chars = "(stephn)a*dowjzr.b\nm,uvklc ?qig!fyx-"


It is a constant, so by PEP 8 -- Style Guide for Python Code its name should be in uppercase letters:

CHARS = "(stephn)a*dowjzr.b\nm,uvklc ?qig!fyx-"


Both functions for converting from base 10

def base_10_to_base_6(n):
s = ""
while n:
s = str(n % 6) + s
n //= 6

if len(s) == 1:
s = "0" + s

if not s:
return "00"

return s

def base_10_to_base_26(n):
s = ""
while n:
s = string.ascii_lowercase[n % 26] + s
n //= 26

if len(s) == 1:
s = "a" + s

if not s:
return "aa"

return s


have very similar structure - by the DRY principle (Don't Repeat Yourself) why not to substitute them with one common function, e. g. with

def base_10_to_other(n, base):
if base not in (6, 26):
raise ValueError("Illegal base - allowed values are only 6 and 26")

DIGIT_SYMBOLS = {6: "012345", 26: string.ascii_lowercase}
zero_symbol = DIGIT_SYMBOLS[base][0]

s = ""
while n:
s = DIGIT_SYMBOLS[base][n % 6] + s
n //= base

if len(s) == 1:
s = zero_symbol + s

if not s:
s = 2 * zero_symbol

return s


Moreover, they are the same in your both modules (encoder.py and decoder.py) - why don't use the 3rd module for all common functions and import from it?
(The same for the other pair of converting functions, of course.)

• That bit about the base conversion functions is very helpful - I knew I should fix it, but I couldn't think of how. – Stephen S Aug 3 '17 at 23:49
• From the cryptographic point of view (if it is not a toy only): 1) Never ever invent your own cipher 2) Use only cryptographic pseudo-random generators. – MarianD Aug 3 '17 at 23:58
• It's just a toy - it's got way too many problems (always has an even number of characters, doesn't use certain letters) to be useful. I'm surprised how long it lasted on Puzzling. – Stephen S Aug 4 '17 at 0:36
• So I congratulate you for it! – MarianD Aug 4 '17 at 1:15
• Always open file using with statement. It makes sure that the file is properly gets closed even when an exception occurs.

From docs:

It is good practice to use the with keyword when dealing with file objects. This has the advantage that the file is properly closed after its suite finishes, even if an exception is raised on the way. It is also much shorter than writing equivalent try-finally blocks

with open("plaintext.txt") as f:

• Instead of lower-casing each letter you can loop over lower-cased version of text itself.
for letter in text.lower():

• String concatenation using result += coded_char could result in performance issues. The fast way is to to create a list and then join them together similar to what you're doing with code = "".join(triplets).