"""Crypto wrapper implementations.

A crypto wrapper encapsulates all cryptographic operations that have to be
applied to node representations before they can be safely inserted into the
(untrusted) database, e.g., encryption and authentication. It defines the keys
(digests) under which (wrapped) node representations are stored and specifies
how node representations can be extracted from (digest, value) pairs stored in
the database.

This module includes several crypto wrapper implementations with different
security properties.
"""
import abc
import hashlib
import hmac
import struct
import logging


# format strings to encode parameters
FORMAT_HEIGHT = 'i'
FORMAT_ISROOT = '?'
FORMAT_HEIGHT_ISROOT = FORMAT_HEIGHT + FORMAT_ISROOT


class IntegrityError(ValueError):
    """Raised if an integrity verification fails."""
    pass


class AuthenticityError(IntegrityError):
    """Raised if an authenticity verification fails."""
    pass


class BaseCryptoWrapper(object):

    """Abstract class specifying the crypto wrapper interface."""
    __metaclass__ = abc.ABCMeta
    __slots__ = []

    """Defines the constant size of digests generated by this crypto wrapper."""
    DIGEST_SIZE = None

    def wrap_value(self, value, height, is_root):
        """Converts a node representation into a (digest, value) pair that can
        be safely inserted into the database.

        Args:
            value (str): The node representation.
            height (int): The height of the node in its chunk tree.
            is_root (bool): Whether or not the node is the chunk tree's root.

        Returns:
            tuple: (wrapped_value, digest).
        """
        raise NotImplementedError

    def unwrap_value(self, value, digest, height, is_root, length=-1):
        """Converts the representation of a node as stored in the database
        (e.g., an encrypted representation) into its `normal`, e.g., decrypted,
        representation.

        Args:
            value (str): Wrapped node representation.
            digest (str): Key under which the node is stored in the database.
            height (int): Height of the node in its chunk tree.
            is_root (bool): Whether or not the node is the chunk tree's root.
            length [Optional(int)]: Length of the content represented by this
                node. Defaults to -1.

        Returns:
            str: Node representation.
        """
        raise NotImplementedError


HASHLIB_SHA256 = hashlib.sha256


class SHA_256(BaseCryptoWrapper):

    """SHA-256 crypto wrapper.

    Provides integrity for chunk tree nodes.

    Nodes are represented as follows:
        * value: <value>
        * digest: SHA-256(<value>)
    """

    __slots__ = []

    DIGEST_SIZE = 32

    def __init__(self):
        super(SHA_256, self).__init__()

    def wrap_value(self, value, height, is_root):
        """Uses SHA-256 hash of node representation as digest and value as is.

        See :meth:`.BaseCryptoWrapper.wrap_value`.
        """
        m = HASHLIB_SHA256()
        m.update(struct.pack(FORMAT_HEIGHT, height))
        m.update(value)
        return value, m.digest()

    def unwrap_value(self, value, digest, height, is_root, length=-1):
        """Verifies SHA-256 hash and returns node representation on success.

        Raises:
            IntegrityError: If digest does not match.

        See :meth:`.BaseCryptoWrapper.unwrap_value`.
        """
        m = HASHLIB_SHA256()
        m.update(struct.pack(FORMAT_HEIGHT, height))
        m.update(value)
        if m.digest() != digest or (height == 0 and len(value) < length):
            raise IntegrityError
        return value


HMAC_new = hmac.new


class HMAC_SHA_256(BaseCryptoWrapper):

    """HMAC-SHA-256 crypto wrapper.

    Provides authenticity for chunk tree nodes based on a symmetric 32-bytes key
    specified during instantiation.

    Root nodes are handled identically to inner nodes at the same level.

    Nodes are represented as follows:
        * value: <value>
        * digest: HMAC-SHA-256(<key>, <height> || <value>)

    Args:
        key (str): Cryptographic key used for symmetric authentication.
    """

    __slots__ = ['_key']

    DIGEST_SIZE = 32

    def __init__(self, key):
        super(HMAC_SHA_256, self).__init__()
        self._key = key

    def wrap_value(self, value, height, is_root):
        """Uses SHA-256-based HMAC of node representation and height as digest
        and value as is.

        See :meth:`.BaseCryptoWrapper.wrap_value`.
        """
        m = HMAC_new(self._key, digestmod=HASHLIB_SHA256)
        m.update(struct.pack(FORMAT_HEIGHT, height))
        m.update(value)
        return value, m.digest()

    def unwrap_value(self, value, digest, height, is_root, length=-1):
        """Verifies SHA-256-based HMAC and returns node representation on
        success.

        Raises:
            AuthenticityError: If digest does not match.

        See :meth:`.BaseCryptoWrapper.unwrap_value`.
        """
        m = HMAC_new(self._key, digestmod=HASHLIB_SHA256)
        m.update(struct.pack(FORMAT_HEIGHT, height))
        m.update(value)
        if m.digest() != digest or (height == 0 and len(value) < length):
            raise AuthenticityError
        return value


