10
\$\begingroup\$

The program starts of with the password() function first, which gets the keyword or key that you want to shift your string with. Then you go into the choice() option, where you can either encrypt() or decrypt() your string.

The script converts each character of the key and string to a corresponding number, and then shifts the string based on the value of each character in the key. The values are listed in a separate python file, which I have appended below. Is It also logs all encryptions and decryptions along with their password into a log.txt file.

My question is, are there any flaws with my calculations? Given a sample key: "jabbathehut" and a sample message: "This is a secret message", could the original string be brute-forced within a week if someone knew the output and the encryption process, but not the key/password used?

try:
   import pyperclip
except ImportError:
    pass
finally:
    import dll
    from dll import alphabets, numbers, date

def encrypt(numlist, string, code):
    encoded_num = []
    encoded_str = []

    #Converting string to encoded numbers and adding shift based on password
    n = 0 
    for i in string:

        if n > len(numlist) - 1:
            n = 0
        try:
            encode_num = alphabets[i] + numlist[n]
        except KeyError as detail:
            dict_error("encrypt", numlist, code, detail)

        while encode_num > 90:
            encode_num = encode_num - 90

        encoded_num.append(encode_num)
        n += 1

    #Convert encoded numbers to encoded words (in list)
    for i in encoded_num:
        encode_str = numbers[i] 
        encoded_str.append(encode_str)

    #Display encoded result to the user
    encoded_display = "".join(encoded_str)   
    print("\nYour string has been encoded and copied to the clipboard:\n{}"
          .format(encoded_display))
    input("")

    #Attempts to copy encoded result into the clipboard
    try:
        pyperclip.copy(encoded_display)
    except NameError:
        pass

    #Save information into log file
    f = open("log.txt", "a")

    #f.write() does not write on a new line!
    f.write(date)
    f.write("\nEncrypted: {}".format(encoded_display))
    f.write("\nPassword: {}\n\n".format(code))

    f.close()
    choice(numlist, code)


def decrypt(numlist, enc_string, code):
    decoded_num = []
    decoded_str = []

    #Converting string to decoded numbers and subtracting shift based on password
    n = 0 
    for i in enc_string:

        if n > len(numlist) - 1:
            n = 0
        try:
            decode_num = alphabets[i] - numlist[n]
        except KeyError as detail:
            dict_error("decrypt", numlist, code, detail)

        while decode_num < 1:
            decode_num = decode_num + 90

        decoded_num.append(decode_num)
        n += 1

    #Convert decoded numbers to decoded words (in list)
    for i in decoded_num:
        decode_str = numbers[i] 
        decoded_str.append(decode_str)

    decoded_display = "".join(decoded_str)   
    print("\nYour string has been decoded:\n{}".format(decoded_display))
    input("")

    f = open("log.txt", "a")

    f.write(date)
    f.write("\nDecrypted: {}".format(decoded_display))
    f.write("\nPassword: {}\n\n".format(code))

    f.close()
    choice(numlist, code)

def choice(numlist, code):
    dll.clear()
    print("\n\tPassword: {}".format(code))
    option = input("\nWhat would you like to do?[1]Encrypt [2]Decrypt [3]Reset pass" 
                   " [4]Color [5]Exit: ")

    if option == "1":
        string = input("\nEnter string here: ")
        encrypt(numlist, string, code)  

    elif option == "2":
        enc_string = input("\nEnter encrypted string here: ")
        decrypt(numlist, enc_string, code)  

    elif option == "3":
        password()

    elif option == "4":
        print("")
        dll.choose_colour()
        choice(numlist, code)

    elif option == "5":
        pass

    else:
        choice(numlist, code)

def password():
    #It's confusing but code is the password, and numlist is the password in number format in a list
    dll.clear()
    numlist = []
    code = input("\nSet the password: ")
    codelist = list(code)

    for letter in codelist:
        try:
            numlist.append(alphabets[letter])
        except KeyError as detail:
            dict_error("password", numlist, code, detail)

    if code == "" :
        print("\n\tError: Your password cannot be empty!")
        input("")
        password()

    choice(numlist, code)

def dict_error(caller, numlist, code, detail):
    if caller == "password":
        print("\n\tError: the key {} in your entry cannot be used as a password".format(detail))
        input("")
        password()

    elif caller == "encrypt" or "decrypt":
        print("\n\tError: the key {} in your entry does not support encryption.".format(detail))
        input("")
        choice(numlist, code)

