# Copyright 2017 Donald Stufft and individual contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import absolute_import, division, print_function

from nacl import exceptions as exc
from nacl._sodium import ffi, lib
from nacl.exceptions import ensure

"""
Implementations of authenticated encription with associated data (*AEAD*)
constructions building on the chacha20 stream cipher and the poly1305
authenticator
"""

crypto_aead_chacha20poly1305_ietf_KEYBYTES = \
    lib.crypto_aead_chacha20poly1305_ietf_keybytes()
crypto_aead_chacha20poly1305_ietf_NSECBYTES = \
    lib.crypto_aead_chacha20poly1305_ietf_nsecbytes()
crypto_aead_chacha20poly1305_ietf_NPUBBYTES = \
    lib.crypto_aead_chacha20poly1305_ietf_npubbytes()
crypto_aead_chacha20poly1305_ietf_ABYTES = \
    lib.crypto_aead_chacha20poly1305_ietf_abytes()
crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX = \
    lib.crypto_aead_chacha20poly1305_ietf_messagebytes_max()
_aead_chacha20poly1305_ietf_CRYPTBYTES_MAX = \
    crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX + \
    crypto_aead_chacha20poly1305_ietf_ABYTES

crypto_aead_chacha20poly1305_KEYBYTES = \
    lib.crypto_aead_chacha20poly1305_keybytes()
crypto_aead_chacha20poly1305_NSECBYTES = \
    lib.crypto_aead_chacha20poly1305_nsecbytes()
crypto_aead_chacha20poly1305_NPUBBYTES = \
    lib.crypto_aead_chacha20poly1305_npubbytes()
crypto_aead_chacha20poly1305_ABYTES = \
    lib.crypto_aead_chacha20poly1305_abytes()
crypto_aead_chacha20poly1305_MESSAGEBYTES_MAX = \
    lib.crypto_aead_chacha20poly1305_messagebytes_max()
_aead_chacha20poly1305_CRYPTBYTES_MAX = \
    crypto_aead_chacha20poly1305_MESSAGEBYTES_MAX + \
    crypto_aead_chacha20poly1305_ABYTES

crypto_aead_xchacha20poly1305_ietf_KEYBYTES = \
    lib.crypto_aead_xchacha20poly1305_ietf_keybytes()
crypto_aead_xchacha20poly1305_ietf_NSECBYTES = \
    lib.crypto_aead_xchacha20poly1305_ietf_nsecbytes()
crypto_aead_xchacha20poly1305_ietf_NPUBBYTES = \
    lib.crypto_aead_xchacha20poly1305_ietf_npubbytes()
crypto_aead_xchacha20poly1305_ietf_ABYTES = \
    lib.crypto_aead_xchacha20poly1305_ietf_abytes()
crypto_aead_xchacha20poly1305_ietf_MESSAGEBYTES_MAX = \
    lib.crypto_aead_xchacha20poly1305_ietf_messagebytes_max()
_aead_xchacha20poly1305_ietf_CRYPTBYTES_MAX = \
    crypto_aead_xchacha20poly1305_ietf_MESSAGEBYTES_MAX + \
    crypto_aead_xchacha20poly1305_ietf_ABYTES


