Search code examples
pythoncryptographycaesar-cipherrot13

ROT2 cipher resulting in different than expected characters when deciphered with Python


I was having fun solving the riddles from the Pythonchallenge website

when I stumbled upon a weird behaviour:

With this input: *g fmnc wms bgblr rpylqjyrc gr zw fylb. rfyrq ufyr amknsrcpq ypc dmp. bmgle gr gl zw fylb gq glcddgagclr ylb rfyr'q ufw rfgq rcvr gq qm jmle. sqgle qrpgle.kyicrpylq() gq pcamkkclbcb. lmu ynnjw ml rfc spj.

We should be able to get the following output: *"i hope you didnt translate it by hand. thats what computers are for. doing it in by hand is inefficient and that's why this text is so long. using string.maketrans() is recommended. now apply on the url." *

Instead what we get when we decipher it with a simple ROT2 script is: i hope you didnt tr{nsl{te it |y h{nd0 th{ts wh{t computers {re for0 doing it in |y h{nd is inefficient {nd th{t)s why this text is so long0 using string0m{ketr{ns+ is recommended0 now {pply on the url0*

My ROT2 script I refer to is as follows:

user_input = input().split(' ')
newletter_int = 0
new_output = []

for word in user_input:
    newletter_int = 0 
    newstr = ''

    for letter in word:
        newletter = ord(letter) + 2
        newstr += chr(newletter)
    new_output.append(newstr)
print(" ".join(new_output))

This naturally happens of course, because the letter "y" has the order number of 121 and when we add 2 to 121 we get the character with order number 123 which is "{". But why would then the Python maketrans result in the correct character?

Please, note that I have solved the task with maketrans and what I am looking for is not a solution to the riddle, as I have been able to find it out myself. I am looking for a simple explanation what is the difference between the two methods. Also, please, do not refer to the pages where the solution is linked, as I am not looking for them, but for explanation of the difference of the functionalities between my script above and the string.maketrans() method and also an answer to the question why is this the recommended way to solve the riddle.


Solution

  • I'm assuming that while using str.maketrans you specifically created a mapping table between alphabets. That's exactly the point: you specify a 1-to-1 map between characters and so you're guaranteed that those transformations will happen as you've specified them.

    Let's now take a look at your script:

    1. You're using ord, which returns an integer representing the Unicode code point of the character you pass it. This inherently means that we should have some basic knowledge about character encodings. For this problem you can ignore Unicode code points as we're dealing with characters that can be encoded using ASCII (the smart people that designed Unicode made sure that the first 256 codepoints are the same). In order to understand what's going on an ASCII table is going to be your best friend:

    1. Let's now look at the most important line of your ROT2 implementation: newletter = ord(letter) + 2. Taking a look at the table above, it should be clear why y is transformed into { or why . results in 0. Because of this, we need to be a little smarter about our implementation; specifically, we need to take a close look at the scenarios where we cross that bound. A common way to circunvent this is to use something like (ord(letter) - 97 + 2) % 26 + 97. I'll let you figure out why that works on your own.

    2. I see you're using str.split so you avoid transforming spaces. Unfortunately that's not going to be enough as the string you're transforming contains other non-letters like ' or .. I suggest you take a look at the constants provided by the string module.

    As to why that might be the recommended way to solve the riddle, I'm guessing it's precisely because of all the effort involved with manually transforming characters using addition. As I've tried to illustrate with my answer, it's much simpler to specify a 1-to-1 character mapping and apply it directly to strings.