password()       

dll.py contains these:

import datetime

date = datetime.datetime.now().strftime("%d %B %Y - %I:%M%p")

alphabets = {
    "a" : 1, "A" : 27, " " : 53, "0" : 79,
    "b" : 2, "B" : 28, "." : 54, "1" : 80,
    "c" : 3, "C" : 29, "?" : 55, "2" : 81,
    "d" : 4, "D" : 30, "!" : 56, "3" : 82,
    "e" : 5, "E" : 31, '"' : 57, "4" : 83,
    "f" : 6, "F" : 32, "'" : 58, "5" : 84,
    "g" : 7, "G" : 33, ":" : 59, "6" : 85,
    "h" : 8, "H" : 34, "-" : 60, "7" : 86,
    "i" : 9, "I" : 35, "/" : 61, "8" : 87,
    "j" : 10, "J" : 36, ">" : 62, "9" : 88,
    "k" : 11, "K" : 37, "<" : 63, "(" : 89,
    "l" : 12, "L" : 38, "=" : 64, ")" : 90,
    "m" : 13, "M" : 39, "," : 65,
    "n" : 14, "N" : 40, "@" : 66,
    "o" : 15, "O" : 41, "#" : 67,
    "p" : 16, "P" : 42, "$" : 68,
    "q" : 17, "Q" : 43, "%" : 69,
    "r" : 18, "R" : 44, "^" : 70,
    "s" : 19, "S" : 45, "&" : 71,
    "t" : 20, "T" : 46, "*" : 72,
    "u" : 21, "U" : 47, "[" : 73,
    "v" : 22, "V" : 48, "]" : 74,
    "w" : 23, "W" : 49, "_" : 75,
    "x" : 24, "X" : 50, "~" : 76,
    "y" : 25, "Y" : 51, "\\" : 77,
    "z" : 26, "Z" : 52, "|" : 78
    }

numbers = {
    1 : "a", 27 : "A", 53 : " ", 79 : "0",
    2 : "b", 28 : "B", 54 : ".", 80 : "1",
    3 : "c", 29 : "C", 55 : "?", 81 : "2",
    4 : "d", 30 : "D", 56 : "!", 82 : "3",
    5 : "e", 31 : "E", 57 : '"', 83 : "4",
    6 : "f", 32 : "F", 58 : "'", 84 : "5",
    7 : "g", 33 : "G", 59 : ":", 85 : "6",
    8 : "h", 34 : "H", 60 : "-", 86 : "7",
    9 : "i", 35 : "I", 61 : "/", 87 : "8",
    10 : "j", 36 : "J", 62 : ">", 88 : "9",
    11 : "k", 37 : "K", 63 : "<", 89 : "(",
    12 : "l", 38 : "L", 64 : "=", 90 : ")",
    13 : "m", 39 : "M", 65 : ",",
    14 : "n", 40 : "N", 66 : "@",
    15 : "o", 41 : "O", 67 : "#",
    16 : "p", 42 : "P", 68 : "$",
    17 : "q", 43 : "Q", 69 : "%",
    18 : "r", 44 : "R", 70 : "^",
    19 : "s", 45 : "S", 71 : "&",
    20 : "t", 46 : "T", 72 : "*",
    21 : "u", 47 : "U", 73 : "[",
    22 : "v", 48 : "V", 74 : "]",
    23 : "w", 49 : "W", 75 : "_",
    24 : "x", 50 : "X", 76 : "~",
    25 : "y", 51 : "Y", 77 : "\\",
    26 : "z", 52 : "Z", 78 : "|"
   }
