Source code for steemengine.helpers

"""
Various helper functions for use in CryptoToken Converter.

Copyright::

    +===================================================+
    |                 © 2019 Privex Inc.                |
    |               https://www.privex.io               |
    +===================================================+
    |                                                   |
    |        CryptoToken Converter                      |
    |                                                   |
    |        Core Developer(s):                         |
    |                                                   |
    |          (+)  Chris (@someguy123) [Privex]        |
    |                                                   |
    +===================================================+

"""
import binascii
import logging
import random
import string

# characters that shouldn't be mistaken
from base64 import urlsafe_b64decode
from typing import Union

from cryptography.exceptions import InvalidSignature
from cryptography.fernet import Fernet, InvalidToken
from django.conf import settings

from payments.exceptions import EncryptKeyMissing, EncryptionError

log = logging.getLogger(__name__)

SAFE_CHARS = 'abcdefhkmnprstwxyz2345679ACDEFGHJKLMNPRSTWXYZ'


[docs]def random_str(size=50, chars=SAFE_CHARS): return ''.join(random.SystemRandom().choice(chars) for _ in range(size))
[docs]def empty(v, zero=False, itr=False) -> bool: """ Quickly check if a variable is empty or not. By default only '' and None are checked, use `itr` and `zero` to test for empty iterable's and zeroed variables. Returns True if a variable is None or '', returns False if variable passes the tests :param v: The variable to check if it's empty :param zero: if zero=True, then return True if the variable is 0 :param itr: if itr=True, then return True if the variable is ``[]``, ``{}``, or is an iterable and has 0 length :return bool is_blank: True if a variable is blank (``None``, ``''``, ``0``, ``[]`` etc.) :return bool is_blank: False if a variable has content (or couldn't be checked properly) """ _check = [None, ''] if zero: _check.append(0) if v in _check: return True if itr: if v == [] or v == {}: return True if hasattr(v, '__len__') and len(v) == 0: return True return False
""" ----- Encryption/Decryption functions ----- Various wrapper functions for simplifying the use of the Python library cryptography's Fernet module. encrypt/decrypt_str facilitate painless encryption and decryption of data using AES-128 CBC, they can either be passed a 32-byte Fernet key (base64 encoded) as an argument, or leave the key as None and they'll try to use the key defined in settings.ENCRYPT_KEY (generally set via .env file) get_fernet - internal use - obtain an instance of Fernet initialised with a key is_encrypted - check if a string is encrypted or not _crypt_str - internal use - handles the encryption/decryption for encrypt/decrypt_str encrypt_str - encrypt a string or bytes using a given Fernet key decrypt_str - decrypt a string or bytes that were encrypted using encrypt_str using a given Fernet key Basic usage: # Generates a 32-byte symmetric key, encoded with base64. Use .decode() to convert the key to a string for storage. >>> k = Fernet.generate_key() # Encrypts the string 'hello world' with AES-128 CBC using key ``k`` , returned as a base64 string >>> enc = encrypt_str('hello world', k) >>> print(enc) gAAAAABc7ERTpu2D_uven3l-KtU_ewUC8YWKqXEbLEKrPKrKWT138MNq-I9RRtCD8UZLdQrcdM_IhUU6r8T16lQkoJZ-I7N39g== # Check if a string/bytes is encrypted >>> is_encrypted(enc, k) True # Decrypt the encrypted data using the same key, outputs as a string >>> data = decrypt_str(enc, k) >>> print(data) hello world """
[docs]def get_fernet(key: Union[str, bytes] = None) -> Fernet: """ Used internally for getting Fernet instance with auto-fallback to settings.ENCRYPT_KEY and exception handling. :param str key: Base64 Fernet symmetric key for en/decrypting data. If empty, will fallback to settings.ENCRYPT_KEY :raises EncryptKeyMissing: Either no key was passed, or something is wrong with the key. :return Fernet f: Instance of Fernet using passed ``key`` or settings.ENCRYPT_KEY for encryption. """ if empty(key) and empty(settings.ENCRYPT_KEY): raise EncryptKeyMissing('No key argument passed, and ENCRYPT_KEY is empty. Cannot encrypt/decrypt.') key = settings.ENCRYPT_KEY if empty(key) else key try: f = Fernet(key) return f except (binascii.Error, ValueError): raise EncryptKeyMissing('The passed ``key`` or settings.ENCRYPT_KEY is not a valid Fernet key')
[docs]def is_encrypted(data: Union[str, bytes], key: Union[str, bytes] = None) -> bool: """ Returns True if the passed ``data`` appears to be encrypted. Can only verify encryption if the same ``key`` that was used to encrypt the data is passed. :param str data: The data to check for encryption, either as a string or bytes :param str key: Base64 encoded Fernet symmetric key for decrypting data. If empty, fallback to settings.ENCRYPT_KEY :raises EncryptKeyMissing: Either no key was passed, or something is wrong with the key. :return bool is_encrypted: True if the data is encrypted, False if it's not encrypted or wrong key used. """ f = get_fernet(key) # Convert the passed data into bytes before trying to decode it data = str(data).encode('utf-8') if type(data) != bytes else data # Attempt to extract the Fernet timestamp from the passed data. If exceptions are raised, then it's not encrypted. try: ts = f.extract_timestamp(data) log.debug(f'data was encrypted, token timestamp is {ts}') return True except (InvalidSignature, InvalidToken, binascii.Error) as e: log.debug('data is not encrypted? exception was: %s %s', type(e), str(e)) return False
def _crypt_str(direction: str, data: Union[str, bytes], key: Union[str, bytes] = None) -> str: """ Used internally by :py:func:`encrypt_str` and :py:func:`decrypt_str` :param str direction: Either 'encrypt' or 'decrypt' :param str data: The data to encrypt or decrypt as either a string or bytes :param str key: Base64 encoded Fernet symmetric key for encrypting/decrypting data. :return str data_out: Either the encrypted data as a base64 encoded string, or decrypted data as a plain string. """ if direction not in ['encrypt', 'decrypt']: raise ValueError('_crypt_str direction must be "encrypt" or "decrypt"') f = get_fernet(key) # Handle encryption/decryption of ``data`` try: # If ``data`` isn't already bytes, cast to a string and convert it to bytes before encrypting/decrypting data = str(data).encode('utf-8') if type(data) != bytes else data out = f.encrypt(data) if direction == 'encrypt' else f.decrypt(data) return out.decode() # Return encrypted/decrypted data as a string, not bytes. except Exception: strdat = str(data) if type(data) != bytes else str(data.decode()) log.exception(f'An exception occurred while trying to {direction} the data starting with "{strdat:.4}"...') raise EncryptionError(f'Failed to {direction} data... An admin must check the logs.')
[docs]def encrypt_str(data: Union[str, bytes], key: Union[str, bytes] = None) -> str: """ Encrypts a piece of data ``data`` passed as a string or bytes using Fernet with the passed 32-bit symmetric encryption key ``key``. Outputs the encrypted data as a Base64 string for easy storage. The ``key`` cannot just be a random "password", it must be a 32-byte key encoded with URL Safe base64. Use the management command ``./manage.py generate_key`` to create a Fernet compatible encryption key. Under the hood, Fernet uses AES-128 CBC to encrypt the data, with PKCS7 padding and HMAC_SHA256 authentication. If the ``key`` parameter isn't passed, or is empty (None / ""), then it will attempt to fall back to ``settings.ENCRYPT_KEY`` - if that's also empty, EncryptKeyMissing will be raised. :param str data: The data to be encrypted, in the form of either a str or bytes. :param str key: A Fernet encryption key (base64) to be used, if left blank will fall back to settings.ENCRYPT_KEY :raises EncryptKeyMissing: Either no key was passed, or something is wrong with the key. :raises EncryptionError: Something went wrong while attempting to encrypt the data :return str encrypted_data: The encrypted version of the passed ``data`` as a base64 encoded string. """ return _crypt_str('encrypt', data, key)
[docs]def decrypt_str(data: Union[str, bytes], key: Union[str, bytes] = None) -> str: """ Decrypts ``data`` previously encrypted using :py:func:`encrypt_str` with the same Fernet compatible ``key``, and returns the decrypted version as a string. The ``key`` cannot just be a random "password", it must be a 32-byte key encoded with URL Safe base64. Use the management command ``./manage.py generate_key`` to create a Fernet compatible encryption key. Under the hood, Fernet uses AES-128 CBC to encrypt the data, with PKCS7 padding and HMAC_SHA256 authentication. If the ``key`` parameter isn't passed, or is empty (None / ""), then it will attempt to fall back to ``settings.ENCRYPT_KEY`` - if that's also empty, EncryptKeyMissing will be raised. :param str data: The base64 encoded data to be decrypted, in the form of either a str or bytes. :param str key: A Fernet encryption key (base64) for decryption, if blank, will fall back to settings.ENCRYPT_KEY :raises EncryptKeyMissing: Either no key was passed, or something is wrong with the key. :raises EncryptionError: Something went wrong while attempting to decrypt the data :return str decrypted_data: The decrypted data as a string """ return _crypt_str('decrypt', data, key)