1

I'm looking to encrypt a string (using a password or key) in Python, without installing any modules. I can work with the default modules included when you download Python (2.7), but I can't install anything like PyCrypto.

I've tried looking at Simple way to encode a string according to a password?, but the Base64 answer doesn't seem safe, and the others require an external module. Can anyone offer any help? I don't have any code to show because I haven't found anything to try.

Hugh
  • 47
  • 2
  • 6
  • It's generally not a good idea to "roll-your-own" crypto. Is this for a class or a learning experience? Are you prohibited from using external modules because of space constraints or are you just trying to learn fundamentals? – Ray Toal Jan 27 '18 at 21:21
  • I'm trying to write a program for some friends of mine to use in school (it's nothing dodgy), and the school doesn't allow external modules to be used unless I specifically ask, and if I do that, it takes them around 2 months – Hugh Jan 27 '18 at 21:23
  • 1
    If it is just a school project and not dodgy, and "no external modules can be used" then you are not in a very good position to expect secure encryption, unless you implement well-known algorithms by hand. The Vigenere cipher mentioned on the question you linked to is not horrible for short messages and long keys. You can also do a web search for things like "Python AES Cipher by Hand" as programmers have uploaded their own versions, but would you trust them? If your messages are short and not very valuable, the Vigenere cipher is fine. Otherwise leave the school and use PyCrypto. :) – Ray Toal Jan 27 '18 at 21:30
  • 1
    Depends on what do you understand by encryption. There are encryption methods invented thousands years ago, but now it's child play breaking them. One such method is `xor`ing each char in the string with the corresponding char in the password (the drawback is that the same algorithm applied to the encrypted text restores it). – CristiFati Jan 27 '18 at 21:33
  • zipfile won't work as it seems to be creating a Zip File (shocker), and I'm looking for something that isn't easily breakable, can anyone provide any useful links to an "AES Cipher By Hand"? – Hugh Jan 27 '18 at 21:38
  • It's messy, I'd prefer to do it all in the python script – Hugh Jan 27 '18 at 21:51
  • @StefanPochmann `zipfile` doesn't support creating password encrypted files, only reading them. – Artyer Jan 27 '18 at 21:58
  • Note regarding `ZipFile`: you **are** doing it all in the *python* script. It's just that the encrypted content would be stored on the non volatile memory (e.g. *HDD*) vs volatile memory (*RAM*). Also it's possible to store it on file like objects (e.g. *sys.stdout*, or I assume you could use some *IO* object). @Artyer: https://docs.python.org/2/library/zipfile.html#zipfile.ZipFile.setpassword. Back to the question: if *AES* (or any similar method in terms of strength) is required, take a look at the implementation in some module that exports it (end eventually copy/paste from there). – CristiFati Jan 27 '18 at 22:06
  • How long are the strings you want to encrypt? – Stefan Pochmann Jan 27 '18 at 22:09
  • +Stefan Pochmann They could be up to 10,000 characters – Hugh Jan 27 '18 at 22:16
  • You don't need to "install" modules to use them in python. – President James K. Polk Jan 27 '18 at 22:29
  • What *OS* is the script supposed to run on? – CristiFati Jan 27 '18 at 23:40
  • Windows 7 +Cristifati – Hugh Jan 28 '18 at 13:16

1 Answers1

2

Considering the fact that the purpose is a school project and not production (where theoretically it should hold against countless attacks), I'm going to post a solution.

code.py:

import sys
from itertools import cycle, izip
# from zipfile import ZipFile
from random import randint, seed
"""
try:
    from StringIO import cStringIO as StringIO
except ImportError:
    from StringIO import StringIO
"""

#DUMMY_ARCHIVED_FILE_NAME = "dummy_archived_file_name"


def encrypt_dummy0(text, password):
    if len(text) > len(password):
        pwd_iterable = cycle(password)
    else:
        pwd_iterable = password
    ret = [chr(ord(i) ^ ord(j)) for i, j in izip(text, pwd_iterable)]
    return "".join(ret)

decrypt_dummy0 = encrypt_dummy0


def encrypt_dummy1(text, password):
    addition_char = randint(0, 0x100)
    if len(text) > len(password):
        pwd_iterable = cycle(password)
    else:
        pwd_iterable = password
    ret = [chr(((ord(i) ^ ord(j)) + addition_char) % 0x100) for i, j in izip(text, pwd_iterable)]
    return "".join(reversed(ret)) + chr(addition_char)


def decrypt_dummy1(encrypted_text, password):
    addition_char = ord(encrypted_text[-1])
    if len(encrypted_text) > len(password):
        pwd_iterable = cycle(password)
    else:
        pwd_iterable = password
    ret = [chr((((ord(i) - addition_char) + 0x100) % 0x100) ^ ord(j)) for i, j in izip(reversed(encrypted_text[:-1]), pwd_iterable)]
    return "".join(ret)


"""
def encrypt_zipfile(text, password):
    buf = StringIO()
    with ZipFile(buf, "w") as zip_file:
        zip_file.setpassword(password)
        zip_file.writestr(DUMMY_ARCHIVED_FILE_NAME, text)
    buf.seek(0)
    return buf.read()


def decrypt_zipfile(encrypted_text, password):
    buf = StringIO(encrypted_text)
    with ZipFile(buf) as zip_file:
        return zip_file.read(DUMMY_ARCHIVED_FILE_NAME, password)
"""


def test(text, password, encrypt_func, decrypt_func):
    print("\nText: ({:d}) [{:s}] will be encrypted ({:s}) with password: ({:d}) [{:s}]".format(len(text), repr(text), encrypt_func.__name__, len(password), password))
    encrypted = encrypt_func(text, password)
    print("Encrypted text: ({:d}) [{:s}]".format(len(encrypted), repr(encrypted)))
    decrypted = decrypt_func(encrypted, password)
    print("Decrypted ({:s}) using password: ({:d}) [{:s}]: ({:d}) [{:s}]\n  Same as original: {}".format(decrypt_func.__name__, len(password), password, len(decrypted), repr(decrypted), decrypted == text))
    password_wrong = password[::-1]
    decrypted_wrong = decrypt_func(encrypted, password_wrong)
    print("Decrypted ({:s}) using password [{:s}]: [{:s}]\n  Same as original: {}".format(decrypt_func.__name__, password_wrong, repr(decrypted_wrong), decrypted_wrong == text))


def main():
    print("{:s} on {:s}".format(sys.version, sys.platform))
    seed()
    text = "The quick brown fox jumps over the lazy dog! :d \t+ digits [bonus!!!]: 0123456789\t + others: \xff\x23\xa9\n"
    while not text:
        text = input("Enter (non empty) text to encrypt: ")
    password = "#2minutes2midnite"
    while not password:
        password = input("Enter (non empty) password to encrypt with: ")

    func_pairs = [
        (encrypt_dummy0, decrypt_dummy0),
        (encrypt_dummy1, decrypt_dummy1),
        #(encrypt_zipfile, decrypt_zipfile),
    ]

    for func_pair in func_pairs:
        test(text, password, *func_pair)


if __name__ == "__main__":
    main()

Notes:

  • Using custom implemented "algorithms":

    • encrypt_dummy0 / decrypt_dummy0(encrypt_dummy0):
    • encrypt_dummy1 / decrypt_dummy1
      1. Encapsulates previous algo
      2. Adds an additional random number (constant over the encryption) to each char (some wrap-around specific operations are done (+, %) to ensure that the resulting char is in [0..255] range)
      3. Reverses the result
      4. Stores the random number at the end of the string (to be recovered and used for decryption)
      5. As expected, decryption does the exact same things in reversed order
    • I also kept @StefanPochmann ZipFile suggestion (but it's commented out), because at runtime, attempting to read from file (with a wrong password) yielded the same (good) result

      • This is a consequence of me failing to carefully RTFM (as pointed out by @Artyer or) according to [Python]: ZipFile.setpassword(pwd)

        ZipFile.setpassword(pwd)
        Set pwd as default password to extract encrypted files.

        New in version 2.6.

  • test - it's just a wrapper that:

    1. Encrypts text using password: calls the encryption func (3rd arg) over the 1st and 2nd args
    2. Attempts to decrypt (previously) encrypted text using the same password: calls the decryption func (4th arg) over the result from previous step and 2nd arg
    3. Attempts to decrypt (previously) encrypted text using the reversed password: calls the decryption func (4th arg) over the result from previous step and 2nd arg reversed
  • The procedure can be repeated indefinitely (at at least limited by computing power) but in the end it's all a matryoska doll (матрёшка, IPA) - which can recursively be opened.

Output:

E:\Work\Dev\StackOverflow\q048480667>"c:\Install\x64\Python\Python\2.7\python.exe" code.py
2.7.13 (v2.7.13:a06454b1afa1, Dec 17 2016, 20:53:40) [MSC v.1500 64 bit (AMD64)] on win32

Text: (96) ['The quick brown fox jumps over the lazy dog! :d \t+ digits [bonus!!!]: 0123456789\t + others: \xff#\xa9\n'] will be encrypted (encrypt_dummy0) with password: (17) [#2minutes2midnite]
Encrypted text: (96) ['wZ\x08I\x1f\x00\x1d\x06\x18\x12\x0f\x1b\x0b\x19\x07T\x03LJM\x03\x1b\x18\x04\x16S]\x1b\x0c\x16N\x1d\x1c\x00\x03^\x0c\x13\x17U\x10\n\x14\x13MS\x00N`_EG[\n\x00\x1a\x06T>\x11]\x03\x1c\x17OHU8\x19\x12]X\\F@PE\x05UPmNBT\nWZ\x08\x1b\x1dOT\x9aP\x9bg']
Decrypted (encrypt_dummy0) using password: (17) [#2minutes2midnite]: (96) ['The quick brown fox jumps over the lazy dog! :d \t+ digits [bonus!!!]: 0123456789\t + others: \xff#\xa9\n']
  Same as original: True
Decrypted (encrypt_dummy0) using password [etindim2setunim2#]: ['\x12.a\'{ip4kw{nepjf )>$m\x7fqi$ 8oyx\'p.#f*e}s<}8gv9&n\'\rmf"/cn~o9\x0cb8wiy&%g\x1b|f468/-b6`!%\x03\'/f)2.auy&9\xa8#\xfe\x13']
  Same as original: False

Text: (96) ['The quick brown fox jumps over the lazy dog! :d \t+ digits [bonus!!!]: 0123456789\t + others: \xff#\xa9\n'] will be encrypted (encrypt_dummy1) with password: (17) [#2minutes2midnite]
Encrypted text: (97) ['R\x86;\x85?:\x08\x06\xf3EB\xf5?-9X;@\xf00;+1GCH\xfd\x04#@3:\x02\x07\xeeH\xfc)?\xf1\x05\xeb\xf5F20JK9\xeb>8\xfe\xff\xf5\xfb@\x02\xfe\xf7I\xee\xeb\x07\x089\x01\xf7\x06H>\x01\xef\x03\x06\xee857\xee?\xf2\x04\xf6\x06\xfa\xfd\x03\xf1\x08\xeb\n4\xf3Eb\xeb']
Decrypted (decrypt_dummy1) using password: (17) [#2minutes2midnite]: (96) ['The quick brown fox jumps over the lazy dog! :d \t+ digits [bonus!!!]: 0123456789\t + others: \xff#\xa9\n']
  Same as original: True
Decrypted (decrypt_dummy1) using password [etindim2setunim2#]: ['\x12.a\'{ip4kw{nepjf )>$m\x7fqi$ 8oyx\'p.#f*e}s<}8gv9&n\'\rmf"/cn~o9\x0cb8wiy&%g\x1b|f468/-b6`!%\x03\'/f)2.auy&9\xa8#\xfe\x13']
  Same as original: False
CristiFati
  • 38,250
  • 9
  • 50
  • 87