\$\endgroup\$
  • 20
    \$\begingroup\$ The Vigenère Cipher is a toy-grade cipher, totally obsolete since World War II. It can never be considered secure. \$\endgroup\$ – 200_success Nov 1 '17 at 4:54
  • 2
    \$\begingroup\$ @200_success: Technically, it is secure as long as the code word is at least as long as the message (then it's basically a OTP). But yeah, usage otherwise is relatively easy to break with letter and letter combination frequencies once a certain number of samples with the same code word has been recorded. \$\endgroup\$ – hoffmale Nov 1 '17 at 5:15
  • \$\begingroup\$ @200_success I know about that, but since this incorporates symbols as well as numbers, wouldn't letter frequency analysis be impossible? Sorry if I'm being ignorant \$\endgroup\$ – AstralWolf Nov 1 '17 at 5:50
  • 8
    \$\begingroup\$ @AstralWolf: Letter frequency analysis is still very possible (you just have to account for some extra letters, especially spaces). \$\endgroup\$ – hoffmale Nov 1 '17 at 6:00
  • 4
    \$\begingroup\$ @AstralWolf Read up on the Kasiski test. This cryptanalysis trivially defeats the Vigenère cipher, regardless of the alphabet used. As an aside, Vigenère can be implemented in a handful of lines of code, in constant space, and doesn’t require hard-coding a dictionary. Just convert the characters to code points and xor them. \$\endgroup\$ – Konrad Rudolph Nov 1 '17 at 11:35
14
\$\begingroup\$

After Ludisposed has correctly pointed out in his answer that the Vigenere cypher is probably not very secure, here are some pointers on your actual code.


The mapping characters to numbers and back in your dll.py can be greatly simplified by using the built-in module string. It contains very helpful constants, like string.ascii_lowercase, string.ascii_characters and string.printable. The last one contains all ASCII characters which can be printed in a terminal (including the upper- and lower-case alphabets, numbers, punctuation and all kinds of whitespace).

This change will break backwards compatibility, because the order will be different than your manual numbering.

import string

numbers = dict(enumerate(string.printable,1))
alphabets = dict(zip(numbers.values(), numbers.keys()))

The second argument to enumerate tells it to start counting at 1, instead of 0, like in your code.


Your try..except clause in the imports does not need the finally clause. Since you pass on an ImportError, the code will continue executing after this. So you can just write:

try:
   import pyperclip
except ImportError:
    print("Could not import module 'pyperclip'. Copying to the clipboard will not be supported. You can install this module using `pip install pyperclip`.")

import dll
from dll import alphabets, numbers, date

I also added a helpful message telling the user what it means that pyperclip is not installed and how to fix that problem.

It is also very confusing that you call your module dll, from which you import some constants, but you also seem to be having another module called dll, which contains functions to make terminal interactions nicer (like dll.clear() and dll.choose_colour()).


Your main code will at some point run into a problem. Python has a maximum stack size of 1000 (by default, it can be increased, but not removed in normal Python). This happens because whenever you call the choice function at the end of one of your functions, this function does not end (until that function you called finishes). So every call to choice increases the stack size by one (and then by one again, when choicecalls the next function), and so on. Have a look at this SO post for why Python was designed like this (thanks to Ilmari Karonen for pointing me to this link in the comments).

You can avoid this, by writing a main function that handles calling the choice function and then the chosen function and loops infinitely (or, until the user quits).

For this to work, your other functions might need to return their results, instead. Your choice function, for example`, could return the function to execute (and maybe any additional arguments).

While you are at it, you should separate your concerns. One concern is en/decrypting a string with a given password. It is a totally different responsibility for the result of this to be printed, written to a file or copied to the clipboard. These things should got into separate functions or be called in the main function. Just return the en-/decrypted string.

You should protect this main call with a if __name__ == "__main__": guard.

When done, this could look something like this:

from datetime import datetime
import string

# Allow all printable characters, except for the last 5 (\t\n\r\x0b\x0c)
characters = string.printable[:-5]
indices = {c: n for n, c in enumerate(characters)}


def encrypt(text, password):
    # Re-calculate the shifts here, for the function to be more independent
    shifts = [indices[char] for char in password]
    # Converting string to encoded numbers and adding shift based on password
    encoded_num = [(indices[char] + shifts[n % len(shifts)])
                   for n, char in enumerate(text)]
    # Convert encoded numbers to encoded characters
    return "".join(characters[i % len(characters)] for i in encoded_num)


def decrypt(text, password):
    # to be implemented by you
    raise NotImplementedError

def ask_password():
    # to be implemented by you
    return "foo"


def save_to_clipboard(text):
    """Copy information to cipboard"""
    # ^ This is a docstring documenting this function
    try:
        pyperclip.copy(text)
        print("\nYour string has been copied to the clipboard:\n{text}")
    except NameError:
        pass


def save_to_log(file_name, mode, text, password):
    """Save information into log file"""
    with open(file_name, "a") as f:
        # f.write() does not write on a new line!
        # Custom format strings are supported by f-strings:
        f.write(f"{datetime.now():%d %B %Y - %I:%M%p}")
        f.write(f"\n{mode}: {text}")
        f.write(f"\nPassword: {password}\n\n")


def main():
    password = None
    while True:
        # This will loop infinitely, or until the user chooses `5`, where the loop is broken or the user presses CTRL-C
        if password is None:
            # password not yet set or reset
            password = ask_password()
        print(f"\n\tPassword: {password}")
        option = input("\nWhat would you like to do?[1]Encrypt [2]Decrypt [3]Reset pass"
                       " [4]Color [5]Exit: ")
        if option == "1":
            string = input("\nEnter string to encrypt here: ")
            encrypted = encrypt(string, password)
            save_to_clipboard(encrypted)
            save_to_log("log.txt", "Encrypted", encrypted, password)
        elif option == "2":
            string = input("\nEnter string to encrypt here: ")
            decrypted = decrypt(string, password)
            save_to_clipboard(decrypted)
            save_to_log("log.txt", "Decrypted", decrypted, password)
        elif option == "3":
            # this is all that is needed to reset the password
            # the user will be asked for a new password in the
            # next loop iteration
            password = None
        elif option == "4":
            # to be implemented by you
            raise NotImplementedError
        elif option == "5":
            print("Good-bye")
            break
        else:
            print("Not a valid input, please try again")


if __name__ == "__main__":
    main()

Miscellaneous comments:

  • print("") and print() do exactly the same thing
  • Use the with keyword to ensure a file is always properly closed:

with open("log.txt", "a") as f:
    f.write(date)
    f.write("\nDecrypted: {}".format(decoded_display))
    f.write("\nPassword: {}\n\n".format(code))
  • I don't like the dict_error function. It makes your code less explicit. I think just writing this code where it appears is way more readable:

try:
    decode_num = alphabets[i] - numlist[n]
except KeyError as detail:
    print("\n\tError: the key {} in your entry cannot be used as a password".format(detail))
    input("")
    password()
  • If you are using Python >= 3.6, you can use the new f-strings, which simplify the above format, by using the locally-defined variables as keys directly:

print(f"\n\tError: the key {detail} in your entry cannot be used as a password")
  • If you have to add a comment like #It's confusing but code is the password, and numlist is the password in number format in a list, then you should think of better names.
  • Try to avoid magic numbers. Instead of while encode_num > 90:, use while encode_num > len(numbers):. Even better yet, use modular division and directly set encode_num %= len(numbers).
\$\endgroup\$
  • \$\begingroup\$ @AstralWolf And so, by trying to keep it simpler, you made it more complicated :D Which is why we usually insist on posting as much of your actual code as possible (but we do have the added note that it is sufficient to post only all relevant pieces of code). \$\endgroup\$ – Graipher Nov 1 '17 at 11:36
  • \$\begingroup\$ @AstralWolf But if that is the case (that you leave out stuff), it is usually a good idea to mention what was left out in the question and that this part is not relevant here. People might still tell you you should include it, if they think it is relevant, though. \$\endgroup\$ – Graipher Nov 1 '17 at 11:38
  • \$\begingroup\$ Sorry I accidentally posted my incomplete comment. For the dict_error() function, I was trying to adhere to the DRY principle. Could you show me what you mean by the main() function, I would think even if I defined a main function, the code would still call encrypt() and choice(), wouldn' t that still be a problem? \$\endgroup\$ – AstralWolf Nov 1 '17 at 11:45
  • \$\begingroup\$ @AstralWolf I will add some code with a main function later. It is quite a lot to re-organize and I need to get lunch now. It will not be a problem if you don't use recursion, but iteration. You let each function actually finish, instead of calling the next function from within that function. \$\endgroup\$ – Graipher Nov 1 '17 at 11:46
  • \$\begingroup\$ thank you! you don't have to redo the whole thing, I think I'll still get the idea if you just show me an example, maybe with the encrypt() function. \$\endgroup\$ – AstralWolf Nov 1 '17 at 11:50
17
\$\begingroup\$

Security

My question is, are there any flaws with my calculations? Given a sample key: "jabbathehut" and a sample message: "This is a secret message", could the original string be brute-forced within a week if someone knew the output and the encryption process, but not the key/password used?

To sum up the key points:

The real security of Vigenère is difficult to quantify. A million character plaintext with a 10 character password is easy to break. But a 10 character plaintext with a 10 character randomly chosen password is essentially a one-time-pad and "theoretically" unbreakable.

mikeazo @ CryptoExchange

Why is Vignere not secure?

Note that this works for most unsecure crypto's see my sorta related xor bruteforce for an example of how to brute-force such a thing

  • An attacker, who knows (or can guess) as many consecutive characters of any plaintext message as there are in the key, can trivially recover the key and thus decrypt all messages. (In fact, the characters need not even be consecutive, they just need to cover the entire key, or at least most of it.)
  • For most natural messages, it's fairly easy to guess the key length, for example by looking at correlations between characters n positions apart.
  • An attacker who knows (or can guess) the key length can divide the ciphertext into blocks of this length and decrypt one block with the other as the key to obtain a linear combination of the two messages. This will often have enough structure to allow the original blocks to be (at least partially) reconstructed, which in turn allows recovery of the key. (Also, if there are multiple messages encrypted with the same key, or if the key itself is not completely random, other similar attacks may be possible even without guessing the key length.)

Ilmari Karonen @ CryptoExchange

Thus it depends on both the length of the password and the length of the text. The longer the text becomes, the easier it would be to crack, because as @hoffmale stated, letter frequency analysis would be fairly simple.

Conclusion

It would be possible to break the key within less then a week. Depending on some factors. If you want to be secure, don't use Vignere, but use a different one-time-pad method. But as mentioned by @Martin Bonner, the problem with OTP is the difficulty of communicating the key.

As a rule of thumb, don't roll your own crypto. Those are likely to never be more secure as the tested cryptography functions. There are many good python crypto libraries that will encrypt data. For instance, pycrypto or pycroptography.


FINAL NOTE

I came acros this great challenge, that shows how easy Vignere cypher can be broken, if for instance the key is used multiple times. The krypto wargames from overthewire provide some good insights on low-level cryptography.

\$\endgroup\$
  • 1
    \$\begingroup\$ The trouble with one-time-pad is the difficulty in securely communicating the key. My recommendation would be to just to use AES-128 as provided by some reputable crypto library. (AES-256 if you need to protect against a nation-state, or you want to be secure post-quantum cryptography). Also, don't forget to protect the ciphertext against alteration en-route - so use AES-GCM. \$\endgroup\$ – Martin Bonner supports Monica Nov 1 '17 at 12:28
  • 5
    \$\begingroup\$ Somehow, your answer seemed oddly familiar. Not that I really mind (after all, everything posted on SE is automatically CC-By-SA licensed anyway), but a backlink or some other form of attribution would've been nice (and technically required by the license). \$\endgroup\$ – Ilmari Karonen Nov 1 '17 at 12:47
  • \$\begingroup\$ @IlmariKaronen Agreed, should have given credit, were credit was due. Edited the post to include the backlink. The licensing part of SE is still a bit vague to me. Should read up on that. Thnx for the feedback \$\endgroup\$ – Ludisposed Nov 1 '17 at 13:09
  • 1
    \$\begingroup\$ @Peilonrayz Should be fixed now. Licencing is hard, I'm not lawyer. ^^ \$\endgroup\$ – Ludisposed Nov 1 '17 at 16:16
  • 1
    \$\begingroup\$ @Ludisposed I've edited the TASLes slightly, to include a link to each post and each user. :) I think they should be clear and good now \$\endgroup\$ – Peilonrayz Nov 1 '17 at 16:38
1
\$\begingroup\$

The question is answered several days ago, to add information, your password's length is 11. This means there are 3,670,344,486,987,776 possible keys combinations which a brute force attacker must test (because each letter of the password leads the number of possible keys, the attacker has to test, to be multiplied by 26). Testing 3,670,344,486,987,776 keys/passwords is a work of years for a modern (ordinary) computer. But this does not mean Vigenere cipher is advised to use in practice for serious business as pointed by @Ludisposed (and you may also check the short but good answer here)

\$\endgroup\$

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.