Philipp Hörist pushed to branch master at gajim / python-nbxmpp
Commits:
-
f2a20338
by Philipp Hörist at 2018-12-09T17:43:48Z
9 changed files:
- nbxmpp/__init__.py
- + nbxmpp/auth.py
- − nbxmpp/auth_nb.py
- + nbxmpp/bind.py
- nbxmpp/client_nb.py
- nbxmpp/const.py
- nbxmpp/protocol.py
- + nbxmpp/util.py
- + test/unit/test_sasl_scram.py
Changes:
| ... | ... | @@ -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
|
| 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
|
| 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()
|
| 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 |
| ... | ... | @@ -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)
|
| ... | ... | @@ -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
|
| ... | ... | @@ -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',
|
| 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()
|
| 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)
|