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