Philipp Hörist pushed to branch master at gajim / python-nbxmpp
Commits: f2a20338 by Philipp Hörist at 2018-12-09T17:43:48Z Rewrite auth module from scratch
- Move NonBlockingBind to own module - Add SCRAM-SHA-256 and SCRAM-SHA-256-PLUS support - Remove DIGEST-MD5 and X-MESSENGER-OAUTH2 support
- - - - -
9 changed files:
- nbxmpp/__init__.py - + nbxmpp/auth.py - − nbxmpp/auth_nb.py - + nbxmpp/bind.py - nbxmpp/client_nb.py - nbxmpp/const.py - nbxmpp/protocol.py - + nbxmpp/util.py - + test/unit/test_sasl_scram.py
Changes:
===================================== nbxmpp/__init__.py ===================================== @@ -9,7 +9,7 @@ Thanks and credits to the xmpppy developers. See: http://xmpppy.sourceforge.net/ """
from .protocol import * -from . import simplexml, protocol, auth_nb, transports_nb, roster_nb +from . import simplexml, protocol, auth, transports_nb, roster_nb from . import dispatcher_nb, features_nb, idlequeue, bosh, tls_nb, proxy_connectors from .client_nb import NonBlockingClient from .plugin import PlugIn
===================================== nbxmpp/auth.py ===================================== @@ -0,0 +1,473 @@ +# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> +# +# This file is part of nbxmpp. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; If not, see http://www.gnu.org/licenses/. + +import os +import hmac +import binascii +import logging +import hashlib +from hashlib import pbkdf2_hmac +from functools import partial + +from nbxmpp.plugin import PlugIn +from nbxmpp.protocol import NS_SASL +from nbxmpp.protocol import Node +from nbxmpp.protocol import NodeProcessed +from nbxmpp.protocol import SASL_ERROR_CONDITIONS +from nbxmpp.protocol import SASL_AUTH_MECHS +from nbxmpp.util import b64decode +from nbxmpp.util import b64encode +from nbxmpp.const import GSSAPIState + + +log = logging.getLogger('nbxmpp.auth') + +try: + kerberos = __import__('kerberos') + KERBEROS_AVAILABLE = True +except ImportError: + KERBEROS_AVAILABLE = False + + +class SASL(PlugIn): + """ + Implements SASL authentication. Can be plugged into NonBlockingClient + to start authentication + """ + + _default_mechs = set(['SCRAM-SHA-256-PLUS', + 'SCRAM-SHA-256', + 'SCRAM-SHA-1-PLUS', + 'SCRAM-SHA-1', + 'PLAIN']) + + def __init__(self, username, auth_mechs, get_password, on_finished): + """ + :param username: XMPP username + :param auth_mechs: Set of valid authentication mechanisms. + Possible entries are: + 'ANONYMOUS', 'EXTERNAL', 'GSSAPI', 'SCRAM-SHA-1-PLUS', + 'SCRAM-SHA-1', 'SCRAM-SHA-256', 'SCRAM-SHA-256-PLUS', 'PLAIN' + :param on_finished: Callback after SASL is finished + :param get_password: Callback that must return the password for the + chosen mechanism + """ + PlugIn.__init__(self) + self.username = username + self._on_finished = on_finished + self._get_password = get_password + + self._prefered_mechs = auth_mechs + self._enabled_mechs = self._prefered_mechs or self._default_mechs + self._chosen_mechanism = None + self._method = None + + self._channel_binding = None + + def _setup_mechs(self): + if self._owner.connected in ('ssl', 'tls'): + if self._owner.protocol_type == 'BOSH': + # PLUS would break if the server uses any kind of reverse proxy + self._enabled_mechs.discard('SCRAM-SHA-1-PLUS') + self._enabled_mechs.discard('SCRAM-SHA-256-PLUS') + else: + self._channel_binding = self._owner.Connection.NonBlockingTLS.get_channel_binding() + # TLS handshake is finished so channel binding data muss exist + if self._channel_binding is None: + raise ValueError('No channel binding data found') + + else: + self._enabled_mechs.discard('SCRAM-SHA-1-PLUS') + self._enabled_mechs.discard('SCRAM-SHA-256-PLUS') + if self._prefered_mechs is None: + # if the client didnt specify any auth mechs avoid + # sending the password over a plain connection + self._enabled_mechs.discard('PLAIN') + + if not KERBEROS_AVAILABLE: + self._enabled_mechs.discard('GSSAPI') + + def plugin(self, _owner): + self._setup_mechs() + self._owner.RegisterHandler( + 'challenge', self._on_challenge, xmlns=NS_SASL) + self._owner.RegisterHandler( + 'failure', self._on_failure, xmlns=NS_SASL) + self._owner.RegisterHandler( + 'success', self._on_success, xmlns=NS_SASL) + + # Execute the Handler manually, we already received the features + self._on_features(None, self._owner.Dispatcher.Stream.features) + + def plugout(self): + """ + Remove SASL handlers from owner's dispatcher. Used internally + """ + self._owner.UnregisterHandler( + 'challenge', self._on_challenge, xmlns=NS_SASL) + self._owner.UnregisterHandler( + 'failure', self._on_failure, xmlns=NS_SASL) + self._owner.UnregisterHandler( + 'success', self._on_success, xmlns=NS_SASL) + + def _on_features(self, _con, stanza): + """ + Used to determine if server supports SASL auth. Used internally + """ + if not stanza.getTag('mechanisms', namespace=NS_SASL): + return + + mechanisms = stanza.getTag('mechanisms', namespace=NS_SASL) + mechanisms = mechanisms.getTags('mechanism') + + mechs = set(mech.getData() for mech in mechanisms) + available_mechs = mechs & self._enabled_mechs + + log.info('Available mechanisms: %s', available_mechs) + + if not available_mechs: + log.error('No available auth mechanisms found') + self._abort_auth('invalid-mechanism') + return + + for mech in SASL_AUTH_MECHS: + if mech in available_mechs: + self._chosen_mechanism = mech + break + + if self._chosen_mechanism is None: + log.error('No available auth mechanisms found') + self._abort_auth('invalid-mechanism') + return + + log.info('Chosen auth mechanism: %s', self._chosen_mechanism) + self._auth() + + def _auth(self): + password_cb = partial(self._on_password, self.username) + + if self._chosen_mechanism == 'SCRAM-SHA-256-PLUS': + self._method = SCRAM_SHA_256_PLUS(self._owner.Connection, + self._channel_binding) + self._get_password(self._chosen_mechanism, password_cb) + + elif self._chosen_mechanism == 'SCRAM-SHA-256': + self._method = SCRAM_SHA_256(self._owner.Connection, None) + self._get_password(self._chosen_mechanism, password_cb) + + elif self._chosen_mechanism == 'SCRAM-SHA-1-PLUS': + self._method = SCRAM_SHA_1_PLUS(self._owner.Connection, + self._channel_binding) + self._get_password(self._chosen_mechanism, password_cb) + + elif self._chosen_mechanism == 'SCRAM-SHA-1': + self._method = SCRAM_SHA_1(self._owner.Connection, None) + self._get_password(self._chosen_mechanism, password_cb) + + elif self._chosen_mechanism == 'PLAIN': + self._method = PLAIN(self._owner.Connection) + self._get_password(self._chosen_mechanism, password_cb) + + elif self._chosen_mechanism == 'ANONYMOUS': + self._method = ANONYMOUS(self._owner.Connection) + self._method.initiate() + + elif self._chosen_mechanism == 'EXTERNAL': + self._method = EXTERNAL(self._owner.Connection) + self._method.initiate(self.username, self._owner.Server) + + elif self._chosen_mechanism == 'GSSAPI': + self._method = GSSAPI(self._owner.Connection) + self._method.initiate(self._owner.xmpp_hostname) + + else: + log.error('Unknown auth mech') + + def _on_password(self, username, password): + if password is None: + log.warning('No password supplied') + return + self._method.initiate(username, password) + + def _on_challenge(self, _con, stanza): + try: + self._method.response(stanza.getData()) + except AttributeError: + log.info('Mechanism has no response method') + self._abort_auth() + except AuthFail as error: + log.error(error) + self._abort_auth() + raise NodeProcessed + + def _on_success(self, _con, stanza): + log.info('Successfully authenticated with remote server') + try: + self._method.success(stanza.getData()) + except AttributeError: + pass + except AuthFail as error: + log.error(error) + self._abort_auth() + raise NodeProcessed + + self._on_finished(True, None, None) + raise NodeProcessed + + def _on_failure(self, _con, stanza): + text = stanza.getTagData('text') + reason = 'not-authorized' + childs = stanza.getChildren() + for child in childs: + name = child.getName() + if name == 'text': + continue + if name in SASL_ERROR_CONDITIONS: + reason = name + break + + log.info('Failed SASL authentification: %s %s', reason, text) + self._abort_auth(reason, text) + raise NodeProcessed + + def _abort_auth(self, reason='malformed-request', text=None): + node = Node('abort', attrs={'xmlns': NS_SASL}) + self._owner.send(node) + self._owner.Connection.start_disconnect() + self._on_finished(False, reason, text) + + +class PLAIN: + + _mechanism = 'PLAIN' + + def __init__(self, con): + self._con = con + + def initiate(self, username, password): + payload = b64encode('\x00%s\x00%s' % (username, password)) + node = Node('auth', + attrs={'xmlns': NS_SASL, 'mechanism': 'PLAIN'}, + payload=[payload]) + self._con.send(node) + + +class EXTERNAL: + + _mechanism = 'EXTERNAL' + + def __init__(self, con): + self._con = con + + def initiate(self, username, server): + payload = b64encode('%s@%s' % (username, server)) + node = Node('auth', + attrs={'xmlns': NS_SASL, 'mechanism': 'EXTERNAL'}, + payload=[payload]) + self._con.send(node) + + +class ANONYMOUS: + + _mechanism = 'ANONYMOUS' + + def __init__(self, con): + self._con = con + + def initiate(self): + node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'ANONYMOUS'}) + self._con.send(node) + + +class GSSAPI: + + _mechanism = 'GSSAPI' + + def __init__(self, con): + self._con = con + self._gss_vc = None + self._state = GSSAPIState.STEP + + def initiate(self, hostname): + self._gss_vc = kerberos.authGSSClientInit('xmpp@%s' % hostname)[1] + kerberos.authGSSClientStep(self._gss_vc, '') + response = kerberos.authGSSClientResponse(self._gss_vc) + node = Node('auth', + attrs={'xmlns': NS_SASL, 'mechanism': 'GSSAPI'}, + payload=(response or '')) + self._con.send(node) + + def response(self, server_message, *args, **kwargs): + server_message = b64decode(server_message, bytes) + if self._state == GSSAPIState.STEP: + rc = kerberos.authGSSClientStep(self._gss_vc, server_message) + if rc != kerberos.AUTH_GSS_CONTINUE: + self._state = GSSAPIState.WRAP + elif self._state == GSSAPIState.WRAP: + rc = kerberos.authGSSClientUnwrap(self._gss_vc, server_message) + response = kerberos.authGSSClientResponse(self._gss_vc) + rc = kerberos.authGSSClientWrap( + self._gss_vc, + response, + kerberos.authGSSClientUserName(self._gss_vc)) + response = kerberos.authGSSClientResponse(self._gss_vc) + if not response: + response = '' + + node = Node('response', + attrs={'xmlns': NS_SASL}, + payload=response) + self._con.send(node) + + +class SCRAM: + + _mechanism = '' + _channel_binding = '' + _hash_method = '' + + def __init__(self, con, channel_binding): + self._con = con + self._channel_binding_data = channel_binding + self._client_nonce = '%x' % int(binascii.hexlify(os.urandom(24)), 16) + self._client_first_message_bare = None + self._server_signature = None + self._password = None + + @property + def nonce_length(self): + return len(self._client_nonce) + + @property + def _b64_channel_binding_data(self): + if self._mechanism.endswith('PLUS'): + return b64encode(b'%s%s' % (self._channel_binding.encode(), + self._channel_binding_data)) + return b64encode(self._channel_binding) + + @staticmethod + def _scram_parse(scram_data): + return dict(s.split('=', 1) for s in scram_data.split(',')) + + def initiate(self, username, password): + self._password = password + self._client_first_message_bare = 'n=%s,r=%s' % (username, + self._client_nonce) + client_first_message = '%s%s' % (self._channel_binding, + self._client_first_message_bare) + payload = b64encode(client_first_message) + node = Node('auth', + attrs={'xmlns': NS_SASL, 'mechanism': self._mechanism}, + payload=[payload]) + self._con.send(node) + + def response(self, server_first_message): + server_first_message = b64decode(server_first_message) + challenge = self._scram_parse(server_first_message) + + client_nonce = challenge['r'][:self.nonce_length] + if client_nonce != self._client_nonce: + raise AuthFail('Invalid client nonce received from server') + + salt = b64decode(challenge['s'], bytes) + iteration_count = int(challenge['i']) + + if iteration_count < 4096: + raise AuthFail('Salt iteration count to low: %s' % iteration_count) + + salted_password = pbkdf2_hmac(self._hash_method, + self._password.encode('utf8'), + salt, + iteration_count) + + client_final_message_wo_proof = 'c=%s,r=%s' % ( + self._b64_channel_binding_data, + challenge['r'] + ) + + client_key = self._hmac(salted_password, 'Client Key') + stored_key = self._h(client_key) + auth_message = '%s,%s,%s' % (self._client_first_message_bare, + server_first_message, + client_final_message_wo_proof) + client_signature = self._hmac(stored_key, auth_message) + client_proof = self._xor(client_key, client_signature) + + client_finale_message = 'c=%s,r=%s,p=%s' % ( + self._b64_channel_binding_data, + challenge['r'], + b64encode(client_proof) + ) + + server_key = self._hmac(salted_password, 'Server Key') + self._server_signature = self._hmac(server_key, auth_message) + + log.debug('Response: %s', client_finale_message) + payload = b64encode(client_finale_message) + node = Node('response', + attrs={'xmlns': NS_SASL}, + payload=[payload]) + self._con.send(node) + + def success(self, server_last_message): + server_last_message = b64decode(server_last_message) + success = self._scram_parse(server_last_message) + server_signature = b64decode(success['v'], bytes) + if server_signature != self._server_signature: + raise AuthFail('Invalid server signature') + + def _hmac(self, key, message): + return hmac.new(key=key, + msg=message.encode(), + digestmod=self._hash_method).digest() + + @staticmethod + def _xor(x, y): + return bytes([px ^ py for px, py in zip(x, y)]) + + def _h(self, data): + return hashlib.new(self._hash_method, data).digest() + + +class SCRAM_SHA_1(SCRAM): + + _mechanism = 'SCRAM-SHA-1' + _channel_binding = 'y,,' + _hash_method = 'sha1' + + +class SCRAM_SHA_1_PLUS(SCRAM_SHA_1): + + _mechanism = 'SCRAM-SHA-1-PLUS' + _channel_binding = 'p=tls-unique,,' + + +class SCRAM_SHA_256(SCRAM): + + _mechanism = 'SCRAM-SHA-256' + _channel_binding = 'y,,' + _hash_method = 'sha256' + + +class SCRAM_SHA_256_PLUS(SCRAM_SHA_256): + + _mechanism = 'SCRAM-SHA-256-PLUS' + _channel_binding = 'p=tls-unique,,' + + +class AuthFail(Exception): + pass
===================================== nbxmpp/auth_nb.py deleted ===================================== @@ -1,658 +0,0 @@ -## auth_nb.py -## based on auth.py, changes backported up to revision 1.41 -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## modified by Dimitur Kirov dkirov@gmail.com -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -""" -Provides plugs for SASL and NON-SASL authentication mechanisms. -Can be used both for client and transport authentication - -See client_nb.py -""" - -import re -import os -import binascii -import base64 -import hmac -import hashlib -import logging - -from .protocol import NS_SASL, NS_SESSION, NS_STREAMS, NS_BIND -from .protocol import NS_STREAM_MGMT -from .protocol import Node, NodeProcessed, isResultNode, Protocol -from .protocol import SASL_ERROR_CONDITIONS -from .plugin import PlugIn -from .const import Realm -from .const import Event - -from . import dispatcher_nb - -log = logging.getLogger('nbxmpp.auth_nb') - -def HH(some): return hashlib.md5(some).hexdigest() -def H(some): return hashlib.md5(some).digest() -def C(some): return b':'.join(some) - -try: - kerberos = __import__('kerberos') - have_kerberos = True -except ImportError: - have_kerberos = False - -GSS_STATE_STEP = 0 -GSS_STATE_WRAP = 1 -SASL_FAILURE_IN_PROGRESS = 'failure-in-process' -SASL_FAILURE = 'failure' -SASL_SUCCESS = 'success' -SASL_UNSUPPORTED = 'not-supported' -SASL_IN_PROCESS = 'in-process' - -# compile the search regex for _challenge_splitter -_challenge_regex = re.compile( - r'(\w+)=' # keyword - r'("[^"]+"|[^,]+)' # value - r',?', # optional comma separator - re.VERBOSE -) - -def _challenge_splitter(data): - """ - Helper function that creates a dict from challenge string. Used - for DIGEST-MD5 authentication. - - Sample challenge string: - - username="example.org",realm="somerealm", - nonce="OA6MG9tEQGm2hh",cnonce="OA6MHXh6VqTrRk", - nc=00000001,qop="auth,auth-int,auth-conf",charset=utf-8 - - Expected result for challan: - - dict['qop'] = ('auth','auth-int','auth-conf') - - dict['realm'] = 'somerealm' - """ - dict_ = {} - for match in _challenge_regex.finditer(data): - k = match.group(1) - v = match.group(2) - if v.startswith('"'): - v = v[1:-1] # Remove quote - if v.find(',') >= 0: - v = v.split(',') # Split using comma - dict_[k] = v - return dict_ - -def _scram_parse(chatter): - """Helper function. Used for SCRAM-SHA-1, SCRAM-SHA-1-PLUS authentication""" - return dict(s.split('=', 1) for s in chatter.split(',')) - -SASL_AUTHENTICATION_MECHANISMS = \ - set(['ANONYMOUS', 'EXTERNAL', 'GSSAPI', 'SCRAM-SHA-1-PLUS', 'SCRAM-SHA-1', - 'PLAIN', 'X-MESSENGER-OAUTH2']) - -class SASL(PlugIn): - """ - Implements SASL authentication. Can be plugged into NonBlockingClient - to start authentication - """ - - def __init__(self, username, password, on_sasl, channel_binding, - auth_mechs): - """ - :param username: XMPP username - :param password: XMPP password - :param on_sasl: Callback, will be called after each SASL auth-step. - :param channel_binding: TLS channel binding data, None if the - binding data is not available - :param auth_mechs: Set of valid authentication mechanisms. - Possible entries are: - 'ANONYMOUS', 'EXTERNAL', 'GSSAPI', 'SCRAM-SHA-1-PLUS', - 'SCRAM-SHA-1', 'DIGEST-MD5', 'PLAIN', 'X-MESSENGER-OAUTH2' - """ - PlugIn.__init__(self) - self.username = username - self.password = password - self.on_sasl = on_sasl - self.channel_binding = channel_binding - self.enabled_auth_mechs = auth_mechs - self.realm = None - - def plugin(self, owner): - if 'version' not in self._owner.Dispatcher.Stream._document_attrs: - self.startsasl = SASL_UNSUPPORTED - elif self._owner.Dispatcher.Stream.features: - try: - self.FeaturesHandler(self._owner.Dispatcher, - self._owner.Dispatcher.Stream.features) - except NodeProcessed: - pass - else: - self.startsasl = None - - def plugout(self): - """ - Remove SASL handlers from owner's dispatcher. Used internally - """ - if 'features' in self._owner.__dict__: - self._owner.UnregisterHandler('features', self.FeaturesHandler, - xmlns=NS_STREAMS) - if 'challenge' in self._owner.__dict__: - self._owner.UnregisterHandler('challenge', self.SASLHandler, - xmlns=NS_SASL) - if 'failure' in self._owner.__dict__: - self._owner.UnregisterHandler('failure', self.SASLHandler, - xmlns=NS_SASL) - if 'success' in self._owner.__dict__: - self._owner.UnregisterHandler('success', self.SASLHandler, - xmlns=NS_SASL) - - def auth(self): - """ - Start authentication. Result can be obtained via "SASL.startsasl" - attribute and will be either SASL_SUCCESS or SASL_FAILURE - - Note that successful auth will take at least two Dispatcher.Process() - calls. - """ - if self.startsasl: - pass - elif self._owner.Dispatcher.Stream.features: - try: - self.FeaturesHandler(self._owner.Dispatcher, - self._owner.Dispatcher.Stream.features) - except NodeProcessed: - pass - else: - self._owner.RegisterHandler('features', - self.FeaturesHandler, xmlns=NS_STREAMS) - - def FeaturesHandler(self, conn, feats): - """ - Used to determine if server supports SASL auth. Used internally - """ - if not feats.getTag('mechanisms', namespace=NS_SASL): - self.startsasl='not-supported' - log.info('SASL not supported by server') - return - - self.mecs = set( - mec.getData() - for mec - in feats.getTag('mechanisms', namespace=NS_SASL).getTags('mechanism') - ) & self.enabled_auth_mechs - - # Password based authentication mechanism ordered by strength. - # If the server supports a mechanism disable all weaker mechanisms. - password_auth_mechs_strength = ['SCRAM-SHA-1-PLUS', 'SCRAM-SHA-1', - 'DIGEST-MD5', 'PLAIN', 'X-MESSENGER-OAUTH2'] - if self.channel_binding is None: - password_auth_mechs_strength.remove('SCRAM-SHA-1-PLUS') - for i in range(0, len(password_auth_mechs_strength)): - if password_auth_mechs_strength[i] in self.mecs: - for m in password_auth_mechs_strength[i + 1:]: - self.mecs.discard(m) - break - - self._owner.RegisterHandler('challenge', self.SASLHandler, - xmlns=NS_SASL) - self._owner.RegisterHandler('failure', self.SASLHandler, xmlns=NS_SASL) - self._owner.RegisterHandler('success', self.SASLHandler, xmlns=NS_SASL) - self.MechanismHandler() - - def MechanismHandler(self): - if 'ANONYMOUS' in self.mecs and self.username is None: - self.mecs.remove('ANONYMOUS') - node = Node('auth', attrs={'xmlns': NS_SASL, - 'mechanism': 'ANONYMOUS'}) - self.mechanism = 'ANONYMOUS' - self.startsasl = SASL_IN_PROCESS - self._owner.send(str(node)) - raise NodeProcessed - if "EXTERNAL" in self.mecs: - self.mecs.remove('EXTERNAL') - sasl_data = '%s@%s' % (self.username, self._owner.Server) - sasl_data = base64.b64encode(sasl_data.encode('utf-8')).decode( - 'utf-8').replace('\n', '') - node = Node('auth', attrs={'xmlns': NS_SASL, - 'mechanism': 'EXTERNAL'}, payload=[sasl_data]) - self.mechanism = 'EXTERNAL' - self.startsasl = SASL_IN_PROCESS - self._owner.send(str(node)) - raise NodeProcessed - if 'GSSAPI' in self.mecs and have_kerberos: - self.mecs.remove('GSSAPI') - try: - self.gss_vc = kerberos.authGSSClientInit('xmpp@' + \ - self._owner.xmpp_hostname)[1] - kerberos.authGSSClientStep(self.gss_vc, '') - response = kerberos.authGSSClientResponse(self.gss_vc) - node=Node('auth', attrs={'xmlns': NS_SASL, - 'mechanism': 'GSSAPI'}, payload=(response or '')) - self.mechanism = 'GSSAPI' - self.gss_step = GSS_STATE_STEP - self.startsasl = SASL_IN_PROCESS - self._owner.send(str(node)) - raise NodeProcessed - except kerberos.GSSError as e: - log.info('GSSAPI authentication failed: %s' % str(e)) - if 'SCRAM-SHA-1-PLUS' in self.mecs and self.channel_binding is not None: - self.mecs.remove('SCRAM-SHA-1-PLUS') - self.mechanism = 'SCRAM-SHA-1-PLUS' - self._owner._caller.get_password(self.set_password, self.mechanism) - self.scram_step = 0 - self.startsasl = SASL_IN_PROCESS - raise NodeProcessed - if 'SCRAM-SHA-1' in self.mecs: - self.mecs.remove('SCRAM-SHA-1') - self.mechanism = 'SCRAM-SHA-1' - self._owner._caller.get_password(self.set_password, self.mechanism) - self.scram_step = 0 - self.startsasl = SASL_IN_PROCESS - raise NodeProcessed - if 'DIGEST-MD5' in self.mecs: - self.mecs.remove('DIGEST-MD5') - node = Node('auth', attrs={'xmlns': NS_SASL, - 'mechanism': 'DIGEST-MD5'}) - self.mechanism = 'DIGEST-MD5' - self.startsasl = SASL_IN_PROCESS - self._owner.send(str(node)) - raise NodeProcessed - if 'PLAIN' in self.mecs: - self.mecs.remove('PLAIN') - self.mechanism = 'PLAIN' - self._owner._caller.get_password(self.set_password, self.mechanism) - self.startsasl = SASL_IN_PROCESS - raise NodeProcessed - if 'X-MESSENGER-OAUTH2' in self.mecs: - self.mecs.remove('X-MESSENGER-OAUTH2') - self.mechanism = 'X-MESSENGER-OAUTH2' - self._owner._caller.get_password(self.set_password, self.mechanism) - self.startsasl = SASL_IN_PROCESS - raise NodeProcessed - self.startsasl = SASL_FAILURE - log.info('I can only use ANONYMOUS, EXTERNAL, GSSAPI, SCRAM-SHA-1-PLUS,' - ' SCRAM-SHA-1, DIGEST-MD5, PLAIN and X-MESSENGER-OAUTH2' - ' mechanisms.') - if self.on_sasl: - self.on_sasl() - return - - def SASLHandler(self, conn, challenge): - """ - Perform next SASL auth step. Used internally - """ - if challenge.getNamespace() != NS_SASL: - return - - def scram_base64(s): - try: - s = s.encode('utf-8') - except: - pass - return ''.join(base64.b64encode(s).decode('utf-8').\ - split('\n')) - - incoming_data = challenge.getData() - data=base64.b64decode(incoming_data.encode('utf-8')) - - if self.mechanism != 'GSSAPI': - data=data.decode('utf-8') - - ### Handle Auth result - def on_auth_fail(reason, text=None): - log.info('Failed SASL authentification: %s %s', reason, text) - self._owner.send(str(Node('abort', attrs={'xmlns': NS_SASL}))) - if len(self.mecs) > 0: - # There are other mechanisms to test, but wait for <failure> - # answer from server - self.startsasl = SASL_FAILURE_IN_PROGRESS - raise NodeProcessed - if self.on_sasl: - self.on_sasl((reason, text)) - raise NodeProcessed - - if challenge.getName() == 'failure': - if self.startsasl == SASL_FAILURE_IN_PROGRESS: - self.MechanismHandler() - raise NodeProcessed - self.startsasl = SASL_FAILURE - - text = challenge.getTagData('text') - reason = 'not-authorized' - childs = challenge.getChildren() - for child in childs: - name = child.getName() - if name == 'text': - continue - if name in SASL_ERROR_CONDITIONS: - reason = name - break - on_auth_fail(reason, text) - - elif challenge.getName() == 'success': - if self.mechanism in ('SCRAM-SHA-1', 'SCRAM-SHA-1-PLUS'): - # check data-with-success - data = _scram_parse(data) - if data['v'] != scram_base64(self.scram_ServerSignature): - on_auth_fail('ServerSignature is wrong') - - self.startsasl = SASL_SUCCESS - log.info('Successfully authenticated with remote server.') - handlers = self._owner.Dispatcher.dumpHandlers() - - # Bosh specific dispatcher replugging - # save old features. They will be used in case we won't get response - # on stream restart after SASL auth (happens with XMPP over BOSH - # with Openfire) - old_features = self._owner.Dispatcher.Stream.features - self._owner.Dispatcher.PlugOut() - dispatcher_nb.Dispatcher.get_instance().PlugIn(self._owner, - after_SASL=True, old_features=old_features) - self._owner.Dispatcher.restoreHandlers(handlers) - self._owner.User = self.username - - if self.on_sasl: - self.on_sasl() - raise NodeProcessed - - ### Perform auth step - if self.mechanism != 'GSSAPI': - log.info('Got challenge:' + data) - else: - log.info('Got challenge') - - if self.mechanism == 'GSSAPI': - if self.gss_step == GSS_STATE_STEP: - rc = kerberos.authGSSClientStep(self.gss_vc, incoming_data) - if rc != kerberos.AUTH_GSS_CONTINUE: - self.gss_step = GSS_STATE_WRAP - elif self.gss_step == GSS_STATE_WRAP: - rc = kerberos.authGSSClientUnwrap(self.gss_vc, incoming_data) - response = kerberos.authGSSClientResponse(self.gss_vc) - rc = kerberos.authGSSClientWrap(self.gss_vc, response, - kerberos.authGSSClientUserName(self.gss_vc)) - response = kerberos.authGSSClientResponse(self.gss_vc) - if not response: - response = '' - self._owner.send(Node('response', attrs={'xmlns': NS_SASL}, - payload=response).__str__()) - raise NodeProcessed - if self.mechanism in ('SCRAM-SHA-1', 'SCRAM-SHA-1-PLUS'): - hashfn = hashlib.sha1 - - def HMAC(k, s): - return hmac.new(key=k, msg=s, digestmod=hashfn).digest() - - def XOR(x, y): - r = [px ^ py for px, py in zip(x, y)] - return bytes(r) - - def Hi(s, salt, iters): - ii = 1 - try: - s = s.encode('utf-8') - except: - pass - ui_1 = HMAC(s, salt + b'\0\0\0\01') - ui = ui_1 - for i in range(iters - 1): - ii += 1 - ui_1 = HMAC(s, ui_1) - ui = XOR(ui, ui_1) - return ui - - def scram_H(s): - return hashfn(s).digest() - - if self.scram_step == 0: - self.scram_step = 1 - self.scram_soup += ',' + data + ',' - data = _scram_parse(data) - # Check server nonce here. - # The first part of server nonce muss be the nonce send by client. - if (data['r'][:len(self.client_nonce)] != self.client_nonce): - on_auth_fail('Server nonce is incorrect') - raise NodeProcessed - if self.mechanism == 'SCRAM-SHA-1': - r = 'c=' + scram_base64(self.scram_gs2) - else: - # Channel binding data goes in here too. - r = 'c=' + scram_base64(self.scram_gs2.encode('utf-8') - + self.channel_binding) - r += ',r=' + data['r'] - self.scram_soup += r - self.scram_soup = self.scram_soup.encode('utf-8') - salt = base64.b64decode(data['s'].encode('utf-8')) - iter = int(data['i']) - SaltedPassword = Hi(self.password, salt, iter) - # TODO: Could cache this, along with salt+iter. - ClientKey = HMAC(SaltedPassword, b'Client Key') - StoredKey = scram_H(ClientKey) - ClientSignature = HMAC(StoredKey, self.scram_soup) - ClientProof = XOR(ClientKey, ClientSignature) - r += ',p=' + scram_base64(ClientProof) - ServerKey = HMAC(SaltedPassword, b'Server Key') - self.scram_ServerSignature = HMAC(ServerKey, self.scram_soup) - sasl_data = scram_base64(r) - node = Node('response', attrs={'xmlns': NS_SASL}, - payload=[sasl_data]) - self._owner.send(str(node)) - raise NodeProcessed - - if self.scram_step == 1: - data = _scram_parse(data) - if base64.b64decode(data['v'].encode('utf-8')).decode('utf-8') \ - != self.scram_ServerSignature: - # TODO: Not clear what to do here - need to abort. - raise Exception - node = Node('response', attrs={'xmlns': NS_SASL}); - self._owner.send(str(node)) - raise NodeProcessed - - # DIGEST-MD5 - # magic foo... - chal = _challenge_splitter(data) - if not self.realm and 'realm' in chal: - self.realm = chal['realm'] - if 'qop' in chal and ((chal['qop'] =='auth') or \ - (isinstance(chal['qop'], list) and 'auth' in chal['qop'])): - self.resp = {'username': self.username, - 'nonce': chal['nonce'], - 'cnonce': '%x' % int(binascii.hexlify(os.urandom(24)), 16), - 'nc': ('00000001'), # ToDo: Is this a tupel or only a string? - 'qop': 'auth', - 'digest-uri': 'xmpp/' + self._owner.Server, - 'charset': 'utf-8' - } - if self.realm: - self.resp['realm'] = self.realm - else: - self.resp['realm'] = self._owner.Server - # Password is now required - self._owner._caller.get_password(self.set_password, self.mechanism) - elif 'rspauth' in chal: - # Check rspauth value - if chal['rspauth'] != self.digest_rspauth: - on_auth_fail('rspauth is wrong') - self._owner.send(str(Node('response', attrs={'xmlns':NS_SASL}))) - else: - self.startsasl = SASL_FAILURE - log.info('Failed SASL authentification: unknown challenge') - if self.on_sasl: - self.on_sasl() - raise NodeProcessed - - @staticmethod - def _convert_to_iso88591(string): - try: - string = string.encode('iso-8859-1') - except UnicodeEncodeError: - pass - return string - - def set_password(self, password): - self.password = '' if password is None else password - if self.mechanism in ('SCRAM-SHA-1', 'SCRAM-SHA-1-PLUS'): - self.client_nonce = '%x' % int(binascii.hexlify(os.urandom(24)), 16) - self.scram_soup = 'n=' + self.username + ',r=' + self.client_nonce - if self.mechanism == 'SCRAM-SHA-1': - if self.channel_binding is None: - # Client doesn't support Channel Binding - self.scram_gs2 = 'n,,' # No CB yet. - else: - # Client supports CB, but server doesn't support CB - self.scram_gs2 = 'y,,' - else: - self.scram_gs2 = 'p=tls-unique,,' - sasl_data = base64.b64encode((self.scram_gs2 + self.scram_soup).\ - encode('utf-8')).decode('utf-8').replace('\n', '') - node = Node('auth', attrs={'xmlns': NS_SASL, - 'mechanism': self.mechanism}, payload=[sasl_data]) - elif self.mechanism == 'DIGEST-MD5': - hash_username = self._convert_to_iso88591(self.resp['username']) - hash_realm = self._convert_to_iso88591(self.resp['realm']) - hash_password = self._convert_to_iso88591(self.password) - A1 = C([H(C([hash_username, hash_realm, hash_password])), - self.resp['nonce'].encode('utf-8'), self.resp['cnonce'].encode( - 'utf-8')]) - A2 = C([b'AUTHENTICATE', self.resp['digest-uri'].encode('utf-8')]) - response = HH(C([HH(A1).encode('utf-8'), self.resp['nonce'].encode( - 'utf-8'), self.resp['nc'].encode('utf-8'), self.resp['cnonce'].\ - encode('utf-8'), self.resp['qop'].encode('utf-8'), HH(A2).\ - encode('utf-8')])) - A2 = C([b'', self.resp['digest-uri'].encode('utf-8')]) - self.digest_rspauth = HH(C([HH(A1).encode('utf-8'), self.resp[ - 'nonce'].encode('utf-8'), self.resp['nc'].encode('utf-8'), - self.resp['cnonce'].encode('utf-8'), self.resp['qop'].encode( - 'utf-8'), HH(A2).encode('utf-8')])) - self.resp['response'] = response - sasl_data = '' - for key in ('charset', 'username', 'realm', 'nonce', 'nc', 'cnonce', - 'digest-uri', 'response', 'qop'): - if key in ('nc', 'qop', 'response', 'charset'): - sasl_data += "%s=%s," % (key, self.resp[key]) - else: - sasl_data += '%s="%s",' % (key, self.resp[key]) - sasl_data = base64.b64encode(sasl_data[:-1].encode('utf-8')).\ - decode('utf-8').replace('\r', '').replace('\n', '') - node = Node('response', attrs={'xmlns': NS_SASL}, - payload=[sasl_data]) - elif self.mechanism == 'PLAIN': - sasl_data = '\x00%s\x00%s' % (self.username, self.password) - sasl_data = base64.b64encode(sasl_data.encode('utf-8')).decode( - 'utf-8').replace('\n', '') - node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'PLAIN'}, - payload=[sasl_data]) - elif self.mechanism == 'X-MESSENGER-OAUTH2': - node = Node('auth', attrs={'xmlns': NS_SASL, - 'mechanism': 'X-MESSENGER-OAUTH2'}) - node.addData(password) - self._owner.send(str(node)) - - -class NonBlockingBind(PlugIn): - """ - Bind some JID to the current connection to allow router know of our - location. Must be plugged after successful SASL auth - """ - - def __init__(self): - PlugIn.__init__(self) - self._session_required = False - - def plugin(self, _owner): - self._owner.RegisterHandler( - 'features', self.FeaturesHandler, xmlns=NS_STREAMS) - # Execute the Handler manually, maybe we registered the features - # handler to late. This can happen when the client does not call - # bind() immediately - self.FeaturesHandler(None, self._owner.Dispatcher.Stream.features) - - def FeaturesHandler(self, con, feats): - """ - Determine if server supports resource binding and set some internal - attributes accordingly. - """ - if not feats or not feats.getTag('bind', namespace=NS_BIND): - return - - session = feats.getTag('session', namespace=NS_SESSION) - if session is not None: - if session.getTag('optional') is None: - self._session_required = True - - self.NonBlockingBind() - - def plugout(self): - """ - Remove Bind handler from owner's dispatcher. Used internally - """ - self._owner.UnregisterHandler( - 'features', self.FeaturesHandler, xmlns=NS_STREAMS) - - def NonBlockingBind(self): - """ - Perform binding. Use provided resource name or random (if not provided). - """ - - resource = [] - if self._owner._Resource: - resource = [Node('resource', payload=[self._owner._Resource])] - - self._owner.Dispatcher.SendAndWaitForResponse( - Protocol('iq', typ='set', payload=[Node('bind', - attrs={'xmlns': NS_BIND}, payload=resource)]), - func=self._on_bound) - - def _on_bound(self, resp): - if isResultNode(resp): - bind = resp.getTag('bind') - if bind is not None: - jid = bind.getTagData('jid') - log.info('Successfully bound %s', jid) - self._owner.set_bound_jid(jid) - - if not self._session_required: - # Server don't want us to initialize a session - log.info('No session required') - self._on_bind_successful() - else: - node = Node('session', attrs={'xmlns':NS_SESSION}) - iq = Protocol('iq', typ='set', payload=[node]) - self._owner.SendAndWaitForResponse( - iq, func=self._on_session) - return - if resp: - log.error('Binding failed: %s.', resp.getTag('error')) - else: - log.error('Binding failed: timeout expired') - self._owner.Connection.start_disconnect() - self._owner.Dispatcher.Event(Realm.CONNECTING, Event.BIND_FAILED) - self.PlugOut() - - def _on_session(self, resp): - if isResultNode(resp): - log.info('Successfully started session') - self._on_bind_successful() - else: - log.error('Session open failed') - self._owner.Connection.start_disconnect() - self._owner.Dispatcher.Event(Realm.CONNECTING, Event.SESSION_FAILED) - self.PlugOut() - - def _on_bind_successful(self): - feats = self._owner.Dispatcher.Stream.features - if feats.getTag('sm', namespace=NS_STREAM_MGMT): - self._owner.Smacks.send_enable() - self._owner.Dispatcher.Event(Realm.CONNECTING, Event.CONNECTION_ACTIVE) - self.PlugOut()
===================================== nbxmpp/bind.py ===================================== @@ -0,0 +1,126 @@ +# Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov +# Copyright (C) Dimitur Kirov <dkirov AT gmail.com> +# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> +# +# This file is part of nbxmpp. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; If not, see http://www.gnu.org/licenses/. + +import logging + +from nbxmpp.plugin import PlugIn +from nbxmpp.protocol import NS_BIND +from nbxmpp.protocol import NS_SESSION +from nbxmpp.protocol import NS_STREAMS +from nbxmpp.protocol import NS_STREAM_MGMT +from nbxmpp.protocol import Node +from nbxmpp.protocol import isResultNode +from nbxmpp.protocol import Protocol +from nbxmpp.const import Realm +from nbxmpp.const import Event + +log = logging.getLogger('nbxmpp.bind') + + +class NonBlockingBind(PlugIn): + """ + Bind some JID to the current connection to allow router know of our + location. Must be plugged after successful SASL auth + """ + + def __init__(self): + PlugIn.__init__(self) + self._session_required = False + + def plugin(self, _owner): + self._owner.RegisterHandler( + 'features', self._on_features, xmlns=NS_STREAMS) + + def _on_features(self, _con, feats): + """ + Determine if server supports resource binding and set some internal + attributes accordingly. + """ + if not feats or not feats.getTag('bind', namespace=NS_BIND): + return + + session = feats.getTag('session', namespace=NS_SESSION) + if session is not None: + if session.getTag('optional') is None: + self._session_required = True + + self._bind() + + def plugout(self): + """ + Remove Bind handler from owner's dispatcher. Used internally + """ + self._owner.UnregisterHandler( + 'features', self._on_features, xmlns=NS_STREAMS) + + def _bind(self): + """ + Perform binding. Use provided resource name or random (if not provided). + """ + resource = [] + if self._owner._Resource: + resource = [Node('resource', payload=[self._owner._Resource])] + + payload = Node('bind', attrs={'xmlns': NS_BIND}, payload=resource) + node = Protocol('iq', typ='set', payload=[payload]) + + self._owner.Dispatcher.SendAndWaitForResponse(node, func=self._on_bind) + + def _on_bind(self, stanza): + if isResultNode(stanza): + bind = stanza.getTag('bind') + if bind is not None: + jid = bind.getTagData('jid') + log.info('Successfully bound %s', jid) + self._owner.set_bound_jid(jid) + + if not self._session_required: + # Server don't want us to initialize a session + log.info('No session required') + self._on_bind_successful() + else: + node = Node('session', attrs={'xmlns':NS_SESSION}) + iq = Protocol('iq', typ='set', payload=[node]) + self._owner.SendAndWaitForResponse( + iq, func=self._on_session) + return + if stanza: + log.error('Binding failed: %s.', stanza.getTag('error')) + else: + log.error('Binding failed: timeout expired') + self._owner.Connection.start_disconnect() + self._owner.Dispatcher.Event(Realm.CONNECTING, Event.BIND_FAILED) + self.PlugOut() + + def _on_session(self, stanza): + if isResultNode(stanza): + log.info('Successfully started session') + self._on_bind_successful() + else: + log.error('Session open failed') + self._owner.Connection.start_disconnect() + self._owner.Dispatcher.Event(Realm.CONNECTING, Event.SESSION_FAILED) + self.PlugOut() + + def _on_bind_successful(self): + feats = self._owner.Dispatcher.Stream.features + if feats.getTag('sm', namespace=NS_STREAM_MGMT): + self._owner.Smacks.send_enable() + self._owner.Dispatcher.Event(Realm.CONNECTING, Event.CONNECTION_ACTIVE) + self.PlugOut() \ No newline at end of file
===================================== nbxmpp/client_nb.py ===================================== @@ -21,11 +21,11 @@ Client class establishes connection to XMPP Server and handles authentication import socket import logging
-from . import transports_nb, dispatcher_nb, auth_nb, roster_nb, protocol, bosh +from . import transports_nb, dispatcher_nb, roster_nb, protocol, bosh from .protocol import NS_TLS from .protocol import JID -from .auth_nb import SASL_AUTHENTICATION_MECHANISMS -from .auth_nb import NonBlockingBind +from .auth import SASL +from .bind import NonBlockingBind from .smacks import Smacks from .const import Realm from .const import Event @@ -135,13 +135,7 @@ class NonBlockingClient: if 'NonBlockingRoster' in self.__dict__: self.NonBlockingRoster.PlugOut() if 'SASL' in self.__dict__: - if 'startsasl' in self.SASL.__dict__ and \ - self.SASL.startsasl == 'failure-in-process': - sasl_failed = True - self.SASL.startsasl = 'failure' - self._on_start_sasl() - else: - self.SASL.PlugOut() + self.SASL.PlugOut() if 'NonBlockingTCP' in self.__dict__: self.NonBlockingTCP.PlugOut() if 'NonBlockingHTTP' in self.__dict__: @@ -521,77 +515,52 @@ class NonBlockingClient: ### follows code for authentication, resource bind, session and roster download ###############################################################################
- def auth(self, user, password, resource='', sasl=True, auth_mechs=None): + def auth(self, user, get_password=None, resource='', auth_mechs=None): """ Authenticate connnection and bind resource. If resource is not provided random one or library name used
:param user: XMPP username - :param password: XMPP password + :param get_password: Callback that must return the password for the + chosen mechanism :param resource: resource that shall be used for auth/connecting - :param sasl: Boolean indicating if SASL shall be used. (default: True) :param auth_mechs: Set of valid authentification mechanisms. If None all - authentification mechanisms will be allowed. Possible entries are: - 'ANONYMOUS', 'EXTERNAL', 'GSSAPI', 'SCRAM-SHA-1-PLUS', - 'SCRAM-SHA-1', 'DIGEST-MD5', 'PLAIN', 'X-MESSENGER-OAUTH2', - 'XEP-0078' - """ - self._User, self._Password = user, password - self._Resource, self._sasl = resource, sasl - self._channel_binding = None - if self.connected in ('ssl', 'tls'): - try: - if self.protocol_type != 'BOSH': - self._channel_binding = self.Connection.NonBlockingTLS.get_channel_binding() - # TLS handshake is finished so channel binding data muss exist - assert (self._channel_binding is not None) - except NotImplementedError: - pass - if auth_mechs is None: - self._auth_mechs = SASL_AUTHENTICATION_MECHANISMS - else: - self._auth_mechs = auth_mechs - - self._on_doc_attrs() - - def _on_doc_attrs(self): - """ - Plug authentication objects and start auth + authentification mechanisms will be allowed. + See the auth module for possible values """ - if self._sasl: - auth_nb.SASL.get_instance(self._User, self._Password, - self._on_start_sasl, self._channel_binding, - self._auth_mechs).PlugIn(self) - if not hasattr(self, 'SASL'): + if 'SASL' in self.__dict__: + log.error('Auth not possible while another auth is in progress') return
- self.SASL.auth() - return True + self._User = user + self._Resource = resource
- def _on_start_sasl(self, data=None): - """ - Callback used by SASL, called on each auth step - """ - if self.SASL.startsasl == 'in-process': - return self.onreceive(None) + SASL.get_instance(self._User, + auth_mechs, + get_password, + self._on_sasl_finished).PlugIn(self) + + def _on_sasl_finished(self, success, reason, text): + if success: + self.SASL.PlugOut() + if self.protocol_type == 'BOSH': + self.Dispatcher.after_SASL = True + self.Dispatcher.StreamInit()
- if self.SASL.startsasl == 'failure': - # wrong user/pass, stop auth - if 'SASL' in self.__dict__: - self.SASL.PlugOut() - self.connected = None # FIXME: is this intended? We use ''elsewhere - self.Dispatcher.Event(Realm.CONNECTING, Event.AUTH_FAILED, data) - - elif self.SASL.startsasl == 'success': self.connected += '+sasl' - self.Dispatcher.Event(Realm.CONNECTING, Event.AUTH_SUCCESSFUL) + self.Dispatcher.Event(Realm.CONNECTING, + Event.AUTH_SUCCESSFUL) self._owner.Smacks.register_handlers() + else: + self.Dispatcher.Event(Realm.CONNECTING, + Event.AUTH_FAILED, + (reason, text))
def bind(self): # Check if we can resume - if self._owner.Smacks.resume_supported: - self._owner.Smacks.resume_request() + if self.Smacks.resume_supported: + self.Smacks.resume_request() else: # If we cant resume we bind and enable sm afterwards NonBlockingBind.get_instance().PlugIn(self)
===================================== nbxmpp/const.py ===================================== @@ -16,6 +16,7 @@ # along with this program; If not, see http://www.gnu.org/licenses/.
from enum import Enum +from enum import IntEnum from enum import unique
@unique @@ -38,3 +39,7 @@ class Event(Enum):
def __str__(self): return self.value + +class GSSAPIState(IntEnum): + STEP = 0 + WRAP = 1
===================================== nbxmpp/protocol.py ===================================== @@ -268,6 +268,17 @@ NS_BOOKMARK_CONVERSION = 'urn:xmpp:bookmarks-conversion:0' #for (k, v) in loc.items(): #print('%s = '%s'' % (k, v))
+SASL_AUTH_MECHS = [ + 'SCRAM-SHA-256-PLUS', + 'SCRAM-SHA-256', + 'SCRAM-SHA-1-PLUS', + 'SCRAM-SHA-1', + 'PLAIN', + 'GSSAPI', + 'EXTERNAL', + 'ANONYMOUS', +] + SASL_ERROR_CONDITIONS = [ 'aborted', 'account-disabled',
===================================== nbxmpp/util.py ===================================== @@ -0,0 +1,34 @@ +# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> +# +# This file is part of nbxmpp. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; If not, see http://www.gnu.org/licenses/. + +import base64 + +def b64decode(data, return_type=str): + if isinstance(data, str): + data = data.encode() + result = base64.b64decode(data) + if return_type == bytes: + return result + return result.decode() + +def b64encode(data, return_type=str): + if isinstance(data, str): + data = data.encode() + result = base64.b64encode(data) + if return_type == bytes: + return result + return result.decode()
===================================== test/unit/test_sasl_scram.py ===================================== @@ -0,0 +1,28 @@ +import unittest +from unittest.mock import Mock + +from nbxmpp.auth import SCRAM_SHA_1 + + +class SCRAM(unittest.TestCase): + def setUp(self): + self.con = Mock() + self._method = SCRAM_SHA_1(self.con, None) + self._method._client_nonce = '4691d8f313ddb02d2eed511d5617a5c6f72efa671613c598' + + self._username = 'philw' + self._password = 'testtest123' + + self.auth = '<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="SCRAM-SHA-1">eSwsbj1waGlsdyxyPTQ2OTFkOGYzMTNkZGIwMmQyZWVkNTExZDU2MTdhNWM2ZjcyZWZhNjcxNjEzYzU5OA==</auth>' + self.challenge = 'cj00NjkxZDhmMzEzZGRiMDJkMmVlZDUxMWQ1NjE3YTVjNmY3MmVmYTY3MTYxM2M1OThDaEJpZGEyb0NJeks5S25QdGsxSUZnPT0scz1iZFkrbkRjdUhuVGFtNzgyaG9neHNnPT0saT00MDk2' + self.response = '<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">Yz1lU3dzLHI9NDY5MWQ4ZjMxM2RkYjAyZDJlZWQ1MTFkNTYxN2E1YzZmNzJlZmE2NzE2MTNjNTk4Q2hCaWRhMm9DSXpLOUtuUHRrMUlGZz09LHA9NUd5a09hWCtSWlllR3E2L2U3YTE2UDVBeFVrPQ==</response>' + self.success = 'dj1qMGtuNlVvT1FjTmJ0MGFlYnEwV1QzYWNkSW89' + + def test_auth(self): + self._method.initiate(self._username, self._password) + self.assertEqual(self.auth, str(self.con.send.call_args[0][0])) + + self._method.response(self.challenge) + self.assertEqual(self.response, str(self.con.send.call_args[0][0])) + + self._method.success(self.success)
View it on GitLab: https://dev.gajim.org/gajim/python-nbxmpp/commit/f2a203387891455c052d44f8b1c...