From 6fe668d863b6abc5195d5257f89e7a620618432b Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Fri, 29 Jun 2007 04:12:08 +0000 Subject: [PATCH] disable logs in encrypted sessions. --- src/chat_control.py | 7 +- src/common/connection.py | 5 +- src/common/connection_handlers.py | 32 +++-- src/common/stanza_session.py | 201 ++++++++++++++++++++++++------ src/gajim.py | 73 ++++++++++- src/message_control.py | 2 + 6 files changed, 256 insertions(+), 64 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 56019c6fa..5e3e30774 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1948,10 +1948,13 @@ class ChatControl(ChatControlBase): def _on_toggle_e2e_menuitem_activate(self, widget): if self.session.enable_encryption: - self.session.enable_encryption = False self.session.terminate_e2e() + + jid = str(self.session.jid) + + gajim.connections[self.account].delete_session(jid, self.session.thread_id) + self.session = gajim.connections[self.account].make_new_session(jid) else: - self.session.enable_encryption = True self.session.negotiate_e2e() def got_connected(self): diff --git a/src/common/connection.py b/src/common/connection.py index 8e6eab6d0..8dfbe96d1 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -921,10 +921,7 @@ class Connection(ConnectionHandlers): self.connection.send(msg_iq) - no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')\ - .split() - ji = gajim.get_jid_without_resource(jid) - if self.name not in no_log_for and ji not in no_log_for: + if session.is_loggable(): log_msg = msg if subject: log_msg = _('Subject: %s\n%s') % (subject, msg) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index c456e3510..c96354ca1 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1461,11 +1461,6 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, tim = time.strptime(tim, '%Y%m%dT%H:%M:%S') tim = time.localtime(timegm(tim)) jid = helpers.get_jid_from_iq(msg) - no_log_for = gajim.config.get_per('accounts', self.name, - 'no_log_for') - if not no_log_for: - no_log_for = '' - no_log_for = no_log_for.split() encrypted = False chatstate = None encTag = msg.getTag('x', namespace = common.xmpp.NS_ENCRYPTED) @@ -1525,7 +1520,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if not error_msg: error_msg = msgtxt msgtxt = None - if self.name not in no_log_for: + if session.is_loggable(): gajim.logger.write('error', frm, error_msg, tim = tim, subject = subject) self.dispatch('MSGERROR', (frm, msg.getErrorCode(), error_msg, msgtxt, @@ -1544,15 +1539,14 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if not self.last_history_line.has_key(jid): return self.dispatch('GC_MSG', (frm, msgtxt, tim, has_timestamp, msghtml)) - if self.name not in no_log_for and not int(float(time.mktime(tim)))\ + if session.is_loggable() and not int(float(time.mktime(tim)))\ <= self.last_history_line[jid] and msgtxt: gajim.logger.write('gc_msg', frm, msgtxt, tim = tim) return elif mtype == 'chat': # it's type 'chat' if not msg.getTag('body') and chatstate is None: #no return - if msg.getTag('body') and self.name not in no_log_for and jid not in\ - no_log_for and msgtxt: + if msg.getTag('body') and session.is_loggable() and msgtxt: msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim, subject = subject) else: # it's single message @@ -1564,7 +1558,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, password = invite.getTagData('password') self.dispatch('GC_INVITATION',(frm, jid_from, reason, password)) return - if self.name not in no_log_for and jid not in no_log_for and msgtxt: + if session.is_loggable()and msgtxt: gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim, subject = subject) mtype = 'normal' @@ -1576,7 +1570,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, # END messageCB def get_session(self, jid, thread_id, type): - '''returns an existing session between this connection and 'jid' or starts a new one.''' + '''returns an existing session between this connection and 'jid', returns a new one if none exist.''' session = self.find_session(jid, thread_id, type) if session: @@ -1588,6 +1582,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if bare_jid != jid: session = self.find_session(bare_jid, thread_id, type) if session: + print repr(bare_jid), repr(thread_id), repr(jid.split("/")[1]) self.move_session(bare_jid, thread_id, jid.split("/")[1]) return session @@ -1609,6 +1604,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, del self.sessions[jid] def move_session(self, original_jid, thread_id, to_resource): + '''moves a session to another resource.''' session = self.sessions[original_jid][thread_id] del self.sessions[original_jid][thread_id] @@ -1622,13 +1618,15 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, self.sessions[new_jid][thread_id] = session def find_null_session(self, jid): - '''returns the session between this connecting and 'jid' that we last sent a message in. -this is needed to handle clients that don't support threads; see XEP-0201.''' - all = self.sessions[jid].values() - null_sessions = filter(lambda s: not s.received_thread_id, all) - null_sessions.sort(key=lambda s: s.last_send) + '''finds all of the sessions between us and jid that jid hasn't sent a thread_id in yet. - return null_sessions[-1] +returns the session that we last sent a message to.''' + + sessions_with_jid = self.sessions[jid].values() + no_threadid_sessions = filter(lambda s: not s.received_thread_id, sessions_with_jid) + no_threadid_sessions.sort(key=lambda s: s.last_send) + + return no_threadid_sessions[-1] def make_new_session(self, jid, thread_id = None, type = 'chat'): sess = EncryptedStanzaSession(self, jid, thread_id, type) diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 6145db44e..edbee3df5 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -41,7 +41,7 @@ class StanzaSession(object): self.last_send = 0 self.status = None - self.features = {} + self.negotiated = {} def generate_thread_id(self): return "".join([random.choice(string.letters) for x in xrange(0,32)]) @@ -55,6 +55,29 @@ class StanzaSession(object): self.last_send = time.time() + def reject_negotiation(self, body = None): + msg = xmpp.Message() + feature = msg.NT.feature + feature.setNamespace(xmpp.NS_FEATURE) + + x = xmpp.DataForm(typ='submit') + x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn')) + x.addChild(node=xmpp.DataField(name='accept', value='0')) + + feature.addChild(node=x) + + if body: + msg.setBody(body) + + self.send(msg) + + self.cancelled_negotiation() + + def cancelled_negotiation(self): + '''A negotiation has been cancelled, so reset this session to its default state.''' + self.status = None + self.negotiated = {} + def terminate(self): msg = xmpp.Message() feature = msg.NT.feature @@ -101,6 +124,8 @@ class EncryptedStanzaSession(StanzaSession): def __init__(self, conn, jid, thread_id, type = 'chat'): StanzaSession.__init__(self, conn, jid, thread_id, type = 'chat') + self.loggable = True + self.xes = {} self.es = {} @@ -195,12 +220,24 @@ class EncryptedStanzaSession(StanzaSession): def hmac(self, key, content): return HMAC.new(key, content, self.hash_alg).digest() - # this should be more generic? def sha256(self, string): sh = SHA256.new() sh.update(string) return sh.digest() + base28_chr = "acdefghikmopqruvwxy123456789" + + def sas_28x5(self, m_a, form_b): + sha = self.sha256(m_a + form_b + 'Short Authentication String') + lsb24 = self.decode_mpi(sha[-3:]) + return self.base28(lsb24) + + def base28(self, n): + if n >= 28: + return self.base28(n / 28) + self.base28_chr[n % 28] + else: + return self.base28_chr[n] + def generate_initiator_keys(self, k): return (self.hmac(k, 'Initiator Cipher Key'), self.hmac(k, 'Initiator MAC Key'), @@ -265,7 +302,15 @@ class EncryptedStanzaSession(StanzaSession): def decrypt(self, ciphertext): return self.decrypter.decrypt(ciphertext) + def logging_preference(self): + if gajim.config.get('log_encrypted_sessions'): + return ["may", "mustnot"] + else: + return ["mustnot", "may"] + def negotiate_e2e(self): + self.negotiated = {} + request = xmpp.Message() feature = request.NT.feature feature.setNamespace(xmpp.NS_FEATURE) @@ -276,8 +321,7 @@ class EncryptedStanzaSession(StanzaSession): x.addChild(node=xmpp.DataField(name='accept', value='1', typ='boolean', required=True)) # this field is incorrectly called 'otr' in XEPs 0116 and 0217 - # unsupported options: 'mustnot' - x.addChild(node=xmpp.DataField(name='logging', typ='list-single', options=['may'], required=True)) + x.addChild(node=xmpp.DataField(name='logging', typ='list-single', options=self.logging_preference(), required=True)) # unsupported options: 'disabled', 'enabled' x.addChild(node=xmpp.DataField(name='disclosure', typ='list-single', options=['never'], required=True)) @@ -317,12 +361,10 @@ class EncryptedStanzaSession(StanzaSession): self.send(request) # 4.3 esession response (bob) - def respond_e2e_bob(self, request_form): - response = xmpp.Message() - feature = response.NT.feature - feature.setNamespace(xmpp.NS_FEATURE) - - x = xmpp.DataForm(typ='submit') + def verify_options_bob(self, form): + negotiated = {} + not_acceptable = [] + ask_user = {} fixed = { 'disclosure': 'never', 'security': 'e2e', @@ -333,21 +375,16 @@ class EncryptedStanzaSession(StanzaSession): 'init_pubkey': 'none', 'resp_pubkey': 'none', 'ver': '1.0', - 'sas_algs': 'sas28x5', - 'logging': 'may' } - - not_acceptable = [] + 'sas_algs': 'sas28x5' } self.encryptable_stanzas = ['message'] + self.sas_algs = 'sas28x5' self.cipher = AES self.hash_alg = SHA256 self.compression = None - x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn')) - x.addChild(node=xmpp.DataField(name='accept', value='true')) - - for name, field in map(lambda name: (name, request_form.getField(name)), request_form.asDict().keys()): + for name, field in map(lambda name: (name, form.getField(name)), form.asDict().keys()): options = map(lambda x: x[1], field.getOptions()) values = field.getValues() @@ -356,28 +393,61 @@ class EncryptedStanzaSession(StanzaSession): if name in fixed: if fixed[name] in options: - x.addChild(node=xmpp.DataField(name=name, value=fixed[name])) + negotiated[name] = fixed[name] else: not_acceptable.append(name) - elif name == 'modp': - # the offset of the group we chose (need it to match up with the dhhash) - group_order = 0 - self.modp = int(options[group_order]) - x.addChild(node=xmpp.DataField(name='modp', value=self.modp)) - - g = dh.generators[self.modp] - p = dh.primes[self.modp] elif name == 'rekey_freq': preferred = int(options[0]) - x.addChild(node=xmpp.DataField(name='rekey_freq', value=preferred)) - + negotiated['rekey_freq'] = preferred self.rekey_freq = preferred - elif name == 'my_nonce': - self.n_o = base64.b64decode(field.getValue()) + elif name == 'logging': + my_prefs = self.logging_preference() - # XXX do something with not_acceptable + if my_prefs[0] in options: + pref = my_prefs[0] + negotiated['logging'] = pref + else: + for pref in my_prefs: + if pref in options: + ask_user['logging'] = pref + break - self.He = request_form.getField('dhhashes').getValues()[group_order].encode("utf8") + if not 'logging' in ask_user: + not_acceptable.append(name) + else: + # some things are handled elsewhere, some things are not-implemented + pass + + return (negotiated, not_acceptable, ask_user) + + # 4.3 esession response (bob) + def respond_e2e_bob(self, form, negotiated, not_acceptable): + response = xmpp.Message() + feature = response.NT.feature + feature.setNamespace(xmpp.NS_FEATURE) + + x = xmpp.DataForm(typ='submit') + + x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn')) + x.addChild(node=xmpp.DataField(name='accept', value='true')) + + for name in negotiated: + x.addChild(node=xmpp.DataField(name=name, value=negotiated[name])) + + self.negotiated = negotiated + + # the offset of the group we chose (need it to match up with the dhhash) + group_order = 0 + self.modp = int(form.getField('modp').getOptions()[group_order][1]) + x.addChild(node=xmpp.DataField(name='modp', value=self.modp)) + + g = dh.generators[self.modp] + p = dh.primes[self.modp] + + self.n_o = base64.b64decode(form['my_nonce']) + + dhhashes = form.getField('dhhashes').getValues() + self.He = dhhashes[group_order].encode("utf8") bytes = int(self.n / 8) @@ -389,30 +459,61 @@ class EncryptedStanzaSession(StanzaSession): self.y = self.srand(2 ** (2 * self.n - 1), p - 1) self.d = self.powmod(g, self.y, p) - to_add = { 'my_nonce': self.n_s, 'dhkeys': self.encode_mpi(self.d), 'counter': self.encode_mpi(self.c_o), 'nonce': self.n_o } + to_add = { 'my_nonce': self.n_s, + 'dhkeys': self.encode_mpi(self.d), + 'counter': self.encode_mpi(self.c_o), + 'nonce': self.n_o } for name in to_add: b64ed = base64.b64encode(to_add[name]) x.addChild(node=xmpp.DataField(name=name, value=b64ed)) - self.form_a = ''.join(map(lambda el: xmpp.c14n.c14n(el), request_form.getChildren())) + self.form_a = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) self.form_b = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren())) self.status = 'responded-e2e' feature.addChild(node=x) + + if not_acceptable: + pass +# XXX +# +# +# +# +# +# + self.send(response) # 'Alice Accepts' - def accept_e2e_alice(self, form): + def verify_options_alice(self, form): # 1. Verify that the ESession options selected by Bob are acceptable + negotiated = {} + ask_user = {} + not_acceptable = [] + + if not form['logging'] in self.logging_preference(): + not_acceptable.append(form['logging']) + elif form['logging'] != self.logging_preference()[0]: + ask_user['logging'] = form['logging'] + else: + negotiated['logging'] = self.logging_preference()[0] + + return (negotiated, not_acceptable, ask_user) + + # 'Alice Accepts', continued + def accept_e2e_alice(self, form, negotiated): self.encryptable_stanzas = ['message'] self.sas_algs = 'sas28x5' self.cipher = AES self.hash_alg = SHA256 self.compression = None + self.negotiated = negotiated + # 2. Return a error to Bob unless: 1 < d < p - 1 self.form_b = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) @@ -457,6 +558,11 @@ class EncryptedStanzaSession(StanzaSession): m_a = self.hmac(self.km_s, self.encode_mpi(old_c_s) + id_a) + # check for a retained secret + # if none exists, prompt the user with the SAS + if self.sas_algs == 'sas28x5': + print "sas: %s" % self.sas_28x5(m_a, self.form_b) + result.addChild(node=xmpp.DataField(name='identity', value=base64.b64encode(id_a))) result.addChild(node=xmpp.DataField(name='mac', value=base64.b64encode(m_a))) @@ -520,6 +626,11 @@ class EncryptedStanzaSession(StanzaSession): self.srs = '' oss = '' + # check for a retained secret + # if none exists, prompt the user with the SAS + if self.sas_algs == 'sas28x5': + print "sas: %s" % self.sas_28x5(m_a, self.form_b) + k = self.sha256(k + self.srs + oss) # XXX I can skip generating ks_o here @@ -556,6 +667,10 @@ class EncryptedStanzaSession(StanzaSession): self.srs = self.hmac(k, 'New Retained Secret') # destroy k + + if self.negotiated['logging'] == 'mustnot': + self.loggable = False + self.status = 'active' self.enable_encryption = True @@ -594,6 +709,9 @@ class EncryptedStanzaSession(StanzaSession): # Note: If Alice discovers an error then she SHOULD ignore any encrypted content she received in the stanza. # XXX check for MAC equality? + + if self.negotiated['logging'] == 'mustnot': + self.loggable = False self.status = 'active' self.enable_encryption = True @@ -647,3 +765,14 @@ class EncryptedStanzaSession(StanzaSession): StanzaSession.acknowledge_termination(self) self.enable_encryption = False + + def is_loggable(self): + name = self.conn.name + no_log_for = gajim.config.get_per('accounts', name, '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 diff --git a/src/gajim.py b/src/gajim.py index ca8bc67dd..b8c7a1b85 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1657,13 +1657,64 @@ class Interface: def handle_session_negotiation(self, account, data): jid, session, form = data - - # encrypted session states - if form.getType() == 'form' and u'e2e' in map(lambda x: x[1], form.getField('security').getOptions()): - session.respond_e2e_bob(form) + + 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)) + session.cancelled_negotiation() return + + # encrypted session states. these are descriped in stanza_session.py + + # bob responds + if form.getType() == 'form' and u'e2e' in map(lambda x: x[1], form.getField('security').getOptions()): + negotiated, not_acceptable, ask_user = session.verify_options_bob(form) + + if ask_user: + def accept_nondefault_options(widget): + negotiated.update(ask_user) + session.respond_e2e_bob(form, negotiated, not_acceptable) + + dialog.destroy() + + def reject_nondefault_options(widget): + for key in ask_user.keys(): + not_acceptable.append(key) # XXX for some reason I can't concatenate using += here? + session.respond_e2e_bob(form, negotiated, not_acceptable) + + dialog.destroy() + + dialog = dialogs.ConfirmationDialog(_('confirm these negotiation options'), + _('are the following options acceptable? %s') % (ask_user), + on_response_ok = accept_nondefault_options, + on_response_cancel = reject_nondefault_options) + else: + session.respond_e2e_bob(form, negotiated, not_acceptable) + + return + + # alice accepts elif session.status == 'requested-e2e' and form.getType() == 'submit': - session.accept_e2e_alice(form) + negotiated, not_acceptable, ask_user = session.verify_options_alice(form) + + if ask_user: + def accept_nondefault_options(widget): + negotiated.update(ask_user) + session.accept_e2e_alice(form, negotiated) + + dialog.destroy() + + def reject_nondefault_options(widget): + session.reject_negotiation() + dialog.destroy() + + dialog = dialogs.ConfirmationDialog(_('confirm these negotiation options'), + _('are the following options acceptable? %s') % (ask_user), + on_response_ok = accept_nondefault_options, + on_response_cancel = reject_nondefault_options) + else: + session.accept_e2e_alice(form, negotiated) + return elif session.status == 'responded-e2e' and form.getType() == 'result': session.accept_e2e_bob(form) @@ -1671,6 +1722,18 @@ class Interface: elif session.status == 'identified-alice' and form.getType() == 'result': session.final_steps_alice(form) return + + if form.getField('terminate'): + if form.getField('terminate').getValue() in ('1', 'true'): + session.acknowledge_termination() + gajim.connections[account].delete_session(str(jid), session.thread_id) + + ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account) + + if ctrl: + ctrl.session = gajim.connections[self.account].make_new_session(str(jid)) + + return # non-esession negotiation. this isn't very useful, but i'm keeping it around # to test my test suite. diff --git a/src/message_control.py b/src/message_control.py index 6835ddb32..d8f4fa0d6 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -115,6 +115,8 @@ class MessageControl: return if self.session: print "starting a new session, forgetting about the old one!" + gajim.connections[self.account].delete_session(self.contact.jid, self.session.thread_id) + self.session = session def send_message(self, message, keyID = '', type = 'chat',