Philipp Hörist pushed to branch master at gajim / python-nbxmpp

Commits:

9 changed files:

Changes:

  • nbxmpp/__init__.py
    ... ... @@ -9,7 +9,7 @@ Thanks and credits to the xmpppy developers. See: http://xmpppy.sourceforge.net/
    9 9
     """
    
    10 10
     
    
    11 11
     from .protocol import *
    
    12
    -from . import simplexml, protocol, auth_nb, transports_nb, roster_nb
    
    12
    +from . import simplexml, protocol, auth, transports_nb, roster_nb
    
    13 13
     from . import dispatcher_nb, features_nb, idlequeue, bosh, tls_nb, proxy_connectors
    
    14 14
     from .client_nb import NonBlockingClient
    
    15 15
     from .plugin import PlugIn
    

  • nbxmpp/auth.py
    1
    +# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
    
    2
    +#
    
    3
    +# This file is part of nbxmpp.
    
    4
    +#
    
    5
    +# This program is free software; you can redistribute it and/or
    
    6
    +# modify it under the terms of the GNU General Public License
    
    7
    +# as published by the Free Software Foundation; either version 2
    
    8
    +# of the License, or (at your option) any later version.
    
    9
    +#
    
    10
    +# This program is distributed in the hope that it will be useful,
    
    11
    +# but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    12
    +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    
    13
    +# GNU General Public License for more details.
    
    14
    +#
    
    15
    +# You should have received a copy of the GNU General Public License
    
    16
    +# along with this program; If not, see <http://www.gnu.org/licenses/>.
    
    17
    +
    
    18
    +import os
    
    19
    +import hmac
    
    20
    +import binascii
    
    21
    +import logging
    
    22
    +import hashlib
    
    23
    +from hashlib import pbkdf2_hmac
    
    24
    +from functools import partial
    
    25
    +
    
    26
    +from nbxmpp.plugin import PlugIn
    
    27
    +from nbxmpp.protocol import NS_SASL
    
    28
    +from nbxmpp.protocol import Node
    
    29
    +from nbxmpp.protocol import NodeProcessed
    
    30
    +from nbxmpp.protocol import SASL_ERROR_CONDITIONS
    
    31
    +from nbxmpp.protocol import SASL_AUTH_MECHS
    
    32
    +from nbxmpp.util import b64decode
    
    33
    +from nbxmpp.util import b64encode
    
    34
    +from nbxmpp.const import GSSAPIState
    
    35
    +
    
    36
    +
    
    37
    +log = logging.getLogger('nbxmpp.auth')
    
    38
    +
    
    39
    +try:
    
    40
    +    kerberos = __import__('kerberos')
    
    41
    +    KERBEROS_AVAILABLE = True
    
    42
    +except ImportError:
    
    43
    +    KERBEROS_AVAILABLE = False
    
    44
    +
    
    45
    +
    
    46
    +class SASL(PlugIn):
    
    47
    +    """
    
    48
    +    Implements SASL authentication. Can be plugged into NonBlockingClient
    
    49
    +    to start authentication
    
    50
    +    """
    
    51
    +
    
    52
    +    _default_mechs = set(['SCRAM-SHA-256-PLUS',
    
    53
    +                          'SCRAM-SHA-256',
    
    54
    +                          'SCRAM-SHA-1-PLUS',
    
    55
    +                          'SCRAM-SHA-1',
    
    56
    +                          'PLAIN'])
    
    57
    +
    
    58
    +    def __init__(self, username, auth_mechs, get_password, on_finished):
    
    59
    +        """
    
    60
    +        :param username: XMPP username
    
    61
    +        :param auth_mechs: Set of valid authentication mechanisms.
    
    62
    +               Possible entries are:
    
    63
    +               'ANONYMOUS', 'EXTERNAL', 'GSSAPI', 'SCRAM-SHA-1-PLUS',
    
    64
    +               'SCRAM-SHA-1', 'SCRAM-SHA-256', 'SCRAM-SHA-256-PLUS', 'PLAIN'
    
    65
    +        :param on_finished: Callback after SASL is finished
    
    66
    +        :param get_password: Callback that must return the password for the
    
    67
    +                             chosen mechanism
    
    68
    +        """
    
    69
    +        PlugIn.__init__(self)
    
    70
    +        self.username = username
    
    71
    +        self._on_finished = on_finished
    
    72
    +        self._get_password = get_password
    
    73
    +
    
    74
    +        self._prefered_mechs = auth_mechs
    
    75
    +        self._enabled_mechs = self._prefered_mechs or self._default_mechs
    
    76
    +        self._chosen_mechanism = None
    
    77
    +        self._method = None
    
    78
    +
    
    79
    +        self._channel_binding = None
    
    80
    +
    
    81
    +    def _setup_mechs(self):
    
    82
    +        if self._owner.connected in ('ssl', 'tls'):
    
    83
    +            if self._owner.protocol_type == 'BOSH':
    
    84
    +                # PLUS would break if the server uses any kind of reverse proxy
    
    85
    +                self._enabled_mechs.discard('SCRAM-SHA-1-PLUS')
    
    86
    +                self._enabled_mechs.discard('SCRAM-SHA-256-PLUS')
    
    87
    +            else:
    
    88
    +                self._channel_binding = self._owner.Connection.NonBlockingTLS.get_channel_binding()
    
    89
    +                # TLS handshake is finished so channel binding data muss exist
    
    90
    +                if self._channel_binding is None:
    
    91
    +                    raise ValueError('No channel binding data found')
    
    92
    +
    
    93
    +        else:
    
    94
    +            self._enabled_mechs.discard('SCRAM-SHA-1-PLUS')
    
    95
    +            self._enabled_mechs.discard('SCRAM-SHA-256-PLUS')
    
    96
    +            if self._prefered_mechs is None:
    
    97
    +                # if the client didnt specify any auth mechs avoid
    
    98
    +                # sending the password over a plain connection
    
    99
    +                self._enabled_mechs.discard('PLAIN')
    
    100
    +
    
    101
    +        if not KERBEROS_AVAILABLE:
    
    102
    +            self._enabled_mechs.discard('GSSAPI')
    
    103
    +
    
    104
    +    def plugin(self, _owner):
    
    105
    +        self._setup_mechs()
    
    106
    +        self._owner.RegisterHandler(
    
    107
    +            'challenge', self._on_challenge, xmlns=NS_SASL)
    
    108
    +        self._owner.RegisterHandler(
    
    109
    +            'failure', self._on_failure, xmlns=NS_SASL)
    
    110
    +        self._owner.RegisterHandler(
    
    111
    +            'success', self._on_success, xmlns=NS_SASL)
    
    112
    +
    
    113
    +        # Execute the Handler manually, we already received the features
    
    114
    +        self._on_features(None, self._owner.Dispatcher.Stream.features)
    
    115
    +
    
    116
    +    def plugout(self):
    
    117
    +        """
    
    118
    +        Remove SASL handlers from owner's dispatcher. Used internally
    
    119
    +        """
    
    120
    +        self._owner.UnregisterHandler(
    
    121
    +            'challenge', self._on_challenge, xmlns=NS_SASL)
    
    122
    +        self._owner.UnregisterHandler(
    
    123
    +            'failure', self._on_failure, xmlns=NS_SASL)
    
    124
    +        self._owner.UnregisterHandler(
    
    125
    +            'success', self._on_success, xmlns=NS_SASL)
    
    126
    +
    
    127
    +    def _on_features(self, _con, stanza):
    
    128
    +        """
    
    129
    +        Used to determine if server supports SASL auth. Used internally
    
    130
    +        """
    
    131
    +        if not stanza.getTag('mechanisms', namespace=NS_SASL):
    
    132
    +            return
    
    133
    +
    
    134
    +        mechanisms = stanza.getTag('mechanisms', namespace=NS_SASL)
    
    135
    +        mechanisms = mechanisms.getTags('mechanism')
    
    136
    +
    
    137
    +        mechs = set(mech.getData() for mech in mechanisms)
    
    138
    +        available_mechs = mechs & self._enabled_mechs
    
    139
    +
    
    140
    +        log.info('Available mechanisms: %s', available_mechs)
    
    141
    +
    
    142
    +        if not available_mechs:
    
    143
    +            log.error('No available auth mechanisms found')
    
    144
    +            self._abort_auth('invalid-mechanism')
    
    145
    +            return
    
    146
    +
    
    147
    +        for mech in SASL_AUTH_MECHS:
    
    148
    +            if mech in available_mechs:
    
    149
    +                self._chosen_mechanism = mech
    
    150
    +                break
    
    151
    +
    
    152
    +        if self._chosen_mechanism is None:
    
    153
    +            log.error('No available auth mechanisms found')
    
    154
    +            self._abort_auth('invalid-mechanism')
    
    155
    +            return
    
    156
    +
    
    157
    +        log.info('Chosen auth mechanism: %s', self._chosen_mechanism)
    
    158
    +        self._auth()
    
    159
    +
    
    160
    +    def _auth(self):
    
    161
    +        password_cb = partial(self._on_password, self.username)
    
    162
    +
    
    163
    +        if self._chosen_mechanism == 'SCRAM-SHA-256-PLUS':
    
    164
    +            self._method = SCRAM_SHA_256_PLUS(self._owner.Connection,
    
    165
    +                                              self._channel_binding)
    
    166
    +            self._get_password(self._chosen_mechanism, password_cb)
    
    167
    +
    
    168
    +        elif self._chosen_mechanism == 'SCRAM-SHA-256':
    
    169
    +            self._method = SCRAM_SHA_256(self._owner.Connection, None)
    
    170
    +            self._get_password(self._chosen_mechanism, password_cb)
    
    171
    +
    
    172
    +        elif self._chosen_mechanism == 'SCRAM-SHA-1-PLUS':
    
    173
    +            self._method = SCRAM_SHA_1_PLUS(self._owner.Connection,
    
    174
    +                                            self._channel_binding)
    
    175
    +            self._get_password(self._chosen_mechanism, password_cb)
    
    176
    +
    
    177
    +        elif self._chosen_mechanism == 'SCRAM-SHA-1':
    
    178
    +            self._method = SCRAM_SHA_1(self._owner.Connection, None)
    
    179
    +            self._get_password(self._chosen_mechanism, password_cb)
    
    180
    +
    
    181
    +        elif self._chosen_mechanism == 'PLAIN':
    
    182
    +            self._method = PLAIN(self._owner.Connection)
    
    183
    +            self._get_password(self._chosen_mechanism, password_cb)
    
    184
    +
    
    185
    +        elif self._chosen_mechanism == 'ANONYMOUS':
    
    186
    +            self._method = ANONYMOUS(self._owner.Connection)
    
    187
    +            self._method.initiate()
    
    188
    +
    
    189
    +        elif self._chosen_mechanism == 'EXTERNAL':
    
    190
    +            self._method = EXTERNAL(self._owner.Connection)
    
    191
    +            self._method.initiate(self.username, self._owner.Server)
    
    192
    +
    
    193
    +        elif self._chosen_mechanism == 'GSSAPI':
    
    194
    +            self._method = GSSAPI(self._owner.Connection)
    
    195
    +            self._method.initiate(self._owner.xmpp_hostname)
    
    196
    +
    
    197
    +        else:
    
    198
    +            log.error('Unknown auth mech')
    
    199
    +
    
    200
    +    def _on_password(self, username, password):
    
    201
    +        if password is None:
    
    202
    +            log.warning('No password supplied')
    
    203
    +            return
    
    204
    +        self._method.initiate(username, password)
    
    205
    +
    
    206
    +    def _on_challenge(self, _con, stanza):
    
    207
    +        try:
    
    208
    +            self._method.response(stanza.getData())
    
    209
    +        except AttributeError:
    
    210
    +            log.info('Mechanism has no response method')
    
    211
    +            self._abort_auth()
    
    212
    +        except AuthFail as error:
    
    213
    +            log.error(error)
    
    214
    +            self._abort_auth()
    
    215
    +        raise NodeProcessed
    
    216
    +
    
    217
    +    def _on_success(self, _con, stanza):
    
    218
    +        log.info('Successfully authenticated with remote server')
    
    219
    +        try:
    
    220
    +            self._method.success(stanza.getData())
    
    221
    +        except AttributeError:
    
    222
    +            pass
    
    223
    +        except AuthFail as error:
    
    224
    +            log.error(error)
    
    225
    +            self._abort_auth()
    
    226
    +            raise NodeProcessed
    
    227
    +
    
    228
    +        self._on_finished(True, None, None)
    
    229
    +        raise NodeProcessed
    
    230
    +
    
    231
    +    def _on_failure(self, _con, stanza):
    
    232
    +        text = stanza.getTagData('text')
    
    233
    +        reason = 'not-authorized'
    
    234
    +        childs = stanza.getChildren()
    
    235
    +        for child in childs:
    
    236
    +            name = child.getName()
    
    237
    +            if name == 'text':
    
    238
    +                continue
    
    239
    +            if name in SASL_ERROR_CONDITIONS:
    
    240
    +                reason = name
    
    241
    +                break
    
    242
    +
    
    243
    +        log.info('Failed SASL authentification: %s %s', reason, text)
    
    244
    +        self._abort_auth(reason, text)
    
    245
    +        raise NodeProcessed
    
    246
    +
    
    247
    +    def _abort_auth(self, reason='malformed-request', text=None):
    
    248
    +        node = Node('abort', attrs={'xmlns': NS_SASL})
    
    249
    +        self._owner.send(node)
    
    250
    +        self._owner.Connection.start_disconnect()
    
    251
    +        self._on_finished(False, reason, text)
    
    252
    +
    
    253
    +
    
    254
    +class PLAIN:
    
    255
    +
    
    256
    +    _mechanism = 'PLAIN'
    
    257
    +
    
    258
    +    def __init__(self, con):
    
    259
    +        self._con = con
    
    260
    +
    
    261
    +    def initiate(self, username, password):
    
    262
    +        payload = b64encode('\x00%s\x00%s' % (username, password))
    
    263
    +        node = Node('auth',
    
    264
    +                    attrs={'xmlns': NS_SASL, 'mechanism': 'PLAIN'},
    
    265
    +                    payload=[payload])
    
    266
    +        self._con.send(node)
    
    267
    +
    
    268
    +
    
    269
    +class EXTERNAL:
    
    270
    +
    
    271
    +    _mechanism = 'EXTERNAL'
    
    272
    +
    
    273
    +    def __init__(self, con):
    
    274
    +        self._con = con
    
    275
    +
    
    276
    +    def initiate(self, username, server):
    
    277
    +        payload = b64encode('%s@%s' % (username, server))
    
    278
    +        node = Node('auth',
    
    279
    +                    attrs={'xmlns': NS_SASL, 'mechanism': 'EXTERNAL'},
    
    280
    +                    payload=[payload])
    
    281
    +        self._con.send(node)
    
    282
    +
    
    283
    +
    
    284
    +class ANONYMOUS:
    
    285
    +
    
    286
    +    _mechanism = 'ANONYMOUS'
    
    287
    +
    
    288
    +    def __init__(self, con):
    
    289
    +        self._con = con
    
    290
    +
    
    291
    +    def initiate(self):
    
    292
    +        node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'ANONYMOUS'})
    
    293
    +        self._con.send(node)
    
    294
    +
    
    295
    +
    
    296
    +class GSSAPI:
    
    297
    +
    
    298
    +    _mechanism = 'GSSAPI'
    
    299
    +
    
    300
    +    def __init__(self, con):
    
    301
    +        self._con = con
    
    302
    +        self._gss_vc = None
    
    303
    +        self._state = GSSAPIState.STEP
    
    304
    +
    
    305
    +    def initiate(self, hostname):
    
    306
    +        self._gss_vc = kerberos.authGSSClientInit('xmpp@%s' % hostname)[1]
    
    307
    +        kerberos.authGSSClientStep(self._gss_vc, '')
    
    308
    +        response = kerberos.authGSSClientResponse(self._gss_vc)
    
    309
    +        node = Node('auth',
    
    310
    +                    attrs={'xmlns': NS_SASL, 'mechanism': 'GSSAPI'},
    
    311
    +                    payload=(response or ''))
    
    312
    +        self._con.send(node)
    
    313
    +
    
    314
    +    def response(self, server_message, *args, **kwargs):
    
    315
    +        server_message = b64decode(server_message, bytes)
    
    316
    +        if self._state == GSSAPIState.STEP:
    
    317
    +            rc = kerberos.authGSSClientStep(self._gss_vc, server_message)
    
    318
    +            if rc != kerberos.AUTH_GSS_CONTINUE:
    
    319
    +                self._state = GSSAPIState.WRAP
    
    320
    +        elif self._state == GSSAPIState.WRAP:
    
    321
    +            rc = kerberos.authGSSClientUnwrap(self._gss_vc, server_message)
    
    322
    +            response = kerberos.authGSSClientResponse(self._gss_vc)
    
    323
    +            rc = kerberos.authGSSClientWrap(
    
    324
    +                self._gss_vc,
    
    325
    +                response,
    
    326
    +                kerberos.authGSSClientUserName(self._gss_vc))
    
    327
    +        response = kerberos.authGSSClientResponse(self._gss_vc)
    
    328
    +        if not response:
    
    329
    +            response = ''
    
    330
    +
    
    331
    +        node = Node('response',
    
    332
    +                    attrs={'xmlns': NS_SASL},
    
    333
    +                    payload=response)
    
    334
    +        self._con.send(node)
    
    335
    +
    
    336
    +
    
    337
    +class SCRAM:
    
    338
    +
    
    339
    +    _mechanism = ''
    
    340
    +    _channel_binding = ''
    
    341
    +    _hash_method = ''
    
    342
    +
    
    343
    +    def __init__(self, con, channel_binding):
    
    344
    +        self._con = con
    
    345
    +        self._channel_binding_data = channel_binding
    
    346
    +        self._client_nonce = '%x' % int(binascii.hexlify(os.urandom(24)), 16)
    
    347
    +        self._client_first_message_bare = None
    
    348
    +        self._server_signature = None
    
    349
    +        self._password = None
    
    350
    +
    
    351
    +    @property
    
    352
    +    def nonce_length(self):
    
    353
    +        return len(self._client_nonce)
    
    354
    +
    
    355
    +    @property
    
    356
    +    def _b64_channel_binding_data(self):
    
    357
    +        if self._mechanism.endswith('PLUS'):
    
    358
    +            return b64encode(b'%s%s' % (self._channel_binding.encode(),
    
    359
    +                                        self._channel_binding_data))
    
    360
    +        return b64encode(self._channel_binding)
    
    361
    +
    
    362
    +    @staticmethod
    
    363
    +    def _scram_parse(scram_data):
    
    364
    +        return dict(s.split('=', 1) for s in scram_data.split(','))
    
    365
    +
    
    366
    +    def initiate(self, username, password):
    
    367
    +        self._password = password
    
    368
    +        self._client_first_message_bare = 'n=%s,r=%s' % (username,
    
    369
    +                                                         self._client_nonce)
    
    370
    +        client_first_message = '%s%s' % (self._channel_binding,
    
    371
    +                                         self._client_first_message_bare)
    
    372
    +        payload = b64encode(client_first_message)
    
    373
    +        node = Node('auth',
    
    374
    +                    attrs={'xmlns': NS_SASL, 'mechanism': self._mechanism},
    
    375
    +                    payload=[payload])
    
    376
    +        self._con.send(node)
    
    377
    +
    
    378
    +    def response(self, server_first_message):
    
    379
    +        server_first_message = b64decode(server_first_message)
    
    380
    +        challenge = self._scram_parse(server_first_message)
    
    381
    +
    
    382
    +        client_nonce = challenge['r'][:self.nonce_length]
    
    383
    +        if client_nonce != self._client_nonce:
    
    384
    +            raise AuthFail('Invalid client nonce received from server')
    
    385
    +
    
    386
    +        salt = b64decode(challenge['s'], bytes)
    
    387
    +        iteration_count = int(challenge['i'])
    
    388
    +
    
    389
    +        if iteration_count < 4096:
    
    390
    +            raise AuthFail('Salt iteration count to low: %s' % iteration_count)
    
    391
    +
    
    392
    +        salted_password = pbkdf2_hmac(self._hash_method,
    
    393
    +                                      self._password.encode('utf8'),
    
    394
    +                                      salt,
    
    395
    +                                      iteration_count)
    
    396
    +
    
    397
    +        client_final_message_wo_proof = 'c=%s,r=%s' % (
    
    398
    +            self._b64_channel_binding_data,
    
    399
    +            challenge['r']
    
    400
    +        )
    
    401
    +
    
    402
    +        client_key = self._hmac(salted_password, 'Client Key')
    
    403
    +        stored_key = self._h(client_key)
    
    404
    +        auth_message = '%s,%s,%s' % (self._client_first_message_bare,
    
    405
    +                                     server_first_message,
    
    406
    +                                     client_final_message_wo_proof)
    
    407
    +        client_signature = self._hmac(stored_key, auth_message)
    
    408
    +        client_proof = self._xor(client_key, client_signature)
    
    409
    +
    
    410
    +        client_finale_message = 'c=%s,r=%s,p=%s' % (
    
    411
    +            self._b64_channel_binding_data,
    
    412
    +            challenge['r'],
    
    413
    +            b64encode(client_proof)
    
    414
    +        )
    
    415
    +
    
    416
    +        server_key = self._hmac(salted_password, 'Server Key')
    
    417
    +        self._server_signature = self._hmac(server_key, auth_message)
    
    418
    +
    
    419
    +        log.debug('Response: %s', client_finale_message)
    
    420
    +        payload = b64encode(client_finale_message)
    
    421
    +        node = Node('response',
    
    422
    +                    attrs={'xmlns': NS_SASL},
    
    423
    +                    payload=[payload])
    
    424
    +        self._con.send(node)
    
    425
    +
    
    426
    +    def success(self, server_last_message):
    
    427
    +        server_last_message = b64decode(server_last_message)
    
    428
    +        success = self._scram_parse(server_last_message)
    
    429
    +        server_signature = b64decode(success['v'], bytes)
    
    430
    +        if server_signature != self._server_signature:
    
    431
    +            raise AuthFail('Invalid server signature')
    
    432
    +
    
    433
    +    def _hmac(self, key, message):
    
    434
    +        return hmac.new(key=key,
    
    435
    +                        msg=message.encode(),
    
    436
    +                        digestmod=self._hash_method).digest()
    
    437
    +
    
    438
    +    @staticmethod
    
    439
    +    def _xor(x, y):
    
    440
    +        return bytes([px ^ py for px, py in zip(x, y)])
    
    441
    +
    
    442
    +    def _h(self, data):
    
    443
    +        return hashlib.new(self._hash_method, data).digest()
    
    444
    +
    
    445
    +
    
    446
    +class SCRAM_SHA_1(SCRAM):
    
    447
    +
    
    448
    +    _mechanism = 'SCRAM-SHA-1'
    
    449
    +    _channel_binding = 'y,,'
    
    450
    +    _hash_method = 'sha1'
    
    451
    +
    
    452
    +
    
    453
    +class SCRAM_SHA_1_PLUS(SCRAM_SHA_1):
    
    454
    +
    
    455
    +    _mechanism = 'SCRAM-SHA-1-PLUS'
    
    456
    +    _channel_binding = 'p=tls-unique,,'
    
    457
    +
    
    458
    +
    
    459
    +class SCRAM_SHA_256(SCRAM):
    
    460
    +
    
    461
    +    _mechanism = 'SCRAM-SHA-256'
    
    462
    +    _channel_binding = 'y,,'
    
    463
    +    _hash_method = 'sha256'
    
    464
    +
    
    465
    +
    
    466
    +class SCRAM_SHA_256_PLUS(SCRAM_SHA_256):
    
    467
    +
    
    468
    +    _mechanism = 'SCRAM-SHA-256-PLUS'
    
    469
    +    _channel_binding = 'p=tls-unique,,'
    
    470
    +
    
    471
    +
    
    472
    +class AuthFail(Exception):
    
    473
    +    pass

  • nbxmpp/auth_nb.py deleted
    1
    -##   auth_nb.py
    
    2
    -##       based on auth.py, changes backported up to revision 1.41
    
    3
    -##
    
    4
    -##   Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
    
    5
    -##       modified by Dimitur Kirov <dkirov@gmail.com>
    
    6
    -##
    
    7
    -##   This program is free software; you can redistribute it and/or modify
    
    8
    -##   it under the terms of the GNU General Public License as published by
    
    9
    -##   the Free Software Foundation; either version 2, or (at your option)
    
    10
    -##   any later version.
    
    11
    -##
    
    12
    -##   This program is distributed in the hope that it will be useful,
    
    13
    -##   but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    14
    -##   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    
    15
    -##   GNU General Public License for more details.
    
    16
    -
    
    17
    -"""
    
    18
    -Provides plugs for SASL and NON-SASL authentication mechanisms.
    
    19
    -Can be used both for client and transport authentication
    
    20
    -
    
    21
    -See client_nb.py
    
    22
    -"""
    
    23
    -
    
    24
    -import re
    
    25
    -import os
    
    26
    -import binascii
    
    27
    -import base64
    
    28
    -import hmac
    
    29
    -import hashlib
    
    30
    -import logging
    
    31
    -
    
    32
    -from .protocol import NS_SASL, NS_SESSION, NS_STREAMS, NS_BIND
    
    33
    -from .protocol import NS_STREAM_MGMT
    
    34
    -from .protocol import Node, NodeProcessed, isResultNode, Protocol
    
    35
    -from .protocol import SASL_ERROR_CONDITIONS
    
    36
    -from .plugin import PlugIn
    
    37
    -from .const import Realm
    
    38
    -from .const import Event
    
    39
    -
    
    40
    -from . import dispatcher_nb
    
    41
    -
    
    42
    -log = logging.getLogger('nbxmpp.auth_nb')
    
    43
    -
    
    44
    -def HH(some): return hashlib.md5(some).hexdigest()
    
    45
    -def H(some): return hashlib.md5(some).digest()
    
    46
    -def C(some): return b':'.join(some)
    
    47
    -
    
    48
    -try:
    
    49
    -    kerberos = __import__('kerberos')
    
    50
    -    have_kerberos = True
    
    51
    -except ImportError:
    
    52
    -    have_kerberos = False
    
    53
    -
    
    54
    -GSS_STATE_STEP = 0
    
    55
    -GSS_STATE_WRAP = 1
    
    56
    -SASL_FAILURE_IN_PROGRESS = 'failure-in-process'
    
    57
    -SASL_FAILURE = 'failure'
    
    58
    -SASL_SUCCESS = 'success'
    
    59
    -SASL_UNSUPPORTED = 'not-supported'
    
    60
    -SASL_IN_PROCESS = 'in-process'
    
    61
    -
    
    62
    -# compile the search regex for _challenge_splitter
    
    63
    -_challenge_regex = re.compile(
    
    64
    -    r'(\w+)='               # keyword
    
    65
    -    r'("[^"]+"|[^,]+)'      # value
    
    66
    -    r',?',                  # optional comma separator
    
    67
    -    re.VERBOSE
    
    68
    -)
    
    69
    -
    
    70
    -def _challenge_splitter(data):
    
    71
    -    """
    
    72
    -    Helper function that creates a dict from challenge string. Used
    
    73
    -    for DIGEST-MD5 authentication.
    
    74
    -
    
    75
    -    Sample challenge string:
    
    76
    -      - username="example.org",realm="somerealm",
    
    77
    -        nonce="OA6MG9tEQGm2hh",cnonce="OA6MHXh6VqTrRk",
    
    78
    -        nc=00000001,qop="auth,auth-int,auth-conf",charset=utf-8
    
    79
    -
    
    80
    -    Expected result for challan:
    
    81
    -      - dict['qop'] = ('auth','auth-int','auth-conf')
    
    82
    -      - dict['realm'] = 'somerealm'
    
    83
    -    """
    
    84
    -    dict_ = {}
    
    85
    -    for match in _challenge_regex.finditer(data):
    
    86
    -        k = match.group(1)
    
    87
    -        v = match.group(2)
    
    88
    -        if v.startswith('"'):
    
    89
    -            v = v[1:-1] # Remove quote
    
    90
    -        if v.find(',') >= 0:
    
    91
    -            v = v.split(',') # Split using comma
    
    92
    -        dict_[k] = v
    
    93
    -    return dict_
    
    94
    -
    
    95
    -def _scram_parse(chatter):
    
    96
    -    """Helper function. Used for SCRAM-SHA-1, SCRAM-SHA-1-PLUS authentication"""
    
    97
    -    return dict(s.split('=', 1) for s in chatter.split(','))
    
    98
    -
    
    99
    -SASL_AUTHENTICATION_MECHANISMS = \
    
    100
    -    set(['ANONYMOUS', 'EXTERNAL', 'GSSAPI', 'SCRAM-SHA-1-PLUS', 'SCRAM-SHA-1',
    
    101
    -         'PLAIN', 'X-MESSENGER-OAUTH2'])
    
    102
    -
    
    103
    -class SASL(PlugIn):
    
    104
    -    """
    
    105
    -    Implements SASL authentication. Can be plugged into NonBlockingClient
    
    106
    -    to start authentication
    
    107
    -    """
    
    108
    -
    
    109
    -    def __init__(self, username, password, on_sasl, channel_binding,
    
    110
    -                 auth_mechs):
    
    111
    -        """
    
    112
    -        :param username: XMPP username
    
    113
    -        :param password: XMPP password
    
    114
    -        :param on_sasl: Callback, will be called after each SASL auth-step.
    
    115
    -        :param channel_binding: TLS channel binding data, None if the
    
    116
    -               binding data is not available
    
    117
    -        :param auth_mechs: Set of valid authentication mechanisms.
    
    118
    -               Possible entries are:
    
    119
    -               'ANONYMOUS', 'EXTERNAL', 'GSSAPI', 'SCRAM-SHA-1-PLUS',
    
    120
    -               'SCRAM-SHA-1', 'DIGEST-MD5', 'PLAIN', 'X-MESSENGER-OAUTH2'
    
    121
    -        """
    
    122
    -        PlugIn.__init__(self)
    
    123
    -        self.username = username
    
    124
    -        self.password = password
    
    125
    -        self.on_sasl = on_sasl
    
    126
    -        self.channel_binding = channel_binding
    
    127
    -        self.enabled_auth_mechs = auth_mechs
    
    128
    -        self.realm = None
    
    129
    -
    
    130
    -    def plugin(self, owner):
    
    131
    -        if 'version' not in self._owner.Dispatcher.Stream._document_attrs:
    
    132
    -            self.startsasl = SASL_UNSUPPORTED
    
    133
    -        elif self._owner.Dispatcher.Stream.features:
    
    134
    -            try:
    
    135
    -                self.FeaturesHandler(self._owner.Dispatcher,
    
    136
    -                    self._owner.Dispatcher.Stream.features)
    
    137
    -            except NodeProcessed:
    
    138
    -                pass
    
    139
    -        else:
    
    140
    -            self.startsasl = None
    
    141
    -
    
    142
    -    def plugout(self):
    
    143
    -        """
    
    144
    -        Remove SASL handlers from owner's dispatcher. Used internally
    
    145
    -        """
    
    146
    -        if 'features' in  self._owner.__dict__:
    
    147
    -            self._owner.UnregisterHandler('features', self.FeaturesHandler,
    
    148
    -                xmlns=NS_STREAMS)
    
    149
    -        if 'challenge' in  self._owner.__dict__:
    
    150
    -            self._owner.UnregisterHandler('challenge', self.SASLHandler,
    
    151
    -                xmlns=NS_SASL)
    
    152
    -        if 'failure' in  self._owner.__dict__:
    
    153
    -            self._owner.UnregisterHandler('failure', self.SASLHandler,
    
    154
    -                xmlns=NS_SASL)
    
    155
    -        if 'success' in  self._owner.__dict__:
    
    156
    -            self._owner.UnregisterHandler('success', self.SASLHandler,
    
    157
    -                xmlns=NS_SASL)
    
    158
    -
    
    159
    -    def auth(self):
    
    160
    -        """
    
    161
    -        Start authentication. Result can be obtained via "SASL.startsasl"
    
    162
    -        attribute and will be either SASL_SUCCESS or SASL_FAILURE
    
    163
    -
    
    164
    -        Note that successful auth will take at least two Dispatcher.Process()
    
    165
    -        calls.
    
    166
    -        """
    
    167
    -        if self.startsasl:
    
    168
    -            pass
    
    169
    -        elif self._owner.Dispatcher.Stream.features:
    
    170
    -            try:
    
    171
    -                self.FeaturesHandler(self._owner.Dispatcher,
    
    172
    -                    self._owner.Dispatcher.Stream.features)
    
    173
    -            except NodeProcessed:
    
    174
    -                pass
    
    175
    -        else:
    
    176
    -            self._owner.RegisterHandler('features',
    
    177
    -                self.FeaturesHandler, xmlns=NS_STREAMS)
    
    178
    -
    
    179
    -    def FeaturesHandler(self, conn, feats):
    
    180
    -        """
    
    181
    -        Used to determine if server supports SASL auth. Used internally
    
    182
    -        """
    
    183
    -        if not feats.getTag('mechanisms', namespace=NS_SASL):
    
    184
    -            self.startsasl='not-supported'
    
    185
    -            log.info('SASL not supported by server')
    
    186
    -            return
    
    187
    -
    
    188
    -        self.mecs = set(
    
    189
    -            mec.getData()
    
    190
    -            for mec
    
    191
    -            in feats.getTag('mechanisms', namespace=NS_SASL).getTags('mechanism')
    
    192
    -        ) & self.enabled_auth_mechs
    
    193
    -
    
    194
    -        # Password based authentication mechanism ordered by strength.
    
    195
    -        # If the server supports a mechanism disable all weaker mechanisms.
    
    196
    -        password_auth_mechs_strength = ['SCRAM-SHA-1-PLUS', 'SCRAM-SHA-1',
    
    197
    -            'DIGEST-MD5', 'PLAIN', 'X-MESSENGER-OAUTH2']
    
    198
    -        if self.channel_binding is None:
    
    199
    -            password_auth_mechs_strength.remove('SCRAM-SHA-1-PLUS')
    
    200
    -        for i in range(0, len(password_auth_mechs_strength)):
    
    201
    -            if password_auth_mechs_strength[i] in self.mecs:
    
    202
    -                for m in password_auth_mechs_strength[i + 1:]:
    
    203
    -                    self.mecs.discard(m)
    
    204
    -                break
    
    205
    -
    
    206
    -        self._owner.RegisterHandler('challenge', self.SASLHandler,
    
    207
    -            xmlns=NS_SASL)
    
    208
    -        self._owner.RegisterHandler('failure', self.SASLHandler, xmlns=NS_SASL)
    
    209
    -        self._owner.RegisterHandler('success', self.SASLHandler, xmlns=NS_SASL)
    
    210
    -        self.MechanismHandler()
    
    211
    -
    
    212
    -    def MechanismHandler(self):
    
    213
    -        if 'ANONYMOUS' in self.mecs and self.username is None:
    
    214
    -            self.mecs.remove('ANONYMOUS')
    
    215
    -            node = Node('auth', attrs={'xmlns': NS_SASL,
    
    216
    -                'mechanism': 'ANONYMOUS'})
    
    217
    -            self.mechanism = 'ANONYMOUS'
    
    218
    -            self.startsasl = SASL_IN_PROCESS
    
    219
    -            self._owner.send(str(node))
    
    220
    -            raise NodeProcessed
    
    221
    -        if "EXTERNAL" in self.mecs:
    
    222
    -            self.mecs.remove('EXTERNAL')
    
    223
    -            sasl_data = '%s@%s' % (self.username, self._owner.Server)
    
    224
    -            sasl_data = base64.b64encode(sasl_data.encode('utf-8')).decode(
    
    225
    -                'utf-8').replace('\n', '')
    
    226
    -            node = Node('auth', attrs={'xmlns': NS_SASL,
    
    227
    -                'mechanism': 'EXTERNAL'}, payload=[sasl_data])
    
    228
    -            self.mechanism = 'EXTERNAL'
    
    229
    -            self.startsasl = SASL_IN_PROCESS
    
    230
    -            self._owner.send(str(node))
    
    231
    -            raise NodeProcessed
    
    232
    -        if 'GSSAPI' in self.mecs and have_kerberos:
    
    233
    -            self.mecs.remove('GSSAPI')
    
    234
    -            try:
    
    235
    -                self.gss_vc = kerberos.authGSSClientInit('xmpp@' + \
    
    236
    -                    self._owner.xmpp_hostname)[1]
    
    237
    -                kerberos.authGSSClientStep(self.gss_vc, '')
    
    238
    -                response = kerberos.authGSSClientResponse(self.gss_vc)
    
    239
    -                node=Node('auth', attrs={'xmlns': NS_SASL,
    
    240
    -                    'mechanism': 'GSSAPI'}, payload=(response or ''))
    
    241
    -                self.mechanism = 'GSSAPI'
    
    242
    -                self.gss_step = GSS_STATE_STEP
    
    243
    -                self.startsasl = SASL_IN_PROCESS
    
    244
    -                self._owner.send(str(node))
    
    245
    -                raise NodeProcessed
    
    246
    -            except kerberos.GSSError as e:
    
    247
    -                log.info('GSSAPI authentication failed: %s' % str(e))
    
    248
    -        if 'SCRAM-SHA-1-PLUS' in self.mecs and self.channel_binding is not None:
    
    249
    -            self.mecs.remove('SCRAM-SHA-1-PLUS')
    
    250
    -            self.mechanism = 'SCRAM-SHA-1-PLUS'
    
    251
    -            self._owner._caller.get_password(self.set_password, self.mechanism)
    
    252
    -            self.scram_step = 0
    
    253
    -            self.startsasl = SASL_IN_PROCESS
    
    254
    -            raise NodeProcessed
    
    255
    -        if 'SCRAM-SHA-1' in self.mecs:
    
    256
    -            self.mecs.remove('SCRAM-SHA-1')
    
    257
    -            self.mechanism = 'SCRAM-SHA-1'
    
    258
    -            self._owner._caller.get_password(self.set_password, self.mechanism)
    
    259
    -            self.scram_step = 0
    
    260
    -            self.startsasl = SASL_IN_PROCESS
    
    261
    -            raise NodeProcessed
    
    262
    -        if 'DIGEST-MD5' in self.mecs:
    
    263
    -            self.mecs.remove('DIGEST-MD5')
    
    264
    -            node = Node('auth', attrs={'xmlns': NS_SASL,
    
    265
    -                'mechanism': 'DIGEST-MD5'})
    
    266
    -            self.mechanism = 'DIGEST-MD5'
    
    267
    -            self.startsasl = SASL_IN_PROCESS
    
    268
    -            self._owner.send(str(node))
    
    269
    -            raise NodeProcessed
    
    270
    -        if 'PLAIN' in self.mecs:
    
    271
    -            self.mecs.remove('PLAIN')
    
    272
    -            self.mechanism = 'PLAIN'
    
    273
    -            self._owner._caller.get_password(self.set_password, self.mechanism)
    
    274
    -            self.startsasl = SASL_IN_PROCESS
    
    275
    -            raise NodeProcessed
    
    276
    -        if 'X-MESSENGER-OAUTH2' in self.mecs:
    
    277
    -            self.mecs.remove('X-MESSENGER-OAUTH2')
    
    278
    -            self.mechanism = 'X-MESSENGER-OAUTH2'
    
    279
    -            self._owner._caller.get_password(self.set_password, self.mechanism)
    
    280
    -            self.startsasl = SASL_IN_PROCESS
    
    281
    -            raise NodeProcessed
    
    282
    -        self.startsasl = SASL_FAILURE
    
    283
    -        log.info('I can only use ANONYMOUS, EXTERNAL, GSSAPI, SCRAM-SHA-1-PLUS,'
    
    284
    -                 ' SCRAM-SHA-1, DIGEST-MD5, PLAIN and X-MESSENGER-OAUTH2'
    
    285
    -                 ' mechanisms.')
    
    286
    -        if self.on_sasl:
    
    287
    -            self.on_sasl()
    
    288
    -        return
    
    289
    -
    
    290
    -    def SASLHandler(self, conn, challenge):
    
    291
    -        """
    
    292
    -        Perform next SASL auth step. Used internally
    
    293
    -        """
    
    294
    -        if challenge.getNamespace() != NS_SASL:
    
    295
    -            return
    
    296
    -
    
    297
    -        def scram_base64(s):
    
    298
    -            try:
    
    299
    -                s = s.encode('utf-8')
    
    300
    -            except:
    
    301
    -                pass
    
    302
    -            return ''.join(base64.b64encode(s).decode('utf-8').\
    
    303
    -                split('\n'))
    
    304
    -
    
    305
    -        incoming_data = challenge.getData()
    
    306
    -        data=base64.b64decode(incoming_data.encode('utf-8'))
    
    307
    -
    
    308
    -        if self.mechanism != 'GSSAPI':
    
    309
    -            data=data.decode('utf-8')
    
    310
    -
    
    311
    -        ### Handle Auth result
    
    312
    -        def on_auth_fail(reason, text=None):
    
    313
    -            log.info('Failed SASL authentification: %s %s', reason, text)
    
    314
    -            self._owner.send(str(Node('abort', attrs={'xmlns': NS_SASL})))
    
    315
    -            if len(self.mecs) > 0:
    
    316
    -                # There are other mechanisms to test, but wait for <failure>
    
    317
    -                # answer from server
    
    318
    -                self.startsasl = SASL_FAILURE_IN_PROGRESS
    
    319
    -                raise NodeProcessed
    
    320
    -            if self.on_sasl:
    
    321
    -                self.on_sasl((reason, text))
    
    322
    -            raise NodeProcessed
    
    323
    -
    
    324
    -        if challenge.getName() == 'failure':
    
    325
    -            if self.startsasl == SASL_FAILURE_IN_PROGRESS:
    
    326
    -                self.MechanismHandler()
    
    327
    -                raise NodeProcessed
    
    328
    -            self.startsasl = SASL_FAILURE
    
    329
    -
    
    330
    -            text = challenge.getTagData('text')
    
    331
    -            reason = 'not-authorized'
    
    332
    -            childs = challenge.getChildren()
    
    333
    -            for child in childs:
    
    334
    -                name = child.getName()
    
    335
    -                if name == 'text':
    
    336
    -                    continue
    
    337
    -                if name in SASL_ERROR_CONDITIONS:
    
    338
    -                    reason = name
    
    339
    -                    break
    
    340
    -            on_auth_fail(reason, text)
    
    341
    -
    
    342
    -        elif challenge.getName() == 'success':
    
    343
    -            if self.mechanism in ('SCRAM-SHA-1', 'SCRAM-SHA-1-PLUS'):
    
    344
    -                # check data-with-success
    
    345
    -                data = _scram_parse(data)
    
    346
    -                if data['v'] != scram_base64(self.scram_ServerSignature):
    
    347
    -                    on_auth_fail('ServerSignature is wrong')
    
    348
    -
    
    349
    -            self.startsasl = SASL_SUCCESS
    
    350
    -            log.info('Successfully authenticated with remote server.')
    
    351
    -            handlers = self._owner.Dispatcher.dumpHandlers()
    
    352
    -
    
    353
    -            # Bosh specific dispatcher replugging
    
    354
    -            # save old features. They will be used in case we won't get response
    
    355
    -            # on stream restart after SASL auth (happens with XMPP over BOSH
    
    356
    -            # with Openfire)
    
    357
    -            old_features = self._owner.Dispatcher.Stream.features
    
    358
    -            self._owner.Dispatcher.PlugOut()
    
    359
    -            dispatcher_nb.Dispatcher.get_instance().PlugIn(self._owner,
    
    360
    -                after_SASL=True, old_features=old_features)
    
    361
    -            self._owner.Dispatcher.restoreHandlers(handlers)
    
    362
    -            self._owner.User = self.username
    
    363
    -
    
    364
    -            if self.on_sasl:
    
    365
    -                self.on_sasl()
    
    366
    -            raise NodeProcessed
    
    367
    -
    
    368
    -        ### Perform auth step
    
    369
    -        if self.mechanism != 'GSSAPI':
    
    370
    -            log.info('Got challenge:' + data)
    
    371
    -        else:
    
    372
    -            log.info('Got challenge')
    
    373
    -
    
    374
    -        if self.mechanism == 'GSSAPI':
    
    375
    -            if self.gss_step == GSS_STATE_STEP:
    
    376
    -                rc = kerberos.authGSSClientStep(self.gss_vc, incoming_data)
    
    377
    -                if rc != kerberos.AUTH_GSS_CONTINUE:
    
    378
    -                    self.gss_step = GSS_STATE_WRAP
    
    379
    -            elif self.gss_step == GSS_STATE_WRAP:
    
    380
    -                rc = kerberos.authGSSClientUnwrap(self.gss_vc, incoming_data)
    
    381
    -                response = kerberos.authGSSClientResponse(self.gss_vc)
    
    382
    -                rc = kerberos.authGSSClientWrap(self.gss_vc, response,
    
    383
    -                    kerberos.authGSSClientUserName(self.gss_vc))
    
    384
    -            response = kerberos.authGSSClientResponse(self.gss_vc)
    
    385
    -            if not response:
    
    386
    -                response = ''
    
    387
    -            self._owner.send(Node('response', attrs={'xmlns': NS_SASL},
    
    388
    -                payload=response).__str__())
    
    389
    -            raise NodeProcessed
    
    390
    -        if self.mechanism in ('SCRAM-SHA-1', 'SCRAM-SHA-1-PLUS'):
    
    391
    -            hashfn = hashlib.sha1
    
    392
    -
    
    393
    -            def HMAC(k, s):
    
    394
    -                return hmac.new(key=k, msg=s, digestmod=hashfn).digest()
    
    395
    -
    
    396
    -            def XOR(x, y):
    
    397
    -                r = [px ^ py for px, py in zip(x, y)]
    
    398
    -                return bytes(r)
    
    399
    -
    
    400
    -            def Hi(s, salt, iters):
    
    401
    -                ii = 1
    
    402
    -                try:
    
    403
    -                    s = s.encode('utf-8')
    
    404
    -                except:
    
    405
    -                    pass
    
    406
    -                ui_1 = HMAC(s, salt + b'\0\0\0\01')
    
    407
    -                ui = ui_1
    
    408
    -                for i in range(iters - 1):
    
    409
    -                    ii += 1
    
    410
    -                    ui_1 = HMAC(s, ui_1)
    
    411
    -                    ui = XOR(ui, ui_1)
    
    412
    -                return ui
    
    413
    -
    
    414
    -            def scram_H(s):
    
    415
    -                return hashfn(s).digest()
    
    416
    -
    
    417
    -            if self.scram_step == 0:
    
    418
    -                self.scram_step = 1
    
    419
    -                self.scram_soup += ',' + data + ','
    
    420
    -                data = _scram_parse(data)
    
    421
    -                # Check server nonce here.
    
    422
    -                # The first part of server nonce muss be the nonce send by client.
    
    423
    -                if (data['r'][:len(self.client_nonce)] != self.client_nonce):
    
    424
    -                    on_auth_fail('Server nonce is incorrect')
    
    425
    -                    raise NodeProcessed
    
    426
    -                if self.mechanism == 'SCRAM-SHA-1':
    
    427
    -                    r = 'c=' + scram_base64(self.scram_gs2)
    
    428
    -                else:
    
    429
    -                    # Channel binding data goes in here too.
    
    430
    -                    r = 'c=' + scram_base64(self.scram_gs2.encode('utf-8')
    
    431
    -                        + self.channel_binding)
    
    432
    -                r += ',r=' + data['r']
    
    433
    -                self.scram_soup += r
    
    434
    -                self.scram_soup = self.scram_soup.encode('utf-8')
    
    435
    -                salt = base64.b64decode(data['s'].encode('utf-8'))
    
    436
    -                iter = int(data['i'])
    
    437
    -                SaltedPassword = Hi(self.password, salt, iter)
    
    438
    -                # TODO: Could cache this, along with salt+iter.
    
    439
    -                ClientKey = HMAC(SaltedPassword, b'Client Key')
    
    440
    -                StoredKey = scram_H(ClientKey)
    
    441
    -                ClientSignature = HMAC(StoredKey, self.scram_soup)
    
    442
    -                ClientProof = XOR(ClientKey, ClientSignature)
    
    443
    -                r += ',p=' + scram_base64(ClientProof)
    
    444
    -                ServerKey = HMAC(SaltedPassword, b'Server Key')
    
    445
    -                self.scram_ServerSignature = HMAC(ServerKey, self.scram_soup)
    
    446
    -                sasl_data = scram_base64(r)
    
    447
    -                node = Node('response', attrs={'xmlns': NS_SASL},
    
    448
    -                    payload=[sasl_data])
    
    449
    -                self._owner.send(str(node))
    
    450
    -                raise NodeProcessed
    
    451
    -
    
    452
    -            if self.scram_step == 1:
    
    453
    -                data = _scram_parse(data)
    
    454
    -                if base64.b64decode(data['v'].encode('utf-8')).decode('utf-8') \
    
    455
    -                != self.scram_ServerSignature:
    
    456
    -                    # TODO: Not clear what to do here - need to abort.
    
    457
    -                    raise Exception
    
    458
    -                node = Node('response', attrs={'xmlns': NS_SASL});
    
    459
    -                self._owner.send(str(node))
    
    460
    -                raise NodeProcessed
    
    461
    -
    
    462
    -        # DIGEST-MD5
    
    463
    -        # magic foo...
    
    464
    -        chal = _challenge_splitter(data)
    
    465
    -        if not self.realm and 'realm' in chal:
    
    466
    -            self.realm = chal['realm']
    
    467
    -        if 'qop' in chal and ((chal['qop'] =='auth') or \
    
    468
    -        (isinstance(chal['qop'], list) and 'auth' in chal['qop'])):
    
    469
    -            self.resp = {'username': self.username,
    
    470
    -                'nonce': chal['nonce'],
    
    471
    -                'cnonce': '%x' % int(binascii.hexlify(os.urandom(24)), 16),
    
    472
    -                'nc': ('00000001'),  # ToDo: Is this a tupel or only a string?
    
    473
    -                'qop': 'auth',
    
    474
    -                'digest-uri': 'xmpp/' + self._owner.Server,
    
    475
    -                'charset': 'utf-8'
    
    476
    -            }
    
    477
    -            if self.realm:
    
    478
    -                self.resp['realm'] = self.realm
    
    479
    -            else:
    
    480
    -                self.resp['realm'] = self._owner.Server
    
    481
    -            # Password is now required
    
    482
    -            self._owner._caller.get_password(self.set_password, self.mechanism)
    
    483
    -        elif 'rspauth' in chal:
    
    484
    -            # Check rspauth value
    
    485
    -            if chal['rspauth'] != self.digest_rspauth:
    
    486
    -                on_auth_fail('rspauth is wrong')
    
    487
    -            self._owner.send(str(Node('response', attrs={'xmlns':NS_SASL})))
    
    488
    -        else:
    
    489
    -            self.startsasl = SASL_FAILURE
    
    490
    -            log.info('Failed SASL authentification: unknown challenge')
    
    491
    -        if self.on_sasl:
    
    492
    -            self.on_sasl()
    
    493
    -        raise NodeProcessed
    
    494
    -
    
    495
    -    @staticmethod
    
    496
    -    def _convert_to_iso88591(string):
    
    497
    -        try:
    
    498
    -            string = string.encode('iso-8859-1')
    
    499
    -        except UnicodeEncodeError:
    
    500
    -            pass
    
    501
    -        return string
    
    502
    -
    
    503
    -    def set_password(self, password):
    
    504
    -        self.password = '' if password is None else password
    
    505
    -        if self.mechanism in ('SCRAM-SHA-1', 'SCRAM-SHA-1-PLUS'):
    
    506
    -            self.client_nonce = '%x' % int(binascii.hexlify(os.urandom(24)), 16)
    
    507
    -            self.scram_soup = 'n=' + self.username + ',r=' + self.client_nonce
    
    508
    -            if self.mechanism == 'SCRAM-SHA-1':
    
    509
    -                if self.channel_binding is None:
    
    510
    -                    # Client doesn't support Channel Binding
    
    511
    -                    self.scram_gs2 = 'n,,' # No CB yet.
    
    512
    -                else:
    
    513
    -                    # Client supports CB, but server doesn't support CB
    
    514
    -                    self.scram_gs2 = 'y,,'
    
    515
    -            else:
    
    516
    -                self.scram_gs2 = 'p=tls-unique,,'
    
    517
    -            sasl_data = base64.b64encode((self.scram_gs2 + self.scram_soup).\
    
    518
    -                encode('utf-8')).decode('utf-8').replace('\n', '')
    
    519
    -            node = Node('auth', attrs={'xmlns': NS_SASL,
    
    520
    -                'mechanism': self.mechanism}, payload=[sasl_data])
    
    521
    -        elif self.mechanism == 'DIGEST-MD5':
    
    522
    -            hash_username = self._convert_to_iso88591(self.resp['username'])
    
    523
    -            hash_realm = self._convert_to_iso88591(self.resp['realm'])
    
    524
    -            hash_password = self._convert_to_iso88591(self.password)
    
    525
    -            A1 = C([H(C([hash_username, hash_realm, hash_password])),
    
    526
    -                self.resp['nonce'].encode('utf-8'), self.resp['cnonce'].encode(
    
    527
    -                'utf-8')])
    
    528
    -            A2 = C([b'AUTHENTICATE', self.resp['digest-uri'].encode('utf-8')])
    
    529
    -            response = HH(C([HH(A1).encode('utf-8'), self.resp['nonce'].encode(
    
    530
    -                'utf-8'), self.resp['nc'].encode('utf-8'), self.resp['cnonce'].\
    
    531
    -                encode('utf-8'), self.resp['qop'].encode('utf-8'), HH(A2).\
    
    532
    -                encode('utf-8')]))
    
    533
    -            A2 = C([b'', self.resp['digest-uri'].encode('utf-8')])
    
    534
    -            self.digest_rspauth = HH(C([HH(A1).encode('utf-8'), self.resp[
    
    535
    -                'nonce'].encode('utf-8'), self.resp['nc'].encode('utf-8'),
    
    536
    -                self.resp['cnonce'].encode('utf-8'), self.resp['qop'].encode(
    
    537
    -                'utf-8'), HH(A2).encode('utf-8')]))
    
    538
    -            self.resp['response'] = response
    
    539
    -            sasl_data = ''
    
    540
    -            for key in ('charset', 'username', 'realm', 'nonce', 'nc', 'cnonce',
    
    541
    -            'digest-uri', 'response', 'qop'):
    
    542
    -                if key in ('nc', 'qop', 'response', 'charset'):
    
    543
    -                    sasl_data += "%s=%s," % (key, self.resp[key])
    
    544
    -                else:
    
    545
    -                    sasl_data += '%s="%s",' % (key, self.resp[key])
    
    546
    -            sasl_data = base64.b64encode(sasl_data[:-1].encode('utf-8')).\
    
    547
    -                decode('utf-8').replace('\r', '').replace('\n', '')
    
    548
    -            node = Node('response', attrs={'xmlns': NS_SASL},
    
    549
    -                payload=[sasl_data])
    
    550
    -        elif self.mechanism == 'PLAIN':
    
    551
    -            sasl_data = '\x00%s\x00%s' % (self.username, self.password)
    
    552
    -            sasl_data = base64.b64encode(sasl_data.encode('utf-8')).decode(
    
    553
    -                'utf-8').replace('\n', '')
    
    554
    -            node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'PLAIN'},
    
    555
    -                payload=[sasl_data])
    
    556
    -        elif self.mechanism == 'X-MESSENGER-OAUTH2':
    
    557
    -            node = Node('auth', attrs={'xmlns': NS_SASL,
    
    558
    -                'mechanism': 'X-MESSENGER-OAUTH2'})
    
    559
    -            node.addData(password)
    
    560
    -        self._owner.send(str(node))
    
    561
    -
    
    562
    -
    
    563
    -class NonBlockingBind(PlugIn):
    
    564
    -    """
    
    565
    -    Bind some JID to the current connection to allow router know of our
    
    566
    -    location. Must be plugged after successful SASL auth
    
    567
    -    """
    
    568
    -
    
    569
    -    def __init__(self):
    
    570
    -        PlugIn.__init__(self)
    
    571
    -        self._session_required = False
    
    572
    -
    
    573
    -    def plugin(self, _owner):
    
    574
    -        self._owner.RegisterHandler(
    
    575
    -            'features', self.FeaturesHandler, xmlns=NS_STREAMS)
    
    576
    -        # Execute the Handler manually, maybe we registered the features
    
    577
    -        # handler to late. This can happen when the client does not call
    
    578
    -        # bind() immediately
    
    579
    -        self.FeaturesHandler(None, self._owner.Dispatcher.Stream.features)
    
    580
    -
    
    581
    -    def FeaturesHandler(self, con, feats):
    
    582
    -        """
    
    583
    -        Determine if server supports resource binding and set some internal
    
    584
    -        attributes accordingly.
    
    585
    -        """
    
    586
    -        if not feats or not feats.getTag('bind', namespace=NS_BIND):
    
    587
    -            return
    
    588
    -
    
    589
    -        session = feats.getTag('session', namespace=NS_SESSION)
    
    590
    -        if session is not None:
    
    591
    -            if session.getTag('optional') is None:
    
    592
    -                self._session_required = True
    
    593
    -
    
    594
    -        self.NonBlockingBind()
    
    595
    -
    
    596
    -    def plugout(self):
    
    597
    -        """
    
    598
    -        Remove Bind handler from owner's dispatcher. Used internally
    
    599
    -        """
    
    600
    -        self._owner.UnregisterHandler(
    
    601
    -            'features', self.FeaturesHandler, xmlns=NS_STREAMS)
    
    602
    -
    
    603
    -    def NonBlockingBind(self):
    
    604
    -        """
    
    605
    -        Perform binding. Use provided resource name or random (if not provided).
    
    606
    -        """
    
    607
    -
    
    608
    -        resource = []
    
    609
    -        if self._owner._Resource:
    
    610
    -            resource = [Node('resource', payload=[self._owner._Resource])]
    
    611
    -
    
    612
    -        self._owner.Dispatcher.SendAndWaitForResponse(
    
    613
    -            Protocol('iq', typ='set', payload=[Node('bind',
    
    614
    -            attrs={'xmlns': NS_BIND}, payload=resource)]),
    
    615
    -            func=self._on_bound)
    
    616
    -
    
    617
    -    def _on_bound(self, resp):
    
    618
    -        if isResultNode(resp):
    
    619
    -            bind = resp.getTag('bind')
    
    620
    -            if bind is not None:
    
    621
    -                jid = bind.getTagData('jid')
    
    622
    -                log.info('Successfully bound %s', jid)
    
    623
    -                self._owner.set_bound_jid(jid)
    
    624
    -
    
    625
    -                if not self._session_required:
    
    626
    -                    # Server don't want us to initialize a session
    
    627
    -                    log.info('No session required')
    
    628
    -                    self._on_bind_successful()
    
    629
    -                else:
    
    630
    -                    node = Node('session', attrs={'xmlns':NS_SESSION})
    
    631
    -                    iq = Protocol('iq', typ='set', payload=[node])
    
    632
    -                    self._owner.SendAndWaitForResponse(
    
    633
    -                        iq, func=self._on_session)
    
    634
    -                return
    
    635
    -        if resp:
    
    636
    -            log.error('Binding failed: %s.', resp.getTag('error'))
    
    637
    -        else:
    
    638
    -            log.error('Binding failed: timeout expired')
    
    639
    -        self._owner.Connection.start_disconnect()
    
    640
    -        self._owner.Dispatcher.Event(Realm.CONNECTING, Event.BIND_FAILED)
    
    641
    -        self.PlugOut()
    
    642
    -
    
    643
    -    def _on_session(self, resp):
    
    644
    -        if isResultNode(resp):
    
    645
    -            log.info('Successfully started session')
    
    646
    -            self._on_bind_successful()
    
    647
    -        else:
    
    648
    -            log.error('Session open failed')
    
    649
    -            self._owner.Connection.start_disconnect()
    
    650
    -            self._owner.Dispatcher.Event(Realm.CONNECTING, Event.SESSION_FAILED)
    
    651
    -            self.PlugOut()
    
    652
    -
    
    653
    -    def _on_bind_successful(self):
    
    654
    -        feats = self._owner.Dispatcher.Stream.features
    
    655
    -        if feats.getTag('sm', namespace=NS_STREAM_MGMT):
    
    656
    -            self._owner.Smacks.send_enable()
    
    657
    -        self._owner.Dispatcher.Event(Realm.CONNECTING, Event.CONNECTION_ACTIVE)
    
    658
    -        self.PlugOut()

  • nbxmpp/bind.py
    1
    +# Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
    
    2
    +# Copyright (C)           Dimitur Kirov <dkirov AT gmail.com>
    
    3
    +# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
    
    4
    +#
    
    5
    +# This file is part of nbxmpp.
    
    6
    +#
    
    7
    +# This program is free software; you can redistribute it and/or
    
    8
    +# modify it under the terms of the GNU General Public License
    
    9
    +# as published by the Free Software Foundation; either version 2
    
    10
    +# of the License, or (at your option) any later version.
    
    11
    +#
    
    12
    +# This program is distributed in the hope that it will be useful,
    
    13
    +# but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    14
    +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    
    15
    +# GNU General Public License for more details.
    
    16
    +#
    
    17
    +# You should have received a copy of the GNU General Public License
    
    18
    +# along with this program; If not, see <http://www.gnu.org/licenses/>.
    
    19
    +
    
    20
    +import logging
    
    21
    +
    
    22
    +from nbxmpp.plugin import PlugIn
    
    23
    +from nbxmpp.protocol import NS_BIND
    
    24
    +from nbxmpp.protocol import NS_SESSION
    
    25
    +from nbxmpp.protocol import NS_STREAMS
    
    26
    +from nbxmpp.protocol import NS_STREAM_MGMT
    
    27
    +from nbxmpp.protocol import Node
    
    28
    +from nbxmpp.protocol import isResultNode
    
    29
    +from nbxmpp.protocol import Protocol
    
    30
    +from nbxmpp.const import Realm
    
    31
    +from nbxmpp.const import Event
    
    32
    +
    
    33
    +log = logging.getLogger('nbxmpp.bind')
    
    34
    +
    
    35
    +
    
    36
    +class NonBlockingBind(PlugIn):
    
    37
    +    """
    
    38
    +    Bind some JID to the current connection to allow router know of our
    
    39
    +    location. Must be plugged after successful SASL auth
    
    40
    +    """
    
    41
    +
    
    42
    +    def __init__(self):
    
    43
    +        PlugIn.__init__(self)
    
    44
    +        self._session_required = False
    
    45
    +
    
    46
    +    def plugin(self, _owner):
    
    47
    +        self._owner.RegisterHandler(
    
    48
    +            'features', self._on_features, xmlns=NS_STREAMS)
    
    49
    +
    
    50
    +    def _on_features(self, _con, feats):
    
    51
    +        """
    
    52
    +        Determine if server supports resource binding and set some internal
    
    53
    +        attributes accordingly.
    
    54
    +        """
    
    55
    +        if not feats or not feats.getTag('bind', namespace=NS_BIND):
    
    56
    +            return
    
    57
    +
    
    58
    +        session = feats.getTag('session', namespace=NS_SESSION)
    
    59
    +        if session is not None:
    
    60
    +            if session.getTag('optional') is None:
    
    61
    +                self._session_required = True
    
    62
    +
    
    63
    +        self._bind()
    
    64
    +
    
    65
    +    def plugout(self):
    
    66
    +        """
    
    67
    +        Remove Bind handler from owner's dispatcher. Used internally
    
    68
    +        """
    
    69
    +        self._owner.UnregisterHandler(
    
    70
    +            'features', self._on_features, xmlns=NS_STREAMS)
    
    71
    +
    
    72
    +    def _bind(self):
    
    73
    +        """
    
    74
    +        Perform binding. Use provided resource name or random (if not provided).
    
    75
    +        """
    
    76
    +        resource = []
    
    77
    +        if self._owner._Resource:
    
    78
    +            resource = [Node('resource', payload=[self._owner._Resource])]
    
    79
    +
    
    80
    +        payload = Node('bind', attrs={'xmlns': NS_BIND}, payload=resource)
    
    81
    +        node = Protocol('iq', typ='set', payload=[payload])
    
    82
    +
    
    83
    +        self._owner.Dispatcher.SendAndWaitForResponse(node, func=self._on_bind)
    
    84
    +
    
    85
    +    def _on_bind(self, stanza):
    
    86
    +        if isResultNode(stanza):
    
    87
    +            bind = stanza.getTag('bind')
    
    88
    +            if bind is not None:
    
    89
    +                jid = bind.getTagData('jid')
    
    90
    +                log.info('Successfully bound %s', jid)
    
    91
    +                self._owner.set_bound_jid(jid)
    
    92
    +
    
    93
    +                if not self._session_required:
    
    94
    +                    # Server don't want us to initialize a session
    
    95
    +                    log.info('No session required')
    
    96
    +                    self._on_bind_successful()
    
    97
    +                else:
    
    98
    +                    node = Node('session', attrs={'xmlns':NS_SESSION})
    
    99
    +                    iq = Protocol('iq', typ='set', payload=[node])
    
    100
    +                    self._owner.SendAndWaitForResponse(
    
    101
    +                        iq, func=self._on_session)
    
    102
    +                return
    
    103
    +        if stanza:
    
    104
    +            log.error('Binding failed: %s.', stanza.getTag('error'))
    
    105
    +        else:
    
    106
    +            log.error('Binding failed: timeout expired')
    
    107
    +        self._owner.Connection.start_disconnect()
    
    108
    +        self._owner.Dispatcher.Event(Realm.CONNECTING, Event.BIND_FAILED)
    
    109
    +        self.PlugOut()
    
    110
    +
    
    111
    +    def _on_session(self, stanza):
    
    112
    +        if isResultNode(stanza):
    
    113
    +            log.info('Successfully started session')
    
    114
    +            self._on_bind_successful()
    
    115
    +        else:
    
    116
    +            log.error('Session open failed')
    
    117
    +            self._owner.Connection.start_disconnect()
    
    118
    +            self._owner.Dispatcher.Event(Realm.CONNECTING, Event.SESSION_FAILED)
    
    119
    +            self.PlugOut()
    
    120
    +
    
    121
    +    def _on_bind_successful(self):
    
    122
    +        feats = self._owner.Dispatcher.Stream.features
    
    123
    +        if feats.getTag('sm', namespace=NS_STREAM_MGMT):
    
    124
    +            self._owner.Smacks.send_enable()
    
    125
    +        self._owner.Dispatcher.Event(Realm.CONNECTING, Event.CONNECTION_ACTIVE)
    
    126
    +        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
    21 21
     import socket
    
    22 22
     import logging
    
    23 23
     
    
    24
    -from . import transports_nb, dispatcher_nb, auth_nb, roster_nb, protocol, bosh
    
    24
    +from . import transports_nb, dispatcher_nb, roster_nb, protocol, bosh
    
    25 25
     from .protocol import NS_TLS
    
    26 26
     from .protocol import JID
    
    27
    -from .auth_nb import SASL_AUTHENTICATION_MECHANISMS
    
    28
    -from .auth_nb import NonBlockingBind
    
    27
    +from .auth import SASL
    
    28
    +from .bind import NonBlockingBind
    
    29 29
     from .smacks import Smacks
    
    30 30
     from .const import Realm
    
    31 31
     from .const import Event
    
    ... ... @@ -135,13 +135,7 @@ class NonBlockingClient:
    135 135
             if 'NonBlockingRoster' in self.__dict__:
    
    136 136
                 self.NonBlockingRoster.PlugOut()
    
    137 137
             if 'SASL' in self.__dict__:
    
    138
    -            if 'startsasl' in self.SASL.__dict__ and \
    
    139
    -            self.SASL.startsasl == 'failure-in-process':
    
    140
    -                sasl_failed = True
    
    141
    -                self.SASL.startsasl = 'failure'
    
    142
    -                self._on_start_sasl()
    
    143
    -            else:
    
    144
    -                self.SASL.PlugOut()
    
    138
    +            self.SASL.PlugOut()
    
    145 139
             if 'NonBlockingTCP' in self.__dict__:
    
    146 140
                 self.NonBlockingTCP.PlugOut()
    
    147 141
             if 'NonBlockingHTTP' in self.__dict__:
    
    ... ... @@ -521,77 +515,52 @@ class NonBlockingClient:
    521 515
     ### follows code for authentication, resource bind, session and roster download
    
    522 516
     ###############################################################################
    
    523 517
     
    
    524
    -    def auth(self, user, password, resource='', sasl=True, auth_mechs=None):
    
    518
    +    def auth(self, user, get_password=None, resource='', auth_mechs=None):
    
    525 519
             """
    
    526 520
             Authenticate connnection and bind resource. If resource is not provided
    
    527 521
             random one or library name used
    
    528 522
     
    
    529 523
             :param user: XMPP username
    
    530
    -        :param password: XMPP password
    
    524
    +        :param get_password: Callback that must return the password for the
    
    525
    +                             chosen mechanism
    
    531 526
             :param resource: resource that shall be used for auth/connecting
    
    532
    -        :param sasl: Boolean indicating if SASL shall be used. (default: True)
    
    533 527
             :param auth_mechs: Set of valid authentification mechanisms. If None all
    
    534
    -               authentification mechanisms will be allowed. Possible entries are:
    
    535
    -               'ANONYMOUS', 'EXTERNAL', 'GSSAPI', 'SCRAM-SHA-1-PLUS',
    
    536
    -               'SCRAM-SHA-1', 'DIGEST-MD5', 'PLAIN', 'X-MESSENGER-OAUTH2',
    
    537
    -               'XEP-0078'
    
    538
    -        """
    
    539
    -        self._User, self._Password = user, password
    
    540
    -        self._Resource, self._sasl = resource, sasl
    
    541
    -        self._channel_binding = None
    
    542
    -        if self.connected in ('ssl', 'tls'):
    
    543
    -            try:
    
    544
    -                if self.protocol_type != 'BOSH':
    
    545
    -                    self._channel_binding = self.Connection.NonBlockingTLS.get_channel_binding()
    
    546
    -                    # TLS handshake is finished so channel binding data muss exist
    
    547
    -                    assert (self._channel_binding is not None)
    
    548
    -            except NotImplementedError:
    
    549
    -                pass
    
    550
    -        if auth_mechs is None:
    
    551
    -            self._auth_mechs = SASL_AUTHENTICATION_MECHANISMS
    
    552
    -        else:
    
    553
    -            self._auth_mechs = auth_mechs
    
    554
    -
    
    555
    -        self._on_doc_attrs()
    
    556
    -
    
    557
    -    def _on_doc_attrs(self):
    
    558
    -        """
    
    559
    -        Plug authentication objects and start auth
    
    528
    +                           authentification mechanisms will be allowed.
    
    529
    +                           See the auth module for possible values
    
    560 530
             """
    
    561
    -        if self._sasl:
    
    562
    -            auth_nb.SASL.get_instance(self._User, self._Password,
    
    563
    -                    self._on_start_sasl, self._channel_binding,
    
    564
    -                    self._auth_mechs).PlugIn(self)
    
    565
    -        if not hasattr(self, 'SASL'):
    
    531
    +        if 'SASL' in self.__dict__:
    
    532
    +            log.error('Auth not possible while another auth is in progress')
    
    566 533
                 return
    
    567 534
     
    
    568
    -        self.SASL.auth()
    
    569
    -        return True
    
    535
    +        self._User = user
    
    536
    +        self._Resource = resource
    
    570 537
     
    
    571
    -    def _on_start_sasl(self, data=None):
    
    572
    -        """
    
    573
    -        Callback used by SASL, called on each auth step
    
    574
    -        """
    
    575
    -        if self.SASL.startsasl == 'in-process':
    
    576
    -            return
    
    577 538
             self.onreceive(None)
    
    539
    +        SASL.get_instance(self._User,
    
    540
    +                          auth_mechs,
    
    541
    +                          get_password,
    
    542
    +                          self._on_sasl_finished).PlugIn(self)
    
    543
    +
    
    544
    +    def _on_sasl_finished(self, success, reason, text):
    
    545
    +        if success:
    
    546
    +            self.SASL.PlugOut()
    
    547
    +            if self.protocol_type == 'BOSH':
    
    548
    +                self.Dispatcher.after_SASL = True
    
    549
    +            self.Dispatcher.StreamInit()
    
    578 550
     
    
    579
    -        if self.SASL.startsasl == 'failure':
    
    580
    -            # wrong user/pass, stop auth
    
    581
    -            if 'SASL' in self.__dict__:
    
    582
    -                self.SASL.PlugOut()
    
    583
    -            self.connected = None # FIXME: is this intended? We use ''elsewhere
    
    584
    -            self.Dispatcher.Event(Realm.CONNECTING, Event.AUTH_FAILED, data)
    
    585
    -
    
    586
    -        elif self.SASL.startsasl == 'success':
    
    587 551
                 self.connected += '+sasl'
    
    588
    -            self.Dispatcher.Event(Realm.CONNECTING, Event.AUTH_SUCCESSFUL)
    
    552
    +            self.Dispatcher.Event(Realm.CONNECTING,
    
    553
    +                                  Event.AUTH_SUCCESSFUL)
    
    589 554
                 self._owner.Smacks.register_handlers()
    
    555
    +        else:
    
    556
    +            self.Dispatcher.Event(Realm.CONNECTING,
    
    557
    +                                  Event.AUTH_FAILED,
    
    558
    +                                  (reason, text))
    
    590 559
     
    
    591 560
         def bind(self):
    
    592 561
             # Check if we can resume
    
    593
    -        if self._owner.Smacks.resume_supported:
    
    594
    -            self._owner.Smacks.resume_request()
    
    562
    +        if self.Smacks.resume_supported:
    
    563
    +            self.Smacks.resume_request()
    
    595 564
             else:
    
    596 565
                 # If we cant resume we bind and enable sm afterwards
    
    597 566
                 NonBlockingBind.get_instance().PlugIn(self)
    

  • nbxmpp/const.py
    ... ... @@ -16,6 +16,7 @@
    16 16
     # along with this program; If not, see <http://www.gnu.org/licenses/>.
    
    17 17
     
    
    18 18
     from enum import Enum
    
    19
    +from enum import IntEnum
    
    19 20
     from enum import unique
    
    20 21
     
    
    21 22
     @unique
    
    ... ... @@ -38,3 +39,7 @@ class Event(Enum):
    38 39
     
    
    39 40
         def __str__(self):
    
    40 41
             return self.value
    
    42
    +
    
    43
    +class GSSAPIState(IntEnum):
    
    44
    +    STEP = 0
    
    45
    +    WRAP = 1

  • nbxmpp/protocol.py
    ... ... @@ -268,6 +268,17 @@ NS_BOOKMARK_CONVERSION = 'urn:xmpp:bookmarks-conversion:0'
    268 268
     #for (k, v) in loc.items():
    
    269 269
          #print('%s = \'%s\'' % (k, v))
    
    270 270
     
    
    271
    +SASL_AUTH_MECHS = [
    
    272
    +    'SCRAM-SHA-256-PLUS',
    
    273
    +    'SCRAM-SHA-256',
    
    274
    +    'SCRAM-SHA-1-PLUS',
    
    275
    +    'SCRAM-SHA-1',
    
    276
    +    'PLAIN',
    
    277
    +    'GSSAPI',
    
    278
    +    'EXTERNAL',
    
    279
    +    'ANONYMOUS',
    
    280
    +]
    
    281
    +
    
    271 282
     SASL_ERROR_CONDITIONS = [
    
    272 283
         'aborted',
    
    273 284
         'account-disabled',
    

  • nbxmpp/util.py
    1
    +# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
    
    2
    +#
    
    3
    +# This file is part of nbxmpp.
    
    4
    +#
    
    5
    +# This program is free software; you can redistribute it and/or
    
    6
    +# modify it under the terms of the GNU General Public License
    
    7
    +# as published by the Free Software Foundation; either version 2
    
    8
    +# of the License, or (at your option) any later version.
    
    9
    +#
    
    10
    +# This program is distributed in the hope that it will be useful,
    
    11
    +# but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    12
    +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    
    13
    +# GNU General Public License for more details.
    
    14
    +#
    
    15
    +# You should have received a copy of the GNU General Public License
    
    16
    +# along with this program; If not, see <http://www.gnu.org/licenses/>.
    
    17
    +
    
    18
    +import base64
    
    19
    +
    
    20
    +def b64decode(data, return_type=str):
    
    21
    +    if isinstance(data, str):
    
    22
    +        data = data.encode()
    
    23
    +    result = base64.b64decode(data)
    
    24
    +    if return_type == bytes:
    
    25
    +        return result
    
    26
    +    return result.decode()
    
    27
    +
    
    28
    +def b64encode(data, return_type=str):
    
    29
    +    if isinstance(data, str):
    
    30
    +        data = data.encode()
    
    31
    +    result = base64.b64encode(data)
    
    32
    +    if return_type == bytes:
    
    33
    +        return result
    
    34
    +    return result.decode()

  • test/unit/test_sasl_scram.py
    1
    +import unittest
    
    2
    +from unittest.mock import Mock
    
    3
    +
    
    4
    +from nbxmpp.auth import SCRAM_SHA_1
    
    5
    +
    
    6
    +
    
    7
    +class SCRAM(unittest.TestCase):
    
    8
    +    def setUp(self):
    
    9
    +        self.con = Mock()
    
    10
    +        self._method = SCRAM_SHA_1(self.con, None)
    
    11
    +        self._method._client_nonce = '4691d8f313ddb02d2eed511d5617a5c6f72efa671613c598'
    
    12
    +
    
    13
    +        self._username = 'philw'
    
    14
    +        self._password = 'testtest123'
    
    15
    +
    
    16
    +        self.auth = '<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="SCRAM-SHA-1">eSwsbj1waGlsdyxyPTQ2OTFkOGYzMTNkZGIwMmQyZWVkNTExZDU2MTdhNWM2ZjcyZWZhNjcxNjEzYzU5OA==</auth>'
    
    17
    +        self.challenge = 'cj00NjkxZDhmMzEzZGRiMDJkMmVlZDUxMWQ1NjE3YTVjNmY3MmVmYTY3MTYxM2M1OThDaEJpZGEyb0NJeks5S25QdGsxSUZnPT0scz1iZFkrbkRjdUhuVGFtNzgyaG9neHNnPT0saT00MDk2'
    
    18
    +        self.response = '<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">Yz1lU3dzLHI9NDY5MWQ4ZjMxM2RkYjAyZDJlZWQ1MTFkNTYxN2E1YzZmNzJlZmE2NzE2MTNjNTk4Q2hCaWRhMm9DSXpLOUtuUHRrMUlGZz09LHA9NUd5a09hWCtSWlllR3E2L2U3YTE2UDVBeFVrPQ==</response>'
    
    19
    +        self.success = 'dj1qMGtuNlVvT1FjTmJ0MGFlYnEwV1QzYWNkSW89'
    
    20
    +
    
    21
    +    def test_auth(self):
    
    22
    +        self._method.initiate(self._username, self._password)
    
    23
    +        self.assertEqual(self.auth, str(self.con.send.call_args[0][0]))
    
    24
    +
    
    25
    +        self._method.response(self.challenge)
    
    26
    +        self.assertEqual(self.response, str(self.con.send.call_args[0][0]))
    
    27
    +
    
    28
    +        self._method.success(self.success)