3

I have written a simple caesar cipher code to take a string and a positional shift argument i.e cipher to encrypt the string. However, I have realized some of the outputs won't decrypt correctly. For example:

python .\caesar_cipher.py 'fortuna' 6771 --encrypt outputs ☼↑↔▲↨
python .\caesar_cipher.py '☼↑↔▲↨' 6771 --decrypt outputs \`,/UC ( \ should be ` forgive my markdown skills)

I'm fairly certain there is some issue of encoding but I couldn't pinpoint it. Instead of printing and passing it as a command-line argument between two runs, if I were to just encrypt and decrypt in the same run output seems correct.
I'm using windows and I tried to run the above example (and a couple of others) both in cmd and PowerShell to test it.

Here is my code:

import argparse

# 127 number of chars in ascii
NO_OF_CHARS = 127


def encrypt(s: str) -> str:
    return ''.join([chr((ord(c)+cipher) % NO_OF_CHARS) for c in s])


def decrypt(s: str) -> str:
    return ''.join([chr((ord(c)-cipher) % NO_OF_CHARS) for c in s])


parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--encrypt", help="encrypt the string", action="store_true")
group.add_argument("--decrypt", help="decrypt the string", action="store_true")
parser.add_argument("string", type=str, help="string to encrypt/decrypt")
parser.add_argument("cipher", type=int,
                    help="positional shift amount for caesar cipher")
args = parser.parse_args()

string = args.string

encrypt_arg = args.encrypt
decrypt_arg = args.decrypt
cipher = args.cipher

if encrypt_arg:
    result = encrypt(string)

else:
    result = decrypt(string)

print(result)

Remko
  • 7,214
  • 2
  • 32
  • 52
Berkay
  • 53
  • 9
  • Add `print(' '.join([str(ord(c)) for c in result]))` along with `print(result)`. Then you see that `result` contains _unprintable_ characters under 32 (0x20). – JosefZ May 13 '21 at 13:06
  • why are you tagging this with `powershell` and `cmd` it has nothing to do with it? – Remko May 13 '21 at 13:32
  • I thought maybe there was something related to PowerShell's or cmd's char encodings. Sorry for that. – Berkay May 13 '21 at 14:09
  • @JosefZ yes, you are right thank you. So if I were to continue using terminal input I would have to restrict my input to alphabet chars, right? – Berkay May 13 '21 at 14:20

3 Answers3

1

I think the problem is after the encryption in copy and pasting the value. When I tested this code, what I found and you mentioned that too, directly transferring the encrypted value to the decrypt function by storing in a variable, doesn't cause any problem, but when directly pasting it is causing problem.

To overcome this problem, you write the encrypted text by encoding it in binary to file and then reading from that file.

File name has to be passed to the the CLI and CIPHER, it will give you the correct output.

This would work:

import argparse
# 127 number of chars in ascii
NO_OF_CHARS = 127


def encrypt(s: str) -> str:
    return ''.join([chr((ord(c)+cipher) % NO_OF_CHARS) for c in s])


def decrypt(s: str) -> str:
    return ''.join([chr((ord(c)-cipher) % NO_OF_CHARS) for c in s])


parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--encrypt", help="encrypt the string", action="store_true")
group.add_argument("--decrypt", help="decrypt the string", action="store_true")
parser.add_argument("string", type=str, help="string to encrypt/decrypt")
parser.add_argument("cipher", type=int,
                    help="positional shift amount for caesar cipher")
args = parser.parse_args()

string = args.string

encrypt_arg = args.encrypt
decrypt_arg = args.decrypt
cipher = args.cipher

if encrypt_arg:
    result = encrypt(string)
    with open('encrypt','wb') as f:
        a = result.encode(encoding='utf-8')
        f.write(a)
        f.close()
        print("Encrypted File created with name encrypt")

else:
    with open(string,'rb') as r:
        text = r.readlines()
        print(decrypt(text[0].decode('utf-8')))

To test:

$ python caesar_cipher.py 'fortuna' 6771 --encrypt

Encrypted File created with name encrypt

$ python caesar_cipher.py 'encrypt' 6771 --decrypt

fortuna
KnowledgeGainer
  • 1,017
  • 1
  • 5
  • 14
0

As @KnowledgeGainer mentioned, there is no problem with your code. The issue arises because you copied the output of your encryption from the terminal, and used that as your input for decryption. The terminal you're using is trying its best to interpret some potential non-printable control characters - fortuna has seven characters, but ☼↑↔▲↨ appears to be only five - but these are obviously unicode characters. In a caesar-cipher, your plaintext and encrypted message should be the same length, so it's clear that the terminal is mapping one or more "output bytes" to unicode characters. In what way it's doing this is not immediately obvious to me.

Paul M.
  • 10,481
  • 2
  • 9
  • 15
  • Yeah, I see the problem now. I thought although these chars look meaningless their Unicode values would map back to the same value it appears that's not the case as there are chars that won't even print. I should have paid more attention to the number of chars. Thank you for the answer. So I also want to ask if I wanted to keep working with command-line arguments do I need to keep it ASCII exclusive? or just alphabet (i.e a-z A-Z) exclusive? – Berkay May 13 '21 at 14:17
  • I would suggest you do something similar to @JosefZ's answer. If you can ensure that your encrypted output will never contain unprintable characters, and your input only contains printable ASCII characters, then your code should work as expected. – Paul M. May 13 '21 at 14:24
  • I just saw the answer, will do so. Thank you. – Berkay May 13 '21 at 14:30
0

Add print(' '.join([str(ord(c)) for c in result])) along with print(result). Then you see that result contains unprintable characters under 32 (0x20): 15,24,27,29,30,23,10.

Here's a possible technique how to stay in printable range of ASCII:

NO_OF_NOPRNT =  32                 # number of unprintable chars in ascii
NO_OF_CHARS  = 128 - NO_OF_NOPRNT  # number of   printable chars in ascii

def encrypt(s: str) -> str:
    return ''.join([chr(((ord(c)-NO_OF_NOPRNT+cipher) % NO_OF_CHARS)+NO_OF_NOPRNT) for c in s])


def decrypt(s: str) -> str:
    return ''.join([chr(((ord(c)-NO_OF_NOPRNT-cipher) % NO_OF_CHARS)+NO_OF_NOPRNT) for c in s])

With above improvement of your script:

.\SO\67519212.py  "fortunaX" 6771 --encrypt
9BEGHA4+
.\SO\67519212.py  "9BEGHA4+" 6771 --decrypt
fortunaX
JosefZ
  • 28,460
  • 5
  • 44
  • 83