class ClientAuthenticator:
    """Base class for client authenticators.

    A client authenticator class is a client-side implementation of a SASL
    mechanism. One `ClientAuthenticator` object may be used for one
    client authentication process.
    """
    __metaclass__ = ABCMeta
    def __init__(self):
        """Initialize a `ClientAuthenticator` object."""
        pass

    @abstractclassmethod
    def are_properties_sufficient(cls, properties):
        """Check if the provided properties are sufficient for
        this authentication mechanism.

        If `are_properties_sufficient` returns False for given `properties`
        mapping, the `start` method of `cls` instance will also fail with
        such argument.

        :Parameters:
            - `properties`: the `authentication properties`_
        :Types:
            - `properties`: mapping

        :Return: if the mechanism can be used with those properties
        """
        # pylint: disable=E0213,W0613,R0201
        return False

    @abstractmethod
    def start(self, properties):
        """Start the authentication process.

        :Parameters:
            - `properties`: the `authentication properties`_
        :Types:
            - `properties`: mapping

        :return: the initial response to send to the server or a failuer
            indicator.
        :returntype: `Response` or `Failure`
        """
        raise NotImplementedError

    @abstractmethod
    def challenge(self, challenge):
        """Process the server's challenge.

        :Parameters:
            - `challenge`: the challenge.
        :Types:
            - `challenge`: `bytes`

        :return: the response or a failure indicator.
        :returntype: `Response` or `Failure`"""
        raise NotImplementedError

    @abstractmethod
    def finish(self, data):
        """Handle authentication succes information from the server.

        :Parameters:
            - `data`: the optional additional data returned with the success.
        :Types:
            - `data`: `bytes`

        :return: success or failure indicator.
        :returntype: `Success` or `Failure`"""
        raise NotImplementedError class ServerAuthenticator:
    """Base class for server authenticators.

    A server authenticator class is a server-side implementation of a SASL
    mechanism. One `ServerAuthenticator` object may be used for one
    client authentication process.
    """
    __metaclass__ = ABCMeta
    def __init__(self, password_database):
        """Initialize a `ServerAuthenticator` object.

        :Parameters:
            - `password_database`: a password database
        :Types:
            - `password_database`: `PasswordDataBase`
        """
        self.password_database = password_database

    @classmethod
    def are_properties_sufficient(cls, properties):
        """Check if the provided properties are sufficient for
        this authentication mechanism.

        If `are_properties_sufficient` returns False for given `properties`
        mapping, the `start` method of `cls` instance will also fail with
        such argument.

        :Parameters:
            - `properties`: the `authentication properties`_
        :Types:
            - `properties`: mapping

        :Return: if the mechanism can be used with those properties
        """
        # pylint: disable=E0213,W0613,R0201
        return True

    @abstractmethod
    def start(self, properties, initial_response):
        """Start the authentication process.

        :Parameters:
            - `properties`: the `authentication properties`_
            - `initial_response`: the initial response send by the client with
              the authentication request.

        :Types:
            - `properties`: mapping
            - `initial_response`: `bytes`

        :return: a challenge, a success or a failure indicator.
        :returntype: `Challenge` or `Failure` or `Success`"""
        raise NotImplementedError

    @abstractmethod
    def response(self, response):
        """Process a response from a client.

        :Parameters:
            - `response`: the response from the client to our challenge.
        :Types:
            - `response`: `bytes`

        :return: a challenge, a success or a failure indicator.
        :returntype: `Challenge` or `Success` or `Failure`"""
        raise NotImplementedError class Failure(Reply):
    """The failure SASL message.

    :Ivariables:
        - `reason`: the failure reason.
    :Types:
        - `reason`: `unicode`.
    """
    # pylint: disable-msg=R0903
    def __init__(self, reason):
        """Initialize the `Failure` object.

        :Parameters:
            - `reason`: the failure reason.
        :Types:
            - `reason`: `unicode`.
        """
        Reply.__init__(self, None)
        self.reason = reason
    def __repr__(self):
        return "<sasl.Failure: {0!r}>".format(self.reason) class Response(Reply):
    """The response SASL message (clients's reply the server's
    challenge)."""
    # pylint: disable-msg=R0903
    def __init__(self, data):
        """Initialize the `Response` object."""
        Reply.__init__(self, data)
    def __repr__(self):
        return "<sasl.Response: {0!r}>".format(self.data) class Challenge(Reply):
    """The challenge SASL message (server's challenge for the client)."""
    # pylint: disable-msg=R0903
    def __init__(self, data):
        """Initialize the `Challenge` object."""
        Reply.__init__(self, data)
    def __repr__(self):
        return "<sasl.Challenge: {0!r}>".format(self.data) class Success(Reply):
    """The success SASL message (sent by the server on authentication
    success).
    """
    # pylint: disable-msg=R0903
    def __init__(self, properties = None, data = None):
        """Initialize the `Success` object.

        :Parameters:
            - `properties`: the `authentication properties`_ obtained
            - `data`: the success data to be sent to the client
        :Types:
            - `properties`: mapping
            - `data`: `bytes`
        """
        # pylint: disable-msg=R0913
        Reply.__init__(self, data)
        if properties:
            self.properties = properties
        else:
            self.properties = {}

    def __repr__(self):
        return "<sasl.Success: {0!r} data: {1!r}>".format(
                                                    self.properties, self.data) class Failure(Reply):
    """The failure SASL message.

    :Ivariables:
        - `reason`: the failure reason.
    :Types:
        - `reason`: `unicode`.
    """
    # pylint: disable-msg=R0903
    def __init__(self, reason):
        """Initialize the `Failure` object.

        :Parameters:
            - `reason`: the failure reason.
        :Types:
            - `reason`: `unicode`.
        """
        Reply.__init__(self, None)
        self.reason = reason
    def __repr__(self):
        return "<sasl.Failure: {0!r}>".format(self.reason) def sasl_mechanism(name, secure, preference = 50):
    """Class decorator generator for `ClientAuthenticator` or
    `ServerAuthenticator` subclasses. Adds the class to the pyxmpp.sasl
    mechanism registry.

    :Parameters:
        - `name`: SASL mechanism name
        - `secure`: if the mechanims can be considered secure - `True`
          if it can be used over plain-text channel
        - `preference`: mechanism preference level (the higher the better)
    :Types:
        - `name`: `unicode`
        - `secure`: `bool`
        - `preference`: `int`
    """
    # pylint: disable-msg=W0212
    def decorator(klass):
        """The decorator."""
        klass._pyxmpp_sasl_secure = secure
        klass._pyxmpp_sasl_preference = preference
        if issubclass(klass, ClientAuthenticator):
            _register_client_authenticator(klass, name)
        elif issubclass(klass, ServerAuthenticator):
            _register_server_authenticator(klass, name)
        else:
            raise TypeError("Not a ClientAuthenticator"
                                            " or ServerAuthenticator class")
        return klass
    return decorator def default_nonce_factory():
    """Generate a random string for digest authentication challenges.

    The string should be cryptographicaly secure random pattern.

    :return: the string generated.
    :returntype: `bytes`
    """
    return uuid.uuid4().hex.encode("us-ascii") SASLPREP = Profile(
    unassigned = (stringprep.in_table_a1,),
    mapping = (b1_mapping, c12_mapping),
    normalization = nfkc,
    prohibited = (  stringprep.in_table_c12, stringprep.in_table_c21,
                    stringprep.in_table_c22, stringprep.in_table_c3,
                    stringprep.in_table_c4, stringprep.in_table_c5,
                    stringprep.in_table_c6, stringprep.in_table_c7,
                    stringprep.in_table_c8, stringprep.in_table_c9 ),
    bidi = True)