def crypto_aead_chacha20poly1305_ietf_encrypt(message, aad, nonce, key):
    """
    Encrypt the given ``message`` using the IETF ratified chacha20poly1305
    construction described in RFC7539.

    :param message:
    :type message: bytes
    :param aad:
    :type aad: bytes
    :param nonce:
    :type nonce: bytes
    :param key:
    :type key: bytes
    :return: authenticated ciphertext
    :rtype: bytes
    """
    ensure(isinstance(message, bytes), 'Input message type must be bytes',
           raising=exc.TypeError)

    mlen = len(message)

    ensure(mlen <= crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX,
           'Message must be at most {0} bytes long'.format(
               crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX),
           raising=exc.ValueError)

    ensure(isinstance(aad, bytes) or (aad is None),
           'Additional data must be bytes or None',
           raising=exc.TypeError)

    ensure(isinstance(nonce, bytes) and
           len(nonce) == crypto_aead_chacha20poly1305_ietf_NPUBBYTES,
           'Nonce must be a {0} bytes long bytes sequence'.format(
               crypto_aead_chacha20poly1305_ietf_NPUBBYTES),
           raising=exc.TypeError)

    ensure(isinstance(key, bytes) and
           len(key) == crypto_aead_chacha20poly1305_ietf_KEYBYTES,
           'Key must be a {0} bytes long bytes sequence'.format(
               crypto_aead_chacha20poly1305_ietf_KEYBYTES),
           raising=exc.TypeError)

    if aad:
        _aad = aad
        aalen = len(aad)
    else:
        _aad = ffi.NULL
        aalen = 0

    mxout = mlen + crypto_aead_chacha20poly1305_ietf_ABYTES

    clen = ffi.new("unsigned long long *")

    ciphertext = ffi.new("unsigned char[]", mxout)

    res = lib.crypto_aead_chacha20poly1305_ietf_encrypt(ciphertext,
                                                        clen,
                                                        message,
                                                        mlen,
                                                        _aad,
                                                        aalen,
                                                        ffi.NULL,
                                                        nonce,
                                                        key)

    ensure(res == 0, "Encryption failed.", raising=exc.CryptoError)
    return ffi.buffer(ciphertext, clen[0])[:]


def crypto_aead_chacha20poly1305_ietf_decrypt(ciphertext, aad, nonce, key):
    """
    Decrypt the given ``ciphertext`` using the IETF ratified chacha20poly1305
    construction described in RFC7539.

    :param ciphertext:
    :type ciphertext: bytes
    :param aad:
    :type aad: bytes
    :param nonce:
    :type nonce: bytes
    :param key:
    :type key: bytes
    :return: message
    :rtype: bytes
    """
    ensure(isinstance(ciphertext, bytes),
           'Input ciphertext type must be bytes',
           raising=exc.TypeError)

    clen = len(ciphertext)

    ensure(clen <= _aead_chacha20poly1305_ietf_CRYPTBYTES_MAX,
           'Ciphertext must be at most {0} bytes long'.format(
               _aead_chacha20poly1305_ietf_CRYPTBYTES_MAX),
           raising=exc.ValueError)

    ensure(isinstance(aad, bytes) or (aad is None),
           'Additional data must be bytes or None',
           raising=exc.TypeError)

    ensure(isinstance(nonce, bytes) and
           len(nonce) == crypto_aead_chacha20poly1305_ietf_NPUBBYTES,
           'Nonce must be a {0} bytes long bytes sequence'.format(
               crypto_aead_chacha20poly1305_ietf_NPUBBYTES),
           raising=exc.TypeError)

    ensure(isinstance(key, bytes) and
           len(key) == crypto_aead_chacha20poly1305_ietf_KEYBYTES,
           'Key must be a {0} bytes long bytes sequence'.format(
               crypto_aead_chacha20poly1305_ietf_KEYBYTES),
           raising=exc.TypeError)

    mxout = clen - crypto_aead_chacha20poly1305_ietf_ABYTES

    mlen = ffi.new("unsigned long long *")
    message = ffi.new("unsigned char[]", mxout)

    if aad:
        _aad = aad
        aalen = len(aad)
    else:
        _aad = ffi.NULL
        aalen = 0

    res = lib.crypto_aead_chacha20poly1305_ietf_decrypt(message,
                                                        mlen,
                                                        ffi.NULL,
                                                        ciphertext,
                                                        clen,
                                                        _aad,
                                                        aalen,
                                                        nonce,
                                                        key)

    ensure(res == 0, "Decryption failed.", raising=exc.CryptoError)

    return ffi.buffer(message, mlen[0])[:]


