Cosmetic only... as usaual: stanza_session.py. TODO: better variable names

This commit is contained in:
Stephan Erb 2009-01-11 22:29:58 +00:00
parent d23aa630ba
commit 593c23ebc8
2 changed files with 97 additions and 99 deletions

View file

@ -19,12 +19,13 @@
## along with Gajim. If not, see <http://www.gnu.org/licenses/>. ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
## ##
import string '''
This module defines a number of constants; specifically, large primes suitable
for use with the Diffie-Hellman key exchange.
# This file defines a number of constants; specifically, large primes suitable for These constants have been obtained from RFC2409 and RFC3526.
# use with the Diffie-Hellman key exchange. '''
# import string
# These constants have been obtained from RFC2409 and RFC3526.
generators = [ None, # one to get the right offset generators = [ None, # one to get the right offset
2, 2,

View file

@ -2,7 +2,6 @@
## src/common/stanza_session.py ## src/common/stanza_session.py
## ##
## Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com> ## Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
## Stephan Erb <steve-e AT h3c.de>
## Copyright (C) 2007-2008 Yann Leboulanger <asterix AT lagaule.org> ## Copyright (C) 2007-2008 Yann Leboulanger <asterix AT lagaule.org>
## Brendan Taylor <whateley AT gmail.com> ## Brendan Taylor <whateley AT gmail.com>
## Jean-Marie Traissard <jim AT lapin.org> ## Jean-Marie Traissard <jim AT lapin.org>
@ -24,29 +23,36 @@
## ##
from common import gajim from common import gajim
from common import xmpp from common import xmpp
from common import exceptions from common.exceptions import DecryptionError, NegotiationError
import xmpp.c14n
import itertools import itertools
import random import random
import string import string
import time import time
import xmpp.c14n
import base64 import base64
import os import os
if gajim.HAVE_PYCRYPTO:
from Crypto.Cipher import AES
from Crypto.Hash import HMAC, SHA256
from Crypto.PublicKey import RSA
from common import crypto
from common import dh
import secrets
XmlDsig = 'http://www.w3.org/2000/09/xmldsig#' XmlDsig = 'http://www.w3.org/2000/09/xmldsig#'
class StanzaSession(object): class StanzaSession(object):
'''
'''
def __init__(self, conn, jid, thread_id, type_): def __init__(self, conn, jid, thread_id, type_):
'''
'''
self.conn = conn self.conn = conn
self.jid = jid self.jid = jid
self.type = type_ self.type = type_
if thread_id: if thread_id:
@ -69,9 +75,11 @@ class StanzaSession(object):
def is_loggable(self): def is_loggable(self):
return self.loggable and gajim.config.should_log(self.conn.name, self.jid) return self.loggable and gajim.config.should_log(self.conn.name, self.jid)
# remove events associated with this session from the queue
# returns True if any events were removed (unlike gajim.events.remove_events)
def remove_events(self, types): def remove_events(self, types):
'''
Remove events associated with this session from the queue.
returns True if any events were removed (unlike events.py remove_events)
'''
any_removed = False any_removed = False
for j in (self.jid, self.jid.getStripped()): for j in (self.jid, self.jid.getStripped()):
@ -124,9 +132,10 @@ class StanzaSession(object):
self.cancelled_negotiation() self.cancelled_negotiation()
def cancelled_negotiation(self): def cancelled_negotiation(self):
'''A negotiation has been cancelled, so reset this session to its default '''
state.''' A negotiation has been cancelled, so reset this session to its default
state.
'''
if self.control: if self.control:
self.control.on_cancel_session_negotiation() self.control.on_cancel_session_negotiation()
@ -156,61 +165,52 @@ class StanzaSession(object):
# we could send an acknowledgement message to the remote client here # we could send an acknowledgement message to the remote client here
self.status = None self.status = None
if gajim.HAVE_PYCRYPTO:
from Crypto.Cipher import AES
from Crypto.Hash import HMAC, SHA256
from Crypto.PublicKey import RSA
from common import crypto
from common import dh
import secrets
# an encrypted stanza negotiation has several states. i've represented them
# as the following values in the 'status'
# attribute of the session object:
# 1. None:
# default state
# 2. 'requested-e2e':
# this client has initiated an esession negotiation and is waiting
# for a response
# 3. 'responded-e2e':
# this client has responded to an esession negotiation request and
# is waiting for the initiator to identify itself and complete the
# negotiation
# 4. 'identified-alice':
# this client identified itself and is waiting for the responder to
# identify itself and complete the negotiation
# 5. 'active':
# an encrypted session has been successfully negotiated. messages
# of any of the types listed in 'encryptable_stanzas' should be
# encrypted before they're sent.
# the transition between these states is handled in gajim.py's
# handle_session_negotiation method.
class EncryptedStanzaSession(StanzaSession): class EncryptedStanzaSession(StanzaSession):
'''
An encrypted stanza negotiation has several states. They arerepresented as
the following values in the 'status' attribute of the session object:
1. None:
default state
2. 'requested-e2e':
this client has initiated an esession negotiation and is waiting
for a response
3. 'responded-e2e':
this client has responded to an esession negotiation request and
is waiting for the initiator to identify itself and complete the
negotiation
4. 'identified-alice':
this client identified itself and is waiting for the responder to
identify itself and complete the negotiation
5. 'active':
an encrypted session has been successfully negotiated. messages
of any of the types listed in 'encryptable_stanzas' should be
encrypted before they're sent.
The transition between these states is handled in gajim.py's
handle_session_negotiation method.
'''
def __init__(self, conn, jid, thread_id, type_='chat'): def __init__(self, conn, jid, thread_id, type_='chat'):
StanzaSession.__init__(self, conn, jid, thread_id, type_='chat') StanzaSession.__init__(self, conn, jid, thread_id, type_='chat')
self.xes = {} self.xes = {}
self.es = {} self.es = {}
self.n = 128 self.n = 128
self.enable_encryption = False self.enable_encryption = False
# _s denotes 'self' (ie. this client) # _s denotes 'self' (ie. this client)
self._kc_s = None self._kc_s = None
# _o denotes 'other' (ie. the client at the other end of the session) # _o denotes 'other' (ie. the client at the other end of the session)
self._kc_o = None self._kc_o = None
# has the remote contact's identity ever been verified? # has the remote contact's identity ever been verified?
self.verified_identity = False self.verified_identity = False
# keep the encrypter updated with my latest cipher key
def set_kc_s(self, value): def set_kc_s(self, value):
'''
keep the encrypter updated with my latest cipher key
'''
self._kc_s = value self._kc_s = value
self.encrypter = self.cipher.new(self._kc_s, self.cipher.MODE_CTR, self.encrypter = self.cipher.new(self._kc_s, self.cipher.MODE_CTR,
counter=self.encryptcounter) counter=self.encryptcounter)
@ -218,8 +218,10 @@ class EncryptedStanzaSession(StanzaSession):
def get_kc_s(self): def get_kc_s(self):
return self._kc_s return self._kc_s
# keep the decrypter updated with the other party's latest cipher key
def set_kc_o(self, value): def set_kc_o(self, value):
'''
keep the decrypter updated with the other party's latest cipher key
'''
self._kc_o = value self._kc_o = value
self.decrypter = self.cipher.new(self._kc_o, self.cipher.MODE_CTR, self.decrypter = self.cipher.new(self._kc_o, self.cipher.MODE_CTR,
counter=self.decryptcounter) counter=self.decryptcounter)
@ -244,10 +246,10 @@ class EncryptedStanzaSession(StanzaSession):
return crypto.encode_mpi(gajim.pubkey.sign(hash_, '')[0]) return crypto.encode_mpi(gajim.pubkey.sign(hash_, '')[0])
def encrypt_stanza(self, stanza): def encrypt_stanza(self, stanza):
encryptable = [x for x in stanza.getChildren() if x.getName() not in ('error', 'amp', encryptable = [x for x in stanza.getChildren() if x.getName() not in
'thread')] ('error', 'amp', 'thread')]
# XXX can also encrypt contents of <error/> elements in stanzas @type = # FIXME can also encrypt contents of <error/> elements in stanzas @type =
# 'error' # 'error'
# (except for <defined-condition # (except for <defined-condition
# xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> child elements) # xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> child elements)
@ -266,7 +268,7 @@ class EncryptedStanzaSession(StanzaSession):
c.setNamespace('http://www.xmpp.org/extensions/xep-0200.html#ns') c.setNamespace('http://www.xmpp.org/extensions/xep-0200.html#ns')
c.NT.data = base64.b64encode(m_final) c.NT.data = base64.b64encode(m_final)
# XXX check for rekey request, handle <key/> elements # FIXME check for rekey request, handle <key/> elements
m_content = ''.join(map(str, c.getChildren())) m_content = ''.join(map(str, c.getChildren()))
c.NT.mac = base64.b64encode(self.hmac(self.km_s, m_content + \ c.NT.mac = base64.b64encode(self.hmac(self.km_s, m_content + \
@ -313,7 +315,7 @@ class EncryptedStanzaSession(StanzaSession):
return self.encrypter.encrypt(padded) return self.encrypter.encrypt(padded)
def decrypt_stanza(self, stanza): def decrypt_stanza(self, stanza):
# delete the unencrypted explanation body, if it exists ''' delete the unencrypted explanation body, if it exists '''
orig_body = stanza.getTag('body') orig_body = stanza.getTag('body')
if orig_body: if orig_body:
stanza.delChild(orig_body) stanza.delChild(orig_body)
@ -331,7 +333,7 @@ class EncryptedStanzaSession(StanzaSession):
crypto.encode_mpi_with_padding(self.c_o)) crypto.encode_mpi_with_padding(self.c_o))
if not calculated_mac == received_mac: if not calculated_mac == received_mac:
raise exceptions.DecryptionError, 'bad signature' raise DecryptionError('bad signature')
m_final = base64.b64decode(c.getTagData('data')) m_final = base64.b64decode(c.getTagData('data'))
m_compressed = self.decrypt(m_final) m_compressed = self.decrypt(m_final)
@ -340,7 +342,7 @@ class EncryptedStanzaSession(StanzaSession):
try: try:
parsed = xmpp.Node(node='<node>' + plaintext + '</node>') parsed = xmpp.Node(node='<node>' + plaintext + '</node>')
except Exception: except Exception:
raise exceptions.DecryptionError, 'decrypted <data/> not parseable as XML' raise DecryptionError('decrypted <data/> not parseable as XML')
for child in parsed.getChildren(): for child in parsed.getChildren():
stanza.addChild(node=child) stanza.addChild(node=child)
@ -358,7 +360,7 @@ class EncryptedStanzaSession(StanzaSession):
def get_shared_secret(self, e, y, p): def get_shared_secret(self, e, y, p):
if (not 1 < e < (p - 1)): if (not 1 < e < (p - 1)):
raise exceptions.NegotiationError, 'invalid DH value' raise NegotiationError('invalid DH value')
return crypto.sha256(crypto.encode_mpi(crypto.powmod(e, y, p))) return crypto.sha256(crypto.encode_mpi(crypto.powmod(e, y, p)))
@ -374,7 +376,8 @@ class EncryptedStanzaSession(StanzaSession):
m_o_calculated = self.hmac(self.km_o, crypto.encode_mpi(self.c_o) + id_o) m_o_calculated = self.hmac(self.km_o, crypto.encode_mpi(self.c_o) + id_o)
if m_o_calculated != m_o: if m_o_calculated != m_o:
raise exceptions.NegotiationError, 'calculated m_%s differs from received m_%s' % (i_o, i_o) raise NegotiationError('calculated m_%s differs from received m_%s' %
(i_o, i_o))
if i_o == 'a' and self.sas_algs == 'sas28x5': if i_o == 'a' and self.sas_algs == 'sas28x5':
# we don't need to calculate this if there's a verified retained secret # we don't need to calculate this if there's a verified retained secret
@ -387,7 +390,7 @@ class EncryptedStanzaSession(StanzaSession):
if self.negotiated['recv_pubkey'] == 'hash': if self.negotiated['recv_pubkey'] == 'hash':
# fingerprint = parsed.getTagData('fingerprint') # fingerprint = parsed.getTagData('fingerprint')
# XXX find stored pubkey or terminate session # FIXME find stored pubkey or terminate session
raise NotImplementedError() raise NotImplementedError()
else: else:
if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'): if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'):
@ -399,7 +402,7 @@ class EncryptedStanzaSession(StanzaSession):
pubkey_o = xmpp.c14n.c14n(keyvalue) pubkey_o = xmpp.c14n.c14n(keyvalue)
else: else:
# XXX DSA, etc. # FIXME DSA, etc.
raise NotImplementedError() raise NotImplementedError()
enc_sig = parsed.getTag(name='SignatureValue', enc_sig = parsed.getTag(name='SignatureValue',
@ -426,10 +429,11 @@ class EncryptedStanzaSession(StanzaSession):
hash_ = crypto.sha256(mac_o_calculated) hash_ = crypto.sha256(mac_o_calculated)
if not eir_pubkey.verify(hash_, signature): if not eir_pubkey.verify(hash_, signature):
raise exceptions.NegotiationError, 'public key signature verification failed!' raise NegotiationError('public key signature verification failed!')
elif mac_o_calculated != mac_o: elif mac_o_calculated != mac_o:
raise exceptions.NegotiationError, 'calculated mac_%s differs from received mac_%s' % (i_o, i_o) raise NegotiationError('calculated mac_%s differs from received mac_%s'
% (i_o, i_o))
def make_identity(self, form, dh_i): def make_identity(self, form, dh_i):
if self.negotiated['send_pubkey']: if self.negotiated['send_pubkey']:
@ -476,7 +480,7 @@ class EncryptedStanzaSession(StanzaSession):
self.sas = crypto.sas_28x5(m_s, self.form_o) self.sas = crypto.sas_28x5(m_s, self.form_o)
if self.sigmai: if self.sigmai:
# XXX save retained secret? # FIXME save retained secret?
self.check_identity(tuple) self.check_identity(tuple)
return (xmpp.DataField(name='identity', value=base64.b64encode(id_s)), return (xmpp.DataField(name='identity', value=base64.b64encode(id_s)),
@ -519,7 +523,7 @@ class EncryptedStanzaSession(StanzaSession):
x.addChild(node=xmpp.DataField(name='init_pubkey', options=['none', 'key', x.addChild(node=xmpp.DataField(name='init_pubkey', options=['none', 'key',
'hash'], typ='list-single')) 'hash'], typ='list-single'))
# XXX store key, use hash # FIXME store key, use hash
x.addChild(node=xmpp.DataField(name='resp_pubkey', options=['none', x.addChild(node=xmpp.DataField(name='resp_pubkey', options=['none',
'key'], typ='list-single')) 'key'], typ='list-single'))
@ -555,8 +559,8 @@ class EncryptedStanzaSession(StanzaSession):
self.send(request) self.send(request)
# 4.3 esession response (bob)
def verify_options_bob(self, form): def verify_options_bob(self, form):
''' 4.3 esession response (bob) '''
negotiated = {'recv_pubkey': None, 'send_pubkey': None} negotiated = {'recv_pubkey': None, 'send_pubkey': None}
not_acceptable = [] not_acceptable = []
ask_user = {} ask_user = {}
@ -618,14 +622,14 @@ class EncryptedStanzaSession(StanzaSession):
if (XmlDsig + 'rsa-sha256') in options: if (XmlDsig + 'rsa-sha256') in options:
negotiated['sign_algs'] = XmlDsig + 'rsa-sha256' negotiated['sign_algs'] = XmlDsig + 'rsa-sha256'
else: else:
# XXX some things are handled elsewhere, some things are # FIXME some things are handled elsewhere, some things are
# not-implemented # not-implemented
pass pass
return (negotiated, not_acceptable, ask_user) return (negotiated, not_acceptable, ask_user)
# 4.3 esession response (bob)
def respond_e2e_bob(self, form, negotiated, not_acceptable): def respond_e2e_bob(self, form, negotiated, not_acceptable):
''' 4.3 esession response (bob) '''
response = xmpp.Message() response = xmpp.Message()
feature = response.NT.feature feature = response.NT.feature
feature.setNamespace(xmpp.NS_FEATURE) feature.setNamespace(xmpp.NS_FEATURE)
@ -697,8 +701,8 @@ class EncryptedStanzaSession(StanzaSession):
self.send(response) self.send(response)
# 'Alice Accepts'
def verify_options_alice(self, form): def verify_options_alice(self, form):
''' 'Alice Accepts' '''
negotiated = {} negotiated = {}
ask_user = {} ask_user = {}
not_acceptable = [] not_acceptable = []
@ -725,8 +729,8 @@ class EncryptedStanzaSession(StanzaSession):
return (negotiated, not_acceptable, ask_user) return (negotiated, not_acceptable, ask_user)
# 'Alice Accepts', continued
def accept_e2e_alice(self, form, negotiated): def accept_e2e_alice(self, form, negotiated):
''' 'Alice Accepts', continued '''
self.encryptable_stanzas = ['message'] self.encryptable_stanzas = ['message']
self.sas_algs = 'sas28x5' self.sas_algs = 'sas28x5'
self.cipher = AES self.cipher = AES
@ -743,7 +747,6 @@ class EncryptedStanzaSession(StanzaSession):
self.c_s = crypto.decode_mpi(base64.b64decode(form['counter'])) self.c_s = crypto.decode_mpi(base64.b64decode(form['counter']))
self.c_o = self.c_s ^ (2 ** (self.n - 1)) self.c_o = self.c_s ^ (2 ** (self.n - 1))
self.n_o = base64.b64decode(form['my_nonce']) self.n_o = base64.b64decode(form['my_nonce'])
mod_p = int(form['modp']) mod_p = int(form['modp'])
@ -752,7 +755,6 @@ class EncryptedStanzaSession(StanzaSession):
e = self.es[mod_p] e = self.es[mod_p]
self.d = crypto.decode_mpi(base64.b64decode(form['dhkeys'])) self.d = crypto.decode_mpi(base64.b64decode(form['dhkeys']))
self.k = self.get_shared_secret(self.d, x, p) self.k = self.get_shared_secret(self.d, x, p)
result.addChild(node=xmpp.DataField(name='FORM_TYPE', result.addChild(node=xmpp.DataField(name='FORM_TYPE',
@ -798,8 +800,8 @@ class EncryptedStanzaSession(StanzaSession):
else: else:
self.status = 'identified-alice' self.status = 'identified-alice'
# 4.5 esession accept (bob)
def accept_e2e_bob(self, form): def accept_e2e_bob(self, form):
''' 4.5 esession accept (bob) '''
response = xmpp.Message() response = xmpp.Message()
init = response.NT.init init = response.NT.init
@ -808,6 +810,7 @@ class EncryptedStanzaSession(StanzaSession):
x = xmpp.DataForm(typ='result') x = xmpp.DataForm(typ='result')
for field in ('nonce', 'dhkeys', 'rshashes', 'identity', 'mac'): for field in ('nonce', 'dhkeys', 'rshashes', 'identity', 'mac'):
# FIXME: will do nothing in real world...
assert field in form.asDict(), "alice's form didn't have a %s field" \ assert field in form.asDict(), "alice's form didn't have a %s field" \
% field % field
@ -816,18 +819,15 @@ class EncryptedStanzaSession(StanzaSession):
p = dh.primes[self.modp] p = dh.primes[self.modp]
if crypto.sha256(crypto.encode_mpi(e)) != self.negotiated['He']: if crypto.sha256(crypto.encode_mpi(e)) != self.negotiated['He']:
raise exceptions.NegotiationError, 'SHA256(e) != He' raise NegotiationError('SHA256(e) != He')
k = self.get_shared_secret(e, self.y, p) k = self.get_shared_secret(e, self.y, p)
self.kc_o, self.km_o, self.ks_o = self.generate_initiator_keys(k) self.kc_o, self.km_o, self.ks_o = self.generate_initiator_keys(k)
# 4.5.2 verifying alice's identity # 4.5.2 verifying alice's identity
self.verify_identity(form, e, False, 'a') self.verify_identity(form, e, False, 'a')
# 4.5.4 generating bob's final session keys # 4.5.4 generating bob's final session keys
srs = '' srs = ''
srses = secrets.secrets().retained_secrets(self.conn.name, srses = secrets.secrets().retained_secrets(self.conn.name,
@ -904,7 +904,6 @@ class EncryptedStanzaSession(StanzaSession):
self.kc_o, self.km_o, self.ks_o = self.generate_responder_keys(k) self.kc_o, self.km_o, self.ks_o = self.generate_responder_keys(k)
# 4.6.2 Verifying Bob's Identity # 4.6.2 Verifying Bob's Identity
self.verify_identity(form, self.d, False, 'b') self.verify_identity(form, self.d, False, 'b')
# Note: If Alice discovers an error then she SHOULD ignore any encrypted # Note: If Alice discovers an error then she SHOULD ignore any encrypted
# content she received in the stanza. # content she received in the stanza.
@ -919,10 +918,11 @@ class EncryptedStanzaSession(StanzaSession):
self.control.print_esession_details() self.control.print_esession_details()
def do_retained_secret(self, k, old_srs): def do_retained_secret(self, k, old_srs):
'''calculate the new retained secret. determine if the user needs to check '''
the remote party's identity. set up callbacks for when the identity has Calculate the new retained secret. determine if the user needs to check
been verified.''' the remote party's identity. Set up callbacks for when the identity has
been verified.
'''
new_srs = self.hmac(k, 'New Retained Secret') new_srs = self.hmac(k, 'New Retained Secret')
self.srs = new_srs self.srs = new_srs
@ -962,7 +962,7 @@ class EncryptedStanzaSession(StanzaSession):
x = crypto.srand(2 ** (2 * self.n - 1), p - 1) x = crypto.srand(2 ** (2 * self.n - 1), p - 1)
# XXX this may be a source of performance issues # FIXME this may be a source of performance issues
e = crypto.powmod(g, x, p) e = crypto.powmod(g, x, p)
self.xes[modp] = x self.xes[modp] = x
@ -980,21 +980,19 @@ class EncryptedStanzaSession(StanzaSession):
def terminate_e2e(self): def terminate_e2e(self):
self.terminate() self.terminate()
self.enable_encryption = False self.enable_encryption = False
def acknowledge_termination(self): def acknowledge_termination(self):
StanzaSession.acknowledge_termination(self) StanzaSession.acknowledge_termination(self)
self.enable_encryption = False self.enable_encryption = False
def fail_bad_negotiation(self, reason, fields = None): def fail_bad_negotiation(self, reason, fields=None):
'''sends an error and cancels everything. '''
Sends an error and cancels everything.
if fields is None, the remote party has given us a bad cryptographic value of some kind
If fields is None, the remote party has given us a bad cryptographic
otherwise, list the fields we haven't implemented''' value of some kind. Otherwise, list the fields we haven't implemented
'''
err = xmpp.Error(xmpp.Message(), xmpp.ERR_FEATURE_NOT_IMPLEMENTED) err = xmpp.Error(xmpp.Message(), xmpp.ERR_FEATURE_NOT_IMPLEMENTED)
err.T.error.T.text.setData(reason) err.T.error.T.text.setData(reason)
@ -1020,7 +1018,6 @@ otherwise, list the fields we haven't implemented'''
def cancelled_negotiation(self): def cancelled_negotiation(self):
StanzaSession.cancelled_negotiation(self) StanzaSession.cancelled_negotiation(self)
self.enable_encryption = False self.enable_encryption = False
self.km_o = '' self.km_o = ''
# vim: se ts=3: # vim: se ts=3: