auth_nb.py 30.2 KB
Newer Older
Yann Leboulanger's avatar
Yann Leboulanger committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
##   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
"""

24 25
from __future__ import unicode_literals

Yann Leboulanger's avatar
Yann Leboulanger committed
26 27 28 29
from .protocol import NS_SASL, NS_SESSION, NS_STREAMS, NS_BIND, NS_AUTH
from .protocol import NS_STREAM_MGMT
from .protocol import Node, NodeProcessed, isResultNode, Iq, Protocol, JID
from .plugin import PlugIn
30
import sys
31
import re
Yann Leboulanger's avatar
Yann Leboulanger committed
32
import base64
Yann Leboulanger's avatar
Yann Leboulanger committed
33
from . import dispatcher_nb
Yann Leboulanger's avatar
Yann Leboulanger committed
34 35 36 37 38 39
import hmac
import hashlib

import logging
log = logging.getLogger('nbxmpp.auth_nb')

Yann Leboulanger's avatar
Yann Leboulanger committed
40
from . import rndg
41

Yann Leboulanger's avatar
Yann Leboulanger committed
42 43
def HH(some): return hashlib.md5(some).hexdigest()
def H(some): return hashlib.md5(some).digest()
44
def C(some): return b':'.join(some)
Yann Leboulanger's avatar
Yann Leboulanger committed
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

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'

60 61 62 63 64 65 66 67 68
# compile the search regex for _challenge_splitter
_challenge_regex = re.compile("""
    (\w+)      # keyword
    =
    ("[^"]+"|[^,]+)   # value
    ,?         # optional comma separator
""", re.VERBOSE)

def _challenge_splitter(data):
Yann Leboulanger's avatar
Yann Leboulanger committed
69
    """
70 71
    Helper function that creates a dict from challenge string. Used
    for DIGEST-MD5 authentication.
Yann Leboulanger's avatar
Yann Leboulanger committed
72 73 74 75 76 77 78 79 80 81 82

    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_ = {}
83 84 85 86 87 88 89 90
    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
Yann Leboulanger's avatar
Yann Leboulanger committed
91 92
    return dict_

93
def _scram_parse(chatter):
94
    """Helper function. Used for SCRAM-SHA-1, SCRAM-SHA-1-PLUS authentication"""
Yann Leboulanger's avatar
Yann Leboulanger committed
95 96
    return dict(s.split('=', 1) for s in chatter.split(','))

97 98 99 100
SASL_AUTHENTICATION_MECHANISMS = \
    set(['ANONYMOUS', 'EXTERNAL', 'GSSAPI', 'SCRAM-SHA-1-PLUS', 'SCRAM-SHA-1',
         'DIGEST-MD5', 'PLAIN', 'X-MESSENGER-OAUTH2'])

Yann Leboulanger's avatar
Yann Leboulanger committed
101 102 103 104 105 106
class SASL(PlugIn):
    """
    Implements SASL authentication. Can be plugged into NonBlockingClient
    to start authentication
    """

107 108
    def __init__(self, username, password, on_sasl, channel_binding,
                 auth_mechs):
Yann Leboulanger's avatar
Yann Leboulanger committed
109 110 111 112
        """
        :param username: XMPP username
        :param password: XMPP password
        :param on_sasl: Callback, will be called after each SASL auth-step.
113
        :param channel_binding: TLS channel binding data, None if the
114
               binding data is not available
115 116 117 118
        :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'
Yann Leboulanger's avatar
Yann Leboulanger committed
119 120 121 122 123
        """
        PlugIn.__init__(self)
        self.username = username
        self.password = password
        self.on_sasl = on_sasl
124
        self.channel_binding = channel_binding
125
        self.enabled_auth_mechs = auth_mechs
Yann Leboulanger's avatar
Yann Leboulanger committed
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
        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

162
        Note that successful auth will take at least two Dispatcher.Process()
Yann Leboulanger's avatar
Yann Leboulanger committed
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
        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
185 186

        self.mecs = set(
187 188
            mec.getData()
            for mec
189 190
            in feats.getTag('mechanisms', namespace=NS_SASL).getTags('mechanism')
        ) & self.enabled_auth_mechs
191

192 193
        # Password based authentication mechanism ordered by strength.
        # If the server supports a mechanism disable all weaker mechanisms.
194 195 196 197
        password_auth_mechs_strength = ['SCRAM-SHA-1-PLUS', 'SCRAM-SHA-1',
            'DIGEST-MD5', 'PLAIN', 'X-MESSENGER-OAUTH2']
        if self.channel_binding == None:
            password_auth_mechs_strength.remove('SCRAM-SHA-1-PLUS')
198 199 200 201 202
        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
Yann Leboulanger's avatar
Yann Leboulanger committed
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220

        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')
Yann Leboulanger's avatar
Yann Leboulanger committed
221
            sasl_data = '%s@%s' % (self.username, self._owner.Server)
222 223
            sasl_data = base64.b64encode(sasl_data.encode('utf-8')).decode(
                'utf-8').replace('\n', '')
Yann Leboulanger's avatar
Yann Leboulanger committed
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
            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
Yann Leboulanger's avatar
Yann Leboulanger committed
244
            except kerberos.GSSError as e:
Yann Leboulanger's avatar
Yann Leboulanger committed
245
                log.info('GSSAPI authentication failed: %s' % str(e))
246 247 248 249 250 251 252
        if 'SCRAM-SHA-1-PLUS' in self.mecs and self.channel_binding != 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
Yann Leboulanger's avatar
Yann Leboulanger committed
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
        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
281 282 283
        log.info('I can only use ANONYMOUS, EXTERNAL, GSSAPI, SCRAM-SHA-1-PLUS,'
                 ' SCRAM-SHA-1, DIGEST-MD5, PLAIN and X-MESSENGER-OAUTH2'
                 ' mechanisms.')
Yann Leboulanger's avatar
Yann Leboulanger committed
284 285 286 287 288 289 290 291 292 293 294 295
        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):
296 297 298 299 300 301
            try:
                s = s.encode('utf-8')
            except:
                pass
            return ''.join(base64.b64encode(s).decode('utf-8').\
                split('\n'))
Yann Leboulanger's avatar
Yann Leboulanger committed
302 303

        incoming_data = challenge.getData()
304 305 306 307 308
        data=base64.b64decode(incoming_data.encode('utf-8'))

        if self.mechanism != 'GSSAPI':
            data=data.decode('utf-8')

Yann Leboulanger's avatar
Yann Leboulanger committed
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
        ### Handle Auth result
        def on_auth_fail(reason):
            log.info('Failed SASL authentification: %s' % reason)
            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()
            raise NodeProcessed

        if challenge.getName() == 'failure':
            if self.startsasl == SASL_FAILURE_IN_PROGRESS:
                self.MechanismHandler()
                raise NodeProcessed
            self.startsasl = SASL_FAILURE
            try:
                reason = challenge.getChildren()[0]
            except Exception:
                reason = challenge
            on_auth_fail(reason)
        elif challenge.getName() == 'success':
333
            if self.mechanism in ('SCRAM-SHA-1', 'SCRAM-SHA-1-PLUS'):
Yann Leboulanger's avatar
Yann Leboulanger committed
334
                # check data-with-success
335
                data = _scram_parse(data)
Yann Leboulanger's avatar
Yann Leboulanger committed
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
                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
359 360 361 362
        if self.mechanism != 'GSSAPI':
            log.info('Got challenge:' + data)
        else:
            log.info('Got challenge')
Yann Leboulanger's avatar
Yann Leboulanger committed
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379

        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
380
        if self.mechanism in ('SCRAM-SHA-1', 'SCRAM-SHA-1-PLUS'):
Yann Leboulanger's avatar
Yann Leboulanger committed
381 382 383
            hashfn = hashlib.sha1

            def HMAC(k, s):
384
                return hmac.new(key=k, msg=s, digestmod=hashfn).digest()
Yann Leboulanger's avatar
Yann Leboulanger committed
385

386 387 388 389 390 391 392 393
            if sys.version_info[0] == 2:
                def XOR(x, y):
                    r = (chr(ord(px) ^ ord(py)) for px, py in zip(x, y))
                    return bytes(b''.join(r))
            else:
                def XOR(x, y):
                    r = [px ^ py for px, py in zip(x, y)]
                    return bytes(r)
Yann Leboulanger's avatar
Yann Leboulanger committed
394 395 396 397 398 399 400

            def Hi(s, salt, iters):
                ii = 1
                try:
                    s = s.encode('utf-8')
                except:
                    pass
Yann Leboulanger's avatar
Yann Leboulanger committed
401
                ui_1 = HMAC(s, salt + b'\0\0\0\01')
Yann Leboulanger's avatar
Yann Leboulanger committed
402 403 404 405 406 407 408 409 410 411 412 413 414
                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 + ','
415
                data = _scram_parse(data)
416 417 418 419 420
                # 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
421 422 423 424
                if self.mechanism == 'SCRAM-SHA-1':
                    r = 'c=' + scram_base64(self.scram_gs2)
                else:
                    # Channel binding data goes in here too.
425 426
                    r = 'c=' + scram_base64(self.scram_gs2.encode('utf-8')
                        + self.channel_binding)
Yann Leboulanger's avatar
Yann Leboulanger committed
427 428
                r += ',r=' + data['r']
                self.scram_soup += r
429
                self.scram_soup = self.scram_soup.encode('utf-8')
Yann Leboulanger's avatar
Yann Leboulanger committed
430
                salt = base64.b64decode(data['s'].encode('utf-8'))
Yann Leboulanger's avatar
Yann Leboulanger committed
431 432 433
                iter = int(data['i'])
                SaltedPassword = Hi(self.password, salt, iter)
                # TODO: Could cache this, along with salt+iter.
434
                ClientKey = HMAC(SaltedPassword, b'Client Key')
Yann Leboulanger's avatar
Yann Leboulanger committed
435 436 437 438
                StoredKey = scram_H(ClientKey)
                ClientSignature = HMAC(StoredKey, self.scram_soup)
                ClientProof = XOR(ClientKey, ClientSignature)
                r += ',p=' + scram_base64(ClientProof)
439
                ServerKey = HMAC(SaltedPassword, b'Server Key')
Yann Leboulanger's avatar
Yann Leboulanger committed
440 441 442 443 444 445 446 447
                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:
448
                data = _scram_parse(data)
449 450
                if base64.b64decode(data['v'].encode('utf-8')).decode('utf-8') \
                != self.scram_ServerSignature:
Yann Leboulanger's avatar
Yann Leboulanger committed
451 452 453 454 455 456
                    # 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

457
        # DIGEST-MD5
Yann Leboulanger's avatar
Yann Leboulanger committed
458
        # magic foo...
459
        chal = _challenge_splitter(data)
Yann Leboulanger's avatar
Yann Leboulanger committed
460 461
        if not self.realm and 'realm' in chal:
            self.realm = chal['realm']
462 463
        if 'qop' in chal and ((chal['qop'] =='auth') or \
        (isinstance(chal['qop'], list) and 'auth' in chal['qop'])):
464 465 466 467 468 469 470 471
            self.resp = {'username': self.username,
                'nonce': chal['nonce'],
                'cnonce': '%x' % rndg.getrandbits(196),
                'nc': ('00000001'),  # ToDo: Is this a tupel or only a string?
                'qop': 'auth',
                'digest-uri': 'xmpp/' + self._owner.Server,
                'charset': 'utf-8'
            }
Yann Leboulanger's avatar
Yann Leboulanger committed
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492
            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:
493
            string = string.encode('iso-8859-1')
Yann Leboulanger's avatar
Yann Leboulanger committed
494 495 496 497 498 499
        except UnicodeEncodeError:
            pass
        return string

    def set_password(self, password):
        self.password = '' if password is None else password
500
        if self.mechanism in ('SCRAM-SHA-1', 'SCRAM-SHA-1-PLUS'):
501 502
            self.client_nonce = '%x' % rndg.getrandbits(196)
            self.scram_soup = 'n=' + self.username + ',r=' + self.client_nonce
503
            if self.mechanism == 'SCRAM-SHA-1':
504 505 506 507 508 509
                if self.channel_binding == 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,,'
510 511
            else:
                self.scram_gs2 = 'p=tls-unique,,'
512 513
            sasl_data = base64.b64encode((self.scram_gs2 + self.scram_soup).\
                encode('utf-8')).decode('utf-8').replace('\n', '')
Yann Leboulanger's avatar
Yann Leboulanger committed
514 515 516 517 518 519 520
            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])),
521 522 523 524 525 526 527 528 529 530 531 532
                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')]))
Yann Leboulanger's avatar
Yann Leboulanger committed
533
            self.resp['response'] = response
Yann Leboulanger's avatar
Yann Leboulanger committed
534
            sasl_data = ''
Yann Leboulanger's avatar
Yann Leboulanger committed
535 536 537
            for key in ('charset', 'username', 'realm', 'nonce', 'nc', 'cnonce',
            'digest-uri', 'response', 'qop'):
                if key in ('nc', 'qop', 'response', 'charset'):
Yann Leboulanger's avatar
Yann Leboulanger committed
538
                    sasl_data += "%s=%s," % (key, self.resp[key])
Yann Leboulanger's avatar
Yann Leboulanger committed
539
                else:
Yann Leboulanger's avatar
Yann Leboulanger committed
540
                    sasl_data += '%s="%s",' % (key, self.resp[key])
541 542
            sasl_data = base64.b64encode(sasl_data[:-1].encode('utf-8')).\
                decode('utf-8').replace('\r', '').replace('\n', '')
Yann Leboulanger's avatar
Yann Leboulanger committed
543 544 545
            node = Node('response', attrs={'xmlns': NS_SASL},
                payload=[sasl_data])
        elif self.mechanism == 'PLAIN':
Yann Leboulanger's avatar
Yann Leboulanger committed
546
            sasl_data = '\x00%s\x00%s' % (self.username, self.password)
547 548
            sasl_data = base64.b64encode(sasl_data.encode('utf-8')).decode(
                'utf-8').replace('\n', '')
Yann Leboulanger's avatar
Yann Leboulanger committed
549 550 551 552 553 554 555 556 557 558 559
            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 NonBlockingNonSASL(PlugIn):
    """
560
    Implements old Non-SASL (XEP-0078) authentication used in jabberd1.4 and
Yann Leboulanger's avatar
Yann Leboulanger committed
561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
    transport authentication
    """

    def __init__(self, user, password, resource, on_auth):
        """
        Caches username, password and resource for auth
        """
        PlugIn.__init__(self)
        self.user = user
        if password is None:
            self.password = ''
        else:
            self.password = password
        self.resource = resource
        self.on_auth = on_auth

    def plugin(self, owner):
        """
        Determine the best auth method (digest/0k/plain) and use it for auth.
        Returns used method name on success. Used internally
        """
        log.info('Querying server about possible auth methods')
        self.owner = owner

        owner.Dispatcher.SendAndWaitForResponse(
            Iq('get', NS_AUTH, payload=[Node('username', payload=[self.user])]),
            func=self._on_username)

    def _on_username(self, resp):
        if not isResultNode(resp):
            log.info('No result node arrived! Aborting...')
            return self.on_auth(None)

        iq=Iq(typ='set', node=resp)
        query = iq.getTag('query')
        query.setTagData('username', self.user)
        query.setTagData('resource', self.resource)

        if query.getTag('digest'):
            log.info("Performing digest authentication")
            query.setTagData('digest',
                hashlib.sha1(self.owner.Dispatcher.Stream._document_attrs['id']
                + self.password).hexdigest())
            if query.getTag('password'):
                query.delChild('password')
            self._method = 'digest'
        elif query.getTag('token'):
            token = query.getTagData('token')
            seq = query.getTagData('sequence')
            log.info("Performing zero-k authentication")

            def hasher(s):
                return hashlib.sha1(s).hexdigest()

            def hash_n_times(s, count):
                return count and hasher(hash_n_times(s, count-1)) or s

            hash_ = hash_n_times(hasher(hasher(self.password) + token),
                int(seq))
            query.setTagData('hash', hash_)
            self._method='0k'
        else:
623
            log.warning("Secure methods unsupported, performing plain text \
Yann Leboulanger's avatar
Yann Leboulanger committed
624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665
                authentication")
            self._method = 'plain'
            self._owner._caller.get_password(self._on_password, self._method)
            return
        resp = self.owner.Dispatcher.SendAndWaitForResponse(iq,
            func=self._on_auth)

    def _on_password(self, password):
        self.password = '' if password is None else password
        iq=Iq('set', NS_AUTH)
        query = iq.getTag('query')
        query.setTagData('username', self.user)
        query.setTagData('resource', self.resource)
        query.setTagData('password', self.password)
        resp = self.owner.Dispatcher.SendAndWaitForResponse(iq,
            func=self._on_auth)

    def _on_auth(self, resp):
        if isResultNode(resp):
            log.info('Sucessfully authenticated with remote host.')
            self.owner.User = self.user
            self.owner.Resource = self.resource
            self.owner._registered_name = self.owner.User + '@' + \
                self.owner.Server+ '/' + self.owner.Resource
            return self.on_auth(self._method)
        log.info('Authentication failed!')
        return self.on_auth(None)


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.bound = None
        self.supports_sm = False
        self.resuming = False

    def plugin(self, owner):
666
        """ Start resource binding, if allowed at this time. Used internally. """
Yann Leboulanger's avatar
Yann Leboulanger committed
667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739
        if 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):
        """
        Determine if server supports resource binding and set some internal
        attributes accordingly.

        It also checks if server supports stream management
        """

        if feats.getTag('sm', namespace=NS_STREAM_MGMT):
            self.supports_sm = True # server supports stream management
            if self.resuming:
                self._owner._caller.sm.resume_request()

        if not feats.getTag('bind', namespace=NS_BIND):
            log.info('Server does not requested binding.')
            # we try to bind resource anyway
            #self.bound='failure'
            self.bound = []
            return
        if feats.getTag('session', namespace=NS_SESSION):
            self.session = 1
        else:
            self.session = -1
        self.bound = []

    def plugout(self):
        """
        Remove Bind handler from owner's dispatcher. Used internally
        """
        self._owner.UnregisterHandler('features', self.FeaturesHandler,
            xmlns=NS_STREAMS)

    def NonBlockingBind(self, resource=None, on_bound=None):
        """
        Perform binding. Use provided resource name or random (if not provided).
        """
        if self.resuming: # We don't bind if we resume the stream
            return
        self.on_bound = on_bound
        self._resource = resource
        if self._resource:
            self._resource = [Node('resource', payload=[self._resource])]
        else:
            self._resource = []

        self._owner.onreceive(None)
        self._owner.Dispatcher.SendAndWaitForResponse(
            Protocol('iq', typ='set', payload=[Node('bind',
            attrs={'xmlns': NS_BIND}, payload=self._resource)]),
            func=self._on_bound)

    def _on_bound(self, resp):
        if isResultNode(resp):
            if resp.getTag('bind') and resp.getTag('bind').getTagData('jid'):
                self.bound.append(resp.getTag('bind').getTagData('jid'))
                log.info('Successfully bound %s.' % self.bound[-1])
                jid = JID(resp.getTag('bind').getTagData('jid'))
                self._owner.User = jid.getNode()
                self._owner.Resource = jid.getResource()
                # Only negociate stream management after bounded
                sm = self._owner._caller.sm
                if self.supports_sm:
                    # starts negociation
740
                    sm.supports_sm = True
Yann Leboulanger's avatar
Yann Leboulanger committed
741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770
                    sm.set_owner(self._owner)
                    sm.negociate()
                    self._owner.Dispatcher.sm = sm

                if hasattr(self, 'session') and self.session == -1:
                    # Server don't want us to initialize a session
                    log.info('No session required.')
                    self.on_bound('ok')
                else:
                    self._owner.SendAndWaitForResponse(Protocol('iq', typ='set',
                        payload=[Node('session', attrs={'xmlns':NS_SESSION})]),
                        func=self._on_session)
                return
        if resp:
            log.info('Binding failed: %s.' % resp.getTag('error'))
            self.on_bound(None)
        else:
            log.info('Binding failed: timeout expired.')
            self.on_bound(None)

    def _on_session(self, resp):
        self._owner.onreceive(None)
        if isResultNode(resp):
            log.info('Successfully opened session.')
            self.session = 1
            self.on_bound('ok')
        else:
            log.error('Session open failed.')
            self.session = 0
            self.on_bound(None)