Package common :: Module stanza_session
[hide private]
[frames] | no frames]

Source Code for Module common.stanza_session

   1  # -*- coding:utf-8 -*- 
   2  ## src/common/stanza_session.py 
   3  ## 
   4  ## Copyright (C) 2007-2010 Yann Leboulanger <asterix AT lagaule.org> 
   5  ## Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com> 
   6  ## Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com> 
   7  ##                         Jean-Marie Traissard <jim AT lapin.org> 
   8  ## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org> 
   9  ## 
  10  ## This file is part of Gajim. 
  11  ## 
  12  ## Gajim is free software; you can redistribute it and/or modify 
  13  ## it under the terms of the GNU General Public License as published 
  14  ## by the Free Software Foundation; version 3 only. 
  15  ## 
  16  ## Gajim is distributed in the hope that it will be useful, 
  17  ## but WITHOUT ANY WARRANTY; without even the implied warranty of 
  18  ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 
  19  ## GNU General Public License for more details. 
  20  ## 
  21  ## You should have received a copy of the GNU General Public License 
  22  ## along with Gajim. If not, see <http://www.gnu.org/licenses/>. 
  23  ## 
  24   
  25  from common import gajim 
  26  from common import xmpp 
  27  from common.exceptions import DecryptionError, NegotiationError 
  28  import xmpp.c14n 
  29   
  30  import itertools 
  31  import random 
  32  import string 
  33  import time 
  34  import base64 
  35  import os 
  36  from hashlib import sha256 
  37  from hmac import HMAC 
  38  from common import crypto 
  39   
  40  if gajim.HAVE_PYCRYPTO: 
  41      from Crypto.Cipher import AES 
  42      from Crypto.PublicKey import RSA 
  43   
  44      from common import dh 
  45      import secrets 
  46   
  47  XmlDsig = 'http://www.w3.org/2000/09/xmldsig#' 
  48   
