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

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

#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 &lt;= 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

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

#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 &lt;= 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="")
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:
    text = pt.read()


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.)