1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 from protocol import NS_SASL, NS_SESSION, NS_STREAMS, NS_BIND, NS_AUTH
25 from protocol import NS_STREAM_MGMT
26 from protocol import Node, NodeProcessed, isResultNode, Iq, Protocol, JID
27 from plugin import PlugIn
28 from smacks import Smacks
29 import base64
30 import random
31 import itertools
32 import dispatcher_nb
33 import hashlib
34 import hmac
35 import hashlib
36
37 import logging
38 log = logging.getLogger('nbxmpp.auth_nb')
39
40 -def HH(some): return hashlib.md5(some).hexdigest()
41 -def H(some): return hashlib.md5(some).digest()
42 -def C(some): return ':'.join(some)
43
44 try:
45 kerberos = __import__('kerberos')
46 have_kerberos = True
47 except ImportError:
48 have_kerberos = False
49
50 GSS_STATE_STEP = 0
51 GSS_STATE_WRAP = 1
52 SASL_FAILURE_IN_PROGRESS = 'failure-in-process'
53 SASL_FAILURE = 'failure'
54 SASL_SUCCESS = 'success'
55 SASL_UNSUPPORTED = 'not-supported'
56 SASL_IN_PROCESS = 'in-process'
59 """
60 Helper function that creates a dict from challenge string
61
62 Sample challenge string:
63 - username="example.org",realm="somerealm",
64 nonce="OA6MG9tEQGm2hh",cnonce="OA6MHXh6VqTrRk",
65 nc=00000001,qop="auth,auth-int,auth-conf",charset=utf-8
66
67 Expected result for challan:
68 - dict['qop'] = ('auth','auth-int','auth-conf')
69 - dict['realm'] = 'somerealm'
70 """
71 X_KEYWORD, X_VALUE, X_END = 0, 1, 2
72 quotes_open = False
73 keyword, value = '', ''
74 dict_ = {}
75 arr = None
76
77 expecting = X_KEYWORD
78 for iter_ in range(len(data) + 1):
79 end = False
80 if iter_ == len(data):
81 expecting = X_END
82 end = True
83 else:
84 char = data[iter_]
85 if expecting == X_KEYWORD:
86 if char == '=':
87 expecting = X_VALUE
88 elif char in (',', ' ', '\t'):
89 pass
90 else:
91 keyword = '%s%c' % (keyword, char)
92 elif expecting == X_VALUE:
93 if char == '"':
94 if quotes_open:
95 end = True
96 else:
97 quotes_open = True
98 elif char in (',', ' ', '\t'):
99 if quotes_open:
100 if not arr:
101 arr = [value]
102 else:
103 arr.append(value)
104 value = ""
105 else:
106 end = True
107 else:
108 value = '%s%c' % (value, char)
109 if end:
110 if arr:
111 arr.append(value)
112 dict_[keyword] = arr
113 arr = None
114 else:
115 dict_[keyword] = value
116 value, keyword = '', ''
117 expecting = X_KEYWORD
118 quotes_open = False
119 return dict_
120
122 return dict(s.split('=', 1) for s in chatter.split(','))
123
125 """
126 Implements SASL authentication. Can be plugged into NonBlockingClient
127 to start authentication
128 """
129
130 - def __init__(self, username, password, on_sasl):
131 """
132 :param username: XMPP username
133 :param password: XMPP password
134 :param on_sasl: Callback, will be called after each SASL auth-step.
135 """
136 PlugIn.__init__(self)
137 self.username = username
138 self.password = password
139 self.on_sasl = on_sasl
140 self.realm = None
141
153
170
172 """
173 Start authentication. Result can be obtained via "SASL.startsasl"
174 attribute and will be either SASL_SUCCESS or SASL_FAILURE
175
176 Note that successfull auth will take at least two Dispatcher.Process()
177 calls.
178 """
179 if self.startsasl:
180 pass
181 elif self._owner.Dispatcher.Stream.features:
182 try:
183 self.FeaturesHandler(self._owner.Dispatcher,
184 self._owner.Dispatcher.Stream.features)
185 except NodeProcessed:
186 pass
187 else:
188 self._owner.RegisterHandler('features',
189 self.FeaturesHandler, xmlns=NS_STREAMS)
190
209
211 if 'ANONYMOUS' in self.mecs and self.username is None:
212 self.mecs.remove('ANONYMOUS')
213 node = Node('auth', attrs={'xmlns': NS_SASL,
214 'mechanism': 'ANONYMOUS'})
215 self.mechanism = 'ANONYMOUS'
216 self.startsasl = SASL_IN_PROCESS
217 self._owner.send(str(node))
218 raise NodeProcessed
219 if "EXTERNAL" in self.mecs:
220 self.mecs.remove('EXTERNAL')
221 sasl_data = u'%s@%s' % (self.username, self._owner.Server)
222 sasl_data = sasl_data.encode('utf-8').encode('base64').replace(
223 '\n', '')
224 node = Node('auth', attrs={'xmlns': NS_SASL,
225 'mechanism': 'EXTERNAL'}, payload=[sasl_data])
226 self.mechanism = 'EXTERNAL'
227 self.startsasl = SASL_IN_PROCESS
228 self._owner.send(str(node))
229 raise NodeProcessed
230 if 'GSSAPI' in self.mecs and have_kerberos:
231 self.mecs.remove('GSSAPI')
232 try:
233 self.gss_vc = kerberos.authGSSClientInit('xmpp@' + \
234 self._owner.xmpp_hostname)[1]
235 kerberos.authGSSClientStep(self.gss_vc, '')
236 response = kerberos.authGSSClientResponse(self.gss_vc)
237 node=Node('auth', attrs={'xmlns': NS_SASL,
238 'mechanism': 'GSSAPI'}, payload=(response or ''))
239 self.mechanism = 'GSSAPI'
240 self.gss_step = GSS_STATE_STEP
241 self.startsasl = SASL_IN_PROCESS
242 self._owner.send(str(node))
243 raise NodeProcessed
244 except kerberos.GSSError, e:
245 log.info('GSSAPI authentication failed: %s' % str(e))
246 if 'SCRAM-SHA-1' in self.mecs:
247 self.mecs.remove('SCRAM-SHA-1')
248 self.mechanism = 'SCRAM-SHA-1'
249 self._owner._caller.get_password(self.set_password, self.mechanism)
250 self.scram_step = 0
251 self.startsasl = SASL_IN_PROCESS
252 raise NodeProcessed
253 if 'DIGEST-MD5' in self.mecs:
254 self.mecs.remove('DIGEST-MD5')
255 node = Node('auth', attrs={'xmlns': NS_SASL,
256 'mechanism': 'DIGEST-MD5'})
257 self.mechanism = 'DIGEST-MD5'
258 self.startsasl = SASL_IN_PROCESS
259 self._owner.send(str(node))
260 raise NodeProcessed
261 if 'PLAIN' in self.mecs:
262 self.mecs.remove('PLAIN')
263 self.mechanism = 'PLAIN'
264 self._owner._caller.get_password(self.set_password, self.mechanism)
265 self.startsasl = SASL_IN_PROCESS
266 raise NodeProcessed
267 if 'X-MESSENGER-OAUTH2' in self.mecs:
268 self.mecs.remove('X-MESSENGER-OAUTH2')
269 self.mechanism = 'X-MESSENGER-OAUTH2'
270 self._owner._caller.get_password(self.set_password, self.mechanism)
271 self.startsasl = SASL_IN_PROCESS
272 raise NodeProcessed
273 self.startsasl = SASL_FAILURE
274 log.info('I can only use EXTERNAL, SCRAM-SHA-1, DIGEST-MD5, GSSAPI and '
275 'PLAIN mecanisms.')
276 if self.on_sasl:
277 self.on_sasl()
278 return
279
281 """
282 Perform next SASL auth step. Used internally
283 """
284 if challenge.getNamespace() != NS_SASL:
285 return
286
287 def scram_base64(s):
288 return ''.join(s.encode('base64').split('\n'))
289
290 incoming_data = challenge.getData()
291 data=base64.decodestring(incoming_data)
292
293 def on_auth_fail(reason):
294 log.info('Failed SASL authentification: %s' % reason)
295 self._owner.send(str(Node('abort', attrs={'xmlns': NS_SASL})))
296 if len(self.mecs) > 0:
297
298
299 self.startsasl = SASL_FAILURE_IN_PROGRESS
300 raise NodeProcessed
301 if self.on_sasl:
302 self.on_sasl()
303 raise NodeProcessed
304
305 if challenge.getName() == 'failure':
306 if self.startsasl == SASL_FAILURE_IN_PROGRESS:
307 self.MechanismHandler()
308 raise NodeProcessed
309 self.startsasl = SASL_FAILURE
310 try:
311 reason = challenge.getChildren()[0]
312 except Exception:
313 reason = challenge
314 on_auth_fail(reason)
315 elif challenge.getName() == 'success':
316 if self.mechanism == 'SCRAM-SHA-1':
317
318 data = scram_parse(data)
319 if data['v'] != scram_base64(self.scram_ServerSignature):
320 on_auth_fail('ServerSignature is wrong')
321
322 self.startsasl = SASL_SUCCESS
323 log.info('Successfully authenticated with remote server.')
324 handlers = self._owner.Dispatcher.dumpHandlers()
325
326
327
328
329
330 old_features = self._owner.Dispatcher.Stream.features
331 self._owner.Dispatcher.PlugOut()
332 dispatcher_nb.Dispatcher.get_instance().PlugIn(self._owner,
333 after_SASL=True, old_features=old_features)
334 self._owner.Dispatcher.restoreHandlers(handlers)
335 self._owner.User = self.username
336
337 if self.on_sasl:
338 self.on_sasl()
339 raise NodeProcessed
340
341
342 log.info('Got challenge:' + data)
343
344 if self.mechanism == 'GSSAPI':
345 if self.gss_step == GSS_STATE_STEP:
346 rc = kerberos.authGSSClientStep(self.gss_vc, incoming_data)
347 if rc != kerberos.AUTH_GSS_CONTINUE:
348 self.gss_step = GSS_STATE_WRAP
349 elif self.gss_step == GSS_STATE_WRAP:
350 rc = kerberos.authGSSClientUnwrap(self.gss_vc, incoming_data)
351 response = kerberos.authGSSClientResponse(self.gss_vc)
352 rc = kerberos.authGSSClientWrap(self.gss_vc, response,
353 kerberos.authGSSClientUserName(self.gss_vc))
354 response = kerberos.authGSSClientResponse(self.gss_vc)
355 if not response:
356 response = ''
357 self._owner.send(Node('response', attrs={'xmlns': NS_SASL},
358 payload=response).__str__())
359 raise NodeProcessed
360 if self.mechanism == 'SCRAM-SHA-1':
361 hashfn = hashlib.sha1
362
363 def HMAC(k, s):
364 return hmac.HMAC(key=k, msg=s, digestmod=hashfn).digest()
365
366 def XOR(x, y):
367 r = (chr(ord(px) ^ ord(py)) for px, py in zip(x, y))
368 return ''.join(r)
369
370 def Hi(s, salt, iters):
371 ii = 1
372 try:
373 s = s.encode('utf-8')
374 except:
375 pass
376 ui_1 = HMAC(s, salt + '\0\0\0\01')
377 ui = ui_1
378 for i in range(iters - 1):
379 ii += 1
380 ui_1 = HMAC(s, ui_1)
381 ui = XOR(ui, ui_1)
382 return ui
383
384 def scram_H(s):
385 return hashfn(s).digest()
386
387 if self.scram_step == 0:
388 self.scram_step = 1
389 self.scram_soup += ',' + data + ','
390 data = scram_parse(data)
391
392
393 r = 'c=' + scram_base64(self.scram_gs2)
394 r += ',r=' + data['r']
395 self.scram_soup += r
396 salt = data['s'].decode('base64')
397 iter = int(data['i'])
398 SaltedPassword = Hi(self.password, salt, iter)
399
400 ClientKey = HMAC(SaltedPassword, 'Client Key')
401 StoredKey = scram_H(ClientKey)
402 ClientSignature = HMAC(StoredKey, self.scram_soup)
403 ClientProof = XOR(ClientKey, ClientSignature)
404 r += ',p=' + scram_base64(ClientProof)
405 ServerKey = HMAC(SaltedPassword, 'Server Key')
406 self.scram_ServerSignature = HMAC(ServerKey, self.scram_soup)
407 sasl_data = scram_base64(r)
408 node = Node('response', attrs={'xmlns': NS_SASL},
409 payload=[sasl_data])
410 self._owner.send(str(node))
411 raise NodeProcessed
412
413 if self.scram_step == 1:
414 data = scram_parse(data)
415 if data['v'].decode('base64') != self.scram_ServerSignature:
416
417 raise Exception
418 node = Node('response', attrs={'xmlns': NS_SASL});
419 self._owner.send(str(node))
420 raise NodeProcessed
421
422
423 chal = challenge_splitter(data)
424 if not self.realm and 'realm' in chal:
425 self.realm = chal['realm']
426 if 'qop' in chal and ((isinstance(chal['qop'], str) and \
427 chal['qop'] =='auth') or (isinstance(chal['qop'], list) and 'auth' in \
428 chal['qop'])):
429 self.resp = {}
430 self.resp['username'] = self.username
431 if self.realm:
432 self.resp['realm'] = self.realm
433 else:
434 self.resp['realm'] = self._owner.Server
435 self.resp['nonce'] = chal['nonce']
436 self.resp['cnonce'] = ''.join("%x" % randint(0, 2**28) for randint \
437 in itertools.repeat(random.randint, 7))
438 self.resp['nc'] = ('00000001')
439 self.resp['qop'] = 'auth'
440 self.resp['digest-uri'] = 'xmpp/' + self._owner.Server
441 self.resp['charset'] = 'utf-8'
442
443 self._owner._caller.get_password(self.set_password, self.mechanism)
444 elif 'rspauth' in chal:
445
446 if chal['rspauth'] != self.digest_rspauth:
447 on_auth_fail('rspauth is wrong')
448 self._owner.send(str(Node('response', attrs={'xmlns':NS_SASL})))
449 else:
450 self.startsasl = SASL_FAILURE
451 log.info('Failed SASL authentification: unknown challenge')
452 if self.on_sasl:
453 self.on_sasl()
454 raise NodeProcessed
455
456 @staticmethod
458 try:
459 string = string.decode('utf-8').encode('iso-8859-1')
460 except UnicodeEncodeError:
461 pass
462 return string
463
465 self.password = '' if password is None else password
466 if self.mechanism == 'SCRAM-SHA-1':
467 nonce = ''.join('%x' % randint(0, 2 ** 28) for randint in \
468 itertools.repeat(random.randint, 7))
469 self.scram_soup = 'n=' + self.username + ',r=' + nonce
470 self.scram_gs2 = 'n,,'
471 sasl_data = (self.scram_gs2 + self.scram_soup).encode('base64').\
472 replace('\n', '')
473 node = Node('auth', attrs={'xmlns': NS_SASL,
474 'mechanism': self.mechanism}, payload=[sasl_data])
475 elif self.mechanism == 'DIGEST-MD5':
476 hash_username = self._convert_to_iso88591(self.resp['username'])
477 hash_realm = self._convert_to_iso88591(self.resp['realm'])
478 hash_password = self._convert_to_iso88591(self.password)
479 A1 = C([H(C([hash_username, hash_realm, hash_password])),
480 self.resp['nonce'], self.resp['cnonce']])
481 A2 = C(['AUTHENTICATE', self.resp['digest-uri']])
482 response = HH(C([HH(A1), self.resp['nonce'], self.resp['nc'],
483 self.resp['cnonce'], self.resp['qop'], HH(A2)]))
484 A2 = C(['', self.resp['digest-uri']])
485 self.digest_rspauth = HH(C([HH(A1), self.resp['nonce'],
486 self.resp['nc'], self.resp['cnonce'], self.resp['qop'],
487 HH(A2)]))
488 self.resp['response'] = response
489 sasl_data = u''
490 for key in ('charset', 'username', 'realm', 'nonce', 'nc', 'cnonce',
491 'digest-uri', 'response', 'qop'):
492 if key in ('nc', 'qop', 'response', 'charset'):
493 sasl_data += u"%s=%s," % (key, self.resp[key])
494 else:
495 sasl_data += u'%s="%s",' % (key, self.resp[key])
496 sasl_data = sasl_data[:-1].encode('utf-8').encode('base64').replace(
497 '\r', '').replace('\n', '')
498 node = Node('response', attrs={'xmlns': NS_SASL},
499 payload=[sasl_data])
500 elif self.mechanism == 'PLAIN':
501 sasl_data = u'\x00%s\x00%s' % (self.username, self.password)
502 sasl_data = sasl_data.encode('utf-8').encode('base64').replace(
503 '\n', '')
504 node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'PLAIN'},
505 payload=[sasl_data])
506 elif self.mechanism == 'X-MESSENGER-OAUTH2':
507 node = Node('auth', attrs={'xmlns': NS_SASL,
508 'mechanism': 'X-MESSENGER-OAUTH2'})
509 node.addData(password)
510 self._owner.send(str(node))
511
514 """
515 Implements old Non-SASL (JEP-0078) authentication used in jabberd1.4 and
516 transport authentication
517 """
518
519 - def __init__(self, user, password, resource, on_auth):
520 """
521 Caches username, password and resource for auth
522 """
523 PlugIn.__init__(self)
524 self.user = user
525 if password is None:
526 self.password = ''
527 else:
528 self.password = password
529 self.resource = resource
530 self.on_auth = on_auth
531
533 """
534 Determine the best auth method (digest/0k/plain) and use it for auth.
535 Returns used method name on success. Used internally
536 """
537 log.info('Querying server about possible auth methods')
538 self.owner = owner
539
540 owner.Dispatcher.SendAndWaitForResponse(
541 Iq('get', NS_AUTH, payload=[Node('username', payload=[self.user])]),
542 func=self._on_username)
543
545 if not isResultNode(resp):
546 log.info('No result node arrived! Aborting...')
547 return self.on_auth(None)
548
549 iq=Iq(typ='set', node=resp)
550 query = iq.getTag('query')
551 query.setTagData('username', self.user)
552 query.setTagData('resource', self.resource)
553
554 if query.getTag('digest'):
555 log.info("Performing digest authentication")
556 query.setTagData('digest',
557 hashlib.sha1(self.owner.Dispatcher.Stream._document_attrs['id']
558 + self.password).hexdigest())
559 if query.getTag('password'):
560 query.delChild('password')
561 self._method = 'digest'
562 elif query.getTag('token'):
563 token = query.getTagData('token')
564 seq = query.getTagData('sequence')
565 log.info("Performing zero-k authentication")
566
567 def hasher(s):
568 return hashlib.sha1(s).hexdigest()
569
570 def hash_n_times(s, count):
571 return count and hasher(hash_n_times(s, count-1)) or s
572
573 hash_ = hash_n_times(hasher(hasher(self.password) + token),
574 int(seq))
575 query.setTagData('hash', hash_)
576 self._method='0k'
577 else:
578 log.warn("Secure methods unsupported, performing plain text \
579 authentication")
580 self._method = 'plain'
581 self._owner._caller.get_password(self._on_password, self._method)
582 return
583 resp = self.owner.Dispatcher.SendAndWaitForResponse(iq,
584 func=self._on_auth)
585
595
597 if isResultNode(resp):
598 log.info('Sucessfully authenticated with remote host.')
599 self.owner.User = self.user
600 self.owner.Resource = self.resource
601 self.owner._registered_name = self.owner.User + '@' + \
602 self.owner.Server+ '/' + self.owner.Resource
603 return self.on_auth(self._method)
604 log.info('Authentication failed!')
605 return self.on_auth(None)
606
609 """
610 Bind some JID to the current connection to allow router know of our
611 location. Must be plugged after successful SASL auth
612 """
613
615 PlugIn.__init__(self)
616 self.bound = None
617 self.supports_sm = False
618 self.resuming = False
619
631
633 """
634 Determine if server supports resource binding and set some internal
635 attributes accordingly.
636
637 It also checks if server supports stream management
638 """
639
640 if feats.getTag('sm', namespace=NS_STREAM_MGMT):
641 self.supports_sm = True
642 if self.resuming:
643 self._owner._caller.sm.resume_request()
644
645 if not feats.getTag('bind', namespace=NS_BIND):
646 log.info('Server does not requested binding.')
647
648
649 self.bound = []
650 return
651 if feats.getTag('session', namespace=NS_SESSION):
652 self.session = 1
653 else:
654 self.session = -1
655 self.bound = []
656
663
665 """
666 Perform binding. Use provided resource name or random (if not provided).
667 """
668 if self.resuming:
669 return
670 self.on_bound = on_bound
671 self._resource = resource
672 if self._resource:
673 self._resource = [Node('resource', payload=[self._resource])]
674 else:
675 self._resource = []
676
677 self._owner.onreceive(None)
678 self._owner.Dispatcher.SendAndWaitForResponse(
679 Protocol('iq', typ='set', payload=[Node('bind',
680 attrs={'xmlns': NS_BIND}, payload=self._resource)]),
681 func=self._on_bound)
682
684 if isResultNode(resp):
685 if resp.getTag('bind') and resp.getTag('bind').getTagData('jid'):
686 self.bound.append(resp.getTag('bind').getTagData('jid'))
687 log.info('Successfully bound %s.' % self.bound[-1])
688 jid = JID(resp.getTag('bind').getTagData('jid'))
689 self._owner.User = jid.getNode()
690 self._owner.Resource = jid.getResource()
691
692 sm = self._owner._caller.sm
693 if self.supports_sm:
694
695 sm.supports_sm = True
696 sm.set_owner(self._owner)
697 sm.negociate()
698 self._owner.Dispatcher.sm = sm
699
700 if hasattr(self, 'session') and self.session == -1:
701
702 log.info('No session required.')
703 self.on_bound('ok')
704 else:
705 self._owner.SendAndWaitForResponse(Protocol('iq', typ='set',
706 payload=[Node('session', attrs={'xmlns':NS_SESSION})]),
707 func=self._on_session)
708 return
709 if resp:
710 log.info('Binding failed: %s.' % resp.getTag('error'))
711 self.on_bound(None)
712 else:
713 log.info('Binding failed: timeout expired.')
714 self.on_bound(None)
715
717 self._owner.onreceive(None)
718 if isResultNode(resp):
719 log.info('Successfully opened session.')
720 self.session = 1
721 self.on_bound('ok')
722 else:
723 log.error('Session open failed.')
724 self.session = 0
725 self.on_bound(None)
726