49 -class StanzaSession(object):
50 ''' 51 '''
52 - def __init__(self, conn, jid, thread_id, type_):
53 ''' 54 ''' 55 self.conn = conn 56 self.jid = jid 57 self.type = type_ 58 self.resource = None 59 60 if thread_id: 61 self.received_thread_id = True 62 self.thread_id = thread_id 63 else: 64 self.received_thread_id = False 65 if type_ == 'normal': 66 self.thread_id = None 67 else: 68 self.thread_id = self.generate_thread_id() 69 70 self.loggable = True 71 72 self.last_send = 0 73 self.last_receive = 0 74 self.status = None 75 self.negotiated = {}
76
77 - def is_loggable(self):
78 return self.loggable and gajim.config.should_log(self.conn.name, self.jid)
79
80 - def get_to(self):
81 to = str(self.jid) 82 if self.resource and not to.endswith(self.resource): 83 to += '/' + self.resource 84 return to
85
86 - def remove_events(self, types):
87 """ 88 Remove events associated with this session from the queue 89 90 Returns True if any events were removed (unlike events.py remove_events) 91 """ 92 any_removed = False 93 94 for j in (self.jid, self.jid.getStripped()): 95 for event in gajim.events.get_events(self.conn.name, j, types=types): 96 # the event wasn't in this session 97 if (event.type_ == 'chat' and event.parameters[8] != self) or \ 98 (event.type_ == 'printed_chat' and event.parameters[0].session != \ 99 self): 100 continue 101 102 # events.remove_events returns True when there were no events 103 # for some reason 104 r = gajim.events.remove_events(self.conn.name, j, event) 105 106 if not r: 107 any_removed = True 108 109 return any_removed
110
111 - def generate_thread_id(self):
112 return ''.join([f(string.ascii_letters) for f in itertools.repeat( 113 random.choice, 32)])
114
115 - def send(self, msg):
116 if self.thread_id: 117 msg.NT.thread = self.thread_id 118 119 msg.setAttr('to', self.get_to()) 120 self.conn.send_stanza(msg) 121 122 if isinstance(msg, xmpp.Message): 123 self.last_send = time.time()
124
125 - def reject_negotiation(self, body=None):
126 msg = xmpp.Message() 127 feature = msg.NT.feature 128 feature.setNamespace(xmpp.NS_FEATURE) 129 130 x = xmpp.DataForm(typ='submit') 131 x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn')) 132 x.addChild(node=xmpp.DataField(name='accept', value='0')) 133 134 feature.addChild(node=x) 135 136 if body: 137 msg.setBody(body) 138 139 self.send(msg) 140 141 self.cancelled_negotiation()
142
143 - def cancelled_negotiation(self):
144 """ 145 A negotiation has been cancelled, so reset this session to its default 146 state 147 """ 148 if self.control: 149 self.control.on_cancel_session_negotiation() 150 151 self.status = None 152 self.negotiated = {}
153
154 - def terminate(self, send_termination = True):
155 # only send termination message if we've sent a message and think they 156 # have XEP-0201 support 157 if send_termination and self.last_send > 0 and \ 158 (self.received_thread_id or self.last_receive == 0): 159 msg = xmpp.Message() 160 feature = msg.NT.feature 161 feature.setNamespace(xmpp.NS_FEATURE) 162 163 x = xmpp.DataForm(typ='submit') 164 x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn')) 165 x.addChild(node=xmpp.DataField(name='terminate', value='1')) 166 167 feature.addChild(node=x) 168 169 self.send(msg) 170 171 self.status = None
172
173 - def acknowledge_termination(self):
174 # we could send an acknowledgement message to the remote client here 175 self.status = None
176 177
178 -class EncryptedStanzaSession(StanzaSession):
179 """ 180 An encrypted stanza negotiation has several states. They arerepresented as 181 the following values in the 'status' attribute of the session object: 182 183 1. None: 184 default state 185 2. 'requested-e2e': 186 this client has initiated an esession negotiation and is waiting 187 for a response 188 3. 'responded-e2e': 189 this client has responded to an esession negotiation request and 190 is waiting for the initiator to identify itself and complete the 191 negotiation 192 4. 'identified-alice': 193 this client identified itself and is waiting for the responder to 194 identify itself and complete the negotiation 195 5. 'active': 196 an encrypted session has been successfully negotiated. messages 197 of any of the types listed in 'encryptable_stanzas' should be 198 encrypted before they're sent. 199 200 The transition between these states is handled in gajim.py's 201 handle_session_negotiation method. 202 """ 203
204 - def __init__(self, conn, jid, thread_id, type_='chat'):
205 StanzaSession.__init__(self, conn, jid, thread_id, type_='chat') 206 207 self.xes = {} 208 self.es = {} 209 self.n = 128 210 self.enable_encryption = False 211 212 # _s denotes 'self' (ie. this client) 213 self._kc_s = None 214 # _o denotes 'other' (ie. the client at the other end of the session) 215 self._kc_o = None 216 217 # has the remote contact's identity ever been verified? 218 self.verified_identity = False
219
220 - def _get_contact(self):
221 c = gajim.contacts.get_contact(self.conn.name, self.jid, self.resource) 222 if not c: 223 c = gajim.contacts.get_contact(self.conn.name, self.jid) 224 return c
225
226 - def _is_buggy_gajim(self):
227 c = self._get_contact() 228 if c and c.supports(xmpp.NS_ROSTERX): 229 return False 230 return True
231
232 - def set_kc_s(self, value):
233 """ 234 Keep the encrypter updated with my latest cipher key 235 """ 236 self._kc_s = value 237 self.encrypter = self.cipher.new(self._kc_s, self.cipher.MODE_CTR, 238 counter=self.encryptcounter)
239
240 - def get_kc_s(self):
241 return self._kc_s
242
243 - def set_kc_o(self, value):
244 """ 245 Keep the decrypter updated with the other party's latest cipher key 246 """ 247 self._kc_o = value 248 self.decrypter = self.cipher.new(self._kc_o, self.cipher.MODE_CTR, 249 counter=self.decryptcounter)
250
251 - def get_kc_o(self):
252 return self._kc_o
253 254 kc_s = property(get_kc_s, set_kc_s) 255 kc_o = property(get_kc_o, set_kc_o) 256
257 - def encryptcounter(self):
258 self.c_s = (self.c_s + 1) % (2 ** self.n) 259 return crypto.encode_mpi_with_padding(self.c_s)
260
261 - def decryptcounter(self):
262 self.c_o = (self.c_o + 1) % (2 ** self.n) 263 return crypto.encode_mpi_with_padding(self.c_o)
264
265 - def sign(self, string):
266 if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'): 267 hash_ = crypto.sha256(string) 268 return crypto.encode_mpi(gajim.pubkey.sign(hash_, '')[0])
269
270 - def encrypt_stanza(self, stanza):
271 encryptable = [x for x in stanza.getChildren() if x.getName() not in 272 ('error', 'amp', 'thread')] 273 274 # FIXME can also encrypt contents of <error/> elements in stanzas @type = 275 # 'error' 276 # (except for <defined-condition 277 # xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> child elements) 278 279 old_en_counter = self.c_s 280 281 for element in encryptable: 282 stanza.delChild(element) 283 284 plaintext = ''.join(map(str, encryptable)) 285 286 m_compressed = self.compress(plaintext) 287 m_final = self.encrypt(m_compressed) 288 289 c = stanza.NT.c 290 c.setNamespace('http://www.xmpp.org/extensions/xep-0200.html#ns') 291 c.NT.data = base64.b64encode(m_final) 292 293 # FIXME check for rekey request, handle <key/> elements 294 295 m_content = ''.join(map(str, c.getChildren())) 296 c.NT.mac = base64.b64encode(self.hmac(self.km_s, m_content + \ 297 crypto.encode_mpi(old_en_counter))) 298 299 msgtxt = '[This is part of an encrypted session. ' \ 300 'If you see this message, something went wrong.]' 301 lang = os.getenv('LANG') 302 if lang is not None and lang != 'en': # we're not english 303 msgtxt = _('[This is part of an encrypted session. ' 304 'If you see this message, something went wrong.]') + ' (' + \ 305 msgtxt + ')' 306 stanza.setBody(msgtxt) 307 308 return stanza
309
310 - def is_xep_200_encrypted(self, msg):
312
313 - def hmac(self, key, content):
314 return HMAC(key, content, self.hash_alg).digest()
315
316 - def generate_initiator_keys(self, k):
317 return (self.hmac(k, 'Initiator Cipher Key'), 318 self.hmac(k, 'Initiator MAC Key'), 319 self.hmac(k, 'Initiator SIGMA Key'))
320
321 - def generate_responder_keys(self, k):
322 return (self.hmac(k, 'Responder Cipher Key'), 323 self.hmac(k, 'Responder MAC Key'), 324 self.hmac(k, 'Responder SIGMA Key'))
325
326 - def compress(self, plaintext):
327 if self.compression is None: 328 return plaintext
329
330 - def decompress(self, compressed):
331 if self.compression is None: 332 return compressed
333
334 - def encrypt(self, encryptable):
335 padded = crypto.pad_to_multiple(encryptable, 16, ' ', False) 336 337 return self.encrypter.encrypt(padded)
338
339 - def decrypt_stanza(self, stanza):
340 """ 341 Delete the unencrypted explanation body, if it exists 342 """ 343 orig_body = stanza.getTag('body') 344 if orig_body: 345 stanza.delChild(orig_body) 346 347 c = stanza.getTag(name='c', 348 namespace='http://www.xmpp.org/extensions/xep-0200.html#ns') 349 350 stanza.delChild(c) 351 352 # contents of <c>, minus <mac>, minus whitespace 353 macable = ''.join(str(x) for x in c.getChildren() if x.getName() != 'mac') 354 355 received_mac = base64.b64decode(c.getTagData('mac')) 356 calculated_mac = self.hmac(self.km_o, macable + \ 357 crypto.encode_mpi_with_padding(self.c_o)) 358 359 if not calculated_mac == received_mac: 360 raise DecryptionError('bad signature') 361 362 m_final = base64.b64decode(c.getTagData('data')) 363 m_compressed = self.decrypt(m_final) 364 plaintext = self.decompress(m_compressed) 365 366 try: 367 parsed = xmpp.Node(node='<node>' + plaintext + '</node>') 368 except Exception: 369 raise DecryptionError('decrypted <data/> not parseable as XML') 370 371 for child in parsed.getChildren(): 372 stanza.addChild(node=child) 373 374 return stanza
375
376 - def decrypt(self, ciphertext):
377 return self.decrypter.decrypt(ciphertext)
378
379 - def logging_preference(self):
380 if gajim.config.get_per('accounts', self.conn.name, 381 'log_encrypted_sessions'): 382 return ['may', 'mustnot'] 383 else: 384 return ['mustnot', 'may']
385
386 - def get_shared_secret(self, e, y, p):
387 if (not 1 < e < (p - 1)): 388 raise NegotiationError('invalid DH value') 389 390 return crypto.sha256(crypto.encode_mpi(crypto.powmod(e, y, p)))
391
392 - def c7lize_mac_id(self, form):
393 kids = form.getChildren() 394 macable = [x for x in kids if x.getVar() not in ('mac', 'identity')] 395 return ''.join(xmpp.c14n.c14n(el, self._is_buggy_gajim()) for el in \ 396 macable)
397
398 - def verify_identity(self, form, dh_i, sigmai, i_o):
399 m_o = base64.b64decode(form['mac']) 400 id_o = base64.b64decode(form['identity']) 401 402 m_o_calculated = self.hmac(self.km_o, crypto.encode_mpi(self.c_o) + id_o) 403 404 if m_o_calculated != m_o: 405 raise NegotiationError('calculated m_%s differs from received m_%s' % 406 (i_o, i_o)) 407 408 if i_o == 'a' and self.sas_algs == 'sas28x5': 409 # we don't need to calculate this if there's a verified retained secret 410 # (but we do anyways) 411 self.sas = crypto.sas_28x5(m_o, self.form_s) 412 413 if self.negotiated['recv_pubkey']: 414 plaintext = self.decrypt(id_o) 415 parsed = xmpp.Node(node='<node>' + plaintext + '</node>') 416 417 if self.negotiated['recv_pubkey'] == 'hash': 418 # fingerprint = parsed.getTagData('fingerprint') 419 # FIXME find stored pubkey or terminate session 420 raise NotImplementedError() 421 else: 422 if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'): 423 keyvalue = parsed.getTag(name='RSAKeyValue', namespace=XmlDsig) 424 425 n, e = (crypto.decode_mpi(base64.b64decode( 426 keyvalue.getTagData(x))) for x in ('Modulus', 'Exponent')) 427 eir_pubkey = RSA.construct((n, long(e))) 428 429 pubkey_o = xmpp.c14n.c14n(keyvalue, self._is_buggy_gajim()) 430 else: 431 # FIXME DSA, etc. 432 raise NotImplementedError() 433 434 enc_sig = parsed.getTag(name='SignatureValue', 435 namespace=XmlDsig).getData() 436 signature = (crypto.decode_mpi(base64.b64decode(enc_sig)), ) 437 else: 438 mac_o = self.decrypt(id_o) 439 pubkey_o = '' 440 441 c7l_form = self.c7lize_mac_id(form) 442 443 content = self.n_s + self.n_o + crypto.encode_mpi(dh_i) + pubkey_o 444 445 if sigmai: 446 self.form_o = c7l_form 447 content += self.form_o 448 else: 449 form_o2 = c7l_form 450 content += self.form_o + form_o2 451 452 mac_o_calculated = self.hmac(self.ks_o, content) 453 454 if self.negotiated['recv_pubkey']: 455 hash_ = crypto.sha256(mac_o_calculated) 456 457 if not eir_pubkey.verify(hash_, signature): 458 raise NegotiationError('public key signature verification failed!') 459 460 elif mac_o_calculated != mac_o: 461 raise NegotiationError('calculated mac_%s differs from received mac_%s' 462 % (i_o, i_o))
463
464 - def make_identity(self, form, dh_i):
465 if self.negotiated['send_pubkey']: 466 if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'): 467 pubkey = secrets.secrets().my_pubkey(self.conn.name) 468 fields = (pubkey.n, pubkey.e) 469 470 cb_fields = [base64.b64encode(crypto.encode_mpi(f)) for f in 471 fields] 472 473 pubkey_s = '<RSAKeyValue xmlns="http://www.w3.org/2000/09/xmldsig#"' 474 '><Modulus>%s</Modulus><Exponent>%s</Exponent></RSAKeyValue>' % \ 475 tuple(cb_fields) 476 else: 477 pubkey_s = '' 478 479 form_s2 = ''.join(xmpp.c14n.c14n(el, self._is_buggy_gajim()) for el in \ 480 form.getChildren()) 481 482 old_c_s = self.c_s 483 content = self.n_o + self.n_s + crypto.encode_mpi(dh_i) + pubkey_s + \ 484 self.form_s + form_s2 485 486 mac_s = self.hmac(self.ks_s, content) 487 488 if self.negotiated['send_pubkey']: 489 signature = self.sign(mac_s) 490 491 sign_s = '<SignatureValue xmlns="http://www.w3.org/2000/09/xmldsig#">' 492 '%s</SignatureValue>' % base64.b64encode(signature) 493 494 if self.negotiated['send_pubkey'] == 'hash': 495 b64ed = base64.b64encode(self.hash(pubkey_s)) 496 pubkey_s = '<fingerprint>%s</fingerprint>' % b64ed 497 498 id_s = self.encrypt(pubkey_s + sign_s) 499 else: 500 id_s = self.encrypt(mac_s) 501 502 m_s = self.hmac(self.km_s, crypto.encode_mpi(old_c_s) + id_s) 503 504 if self.status == 'requested-e2e' and self.sas_algs == 'sas28x5': 505 # we're alice; check for a retained secret 506 # if none exists, prompt the user with the SAS 507 self.sas = crypto.sas_28x5(m_s, self.form_o) 508 509 if self.sigmai: 510 # FIXME save retained secret? 511 self.check_identity(tuple) 512 513 return (xmpp.DataField(name='identity', value=base64.b64encode(id_s)), 514 xmpp.DataField(name='mac', value=base64.b64encode(m_s)))
515
516 - def negotiate_e2e(self, sigmai):
517 self.negotiated = {} 518 519 request = xmpp.Message() 520 feature = request.NT.feature 521 feature.setNamespace(xmpp.NS_FEATURE) 522 523 x = xmpp.DataForm(typ='form') 524 525 x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn', 526 typ='hidden')) 527 x.addChild(node=xmpp.DataField(name='accept', value='1', typ='boolean', 528 required=True)) 529 530 # this field is incorrectly called 'otr' in XEPs 0116 and 0217 531 x.addChild(node=xmpp.DataField(name='logging', typ='list-single', 532 options=self.logging_preference(), required=True)) 533 534 # unsupported options: 'disabled', 'enabled' 535 x.addChild(node=xmpp.DataField(name='disclosure', typ='list-single', 536 options=['never'], required=True)) 537 x.addChild(node=xmpp.DataField(name='security', typ='list-single', 538 options=['e2e'], required=True)) 539 x.addChild(node=xmpp.DataField(name='crypt_algs', value='aes128-ctr', 540 typ='hidden')) 541 x.addChild(node=xmpp.DataField(name='hash_algs', value='sha256', 542 typ='hidden')) 543 x.addChild(node=xmpp.DataField(name='compress', value='none', 544 typ='hidden')) 545 546 # unsupported options: 'iq', 'presence' 547 x.addChild(node=xmpp.DataField(name='stanzas', typ='list-multi', 548 options=['message'])) 549 550 x.addChild(node=xmpp.DataField(name='init_pubkey', options=['none', 'key', 551 'hash'], typ='list-single')) 552 553 # FIXME store key, use hash 554 x.addChild(node=xmpp.DataField(name='resp_pubkey', options=['none', 555 'key'], typ='list-single')) 556 557 x.addChild(node=xmpp.DataField(name='ver', value='1.0', typ='hidden')) 558 559 x.addChild(node=xmpp.DataField(name='rekey_freq', value='4294967295', 560 typ='hidden')) 561 562 x.addChild(node=xmpp.DataField(name='sas_algs', value='sas28x5', 563 typ='hidden')) 564 x.addChild(node=xmpp.DataField(name='sign_algs', 565 value='http://www.w3.org/2000/09/xmldsig#rsa-sha256', typ='hidden')) 566 567 self.n_s = crypto.generate_nonce() 568 569 x.addChild(node=xmpp.DataField(name='my_nonce', 570 value=base64.b64encode(self.n_s), typ='hidden')) 571 572 modp_options = [ int(g) for g in gajim.config.get('esession_modp').split( 573 ',') ] 574 575 x.addChild(node=xmpp.DataField(name='modp', typ='list-single', 576 options=[[None, y] for y in modp_options])) 577 578 x.addChild(node=self.make_dhfield(modp_options, sigmai)) 579 self.sigmai = sigmai 580 581 self.form_s = ''.join(xmpp.c14n.c14n(el, self._is_buggy_gajim()) for el \ 582 in x.getChildren()) 583 584 feature.addChild(node=x) 585 586 self.status = 'requested-e2e' 587 588 self.send(request)
589
590 - def verify_options_bob(self, form):
591 """ 592 4.3 esession response (bob) 593 """ 594 negotiated = {'recv_pubkey': None, 'send_pubkey': None} 595 not_acceptable = [] 596 ask_user = {} 597 598 fixed = { 'disclosure': 'never', 'security': 'e2e', 599 'crypt_algs': 'aes128-ctr', 'hash_algs': 'sha256', 'compress': 'none', 600 'stanzas': 'message', 'init_pubkey': 'none', 'resp_pubkey': 'none', 601 'ver': '1.0', 'sas_algs': 'sas28x5' } 602 603 self.encryptable_stanzas = ['message'] 604 605 self.sas_algs = 'sas28x5' 606 self.cipher = AES 607 self.hash_alg = sha256 608 self.compression = None 609 610 for name in form.asDict(): 611 field = form.getField(name) 612 options = [x[1] for x in field.getOptions()] 613 values = field.getValues() 614 615 if not field.getType() in ('list-single', 'list-multi'): 616 options = values 617 618 if name in fixed: 619 if fixed[name] in options: 620 negotiated[name] = fixed[name] 621 else: 622 not_acceptable.append(name) 623 elif name == 'rekey_freq': 624 preferred = int(options[0]) 625 negotiated['rekey_freq'] = preferred 626 self.rekey_freq = preferred 627 elif name == 'logging': 628 my_prefs = self.logging_preference() 629 630 if my_prefs[0] in options: # our first choice is offered, select it 631 pref = my_prefs[0] 632 negotiated['logging'] = pref 633 else: # see if other acceptable choices are offered 634 for pref in my_prefs: 635 if pref in options: 636 ask_user['logging'] = pref 637 break 638 639 if not 'logging' in ask_user: 640 not_acceptable.append(name) 641 elif name == 'init_pubkey': 642 for x in ('key'): 643 if x in options: 644 negotiated['recv_pubkey'] = x 645 break 646 elif name == 'resp_pubkey': 647 for x in ('hash', 'key'): 648 if x in options: 649 negotiated['send_pubkey'] = x 650 break 651 elif name == 'sign_algs': 652 if (XmlDsig + 'rsa-sha256') in options: 653 negotiated['sign_algs'] = XmlDsig + 'rsa-sha256' 654 else: 655 # FIXME some things are handled elsewhere, some things are 656 # not-implemented 657 pass 658 659 return (negotiated, not_acceptable, ask_user)
660
661 - def respond_e2e_bob(self, form, negotiated, not_acceptable):
662 """ 663 4.3 esession response (bob) 664 """ 665 response = xmpp.Message() 666 feature = response.NT.feature 667 feature.setNamespace(xmpp.NS_FEATURE) 668 669 x = xmpp.DataForm(typ='submit') 670 671 x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn')) 672 x.addChild(node=xmpp.DataField(name='accept', value='true')) 673 674 for name in negotiated: 675 # some fields are internal and should not be sent 676 if not name in ('send_pubkey', 'recv_pubkey'): 677 x.addChild(node=xmpp.DataField(name=name, value=negotiated[name])) 678 679 self.negotiated = negotiated 680 681 # the offset of the group we chose (need it to match up with the dhhash) 682 group_order = 0 683 self.modp = int(form.getField('modp').getOptions()[group_order][1]) 684 x.addChild(node=xmpp.DataField(name='modp', value=self.modp)) 685 686 g = dh.generators[self.modp] 687 p = dh.primes[self.modp] 688 689 self.n_o = base64.b64decode(form['my_nonce']) 690 691 dhhashes = form.getField('dhhashes').getValues() 692 self.negotiated['He'] = base64.b64decode(dhhashes[group_order].encode( 693 'utf8')) 694 695 bytes = int(self.n / 8) 696 697 self.n_s = crypto.generate_nonce() 698 699 # n-bit random number 700 self.c_o = crypto.decode_mpi(crypto.random_bytes(bytes)) 701 self.c_s = self.c_o ^ (2 ** (self.n - 1)) 702 703 self.y = crypto.srand(2 ** (2 * self.n - 1), p - 1) 704 self.d = crypto.powmod(g, self.y, p) 705 706 to_add = {'my_nonce': self.n_s, 707 'dhkeys': crypto.encode_mpi(self.d), 708 'counter': crypto.encode_mpi(self.c_o), 709 'nonce': self.n_o} 710 711 for name in to_add: 712 b64ed = base64.b64encode(to_add[name]) 713 x.addChild(node=xmpp.DataField(name=name, value=b64ed)) 714 715 self.form_o = ''.join(xmpp.c14n.c14n(el, self._is_buggy_gajim()) for el \ 716 in form.getChildren()) 717 self.form_s = ''.join(xmpp.c14n.c14n(el, self._is_buggy_gajim()) for el \ 718 in x.getChildren()) 719 720 self.status = 'responded-e2e' 721 722 feature.addChild(node=x) 723 724 if not_acceptable: 725 response = xmpp.Error(response, xmpp.ERR_NOT_ACCEPTABLE) 726 727 feature = xmpp.Node(xmpp.NS_FEATURE + ' feature') 728 729 for f in not_acceptable: 730 n = xmpp.Node('field') 731 n['var'] = f 732 feature.addChild(node=n) 733 734 response.T.error.addChild(node=feature) 735 736 self.send(response)
737
738 - def verify_options_alice(self, form):
739 """ 740 'Alice Accepts' 741 """ 742 negotiated = {} 743 ask_user = {} 744 not_acceptable = [] 745 746 if not form['logging'] in self.logging_preference(): 747 not_acceptable.append(form['logging']) 748 elif form['logging'] != self.logging_preference()[0]: 749 ask_user['logging'] = form['logging'] 750 else: 751 negotiated['logging'] = self.logging_preference()[0] 752 753 for r, a in (('recv_pubkey', 'resp_pubkey'), ('send_pubkey', 754 'init_pubkey')): 755 negotiated[r] = None 756 757 if a in form.asDict() and form[a] in ('key', 'hash'): 758 negotiated[r] = form[a] 759 760 if 'sign_algs' in form.asDict(): 761 if form['sign_algs'] in (XmlDsig + 'rsa-sha256', ): 762 negotiated['sign_algs'] = form['sign_algs'] 763 else: 764 not_acceptable.append(form['sign_algs']) 765 766 return (negotiated, not_acceptable, ask_user)
767
768 - def accept_e2e_alice(self, form, negotiated):
769 """ 770 'Alice Accepts', continued 771 """ 772 self.encryptable_stanzas = ['message'] 773 self.sas_algs = 'sas28x5' 774 self.cipher = AES 775 self.hash_alg = sha256 776 self.compression = None 777 778 self.negotiated = negotiated 779 780 accept = xmpp.Message() 781 feature = accept.NT.feature 782 feature.setNamespace(xmpp.NS_FEATURE) 783 784 result = xmpp.DataForm(typ='result') 785 786 self.c_s = crypto.decode_mpi(base64.b64decode(form['counter'])) 787 self.c_o = self.c_s ^ (2 ** (self.n - 1)) 788 self.n_o = base64.b64decode(form['my_nonce']) 789 790 mod_p = int(form['modp']) 791 p = dh.primes[mod_p] 792 x = self.xes[mod_p] 793 e = self.es[mod_p] 794 795 self.d = crypto.decode_mpi(base64.b64decode(form['dhkeys'])) 796 self.k = self.get_shared_secret(self.d, x, p) 797 798 result.addChild(node=xmpp.DataField(name='FORM_TYPE', 799 value='urn:xmpp:ssn')) 800 result.addChild(node=xmpp.DataField(name='accept', value='1')) 801 result.addChild(node=xmpp.DataField(name='nonce', 802 value=base64.b64encode(self.n_o))) 803 804 self.kc_s, self.km_s, self.ks_s = self.generate_initiator_keys(self.k) 805 806 if self.sigmai: 807 self.kc_o, self.km_o, self.ks_o = self.generate_responder_keys(self.k) 808 self.verify_identity(form, self.d, True, 'b') 809 else: 810 srses = secrets.secrets().retained_secrets(self.conn.name, 811 self.jid.getStripped()) 812 rshashes = [self.hmac(self.n_s, rs[0]) for rs in srses] 813 814 if not rshashes: 815 # we've never spoken before, but we'll pretend we have 816 rshash_size = self.hash_alg().digest_size 817 rshashes.append(crypto.random_bytes(rshash_size)) 818 819 rshashes = [base64.b64encode(rshash) for rshash in rshashes] 820 result.addChild(node=xmpp.DataField(name='rshashes', value=rshashes)) 821 result.addChild(node=xmpp.DataField(name='dhkeys', 822 value=base64.b64encode(crypto.encode_mpi(e)))) 823 824 self.form_o = ''.join(xmpp.c14n.c14n(el, self._is_buggy_gajim()) for \ 825 el in form.getChildren()) 826 827 # MUST securely destroy K unless it will be used later to generate the 828 # final shared secret 829 830 for datafield in self.make_identity(result, e): 831 result.addChild(node=datafield) 832 833 feature.addChild(node=result) 834 self.send(accept) 835 836 if self.sigmai: 837 self.status = 'active' 838 self.enable_encryption = True 839 else: 840 self.status = 'identified-alice'
841
842 - def accept_e2e_bob(self, form):
843 """ 844 4.5 esession accept (bob) 845 """ 846 response = xmpp.Message() 847 848 init = response.NT.init 849 init.setNamespace(xmpp.NS_ESESSION_INIT) 850 851 x = xmpp.DataForm(typ='result') 852 853 for field in ('nonce', 'dhkeys', 'rshashes', 'identity', 'mac'): 854 # FIXME: will do nothing in real world... 855 assert field in form.asDict(), "alice's form didn't have a %s field" \ 856 % field 857 858 # 4.5.1 generating provisory session keys 859 e = crypto.decode_mpi(base64.b64decode(form['dhkeys'])) 860 p = dh.primes[self.modp] 861 862 if crypto.sha256(crypto.encode_mpi(e)) != self.negotiated['He']: 863 raise NegotiationError('SHA256(e) != He') 864 865 k = self.get_shared_secret(e, self.y, p) 866 self.kc_o, self.km_o, self.ks_o = self.generate_initiator_keys(k) 867 868 # 4.5.2 verifying alice's identity 869 self.verify_identity(form, e, False, 'a') 870 871 # 4.5.4 generating bob's final session keys 872 srs = '' 873 874 srses = secrets.secrets().retained_secrets(self.conn.name, 875 self.jid.getStripped()) 876 rshashes = [base64.b64decode(rshash) for rshash in form.getField( 877 'rshashes').getValues()] 878 879 for s in srses: 880 secret = s[0] 881 if self.hmac(self.n_o, secret) in rshashes: 882 srs = secret 883 break 884 885 # other shared secret 886 # (we're not using one) 887 oss = '' 888 889 k = crypto.sha256(k + srs + oss) 890 891 self.kc_s, self.km_s, self.ks_s = self.generate_responder_keys(k) 892 self.kc_o, self.km_o, self.ks_o = self.generate_initiator_keys(k) 893 894 # 4.5.5 895 if srs: 896 srshash = self.hmac(srs, 'Shared Retained Secret') 897 else: 898 srshash = crypto.random_bytes(32) 899 900 x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn')) 901 x.addChild(node=xmpp.DataField(name='nonce', value=base64.b64encode( 902 self.n_o))) 903 x.addChild(node=xmpp.DataField(name='srshash', value=base64.b64encode( 904 srshash))) 905 906 for datafield in self.make_identity(x, self.d): 907 x.addChild(node=datafield) 908 909 init.addChild(node=x) 910 911 self.send(response) 912 913 self.do_retained_secret(k, srs) 914 915 if self.negotiated['logging'] == 'mustnot': 916 self.loggable = False 917 918 self.status = 'active' 919 self.enable_encryption = True 920 921 if self.control: 922 self.control.print_esession_details()
923
924 - def final_steps_alice(self, form):
925 srs = '' 926 srses = secrets.secrets().retained_secrets(self.conn.name, 927 self.jid.getStripped()) 928 929 try: 930 srshash = base64.b64decode(form['srshash']) 931 except IndexError: 932 return 933 934 for s in srses: 935 secret = s[0] 936 if self.hmac(secret, 'Shared Retained Secret') == srshash: 937 srs = secret 938 break 939 940 oss = '' 941 k = crypto.sha256(self.k + srs + oss) 942 del self.k 943 944 self.do_retained_secret(k, srs) 945 946 # ks_s doesn't need to be calculated here 947 self.kc_s, self.km_s, self.ks_s = self.generate_initiator_keys(k) 948 self.kc_o, self.km_o, self.ks_o = self.generate_responder_keys(k) 949 950 # 4.6.2 Verifying Bob's Identity 951 self.verify_identity(form, self.d, False, 'b') 952 # Note: If Alice discovers an error then she SHOULD ignore any encrypted 953 # content she received in the stanza. 954 955 if self.negotiated['logging'] == 'mustnot': 956 self.loggable = False 957 958 self.status = 'active' 959 self.enable_encryption = True 960 961 if self.control: 962 self.control.print_esession_details()
963
964 - def do_retained_secret(self, k, old_srs):
965 """ 966 Calculate the new retained secret. determine if the user needs to check 967 the remote party's identity. Set up callbacks for when the identity has 968 been verified 969 """ 970 new_srs = self.hmac(k, 'New Retained Secret') 971 self.srs = new_srs 972 973 account = self.conn.name 974 bjid = self.jid.getStripped() 975 976 self.verified_identity = False 977 978 if old_srs: 979 if secrets.secrets().srs_verified(account, bjid, old_srs): 980 # already had a stored secret verified by the user. 981 secrets.secrets().replace_srs(account, bjid, old_srs, new_srs, True) 982 # continue without warning. 983 self.verified_identity = True 984 else: 985 # had a secret, but it wasn't verified. 986 secrets.secrets().replace_srs(account, bjid, old_srs, new_srs, 987 False) 988 else: 989 # we don't even have an SRS 990 secrets.secrets().save_new_srs(account, bjid, new_srs, False)
991
992 - def _verified_srs_cb(self):
993 secrets.secrets().replace_srs(self.conn.name, self.jid.getStripped(), 994 self.srs, self.srs, True)
995
996 - def _unverified_srs_cb(self):
997 secrets.secrets().replace_srs(self.conn.name, self.jid.getStripped(), 998 self.srs, self.srs, False)
999
1000 - def make_dhfield(self, modp_options, sigmai):
1001 dhs = [] 1002 1003 for modp in modp_options: 1004 p = dh.primes[modp] 1005 g = dh.generators[modp] 1006 1007 x = crypto.srand(2 ** (2 * self.n - 1), p - 1) 1008 1009 # FIXME this may be a source of performance issues 1010 e = crypto.powmod(g, x, p) 1011 1012 self.xes[modp] = x 1013 self.es[modp] = e 1014 1015 if sigmai: 1016 dhs.append(base64.b64encode(crypto.encode_mpi(e))) 1017 name = 'dhkeys' 1018 else: 1019 He = crypto.sha256(crypto.encode_mpi(e)) 1020 dhs.append(base64.b64encode(He)) 1021 name = 'dhhashes' 1022 1023 return xmpp.DataField(name=name, typ='hidden', value=dhs)
1024
1025 - def terminate_e2e(self):
1026 self.terminate() 1027 self.enable_encryption = False
1028
1029 - def acknowledge_termination(self):
1030 StanzaSession.acknowledge_termination(self) 1031 self.enable_encryption = False
1032
1033 - def fail_bad_negotiation(self, reason, fields=None):
1034 """ 1035 Send an error and cancels everything 1036 1037 If fields is None, the remote party has given us a bad cryptographic 1038 value of some kind. Otherwise, list the fields we haven't implemented. 1039 """ 1040 err = xmpp.Error(xmpp.Message(), xmpp.ERR_FEATURE_NOT_IMPLEMENTED) 1041 err.T.error.T.text.setData(reason) 1042 1043 if fields: 1044 feature = xmpp.Node(xmpp.NS_FEATURE + ' feature') 1045 1046 for field in fields: 1047 fn = xmpp.Node('field') 1048 fn['var'] = field 1049 feature.addChild(node=feature) 1050 1051 err.addChild(node=feature) 1052 1053 self.send(err) 1054 1055 self.status = None 1056 self.enable_encryption = False 1057 1058 # this prevents the MAC check on decryption from succeeding, 1059 # preventing falsified messages from going through. 1060 self.km_o = ''
1061
1062 - def cancelled_negotiation(self):
1063 StanzaSession.cancelled_negotiation(self) 1064 self.enable_encryption = False 1065 self.km_o = ''
1066