# Decoding a string encoded by Caesar Cipher and some delimiters [closed]

I have the following code which I seek to optimize. The result of the bruteforce I already have but I'm just trying to learn some more python using the same example.

The code in the data needs to be split where the ++ are spaces and the || are just separators between letters. The // are carriage returns.

The code below spits out a readable message depending on the key but to learn more about splitting/replacements & python I want to try to get the output format better as well. On top of that I want to see if some language recognition or analysis on the output string would be handy in not having to scroll through all output manually to find potential candidates for the decoded message. So recognize a potential human readable and meaningful string.

Just to state again. I already have the decoded message. So I'm note here to score some easy points for a CTF somewhere. I just use the sample to learn python.

So to sum up I have two questions:

1.What is the best way to split the initial string into parts that preserve the format (given the || between letters and the ++ for the spaces between words)

2.What would be a good method to do an analysis on the generated strings. (counters or language detect?)

#!/usr/bin/env python

import re

result=""
plaintext=''

data = """|125||104||120||118||++||107||100||118||++||100||118||110||104||103||++||112||104||++||119||114||++||112||100||110||104|
|++||100||113||++||104||113||102||114||103||104||117||++||105||114||117||++||114||120||117||++||120||115||103||100||119|
|104||118||//||++||119||107||108||118||++||108||118||++||112||104||++||119||104||118||119||108||113||106||++||108||119|
|++||114||120||119||//||++||108||105||++||108||119||++||122||114||117||110||118||++||108||++||122||108||111||111||++|
|101||104||++||118||104||113||103||108||113||106||++||108||119||++||119||114||++||119||107||104||++||117||104||118||119|
|++||114||105||++||124||114||120||++||100||118||++||122||104||111||111||++||100||118||++||100||++||103||104||102||114|
|103||104||117||//||++||119||108||117||//|"""

newdata =re.sub("\D", "", data)

n = 3

splitted = [newdata[i:i+n] for i in range(0, len(newdata), n)]

integers = map(int, splitted)

print integers  # this is a list with integers

def bruteforce(key):

global plaintext
plaintext =''
for char in integers:

plaintext = plaintext + chr(char+key)

return

for key in range (-100,100):

bruteforce(key)
print plaintext


## closed as off-topic by 200_success, yuri, Stephen Rauch, vnp, Jamal♦Sep 16 '18 at 0:58

This question appears to be off-topic. The users who voted to close gave this specific reason:

• "Code not implemented or not working as intended: Code Review is a community where programmers peer-review your working code to address issues such as security, maintainability, performance, and scalability. We require that the code be working correctly, to the best of the author's knowledge, before proceeding with a review." – 200_success, yuri, Stephen Rauch, vnp, Jamal
If this question can be reworded to fit the rules in the help center, please edit the question.

I would redo this completely. First some things that should be addressed by the rewrite:

• Define functions for small, separate tasks that do simple things.
• Not mess up the global namespace too much (by having variables lying around, as well as calling code (which is better protected by a if __name__ == "__main__": guard).
• Obey Python's official style-guide, PEP8, especially by using ALL_CAPS for global constants and following the scheme of first defining all functions and afterwards calling them.
• Have nice docstrings explaining what each function does.
• Have meaningful names that explain what each variable/function represents

So, lets separate this into separate tasks. The first task is reading this weird data format into a more normal one, for example a list of words (which are strings):

def get_words(data):
words = filter(None, re.split(r'\|\+\+\||\|\/\/\|', data))
words_int = [map(int, re.findall(r'\|(\d*)\|', word)) for word in words]
return map(lambda word: "".join(map(chr, word)), words_int)


The next task is taking a list of words and shifting each character by some offset. This procedure is usually called a Caesar shift:

def caesar_shift(word, offset):
return "".join(map(lambda c: chr(ord(c) + offset), word))


Then we just need to loop over all possible offsets to get all possible clear texts:

words = get_words(data)

for offset in range(-100, 100):
words_ = [caesar_shift(word, offset) for word in words]
print " ".join(words_)


But we can be a bit more clever. First, note that it does not make sense to shift the lowest value lower than the lowest printable ASCII value (\t is number 9). The same is true for the maximum value (the largest printable value, ~ is 126)

Furthermore, a plaintext that contains unprintable characters is probably wrong. We can therefore exclude all of those:

from itertools import chain
import string

PRINTABLE = set(string.printable)
MIN_PRINTABLE = ord(min(PRINTABLE))
MAX_PRINTABLE = ord(max(PRINTABLE))

min_chr = min(chain(*words))
max_chr = max(chain(*words))

for offset in range(MIN_PRINTABLE - ord(min_chr),
MAX_PRINTABLE - ord(max_chr) + 1):  # printable ASCII range
words_ = [caesar_shift(word, offset) for word in words]
text = " ".join(words_)
if all(c in printable for c in text):
print text


And finally, we can be even more clever and take a dictionary and count how many words of the decoded clear text are in there. We then select the cleartext with the highest number as the chosen clear text. (Here I actually gave longer words a higher score if they are in the dictionary. The result is the same in this case, though.)

def get_dictionary(file_name):
with open(file_name) as f:
for line in f:
yield line.split("/")[0].strip()

# This works on most linux machines, might have to download it manually, though:
dictionary = set(get_dictionary("/usr/share/hunspell/en_US.dic"))

def decode(words):
min_chr = min(chain(*words))
max_chr = max(chain(*words))
for offset in range(MIN_PRINTABLE - ord(min_chr),
MAX_PRINTABLE - ord(max_chr) + 1):  # printable ASCII range
words_ = [caesar_shift(word, offset) for word in words]
text = " ".join(words_)
if all(c in printable for c in text):
score = 0
if dictionary is not None:
score = sum(len(word) for word in words_ if word in dictionary)
yield score, text


We then wrap it all together:

from itertools import chain
import string

PRINTABLE = set(string.printable)
MIN_PRINTABLE = ord(min(PRINTABLE))
MAX_PRINTABLE = ord(max(PRINTABLE))

def get_words(data):
"""Read the weird data format into a list of (encoded) words"""
words = filter(None, re.split(r'\|\+\+\||\|\/\/\|', data))
words_int = [map(int, re.findall(r'\|(\d*)\|', word)) for word in words]
return map(lambda word: "".join(map(chr, word)), words_int)

def caesar_shift(word, offset):
"""Shift each character in word by offset"""
return "".join(map(lambda c: chr(ord(c) + offset), word))

def get_dictionary(file_name):
"""Read a dictionary file in the hunspell format.
Yields each word.
"""
with open(file_name) as f:
for line in f:
yield line.split("/")[0].strip()

def decode(words, dictionary=None):
"""Decode the text consisting of words.
Considers only plaintexts where each character is printable
If dictionary is given it also calculates a score for each plain text.
The score is the sum of the length of all words included in the dictionary.

Yields tuples of (score, text) for each plain text. If no dictionary is given, all scores are zero.
"""
min_chr = min(chain(*words))
max_chr = max(chain(*words))
for offset in range(MIN_PRINTABLE - ord(min_chr),
MAX_PRINTABLE - ord(max_chr) + 1):  # printable ASCII range
words_ = [caesar_shift(word, offset) for word in words]
text = " ".join(words_)
if all(c in printable for c in text):
score = 0
if dictionary is not None:
score = sum(len(word) for word in words_ if word in dictionary)
yield score, text

if __name__ == "__main__":
data = """|125||104||120||118||++||107||100||118||++||100||118||110||104||103||++||112||104||++||119||114||++||112||100||110||104|
|++||100||113||++||104||113||102||114||103||104||117||++||105||114||117||++||114||120||117||++||120||115||103||100||119|
|104||118||//||++||119||107||108||118||++||108||118||++||112||104||++||119||104||118||119||108||113||106||++||108||119|
|++||114||120||119||//||++||108||105||++||108||119||++||122||114||117||110||118||++||108||++||122||108||111||111||++|
|101||104||++||118||104||113||103||108||113||106||++||108||119||++||119||114||++||119||107||104||++||117||104||118||119|
|++||114||105||++||124||114||120||++||100||118||++||122||104||111||111||++||100||118||++||100||++||103||104||102||114|
|103||104||117||//||++||119||108||117||//|"""
words = get_words(data)
dictionary = dictionary = set(get_dictionary("/usr/share/hunspell/en_US.dic"))
# Let's just hope the maximum score makes sense:
print max(decode(words, dictionary))[1]
# zeus has asked me to make an encoder for our updates this is me testing it out if it works i will be sending it to the rest of you as well as a decoder tir

• Please do not answer off-topic questions. – 200_success Sep 15 '18 at 15:27
• @200_success: I don't completely agree that it is off-topic. The second question, probably yes, but an edit of the question (and not the code) would make it completely on-topic, IMO. – Graipher Sep 15 '18 at 15:38