class HMAC_SHA_256_DISTINGUISHED_ROOT(HMAC_SHA_256):

    """HMAC-SHA-256 crypto wrapper with distinguished root representation.

    Provides authenticity for chunk tree nodes based on a symmetric 32-bytes key
    specified during instantiation.

    Root nodes are handled differently from inner nodes at the same level.

    Nodes are represented as follows:
        * value: <value>
        * digest: HMAC-SHA-256(<key>, <height> || <is_root> || <value>)

    Args:
        key (str): Cryptographic key used for symmetric authentication.
    """

    __slots__ = []

    def __init__(self, key):
        HMAC_SHA_256.__init__(self, key)

    def wrap_value(self, value, height, is_root):
        """Uses SHA-256-based HMAC of node representation, height and is_root
        flag as digest and value as is.

        See :meth:`.BaseCryptoWrapper.wrap_value`.
        """
        m = HMAC_new(self._key, digestmod=HASHLIB_SHA256)
        m.update(struct.pack(FORMAT_HEIGHT_ISROOT, height, is_root))
        m.update(value)
        return value, m.digest()

    def unwrap_value(self, value, digest, height, is_root, length=-1):
        """Verifies SHA-256-based HMAC and returns node representation on
        success.

        Raises:
            AuthenticityError: If digest does not match.

        See :meth:`.BaseCryptoWrapper.unwrap_value`.
        """
        m = HMAC_new(self._key, digestmod=HASHLIB_SHA256)
        m.update(struct.pack(FORMAT_HEIGHT_ISROOT, height, is_root))
        m.update(value)
        if m.digest() != digest or (height == 0 and len(value) < length):
            raise AuthenticityError
        return value


# HMAC_SHA_256_R = HMAC_SHA_256.DIGEST_SIZE + 8
# 
# 
# class HMAC_SHA_256_DISTINGUISHED_ROOT_WITH_LEAF_PADDING(HMAC_SHA_256):
#  
#     """HMAC-SHA-256 crypto wrapper with distinguished root representation and
#     leaf padding.
#  
#     Provides authenticity for chunk tree nodes based on a symmetric 32-bytes key
#     specified during instantiation.
#  
#     Root nodes are handled differently from inner nodes at the same level.
#     
#     WARNING: not yet supported by SecCS!
#  
#     Nodes are represented as follows:
#         * value: <value>
#         * digest: HMAC-SHA-256(<key>, <height> || <is_root> || <value>)
#  
#     Args:
#         key (str): Cryptographic key used for symmetric authentication.
#     """
#      
#     __slots__ = []
#  
#     def __init__(self, key):
#         HMAC_SHA_256.__init__(self, key)
#  
#     def wrap_value(self, value, height, is_root):
#         """Pads leaf node representations with zero bytes to make them
#         indifferentiable from superchunks and uses SHA-256-based HMAC of the
#         resulting node representation, height and is_root flag as digest.
#  
#         See :meth:`.BaseCryptoWrapper.wrap_value`.
#         """
#         m = HMAC_new(self._key, digestmod=HASHLIB_SHA256)
#         m.update(struct.pack(FORMAT_HEIGHT_ISROOT, height, is_root))
#         if height == 0:
#             value += b'\x00' * (HMAC_SHA_256_R - (len(value) % HMAC_SHA_256_R))
#         m.update(value)
#         return value, m.digest()
#  
#     def unwrap_value(self, value, digest, height, is_root, length=-1):
#         """Verifies SHA-256-based HMAC, removes padding and returns resulting
#         node representation on success.
#  
#         Raises:
#             AuthenticityError: If digest does not match.
#  
#         See :meth:`.BaseCryptoWrapper.unwrap_value`.
#         """
#         m = HMAC_new(self._key, digestmod=HASHLIB_SHA256)
#         m.update(struct.pack(FORMAT_HEIGHT_ISROOT, height, is_root))
#         m.update(value)
#         if m.digest() != digest or (height == 0 and len(value) < length):
#             raise AuthenticityError
#         return value[:length] if height == 0 and length > -1 else value


try:
    from Crypto.Cipher import AES
    AES_MODE_SIV = AES.MODE_SIV
except ImportError:
    logging.getLogger(__name__).warn('PyCrypto is not available, disabling AES wrappers.')
except AttributeError:
    logging.getLogger(__name__).warn('Your PyCrypto version is too old, disabling '
                                     'AES-SIV wrappers which depend on PyCrypto >= 2.7a1.')