import sys
import re
import logging
import hashlib
import hmac
from binascii import a2b_base64
from base64 import standard_b64encode
from .core import ClientAuthenticator, ServerAuthenticator
from .core import Failure, Response, Challenge, Success, Failure
from .core import sasl_mechanism, default_nonce_factory
from .saslprep import SASLPREP
        self._server_first_message = server_first_message
        return Challenge(server_first_message)

    def _handle_final_response(self, response):
        match = CLIENT_FINAL_MESSAGE_RE.match(response)
        if not match:
            logger.debug("Bad response syntax: {0!r}".format(response))
            return Failure("not-authorized")
        if match.group("nonce") != self._nonce:
            logger.debug("Bad nonce in the final client response")
            return Failure("not-authorized")
        cb_input = a2b_base64(match.group("cb"))
        if not cb_input.startswith(self._gs2_header):
            logger.debug("GS2 header in the final response ({0!r}) doesn't"
                    " match the one sent in the first message ({1!r})"
                                        .format(cb_input, self._gs2_header))
            return Failure("not-authorized")
        if self._cb_name:
            cb_data = cb_input[len(self._gs2_header):]
            if cb_data != self.properties["channel-binding"][self._cb_name]:
                logger.debug("Channel binding data doesn't match")
                return Failure("not-authorized")

        proof = a2b_base64(match.group("proof"))

        auth_message = (self._client_first_message_bare + b"," +
                                    self._server_first_message + b"," +
                                        match.group("without_proof"))
        if self._stored_key is None:
            # compute something to prevent timing attack
            client_signature = self.HMAC(b"", auth_message)
            client_key = self.XOR(client_signature, proof)
            self.H(client_key)
            logger.debug("Authentication failed (bad username)")
            return Failure("not-authorized")

        client_signature = self.HMAC(self._stored_key, auth_message)
        client_key = self.XOR(client_signature, proof)
        if self.H(client_key) != self._stored_key:
            logger.debug("Authentication failed")
            return Failure("not-authorized")

        server_signature = self.HMAC(self._server_key, auth_message)
        server_final_message = b"v=" + standard_b64encode(server_signature)
        return Success(self.out_properties, server_final_message)

