Proper data encryption with a user-set password in python3

Question:

I have been looking for a proper data encryption library
in python for a long while, today I needed it once again, cannot
find anything, so is there any way to encrypt data using a
user-set password, if I find something it’s usually insecure,
if I find a good solution it has no support for user-set passwords,
meaning I’m stuck, any way to do it?

Here’s some pseudocode:

import encryption

encryptor: encryption.Crypt = encryption.Crypt("my secret password")

encryptor.encrypt("hello this is my very secret string")  # => 9oe gyu yp9q*(Y 28j
encryptor.decrypt("9oe gyu yp9q*(Y 28j")  # => hello this is my very secret string

I don’t care if it’s an object, for all I care
it can also be a function which accepts the password:

import encryption

encryption.encrypt("hello this is my very secret string", "my secret password")  # => 9oe gyu yp9q*(Y 28j
encryption.decrypt("9oe gyu yp9q*(Y 28j", "my secret password")  # => hello this is my very secret string

I don’t mind the way it’s encrypted or decrypted, I
just want to have a way to do it :), I also don’t
care abt it’s output, it can be binary, an object,
a string, anything

Asked By: Ari157

||

Answers:

With a little elbow grease, the cryptography package (pip install cryptography) works fine for this. I think the reason that you don’t see a simple OOTB string-based implementation is that cryptography works on the level of bytes, not strings (which might be encoded any which way). Regardless, the following seems to work for the simple use case you outlined.

Create an ‘encryptor’ and encrypt something

from cryptography.fernet import Fernet
import base64
entered_pw = "secretpw"
key = base64.b64encode(f"{entered_pw:<32}".encode("utf-8"))
encryptor = Fernet(key=key)
encrypted = encryptor.encrypt(
    "my super secret data with a password".encode("utf-8")
)
print(encrypted)

Output:

b'gAAAAABjDVew9-VszAFP1ZdlDz-ZqwdIksCBhLbH8OEJjxEZyEy6cQ4jrxEBHZtHsGtWxHsG0qJIkQ_b5e5Ibx5-1uAa1HXdBwWys6YE7WwhmMPtAbj_VP2F8rKFck7MYvv5nKrZbWGF'

Decrypt result with correctly entered password

from cryptography.fernet import Fernet
import base64
entered_pw = "secretpw"
key = base64.b64encode(f"{entered_pw:<32}".encode("utf-8"))
encryptor = Fernet(key=key)
encrypted = b'gAAAAABjDVew9-VszAFP1ZdlDz-ZqwdIksCBhLbH8OEJjxEZyEy6cQ4jrxEBHZtHsGtWxHsG0qJIkQ_b5e5Ibx5-1uAa1HXdBwWys6YE7WwhmMPtAbj_VP2F8rKFck7MYvv5nKrZbWGF'
print(encryptor.decrypt(encrypted).decode("utf-8"))

Output:

'my super secret data with a password'

If you supply the wrong password at decrypt time, you’ll get an InvalidSignature exception.

Full disclosure, I am NOT a cryptography expert, so this might be a very bad and insecure method, but it seems legit ¯_(ツ)_/¯. If there are any crypto experts out there who want to take me to task, feel free… 😉

Answered By: Sam Hartzog

Building on the answer from Sam Hartzog, below is an example which follows the logic described for PBES2 (Password Based Encryption Scheme 2) defined in RFC8018, Section 6.2. However, it stops short of encoding algorithm choices and parameters.

#!/usr/bin/python

import base64
import secrets
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

KDF_ALGORITHM = hashes.SHA256()
KDF_LENGTH = 32
KDF_ITERATIONS = 120000

def encrypt(plaintext: str, password: str) -> (bytes, bytes):
    # Derive a symmetric key using the passsword and a fresh random salt.
    salt = secrets.token_bytes(16)
    kdf = PBKDF2HMAC(
        algorithm=KDF_ALGORITHM, length=KDF_LENGTH, salt=salt,
        iterations=KDF_ITERATIONS)
    key = kdf.derive(password.encode("utf-8"))

    # Encrypt the message.
    f = Fernet(base64.urlsafe_b64encode(key))
    ciphertext = f.encrypt(plaintext.encode("utf-8"))

    return ciphertext, salt

def decrypt(ciphertext: bytes, password: str, salt: bytes) -> str:
    # Derive the symmetric key using the password and provided salt.
    kdf = PBKDF2HMAC(
        algorithm=KDF_ALGORITHM, length=KDF_LENGTH, salt=salt,
        iterations=KDF_ITERATIONS)
    key = kdf.derive(password.encode("utf-8"))

    # Decrypt the message
    f = Fernet(base64.urlsafe_b64encode(key))
    plaintext = f.decrypt(ciphertext)

    return plaintext.decode("utf-8")

def main():
    password = "aStrongPassword"
    message = "a secret message"

    encrypted, salt = encrypt(message, password)
    decrypted = decrypt(encrypted, password, salt)

    print(f"message: {message}")
    print(f"encrypted: {encrypted}")
    print(f"decrypted: {decrypted}")

Output:

message: a secret message
encrypted: b'gAAAAABjDlH2eaRZmB4rduBdNHUOITV5q4oelpnLRUgI_uyQyNpUyW8h3c2lZYS1MwMpRWIZposcZvag9si1pc4IEK83_CzyBdXF27Aop9WWS6ybxTg9BSo='
decrypted: a secret message
Answered By: chuckx

Give a try with this tiny python module acting as a wrapper to cryptography

pip install pyeasyencrypt

Example of code:

import logging, os
# pip install pyeasyencrypt
from pyeasyencrypt.pyeasyencrypt import encrypt_string, decrypt_string

level = os.getenv("LOGGER", "INFO")
logging.basicConfig(level=level)
logger = logging.getLogger(__name__)

def main():
    logger.info("Example")
    clear_string = 'stringA'
    password = "my password"
    encrypted_string = encrypt_string(clear_string, password)
    decrypted_string = decrypt_string(encrypted_string, password)
    logger.info(f"clear_string={clear_string} decrypted_string={decrypt_string} password={password}  encrypted_string={encrypted_string}")
    logger.debug("Done")

if __name__ == '__main__':
    main()

For more details of the source code check out at github https://github.com/redcorjo/pyeasyencrypt

https://pypi.org/project/pyeasyencrypt/

Answered By: user20955183