def crypto_aead_chacha20poly1305_encrypt(message, aad, nonce, key):
    """
    Encrypt the given ``message`` using the "legacy" construction
    described in draft-agl-tls-chacha20poly1305.

    :param message:
    :type message: bytes
    :param aad:
    :type aad: bytes
    :param nonce:
    :type nonce: bytes
    :param key:
    :type key: bytes
    :return: authenticated ciphertext
    :rtype: bytes
    """
    ensure(isinstance(message, bytes), 'Input message type must be bytes',
           raising=exc.TypeError)

    mlen = len(message)

    ensure(mlen <= crypto_aead_chacha20poly1305_MESSAGEBYTES_MAX,
           'Message must be at most {0} bytes long'.format(
               crypto_aead_chacha20poly1305_MESSAGEBYTES_MAX),
           raising=exc.ValueError)

    ensure(isinstance(aad, bytes) or (aad is None),
           'Additional data must be bytes or None',
           raising=exc.TypeError)

    ensure(isinstance(nonce, bytes) and
           len(nonce) == crypto_aead_chacha20poly1305_NPUBBYTES,
           'Nonce must be a {0} bytes long bytes sequence'.format(
               crypto_aead_chacha20poly1305_NPUBBYTES),
           raising=exc.TypeError)

    ensure(isinstance(key, bytes) and
           len(key) == crypto_aead_chacha20poly1305_KEYBYTES,
           'Key must be a {0} bytes long bytes sequence'.format(
               crypto_aead_chacha20poly1305_KEYBYTES),
           raising=exc.TypeError)

    if aad:
        _aad = aad
        aalen = len(aad)
    else:
        _aad = ffi.NULL
        aalen = 0

    mlen = len(message)
    mxout = mlen + crypto_aead_chacha20poly1305_ietf_ABYTES

    clen = ffi.new("unsigned long long *")

    ciphertext = ffi.new("unsigned char[]", mxout)

    res = lib.crypto_aead_chacha20poly1305_encrypt(ciphertext,
                                                   clen,
                                                   message,
                                                   mlen,
                                                   _aad,
                                                   aalen,
                                                   ffi.NULL,
                                                   nonce,
                                                   key)

    ensure(res == 0, "Encryption failed.", raising=exc.CryptoError)
    return ffi.buffer(ciphertext, clen[0])[:]


def crypto_aead_chacha20poly1305_decrypt(ciphertext, aad, nonce, key):
    """
    Decrypt the given ``ciphertext`` using the "legacy" construction
    described in draft-agl-tls-chacha20poly1305.

    :param ciphertext: authenticated ciphertext
    :type ciphertext: bytes
    :param aad:
    :type aad: bytes
    :param nonce:
    :type nonce: bytes
    :param key:
    :type key: bytes
    :return: message
    :rtype: bytes
    """
    ensure(isinstance(ciphertext, bytes),
           'Input ciphertext type must be bytes',
           raising=exc.TypeError)

    clen = len(ciphertext)

    ensure(clen <= _aead_chacha20poly1305_CRYPTBYTES_MAX,
           'Ciphertext must be at most {0} bytes long'.format(
               _aead_chacha20poly1305_CRYPTBYTES_MAX),
           raising=exc.ValueError)

    ensure(isinstance(aad, bytes) or (aad is None),
           'Additional data must be bytes or None',
           raising=exc.TypeError)

    ensure(isinstance(nonce, bytes) and
           len(nonce) == crypto_aead_chacha20poly1305_NPUBBYTES,
           'Nonce must be a {0} bytes long bytes sequence'.format(
               crypto_aead_chacha20poly1305_NPUBBYTES),
           raising=exc.TypeError)

    ensure(isinstance(key, bytes) and
           len(key) == crypto_aead_chacha20poly1305_KEYBYTES,
           'Key must be a {0} bytes long bytes sequence'.format(
               crypto_aead_chacha20poly1305_KEYBYTES),
           raising=exc.TypeError)

    mxout = clen - crypto_aead_chacha20poly1305_ABYTES

    mlen = ffi.new("unsigned long long *")
    message = ffi.new("unsigned char[]", mxout)

    if aad:
        _aad = aad
        aalen = len(aad)
    else:
        _aad = ffi.NULL
        aalen = 0

    res = lib.crypto_aead_chacha20poly1305_decrypt(message,
                                                   mlen,
                                                   ffi.NULL,
                                                   ciphertext,
                                                   clen,
                                                   _aad,
                                                   aalen,
                                                   nonce,
                                                   key)

    ensure(res == 0, "Decryption failed.", raising=exc.CryptoError)

    return ffi.buffer(message, mlen[0])[:]