else:
    AES_new_fn = AES.new
    
    
    class AES_SIV_256(BaseCryptoWrapper):
    
        """AES-SIV-256 crypto wrapper.
    
        Provides confidentiality and authenticity for chunk tree nodes based on a
        symmetric 32-bytes key specified during instantiation.
    
        Root nodes are handled identically to inner nodes at the same level.

        Nodes are represented as follows:
            * value: AES-SIV-256(<key>, <value>, additional_data=<height>)
            * digest: <digest produced by AES-SIV-256>
    
        Args:
            key (str): Cryptographic key used for symmetric encryption and
                authentication.

        Note:
            Requires PyCrypto >= 2.7a1.
        """
    
        __slots__ = ['_key', '_zero_digest']
    
        DIGEST_SIZE = 16
    
        def __init__(self, key):
            super(AES_SIV_256, self).__init__()
            self._key = key
    
            """The AES-SIV implementation does not support encryption of empty
            strings, so we represent empty-string nodes by a designated zero_digest
            instead.
            """
            cipher = AES_new_fn(self._key, AES_MODE_SIV)
            cipher.update(b'empty_additional_data')
            _, self._zero_digest = cipher.encrypt_and_digest(b'empty_string')
            self._zero_digest = b'\x00' * AES_SIV_256.DIGEST_SIZE
    
        def wrap_value(self, value, height, is_root):
            """Encrypts node representation using deterministic authenticated
            encryption, i.e., with AES in SIV mode, including node height as
            additional data that is authenticated, resulting in a digest (MAC)
            that is used as `digest` and a ciphertext that is used as `value`.
    
            Note:
                As AES-SIV cannot encrypt empty contents, a distinguished zero
                digest is artifically assigned to empty node representations
                instead.
    
            See :meth:`.BaseCryptoWrapper.wrap_value`.
            """
            if value == b'':
                return value, self._zero_digest
            cipher = AES_new_fn(self._key, AES_MODE_SIV)
            cipher.update(struct.pack(FORMAT_HEIGHT, height))
            return cipher.encrypt_and_digest(value)
    
        def unwrap_value(self, value, digest, height, is_root, length=-1):
            """Decrypts and verifies node representation and returns the result on
            success.
    
            Raises:
                AuthenticityError: If digest does not match.
    
            See :meth:`.BaseCryptoWrapper.unwrap_value`.
            """
            if value == b'' and digest == self._zero_digest:
                return value
            cipher = AES_new_fn(self._key, AES_MODE_SIV)
            cipher.update(struct.pack(FORMAT_HEIGHT, height))
            try:
                return cipher.decrypt_and_verify(value, digest)
            except ValueError as e:
                raise AuthenticityError(e)
    
    
    class AES_SIV_256_DISTINGUISHED_ROOT(AES_SIV_256):
    
        """AES-SIV-256 crypto wrapper with distinguished root representation.
    
        Provides confidentiality and authenticity for chunk tree nodes based on a
        symmetric 32-bytes key specified during instantiation.
    
        Root nodes are handled differently from inner nodes at the same level.

        Nodes are represented as follows:
            * value: AES-SIV-256(<key>, <value>, additional_data=<height>||<is_root>)
            * digest: <digest produced by AES-SIV-256>
    
        Args:
            key (str): Cryptographic key used for symmetric encryption and
                authentication.

        Note:
            Requires PyCrypto >= 2.7a1.
        """

        __slots__ = []
            
        def __init__(self, key):
            AES_SIV_256.__init__(self, key)
    
        def wrap_value(self, value, height, is_root):
            """Encrypts node representation using deterministic authenticated
            encryption, i.e., with AES in SIV mode, including node height and
            is_root flag as additional data that is authenticated, resulting in a
            digest (MAC) that is used as `digest` and a ciphertext that is used as
            `value`.
    
            See :meth:`.AES_SIV_256.wrap_value`.
            """
            if value == b'':
                return value, self._zero_digest
            cipher = AES_new_fn(self._key, AES_MODE_SIV)
            cipher.update(struct.pack(FORMAT_HEIGHT_ISROOT, height, is_root))
            return cipher.encrypt_and_digest(value)
    
        def unwrap_value(self, value, digest, height, is_root, length=-1):
            """Decrypts and verifies node representation and returns the result on
            success.
    
            Raises:
                AuthenticityError: If digest does not match.
    
            See :meth:`.BaseCryptoWrapper.unwrap_value`.
            """
            if value == b'' and digest == self._zero_digest:
                return value
            cipher = AES_new_fn(self._key, AES_MODE_SIV)
            cipher.update(struct.pack(FORMAT_HEIGHT_ISROOT, height, is_root))
            try:
                return cipher.decrypt_and_verify(value, digest)
            except ValueError as e:
                raise AuthenticityError(e)
