# Copyright 2018 by Kurt Griffiths
#
# 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 base64 import b64decode, b64encode

import msgpack
import nacl.encoding
import nacl.secret
import nacl.signing
import nacl.utils

from .version import __version__  # NOQA


class Signer:
    """Message signer based on Ed25519 and nacl.signing.

    Arguments:
        key (str): Base64-encoded key obtained from keygen()
    """

    __slots__ = ('_signing_key',)

    def __init__(self, skey):
        self._signing_key = nacl.signing.SigningKey(skey, nacl.encoding.Base64Encoder)

    @staticmethod
    def keygen():
        signing_key = nacl.signing.SigningKey.generate()

        return (
            signing_key.encode(nacl.encoding.Base64Encoder).decode(),
            signing_key.verify_key.encode(nacl.encoding.Base64Encoder).decode(),
        )

    @staticmethod
    def vkey(skey):
        signing_key = nacl.signing.SigningKey(skey, nacl.encoding.Base64Encoder)
        return signing_key.verify_key.encode(nacl.encoding.Base64Encoder)

    def signb(self, message):
        """Sign a binary message with its signature attached.

        Arguments:
            message(bytes): Data to sign.

        Returns:
            bytes: Signed message
        """
        return self._signing_key.sign(message)

    def pack(self, doc):
        return b64encode(self.packb(doc)).decode()

    def packb(self, doc):
        packed = msgpack.packb(doc, encoding='utf-8', use_bin_type=True)
        return self.signb(packed)


class Verifier:
    """Signature verifier based on Ed25519 and nacl.signing.

    Arguments:
        key (str): Base64-encoded verify key
    """

    __slots__ = ('_verify_key',)

    def __init__(self, vkey):
        self._verify_key = nacl.signing.VerifyKey(vkey, nacl.encoding.Base64Encoder)

    def verifyb(self, message):
        """Verify a signed binary message.

        Arguments:
            message(bytes): Data to verify.

        Returns:
            bytes: The orignal message, sans signature.
        """
        return self._verify_key.verify(message)

    def unpack(self, packed):
        return self.unpackb(b64decode(packed))

    def unpackb(self, packed):
        packed = self.verifyb(packed)
        return msgpack.unpackb(packed, raw=False, encoding='utf-8')


class BlackBox:
    """Encryption engine based on PyNaCl's SecretBox (Salsa20/Poly1305).

    Warning per the SecretBox docs:

        Once you’ve decrypted the message you’ve demonstrated the ability to
        create arbitrary valid messages, so messages you send are repudiable.
        For non-repudiable messages, sign them after encryption.

        (See also: https://pynacl.readthedocs.io/en/stable/signing)

    Arguments:
        key (str): Base64-encoded key obtained from keygen()
    """

    __slots__ = ('_box',)

    def __init__(self, key):
        self._box = nacl.secret.SecretBox(b64decode(key))

    @staticmethod
    def keygen():
        return b64encode(nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)).decode()

    def encrypt(self, doc, signer=None):
        """Serialize and encrypt a document to Base64-encoded ciphertext.

        Arguments:
            doc: The string, dict, array, or other JSON-compatible
                object to serialize and encrypt.

        Keyword Arguments:
            signer: An instance of Signer to use in signing the result. If
                not provided, the ciphertext is not signed.

        Returns:
            str: Ciphertext
        """

        data = msgpack.packb(doc, encoding='utf-8', use_bin_type=True)

        ciphertext = self._box.encrypt(data)
        if signer:
            ciphertext = signer.signb(ciphertext)

        return b64encode(ciphertext).decode()

    def decrypt(self, ciphertext, verifier=None):
        """Unpack Base64-encoded ciphertext.

        Arguments:
            ciphertext (bytes): Ciphertext to decrypt and deserialize.

        Keyword Arguments:
            verifier: An instance of Verifier to use in verifying the
                signed ciphertext. If not provided, the ciphertext is
                assumed to be unsigned.

        Returns:
            doc: Deserialized JSON-compatible object.
        """

        ciphertext = b64decode(ciphertext)
        if verifier:
            ciphertext = verifier.verifyb(ciphertext)

        data = self._box.decrypt(ciphertext)
        return msgpack.unpackb(data, raw=False, encoding='utf-8')

[((67, 17, 67, 72), 'msgpack.packb', 'msgpack.packb', (), '', False, 'import msgpack\n'), ((99, 15, 99, 67), 'msgpack.unpackb', 'msgpack.unpackb', (), '', False, 'import msgpack\n'), ((141, 15, 141, 70), 'msgpack.packb', 'msgpack.packb', (), '', False, 'import msgpack\n'), ((164, 21, 164, 42), 'base64.b64decode', 'b64decode', ({(164, 31, 164, 41): 'ciphertext'}, {}), '(ciphertext)', False, 'from base64 import b64decode, b64encode\n'), ((169, 15, 169, 65), 'msgpack.unpackb', 'msgpack.unpackb', (), '', False, 'import msgpack\n'), ((95, 28, 95, 45), 'base64.b64decode', 'b64decode', ({(95, 38, 95, 44): 'packed'}, {}), '(packed)', False, 'from base64 import b64decode, b64encode\n'), ((120, 42, 120, 56), 'base64.b64decode', 'b64decode', ({(120, 52, 120, 55): 'key'}, {}), '(key)', False, 'from base64 import b64decode, b64encode\n'), ((147, 15, 147, 36), 'base64.b64encode', 'b64encode', ({(147, 25, 147, 35): 'ciphertext'}, {}), '(ciphertext)', False, 'from base64 import b64decode, b64encode\n')]