def crypto_aead_xchacha20poly1305_ietf_encrypt(message, aad, nonce, key):
    """
    Encrypt the given ``message`` using the long-nonces xchacha20poly1305
    construction.

    :param message:
    :type message: bytes
    :param aad:
    :type aad: bytes
    :param nonce:
    :type nonce: bytes
    :param key:
    :type key: bytes
    :return: authenticated ciphertext
    :rtype: bytes
    """
    ensure(isinstance(message, bytes), 'Input message type must be bytes',
           raising=exc.TypeError)

    mlen = len(message)

    ensure(mlen <= crypto_aead_xchacha20poly1305_ietf_MESSAGEBYTES_MAX,
           'Message must be at most {0} bytes long'.format(
               crypto_aead_xchacha20poly1305_ietf_MESSAGEBYTES_MAX),
           raising=exc.ValueError)

    ensure(isinstance(aad, bytes) or (aad is None),
           'Additional data must be bytes or None',
           raising=exc.TypeError)

    ensure(isinstance(nonce, bytes) and
           len(nonce) == crypto_aead_xchacha20poly1305_ietf_NPUBBYTES,
           'Nonce must be a {0} bytes long bytes sequence'.format(
               crypto_aead_xchacha20poly1305_ietf_NPUBBYTES),
           raising=exc.TypeError)

    ensure(isinstance(key, bytes) and
           len(key) == crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
           'Key must be a {0} bytes long bytes sequence'.format(
               crypto_aead_xchacha20poly1305_ietf_KEYBYTES),
           raising=exc.TypeError)

    if aad:
        _aad = aad
        aalen = len(aad)
    else:
        _aad = ffi.NULL
        aalen = 0

    mlen = len(message)
    mxout = mlen + crypto_aead_xchacha20poly1305_ietf_ABYTES

    clen = ffi.new("unsigned long long *")

    ciphertext = ffi.new("unsigned char[]", mxout)

    res = lib.crypto_aead_xchacha20poly1305_ietf_encrypt(ciphertext,
                                                         clen,
                                                         message,
                                                         mlen,
                                                         _aad,
                                                         aalen,
                                                         ffi.NULL,
                                                         nonce,
                                                         key)

    ensure(res == 0, "Encryption failed.", raising=exc.CryptoError)
    return ffi.buffer(ciphertext, clen[0])[:]


def crypto_aead_xchacha20poly1305_ietf_decrypt(ciphertext, aad, nonce, key):
    """
    Decrypt the given ``ciphertext`` using the long-nonces xchacha20poly1305
    construction.

    :param ciphertext: authenticated ciphertext
    :type ciphertext: bytes
    :param aad:
    :type aad: bytes
    :param nonce:
    :type nonce: bytes
    :param key:
    :type key: bytes
    :return: message
    :rtype: bytes
    """
    ensure(isinstance(ciphertext, bytes),
           'Input ciphertext type must be bytes',
           raising=exc.TypeError)

    clen = len(ciphertext)

    ensure(clen <= _aead_xchacha20poly1305_ietf_CRYPTBYTES_MAX,
           'Ciphertext must be at most {0} bytes long'.format(
               _aead_xchacha20poly1305_ietf_CRYPTBYTES_MAX),
           raising=exc.ValueError)

    ensure(isinstance(aad, bytes) or (aad is None),
           'Additional data must be bytes or None',
           raising=exc.TypeError)

    ensure(isinstance(nonce, bytes) and
           len(nonce) == crypto_aead_xchacha20poly1305_ietf_NPUBBYTES,
           'Nonce must be a {0} bytes long bytes sequence'.format(
               crypto_aead_xchacha20poly1305_ietf_NPUBBYTES),
           raising=exc.TypeError)

    ensure(isinstance(key, bytes) and
           len(key) == crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
           'Key must be a {0} bytes long bytes sequence'.format(
               crypto_aead_xchacha20poly1305_ietf_KEYBYTES),
           raising=exc.TypeError)

    mxout = clen - crypto_aead_xchacha20poly1305_ietf_ABYTES
    mlen = ffi.new("unsigned long long *")
    message = ffi.new("unsigned char[]", mxout)

    if aad:
        _aad = aad
        aalen = len(aad)
    else:
        _aad = ffi.NULL
        aalen = 0

    res = lib.crypto_aead_xchacha20poly1305_ietf_decrypt(message,
                                                         mlen,
                                                         ffi.NULL,
                                                         ciphertext,
                                                         clen,
                                                         _aad,
                                                         aalen,
                                                         nonce,
                                                         key)

    ensure(res == 0, "Decryption failed.", raising=exc.CryptoError)

    return ffi.buffer(message, mlen[0])[:]
