From fdef1c3d72ffafa724930cfc89bf869c9b7eb106 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Tue, 17 Jul 2007 08:08:27 +0000 Subject: [PATCH] shared retained secrets --- src/common/connection_handlers.py | 4 +- src/common/stanza_session.py | 95 +++++++++++++++++++------------ src/gajim.py | 60 +++++++++++++++++-- 3 files changed, 116 insertions(+), 43 deletions(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index b6fc50f1f..dbd00e990 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1609,7 +1609,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, del self.sessions[original_jid][thread_id] new_jid = gajim.get_jid_without_resource(original_jid) + '/' + to_resource - session.jid = new_jid + session.jid = common.xmpp.JID(new_jid) if not new_jid in self.sessions: self.sessions[new_jid] = {} @@ -1628,7 +1628,7 @@ returns the session that we last sent a message to.''' return no_threadid_sessions[-1] def make_new_session(self, jid, thread_id = None, type = 'chat'): - sess = EncryptedStanzaSession(self, jid, thread_id, type) + sess = EncryptedStanzaSession(self, common.xmpp.JID(jid), thread_id, type) if not jid in self.sessions: self.sessions[jid] = {} diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 0c19305ca..b1848091a 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -1,4 +1,4 @@ -import gajim +from common import gajim from common import xmpp from common import helpers @@ -23,10 +23,7 @@ class StanzaSession(object): def __init__(self, conn, jid, thread_id, type): self.conn = conn - if isinstance(jid, str) or isinstance(jid, unicode): - self.jid = xmpp.JID(jid) - else: - self.jid = jid + self.jid = jid self.type = type @@ -43,7 +40,7 @@ class StanzaSession(object): self.last_send = 0 self.status = None self.negotiated = {} - + def generate_thread_id(self): return "".join([random.choice(string.letters) for x in xrange(0,32)]) @@ -543,8 +540,15 @@ class EncryptedStanzaSession(StanzaSession): result.addChild(node=xmpp.DataField(name='nonce', value=base64.b64encode(self.n_o))) result.addChild(node=xmpp.DataField(name='dhkeys', value=base64.b64encode(self.encode_mpi(e)))) - # TODO: store and return rshashes, or at least randomly generate some - result.addChild(node=xmpp.DataField(name='rshashes', value=[])) + secrets = gajim.interface.list_secrets(self.conn.name, self.jid.getStripped()) + rshashes = [self.hmac(self.n_s, rs) for rs in secrets] + + # XXX add some random fake rshashes here + + rshashes.sort() + + rshashes = [base64.b64encode(rshash) for rshash in rshashes] + result.addChild(node=xmpp.DataField(name='rshashes', value=rshashes)) form_a2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), result.getChildren())) @@ -615,23 +619,34 @@ class EncryptedStanzaSession(StanzaSession): raise exceptions.NegotiationError, 'calculated mac_a differs from received mac_a' # 4.5.4 generating bob's final session keys - self.srs = '' + + srs = '' + + secrets = gajim.interface.list_secrets(self.conn.name, self.jid.getStripped()) + rshashes = [base64.b64decode(rshash) for rshash in form.getField('rshashes').getValues()] + + for secret in secrets: + if self.hmac(self.n_o, secret) in rshashes: + srs = secret + break + + # other shared secret, we haven't got one. oss = '' # check for a retained secret # if none exists, prompt the user with the SAS if self.sas_algs == 'sas28x5': self.sas = self.sas_28x5(m_a, self.form_b) - - k = self.sha256(k + self.srs + oss) + + k = self.sha256(k + srs + oss) # XXX I can skip generating ks_o here self.kc_s, self.km_s, self.ks_s = self.generate_responder_keys(k) self.kc_o, self.km_o, self.ks_o = self.generate_initiator_keys(k) # 4.5.5 - if self.srs: - srshash = self.hmac(self.srs, 'Shared Retained Secret') + if srs: + srshash = self.hmac(srs, 'Shared Retained Secret') else: srshash = self.random_bytes(32) @@ -655,11 +670,7 @@ class EncryptedStanzaSession(StanzaSession): self.send(response) - # destroy all copies of srs - - self.srs = self.hmac(k, 'New Retained Secret') - - # destroy k + self.do_retained_secret(k, srs) if self.negotiated['logging'] == 'mustnot': self.loggable = False @@ -668,27 +679,28 @@ class EncryptedStanzaSession(StanzaSession): self.enable_encryption = True def final_steps_alice(self, form): - # Alice MUST identify the shared retained secret (SRS) by selecting from her client's list of the secrets it retained from sessions with Bob's clients (the most recent secret for each of the clients he has used to negotiate ESessions with Alice's client). - - # Alice does this by using each secret in the list in turn as the key to calculate the HMAC (with SHA256) of the string "Shared Retained Secret", and comparing the calculated value with the value in the 'srshash' field she received from Bob (see Sending Bob's Identity). Once she finds a match, and has confirmed that the secret has not expired (because it is older than an implementation-defined period of time), then she has found the SRS. - - srs = '' + secrets = gajim.interface.list_secrets(self.conn.name, self.jid.getStripped()) + + srshash = base64.b64decode(form['srshash']) + + for secret in secrets: + if self.hmac(secret, 'Shared Retained Secret') == srshash: + srs = secret + break + oss = '' - self.k = self.sha256(self.k + srs + oss) + k = self.sha256(self.k + srs + oss) + del self.k - # Alice MUST destroy all her copies of the old retained secret (SRS) she was keeping for Bob's client, and calculate a new retained secret for this session: - - srs = self.hmac('New Retained Secret', self.k) - - # Alice MUST securely store the new value along with the retained secrets her client shares with Bob's other clients. + self.do_retained_secret(k, srs) # don't need to calculate ks_s here - self.kc_s, self.km_s, self.ks_s = self.generate_initiator_keys(self.k) - self.kc_o, self.km_o, self.ks_o = self.generate_responder_keys(self.k) + self.kc_s, self.km_s, self.ks_s = self.generate_initiator_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 m_b = base64.b64decode(form['mac']) id_b = base64.b64decode(form['identity']) @@ -716,6 +728,19 @@ class EncryptedStanzaSession(StanzaSession): self.status = 'active' self.enable_encryption = True + # calculate and store the new retained secret + # prompt the user to check the remote party's identity (if necessary) + def do_retained_secret(self, k, srs): + new_srs = self.hmac(k, 'New Retained Secret') + account = self.conn.name + bjid = self.jid.getStripped() + + if srs: + gajim.interface.replace_secret(account, bjid, srs, new_srs) + else: + self.check_identity() + gajim.interface.save_new_secret(account, bjid, new_srs) + # generate a random number between 'bottom' and 'top' def srand(self, bottom, top): # minimum number of bytes needed to represent that range @@ -773,12 +798,12 @@ class EncryptedStanzaSession(StanzaSession): self.status = None def is_loggable(self): - name = self.conn.name - no_log_for = gajim.config.get_per('accounts', name, 'no_log_for') + account = self.conn.name + no_log_for = gajim.config.get_per('accounts', account, 'no_log_for') if not no_log_for: no_log_for = '' no_log_for = no_log_for.split() - return self.loggable and name not in no_log_for and self.jid not in no_log_for + return self.loggable and account not in no_log_for and self.jid not in no_log_for diff --git a/src/gajim.py b/src/gajim.py index 537436776..3514273e4 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -109,6 +109,8 @@ from common import exceptions from common.zeroconf import connection_zeroconf from common import dbus_support +import pickle + if os.name == 'posix': # dl module is Unix Only try: # rename the process name to gajim import dl @@ -208,6 +210,7 @@ gajimpaths = common.configpaths.gajimpaths pid_filename = gajimpaths['PID_FILE'] config_filename = gajimpaths['CONFIG_FILE'] +secrets_filename = gajimpaths['SECRETS_FILE'] import traceback import errno @@ -1390,6 +1393,46 @@ class Interface: if os.path.isfile(path_to_original_file): os.remove(path_to_original_file) + def list_secrets(self, account, jid): + f = open(secrets_filename) + + try: + s = pickle.load(f)[account][jid] + except KeyError: + s = [] + + f.close() + return s + + def save_new_secret(self, account, jid, secret): + f = open(secrets_filename, 'r') + secrets = pickle.load(f) + f.close() + + if not account in secrets: + secrets[account] = {} + + if not jid in secrets[account]: + secrets[account][jid] = [] + + secrets[account][jid].append(secret) + + f = open(secrets_filename, 'w') + pickle.dump(secrets, f) + f.close() + + def replace_secret(self, account, jid, old_secret, new_secret): + f = open(secrets_filename, 'r') + secrets = pickle.load(f) + f.close() + + this_secrets = secrets[account][jid] + this_secrets[this_secrets.index(old_secret)] = new_secret + + f = open(secrets_filename, 'w') + pickle.dump(secrets, f) + f.close() + def add_event(self, account, jid, type_, event_args): '''add an event to the gajim.events var''' # We add it to the gajim.events queue @@ -1657,7 +1700,7 @@ class Interface: def handle_session_negotiation(self, account, data): jid, session, form = data - + if form.getField('accept') and not form['accept'] in ('1', 'true'): dialogs.InformationDialog(_('Session negotiation cancelled'), _('The client at %s cancelled the session negotiation.') % (jid)) @@ -1727,8 +1770,6 @@ class Interface: negotiated.update(ask_user) session.accept_e2e_alice(form, negotiated) - negotiation.show_sas_dialog(jid, session.sas) - def reject_nondefault_options(widget): session.reject_negotiation() dialog.destroy() @@ -1739,19 +1780,20 @@ class Interface: on_response_no = reject_nondefault_options) else: session.accept_e2e_alice(form, negotiated) - negotiation.show_sas_dialog(jid, session.sas) return elif session.status == 'responded-e2e' and form.getType() == 'result': + session.check_identity = lambda: negotiation.show_sas_dialog(jid, session.sas) + try: session.accept_e2e_bob(form) except exceptions.NegotiationError, details: session.fail_bad_negotiation(details) - else: - negotiation.show_sas_dialog(jid, session.sas) return elif session.status == 'identified-alice' and form.getType() == 'result': + session.check_identity = lambda: negotiation.show_sas_dialog(jid, session.sas) + try: session.final_steps_alice(form) except exceptions.NegotiationError, details: @@ -2549,5 +2591,11 @@ if __name__ == '__main__': check_paths.check_and_possibly_create_paths() + # create secrets file (unless it exists) + if not os.path.exists(secrets_filename): + f = open(secrets_filename, 'w') + pickle.dump({}, f) + f.close() + Interface() gtk.main()