Source code for auxlib.crypt

# -*- coding: utf-8 -*-
"""wrapper around PyCrypto cryptography library

Information sources:
    - http://eli.thegreenplace.net/2010/06/25/aes-encryption-of-files-in-python-with-pycrypto/
    - http://code.activestate.com/recipes/576980-authenticated-encryption-with-pycrypto/

"""
import base64
import hashlib
import hmac
import logging
import os

try:
    from Crypto.Cipher import AES
    AES_BLOCK_SIZE = AES.block_size
except ImportError:  # pragma: no cover
    logging.getLogger(__name__).error('auxlib.crypt is a pycrypto wrapper, '
                                      'which is not installed in the current '
                                      'environment.')  # pragma: no cover

from .compat import text_type
from .exceptions import AuthenticationError

log = logging.getLogger(__name__)

__all__ = ["as_base64", "from_base64", "encrypt", "decrypt", "aes_encrypt", "aes_decrypt"]

AES_KEY_SIZE = 32   # 32 byte key size ==> AES-256
HMAC_SIG_SIZE = hashlib.sha256().digest_size


[docs]def encrypt(secret_key, data): message_encryption_key = generate_encryption_key() encrypted_data = aes_encrypt(message_encryption_key, data) hashed_secret = generate_hash_from_secret(secret_key) encryption_key_encrypted = aes_encrypt(hashed_secret, message_encryption_key) return encryption_key_encrypted, encrypted_data
[docs]def decrypt(secret_key, encryption_key_encrypted, encrypted_data): hashed_secret = generate_hash_from_secret(secret_key) message_encryption_key = aes_decrypt(hashed_secret, encryption_key_encrypted) data = aes_decrypt(message_encryption_key, encrypted_data) return data
[docs]def as_base64(content): if isinstance(content, text_type): content = content.encode("UTF-8") return base64.urlsafe_b64encode(content)
[docs]def from_base64(content): if isinstance(content, text_type): content = content.encode('UTF-8') return base64.urlsafe_b64decode(content)
def generate_encryption_key(): """Create a new, random encryption key for use by this module. The encryption key is composed of an AES key and an HMAC signing key. Returns: str: base64-encoded encryption key """ return as_base64(os.urandom(AES_KEY_SIZE + HMAC_SIG_SIZE)) def generate_hash_from_secret(secret): return as_base64(hashlib.sha512(text_type(secret).encode('UTF-8')).digest())
[docs]def aes_encrypt(base64_encryption_key, data): """Encrypt data with AES-CBC and sign it with HMAC-SHA256 Arguments: base64_encryption_key (str): a base64-encoded string containing an AES encryption key and HMAC signing key as generated by generate_encryption_key() data (str): a byte string containing the data to be encrypted Returns: str: the encrypted data as a byte string with the HMAC signature appended to the end """ if isinstance(data, text_type): data = data.encode("UTF-8") aes_key_bytes, hmac_key_bytes = _extract_keys(base64_encryption_key) data = _pad(data) iv_bytes = os.urandom(AES_BLOCK_SIZE) cipher = AES.new(aes_key_bytes, mode=AES.MODE_CBC, IV=iv_bytes) data = iv_bytes + cipher.encrypt(data) # prepend init vector hmac_signature = hmac.new(hmac_key_bytes, data, hashlib.sha256).digest() return as_base64(data + hmac_signature)
[docs]def aes_decrypt(base64_encryption_key, base64_data): """Verify HMAC-SHA256 signature and decrypt data with AES-CBC Arguments: encryption_key (str): a base64-encoded string containing an AES encryption key and HMAC signing key as generated by generate_encryption_key() data (str): a byte string containing the data decrypted with an HMAC signing key appended to the end Returns: str: a byte string containing the data that was originally encrypted Raises: AuthenticationError: when the HMAC-SHA256 signature authentication fails """ data = from_base64(base64_data) aes_key_bytes, hmac_key_bytes = _extract_keys(base64_encryption_key) data, hmac_signature = data[:-HMAC_SIG_SIZE], data[-HMAC_SIG_SIZE:] if hmac.new(hmac_key_bytes, data, hashlib.sha256).digest() != hmac_signature: raise AuthenticationError("HMAC authentication failed") iv_bytes, data = data[:AES_BLOCK_SIZE], data[AES_BLOCK_SIZE:] cipher = AES.new(aes_key_bytes, AES.MODE_CBC, iv_bytes) data = cipher.decrypt(data) return _unpad(data)
def _pad(s): padding_bytes = AES_BLOCK_SIZE - len(s) % AES_BLOCK_SIZE return s + (chr(padding_bytes) * padding_bytes).encode('UTF-8') def _unpad(s): return s[:-ord(s.decode('UTF-8')[-1])] def _extract_keys(key_str): key_bytes = from_base64(key_str) return key_bytes[:-HMAC_SIG_SIZE], key_bytes[-HMAC_SIG_SIZE:]