3
\$\begingroup\$

I recently wrote a code for a encoder and decoder that works off of a key and the only way to decode the message is with this program and the case-sensitive key. I have yet to incorporate punctuation but that will come soon. I am currently designing a GUI using the Tkinter module. I would love any feedback.

from random import seed, shuffle
#Encoder Function
def Encoder(user_input,SEED):
    user_input = user_input.lower()
    letter = ["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']
    Letter_code = {"a":0,"b":1,"c":2,"d":3,"e":4,"f":5,"g":6,"h":7,"i":8,"j":9,"k":10,'l':11,'m':12,'n':13,'o':14,'p':15,'q':16,'r':17,'s':18,'t':19,'u':20,'v':21,'w':22,'x':23,'y':24,'z':25}
    code = ["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',]
    n = []
    seed(SEED)
    shuffle(code)
    for letter in user_input:
        for let in letter:
            if letter != " ":
                if letter == let:
                    first = Letter_code[let]
                    n.append(code[first])
            else:
                n.append("~")
    return ''.join(n)

#Decoder Function
def Decoder(user_input,SEED):
    user_input = user_input.lower
    key_list = ["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']
    final = ["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']
    seed(SEED)
    shuffle(key_list)
    key_code = {}
    z = 0
    n = []
    for key in key_list:
        key_code[key] = z
        z += 1
    for let in user_input:
        if let != "~":
            for Ke in key_list:
                if let == Ke:
                    a = key_code[Ke]
                    n.append(final[a])
        else:
            n.append(" ")
return ''.join(n)

#Prompt 
encode_decode = raw_input("Would you like to encode or decode a message?(encode/decode)")
encode_decode = encode_decode.lower()
if encode_decode == "encode":
    message = raw_input("Message to encode (no puncuation):")
    SEED = raw_input("Key:")
    print Encoder(message,SEED)
elif encode_decode == "decode":
    message = raw_input("Message to decode:")
    SEED = raw_input("Key:")
    print Decoder(message,SEED)
else:
    print "!!!INVALID RESPONSE!!!"
\$\endgroup\$
2
\$\begingroup\$

Concept

Interesting idea to use the pseudo-random number generator seed as an encryption key. Note, however, that there's no guarantee that the PRNG implementation will be consistent from release to release of Python, or that it will act identically across platforms. The same goes for the shuffle() function.

As you probably know, you should never try to implement your own cryptography for any meaningful usage, and this substitution cipher is trivially crackable by letter frequency analysis. I'll treat this as nothing more than a fun programming exercise.

Correctness

In the decoder, the call to lower() is missing parentheses. The return statement is incorrectly indented.

In the encoder, the letter iteration dummy variable shadows the letter array defined earlier. In effect, the code becomes:

def Encoder(user_input,SEED):
    user_input = user_input.lower()
    letter = ["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']
    Letter_code = {"a":0,"b":1,"c":2,"d":3,"e":4,"f":5,"g":6,"h":7,"i":8,"j":9,"k":10,'l':11,'m':12,'n':13,'o':14,'p':15,'q':16,'r':17,'s':18,'t':19,'u':20,'v':21,'w':22,'x':23,'y':24,'z':25}
    code = ["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',]
    n = []
    seed(SEED)
    shuffle(code)
    for letter in user_input:    # Note: This letter variable shadows the previous letter
        let = letter             # Originally… for let in letter:
        if letter != " ":
            if True:             # Originally… if letter == let:
                first = Letter_code[let]
                n.append(code[first])
        else:
            n.append("~")
    return ''.join(n)

That happens to mostly work! The only thing that breaks was that you had intended to filter out unencodable characters, and now it will raise a KeyError if user_input contains anything other than letters or spaces.

If you dig deeper for a root cause of your bug, you can blame it on a proliferation of variables, causing confusion. See if you can eliminate some variables, and name the remaining ones better.

Efficiency

In the encoder, your if letter != " " should be put between the outer and inner for-loops. The decoder does it right.

Nested for-loops are a sign of poor efficiency. Lookups should be done using a dictionary. A dictionary would be more efficient (constant-time access rather than a scan through 26 list items), and it would look cleaner in the code. (If you were doing serious cryptography, you would need all operations to be constant-time, to thwart attacks based on analyzing timing.)

Style

  • Naming conventions:
    • Encoder(user_input, SEED)encode(user_input, seed) because it's a function, not a class. You want a verb, not a noun. SEED is not a constant.
    • Decoder(user_input, SEED)decode(user_input, seed) for the same reasons.
    • letterLETTER because it's a constant.
    • Letter_codeLETTER_CODE because it's a constant.
    • nencoded or ndecoded because n has the connotation of being a number.
  • Prefer list comprehensions to loops: If you ever want to transform one list into another list, use a list comprehension instead of a for-loop if it's at all possible. It's one assignment statement, and it forces you to express yourself using higher-level functions rather than step-by-step procedural code.
  • Work smarter, not harder: Save yourself some error-prone typing — let the computer generate your lists of letters and their corresponding codes.
  • Symmetry: The decoder is just the inverse of the encoder, so there's no reason why it should be any more complex.
  • If-else: Whenever you have an if-else construct, prefer to put the branch with the shorter body first to reduce mental workload. For example, write the decoder this way:

    if let == "~":
        n.append(" ")
    else:
        for Ke in key_list:
            if let == Ke:
                a = key_code[Ke]
                n.append(final[a])
    

Proposal

from collections import defaultdict
from random import seed, shuffle

def encode(plaintext, key):
    """
    Performs a toy-grade substitution cipher on plaintext using the key.
    Any characters other than letters and spaces in plaintext will be discarded.
    """
    LETTERS = [chr(ord('a') + i) for i in range(26)]
    seed(key)   # influences shuffle()
    code = LETTERS[:] ; shuffle(code)
    # letter_code maps LETTERS to a copy of LETTERS that is scrambled using the key;
    # unrecognized characters map to the empty string.
    letter_code = defaultdict(str, zip(LETTERS, code))
    letter_code[' '] = '~'
    return ''.join([letter_code[c] for c in plaintext.lower()])

def decode(ciphertext, key):
    """
    Performs the inverse of encode().
    Any characters other than letters and spaces in ciphertext will be discarded.
    """
    LETTERS = [chr(ord('a') + i) for i in range(26)]
    seed(key)   # influences shuffle()
    code = LETTERS[:] ; shuffle(code)
    # The inverse of letter_code in encode()
    code_letter = defaultdict(str, zip(code, LETTERS))
    code_letter['~'] = ' '
    return ''.join([code_letter[c] for c in ciphertext.lower()])
\$\endgroup\$
1
  • 1
    \$\begingroup\$ +1, but a couple of points: (i) Python has a built-in string.ascii_lowercase, so no need to define your own LETTERS; (ii) Python strings have a built-in translate method which would simplify this code. \$\endgroup\$ Nov 13 '13 at 12:12
1
\$\begingroup\$

Here's a simple way to implement this, using str.maketrans to build the encryption table, and str.translate to apply it:

from string import ascii_lowercase as letters
from random import seed, shuffle

def encode(plaintext, key):
    cipher = list(letters)
    seed(key)
    shuffle(cipher)
    return plaintext.translate(str.maketrans(letters, ''.join(cipher)))

def decode(ciphertext, key):
    cipher = list(letters)
    seed(key)
    shuffle(cipher)
    return ciphertext.translate(str.maketrans(''.join(cipher), letters))

You write:

the only way to decode the message is with this program and the case-sensitive key

Unfortunately, that's not the case. What you've implemented here is a monoalphabetic substitution cipher, and that's easy to break using frequency analysis and cribbing.

For example, a code-breaker might do this kind of thing:

from collections import Counter
from itertools import combinations
from math import log

def scorer(n, corpus):
    """Return a function that scores a plaintext based on the frequency of
    occurrence of n-grams compared to those found in a corpus.

    """
    ngrams = len(corpus) - n + 1
    freq = Counter(corpus[i:i + n] for i in range(ngrams))
    log_freq = {k: log(float(v) / ngrams) for k, v in freq.items()}
    min_log_freq = log(0.1 / ngrams)
    def score(plaintext):
        return sum(log_freq.get(plaintext[i:i + n], min_log_freq)
                   for i in range(len(plaintext) - n + 1))
    return score

def decrypt(ciphertext, score_fun):
    """Decrypt ciphertext by hill-climbing search, using score_fun
    to score the decryptions. Return the best decryption found.

    """
    best_score = float('-inf')
    best_cipher = list(letters)
    best_plaintext = ciphertext
    swaps = list(combinations(range(len(letters)), 2))
    while True:
        shuffle(swaps)
        for i, j in swaps:
            cipher = best_cipher[:]
            cipher[i], cipher[j] = cipher[j], cipher[i]
            plaintext = ciphertext.translate(str.maketrans(''.join(cipher), letters))
            score = score_fun(plaintext)
            if score > best_score:
                best_score = score
                best_cipher = cipher
                best_plaintext = plaintext
                break
        else:
            return best_plaintext

This easily breaks your encryption if there's enough ciphertext and the cryptanalyst knows (or can guess) a good model:

>>> ciphertext = encode(plaintext, 442389743299116)
>>> print(ciphertext)
vhlgd nvr mdclfflfc ia cdi jdsz ilsdt aq rliilfc mz kds rlrids
af ikd mvfu, vft aq kvjlfc faiklfc ia ta: afgd as inlgd rkd kvt
oddodt lfia ikd maau kds rlrids nvr sdvtlfc, mxi li kvt fa
olgixsdr as gafjdsrvilafr lf li, vft nkvi lr ikd xrd aq v maau,
ikaxcki vhlgd, nlikaxi olgixsdr as gafjdsrvilaf?
>>> corpus = open('War and Peace.txt').read().lower()
>>> score_fun = scorer(4, corpus)
>>> print(decrypt(ciphertext, score_fun))
alice was beginning to get very tired of sitting by her sister
on the bank, and of having nothing to do: once or twice she had
peeped into the book her sister was reading, but it had no
pictures or conversations in it, and what is the use of a book,
thought alice, without pictures or conversation?
\$\endgroup\$
4
  • \$\begingroup\$ You dropped the space ⟷ tilde substitution that was in the original code. \$\endgroup\$ Nov 13 '13 at 17:35
  • \$\begingroup\$ Yes, that's right. It didn't seem like a critical detail to me: once you've seen how to use maketrans then it's easy enough to add more substitutions. \$\endgroup\$ Nov 13 '13 at 17:38
  • \$\begingroup\$ Also, note that this version preserves unrecognized characters, whereas the original would discard them. \$\endgroup\$ Nov 13 '13 at 17:43
  • \$\begingroup\$ In Python 2, it's string.maketrans() and string.translate(). \$\endgroup\$ Nov 13 '13 at 17:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.