@sasl_mechanism("SCRAM-SHA-1", 80)
class SCRAM_SHA_1_ClientAuthenticator(SCRAMClientAuthenticator):
    """The SCRAM-SHA-1 client authenticator.

    Authentication properties used:

        - ``"username"`` - user name (required)
        - ``"authzid"`` - authorization id (optional)
        - ``"enabled_mechanisms"`` - list of mechanism enabled on the client.
          Used to detect when an attacker removes the -PLUS version from the
          list of mechanism supported by the server.

    Authentication properties returned:

        - ``"username"`` - user name
        - ``"authzid"`` - authorization id
    """
    # pylint: disable=C0103
    def __init__(self):
        SCRAMClientAuthenticator.__init__(self, "SHA-1", False)

@sasl_mechanism("SCRAM-SHA-1-PLUS", 90)
class SCRAM_SHA_1_PLUS_ClientAuthenticator(SCRAMClientAuthenticator):
    """The SCRAM-SHA-1-PLUS client authenticator.

    Authentication properties used: same as for
    `SCRAM_SHA_1_ClientAuthenticator`, plus:

        - ``"channel-binding"`` - channel-binding data, as a dictionary
          channel-binding-type (`unicode`) -> channel-binding data(`bytes`).
          Channel binding type should be 'tls-unique', as other may be not
          supported by the other side.

    Authentication properties returned: same as for
    `SCRAM_SHA_1_ClientAuthenticator`

    """
    # pylint: disable=C0103
    def __init__(self):
        SCRAMClientAuthenticator.__init__(self, "SHA-1", True)
    @classmethod
    def are_properties_sufficient(cls, properties):
        ret = super(SCRAM_SHA_1_PLUS_ClientAuthenticator, cls
                                ).are_properties_sufficient(properties)
        if not ret:
            return False
        return bool(properties.get("channel-binding"))

@sasl_mechanism("SCRAM-SHA-1", 80)
class SCRAM_SHA_1_ServerAuthenticator(SCRAMServerAuthenticator):
    """The SCRAM-SHA-1 server authenticator.

    Authentication properties used:

        - ``"enabled_mechanisms"`` - list of mechanism enabled on the server.
          Used to detect when an attacker removes the -PLUS version from the
          list of mechanism supported by the server while it is sent to the
          client.
        - ``"SCRAM-salt"`` - salt to be applied on a plain text password
          (default: a random string)
        - ``"SCRAM-iteration-count"`` - iteration-count parameter for hashing
          a plain text password (default: 4096)

    Authentication properties returned:

        - ``"username"`` - user name
        - ``"authzid"`` - authorization id

    """
    # pylint: disable=C0103
    def __init__(self, password_database):
        SCRAMServerAuthenticator.__init__(self, "SHA-1", False,
                                                            password_database)


@sasl_mechanism("SCRAM-SHA-1-PLUS", 90)