From ec37a97ed637cbb4d23321258f2f1226a1646226 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Wed, 30 May 2007 15:31:41 +0000 Subject: [PATCH 01/40] add to replies to single message that contain a --- src/common/connection.py | 6 +++++- src/common/connection_handlers.py | 3 ++- src/dialogs.py | 7 ++++--- src/gajim.py | 7 ++++--- src/roster_window.py | 8 ++++---- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/common/connection.py b/src/common/connection.py index 717d8ff91..a8460ba27 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -839,7 +839,7 @@ class Connection(ConnectionHandlers): def send_message(self, jid, msg, keyID, type = 'chat', subject='', chatstate = None, msg_id = None, composing_jep = None, resource = None, - user_nick = None, xhtml = None): + user_nick = None, xhtml = None, thread = None): if not self.connection: return 1 if msg and not xhtml and gajim.config.get('rst_formatting_outgoing_messages'): @@ -883,6 +883,10 @@ class Connection(ConnectionHandlers): if msgenc: msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc) + # XEP-0201 + if thread: + msg_iq.setTag("thread").setData(thread) + # JEP-0172: user_nickname if user_nick: msg_iq.setTag('nick', namespace = common.xmpp.NS_NICK).setData( diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 4f8ae386e..98e7f892a 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1407,6 +1407,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, msghtml = msg.getXHTML() mtype = msg.getType() subject = msg.getSubject() # if not there, it's None + thread = msg.getThread() tim = msg.getTimestamp() tim = time.strptime(tim, '%Y%m%dT%H:%M:%S') tim = time.localtime(timegm(tim)) @@ -1523,7 +1524,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if treat_as: mtype = treat_as self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, - subject, chatstate, msg_id, composing_jep, user_nick, msghtml)) + subject, chatstate, msg_id, composing_jep, user_nick, msghtml, thread)) # END messageCB def _pubsubEventCB(self, con, msg): diff --git a/src/dialogs.py b/src/dialogs.py index 198215aab..2d0409676 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -1686,7 +1686,7 @@ class SingleMessageWindow: or 'receive'. ''' def __init__(self, account, to = '', action = '', from_whom = '', - subject = '', message = '', resource = ''): + subject = '', message = '', resource = '', thread = None): self.account = account self.action = action @@ -1695,6 +1695,7 @@ class SingleMessageWindow: self.to = to self.from_whom = from_whom self.resource = resource + self.thread = thread self.xml = gtkgui_helpers.get_glade('single_message_window.glade') self.window = self.xml.get_widget('single_message_window') @@ -1896,7 +1897,7 @@ class SingleMessageWindow: # FIXME: allow GPG message some day gajim.connections[self.account].send_message(to_whom_jid, message, - keyID = None, type = 'normal', subject=subject) + keyID = None, type = 'normal', subject=subject, thread = self.thread) self.subject_entry.set_text('') # we sent ok, clear the subject self.message_tv_buffer.set_text('') # we sent ok, clear the textview @@ -1913,7 +1914,7 @@ class SingleMessageWindow: self.window.destroy() SingleMessageWindow(self.account, to = self.from_whom, action = 'send', from_whom = self.from_whom, subject = self.subject, - message = self.message) + message = self.message, thread = self.thread) def on_send_and_close_button_clicked(self, widget): self.send_single_message() diff --git a/src/gajim.py b/src/gajim.py index 0bb8df28d..738d98962 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -665,7 +665,7 @@ class Interface: def handle_event_msg(self, account, array): # 'MSG' (account, (jid, msg, time, encrypted, msg_type, subject, - # chatstate, msg_id, composing_jep, user_nick, xhtml)) + # chatstate, msg_id, composing_jep, user_nick, xhtml, thread)) # user_nick is JEP-0172 full_jid_with_resource = array[0] @@ -680,6 +680,7 @@ class Interface: msg_id = array[7] composing_jep = array[8] xhtml = array[10] + thread = array[11] if gajim.config.get('ignore_incoming_xhtml'): xhtml = None if gajim.jid_is_transport(jid): @@ -775,12 +776,12 @@ class Interface: if encrypted: self.roster.on_message(jid, message, array[2], account, array[3], msg_type, subject, resource, msg_id, array[9], - advanced_notif_num) + advanced_notif_num, thread = thread) else: # xhtml in last element self.roster.on_message(jid, message, array[2], account, array[3], msg_type, subject, resource, msg_id, array[9], - advanced_notif_num, xhtml = xhtml) + advanced_notif_num, xhtml = xhtml, thread = thread) nickname = gajim.get_name_from_jid(account, jid) # Check and do wanted notifications msg = message diff --git a/src/roster_window.py b/src/roster_window.py index 15c67e158..b1c3700d0 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -3531,7 +3531,7 @@ class RosterWindow: def on_message(self, jid, msg, tim, account, encrypted = False, msg_type = '', subject = None, resource = '', msg_id = None, - user_nick = '', advanced_notif_num = None, xhtml = None): + user_nick = '', advanced_notif_num = None, xhtml = None, thread = None): '''when we receive a message''' contact = None # if chat window will be for specific resource @@ -3588,7 +3588,7 @@ class RosterWindow: if msg_type == 'normal' and popup: # it's single message to be autopopuped dialogs.SingleMessageWindow(account, contact.jid, action = 'receive', from_whom = jid, subject = subject, - message = msg, resource = resource) + message = msg, resource = resource, thread = thread) return # We print if window is opened and it's not a single message @@ -3611,7 +3611,7 @@ class RosterWindow: show_in_roster = notify.get_show_in_roster(event_type, account, contact) show_in_systray = notify.get_show_in_systray(event_type, account, contact) event = gajim.events.create_event(type_, (msg, subject, msg_type, tim, - encrypted, resource, msg_id, xhtml), show_in_roster = show_in_roster, + encrypted, resource, msg_id, xhtml, thread), show_in_roster = show_in_roster, show_in_systray = show_in_systray) gajim.events.add_event(account, fjid, event) if popup: @@ -3873,7 +3873,7 @@ class RosterWindow: if event.type_ == 'normal': dialogs.SingleMessageWindow(account, jid, action = 'receive', from_whom = jid, subject = data[1], - message = data[0], resource = data[5]) + message = data[0], resource = data[5], thread = data[8]) gajim.interface.remove_first_event(account, jid, event.type_) return True elif event.type_ == 'file-request': From 3bb2eabadb68d102a91209c5b568a8c71c77ca99 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 30 May 2007 16:57:37 +0000 Subject: [PATCH 02/40] add thread to zeroconf connections too --- src/common/zeroconf/connection_handlers_zeroconf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/zeroconf/connection_handlers_zeroconf.py b/src/common/zeroconf/connection_handlers_zeroconf.py index 2d36d4661..4391abaf5 100644 --- a/src/common/zeroconf/connection_handlers_zeroconf.py +++ b/src/common/zeroconf/connection_handlers_zeroconf.py @@ -639,6 +639,7 @@ class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream): msghtml = msg.getXHTML() mtype = msg.getType() subject = msg.getSubject() # if not there, it's None + thread = msg.getThread() tim = msg.getTimestamp() tim = time.strptime(tim, '%Y%m%dT%H:%M:%S') tim = time.localtime(timegm(tim)) @@ -715,7 +716,7 @@ class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream): msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim, subject = subject) self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, subject, - chatstate, msg_id, composing_jep, user_nick, msghtml)) + chatstate, msg_id, composing_jep, user_nick, msghtml, thread)) elif mtype == 'normal': # it's single message if self.name not in no_log_for and jid not in no_log_for and msgtxt: gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim, From 00209abaf933844555cf84ce95dabfdba1877e36 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Thu, 31 May 2007 01:47:08 +0000 Subject: [PATCH 03/40] support for one 'chat' session per Contact --- src/chat_control.py | 3 +++ src/common/contacts.py | 16 ++++++++++++++++ src/message_control.py | 16 +++++++++++++++- src/roster_window.py | 2 ++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/chat_control.py b/src/chat_control.py index baa9beec8..7b62ec6cd 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1815,6 +1815,9 @@ class ChatControl(ChatControlBase): encrypted = data[4], subject = data[1], xhtml = data[7]) if len(data) > 6 and isinstance(data[6], int): message_ids.append(data[6]) + + if len(data) > 8: + self.set_thread_id(data[8]) if message_ids: gajim.logger.set_read_messages(message_ids) gajim.events.remove_events(self.account, jid_with_resource, diff --git a/src/common/contacts.py b/src/common/contacts.py index b309981d4..d6352cea6 100644 --- a/src/common/contacts.py +++ b/src/common/contacts.py @@ -16,6 +16,8 @@ import common.gajim +import random, string + class Contact: '''Information concerning each contact''' def __init__(self, jid='', name='', groups=[], show='', status='', sub='', @@ -50,6 +52,20 @@ class Contact: self.chatstate = chatstate self.last_status_time = last_status_time + # XEP-0201 + self.sessions = {} + + def new_session(self): + thread_id = "".join([random.choice(string.letters) for x in xrange(0,32)]) + self.sessions[self.get_full_jid()] = thread_id + return thread_id + + def get_session(self): + try: + return self.sessions[self.get_full_jid()] + except KeyError: + return None + def get_full_jid(self): if self.resource: return self.jid + '/' + self.resource diff --git a/src/message_control.py b/src/message_control.py index e2d16a45b..11b3b6996 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -37,6 +37,8 @@ class MessageControl: self.hide_chat_buttons_current = False self.resource = resource + self.thread_id = self.contact.get_session() + gajim.last_message_time[self.account][self.get_full_jid()] = 0 self.xml = gtkgui_helpers.get_glade('message_window.glade', widget_name) @@ -110,14 +112,26 @@ class MessageControl: def get_specific_unread(self): return len(gajim.events.get_events(self.account, self.contact.jid)) + def set_thread_id(self, thread_id): + if thread_id == self.thread_id: + return + if self.thread_id: + print "starting a new session, forgetting about the old one!" + self.thread_id = thread_id + self.contact.sessions[self.contact.get_full_jid()] = thread_id + def send_message(self, message, keyID = '', type = 'chat', chatstate = None, msg_id = None, composing_jep = None, resource = None, user_nick = None): '''Send the given message to the active tab. Doesn't return None if error ''' jid = self.contact.jid + + if not self.thread_id: + self.thread_id = self.contact.new_session() + # Send and update history return gajim.connections[self.account].send_message(jid, message, keyID, type = type, chatstate = chatstate, msg_id = msg_id, composing_jep = composing_jep, resource = self.resource, - user_nick = user_nick) + user_nick = user_nick, thread = self.thread_id) diff --git a/src/roster_window.py b/src/roster_window.py index b1c3700d0..4f0019aa5 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -3596,6 +3596,8 @@ class RosterWindow: typ = '' if msg_type == 'error': typ = 'status' + if thread: + ctrl.set_thread_id(thread) ctrl.print_conversation(msg, typ, tim = tim, encrypted = encrypted, subject = subject, xhtml = xhtml) if msg_id: From 46bc373cc3c921064600d49090c1b985a5262c87 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Fri, 1 Jun 2007 17:09:56 +0000 Subject: [PATCH 04/40] pop up a (useless) DataFormWidget when someone initiates a session negotiation --- src/common/connection_handlers.py | 21 +++++++++++++++++++++ src/gajim.py | 8 ++++++++ 2 files changed, 29 insertions(+) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 98e7f892a..fa392b71b 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1197,6 +1197,22 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, self.dispatch('HTTP_AUTH', (method, url, id, iq_obj, msg)); raise common.xmpp.NodeProcessed + def _FeatureNegCB(self, con, stanza): + gajim.log.debug('FeatureNegCB') + feature = stanza.getTag('feature') + form = common.xmpp.DataForm(node=feature.getTag('x')) + + if form['FORM_TYPE'] == 'urn:xmpp:ssn': + self.dispatch('SESSION_NEG', (stanza.getFrom(), stanza.getThread(), form)) + else: + reply = stanza.buildReply() + reply.setType('error') + + reply.addChild(feature) + reply.addChild(node=xmpp.ErrorNode('service-unavailable', typ='cancel')) + + con.send(reply) + def _ErrorCB(self, con, iq_obj): gajim.log.debug('ErrorCB') if iq_obj.getQueryNS() == common.xmpp.NS_VERSION: @@ -1403,6 +1419,11 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, common.xmpp.NS_HTTP_AUTH: self._HttpAuthCB(con, msg) return + if msg.getTag('feature') and msg.getTag('feature').namespace == \ + common.xmpp.NS_FEATURE: + self._FeatureNegCB(con, msg) + return + msgtxt = msg.getBody() msghtml = msg.getXHTML() mtype = msg.getType() diff --git a/src/gajim.py b/src/gajim.py index 738d98962..2607b6b86 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -103,6 +103,8 @@ import message_control from chat_control import ChatControlBase from atom_window import AtomWindow +import negotiation + from common import exceptions from common.zeroconf import connection_zeroconf from common import dbus_support @@ -1653,6 +1655,11 @@ class Interface: atom_entry, = data AtomWindow.newAtomEntry(atom_entry) + def handle_session_negotiation(self, account, data): + jid, thread_id, form = data + # XXX check negotiation state, etc. + negotiation.FeatureNegotiationWindow(account, jid, thread_id, form) + def handle_event_privacy_lists_received(self, account, data): # ('PRIVACY_LISTS_RECEIVED', account, list) if not self.instances.has_key(account): @@ -2090,6 +2097,7 @@ class Interface: 'SEARCH_FORM': self.handle_event_search_form, 'SEARCH_RESULT': self.handle_event_search_result, 'RESOURCE_CONFLICT': self.handle_event_resource_conflict, + 'SESSION_NEG': self.handle_session_negotiation, } gajim.handlers = self.handlers From 654b3ff0126777985ee98004dc6db75bd9b779ae Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Fri, 1 Jun 2007 17:12:49 +0000 Subject: [PATCH 05/40] forgot to include this in last commit --- src/negotiation.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/negotiation.py diff --git a/src/negotiation.py b/src/negotiation.py new file mode 100644 index 000000000..2cc824eb0 --- /dev/null +++ b/src/negotiation.py @@ -0,0 +1,23 @@ +import gtkgui_helpers +import dataforms_widget + +from common import dataforms + +class FeatureNegotiationWindow: + '''FeatureNegotiotionWindow class''' + def __init__(self, account, jid, thread_id, form): + self.account = account + self.jid = jid + self.form = form + + self.xml = gtkgui_helpers.get_glade('data_form_window.glade', 'data_form_window') + self.window = self.xml.get_widget('data_form_window') + + config_vbox = self.xml.get_widget('config_vbox') + dataform = dataforms.ExtendForm(node = self.form) + self.data_form_widget = dataforms_widget.DataFormWidget(dataform) + self.data_form_widget.show() + config_vbox.pack_start(self.data_form_widget) + + self.xml.signal_autoconnect(self) + self.window.show_all() From 32804eed1c46e9ab25c31fbb44e883e8e00010c7 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Fri, 1 Jun 2007 18:19:19 +0000 Subject: [PATCH 06/40] reject a session negotiation request --- src/gajim.py | 2 ++ src/negotiation.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/gajim.py b/src/gajim.py index 2607b6b86..8ab4d7ad3 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1658,6 +1658,8 @@ class Interface: def handle_session_negotiation(self, account, data): jid, thread_id, form = data # XXX check negotiation state, etc. + + # XXX check if the user has chosen to autoaccept these values negotiation.FeatureNegotiationWindow(account, jid, thread_id, form) def handle_event_privacy_lists_received(self, account, data): diff --git a/src/negotiation.py b/src/negotiation.py index 2cc824eb0..9942f5dcc 100644 --- a/src/negotiation.py +++ b/src/negotiation.py @@ -2,6 +2,8 @@ import gtkgui_helpers import dataforms_widget from common import dataforms +from common import gajim +from common import xmpp class FeatureNegotiationWindow: '''FeatureNegotiotionWindow class''' @@ -9,6 +11,7 @@ class FeatureNegotiationWindow: self.account = account self.jid = jid self.form = form + self.thread_id = thread_id self.xml = gtkgui_helpers.get_glade('data_form_window.glade', 'data_form_window') self.window = self.xml.get_widget('data_form_window') @@ -21,3 +24,23 @@ class FeatureNegotiationWindow: self.xml.signal_autoconnect(self) self.window.show_all() + + def on_cancel_button_clicked(self, widget): + # XXX determine whether to reveal presence + + rejection = xmpp.Message(self.jid) + rejection.setThread(self.thread_id) + feature = rejection.NT.feature + feature.setNamespace(xmpp.NS_FEATURE) + + x = xmpp.DataForm(typ='submit') + x.addChild(node=xmpp.DataField('FORM_TYPE', value='urn:xmpp:ssn')) + x.addChild(node=xmpp.DataField('accept', value='false', typ='boolean')) + + feature.addChild(node=x) + + # XXX optional + + gajim.connections[self.account].send_stanza(rejection) + + self.window.destroy() From d696f79c558343f8e9a31ad7221094487a472437 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Fri, 1 Jun 2007 22:08:23 +0000 Subject: [PATCH 07/40] can now accept a session negotiation --- src/gajim.py | 18 ++++++++++++++++-- src/negotiation.py | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/gajim.py b/src/gajim.py index 8ab4d7ad3..53ab20863 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1658,9 +1658,23 @@ class Interface: def handle_session_negotiation(self, account, data): jid, thread_id, form = data # XXX check negotiation state, etc. + # XXX check if we can autoaccept - # XXX check if the user has chosen to autoaccept these values - negotiation.FeatureNegotiationWindow(account, jid, thread_id, form) + if form.getType() == 'form': + ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account) + if not ctrl: + resource = jid.getResource() + contact = gajim.contacts.get_contact(account, str(jid), resource) + if not contact: + connection = gajim.connections[account] + contact = gajim.contacts.create_contact(jid = jid.getStripped(), resource = resource, show = connection.get_status()) + self.roster.new_chat(contact, account, resource = resource) + + ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account) + + ctrl.set_thread_id(thread_id) + + negotiation.FeatureNegotiationWindow(account, jid, thread_id, form) def handle_event_privacy_lists_received(self, account, data): # ('PRIVACY_LISTS_RECEIVED', account, list) diff --git a/src/negotiation.py b/src/negotiation.py index 9942f5dcc..32fbf0a3a 100644 --- a/src/negotiation.py +++ b/src/negotiation.py @@ -25,6 +25,21 @@ class FeatureNegotiationWindow: self.xml.signal_autoconnect(self) self.window.show_all() + def on_ok_button_clicked(self, widget): + acceptance = xmpp.Message(self.jid) + acceptance.setThread(self.thread_id) + feature = acceptance.NT.feature + feature.setNamespace(xmpp.NS_FEATURE) + + form = self.data_form_widget.data_form + form.setAttr('type', 'submit') + + feature.addChild(node=form) + + gajim.connections[self.account].send_stanza(acceptance) + + self.window.destroy() + def on_cancel_button_clicked(self, widget): # XXX determine whether to reveal presence From 17c5bf5e525a9e570dfa068ff9760e3983bffa7f Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Tue, 5 Jun 2007 21:26:45 +0000 Subject: [PATCH 08/40] massive everything-breaking overhaul for per-session windows --- src/chat_control.py | 21 +++--- src/common/connection.py | 10 ++- src/common/connection_handlers.py | 57 +++++++++++++-- src/common/contacts.py | 24 +++---- src/common/helpers.py | 8 +-- src/common/stanza_session.py | 51 ++++++++++++++ src/dialogs.py | 12 ++-- src/gajim.py | 98 ++++++++++++++------------ src/message_control.py | 15 +--- src/message_window.py | 86 +++++++++++++++-------- src/negotiation.py | 8 +-- src/notify.py | 4 +- src/roster_window.py | 113 +++++++++++++++++------------- 13 files changed, 319 insertions(+), 188 deletions(-) create mode 100644 src/common/stanza_session.py diff --git a/src/chat_control.py b/src/chat_control.py index 7b62ec6cd..7d80a410e 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -577,7 +577,7 @@ class ChatControlBase(MessageControl): type_ = 'printed_' + self.type_id event = 'message_received' show_in_roster = notify.get_show_in_roster(event, - self.account, self.contact) + self.account, self.contact, self.session) show_in_systray = notify.get_show_in_systray(event, self.account, self.contact) if gc_message: @@ -606,7 +606,7 @@ class ChatControlBase(MessageControl): not self.parent_win.is_active() or not end) and \ kind in ('incoming', 'incoming_queue'): self.parent_win.redraw_tab(self) - ctrl = gajim.interface.msg_win_mgr.get_control(full_jid, self.account) + ctrl = gajim.interface.msg_win_mgr.get_control(full_jid, self.account, self.session.thread_id) if not self.parent_win.is_active(): self.parent_win.show_title(True, ctrl) # Enabled Urgent hint else: @@ -905,10 +905,10 @@ class ChatControl(ChatControlBase): old_msg_kind = None # last kind of the printed message CHAT_CMDS = ['clear', 'compact', 'help', 'ping'] - def __init__(self, parent_win, contact, acct, resource = None): + def __init__(self, parent_win, contact, acct, session, resource = None): ChatControlBase.__init__(self, self.TYPE_ID, parent_win, 'chat_child_vbox', contact, acct, resource) - + # for muc use: # widget = self.xml.get_widget('muc_window_actions_button') widget = self.xml.get_widget('message_window_actions_button') @@ -935,7 +935,7 @@ class ChatControl(ChatControlBase): # it is on enter-notify and leave-notify so no need to be per jid self.show_bigger_avatar_timeout_id = None self.bigger_avatar_window = None - self.show_avatar(self.contact.resource) + self.show_avatar(self.contact.resource) # chatstate timers and state self.reset_kbd_mouse_timeout_vars() @@ -949,7 +949,7 @@ class ChatControl(ChatControlBase): id = message_tv_buffer.connect('changed', self._on_message_tv_buffer_changed) self.handlers[id] = message_tv_buffer - + widget = self.xml.get_widget('avatar_eventbox') id = widget.connect('enter-notify-event', self.on_avatar_eventbox_enter_notify_event) @@ -969,7 +969,9 @@ class ChatControl(ChatControlBase): if self.contact.jid in gajim.encrypted_chats[self.account]: self.xml.get_widget('gpg_togglebutton').set_active(True) - + + self.session = session + self.status_tooltip = gtk.Tooltips() self.update_ui() # restore previous conversation @@ -1797,7 +1799,7 @@ class ChatControl(ChatControlBase): # Is it a pm ? is_pm = False room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) - control = gajim.interface.msg_win_mgr.get_control(room_jid, self.account) + control = gajim.interface.msg_win_mgr.get_control(room_jid, self.account, self.session.thread_id) if control and control.type_id == message_control.TYPE_GC: is_pm = True # list of message ids which should be marked as read @@ -1815,9 +1817,6 @@ class ChatControl(ChatControlBase): encrypted = data[4], subject = data[1], xhtml = data[7]) if len(data) > 6 and isinstance(data[6], int): message_ids.append(data[6]) - - if len(data) > 8: - self.set_thread_id(data[8]) if message_ids: gajim.logger.set_read_messages(message_ids) gajim.events.remove_events(self.account, jid_with_resource, diff --git a/src/common/connection.py b/src/common/connection.py index a8460ba27..6deee84a6 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -48,6 +48,8 @@ log = logging.getLogger('gajim.c.connection') import gtkgui_helpers +import time + class Connection(ConnectionHandlers): '''Connection class''' def __init__(self, name): @@ -839,7 +841,7 @@ class Connection(ConnectionHandlers): def send_message(self, jid, msg, keyID, type = 'chat', subject='', chatstate = None, msg_id = None, composing_jep = None, resource = None, - user_nick = None, xhtml = None, thread = None): + user_nick = None, xhtml = None, session = None): if not self.connection: return 1 if msg and not xhtml and gajim.config.get('rst_formatting_outgoing_messages'): @@ -884,8 +886,10 @@ class Connection(ConnectionHandlers): msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc) # XEP-0201 - if thread: - msg_iq.setTag("thread").setData(thread) + if session: + session.last_send = time.time() + if session.thread_id: + msg_iq.setThread(session.thread_id) # JEP-0172: user_nickname if user_nick: diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index fa392b71b..346497f5c 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -37,6 +37,8 @@ from common import atom from common.commands import ConnectionCommands from common.pubsub import ConnectionPubSub +from common.stanza_session import StanzaSession + STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'error'] # kind of events we can wait for an answer @@ -1171,6 +1173,10 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, # keep the latest subscribed event for each jid to prevent loop when we # acknoledge presences self.subscribed_events = {} + + # keep track of sessions this connection has with other JIDs + self.sessions = {} + try: idle.init() except: @@ -1197,13 +1203,13 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, self.dispatch('HTTP_AUTH', (method, url, id, iq_obj, msg)); raise common.xmpp.NodeProcessed - def _FeatureNegCB(self, con, stanza): + def _FeatureNegCB(self, con, stanza, session): gajim.log.debug('FeatureNegCB') feature = stanza.getTag('feature') form = common.xmpp.DataForm(node=feature.getTag('x')) if form['FORM_TYPE'] == 'urn:xmpp:ssn': - self.dispatch('SESSION_NEG', (stanza.getFrom(), stanza.getThread(), form)) + self.dispatch('SESSION_NEG', (stanza.getFrom(), session, form)) else: reply = stanza.buildReply() reply.setType('error') @@ -1410,6 +1416,17 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, def _messageCB(self, con, msg): '''Called when we receive a message''' + frm = helpers.get_full_jid_from_iq(msg) + mtype = msg.getType() + thread_id = msg.getThread() + if not mtype: + mtype = 'normal' + + session = self.get_session(frm, thread_id, mtype) + + if thread_id and not session.received_thread_id: + session.received_thread_id = True + # check if the message is pubsub#event if msg.getTag('event') is not None: self._pubsubEventCB(con, msg) @@ -1421,18 +1438,15 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, return if msg.getTag('feature') and msg.getTag('feature').namespace == \ common.xmpp.NS_FEATURE: - self._FeatureNegCB(con, msg) + self._FeatureNegCB(con, msg, session) return msgtxt = msg.getBody() msghtml = msg.getXHTML() - mtype = msg.getType() subject = msg.getSubject() # if not there, it's None - thread = msg.getThread() tim = msg.getTimestamp() tim = time.strptime(tim, '%Y%m%dT%H:%M:%S') tim = time.localtime(timegm(tim)) - frm = helpers.get_full_jid_from_iq(msg) jid = helpers.get_jid_from_iq(msg) no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for') @@ -1541,13 +1555,42 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim, subject = subject) mtype = 'normal' + treat_as = gajim.config.get('treat_incoming_messages') if treat_as: mtype = treat_as self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, - subject, chatstate, msg_id, composing_jep, user_nick, msghtml, thread)) + subject, chatstate, msg_id, composing_jep, user_nick, msghtml, session)) # END messageCB + def get_session(self, jid, thread_id, type): + '''returns an existing session between this connection and 'jid' or starts a new one.''' + try: + if type == 'chat' and not thread_id: + return self.find_null_session(jid) + else: + return self.sessions[jid][thread_id] + except KeyError: + return self.make_new_session(jid, thread_id, type) + + def find_null_session(self, jid): + '''returns the session between this connecting and 'jid' that we last sent a message in.''' + 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) + + return null_sessions[-1] + + def make_new_session(self, jid, thread_id = None, type = 'chat'): + sess = StanzaSession(self, jid, thread_id, type) + + if not jid in self.sessions: + self.sessions[jid] = {} + + self.sessions[jid][sess.thread_id] = sess + + return sess + def _pubsubEventCB(self, con, msg): ''' Called when we receive with pubsub event. ''' # TODO: Logging? (actually services where logging would be useful, should diff --git a/src/common/contacts.py b/src/common/contacts.py index d6352cea6..68db5b766 100644 --- a/src/common/contacts.py +++ b/src/common/contacts.py @@ -16,7 +16,7 @@ import common.gajim -import random, string +#import random, string class Contact: '''Information concerning each contact''' @@ -53,18 +53,18 @@ class Contact: self.last_status_time = last_status_time # XEP-0201 - self.sessions = {} +# self.sessions = {} - def new_session(self): - thread_id = "".join([random.choice(string.letters) for x in xrange(0,32)]) - self.sessions[self.get_full_jid()] = thread_id - return thread_id +# def new_session(self): +# thread_id = "".join([random.choice(string.letters) for x in xrange(0,32)]) +# self.sessions[self.get_full_jid()] = thread_id +# return thread_id - def get_session(self): - try: - return self.sessions[self.get_full_jid()] - except KeyError: - return None +# def get_session(self): +# try: +# return self.sessions[self.get_full_jid()] +# except KeyError: +# return None def get_full_jid(self): if self.resource: @@ -169,7 +169,7 @@ class Contacts: return Contact(jid, name, groups, show, status, sub, ask, resource, priority, keyID, our_chatstate, chatstate, last_status_time, composing_jep) - + def copy_contact(self, contact): return self.create_contact(jid = contact.jid, name = contact.name, groups = contact.groups, show = contact.show, status = contact.status, diff --git a/src/common/helpers.py b/src/common/helpers.py index 12388bf81..60252e6bf 100644 --- a/src/common/helpers.py +++ b/src/common/helpers.py @@ -820,7 +820,7 @@ def allow_sound_notification(sound_event, advanced_notif_num = None): return True return False -def get_chat_control(account, contact): +def get_chat_control(account, contact, session): full_jid_with_resource = contact.jid if contact.resource: full_jid_with_resource += '/' + contact.resource @@ -829,16 +829,16 @@ def get_chat_control(account, contact): # Look for a chat control that has the given resource, or default to # one without resource ctrl = gajim.interface.msg_win_mgr.get_control(full_jid_with_resource, - account) + account, session.thread_id) if ctrl: return ctrl elif not highest_contact or not highest_contact.resource: # unknow contact or offline message - return gajim.interface.msg_win_mgr.get_control(contact.jid, account) + return gajim.interface.msg_win_mgr.get_control(contact.jid, account, session.thread_id) elif highest_contact and contact.resource != \ highest_contact.resource: return None - return gajim.interface.msg_win_mgr.get_control(contact.jid, account) + return gajim.interface.msg_win_mgr.get_control(contact.jid, account, session.thread_id) def reduce_chars_newlines(text, max_chars = 0, max_lines = 0): '''Cut the chars after 'max_chars' on each line diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py new file mode 100644 index 000000000..0e3c3b7ec --- /dev/null +++ b/src/common/stanza_session.py @@ -0,0 +1,51 @@ +import gajim + +from common import xmpp +from common import helpers + +import random +import string + +class StanzaSession: + 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.type = type + + if thread_id: + self.received_thread_id = True + self.thread_id = thread_id + else: + self.received_thread_id = False + if type == 'normal': + self.thread_id = None + else: + self.thread_id = self.generate_thread_id() + + self.last_send = 0 + + def generate_thread_id(self): + return "".join([random.choice(string.letters) for x in xrange(0,32)]) + + def get_control(self, advanced_notif_num = None): + account = self.conn.name + highest_contact = gajim.contacts.get_contact_with_highest_priority(account, str(self.jid)) + contact = gajim.contacts.get_contact(account, self.jid.getStripped(), self.jid.getResource()) + if isinstance(contact, list): + # there was no resource (maybe we're reading unread messages after shutdown). just take the first one for now :/ + contact = contact[0] + + ctrl = gajim.interface.msg_win_mgr.get_control(str(self.jid), account, self.thread_id) +# if not ctrl: +# if highest_contact and contact.resource == highest_contact.resource and not str(self.jid) == gajim.get_jid_from_account(account): +# ctrl = gajim.interface.msg_win_mgr.get_control(self.jid.getStripped(), account, self.thread_id) + + if not ctrl and helpers.allow_popup_window(account, advanced_notif_num): + gajim.new_chat(contact, account, resource = resource_for_chat, session = self) + + return ctrl diff --git a/src/dialogs.py b/src/dialogs.py index 2d0409676..526e6b95b 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -1404,7 +1404,7 @@ class SynchroniseSelectAccountDialog: if not iter: return remote_account = model.get_value(iter, 0).decode('utf-8') - + if gajim.connections[remote_account].connected < 2: ErrorDialog(_('This account is not connected to the server'), _('You cannot synchronize with an account unless it is connected.')) @@ -1686,7 +1686,7 @@ class SingleMessageWindow: or 'receive'. ''' def __init__(self, account, to = '', action = '', from_whom = '', - subject = '', message = '', resource = '', thread = None): + subject = '', message = '', resource = '', session = None): self.account = account self.action = action @@ -1695,7 +1695,7 @@ class SingleMessageWindow: self.to = to self.from_whom = from_whom self.resource = resource - self.thread = thread + self.session = session self.xml = gtkgui_helpers.get_glade('single_message_window.glade') self.window = self.xml.get_widget('single_message_window') @@ -1897,7 +1897,7 @@ class SingleMessageWindow: # FIXME: allow GPG message some day gajim.connections[self.account].send_message(to_whom_jid, message, - keyID = None, type = 'normal', subject=subject, thread = self.thread) + keyID = None, type = 'normal', subject=subject, session = self.session) self.subject_entry.set_text('') # we sent ok, clear the subject self.message_tv_buffer.set_text('') # we sent ok, clear the textview @@ -1914,7 +1914,7 @@ class SingleMessageWindow: self.window.destroy() SingleMessageWindow(self.account, to = self.from_whom, action = 'send', from_whom = self.from_whom, subject = self.subject, - message = self.message, thread = self.thread) + message = self.message, session = self.session) def on_send_and_close_button_clicked(self, widget): self.send_single_message() @@ -2099,7 +2099,7 @@ class PrivacyListWindow: jid_entry_completion.set_text_column(0) jid_entry_completion.set_model(jids_list_store) jid_entry_completion.set_popup_completion(True) - self.edit_type_jabberid_entry.set_completion(jid_entry_completion) + self.edit_type_jabberid_entry.set_completion(jid_entry_completion) if action == 'EDIT': self.refresh_rules() diff --git a/src/gajim.py b/src/gajim.py index 53ab20863..5c13131e1 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -443,9 +443,9 @@ class Interface: (jid_from, file_props)) conn.disconnect_transfer(file_props) return - ctrl = self.msg_win_mgr.get_control(jid_from, account) - if ctrl and ctrl.type_id == message_control.TYPE_GC: - ctrl.print_conversation('Error %s: %s' % (array[2], array[1])) + for ctrl in self.msg_win_mgr.get_controls(jid=jid_from, acct=account): + if ctrl and ctrl.type_id == message_control.TYPE_GC: + ctrl.print_conversation('Error %s: %s' % (array[2], array[1])) def handle_event_con_type(self, account, con_type): # ('CON_TYPE', account, con_type) which can be 'ssl', 'tls', 'tcp' @@ -667,7 +667,7 @@ class Interface: def handle_event_msg(self, account, array): # 'MSG' (account, (jid, msg, time, encrypted, msg_type, subject, - # chatstate, msg_id, composing_jep, user_nick, xhtml, thread)) + # chatstate, msg_id, composing_jep, user_nick, xhtml, session)) # user_nick is JEP-0172 full_jid_with_resource = array[0] @@ -682,13 +682,13 @@ class Interface: msg_id = array[7] composing_jep = array[8] xhtml = array[10] - thread = array[11] + session = array[11] if gajim.config.get('ignore_incoming_xhtml'): xhtml = None if gajim.jid_is_transport(jid): jid = jid.replace('@', '') - groupchat_control = self.msg_win_mgr.get_control(jid, account) + groupchat_control = self.msg_win_mgr.get_control(jid, account, session.thread_id) if not groupchat_control and \ gajim.interface.minimized_controls.has_key(account) and \ jid in gajim.interface.minimized_controls[account]: @@ -700,28 +700,29 @@ class Interface: pm = True msg_type = 'pm' - chat_control = None - jid_of_control = full_jid_with_resource +# chat_control = None +# jid_of_control = full_jid_with_resource highest_contact = gajim.contacts.get_contact_with_highest_priority( account, jid) # Look for a chat control that has the given resource, or default to one # without resource - ctrl = self.msg_win_mgr.get_control(full_jid_with_resource, account) - if ctrl: - chat_control = ctrl - elif not pm and (not highest_contact or not highest_contact.resource): + chat_control = session.get_control() +# ctrl = self.msg_win_mgr.get_control(full_jid_with_resource, account, session.thread_id) +# if ctrl: +# chat_control = ctrl +# elif not pm and (not highest_contact or not highest_contact.resource): # unknow contact or offline message - jid_of_control = jid - chat_control = self.msg_win_mgr.get_control(jid, account) - elif highest_contact and resource != highest_contact.resource and \ - highest_contact.show != 'offline': - jid_of_control = full_jid_with_resource - chat_control = None - elif not pm: - jid_of_control = jid - chat_control = self.msg_win_mgr.get_control(jid, account) +# jid_of_control = jid +# chat_control = self.msg_win_mgr.get_control(jid, account, session.thread_id) +# elif highest_contact and resource != highest_contact.resource and \ +# highest_contact.show != 'offline': +# jid_of_control = full_jid_with_resource +# chat_control = None +# elif not pm: +# jid_of_control = jid +# chat_control = self.msg_win_mgr.get_control(jid, account, session.thread_id) - # Handle chat states + # Handle chat states contact = gajim.contacts.get_contact(account, jid, resource) if contact and isinstance(contact, list): contact = contact[0] @@ -739,7 +740,7 @@ class Interface: # got no valid jep85 answer, peer does not support it contact.chatstate = False elif chatstate == 'active': - # Brand new message, incoming. + # Brand new message, incoming. contact.our_chatstate = chatstate contact.chatstate = chatstate if msg_id: # Do not overwrite an existing msg_id with None @@ -753,10 +754,12 @@ class Interface: if gajim.config.get('ignore_unknown_contacts') and \ not gajim.contacts.get_contact(account, jid) and not pm: return + if not contact: # contact is not in the roster, create a fake one to display # notification - contact = common.contacts.Contact(jid = jid, resource = resource) + contact = common.contacts.Contact(jid = jid, resource = resource) + advanced_notif_num = notify.get_advanced_notification('message_received', account, contact) @@ -765,8 +768,8 @@ class Interface: if msg_type == 'normal': if not gajim.events.get_events(account, jid, ['normal']): first = True - elif not chat_control and not gajim.events.get_events(account, - jid_of_control, [msg_type]): # msg_type can be chat or pm + elif not chat_control and not gajim.events.get_events(account, + full_jid_with_resource, [msg_type]): # msg_type can be chat or pm first = True if pm: @@ -778,18 +781,19 @@ class Interface: if encrypted: self.roster.on_message(jid, message, array[2], account, array[3], msg_type, subject, resource, msg_id, array[9], - advanced_notif_num, thread = thread) + advanced_notif_num, session = session) else: # xhtml in last element self.roster.on_message(jid, message, array[2], account, array[3], msg_type, subject, resource, msg_id, array[9], - advanced_notif_num, xhtml = xhtml, thread = thread) + advanced_notif_num, xhtml = xhtml, session = session) nickname = gajim.get_name_from_jid(account, jid) - # Check and do wanted notifications + + # Check and do wanted notifications msg = message if subject: msg = _('Subject: %s') % subject + '\n' + msg - notify.notify('new_message', jid_of_control, account, [msg_type, + notify.notify('new_message', full_jid_with_resource, account, [msg_type, first, nickname, msg], advanced_notif_num) if self.remote_ctrl: @@ -986,7 +990,7 @@ class Interface: resource = '' if vcard.has_key('resource'): resource = vcard['resource'] - + # vcard window win = None if self.instances[account]['infos'].has_key(jid): @@ -1008,11 +1012,14 @@ class Interface: elif self.msg_win_mgr.has_window(jid, account): win = self.msg_win_mgr.get_window(jid, account) ctrl = win.get_control(jid, account) - if win and ctrl.type_id != message_control.TYPE_GC: - ctrl.show_avatar() + + for ctrl in self.msg_win_mgr.get_controls(jid=jid, acct=account): + if ctrl.type_id != message_control.TYPE_GC: + ctrl.show_avatar() # Show avatar in roster or gc_roster gc_ctrl = self.msg_win_mgr.get_control(jid, account) + # XXX get_gc_control? if gc_ctrl and gc_ctrl.type_id == message_control.TYPE_GC: gc_ctrl.draw_avatar(resource) else: @@ -1656,25 +1663,24 @@ class Interface: AtomWindow.newAtomEntry(atom_entry) def handle_session_negotiation(self, account, data): - jid, thread_id, form = data + jid, session, form = data # XXX check negotiation state, etc. # XXX check if we can autoaccept if form.getType() == 'form': - ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account) - if not ctrl: - resource = jid.getResource() - contact = gajim.contacts.get_contact(account, str(jid), resource) - if not contact: - connection = gajim.connections[account] - contact = gajim.contacts.create_contact(jid = jid.getStripped(), resource = resource, show = connection.get_status()) - self.roster.new_chat(contact, account, resource = resource) + ctrl = session.get_control() +# ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account) +# if not ctrl: +# resource = jid.getResource() +# contact = gajim.contacts.get_contact(account, str(jid), resource) +# if not contact: +# connection = gajim.connections[account] +# contact = gajim.contacts.create_contact(jid = jid.getStripped(), resource = resource, show = connection.get_status()) +# self.roster.new_chat(contact, account, resource = resource) - ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account) +# ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account) - ctrl.set_thread_id(thread_id) - - negotiation.FeatureNegotiationWindow(account, jid, thread_id, form) + negotiation.FeatureNegotiationWindow(account, jid, session, form) def handle_event_privacy_lists_received(self, account, data): # ('PRIVACY_LISTS_RECEIVED', account, list) diff --git a/src/message_control.py b/src/message_control.py index 11b3b6996..c0b409b11 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -37,8 +37,6 @@ class MessageControl: self.hide_chat_buttons_current = False self.resource = resource - self.thread_id = self.contact.get_session() - gajim.last_message_time[self.account][self.get_full_jid()] = 0 self.xml = gtkgui_helpers.get_glade('message_window.glade', widget_name) @@ -112,14 +110,6 @@ class MessageControl: def get_specific_unread(self): return len(gajim.events.get_events(self.account, self.contact.jid)) - def set_thread_id(self, thread_id): - if thread_id == self.thread_id: - return - if self.thread_id: - print "starting a new session, forgetting about the old one!" - self.thread_id = thread_id - self.contact.sessions[self.contact.get_full_jid()] = thread_id - def send_message(self, message, keyID = '', type = 'chat', chatstate = None, msg_id = None, composing_jep = None, resource = None, user_nick = None): @@ -127,11 +117,8 @@ class MessageControl: ''' jid = self.contact.jid - if not self.thread_id: - self.thread_id = self.contact.new_session() - # Send and update history return gajim.connections[self.account].send_message(jid, message, keyID, type = type, chatstate = chatstate, msg_id = msg_id, composing_jep = composing_jep, resource = self.resource, - user_nick = user_nick, thread = self.thread_id) + user_nick = user_nick, session = self.session) diff --git a/src/message_window.py b/src/message_window.py index 6445495be..87e9a204f 100644 --- a/src/message_window.py +++ b/src/message_window.py @@ -123,8 +123,9 @@ class MessageWindow: def get_num_controls(self): n = 0 - for dict in self._controls.values(): - n += len(dict) + for sess_dict in self._controls.values(): + for dict in sess_dict.values(): + n += len(dict) return n def _on_window_focus(self, widget, event): @@ -165,7 +166,9 @@ class MessageWindow: if not self._controls.has_key(control.account): self._controls[control.account] = {} fjid = control.get_full_jid() - self._controls[control.account][fjid] = control + if not self._controls.has_key(fjid): + self._controls[control.account][fjid] = {} + self._controls[control.account][fjid][control.session.thread_id] = control if self.get_num_controls() == 2: # is first conversation_textview scrolled down ? @@ -292,11 +295,11 @@ class MessageWindow: else: gtkgui_helpers.set_unset_urgency_hint(self.window, False) - def set_active_tab(self, jid, acct): - ctrl = self._controls[acct][jid] + def set_active_tab(self, jid, acct, thread_id): + ctrl = self._controls[acct][jid][thread_id] ctrl_page = self.notebook.page_num(ctrl.widget) self.notebook.set_current_page(ctrl_page) - + def remove_tab(self, ctrl, method, reason = None, force = False): '''reason is only for gc (offline status message) if force is True, do not ask any confirmation''' @@ -317,7 +320,9 @@ class MessageWindow: self.notebook.remove_page(self.notebook.page_num(ctrl.widget)) fjid = ctrl.get_full_jid() - del self._controls[ctrl.account][fjid] + del self._controls[ctrl.account][fjid][ctrl.session.thread_id] + if len(self._controls[ctrl.account][fjid]) == 0: + del self._controls[ctrl.account][fjid] if len(self._controls[ctrl.account]) == 0: del self._controls[ctrl.account] @@ -415,7 +420,19 @@ class MessageWindow: for ctrl in self.controls(): ctrl.update_tags() - def get_control(self, key, acct): + def has_control(self, jid, acct, thread_id = None): + try: + if thread_id: + return (thread_id in self._controls[acct][jid]) + else: + return (jid in self._controls[acct]) + except KeyError: + return False + + def get_controls(self, jid, acct): + return self._controls[acct][jid].values() + + def get_control(self, key, acct, thread_id): '''Return the MessageControl for jid or n, where n is a notebook page index. When key is an int index acct may be None''' if isinstance(key, str): @@ -424,7 +441,7 @@ class MessageWindow: if isinstance(key, unicode): jid = key try: - return self._controls[acct][jid] + return self._controls[acct][jid][thread_id] except: return None else: @@ -436,9 +453,10 @@ class MessageWindow: return self._widget_to_control(nth_child) def controls(self): - for ctrl_dict in self._controls.values(): - for ctrl in ctrl_dict.values(): - yield ctrl + for jid_dict in self._controls.values(): + for sess_dict in jid_dict.values(): + for ctrl in sess_dict.values(): + yield ctrl def move_to_next_unread_tab(self, forward): ind = self.notebook.get_current_page() @@ -495,7 +513,7 @@ class MessageWindow: if old_no >= 0: old_ctrl = self._widget_to_control(notebook.get_nth_page(old_no)) old_ctrl.set_control_active(False) - + new_ctrl = self._widget_to_control(notebook.get_nth_page(page_num)) new_ctrl.set_control_active(True) self.show_title(control = new_ctrl) @@ -569,11 +587,11 @@ class MessageWindow: source_child = self.notebook.get_nth_page(source_page_num) if dest_page_num != source_page_num: self.notebook.reorder_child(source_child, dest_page_num) - + def get_tab_at_xy(self, x, y): '''Thanks to Gaim Return the tab under xy and - if its nearer from left or right side of the tab + if its nearer from left or right side of the tab ''' page_num = -1 to_right = False @@ -594,7 +612,7 @@ class MessageWindow: if (y >= tab_alloc.y) and \ (y <= (tab_alloc.y + tab_alloc.height)): page_num = i - + if y > tab_alloc.y + (tab_alloc.height / 2.0): to_right = True break @@ -659,14 +677,22 @@ class MessageWindowMgr: return w return None - def get_window(self, jid, acct): + def get_window(self, jid, acct, thread_id): for win in self.windows(): - if win.get_control(jid, acct): + if win.has_control(jid, acct, thread_id): return win return None - def has_window(self, jid, acct): - return self.get_window(jid, acct) != None + def get_windows(self, jid, acct): + for win in self.windows(): + if win.has_control(jid, acct): + yield win + + def has_window(self, jid, acct, thread_id = None): + for win in self.windows(): + if win.has_control(jid, acct, thread_id): + return True + return False def one_window_opened(self, contact, acct, type): try: @@ -678,7 +704,7 @@ class MessageWindowMgr: '''Resizes window according to config settings''' if not gajim.config.get('saveposition'): return - + if self.mode == self.ONE_MSG_WINDOW_ALWAYS: size = (gajim.config.get('msgwin-width'), gajim.config.get('msgwin-height')) @@ -695,7 +721,7 @@ class MessageWindowMgr: return gtkgui_helpers.resize_window(win.window, size[0], size[1]) - + def _position_window(self, win, acct, type): '''Moves window according to config settings''' if not gajim.config.get('saveposition') or\ @@ -773,18 +799,20 @@ class MessageWindowMgr: del self._windows[k] return - def get_control(self, jid, acct): + def get_control(self, jid, acct, thread_id): '''Amongst all windows, return the MessageControl for jid''' - win = self.get_window(jid, acct) + win = self.get_window(jid, acct, thread_id) if win: - return win.get_control(jid, acct) + return win.get_control(jid, acct, thread_id) return None - def get_controls(self, type = None, acct = None): + def get_controls(self, type = None, acct = None, jid = None): ctrls = [] for c in self.controls(): if acct and c.account != acct: continue + if jid and c.get_full_jid() != jid: + continue if not type or c.type_id == type: ctrls.append(c) return ctrls @@ -808,7 +836,7 @@ class MessageWindowMgr: def save_state(self, msg_win): if not gajim.config.get('saveposition'): return - + # Save window size and position pos_x_key = 'msgwin-x-position' pos_y_key = 'msgwin-y-position' @@ -843,11 +871,11 @@ class MessageWindowMgr: if self.mode != self.ONE_MSG_WINDOW_NEVER: gajim.config.set_per('accounts', acct, pos_x_key, x) gajim.config.set_per('accounts', acct, pos_y_key, y) - + else: gajim.config.set(size_width_key, width) gajim.config.set(size_height_key, height) - + if self.mode != self.ONE_MSG_WINDOW_NEVER: gajim.config.set(pos_x_key, x) gajim.config.set(pos_y_key, y) diff --git a/src/negotiation.py b/src/negotiation.py index 32fbf0a3a..095fd5178 100644 --- a/src/negotiation.py +++ b/src/negotiation.py @@ -7,11 +7,11 @@ from common import xmpp class FeatureNegotiationWindow: '''FeatureNegotiotionWindow class''' - def __init__(self, account, jid, thread_id, form): + def __init__(self, account, jid, session, form): self.account = account self.jid = jid self.form = form - self.thread_id = thread_id + self.session = session self.xml = gtkgui_helpers.get_glade('data_form_window.glade', 'data_form_window') self.window = self.xml.get_widget('data_form_window') @@ -27,7 +27,7 @@ class FeatureNegotiationWindow: def on_ok_button_clicked(self, widget): acceptance = xmpp.Message(self.jid) - acceptance.setThread(self.thread_id) + acceptance.setThread(self.session.thread_id) feature = acceptance.NT.feature feature.setNamespace(xmpp.NS_FEATURE) @@ -44,7 +44,7 @@ class FeatureNegotiationWindow: # XXX determine whether to reveal presence rejection = xmpp.Message(self.jid) - rejection.setThread(self.thread_id) + rejection.setThread(self.session.thread_id) feature = rejection.NT.feature feature.setNamespace(xmpp.NS_FEATURE) diff --git a/src/notify.py b/src/notify.py index 66b130791..8dda70f69 100644 --- a/src/notify.py +++ b/src/notify.py @@ -40,7 +40,7 @@ try: except ImportError: USER_HAS_PYNOTIFY = False -def get_show_in_roster(event, account, contact): +def get_show_in_roster(event, account, contact, session): '''Return True if this event must be shown in roster, else False''' if event == 'gc_message_received': return True @@ -51,7 +51,7 @@ def get_show_in_roster(event, account, contact): if gajim.config.get_per('notifications', str(num), 'roster') == 'no': return False if event == 'message_received': - chat_control = helpers.get_chat_control(account, contact) + chat_control = helpers.get_chat_control(account, contact, session) if chat_control: return False return True diff --git a/src/roster_window.py b/src/roster_window.py index 4f0019aa5..139facee8 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1190,10 +1190,14 @@ class RosterWindow: '''reads from db the unread messages, and fire them up''' for jid in gajim.contacts.get_jid_list(account): results = gajim.logger.get_unread_msgs_for_jid(jid) + + # XXX results should contain sessions anyways + session = gajim.connections[account].make_new_session(jid) + for result in results: tim = time.localtime(float(result[2])) self.on_message(jid, result[1], tim, account, msg_type = 'chat', - msg_id = result[0]) + msg_id = result[0], session = session) def fill_contacts_and_groups_dicts(self, array, account): '''fill gajim.contacts and gajim.groups''' @@ -1255,8 +1259,8 @@ class RosterWindow: gajim.transport_avatar[account][host].append(contact1.jid) # If we already have a chat window opened, update it with new contact # instance - chat_control = gajim.interface.msg_win_mgr.get_control(ji, account) - if chat_control: + chat_controls = gajim.interface.msg_win_mgr.get_controls(jid=ji, acct=account) + for chat_control in chat_controls: chat_control.contact = contact1 def chg_contact_status(self, contact, show, status, account): @@ -1283,14 +1287,14 @@ class RosterWindow: jid_list = [contact.jid] for jid in jid_list: if gajim.interface.msg_win_mgr.has_window(jid, account): - win = gajim.interface.msg_win_mgr.get_window(jid, account) - ctrl = win.get_control(jid, account) - ctrl.contact = gajim.contacts.get_contact_with_highest_priority( - account, contact.jid) - ctrl.update_ui() - win.redraw_tab(ctrl) + for win in gajim.interface.msg_win_mgr.get_windows(jid, account): + for ctrl in win.get_controls(jid=jid, acct=account): + ctrl.contact = gajim.contacts.get_contact_with_highest_priority( + account, contact.jid) + ctrl.update_ui() + win.redraw_tab(ctrl) - name = contact.get_shown_name() + name = contact.get_shown_name() # if multiple resources (or second one disconnecting) if (len(contact_instances) > 1 or (len(contact_instances) == 1 and \ @@ -3477,18 +3481,22 @@ class RosterWindow: # We call this here to avoid race conditions with widget validation chat_control.read_queue() - def new_chat(self, contact, account, resource = None): + def new_chat(self, contact, account, resource = None, session = None): # Get target window, create a control, and associate it with the window type_ = message_control.TYPE_CHAT fjid = contact.jid if resource: fjid += '/' + resource - mw = gajim.interface.msg_win_mgr.get_window(fjid, account) + + if not session: + session = gajim.connections[account].make_new_session(fjid) + + mw = gajim.interface.msg_win_mgr.get_window(fjid, account, session.thread_id) if not mw: mw = gajim.interface.msg_win_mgr.create_window(contact, account, type_) - chat_control = ChatControl(mw, contact, account, resource) + chat_control = ChatControl(mw, contact, account, session, resource) mw.new_tab(chat_control) @@ -3496,6 +3504,8 @@ class RosterWindow: # We call this here to avoid race conditions with widget validation chat_control.read_queue() + return session + def new_chat_from_jid(self, account, fjid): jid, resource = gajim.get_room_and_nick_from_fjid(fjid) if resource: @@ -3531,7 +3541,7 @@ class RosterWindow: def on_message(self, jid, msg, tim, account, encrypted = False, msg_type = '', subject = None, resource = '', msg_id = None, - user_nick = '', advanced_notif_num = None, xhtml = None, thread = None): + user_nick = '', advanced_notif_num = None, xhtml = None, session = None): '''when we receive a message''' contact = None # if chat window will be for specific resource @@ -3569,16 +3579,18 @@ class RosterWindow: path = self.get_path(jid, account) # Try to get line of contact in roster + ctrl = session.get_control(advanced_notif_num) + # Look for a chat control that has the given resource - ctrl = gajim.interface.msg_win_mgr.get_control(fjid, account) - if not ctrl: +# ctrl = gajim.interface.msg_win_mgr.get_control(fjid, account) +# if not ctrl: # if not, if message comes from highest prio, get control or open one # without resource - if highest_contact and contact.resource == highest_contact.resource \ - and not jid == gajim.get_jid_from_account(account): - ctrl = gajim.interface.msg_win_mgr.get_control(jid, account) - fjid = jid - resource_for_chat = None +# if highest_contact and contact.resource == highest_contact.resource \ +# and not jid == gajim.get_jid_from_account(account): +# ctrl = gajim.interface.msg_win_mgr.get_control(jid, account) +# fjid = jid +# resource_for_chat = None # Do we have a queue? no_queue = len(gajim.events.get_events(account, fjid)) == 0 @@ -3588,7 +3600,7 @@ class RosterWindow: if msg_type == 'normal' and popup: # it's single message to be autopopuped dialogs.SingleMessageWindow(account, contact.jid, action = 'receive', from_whom = jid, subject = subject, - message = msg, resource = resource, thread = thread) + message = msg, resource = resource, session = session) return # We print if window is opened and it's not a single message @@ -3596,8 +3608,6 @@ class RosterWindow: typ = '' if msg_type == 'error': typ = 'status' - if thread: - ctrl.set_thread_id(thread) ctrl.print_conversation(msg, typ, tim = tim, encrypted = encrypted, subject = subject, xhtml = xhtml) if msg_id: @@ -3610,26 +3620,26 @@ class RosterWindow: if msg_type == 'normal': type_ = 'normal' event_type = 'single_message_received' - show_in_roster = notify.get_show_in_roster(event_type, account, contact) + show_in_roster = notify.get_show_in_roster(event_type, account, contact, session) show_in_systray = notify.get_show_in_systray(event_type, account, contact) event = gajim.events.create_event(type_, (msg, subject, msg_type, tim, - encrypted, resource, msg_id, xhtml, thread), show_in_roster = show_in_roster, + encrypted, resource, msg_id, xhtml, session), show_in_roster = show_in_roster, show_in_systray = show_in_systray) gajim.events.add_event(account, fjid, event) - if popup: - if not ctrl: - self.new_chat(contact, account, resource = resource_for_chat) - if path and not self.dragging and gajim.config.get( - 'scroll_roster_to_last_message'): - # we curently see contact in our roster OR he - # is not in the roster at all. +# if popup: +# if not ctrl: +# self.new_chat(contact, account, resource = resource_for_chat) +# if path and not self.dragging and gajim.config.get( +# 'scroll_roster_to_last_message'): +# # we curently see contact in our roster OR he +# # is not in the roster at all. # show and select his line in roster # do not change selection while DND'ing - self.tree.expand_row(path[0:1], False) - self.tree.expand_row(path[0:2], False) - self.tree.scroll_to_cell(path) - self.tree.set_cursor(path) - else: +# self.tree.expand_row(path[0:1], False) +# self.tree.expand_row(path[0:2], False) +# self.tree.scroll_to_cell(path) +# self.tree.set_cursor(path) + if not popup: if no_queue: # We didn't have a queue: we change icons self.draw_contact(jid, account) self.show_title() # we show the * or [n] @@ -3875,7 +3885,7 @@ class RosterWindow: if event.type_ == 'normal': dialogs.SingleMessageWindow(account, jid, action = 'receive', from_whom = jid, subject = data[1], - message = data[0], resource = data[5], thread = data[8]) + message = data[0], resource = data[5], session = data[8]) gajim.interface.remove_first_event(account, jid, event.type_) return True elif event.type_ == 'file-request': @@ -3912,22 +3922,22 @@ class RosterWindow: jid = jid + u'/' + resource adhoc_commands.CommandWindow(account, jid) - def on_open_chat_window(self, widget, contact, account, resource = None): + def on_open_chat_window(self, widget, contact, account, resource = None, session = None): # Get the window containing the chat fjid = contact.jid if resource: fjid += '/' + resource - win = gajim.interface.msg_win_mgr.get_window(fjid, account) - if not win: - self.new_chat(contact, account, resource = resource) - win = gajim.interface.msg_win_mgr.get_window(fjid, account) - ctrl = win.get_control(fjid, account) - # last message is long time ago - gajim.last_message_time[account][ctrl.get_full_jid()] = 0 - win.set_active_tab(fjid, account) + + session = self.new_chat(contact, account, resource=resource, session=session) + win = gajim.interface.msg_win_mgr.get_window(fjid, account, session.thread_id) + ctrl = win.get_control(fjid, account, session.thread_id) + # last message is long time ago + gajim.last_message_time[account][ctrl.get_full_jid()] = 0 + + win.set_active_tab(fjid, account, session.thread_id) if gajim.connections[account].is_zeroconf and \ gajim.connections[account].status in ('offline', 'invisible'): - win.get_control(fjid, account).got_disconnected() + win.get_control(fjid, account, session.thread_id).got_disconnected() win.window.present() @@ -3967,7 +3977,10 @@ class RosterWindow: jid = child_jid else: child_iter = model.iter_next(child_iter) + + session = None if first_ev: + session = first_ev.parameters[8] fjid = jid if resource: fjid += '/' + resource @@ -3978,7 +3991,7 @@ class RosterWindow: c = gajim.contacts.get_contact_with_highest_priority(account, jid) if jid == gajim.get_jid_from_account(account): resource = c.resource - self.on_open_chat_window(widget, c, account, resource = resource) + self.on_open_chat_window(widget, c, account, resource = resource, session=session) def on_roster_treeview_row_activated(self, widget, path, col = 0): '''When an iter is double clicked: open the first event window''' From 370818d982879a9cf82991e93b9a753a5e61af1f Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Tue, 5 Jun 2007 22:17:34 +0000 Subject: [PATCH 09/40] updated session handlers --- src/common/connection_handlers.py | 61 +++++++++++++++++++------------ src/common/stanza_session.py | 18 --------- 2 files changed, 38 insertions(+), 41 deletions(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 346497f5c..ab7cb8042 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -37,8 +37,6 @@ from common import atom from common.commands import ConnectionCommands from common.pubsub import ConnectionPubSub -from common.stanza_session import StanzaSession - STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'error'] # kind of events we can wait for an answer @@ -1173,10 +1171,6 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, # keep the latest subscribed event for each jid to prevent loop when we # acknoledge presences self.subscribed_events = {} - - # keep track of sessions this connection has with other JIDs - self.sessions = {} - try: idle.init() except: @@ -1203,13 +1197,13 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, self.dispatch('HTTP_AUTH', (method, url, id, iq_obj, msg)); raise common.xmpp.NodeProcessed - def _FeatureNegCB(self, con, stanza, session): + def _FeatureNegCB(self, con, stanza): gajim.log.debug('FeatureNegCB') feature = stanza.getTag('feature') form = common.xmpp.DataForm(node=feature.getTag('x')) if form['FORM_TYPE'] == 'urn:xmpp:ssn': - self.dispatch('SESSION_NEG', (stanza.getFrom(), session, form)) + self.dispatch('SESSION_NEG', (stanza.getFrom(), stanza.getThread(), form)) else: reply = stanza.buildReply() reply.setType('error') @@ -1416,17 +1410,6 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, def _messageCB(self, con, msg): '''Called when we receive a message''' - frm = helpers.get_full_jid_from_iq(msg) - mtype = msg.getType() - thread_id = msg.getThread() - if not mtype: - mtype = 'normal' - - session = self.get_session(frm, thread_id, mtype) - - if thread_id and not session.received_thread_id: - session.received_thread_id = True - # check if the message is pubsub#event if msg.getTag('event') is not None: self._pubsubEventCB(con, msg) @@ -1438,15 +1421,18 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, return if msg.getTag('feature') and msg.getTag('feature').namespace == \ common.xmpp.NS_FEATURE: - self._FeatureNegCB(con, msg, session) + self._FeatureNegCB(con, msg) return msgtxt = msg.getBody() msghtml = msg.getXHTML() + mtype = msg.getType() subject = msg.getSubject() # if not there, it's None + thread = msg.getThread() tim = msg.getTimestamp() tim = time.strptime(tim, '%Y%m%dT%H:%M:%S') tim = time.localtime(timegm(tim)) + frm = helpers.get_full_jid_from_iq(msg) jid = helpers.get_jid_from_iq(msg) no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for') @@ -1555,23 +1541,52 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim, subject = subject) mtype = 'normal' - treat_as = gajim.config.get('treat_incoming_messages') if treat_as: mtype = treat_as self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, - subject, chatstate, msg_id, composing_jep, user_nick, msghtml, session)) + subject, chatstate, msg_id, composing_jep, user_nick, msghtml, thread)) # END messageCB def get_session(self, jid, thread_id, type): '''returns an existing session between this connection and 'jid' or starts a new one.''' + session = self.find_session(jid, thread_id, type) + + if session: + return session + else: + # it's possible we initiated a session with a bare JID and this is the + # first time we've seen a resource + bare_jid = gajim.get_jid_without_resource(original_jid) + if bare_jid != jid: + session = self.find_session(bare_jid, thread_id, type) + if session: + self.move_session(bare_jid, thread_id, jid.split("/")[1]) + return session + + return self.make_new_session(jid, thread_id, type) + + def find_session(self, jid, thread_id, type): try: if type == 'chat' and not thread_id: return self.find_null_session(jid) else: return self.sessions[jid][thread_id] except KeyError: - return self.make_new_session(jid, thread_id, type) + return None + + def move_session(self, original_jid, thread_id, to_resource): + session = self.sessions[jid][thread_id] + + del self.sessions[jid][thread_id] + + new_jid = gajim.get_jid_without_resource(original_jid) + '/' + to_resource + session.jid = new_jid + + if not new_jid in self.sessions: + self.sessions[new_jid] = {} + + 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.''' diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 0e3c3b7ec..b150e5ee1 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -31,21 +31,3 @@ class StanzaSession: def generate_thread_id(self): return "".join([random.choice(string.letters) for x in xrange(0,32)]) - - def get_control(self, advanced_notif_num = None): - account = self.conn.name - highest_contact = gajim.contacts.get_contact_with_highest_priority(account, str(self.jid)) - contact = gajim.contacts.get_contact(account, self.jid.getStripped(), self.jid.getResource()) - if isinstance(contact, list): - # there was no resource (maybe we're reading unread messages after shutdown). just take the first one for now :/ - contact = contact[0] - - ctrl = gajim.interface.msg_win_mgr.get_control(str(self.jid), account, self.thread_id) -# if not ctrl: -# if highest_contact and contact.resource == highest_contact.resource and not str(self.jid) == gajim.get_jid_from_account(account): -# ctrl = gajim.interface.msg_win_mgr.get_control(self.jid.getStripped(), account, self.thread_id) - - if not ctrl and helpers.allow_popup_window(account, advanced_notif_num): - gajim.new_chat(contact, account, resource = resource_for_chat, session = self) - - return ctrl From 7898686680b20151ef6511ba17f48fd827a7ae69 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Tue, 5 Jun 2007 23:19:34 +0000 Subject: [PATCH 10/40] reverted back to per-JID windows, pass around sessions instead of thread_ids --- src/chat_control.py | 13 +++-- src/common/connection.py | 7 ++- src/common/connection_handlers.py | 42 ++++++++++----- src/common/contacts.py | 18 +------ src/common/helpers.py | 8 +-- src/dialogs.py | 4 +- src/gajim.py | 86 ++++++++++++++--------------- src/message_control.py | 7 +++ src/message_window.py | 86 ++++++++++------------------- src/notify.py | 4 +- src/roster_window.py | 90 +++++++++++++++---------------- 11 files changed, 167 insertions(+), 198 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 7d80a410e..a9a816c1c 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -577,7 +577,7 @@ class ChatControlBase(MessageControl): type_ = 'printed_' + self.type_id event = 'message_received' show_in_roster = notify.get_show_in_roster(event, - self.account, self.contact, self.session) + self.account, self.contact) show_in_systray = notify.get_show_in_systray(event, self.account, self.contact) if gc_message: @@ -606,7 +606,7 @@ class ChatControlBase(MessageControl): not self.parent_win.is_active() or not end) and \ kind in ('incoming', 'incoming_queue'): self.parent_win.redraw_tab(self) - ctrl = gajim.interface.msg_win_mgr.get_control(full_jid, self.account, self.session.thread_id) + ctrl = gajim.interface.msg_win_mgr.get_control(full_jid, self.account) if not self.parent_win.is_active(): self.parent_win.show_title(True, ctrl) # Enabled Urgent hint else: @@ -904,7 +904,7 @@ class ChatControl(ChatControlBase): TYPE_ID = message_control.TYPE_CHAT old_msg_kind = None # last kind of the printed message CHAT_CMDS = ['clear', 'compact', 'help', 'ping'] - + def __init__(self, parent_win, contact, acct, session, resource = None): ChatControlBase.__init__(self, self.TYPE_ID, parent_win, 'chat_child_vbox', contact, acct, resource) @@ -935,7 +935,7 @@ class ChatControl(ChatControlBase): # it is on enter-notify and leave-notify so no need to be per jid self.show_bigger_avatar_timeout_id = None self.bigger_avatar_window = None - self.show_avatar(self.contact.resource) + self.show_avatar(self.contact.resource) # chatstate timers and state self.reset_kbd_mouse_timeout_vars() @@ -1799,7 +1799,7 @@ class ChatControl(ChatControlBase): # Is it a pm ? is_pm = False room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) - control = gajim.interface.msg_win_mgr.get_control(room_jid, self.account, self.session.thread_id) + control = gajim.interface.msg_win_mgr.get_control(room_jid, self.account) if control and control.type_id == message_control.TYPE_GC: is_pm = True # list of message ids which should be marked as read @@ -1817,6 +1817,9 @@ class ChatControl(ChatControlBase): encrypted = data[4], subject = data[1], xhtml = data[7]) if len(data) > 6 and isinstance(data[6], int): message_ids.append(data[6]) + + if len(data) > 8: + self.set_session(data[8]) if message_ids: gajim.logger.set_read_messages(message_ids) gajim.events.remove_events(self.account, jid_with_resource, diff --git a/src/common/connection.py b/src/common/connection.py index 6deee84a6..047e19d67 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -21,6 +21,8 @@ import os import random import socket +import time + try: randomsource = random.SystemRandom() except: @@ -48,8 +50,6 @@ log = logging.getLogger('gajim.c.connection') import gtkgui_helpers -import time - class Connection(ConnectionHandlers): '''Connection class''' def __init__(self, name): @@ -888,8 +888,7 @@ class Connection(ConnectionHandlers): # XEP-0201 if session: session.last_send = time.time() - if session.thread_id: - msg_iq.setThread(session.thread_id) + msg_iq.setThread(session.thread_id) # JEP-0172: user_nickname if user_nick: diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index ab7cb8042..f4f6306ff 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -37,6 +37,8 @@ from common import atom from common.commands import ConnectionCommands from common.pubsub import ConnectionPubSub +from common.stanza_session import StanzaSession + STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'error'] # kind of events we can wait for an answer @@ -55,7 +57,7 @@ except: class ConnectionBytestream: def __init__(self): self.files_props = {} - + def is_transfer_stoped(self, file_props): if file_props.has_key('error') and file_props['error'] != 0: return True @@ -1168,14 +1170,17 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, # keep the jids we auto added (transports contacts) to not send the # SUBSCRIBED event to gui self.automatically_added = [] - # keep the latest subscribed event for each jid to prevent loop when we + # keep the latest subscribed event for each jid to prevent loop when we # acknoledge presences self.subscribed_events = {} + + # keep track of sessions this connection has with other JIDs + self.sessions = {} try: idle.init() except: HAS_IDLE = False - + def build_http_auth_answer(self, iq_obj, answer): if answer == 'yes': self.connection.send(iq_obj.buildReply('result')) @@ -1197,13 +1202,13 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, self.dispatch('HTTP_AUTH', (method, url, id, iq_obj, msg)); raise common.xmpp.NodeProcessed - def _FeatureNegCB(self, con, stanza): + def _FeatureNegCB(self, con, stanza, session): gajim.log.debug('FeatureNegCB') feature = stanza.getTag('feature') form = common.xmpp.DataForm(node=feature.getTag('x')) if form['FORM_TYPE'] == 'urn:xmpp:ssn': - self.dispatch('SESSION_NEG', (stanza.getFrom(), stanza.getThread(), form)) + self.dispatch('SESSION_NEG', (stanza.getFrom(), session, form)) else: reply = stanza.buildReply() reply.setType('error') @@ -1410,6 +1415,18 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, def _messageCB(self, con, msg): '''Called when we receive a message''' + frm = helpers.get_full_jid_from_iq(msg) + mtype = msg.getType() + thread_id = msg.getThread() + + if not mtype: + mtype = 'normal' + + session = self.get_session(frm, thread_id, mtype) + + if thread_id and not session.received_thread_id: + session.received_thread_id = True + # check if the message is pubsub#event if msg.getTag('event') is not None: self._pubsubEventCB(con, msg) @@ -1421,18 +1438,15 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, return if msg.getTag('feature') and msg.getTag('feature').namespace == \ common.xmpp.NS_FEATURE: - self._FeatureNegCB(con, msg) + self._FeatureNegCB(con, msg, session) return msgtxt = msg.getBody() msghtml = msg.getXHTML() - mtype = msg.getType() subject = msg.getSubject() # if not there, it's None - thread = msg.getThread() tim = msg.getTimestamp() tim = time.strptime(tim, '%Y%m%dT%H:%M:%S') tim = time.localtime(timegm(tim)) - frm = helpers.get_full_jid_from_iq(msg) jid = helpers.get_jid_from_iq(msg) no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for') @@ -1486,7 +1500,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if encTag and GnuPG.USE_GPG: #decrypt encmsg = encTag.getData() - + keyID = gajim.config.get_per('accounts', self.name, 'keyid') if keyID: decmsg = self.gpg.decrypt(encmsg, keyID) @@ -1545,7 +1559,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if treat_as: mtype = treat_as self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, - subject, chatstate, msg_id, composing_jep, user_nick, msghtml, thread)) + subject, chatstate, msg_id, composing_jep, user_nick, msghtml, session)) # END messageCB def get_session(self, jid, thread_id, type): @@ -1557,7 +1571,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, else: # it's possible we initiated a session with a bare JID and this is the # first time we've seen a resource - bare_jid = gajim.get_jid_without_resource(original_jid) + bare_jid = gajim.get_jid_without_resource(jid) if bare_jid != jid: session = self.find_session(bare_jid, thread_id, type) if session: @@ -1576,9 +1590,9 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, return None def move_session(self, original_jid, thread_id, to_resource): - session = self.sessions[jid][thread_id] + session = self.sessions[original_jid][thread_id] - del self.sessions[jid][thread_id] + del self.sessions[original_jid][thread_id] new_jid = gajim.get_jid_without_resource(original_jid) + '/' + to_resource session.jid = new_jid diff --git a/src/common/contacts.py b/src/common/contacts.py index 68db5b766..b309981d4 100644 --- a/src/common/contacts.py +++ b/src/common/contacts.py @@ -16,8 +16,6 @@ import common.gajim -#import random, string - class Contact: '''Information concerning each contact''' def __init__(self, jid='', name='', groups=[], show='', status='', sub='', @@ -52,20 +50,6 @@ class Contact: self.chatstate = chatstate self.last_status_time = last_status_time - # XEP-0201 -# self.sessions = {} - -# def new_session(self): -# thread_id = "".join([random.choice(string.letters) for x in xrange(0,32)]) -# self.sessions[self.get_full_jid()] = thread_id -# return thread_id - -# def get_session(self): -# try: -# return self.sessions[self.get_full_jid()] -# except KeyError: -# return None - def get_full_jid(self): if self.resource: return self.jid + '/' + self.resource @@ -169,7 +153,7 @@ class Contacts: return Contact(jid, name, groups, show, status, sub, ask, resource, priority, keyID, our_chatstate, chatstate, last_status_time, composing_jep) - + def copy_contact(self, contact): return self.create_contact(jid = contact.jid, name = contact.name, groups = contact.groups, show = contact.show, status = contact.status, diff --git a/src/common/helpers.py b/src/common/helpers.py index 60252e6bf..12388bf81 100644 --- a/src/common/helpers.py +++ b/src/common/helpers.py @@ -820,7 +820,7 @@ def allow_sound_notification(sound_event, advanced_notif_num = None): return True return False -def get_chat_control(account, contact, session): +def get_chat_control(account, contact): full_jid_with_resource = contact.jid if contact.resource: full_jid_with_resource += '/' + contact.resource @@ -829,16 +829,16 @@ def get_chat_control(account, contact, session): # Look for a chat control that has the given resource, or default to # one without resource ctrl = gajim.interface.msg_win_mgr.get_control(full_jid_with_resource, - account, session.thread_id) + account) if ctrl: return ctrl elif not highest_contact or not highest_contact.resource: # unknow contact or offline message - return gajim.interface.msg_win_mgr.get_control(contact.jid, account, session.thread_id) + return gajim.interface.msg_win_mgr.get_control(contact.jid, account) elif highest_contact and contact.resource != \ highest_contact.resource: return None - return gajim.interface.msg_win_mgr.get_control(contact.jid, account, session.thread_id) + return gajim.interface.msg_win_mgr.get_control(contact.jid, account) def reduce_chars_newlines(text, max_chars = 0, max_lines = 0): '''Cut the chars after 'max_chars' on each line diff --git a/src/dialogs.py b/src/dialogs.py index 526e6b95b..e76ac7dda 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -196,7 +196,7 @@ class EditGroupsDialog: for group in groups: if group not in helpers.special_groups or groups[group] > 0: group_list.append(group) - group_list.sort() + group_list.sort() for group in group_list: iter = store.append() store.set(iter, 0, group) # Group name @@ -2099,7 +2099,7 @@ class PrivacyListWindow: jid_entry_completion.set_text_column(0) jid_entry_completion.set_model(jids_list_store) jid_entry_completion.set_popup_completion(True) - self.edit_type_jabberid_entry.set_completion(jid_entry_completion) + self.edit_type_jabberid_entry.set_completion(jid_entry_completion) if action == 'EDIT': self.refresh_rules() diff --git a/src/gajim.py b/src/gajim.py index 5c13131e1..5309be946 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -443,9 +443,9 @@ class Interface: (jid_from, file_props)) conn.disconnect_transfer(file_props) return - for ctrl in self.msg_win_mgr.get_controls(jid=jid_from, acct=account): - if ctrl and ctrl.type_id == message_control.TYPE_GC: - ctrl.print_conversation('Error %s: %s' % (array[2], array[1])) + ctrl = self.msg_win_mgr.get_control(jid_from, account) + if ctrl and ctrl.type_id == message_control.TYPE_GC: + ctrl.print_conversation('Error %s: %s' % (array[2], array[1])) def handle_event_con_type(self, account, con_type): # ('CON_TYPE', account, con_type) which can be 'ssl', 'tls', 'tcp' @@ -663,7 +663,7 @@ class Interface: # It's maybe a GC_NOTIFY (specialy for MSN gc) self.handle_event_gc_notify(account, (jid, array[1], status_message, array[3], None, None, None, None, None, None, None, None)) - + def handle_event_msg(self, account, array): # 'MSG' (account, (jid, msg, time, encrypted, msg_type, subject, @@ -688,7 +688,7 @@ class Interface: if gajim.jid_is_transport(jid): jid = jid.replace('@', '') - groupchat_control = self.msg_win_mgr.get_control(jid, account, session.thread_id) + groupchat_control = self.msg_win_mgr.get_control(jid, account) if not groupchat_control and \ gajim.interface.minimized_controls.has_key(account) and \ jid in gajim.interface.minimized_controls[account]: @@ -700,29 +700,28 @@ class Interface: pm = True msg_type = 'pm' -# chat_control = None -# jid_of_control = full_jid_with_resource + chat_control = None + jid_of_control = full_jid_with_resource highest_contact = gajim.contacts.get_contact_with_highest_priority( account, jid) # Look for a chat control that has the given resource, or default to one # without resource - chat_control = session.get_control() -# ctrl = self.msg_win_mgr.get_control(full_jid_with_resource, account, session.thread_id) -# if ctrl: -# chat_control = ctrl -# elif not pm and (not highest_contact or not highest_contact.resource): + ctrl = self.msg_win_mgr.get_control(full_jid_with_resource, account) + if ctrl: + chat_control = ctrl + elif not pm and (not highest_contact or not highest_contact.resource): # unknow contact or offline message -# jid_of_control = jid -# chat_control = self.msg_win_mgr.get_control(jid, account, session.thread_id) -# elif highest_contact and resource != highest_contact.resource and \ -# highest_contact.show != 'offline': -# jid_of_control = full_jid_with_resource -# chat_control = None -# elif not pm: -# jid_of_control = jid -# chat_control = self.msg_win_mgr.get_control(jid, account, session.thread_id) + jid_of_control = jid + chat_control = self.msg_win_mgr.get_control(jid, account) + elif highest_contact and resource != highest_contact.resource and \ + highest_contact.show != 'offline': + jid_of_control = full_jid_with_resource + chat_control = None + elif not pm: + jid_of_control = jid + chat_control = self.msg_win_mgr.get_control(jid, account) - # Handle chat states + # Handle chat states contact = gajim.contacts.get_contact(account, jid, resource) if contact and isinstance(contact, list): contact = contact[0] @@ -740,7 +739,7 @@ class Interface: # got no valid jep85 answer, peer does not support it contact.chatstate = False elif chatstate == 'active': - # Brand new message, incoming. + # Brand new message, incoming. contact.our_chatstate = chatstate contact.chatstate = chatstate if msg_id: # Do not overwrite an existing msg_id with None @@ -754,12 +753,10 @@ class Interface: if gajim.config.get('ignore_unknown_contacts') and \ not gajim.contacts.get_contact(account, jid) and not pm: return - if not contact: # contact is not in the roster, create a fake one to display # notification - contact = common.contacts.Contact(jid = jid, resource = resource) - + contact = common.contacts.Contact(jid = jid, resource = resource) advanced_notif_num = notify.get_advanced_notification('message_received', account, contact) @@ -768,8 +765,8 @@ class Interface: if msg_type == 'normal': if not gajim.events.get_events(account, jid, ['normal']): first = True - elif not chat_control and not gajim.events.get_events(account, - full_jid_with_resource, [msg_type]): # msg_type can be chat or pm + elif not chat_control and not gajim.events.get_events(account, + jid_of_control, [msg_type]): # msg_type can be chat or pm first = True if pm: @@ -788,12 +785,11 @@ class Interface: msg_type, subject, resource, msg_id, array[9], advanced_notif_num, xhtml = xhtml, session = session) nickname = gajim.get_name_from_jid(account, jid) - # Check and do wanted notifications msg = message if subject: msg = _('Subject: %s') % subject + '\n' + msg - notify.notify('new_message', full_jid_with_resource, account, [msg_type, + notify.notify('new_message', jid_of_control, account, [msg_type, first, nickname, msg], advanced_notif_num) if self.remote_ctrl: @@ -990,7 +986,7 @@ class Interface: resource = '' if vcard.has_key('resource'): resource = vcard['resource'] - + # vcard window win = None if self.instances[account]['infos'].has_key(jid): @@ -1012,14 +1008,11 @@ class Interface: elif self.msg_win_mgr.has_window(jid, account): win = self.msg_win_mgr.get_window(jid, account) ctrl = win.get_control(jid, account) - - for ctrl in self.msg_win_mgr.get_controls(jid=jid, acct=account): - if ctrl.type_id != message_control.TYPE_GC: - ctrl.show_avatar() + if win and ctrl.type_id != message_control.TYPE_GC: + ctrl.show_avatar() # Show avatar in roster or gc_roster gc_ctrl = self.msg_win_mgr.get_control(jid, account) - # XXX get_gc_control? if gc_ctrl and gc_ctrl.type_id == message_control.TYPE_GC: gc_ctrl.draw_avatar(resource) else: @@ -1668,17 +1661,18 @@ class Interface: # XXX check if we can autoaccept if form.getType() == 'form': - ctrl = session.get_control() -# ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account) -# if not ctrl: -# resource = jid.getResource() -# contact = gajim.contacts.get_contact(account, str(jid), resource) -# if not contact: -# connection = gajim.connections[account] -# contact = gajim.contacts.create_contact(jid = jid.getStripped(), resource = resource, show = connection.get_status()) -# self.roster.new_chat(contact, account, resource = resource) + ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account) + if not ctrl: + resource = jid.getResource() + contact = gajim.contacts.get_contact(account, str(jid), resource) + if not contact: + connection = gajim.connections[account] + contact = gajim.contacts.create_contact(jid = jid.getStripped(), resource = resource, show = connection.get_status()) + self.roster.new_chat(contact, account, resource = resource) -# ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account) + ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account) + + ctrl.set_session(session) negotiation.FeatureNegotiationWindow(account, jid, session, form) diff --git a/src/message_control.py b/src/message_control.py index c0b409b11..6835ddb32 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -110,6 +110,13 @@ class MessageControl: def get_specific_unread(self): return len(gajim.events.get_events(self.account, self.contact.jid)) + def set_session(self, session): + if session == self.session: + return + if self.session: + print "starting a new session, forgetting about the old one!" + self.session = session + def send_message(self, message, keyID = '', type = 'chat', chatstate = None, msg_id = None, composing_jep = None, resource = None, user_nick = None): diff --git a/src/message_window.py b/src/message_window.py index 87e9a204f..6445495be 100644 --- a/src/message_window.py +++ b/src/message_window.py @@ -123,9 +123,8 @@ class MessageWindow: def get_num_controls(self): n = 0 - for sess_dict in self._controls.values(): - for dict in sess_dict.values(): - n += len(dict) + for dict in self._controls.values(): + n += len(dict) return n def _on_window_focus(self, widget, event): @@ -166,9 +165,7 @@ class MessageWindow: if not self._controls.has_key(control.account): self._controls[control.account] = {} fjid = control.get_full_jid() - if not self._controls.has_key(fjid): - self._controls[control.account][fjid] = {} - self._controls[control.account][fjid][control.session.thread_id] = control + self._controls[control.account][fjid] = control if self.get_num_controls() == 2: # is first conversation_textview scrolled down ? @@ -295,11 +292,11 @@ class MessageWindow: else: gtkgui_helpers.set_unset_urgency_hint(self.window, False) - def set_active_tab(self, jid, acct, thread_id): - ctrl = self._controls[acct][jid][thread_id] + def set_active_tab(self, jid, acct): + ctrl = self._controls[acct][jid] ctrl_page = self.notebook.page_num(ctrl.widget) self.notebook.set_current_page(ctrl_page) - + def remove_tab(self, ctrl, method, reason = None, force = False): '''reason is only for gc (offline status message) if force is True, do not ask any confirmation''' @@ -320,9 +317,7 @@ class MessageWindow: self.notebook.remove_page(self.notebook.page_num(ctrl.widget)) fjid = ctrl.get_full_jid() - del self._controls[ctrl.account][fjid][ctrl.session.thread_id] - if len(self._controls[ctrl.account][fjid]) == 0: - del self._controls[ctrl.account][fjid] + del self._controls[ctrl.account][fjid] if len(self._controls[ctrl.account]) == 0: del self._controls[ctrl.account] @@ -420,19 +415,7 @@ class MessageWindow: for ctrl in self.controls(): ctrl.update_tags() - def has_control(self, jid, acct, thread_id = None): - try: - if thread_id: - return (thread_id in self._controls[acct][jid]) - else: - return (jid in self._controls[acct]) - except KeyError: - return False - - def get_controls(self, jid, acct): - return self._controls[acct][jid].values() - - def get_control(self, key, acct, thread_id): + def get_control(self, key, acct): '''Return the MessageControl for jid or n, where n is a notebook page index. When key is an int index acct may be None''' if isinstance(key, str): @@ -441,7 +424,7 @@ class MessageWindow: if isinstance(key, unicode): jid = key try: - return self._controls[acct][jid][thread_id] + return self._controls[acct][jid] except: return None else: @@ -453,10 +436,9 @@ class MessageWindow: return self._widget_to_control(nth_child) def controls(self): - for jid_dict in self._controls.values(): - for sess_dict in jid_dict.values(): - for ctrl in sess_dict.values(): - yield ctrl + for ctrl_dict in self._controls.values(): + for ctrl in ctrl_dict.values(): + yield ctrl def move_to_next_unread_tab(self, forward): ind = self.notebook.get_current_page() @@ -513,7 +495,7 @@ class MessageWindow: if old_no >= 0: old_ctrl = self._widget_to_control(notebook.get_nth_page(old_no)) old_ctrl.set_control_active(False) - + new_ctrl = self._widget_to_control(notebook.get_nth_page(page_num)) new_ctrl.set_control_active(True) self.show_title(control = new_ctrl) @@ -587,11 +569,11 @@ class MessageWindow: source_child = self.notebook.get_nth_page(source_page_num) if dest_page_num != source_page_num: self.notebook.reorder_child(source_child, dest_page_num) - + def get_tab_at_xy(self, x, y): '''Thanks to Gaim Return the tab under xy and - if its nearer from left or right side of the tab + if its nearer from left or right side of the tab ''' page_num = -1 to_right = False @@ -612,7 +594,7 @@ class MessageWindow: if (y >= tab_alloc.y) and \ (y <= (tab_alloc.y + tab_alloc.height)): page_num = i - + if y > tab_alloc.y + (tab_alloc.height / 2.0): to_right = True break @@ -677,22 +659,14 @@ class MessageWindowMgr: return w return None - def get_window(self, jid, acct, thread_id): + def get_window(self, jid, acct): for win in self.windows(): - if win.has_control(jid, acct, thread_id): + if win.get_control(jid, acct): return win return None - def get_windows(self, jid, acct): - for win in self.windows(): - if win.has_control(jid, acct): - yield win - - def has_window(self, jid, acct, thread_id = None): - for win in self.windows(): - if win.has_control(jid, acct, thread_id): - return True - return False + def has_window(self, jid, acct): + return self.get_window(jid, acct) != None def one_window_opened(self, contact, acct, type): try: @@ -704,7 +678,7 @@ class MessageWindowMgr: '''Resizes window according to config settings''' if not gajim.config.get('saveposition'): return - + if self.mode == self.ONE_MSG_WINDOW_ALWAYS: size = (gajim.config.get('msgwin-width'), gajim.config.get('msgwin-height')) @@ -721,7 +695,7 @@ class MessageWindowMgr: return gtkgui_helpers.resize_window(win.window, size[0], size[1]) - + def _position_window(self, win, acct, type): '''Moves window according to config settings''' if not gajim.config.get('saveposition') or\ @@ -799,20 +773,18 @@ class MessageWindowMgr: del self._windows[k] return - def get_control(self, jid, acct, thread_id): + def get_control(self, jid, acct): '''Amongst all windows, return the MessageControl for jid''' - win = self.get_window(jid, acct, thread_id) + win = self.get_window(jid, acct) if win: - return win.get_control(jid, acct, thread_id) + return win.get_control(jid, acct) return None - def get_controls(self, type = None, acct = None, jid = None): + def get_controls(self, type = None, acct = None): ctrls = [] for c in self.controls(): if acct and c.account != acct: continue - if jid and c.get_full_jid() != jid: - continue if not type or c.type_id == type: ctrls.append(c) return ctrls @@ -836,7 +808,7 @@ class MessageWindowMgr: def save_state(self, msg_win): if not gajim.config.get('saveposition'): return - + # Save window size and position pos_x_key = 'msgwin-x-position' pos_y_key = 'msgwin-y-position' @@ -871,11 +843,11 @@ class MessageWindowMgr: if self.mode != self.ONE_MSG_WINDOW_NEVER: gajim.config.set_per('accounts', acct, pos_x_key, x) gajim.config.set_per('accounts', acct, pos_y_key, y) - + else: gajim.config.set(size_width_key, width) gajim.config.set(size_height_key, height) - + if self.mode != self.ONE_MSG_WINDOW_NEVER: gajim.config.set(pos_x_key, x) gajim.config.set(pos_y_key, y) diff --git a/src/notify.py b/src/notify.py index 8dda70f69..66b130791 100644 --- a/src/notify.py +++ b/src/notify.py @@ -40,7 +40,7 @@ try: except ImportError: USER_HAS_PYNOTIFY = False -def get_show_in_roster(event, account, contact, session): +def get_show_in_roster(event, account, contact): '''Return True if this event must be shown in roster, else False''' if event == 'gc_message_received': return True @@ -51,7 +51,7 @@ def get_show_in_roster(event, account, contact, session): if gajim.config.get_per('notifications', str(num), 'roster') == 'no': return False if event == 'message_received': - chat_control = helpers.get_chat_control(account, contact, session) + chat_control = helpers.get_chat_control(account, contact) if chat_control: return False return True diff --git a/src/roster_window.py b/src/roster_window.py index 139facee8..f9e85debc 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1191,9 +1191,8 @@ class RosterWindow: for jid in gajim.contacts.get_jid_list(account): results = gajim.logger.get_unread_msgs_for_jid(jid) - # XXX results should contain sessions anyways + # XXX unread messages should probably have their session with them session = gajim.connections[account].make_new_session(jid) - for result in results: tim = time.localtime(float(result[2])) self.on_message(jid, result[1], tim, account, msg_type = 'chat', @@ -1259,8 +1258,8 @@ class RosterWindow: gajim.transport_avatar[account][host].append(contact1.jid) # If we already have a chat window opened, update it with new contact # instance - chat_controls = gajim.interface.msg_win_mgr.get_controls(jid=ji, acct=account) - for chat_control in chat_controls: + chat_control = gajim.interface.msg_win_mgr.get_control(ji, account) + if chat_control: chat_control.contact = contact1 def chg_contact_status(self, contact, show, status, account): @@ -1287,14 +1286,14 @@ class RosterWindow: jid_list = [contact.jid] for jid in jid_list: if gajim.interface.msg_win_mgr.has_window(jid, account): - for win in gajim.interface.msg_win_mgr.get_windows(jid, account): - for ctrl in win.get_controls(jid=jid, acct=account): - ctrl.contact = gajim.contacts.get_contact_with_highest_priority( - account, contact.jid) - ctrl.update_ui() - win.redraw_tab(ctrl) + win = gajim.interface.msg_win_mgr.get_window(jid, account) + ctrl = win.get_control(jid, account) + ctrl.contact = gajim.contacts.get_contact_with_highest_priority( + account, contact.jid) + ctrl.update_ui() + win.redraw_tab(ctrl) - name = contact.get_shown_name() + name = contact.get_shown_name() # if multiple resources (or second one disconnecting) if (len(contact_instances) > 1 or (len(contact_instances) == 1 and \ @@ -3492,7 +3491,7 @@ class RosterWindow: if not session: session = gajim.connections[account].make_new_session(fjid) - mw = gajim.interface.msg_win_mgr.get_window(fjid, account, session.thread_id) + mw = gajim.interface.msg_win_mgr.get_window(fjid, account) if not mw: mw = gajim.interface.msg_win_mgr.create_window(contact, account, type_) @@ -3504,8 +3503,6 @@ class RosterWindow: # We call this here to avoid race conditions with widget validation chat_control.read_queue() - return session - def new_chat_from_jid(self, account, fjid): jid, resource = gajim.get_room_and_nick_from_fjid(fjid) if resource: @@ -3579,18 +3576,16 @@ class RosterWindow: path = self.get_path(jid, account) # Try to get line of contact in roster - ctrl = session.get_control(advanced_notif_num) - # Look for a chat control that has the given resource -# ctrl = gajim.interface.msg_win_mgr.get_control(fjid, account) -# if not ctrl: + ctrl = gajim.interface.msg_win_mgr.get_control(fjid, account) + if not ctrl: # if not, if message comes from highest prio, get control or open one # without resource -# if highest_contact and contact.resource == highest_contact.resource \ -# and not jid == gajim.get_jid_from_account(account): -# ctrl = gajim.interface.msg_win_mgr.get_control(jid, account) -# fjid = jid -# resource_for_chat = None + if highest_contact and contact.resource == highest_contact.resource \ + and not jid == gajim.get_jid_from_account(account): + ctrl = gajim.interface.msg_win_mgr.get_control(jid, account) + fjid = jid + resource_for_chat = None # Do we have a queue? no_queue = len(gajim.events.get_events(account, fjid)) == 0 @@ -3608,6 +3603,8 @@ class RosterWindow: typ = '' if msg_type == 'error': typ = 'status' + if session: + ctrl.set_session(session) ctrl.print_conversation(msg, typ, tim = tim, encrypted = encrypted, subject = subject, xhtml = xhtml) if msg_id: @@ -3620,26 +3617,26 @@ class RosterWindow: if msg_type == 'normal': type_ = 'normal' event_type = 'single_message_received' - show_in_roster = notify.get_show_in_roster(event_type, account, contact, session) + show_in_roster = notify.get_show_in_roster(event_type, account, contact) show_in_systray = notify.get_show_in_systray(event_type, account, contact) event = gajim.events.create_event(type_, (msg, subject, msg_type, tim, encrypted, resource, msg_id, xhtml, session), show_in_roster = show_in_roster, show_in_systray = show_in_systray) gajim.events.add_event(account, fjid, event) -# if popup: -# if not ctrl: -# self.new_chat(contact, account, resource = resource_for_chat) -# if path and not self.dragging and gajim.config.get( -# 'scroll_roster_to_last_message'): -# # we curently see contact in our roster OR he -# # is not in the roster at all. + if popup: + if not ctrl: + self.new_chat(contact, account, resource = resource_for_chat) + if path and not self.dragging and gajim.config.get( + 'scroll_roster_to_last_message'): + # we curently see contact in our roster OR he + # is not in the roster at all. # show and select his line in roster # do not change selection while DND'ing -# self.tree.expand_row(path[0:1], False) -# self.tree.expand_row(path[0:2], False) -# self.tree.scroll_to_cell(path) -# self.tree.set_cursor(path) - if not popup: + self.tree.expand_row(path[0:1], False) + self.tree.expand_row(path[0:2], False) + self.tree.scroll_to_cell(path) + self.tree.set_cursor(path) + else: if no_queue: # We didn't have a queue: we change icons self.draw_contact(jid, account) self.show_title() # we show the * or [n] @@ -3927,17 +3924,17 @@ class RosterWindow: fjid = contact.jid if resource: fjid += '/' + resource - - session = self.new_chat(contact, account, resource=resource, session=session) - win = gajim.interface.msg_win_mgr.get_window(fjid, account, session.thread_id) - ctrl = win.get_control(fjid, account, session.thread_id) - # last message is long time ago - gajim.last_message_time[account][ctrl.get_full_jid()] = 0 - - win.set_active_tab(fjid, account, session.thread_id) + win = gajim.interface.msg_win_mgr.get_window(fjid, account) + if not win: + self.new_chat(contact, account, resource = resource, session = session) + win = gajim.interface.msg_win_mgr.get_window(fjid, account) + ctrl = win.get_control(fjid, account) + # last message is long time ago + gajim.last_message_time[account][ctrl.get_full_jid()] = 0 + win.set_active_tab(fjid, account) if gajim.connections[account].is_zeroconf and \ gajim.connections[account].status in ('offline', 'invisible'): - win.get_control(fjid, account, session.thread_id).got_disconnected() + win.get_control(fjid, account).got_disconnected() win.window.present() @@ -3977,7 +3974,6 @@ class RosterWindow: jid = child_jid else: child_iter = model.iter_next(child_iter) - session = None if first_ev: session = first_ev.parameters[8] @@ -3991,7 +3987,7 @@ class RosterWindow: c = gajim.contacts.get_contact_with_highest_priority(account, jid) if jid == gajim.get_jid_from_account(account): resource = c.resource - self.on_open_chat_window(widget, c, account, resource = resource, session=session) + self.on_open_chat_window(widget, c, account, resource = resource, session = session) def on_roster_treeview_row_activated(self, widget, path, col = 0): '''When an iter is double clicked: open the first event window''' From 4f2cd0a0fc308fb62ec43ff6e9940d0d1d81d9e1 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Fri, 8 Jun 2007 19:42:02 +0000 Subject: [PATCH 11/40] stubs for XEP-0116 --- data/glade/chat_control_popup_menu.glade | 10 +++ src/chat_control.py | 8 +- src/common/stanza_session.py | 96 ++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/data/glade/chat_control_popup_menu.glade b/data/glade/chat_control_popup_menu.glade index b6a696355..3c56e29fa 100644 --- a/data/glade/chat_control_popup_menu.glade +++ b/data/glade/chat_control_popup_menu.glade @@ -65,6 +65,16 @@ + + + True + Toggle XEP-0116 Encryption + True + False + + + + True diff --git a/src/chat_control.py b/src/chat_control.py index a9a816c1c..c54fe026c 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1255,7 +1255,7 @@ class ChatControl(ChatControlBase): # this is here (and not in send_chatstate) # because we want it sent with REAL message # (not standlone) eg. one that has body - + if contact.our_chatstate: # We already asked for xep 85, don't ask it twice composing_jep = 'asked_once' @@ -1939,6 +1939,12 @@ class ChatControl(ChatControlBase): tb = self.xml.get_widget('gpg_togglebutton') tb.set_active(not tb.get_active()) + def _on_toggle_e2e_menuitem_activate(self, widget): + if 'security' in self.session.features and self.session.features['security'] == 'e2e': + self.session.negotiate_e2e() + else: + self.session.terminate_e2e() + def got_connected(self): ChatControlBase.got_connected(self) # Refreshing contact diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index b150e5ee1..98074c833 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -28,6 +28,102 @@ class StanzaSession: self.thread_id = self.generate_thread_id() self.last_send = 0 + self.features = {} def generate_thread_id(self): return "".join([random.choice(string.letters) for x in xrange(0,32)]) + + def negotiate_e2e(): + + pass + + +# +# +# urn:xmpp:ssn +# +# +# 1 +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# aes128-ctr +# +# +# sha256 +# +# +# none +# +# +# +# +# +# +# +# none +# +# +# none +# +# +# +# +# +# +# 4294967295 +# +# +# ** Alice's Base64 encoded ESession ID ** +# +# +# sas28x5 +# +# +# ** Base64 encoded value of He5 ** +# ** Base64 encoded value of He14 ** +# ** Base64 encoded value of He2 ** +# ** Base64 encoded value of He1 ** +# +# + + def terminate_e2e(): + pass + +# +# ffd7076498744578d10edabfe7f4a866 +# +# ** Base64 encoded encrypted terminate form ** +# ** Base64 encoded old MAC key ** +# ** Base64 encoded a_mac ** +# +# + +# +# +# +# urn:xmpp:ssn +# +# 1 +# +# From 62cf72910f1b905cea18ee6db94c402fdca69a6c Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Mon, 11 Jun 2007 22:42:29 +0000 Subject: [PATCH 12/40] functioning XEP-0200 implementation (hardcoded keys and counters) --- src/chat_control.py | 16 +++- src/common/connection.py | 15 ++-- src/common/connection_handlers.py | 8 +- src/common/stanza_session.py | 137 +++++++++++++++++++++++++++++- src/common/xmpp/protocol.py | 1 + 5 files changed, 164 insertions(+), 13 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index c54fe026c..2df79545c 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1474,6 +1474,7 @@ class ChatControl(ChatControlBase): history_menuitem = xml.get_widget('history_menuitem') toggle_gpg_menuitem = xml.get_widget('toggle_gpg_menuitem') + toggle_e2e_menuitem = xml.get_widget('toggle_e2e_menuitem') add_to_roster_menuitem = xml.get_widget('add_to_roster_menuitem') send_file_menuitem = xml.get_widget('send_file_menuitem') compact_view_menuitem = xml.get_widget('compact_view_menuitem') @@ -1492,7 +1493,7 @@ class ChatControl(ChatControlBase): is_sensitive = gpg_btn.get_property('sensitive') toggle_gpg_menuitem.set_active(isactive) toggle_gpg_menuitem.set_property('sensitive', is_sensitive) - + # If we don't have resource, we can't do file transfer # in transports, contact holds our info we need to disable it too if contact.resource and contact.jid.find('@') != -1: @@ -1527,6 +1528,8 @@ class ChatControl(ChatControlBase): self.handlers[id] = add_to_roster_menuitem id = toggle_gpg_menuitem.connect('activate', self._on_toggle_gpg_menuitem_activate) + id = toggle_e2e_menuitem.connect('activate', + self._on_toggle_e2e_menuitem_activate) self.handlers[id] = toggle_gpg_menuitem id = information_menuitem.connect('activate', self._on_contact_information_menuitem_activate) @@ -1940,10 +1943,15 @@ class ChatControl(ChatControlBase): tb.set_active(not tb.get_active()) def _on_toggle_e2e_menuitem_activate(self, widget): - if 'security' in self.session.features and self.session.features['security'] == 'e2e': - self.session.negotiate_e2e() + #if 'security' in self.session.features and self.session.features['security'] == 'e2e': + if self.session.enable_encryption: + self.session.enable_encryption = False + print "e2e disabled." +# self.session.terminate_e2e() else: - self.session.terminate_e2e() + self.session.enable_encryption = True + print "e2e enabled." +# self.session.negotiate_e2e() def got_connected(self): ChatControlBase.got_connected(self) diff --git a/src/common/connection.py b/src/common/connection.py index 047e19d67..8e6eab6d0 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -885,11 +885,6 @@ class Connection(ConnectionHandlers): if msgenc: msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc) - # XEP-0201 - if session: - session.last_send = time.time() - msg_iq.setThread(session.thread_id) - # JEP-0172: user_nickname if user_nick: msg_iq.setTag('nick', namespace = common.xmpp.NS_NICK).setData( @@ -915,7 +910,17 @@ class Connection(ConnectionHandlers): if chatstate is 'composing' or msgtxt: chatstate_node.addChild(name = 'composing') + if session: + # XEP-0201 + session.last_send = time.time() + msg_iq.setThread(session.thread_id) + + # XEP-0200 + if session.enable_encryption: + msg_iq = session.encrypt_stanza(msg_iq) + 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) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index f4f6306ff..9ea97d14c 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -37,7 +37,7 @@ from common import atom from common.commands import ConnectionCommands from common.pubsub import ConnectionPubSub -from common.stanza_session import StanzaSession +from common.stanza_session import EncryptedStanzaSession STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'error'] @@ -1440,6 +1440,10 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, common.xmpp.NS_FEATURE: self._FeatureNegCB(con, msg, session) return + + e2eTag = msg.getTag('c', namespace = common.xmpp.NS_STANZA_CRYPTO) + if e2eTag: + msg = session.decrypt_stanza(msg) msgtxt = msg.getBody() msghtml = msg.getXHTML() @@ -1611,7 +1615,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, return null_sessions[-1] def make_new_session(self, jid, thread_id = None, type = 'chat'): - sess = StanzaSession(self, jid, thread_id, type) + sess = EncryptedStanzaSession(self, 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 98074c833..eb947fa55 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -6,6 +6,11 @@ from common import helpers import random import string +from Crypto.Cipher import AES +from Crypto.Hash import HMAC, SHA256 + +import base64 + class StanzaSession: def __init__(self, conn, jid, thread_id, type): self.conn = conn @@ -33,11 +38,139 @@ class StanzaSession: def generate_thread_id(self): return "".join([random.choice(string.letters) for x in xrange(0,32)]) + +class EncryptedStanzaSession(StanzaSession): + def __init__(self, conn, jid, thread_id, type = 'chat'): + StanzaSession.__init__(self, conn, jid, thread_id, type = 'chat') + + self.n = 128 + + self.cipher = AES + self.hash_alg = SHA256 + + self.en_key = '................' + self.de_key = '----------------' + + self.en_counter = 777 + self.de_counter = 777 ^ (2 ** (self.n - 1)) + + self.encrypter = self.cipher.new(self.en_key, self.cipher.MODE_CTR, counter=self.encryptcounter) + self.decrypter = self.cipher.new(self.de_key, self.cipher.MODE_CTR, counter=self.decryptcounter) + + self.compression = None + + self.enable_encryption = False + + # convert a large integer to a big-endian bitstring + def encode_mpi(self, n): + if n >= 256: + return self.encode_mpi(n / 256) + chr(n % 256) + else: + return chr(n) + + # convert a large integer to a big-endian bitstring, padded with \x00s to 16 bytes + def encode_mpi_with_padding(self, n): + ret = self.encode_mpi(n) + + mod = len(ret) % 16 + if mod != 0: + ret = ((16 - mod) * '\x00') + ret + + return ret + + # convert a big-endian bitstring to an integer + def decode_mpi(self, s): + if len(s) == 0: + return 0 + else: + return 256 * self.decode_mpi(s[:-1]) + ord(s[-1]) + + def encryptcounter(self): + self.en_counter = (self.en_counter + 1) % 2 ** self.n + return self.encode_mpi_with_padding(self.en_counter) + + def decryptcounter(self): + self.de_counter = (self.de_counter + 1) % 2 ** self.n + return self.encode_mpi_with_padding(self.de_counter) + + def encrypt_stanza(self, stanza): + encryptable = filter(lambda x: x.getName() not in ('error', 'amp', 'thread'), stanza.getChildren()) + + # XXX can also encrypt contents of elements in stanzas @type = 'error' + # (except for child elements) + + old_en_counter = self.en_counter + + for element in encryptable: + stanza.delChild(element) + + plaintext = ''.join(map(str, encryptable)) + + m_compressed = self.compress(plaintext) + m_final = self.encrypt(m_compressed) + + c = stanza.NT.c + c.setNamespace('http://www.xmpp.org/extensions/xep-0200.html#ns') + c.NT.data = base64.b64encode(m_final) + + # XXX check for rekey request, handle elements + + m_content = ''.join(map(str, c.getChildren())) + c.NT.mac = base64.b64encode(self.hmac(m_content, old_en_counter, self.en_key)) + + return stanza + + def hmac(self, content, counter, key): + return HMAC.new(key, content + self.encode_mpi_with_padding(counter), self.hash_alg).digest() + + def compress(self, plaintext): + if self.compression == None: + return plaintext + + def decompress(self, compressed): + if self.compression == None: + return compressed + + def encrypt(self, encryptable): + len_padding = 16 - (len(encryptable) % 16) + encryptable += len_padding * ' ' + + return self.encrypter.encrypt(encryptable) + + def decrypt_stanza(self, stanza): + c = stanza.getTag(name='c', namespace='http://www.xmpp.org/extensions/xep-0200.html#ns') + + stanza.delChild(c) + + # contents of , minus , minus whitespace + macable = ''.join(map(str, filter(lambda x: x.getName() != 'mac', c.getChildren()))) + + received_mac = base64.b64decode(c.getTagData('mac')) + calculated_mac = self.hmac(macable, self.de_counter, self.de_key) + + if not calculated_mac == received_mac: + raise 'bad signature (%s != %s)' % (repr(received_mac), repr(calculated_mac)) + + m_final = base64.b64decode(c.getTagData('data')) + m_compressed = self.decrypt(m_final) + plaintext = self.decompress(m_compressed) + + try: + parsed = xmpp.Node(node='' + plaintext + '') + except: + raise DecryptionError + + for child in parsed.getChildren(): + stanza.addChild(node=child) + + return stanza + + def decrypt(self, ciphertext): + return self.decrypter.decrypt(ciphertext) + def negotiate_e2e(): - pass - # # # urn:xmpp:ssn diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index 6267e201a..7d903a292 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -81,6 +81,7 @@ NS_SESSION ='urn:ietf:params:xml:ns:xmpp-session' NS_SI ='http://jabber.org/protocol/si' # JEP-0096 NS_SI_PUB ='http://jabber.org/protocol/sipub' # JEP-0137 NS_SIGNED ='jabber:x:signed' # JEP-0027 +NS_STANZA_CRYPTO='http://www.xmpp.org/extensions/xep-0200.html#ns' # JEP-0200 NS_STANZAS ='urn:ietf:params:xml:ns:xmpp-stanzas' NS_STREAM ='http://affinix.com/jabber/stream' NS_STREAMS ='http://etherx.jabber.org/streams' From 8b7b368922cdd85a172f75d1a5aeb2b71bf182ed Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Sun, 17 Jun 2007 10:37:59 +0000 Subject: [PATCH 13/40] support libraries for esessions --- src/common/dh.py | 207 ++++++++++++++++++++++++++++++++++++++++ src/common/xmpp/c14n.py | 36 +++++++ 2 files changed, 243 insertions(+) create mode 100644 src/common/dh.py create mode 100644 src/common/xmpp/c14n.py diff --git a/src/common/dh.py b/src/common/dh.py new file mode 100644 index 000000000..8dd73f669 --- /dev/null +++ b/src/common/dh.py @@ -0,0 +1,207 @@ +import string + +# This file defines a number of constants; specifically, large primes suitable for +# use with the Diffie-Hellman key exchange. +# +# These constants have been obtained from RFC2409 and RFC3526. + +generators = [ None, # one to get the right offset + 2, + 2, + None, + None, + 2, + None, + None, + None, + None, + None, + None, + None, + None, + 2, # group 14 + 2, + 2, + 2, + 2, + ] + +hex_primes = [ None, + +# group 1 +'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 +29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD +EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 +E485B576 625E7EC6 F44C42E9 A63A3620 FFFFFFFF FFFFFFFF''', + +# group 2 +'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 +29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD +EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 +E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED +EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE65381 +FFFFFFFF FFFFFFFF''', + +# XXX how do I obtain these? +None, +None, + +# group 5 +'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 +29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD +EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 +E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED +EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D +C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F +83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D +670C354E 4ABC9804 F1746C08 CA237327 FFFFFFFF FFFFFFFF''', + +None, +None, +None, +None, +None, +None, +None, +None, + +# group 14 +'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 +29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD +EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 +E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED +EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D +C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F +83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D +670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B +E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 +DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 +15728E5A 8AACAA68 FFFFFFFF FFFFFFFF''', + +# group 15 +'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 +29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD +EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 +E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED +EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D +C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F +83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D +670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B +E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 +DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 +15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64 +ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7 +ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B +F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C +BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 +43DB5BFC E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF''', + +# group 16 +'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 +29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD +EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 +E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED +EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D +C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F +83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D +670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B +E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 +DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 +15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64 +ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7 +ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B +F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C +BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 +43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 +88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA +2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6 +287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED +1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9 +93B4EA98 8D8FDDC1 86FFFB7DC 90A6C08F 4DF435C9 34063199 +FFFFFFFF FFFFFFFF''', + +# group 17 +'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 +8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B +302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 +A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 +49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 +FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D +670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C +180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 +3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D +04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D +B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 +1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C +BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC +E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26 +99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB +04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2 +233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 +D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492 +36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406 +AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918 +DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151 +2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03 +F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F +BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA +CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B +B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632 +387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E +6DCC4024 FFFFFFFF FFFFFFFF''', + +# group 18 +'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 +29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD +EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 +E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED +EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D +C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F +83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D +670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B +E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 +DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 +15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64 +ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7 +ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B +F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C +BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 +43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 +88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA +2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6 +287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED +1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9 +93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492 +36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD +F8FF9406 AD9E530E E5DB382F 413001AE B06A53ED 9027D831 +179727B0 865A8918 DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B +DB7F1447 E6CC254B 33205151 2BD7AF42 6FB8F401 378CD2BF +5983CA01 C64B92EC F032EA15 D1721D03 F482D7CE 6E74FEF6 +D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F BEC7E8F3 +23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA +CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 +06A1D58B B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C +DA56C9EC 2EF29632 387FE8D7 6E3C0468 043E8F66 3F4860EE +12BF2D5B 0B7474D6 E694F91E 6DBE1159 74A3926F 12FEE5E4 +38777CB6 A932DF8C D8BEC4D0 73B931BA 3BC832B6 8D9DD300 +741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C 5AE4F568 +3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9 +22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B +4BCBC886 2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A +062B3CF5 B3A278A6 6D2A13F8 3F44F82D DF310EE0 74AB6A36 +4597E899 A0255DC1 64F31CC5 0846851D F9AB4819 5DED7EA1 +B1D510BD 7EE74D73 FAF36BC3 1ECFA268 359046F4 EB879F92 +4009438B 481C6CD7 889A002E D5EE382B C9190DA6 FC026E47 +9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71 +60C980DD 98EDD3DF FFFFFFFF FFFFFFFF''' +] + +all_ascii = ''.join(map(chr, range(256))) + +def hex_to_decimal(stripee): + if not stripee: + return None + + return int(stripee.translate(all_ascii, string.whitespace), 16) + +primes = map(hex_to_decimal, hex_primes) diff --git a/src/common/xmpp/c14n.py b/src/common/xmpp/c14n.py new file mode 100644 index 000000000..b58e0a1d6 --- /dev/null +++ b/src/common/xmpp/c14n.py @@ -0,0 +1,36 @@ +from simplexml import ustr + +# XML canonicalisation methods (for XEP-0116) +def c14n(node): + s = "<" + node.name + if node.namespace: + if not node.parent or node.parent.namespace != node.namespace: + s = s + ' xmlns="%s"' % node.namespace + + sorted_attrs = node.attrs.keys() + sorted_attrs.sort() + for key in sorted_attrs: + val = ustr(node.attrs[key]) + # like XMLescape() but with whitespace and without > + s = s + ' %s="%s"' % ( key, normalise_attr(val) ) + s = s + ">" + cnt = 0 + if node.kids: + for a in node.kids: + if (len(node.data)-1) >= cnt: + s = s + normalise_text(node.data[cnt]) + s = s + c14n(a) + cnt=cnt+1 + if (len(node.data)-1) >= cnt: s = s + normalise_text(node.data[cnt]) + if not node.kids and s[-1:]=='>': + s=s[:-1]+' />' + else: + s = s + "" + return s.encode('utf-8') + +def normalise_attr(val): + return val.replace('&', '&').replace('<', '<').replace('"', '"').replace('\t', ' ').replace('\n', ' ').replace('\r', ' ') + +def normalise_text(val): + return val.replace('&', '&').replace('<', '<').replace('>', '>').replace('\r', ' ') + From d1fe7b41f3aecb0ee02266be58198f39e49e1041 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Sun, 17 Jun 2007 10:39:19 +0000 Subject: [PATCH 14/40] functioning XEP-0217 implementation! (initiation only) --- src/chat_control.py | 8 +- src/common/connection_handlers.py | 11 +- src/common/stanza_session.py | 352 ++++++++++++++++++++++-------- src/gajim.py | 16 ++ 4 files changed, 291 insertions(+), 96 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 2df79545c..05db97d0d 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1946,12 +1946,12 @@ class ChatControl(ChatControlBase): #if 'security' in self.session.features and self.session.features['security'] == 'e2e': if self.session.enable_encryption: self.session.enable_encryption = False - print "e2e disabled." -# self.session.terminate_e2e() + print "terminating e2e." + self.session.terminate_e2e() else: self.session.enable_encryption = True - print "e2e enabled." -# self.session.negotiate_e2e() + print "negotiating e2e." + self.session.negotiate_e2e() def got_connected(self): ChatControlBase.got_connected(self) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 9ea97d14c..56150b20c 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1204,7 +1204,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, def _FeatureNegCB(self, con, stanza, session): gajim.log.debug('FeatureNegCB') - feature = stanza.getTag('feature') + feature = stanza.getTag(name='feature', namespace=common.xmpp.NS_FEATURE) form = common.xmpp.DataForm(node=feature.getTag('x')) if form['FORM_TYPE'] == 'urn:xmpp:ssn': @@ -1218,6 +1218,13 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, con.send(reply) + def _InitE2ECB(self, con, stanza, session): + gajim.log.debug('InitE2ECB') + init = stanza.getTag(name='init', namespace='http://www.xmpp.org/extensions/xep-0116.html#ns-init') + form = common.xmpp.DataForm(node=init.getTag('x')) + + self.dispatch('SESSION_NEG', (stanza.getFrom(), session, form)) + def _ErrorCB(self, con, iq_obj): gajim.log.debug('ErrorCB') if iq_obj.getQueryNS() == common.xmpp.NS_VERSION: @@ -1440,6 +1447,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, common.xmpp.NS_FEATURE: self._FeatureNegCB(con, msg, session) return + if msg.getTag('init') and msg.getTag('init').namespace == 'http://www.xmpp.org/extensions/xep-0116.html#ns-init': + self._InitE2ECB(con, msg, session) e2eTag = msg.getTag('c', namespace = common.xmpp.NS_STANZA_CRYPTO) if e2eTag: diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index eb947fa55..075fb9382 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -6,12 +6,19 @@ from common import helpers import random import string +import math +import os +import time + +from common import dh +import xmpp.c14n + from Crypto.Cipher import AES from Crypto.Hash import HMAC, SHA256 import base64 -class StanzaSession: +class StanzaSession(object): def __init__(self, conn, jid, thread_id, type): self.conn = conn @@ -33,11 +40,20 @@ class StanzaSession: self.thread_id = self.generate_thread_id() self.last_send = 0 + self.status = None self.features = {} def generate_thread_id(self): return "".join([random.choice(string.letters) for x in xrange(0,32)]) + def send(self, msg): + if self.thread_id: + msg.setThread(self.thread_id) + + msg.setAttr('to', self.jid) + self.conn.send_stanza(msg) + + self.last_send = time.time() class EncryptedStanzaSession(StanzaSession): def __init__(self, conn, jid, thread_id, type = 'chat'): @@ -48,19 +64,35 @@ class EncryptedStanzaSession(StanzaSession): self.cipher = AES self.hash_alg = SHA256 - self.en_key = '................' - self.de_key = '----------------' - - self.en_counter = 777 - self.de_counter = 777 ^ (2 ** (self.n - 1)) - - self.encrypter = self.cipher.new(self.en_key, self.cipher.MODE_CTR, counter=self.encryptcounter) - self.decrypter = self.cipher.new(self.de_key, self.cipher.MODE_CTR, counter=self.decryptcounter) - self.compression = None + self.xes = {} + self.es = {} + self.enable_encryption = False + self._kc_s = None + self._kc_o = None + + # keep the encrypter updated with my latest cipher key + def set_kc_s(self, value): + self._kc_s = value + self.encrypter = self.cipher.new(self._kc_s, self.cipher.MODE_CTR, counter=self.encryptcounter) + + def get_kc_s(self): + return self._kc_s + + # keep the decrypter updated with the other party's latest cipher key + def set_kc_o(self, value): + self._kc_o = value + self.decrypter = self.cipher.new(self._kc_o, self.cipher.MODE_CTR, counter=self.decryptcounter) + + def get_kc_o(self): + return self._kc_o + + kc_s = property(get_kc_s, set_kc_s) + kc_o = property(get_kc_o, set_kc_o) + # convert a large integer to a big-endian bitstring def encode_mpi(self, n): if n >= 256: @@ -86,12 +118,12 @@ class EncryptedStanzaSession(StanzaSession): return 256 * self.decode_mpi(s[:-1]) + ord(s[-1]) def encryptcounter(self): - self.en_counter = (self.en_counter + 1) % 2 ** self.n - return self.encode_mpi_with_padding(self.en_counter) + self.c_s = (self.c_s + 1) % (2 ** self.n) + return self.encode_mpi_with_padding(self.c_s) def decryptcounter(self): - self.de_counter = (self.de_counter + 1) % 2 ** self.n - return self.encode_mpi_with_padding(self.de_counter) + self.c_o = (self.c_o + 1) % (2 ** self.n) + return self.encode_mpi_with_padding(self.c_o) def encrypt_stanza(self, stanza): encryptable = filter(lambda x: x.getName() not in ('error', 'amp', 'thread'), stanza.getChildren()) @@ -99,7 +131,7 @@ class EncryptedStanzaSession(StanzaSession): # XXX can also encrypt contents of elements in stanzas @type = 'error' # (except for child elements) - old_en_counter = self.en_counter + old_en_counter = self.c_s for element in encryptable: stanza.delChild(element) @@ -116,12 +148,28 @@ class EncryptedStanzaSession(StanzaSession): # XXX check for rekey request, handle elements m_content = ''.join(map(str, c.getChildren())) - c.NT.mac = base64.b64encode(self.hmac(m_content, old_en_counter, self.en_key)) + c.NT.mac = base64.b64encode(self.hmac(self.km_s, m_content + self.encode_mpi(old_en_counter))) return stanza - def hmac(self, content, counter, key): - return HMAC.new(key, content + self.encode_mpi_with_padding(counter), self.hash_alg).digest() + 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() + + def generate_initiator_keys(self, k): + return (self.hmac(k, 'Initiator Cipher Key'), + self.hmac(k, 'Initiator MAC Key'), + self.hmac(k, 'Initiator SIGMA Key') ) + + def generate_responder_keys(self, k): + return (self.hmac(k, 'Responder Cipher Key'), + self.hmac(k, 'Responder MAC Key'), + self.hmac(k, 'Responder SIGMA Key') ) def compress(self, plaintext): if self.compression == None: @@ -133,10 +181,15 @@ class EncryptedStanzaSession(StanzaSession): def encrypt(self, encryptable): len_padding = 16 - (len(encryptable) % 16) - encryptable += len_padding * ' ' + if len_padding != 16: + encryptable += len_padding * ' ' return self.encrypter.encrypt(encryptable) + def generate_nonce(self): + # FIXME: this isn't a very good PRNG + return os.urandom(8) + def decrypt_stanza(self, stanza): c = stanza.getTag(name='c', namespace='http://www.xmpp.org/extensions/xep-0200.html#ns') @@ -146,7 +199,7 @@ class EncryptedStanzaSession(StanzaSession): macable = ''.join(map(str, filter(lambda x: x.getName() != 'mac', c.getChildren()))) received_mac = base64.b64decode(c.getTagData('mac')) - calculated_mac = self.hmac(macable, self.de_counter, self.de_key) + calculated_mac = self.hmac(self.km_o, macable + self.encode_mpi_with_padding(self.c_o)) if not calculated_mac == received_mac: raise 'bad signature (%s != %s)' % (repr(received_mac), repr(calculated_mac)) @@ -168,80 +221,197 @@ class EncryptedStanzaSession(StanzaSession): def decrypt(self, ciphertext): return self.decrypter.decrypt(ciphertext) - def negotiate_e2e(): - pass + def negotiate_e2e(self): + request = xmpp.Message() + feature = request.NT.feature + feature.setNamespace(xmpp.NS_FEATURE) -# -# -# urn:xmpp:ssn -# -# -# 1 -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# aes128-ctr -# -# -# sha256 -# -# -# none -# -# -# -# -# -# -# -# none -# -# -# none -# -# -# -# -# -# -# 4294967295 -# -# -# ** Alice's Base64 encoded ESession ID ** -# -# -# sas28x5 -# -# -# ** Base64 encoded value of He5 ** -# ** Base64 encoded value of He14 ** -# ** Base64 encoded value of He2 ** -# ** Base64 encoded value of He1 ** -# -# + x = xmpp.DataForm(typ='form') - def terminate_e2e(): + x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn', typ='hidden')) + 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)) + + # unsupported options: 'disabled', 'enabled' + x.addChild(node=xmpp.DataField(name='disclosure', typ='list-single', options=['never'], required=True)) + x.addChild(node=xmpp.DataField(name='security', typ='list-single', options=['e2e'], required=True)) + x.addChild(node=xmpp.DataField(name='crypt_algs', value='aes128-ctr', typ='hidden')) + x.addChild(node=xmpp.DataField(name='hash_algs', value='sha256', typ='hidden')) + x.addChild(node=xmpp.DataField(name='compress', value='none', typ='hidden')) + + # unsupported options: 'iq', 'presence' + x.addChild(node=xmpp.DataField(name='stanzas', typ='list-multi', options=['message'])) + + x.addChild(node=xmpp.DataField(name='init_pubkey', value='none', typ='hidden')) + x.addChild(node=xmpp.DataField(name='resp_pubkey', value='none', typ='hidden')) + x.addChild(node=xmpp.DataField(name='ver', value='1.0', typ='hidden')) + + x.addChild(node=xmpp.DataField(name='rekey_freq', value='4294967295', typ='hidden')) + + x.addChild(node=xmpp.DataField(name='sas_algs', value='sas28x5', typ='hidden')) + + self.n_s = self.generate_nonce() + + x.addChild(node=xmpp.DataField(name='my_nonce', value=base64.b64encode(self.n_s), typ='hidden')) + + modp_options = [ 5, 14, 2, 1 ] + + x.addChild(node=xmpp.DataField(name='modp', typ='list-single', options=map(lambda x: [ None, x ], modp_options))) + + dhhashes = map(lambda x: self.make_dhhash(x), modp_options) + x.addChild(node=xmpp.DataField(name='dhhashes', typ='hidden', value=dhhashes)) + + self.form_a = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren())) + + feature.addChild(node=x) + + self.status = 'requested-e2e' + + self.send(request) + + # generate a random number between 'bottom' and 'top' + def srand(self, bottom, top): + # minimum number of bytes needed to represent that range + bytes = int(math.ceil(math.log(top - bottom, 256))) + + # FIXME: use a real PRNG + # in retrospect, this is horribly inadequate. + return (self.decode_mpi(os.urandom(bytes)) % (top - bottom)) + bottom + + def make_dhhash(self, modp): + p = dh.primes[modp] + g = dh.generators[modp] + + x = self.srand(2 ** (2 * self.n - 1), p - 1) + + # XXX this may be a source of performance issues + e = self.powmod(g, x, p) + + self.xes[modp] = x + self.es[modp] = e + + He = self.sha256(self.encode_mpi(e)) + + return base64.b64encode(He) + + # a faster version of (base ** exp) % mod + # taken from + def powmod(self, base, exp, mod): + square = base % mod + result = 1 + + while exp > 0: + if exp & 1: # exponent is odd + result = (result * square) % mod + + square = (square * square) % mod + exp /= 2 + + return result + + def terminate_e2e(self): + self.status = None + + # 'Alice Accepts' + def accept_e2e_alice(self, form): +# 1. Verify that the ESession options selected by Bob are acceptable +# 2. Return a error to Bob unless: 1 < d < p - 1 + self.form_b = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) + + accept = xmpp.Message() + feature = accept.NT.feature + feature.setNamespace(xmpp.NS_FEATURE) + + result = xmpp.DataForm(typ='result') + + self.c_s = self.decode_mpi(base64.b64decode(form['counter'])) + self.c_o = self.c_s ^ (2 ** (self.n - 1)) + + self.n_o = base64.b64decode(form['my_nonce']) + + mod_p = int(form['modp']) + p = dh.primes[mod_p] + x = self.xes[mod_p] + e = self.es[mod_p] + + self.d = self.decode_mpi(base64.b64decode(form['dhkeys'])) + + self.k = self.sha256(self.encode_mpi(self.powmod(self.d, x, p))) + + result.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn')) + result.addChild(node=xmpp.DataField(name='accept', value='1')) + 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=[])) + + form_a2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), result.getChildren())) + + self.kc_s, self.km_s, self.ks_s = self.generate_initiator_keys(self.k) + + # MUST securely destroy K unless it will be used later to generate the final shared secret + + old_c_s = self.c_s + + mac_a = self.hmac(self.ks_s, self.n_o + self.n_s + self.encode_mpi(e) + self.form_a + form_a2) + id_a = self.encrypt(mac_a) + + m_a = self.hmac(self.km_s, self.encode_mpi(old_c_s) + id_a) + + result.addChild(node=xmpp.DataField(name='identity', value=base64.b64encode(id_a))) + result.addChild(node=xmpp.DataField(name='mac', value=base64.b64encode(m_a))) + + feature.addChild(node=result) + self.send(accept) + + self.status = 'identified-alice' + + 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 = '' + oss = '' + self.k = self.sha256(self.k + srs + oss) + + # 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. + + # 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) + +#4.6.2 Verifying Bob's Identity + + id_b = base64.b64decode(form['identity']) + + m_b = self.hmac(self.encode_mpi(self.c_o) + id_b, self.km_o) + + mac_b = self.decrypt(id_b) + + form_b2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) + + mac_b = self.hmac(self.n_s + self.n_o + self.encode_mpi(self.d) + self.form_b + form_b2, self.ks_o) + +# Note: If Alice discovers an error then she SHOULD ignore any encrypted content she received in the stanza. + # XXX check for MAC equality? + + self.status = 'active' + self.enable_encryption = True + + def accept_e2e_bob(self): pass + # # ffd7076498744578d10edabfe7f4a866 diff --git a/src/gajim.py b/src/gajim.py index 5309be946..ee2eac9a4 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1660,6 +1660,22 @@ class Interface: # XXX check negotiation state, etc. # XXX check if we can autoaccept + # order of e2e statuses: + # 1. Alice, Bob: None + # 2. Alice: requested-e2e + # 3. Bob: responded-e2e + # 4. Alice: identified-alice + # 5. Bob: identified-bob + # 6. Alice, Bob: active + + if session.status == 'requested-e2e' and form.getType() == 'submit': + print 'accepting' + session.accept_e2e_alice(form) + return + elif session.status == 'identified-alice' and form.getType() == 'result': + session.final_steps_alice(form) + return + if form.getType() == 'form': ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account) if not ctrl: From abd773b52e88afcbba91c9bf71ba00c921379c1f Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Wed, 20 Jun 2007 20:44:33 +0000 Subject: [PATCH 15/40] can respond to an esession request --- src/common/stanza_session.py | 267 +++++++++++++++++++++++++++++------ src/gajim.py | 16 ++- 2 files changed, 233 insertions(+), 50 deletions(-) diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 075fb9382..875c786e3 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -160,7 +160,7 @@ class EncryptedStanzaSession(StanzaSession): sh = SHA256.new() sh.update(string) return sh.digest() - + def generate_initiator_keys(self, k): return (self.hmac(k, 'Initiator Cipher Key'), self.hmac(k, 'Initiator MAC Key'), @@ -186,9 +186,13 @@ class EncryptedStanzaSession(StanzaSession): return self.encrypter.encrypt(encryptable) + # FIXME: get a real PRNG + def random_bytes(self, bytes): + return os.urandom(bytes) + def generate_nonce(self): # FIXME: this isn't a very good PRNG - return os.urandom(8) + return self.random_bytes(8) def decrypt_stanza(self, stanza): c = stanza.getTag(name='c', namespace='http://www.xmpp.org/extensions/xep-0200.html#ns') @@ -271,54 +275,92 @@ class EncryptedStanzaSession(StanzaSession): self.status = 'requested-e2e' 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) - # generate a random number between 'bottom' and 'top' - def srand(self, bottom, top): - # minimum number of bytes needed to represent that range - bytes = int(math.ceil(math.log(top - bottom, 256))) + x = xmpp.DataForm(typ='submit') - # FIXME: use a real PRNG - # in retrospect, this is horribly inadequate. - return (self.decode_mpi(os.urandom(bytes)) % (top - bottom)) + bottom + fixed = { 'disclosure': 'never', + 'security': 'e2e', + 'crypt_algs': 'aes128-ctr', + 'hash_algs': 'sha256', + 'compress': 'none', + 'stanzas': 'message', + 'init_pubkey': 'none', + 'resp_pubkey': 'none', + 'ver': '1.0', + 'sas_algs': 'sas28x5', + 'logging': 'may' } - def make_dhhash(self, modp): - p = dh.primes[modp] - g = dh.generators[modp] + not_acceptable = [] - x = self.srand(2 ** (2 * self.n - 1), p - 1) + x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn')) + x.addChild(node=xmpp.DataField(name='accept', value='true')) - # XXX this may be a source of performance issues - e = self.powmod(g, x, p) + for name, field in map(lambda name: (name, request_form.getField(name)), request_form.asDict().keys()): + options = map(lambda x: x[1], field.getOptions()) + values = field.getValues() - self.xes[modp] = x - self.es[modp] = e + if not field.getType() in ('list-single', 'list-multi'): + options = values - He = self.sha256(self.encode_mpi(e)) + if name in fixed: + if fixed[name] in options: + x.addChild(node=xmpp.DataField(name=name, value=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)) - return base64.b64encode(He) + 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)) - # a faster version of (base ** exp) % mod - # taken from - def powmod(self, base, exp, mod): - square = base % mod - result = 1 + self.rekey_freq = preferred + elif name == 'my_nonce': + self.n_o = base64.b64decode(field.getValue()) - while exp > 0: - if exp & 1: # exponent is odd - result = (result * square) % mod + # XXX do something with not_acceptable - square = (square * square) % mod - exp /= 2 + self.He = request_form.getField('dhhashes').getValues()[group_order].encode("utf8") - return result + bytes = int(self.n / 8) - def terminate_e2e(self): - self.status = None + self.n_s = self.generate_nonce() + + self.c_o = self.decode_mpi(self.random_bytes(bytes)) # n-bit random number + self.c_s = self.c_o ^ (2 ** (self.n - 1)) + + 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 } + + 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_b = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren())) + + self.status = 'responded-e2e' + + feature.addChild(node=x) + self.send(response) # 'Alice Accepts' def accept_e2e_alice(self, form): -# 1. Verify that the ESession options selected by Bob are acceptable -# 2. Return a error to Bob unless: 1 < d < p - 1 +# 1. Verify that the ESession options selected by Bob are acceptable +# 2. Return a error to Bob unless: 1 < d < p - 1 self.form_b = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) accept = xmpp.Message() @@ -369,6 +411,100 @@ class EncryptedStanzaSession(StanzaSession): self.send(accept) self.status = 'identified-alice' + + # 4.5 esession accept (bob) + def accept_e2e_bob(self, form): + response = xmpp.Message() + + init = response.NT.init + init.setNamespace('http://www.xmpp.org/extensions/xep-0116.html#ns-init') + + x = xmpp.DataForm(typ='result') + + for field in ('nonce', 'dhkeys', 'rshashes', 'identity', 'mac'): + assert field in form.asDict(), "your acceptance form didn't have a %s field" % repr(field) + + # 4.5.1 generating provisory session keys + e = self.decode_mpi(base64.b64decode(form['dhkeys'])) + p = dh.primes[self.modp] + + if not self.sha256(self.encode_mpi(e)) == self.He: + # XXX return + pass + + if not e > 1 and e < (p - 1): + # XXX return + pass + + k = self.sha256(self.encode_mpi(self.powmod(e, self.y, p))) + + self.kc_o, self.km_o, self.ks_o = self.generate_initiator_keys(k) + + # 4.5.2 verifying alice's identity + id_a = base64.b64decode(form['identity']) + m_a = base64.b64decode(form['mac']) + + m_a_calculated = self.hmac(self.km_o, self.encode_mpi(self.c_o) + id_a) + + if m_a_calculated != m_a: + # XXX return + pass + + mac_a = self.decrypt(id_a) + + macable_children = filter(lambda x: x.getVar() not in ('mac', 'identity'), form.getChildren()) + form_a2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), macable_children)) + + mac_a_calculated = self.hmac(self.ks_o, self.n_s + self.n_o + self.encode_mpi(e) + self.form_a + form_a2) + + if mac_a_calculated != mac_a: + # XXX return + pass + + # TODO: 4.5.3 + + # 4.5.4 generating bob's final session keys + self.srs = '' + oss = '' + + k = self.sha256(k + self.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') + else: + srshash = self.random_bytes(32) + + x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn')) + x.addChild(node=xmpp.DataField(name='nonce', value=base64.b64encode(self.n_o))) + x.addChild(node=xmpp.DataField(name='srshash', value=base64.b64encode(srshash))) + + form_b2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren())) + + old_c_s = self.c_s + mac_b = self.hmac(self.n_o + self.n_s + self.encode_mpi(self.d) + self.form_b + form_b2, self.ks_s) + id_b = self.encrypt(mac_b) + + m_b = self.hmac(self.km_s, self.encode_mpi(old_c_s) + id_b) + + x.addChild(node=xmpp.DataField(name='identity', value=base64.b64encode(id_b))) + x.addChild(node=xmpp.DataField(name='mac', value=base64.b64encode(m_b))) + + init.addChild(node=x) + + self.send(response) + + # destroy all copies of srs + + self.srs = self.hmac(k, 'New Retained Secret') + + # destroy k + self.status = 'active' + 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). @@ -409,24 +545,63 @@ class EncryptedStanzaSession(StanzaSession): self.status = 'active' self.enable_encryption = True - def accept_e2e_bob(self): - pass - + # generate a random number between 'bottom' and 'top' + def srand(self, bottom, top): + # minimum number of bytes needed to represent that range + bytes = int(math.ceil(math.log(top - bottom, 256))) + # FIXME: use a real PRNG + # in retrospect, this is horribly inadequate. + return (self.decode_mpi(os.urandom(bytes)) % (top - bottom)) + bottom + + def make_dhhash(self, modp): + p = dh.primes[modp] + g = dh.generators[modp] + + x = self.srand(2 ** (2 * self.n - 1), p - 1) + + # XXX this may be a source of performance issues + e = self.powmod(g, x, p) + + self.xes[modp] = x + self.es[modp] = e + + He = self.sha256(self.encode_mpi(e)) + + return base64.b64encode(He) + + # a faster version of (base ** exp) % mod + # taken from + def powmod(self, base, exp, mod): + square = base % mod + result = 1 + + while exp > 0: + if exp & 1: # exponent is odd + result = (result * square) % mod + + square = (square * square) % mod + exp /= 2 + + return result + + def terminate_e2e(self): + self.status = None + # # ffd7076498744578d10edabfe7f4a866 # -# ** Base64 encoded encrypted terminate form ** -# ** Base64 encoded old MAC key ** -# ** Base64 encoded a_mac ** +# ** Base64 encoded encrypted terminate form ** +# ** Base64 encoded old MAC key ** +# ** Base64 encoded a_mac ** # # # -# -# -# urn:xmpp:ssn -# -# 1 -# +# +# +# urn:xmpp:ssn +# +# 1 +# # diff --git a/src/gajim.py b/src/gajim.py index ee2eac9a4..aad978fde 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1665,14 +1665,22 @@ class Interface: # 2. Alice: requested-e2e # 3. Bob: responded-e2e # 4. Alice: identified-alice - # 5. Bob: identified-bob - # 6. Alice, Bob: active + # 5. Alice, Bob: active - if session.status == 'requested-e2e' and form.getType() == 'submit': - print 'accepting' + if form.getType() == 'form' and u'e2e' in map(lambda x: x[1], form.getField('security').getOptions()): + print 'responding' + session.respond_e2e_bob(form) + return + elif session.status == 'requested-e2e' and form.getType() == 'submit': + print 'accepting (alice)' session.accept_e2e_alice(form) return + elif session.status == 'responded-e2e' and form.getType() == 'result': + print 'accepting (bob)' + session.accept_e2e_bob(form) + return elif session.status == 'identified-alice' and form.getType() == 'result': + print 'completing' session.final_steps_alice(form) return From 39bea5fd40f66d8d6b8010097dc5e8962d1e31bd Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Tue, 26 Jun 2007 20:55:49 +0000 Subject: [PATCH 16/40] tidying things up. --- src/chat_control.py | 9 ++++--- src/common/stanza_session.py | 51 +++++++++++++++++++++++++++++++----- src/gajim.py | 18 +++---------- 3 files changed, 53 insertions(+), 25 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 05db97d0d..56019c6fa 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1493,7 +1493,11 @@ class ChatControl(ChatControlBase): is_sensitive = gpg_btn.get_property('sensitive') toggle_gpg_menuitem.set_active(isactive) toggle_gpg_menuitem.set_property('sensitive', is_sensitive) - + + # TODO: check that the remote client supports e2e + isactive = self.session.enable_encryption + toggle_e2e_menuitem.set_active(isactive) + # If we don't have resource, we can't do file transfer # in transports, contact holds our info we need to disable it too if contact.resource and contact.jid.find('@') != -1: @@ -1943,14 +1947,11 @@ class ChatControl(ChatControlBase): tb.set_active(not tb.get_active()) def _on_toggle_e2e_menuitem_activate(self, widget): - #if 'security' in self.session.features and self.session.features['security'] == 'e2e': if self.session.enable_encryption: self.session.enable_encryption = False - print "terminating e2e." self.session.terminate_e2e() else: self.session.enable_encryption = True - print "negotiating e2e." self.session.negotiate_e2e() def got_connected(self): diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 875c786e3..aada6c4ef 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -55,23 +55,47 @@ class StanzaSession(object): self.last_send = time.time() +# 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. +# 6. 'sent-terminate': +# this client has sent a termination notice and is waiting for +# acknowledgement. + +# the transition between these states is handled in gajim.py's +# handle_session_negotiation method. + class EncryptedStanzaSession(StanzaSession): def __init__(self, conn, jid, thread_id, type = 'chat'): StanzaSession.__init__(self, conn, jid, thread_id, type = 'chat') - self.n = 128 - - self.cipher = AES - self.hash_alg = SHA256 - - self.compression = None - self.xes = {} self.es = {} + self.n = 128 + self.enable_encryption = False + # _s denotes 'self' (ie. this client) self._kc_s = None + + # _o denotes 'other' (ie. the client at the other end of the session) self._kc_o = None # keep the encrypter updated with my latest cipher key @@ -298,6 +322,12 @@ class EncryptedStanzaSession(StanzaSession): not_acceptable = [] + 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')) @@ -360,6 +390,13 @@ class EncryptedStanzaSession(StanzaSession): # 'Alice Accepts' def accept_e2e_alice(self, form): # 1. Verify that the ESession options selected by Bob are acceptable + + self.encryptable_stanzas = ['message'] + self.sas_algs = 'sas28x5' + self.cipher = AES + self.hash_alg = SHA256 + self.compression = None + # 2. Return a error to Bob unless: 1 < d < p - 1 self.form_b = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) diff --git a/src/gajim.py b/src/gajim.py index aad978fde..ca8bc67dd 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1657,33 +1657,23 @@ class Interface: def handle_session_negotiation(self, account, data): jid, session, form = data - # XXX check negotiation state, etc. - # XXX check if we can autoaccept - - # order of e2e statuses: - # 1. Alice, Bob: None - # 2. Alice: requested-e2e - # 3. Bob: responded-e2e - # 4. Alice: identified-alice - # 5. Alice, Bob: active - + + # encrypted session states if form.getType() == 'form' and u'e2e' in map(lambda x: x[1], form.getField('security').getOptions()): - print 'responding' session.respond_e2e_bob(form) return elif session.status == 'requested-e2e' and form.getType() == 'submit': - print 'accepting (alice)' session.accept_e2e_alice(form) return elif session.status == 'responded-e2e' and form.getType() == 'result': - print 'accepting (bob)' session.accept_e2e_bob(form) return elif session.status == 'identified-alice' and form.getType() == 'result': - print 'completing' session.final_steps_alice(form) return + # non-esession negotiation. this isn't very useful, but i'm keeping it around + # to test my test suite. if form.getType() == 'form': ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account) if not ctrl: From 4b53db9fe6820b8d7cb1dde193c65a8987b724c6 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Tue, 26 Jun 2007 22:52:50 +0000 Subject: [PATCH 17/40] properly terminate sessions --- src/common/connection_handlers.py | 9 +++++- src/common/stanza_session.py | 47 +++++++++++++++++-------------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 56150b20c..c456e3510 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1602,6 +1602,12 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, except KeyError: return None + def delete_session(self, jid, thread_id): + del self.sessions[jid][thread_id] + + if not self.sessions[jid]: + del self.sessions[jid] + def move_session(self, original_jid, thread_id, to_resource): session = self.sessions[original_jid][thread_id] @@ -1616,7 +1622,8 @@ 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.''' + '''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) diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index aada6c4ef..6145db44e 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -55,6 +55,25 @@ class StanzaSession(object): self.last_send = time.time() + def terminate(self): + 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='terminate', value='1')) + + feature.addChild(node=x) + + self.send(msg) + + self.status = None + + def acknowledge_termination(self): + # we could send an acknowledgement message here, but we won't. + self.status = None + # an encrypted stanza negotiation has several states. i've represented them as the following values in the 'status' # attribute of the session object: @@ -74,9 +93,6 @@ class StanzaSession(object): # an encrypted session has been successfully negotiated. messages of # any of the types listed in 'encryptable_stanzas' should be encrypted # before they're sent. -# 6. 'sent-terminate': -# this client has sent a termination notice and is waiting for -# acknowledgement. # the transition between these states is handled in gajim.py's # handle_session_negotiation method. @@ -623,22 +639,11 @@ class EncryptedStanzaSession(StanzaSession): return result def terminate_e2e(self): - self.status = None - -# -# ffd7076498744578d10edabfe7f4a866 -# -# ** Base64 encoded encrypted terminate form ** -# ** Base64 encoded old MAC key ** -# ** Base64 encoded a_mac ** -# -# + self.terminate() -# -# -# -# urn:xmpp:ssn -# -# 1 -# -# + self.enable_encryption = False + + def acknowledge_termination(self): + StanzaSession.acknowledge_termination(self) + + self.enable_encryption = False From 4bd805cf073262ccf4d558d33de61b15763b56e9 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Thu, 28 Jun 2007 19:15:43 +0000 Subject: [PATCH 18/40] new option for whether encrypted sessions should be logged by default --- src/common/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/config.py b/src/common/config.py index 028d8f031..5a6b1c21a 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -174,6 +174,7 @@ class Config: 'tabs_always_visible': [opt_bool, False, _('Show tab when only one conversation?')], 'tabs_border': [opt_bool, False, _('Show tabbed notebook border in chat windows?')], 'tabs_close_button': [opt_bool, True, _('Show close button in tab?')], + 'log_encrypted_sessions': [opt_bool, False, _('When negotiating an encrypted session, should Gajim assume you want your messages to be logged?')], 'chat_avatar_width': [opt_int, 52], 'chat_avatar_height': [opt_int, 52], 'roster_avatar_width': [opt_int, 32], From 6fe668d863b6abc5195d5257f89e7a620618432b Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Fri, 29 Jun 2007 04:12:08 +0000 Subject: [PATCH 19/40] 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', From c3b81346d9db45f72f6c36442cc79a7ca0343310 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Mon, 2 Jul 2007 16:53:19 +0000 Subject: [PATCH 20/40] human-readable feature negotiation dialogs --- src/gajim.py | 20 ++++++++++---------- src/negotiation.py | 7 +++++++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/gajim.py b/src/gajim.py index b8c7a1b85..3ba655e16 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1659,7 +1659,7 @@ class Interface: jid, session, form = data if form.getField('accept') and not form['accept'] in ('1', 'true'): - dialogs.InformationDialog(_('Session negotiation cancelled.'), + dialogs.InformationDialog(_('Session negotiation cancelled'), _('The client at %s cancelled the session negotiation.') % (jid)) session.cancelled_negotiation() return @@ -1679,15 +1679,15 @@ class Interface: 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? + not_acceptable.append(key) 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) + dialog = dialogs.YesNoDialog(_('Confirm these session options'), + _('The remote client wants to negotiate an session with these features:\n\n%s\n\nAre these options acceptable?') % (negotiation.describe_features(ask_user)), + on_response_yes = accept_nondefault_options, + on_response_no = reject_nondefault_options) else: session.respond_e2e_bob(form, negotiated, not_acceptable) @@ -1708,10 +1708,10 @@ class Interface: 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) + dialog = dialogs.YesNoDialog(_('Confirm these session options'), + _('The remote client selected these options:\n\n%s\n\nContinue with the session?') % (negotiation.describe_features(ask_user)), + on_response_yes = accept_nondefault_options, + on_response_no = reject_nondefault_options) else: session.accept_e2e_alice(form, negotiated) diff --git a/src/negotiation.py b/src/negotiation.py index 095fd5178..f7c58852e 100644 --- a/src/negotiation.py +++ b/src/negotiation.py @@ -5,6 +5,13 @@ from common import dataforms from common import gajim from common import xmpp +def describe_features(features): + '''a human-readable description of the features that have been negotiated''' + if features['logging'] == 'may': + return '- messages will be logged' + elif features['logging'] == 'mustnot': + return '- messages will not be logged' + class FeatureNegotiationWindow: '''FeatureNegotiotionWindow class''' def __init__(self, account, jid, session, form): From 2bed6a522a248f94d54386e692da18fb82c47bbb Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Tue, 3 Jul 2007 18:00:09 +0000 Subject: [PATCH 21/40] fixed a traceback --- src/chat_control.py | 8 +++++--- src/common/connection_handlers.py | 1 - src/gajim.py | 1 + src/message_control.py | 8 ++++++-- src/roster_window.py | 3 --- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 5e3e30774..ec04c7bb7 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -508,7 +508,6 @@ class ChatControlBase(MessageControl): if not message or message == '\n': return 1 - if not self._process_command(message): ret = MessageControl.send_message(self, message, keyID, type = type, chatstate = chatstate, msg_id = msg_id, @@ -1495,7 +1494,7 @@ class ChatControl(ChatControlBase): toggle_gpg_menuitem.set_property('sensitive', is_sensitive) # TODO: check that the remote client supports e2e - isactive = self.session.enable_encryption + isactive = int(self.session != None and self.session.enable_encryption) toggle_e2e_menuitem.set_active(isactive) # If we don't have resource, we can't do file transfer @@ -1947,7 +1946,7 @@ class ChatControl(ChatControlBase): tb.set_active(not tb.get_active()) def _on_toggle_e2e_menuitem_activate(self, widget): - if self.session.enable_encryption: + if self.session and self.session.enable_encryption: self.session.terminate_e2e() jid = str(self.session.jid) @@ -1955,6 +1954,9 @@ class ChatControl(ChatControlBase): gajim.connections[self.account].delete_session(jid, self.session.thread_id) self.session = gajim.connections[self.account].make_new_session(jid) else: + if not self.session: + self.session = gajim.connections[self.account].make_new_session(self.contact.jid) + self.session.negotiate_e2e() def got_connected(self): diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index c96354ca1..b6fc50f1f 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1582,7 +1582,6 @@ 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 diff --git a/src/gajim.py b/src/gajim.py index 3ba655e16..8f2537b20 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1726,6 +1726,7 @@ class Interface: 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) diff --git a/src/message_control.py b/src/message_control.py index d8f4fa0d6..bca808837 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -113,9 +113,10 @@ class MessageControl: def set_session(self, session): if session == self.session: 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) + print "starting a new session, dropping the old one!" + gajim.connections[self.account].delete_session(self.contact.get_full_jid(), self.session.thread_id) self.session = session @@ -126,6 +127,9 @@ class MessageControl: ''' jid = self.contact.jid + if not self.session: + self.session = gajim.connections[self.account].make_new_session(self.contact.get_full_jid()) + # Send and update history return gajim.connections[self.account].send_message(jid, message, keyID, type = type, chatstate = chatstate, msg_id = msg_id, diff --git a/src/roster_window.py b/src/roster_window.py index f9e85debc..cc7c89364 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -3488,9 +3488,6 @@ class RosterWindow: if resource: fjid += '/' + resource - if not session: - session = gajim.connections[account].make_new_session(fjid) - mw = gajim.interface.msg_win_mgr.get_window(fjid, account) if not mw: mw = gajim.interface.msg_win_mgr.create_window(contact, account, type_) From 29c44d8a5e619daa1d825d1d078261fcc348368f Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Wed, 4 Jul 2007 18:55:53 +0000 Subject: [PATCH 22/40] interface for SAS --- src/common/stanza_session.py | 4 ++-- src/gajim.py | 7 ++++++- src/negotiation.py | 11 +++++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index edbee3df5..f8fd70867 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -561,7 +561,7 @@ class EncryptedStanzaSession(StanzaSession): # 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) + self.sas = 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))) @@ -629,7 +629,7 @@ class EncryptedStanzaSession(StanzaSession): # 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) + self.sas = self.sas_28x5(m_a, self.form_b) k = self.sha256(k + self.srs + oss) diff --git a/src/gajim.py b/src/gajim.py index 8f2537b20..dcc16e9cd 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1699,10 +1699,12 @@ class Interface: if ask_user: def accept_nondefault_options(widget): + dialog.destroy() + negotiated.update(ask_user) session.accept_e2e_alice(form, negotiated) - dialog.destroy() + negotiation.show_sas_dialog(jid, session.sas) def reject_nondefault_options(widget): session.reject_negotiation() @@ -1714,10 +1716,13 @@ 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.accept_e2e_bob(form) + negotiation.show_sas_dialog(jid, session.sas) return elif session.status == 'identified-alice' and form.getType() == 'result': session.final_steps_alice(form) diff --git a/src/negotiation.py b/src/negotiation.py index f7c58852e..73917400c 100644 --- a/src/negotiation.py +++ b/src/negotiation.py @@ -1,6 +1,8 @@ import gtkgui_helpers import dataforms_widget +import dialogs + from common import dataforms from common import gajim from common import xmpp @@ -8,9 +10,14 @@ from common import xmpp def describe_features(features): '''a human-readable description of the features that have been negotiated''' if features['logging'] == 'may': - return '- messages will be logged' + return _('- messages will be logged') elif features['logging'] == 'mustnot': - return '- messages will not be logged' + return _('- messages will not be logged') + +def show_sas_dialog(jid, sas): + dialogs.InformationDialog(_('''Verify the remote client's identity'''), _('''You've begun an encrypted session with %s, but it can't be guaranteed that you're talking directly to the person you think you are. + +You should speak with them directly (in person or on the phone) and confirm that their Short Authentication String is identical to this one: %s''') % (jid, sas)) class FeatureNegotiationWindow: '''FeatureNegotiotionWindow class''' From ccac0b42997927423a9a92569d30ba331840db83 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Thu, 5 Jul 2007 21:30:09 +0000 Subject: [PATCH 23/40] fixed presence leaks --- src/gajim.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/gajim.py b/src/gajim.py index dcc16e9cd..9f0e8fe96 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1664,10 +1664,17 @@ class Interface: session.cancelled_negotiation() return - # encrypted session states. these are descriped in stanza_session.py + # encrypted session states. these are described in stanza_session.py # bob responds - if form.getType() == 'form' and u'e2e' in map(lambda x: x[1], form.getField('security').getOptions()): + if form.getType() == 'form' and u'e2e' in \ + map(lambda x: x[1], form.getField('security').getOptions()): + contact = gajim.contacts.get_contact(account, jid.getStripped(), jid.getResource()) + + if gajim.SHOW_LIST[gajim.connections[account].connected] == 'invisible' or \ + contact.sub not in ('from', 'both'): + return + negotiated, not_acceptable, ask_user = session.verify_options_bob(form) if ask_user: @@ -1685,7 +1692,11 @@ class Interface: dialog.destroy() dialog = dialogs.YesNoDialog(_('Confirm these session options'), - _('The remote client wants to negotiate an session with these features:\n\n%s\n\nAre these options acceptable?') % (negotiation.describe_features(ask_user)), + _('''The remote client wants to negotiate an session with these features: + +%s + +Are these options acceptable?''') % (negotiation.describe_features(ask_user)), on_response_yes = accept_nondefault_options, on_response_no = reject_nondefault_options) else: From d32e8352d58b336643aeb788d4e033a327a03517 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Tue, 10 Jul 2007 05:41:43 +0000 Subject: [PATCH 24/40] proper error handling --- src/common/stanza_session.py | 58 +++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index f8fd70867..db90339a0 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -263,12 +263,11 @@ class EncryptedStanzaSession(StanzaSession): return self.encrypter.encrypt(encryptable) - # FIXME: get a real PRNG + # FIXME: use a real PRNG def random_bytes(self, bytes): return os.urandom(bytes) def generate_nonce(self): - # FIXME: this isn't a very good PRNG return self.random_bytes(8) def decrypt_stanza(self, stanza): @@ -581,19 +580,19 @@ class EncryptedStanzaSession(StanzaSession): x = xmpp.DataForm(typ='result') for field in ('nonce', 'dhkeys', 'rshashes', 'identity', 'mac'): - assert field in form.asDict(), "your acceptance form didn't have a %s field" % repr(field) + assert field in form.asDict(), "alice's form didn't have a %s field" % field # 4.5.1 generating provisory session keys e = self.decode_mpi(base64.b64decode(form['dhkeys'])) p = dh.primes[self.modp] - if not self.sha256(self.encode_mpi(e)) == self.He: - # XXX return - pass - - if not e > 1 and e < (p - 1): - # XXX return - pass + if (not self.sha256(self.encode_mpi(e)) == self.He): or \ + (not e > 1 and e < (p - 1)): + err = xmpp.Error(response, xmpp.ERR_FEATURE_NOT_IMPLEMENTED) + err.T.error.T.text.setData("invalid DH value 'e'") + self.send(err) + self.status = None + return k = self.sha256(self.encode_mpi(self.powmod(e, self.y, p))) @@ -606,8 +605,11 @@ class EncryptedStanzaSession(StanzaSession): m_a_calculated = self.hmac(self.km_o, self.encode_mpi(self.c_o) + id_a) if m_a_calculated != m_a: - # XXX return - pass + err = xmpp.Error(response, xmpp.ERR_FEATURE_NOT_IMPLEMENTED) + err.T.error.T.text.setData('calculated m_a differs from received m_a') + self.send(err) + self.status = None + return mac_a = self.decrypt(id_a) @@ -617,10 +619,11 @@ class EncryptedStanzaSession(StanzaSession): mac_a_calculated = self.hmac(self.ks_o, self.n_s + self.n_o + self.encode_mpi(e) + self.form_a + form_a2) if mac_a_calculated != mac_a: - # XXX return - pass - - # TODO: 4.5.3 + err = xmpp.Error(response, xmpp.ERR_FEATURE_NOT_IMPLEMENTED) + err.T.error.T.text.setData('calculated mac_a differs from received mac_a') + self.send(err) + self.status = None + return # 4.5.4 generating bob's final session keys self.srs = '' @@ -697,18 +700,32 @@ class EncryptedStanzaSession(StanzaSession): #4.6.2 Verifying Bob's Identity + m_b = base64.b64decode(form['mac']) id_b = base64.b64decode(form['identity']) - m_b = self.hmac(self.encode_mpi(self.c_o) + id_b, self.km_o) + m_b_calculated = self.hmac(self.encode_mpi(self.c_o) + id_b, self.km_o) + + if m_b_calculated != m_b: + err = xmpp.Error(response, xmpp.ERR_FEATURE_NOT_IMPLEMENTED) + err.T.error.T.text.setData('calculated m_b differs from received m_b') + self.send(err) + self.status = None + return mac_b = self.decrypt(id_b) form_b2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) - mac_b = self.hmac(self.n_s + self.n_o + self.encode_mpi(self.d) + self.form_b + form_b2, self.ks_o) + mac_b_calculated = self.hmac(self.n_s + self.n_o + self.encode_mpi(self.d) + self.form_b + form_b2, self.ks_o) + + if mac_b_calculated != mac_b: + err = xmpp.Error(response, xmpp.ERR_FEATURE_NOT_IMPLEMENTED) + err.T.error.T.text.setData('calculated mac_b differs from received mac_b') + self.send(err) + self.status = None + return # 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 @@ -721,9 +738,8 @@ class EncryptedStanzaSession(StanzaSession): # minimum number of bytes needed to represent that range bytes = int(math.ceil(math.log(top - bottom, 256))) - # FIXME: use a real PRNG # in retrospect, this is horribly inadequate. - return (self.decode_mpi(os.urandom(bytes)) % (top - bottom)) + bottom + return (self.decode_mpi(self.random_bytes(bytes)) % (top - bottom)) + bottom def make_dhhash(self, modp): p = dh.primes[modp] From 8af883e8521892e2e8e9e1dcb590b7a69a527b98 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Thu, 12 Jul 2007 06:25:05 +0000 Subject: [PATCH 25/40] refactored and corrected identity testing, prompt user when a session is initiated by an unsubscribed jid --- src/common/exceptions.py | 10 ++++- src/common/stanza_session.py | 62 +++++++++++--------------- src/gajim.py | 86 ++++++++++++++++++++++-------------- 3 files changed, 88 insertions(+), 70 deletions(-) diff --git a/src/common/exceptions.py b/src/common/exceptions.py index bceef79a4..6aa5b5fa4 100644 --- a/src/common/exceptions.py +++ b/src/common/exceptions.py @@ -45,8 +45,16 @@ class SessionBusNotPresent(Exception): def __str__(self): return _('Session bus is not available.\nTry reading http://trac.gajim.org/wiki/GajimDBus') +class NegotiationError(Exception): + '''A session negotiation failed''' + pass + +class DecryptionError(Exception): + '''A message couldn't be decrypted into usable XML''' + pass + class GajimGeneralException(Exception): - '''This exception ir our general exception''' + '''This exception is our general exception''' def __init__(self, text=''): Exception.__init__(self) self.text = text diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index db90339a0..0c19305ca 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -2,6 +2,7 @@ import gajim from common import xmpp from common import helpers +from common import exceptions import random import string @@ -47,13 +48,14 @@ class StanzaSession(object): return "".join([random.choice(string.letters) for x in xrange(0,32)]) def send(self, msg): - if self.thread_id: + if self.thread_id and isinstance(msg, xmpp.Message): msg.setThread(self.thread_id) msg.setAttr('to', self.jid) self.conn.send_stanza(msg) - - self.last_send = time.time() + + if isinstance(msg, xmpp.Message): + self.last_send = time.time() def reject_negotiation(self, body = None): msg = xmpp.Message() @@ -291,7 +293,7 @@ class EncryptedStanzaSession(StanzaSession): try: parsed = xmpp.Node(node='' + plaintext + '') except: - raise DecryptionError + raise exceptions.DecryptionError for child in parsed.getChildren(): stanza.addChild(node=child) @@ -446,7 +448,7 @@ class EncryptedStanzaSession(StanzaSession): self.n_o = base64.b64decode(form['my_nonce']) dhhashes = form.getField('dhhashes').getValues() - self.He = dhhashes[group_order].encode("utf8") + self.He = base64.b64decode(dhhashes[group_order].encode("utf8")) bytes = int(self.n / 8) @@ -586,13 +588,8 @@ class EncryptedStanzaSession(StanzaSession): e = self.decode_mpi(base64.b64decode(form['dhkeys'])) p = dh.primes[self.modp] - if (not self.sha256(self.encode_mpi(e)) == self.He): or \ - (not e > 1 and e < (p - 1)): - err = xmpp.Error(response, xmpp.ERR_FEATURE_NOT_IMPLEMENTED) - err.T.error.T.text.setData("invalid DH value 'e'") - self.send(err) - self.status = None - return + if (self.sha256(self.encode_mpi(e)) != self.He) or (not 1 < e < (p - 1)): + raise exceptions.NegotiationError, "invalid DH value 'e'" k = self.sha256(self.encode_mpi(self.powmod(e, self.y, p))) @@ -605,11 +602,7 @@ class EncryptedStanzaSession(StanzaSession): m_a_calculated = self.hmac(self.km_o, self.encode_mpi(self.c_o) + id_a) if m_a_calculated != m_a: - err = xmpp.Error(response, xmpp.ERR_FEATURE_NOT_IMPLEMENTED) - err.T.error.T.text.setData('calculated m_a differs from received m_a') - self.send(err) - self.status = None - return + raise exceptions.NegotiationError, 'calculated m_a differs from received m_a' mac_a = self.decrypt(id_a) @@ -619,11 +612,7 @@ class EncryptedStanzaSession(StanzaSession): mac_a_calculated = self.hmac(self.ks_o, self.n_s + self.n_o + self.encode_mpi(e) + self.form_a + form_a2) if mac_a_calculated != mac_a: - err = xmpp.Error(response, xmpp.ERR_FEATURE_NOT_IMPLEMENTED) - err.T.error.T.text.setData('calculated mac_a differs from received mac_a') - self.send(err) - self.status = None - return + raise exceptions.NegotiationError, 'calculated mac_a differs from received mac_a' # 4.5.4 generating bob's final session keys self.srs = '' @@ -653,7 +642,8 @@ class EncryptedStanzaSession(StanzaSession): form_b2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren())) old_c_s = self.c_s - mac_b = self.hmac(self.n_o + self.n_s + self.encode_mpi(self.d) + self.form_b + form_b2, self.ks_s) + + mac_b = self.hmac(self.ks_s, self.n_o + self.n_s + self.encode_mpi(self.d) + self.form_b + form_b2) id_b = self.encrypt(mac_b) m_b = self.hmac(self.km_s, self.encode_mpi(old_c_s) + id_b) @@ -703,27 +693,20 @@ class EncryptedStanzaSession(StanzaSession): m_b = base64.b64decode(form['mac']) id_b = base64.b64decode(form['identity']) - m_b_calculated = self.hmac(self.encode_mpi(self.c_o) + id_b, self.km_o) + m_b_calculated = self.hmac(self.km_o, self.encode_mpi(self.c_o) + id_b) if m_b_calculated != m_b: - err = xmpp.Error(response, xmpp.ERR_FEATURE_NOT_IMPLEMENTED) - err.T.error.T.text.setData('calculated m_b differs from received m_b') - self.send(err) - self.status = None - return + raise exceptions.NegotiationError, 'calculated m_b differs from received m_b' mac_b = self.decrypt(id_b) - form_b2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) + macable_children = filter(lambda x: x.getVar() not in ('mac', 'identity'), form.getChildren()) + form_b2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), macable_children)) - mac_b_calculated = self.hmac(self.n_s + self.n_o + self.encode_mpi(self.d) + self.form_b + form_b2, self.ks_o) + mac_b_calculated = self.hmac(self.ks_o, self.n_s + self.n_o + self.encode_mpi(self.d) + self.form_b + form_b2) if mac_b_calculated != mac_b: - err = xmpp.Error(response, xmpp.ERR_FEATURE_NOT_IMPLEMENTED) - err.T.error.T.text.setData('calculated mac_b differs from received mac_b') - self.send(err) - self.status = None - return + raise exceptions.NegotiationError, 'calculated mac_b differs from received mac_b' # Note: If Alice discovers an error then she SHOULD ignore any encrypted content she received in the stanza. @@ -782,6 +765,13 @@ class EncryptedStanzaSession(StanzaSession): self.enable_encryption = False + def fail_bad_negotiation(self, reason): + '''they've tried to feed us a bogus value, send an error and cancel everything.''' + err = xmpp.Error(xmpp.Message(), xmpp.ERR_FEATURE_NOT_IMPLEMENTED) + err.T.error.T.text.setData(reason) + self.send(err) + self.status = None + def is_loggable(self): name = self.conn.name no_log_for = gajim.config.get_per('accounts', name, 'no_log_for') diff --git a/src/gajim.py b/src/gajim.py index 9f0e8fe96..537436776 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1669,38 +1669,50 @@ class Interface: # bob responds if form.getType() == 'form' and u'e2e' in \ map(lambda x: x[1], form.getField('security').getOptions()): - contact = gajim.contacts.get_contact(account, jid.getStripped(), jid.getResource()) + def continue_with_negotiation(*args): + if len(args): + self.dialog.destroy() - if gajim.SHOW_LIST[gajim.connections[account].connected] == 'invisible' or \ - contact.sub not in ('from', 'both'): + negotiated, not_acceptable, ask_user = session.verify_options_bob(form) + + if ask_user: + def accept_nondefault_options(widget): + self.dialog.destroy() + negotiated.update(ask_user) + session.respond_e2e_bob(form, negotiated, not_acceptable) + + def reject_nondefault_options(widget): + self.dialog.destroy() + for key in ask_user.keys(): + not_acceptable.append(key) + session.respond_e2e_bob(form, negotiated, not_acceptable) + + self.dialog = dialogs.YesNoDialog(_('Confirm these session options'), + _('''The remote client wants to negotiate an session with these features: + + %s + + Are these options acceptable?''') % (negotiation.describe_features(ask_user)), + on_response_yes = accept_nondefault_options, + on_response_no = reject_nondefault_options) + else: + session.respond_e2e_bob(form, negotiated, not_acceptable) + + def ignore_negotiation(widget): + self.dialog.destroy() return - - 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) - session.respond_e2e_bob(form, negotiated, not_acceptable) - - dialog.destroy() - - dialog = dialogs.YesNoDialog(_('Confirm these session options'), - _('''The remote client wants to negotiate an session with these features: - -%s - -Are these options acceptable?''') % (negotiation.describe_features(ask_user)), - on_response_yes = accept_nondefault_options, - on_response_no = reject_nondefault_options) + + contact = gajim.contacts.get_contact_with_highest_priority(account, str(jid)) + + if gajim.SHOW_LIST[gajim.connections[account].connected] == 'invisible' or not contact or\ + contact.sub not in ('from', 'both'): + self.dialog = dialogs.YesNoDialog(_('Start session?'), + _('''%s would like to start a session with you. Should I respond?''') % jid, + on_response_yes = continue_with_negotiation, + on_response_no = ignore_negotiation, + ) else: - session.respond_e2e_bob(form, negotiated, not_acceptable) + continue_with_negotiation() return @@ -1727,16 +1739,24 @@ Are these options acceptable?''') % (negotiation.describe_features(ask_user)), 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.accept_e2e_bob(form) - 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.final_steps_alice(form) + try: + session.final_steps_alice(form) + except exceptions.NegotiationError, details: + session.fail_bad_negotiation(details) + return if form.getField('terminate'): From 9d04cd0a82a89452ab4e00579429ef81ce2dcc55 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Fri, 13 Jul 2007 22:52:23 +0000 Subject: [PATCH 26/40] a file for pickling retained secrets --- src/common/check_paths.py | 12 ++++++++++++ src/common/configpaths.py | 4 ++-- src/common/gajim.py | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/common/check_paths.py b/src/common/check_paths.py index 600855aab..3dfefe501 100644 --- a/src/common/check_paths.py +++ b/src/common/check_paths.py @@ -20,6 +20,8 @@ import stat from common import gajim import logger +import pickle + # DO NOT MOVE ABOVE OF import gajim try: import sqlite3 as sqlite # python 2.5 @@ -84,6 +86,7 @@ def check_and_possibly_create_paths(): LOG_DB_PATH = logger.LOG_DB_PATH VCARD_PATH = gajim.VCARD_PATH AVATAR_PATH = gajim.AVATAR_PATH + SECRETS_PATH = gajim.SECRETS_PATH dot_gajim = os.path.dirname(VCARD_PATH) if os.path.isfile(dot_gajim): print _('%s is a file but it should be a directory') % dot_gajim @@ -115,6 +118,13 @@ def check_and_possibly_create_paths(): print _('%s is a directory but should be a file') % LOG_DB_PATH print _('Gajim will now exit') sys.exit() + + if not os.path.exists(SECRETS_PATH): + pickle.dump({}, SECRETS_PATH) + elif os.path.isdir(SECRETS_PATH): + print _('%s is a directory but should be a file') % SECRETS_PATH + print _('Gajim will now exit') + sys.exit() else: # dot_gajim doesn't exist if dot_gajim: # is '' on win9x so avoid that @@ -126,6 +136,8 @@ def check_and_possibly_create_paths(): if not os.path.isfile(LOG_DB_PATH): create_log_db() gajim.logger.init_vars() + if not os.path.isfile(SECRETS_PATH): + pickle.dump({}, SECRETS_PATH) def create_path(directory): print _('creating %s directory') % directory diff --git a/src/common/configpaths.py b/src/common/configpaths.py index f7e09a686..9350903c3 100644 --- a/src/common/configpaths.py +++ b/src/common/configpaths.py @@ -77,8 +77,8 @@ def init(): paths = ConfigPaths() # LOG is deprecated - k = ( 'LOG', 'LOG_DB', 'VCARD', 'AVATAR', 'MY_EMOTS' ) - v = (u'logs', u'logs.db', u'vcards', u'avatars', u'emoticons') + k = ( 'LOG', 'LOG_DB', 'VCARD', 'AVATAR', 'MY_EMOTS', 'SECRETS' ) + v = (u'logs', u'logs.db', u'vcards', u'avatars', u'emoticons', u'secrets') if os.name == 'nt': v = map(lambda x: x.capitalize(), v) diff --git a/src/common/gajim.py b/src/common/gajim.py index fc109e8d0..cd7715592 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -71,6 +71,7 @@ LOGPATH = gajimpaths['LOG'] # deprecated VCARD_PATH = gajimpaths['VCARD'] AVATAR_PATH = gajimpaths['AVATAR'] MY_EMOTS_PATH = gajimpaths['MY_EMOTS'] +SECRETS_PATH = gajimpaths['SECRETS'] TMP = gajimpaths['TMP'] DATA_DIR = gajimpaths['DATA'] HOME_DIR = gajimpaths['HOME'] From 0ae43eab4e8c0f9dd16f261a078c9211ffe670ff Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Tue, 17 Jul 2007 08:08:01 +0000 Subject: [PATCH 27/40] per-profile srs storage --- src/common/check_paths.py | 12 ------------ src/common/configpaths.py | 8 ++++++-- src/common/gajim.py | 1 - 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/common/check_paths.py b/src/common/check_paths.py index 3dfefe501..dc766563f 100644 --- a/src/common/check_paths.py +++ b/src/common/check_paths.py @@ -20,8 +20,6 @@ import stat from common import gajim import logger -import pickle - # DO NOT MOVE ABOVE OF import gajim try: import sqlite3 as sqlite # python 2.5 @@ -86,7 +84,6 @@ def check_and_possibly_create_paths(): LOG_DB_PATH = logger.LOG_DB_PATH VCARD_PATH = gajim.VCARD_PATH AVATAR_PATH = gajim.AVATAR_PATH - SECRETS_PATH = gajim.SECRETS_PATH dot_gajim = os.path.dirname(VCARD_PATH) if os.path.isfile(dot_gajim): print _('%s is a file but it should be a directory') % dot_gajim @@ -119,13 +116,6 @@ def check_and_possibly_create_paths(): print _('Gajim will now exit') sys.exit() - if not os.path.exists(SECRETS_PATH): - pickle.dump({}, SECRETS_PATH) - elif os.path.isdir(SECRETS_PATH): - print _('%s is a directory but should be a file') % SECRETS_PATH - print _('Gajim will now exit') - sys.exit() - else: # dot_gajim doesn't exist if dot_gajim: # is '' on win9x so avoid that create_path(dot_gajim) @@ -136,8 +126,6 @@ def check_and_possibly_create_paths(): if not os.path.isfile(LOG_DB_PATH): create_log_db() gajim.logger.init_vars() - if not os.path.isfile(SECRETS_PATH): - pickle.dump({}, SECRETS_PATH) def create_path(directory): print _('creating %s directory') % directory diff --git a/src/common/configpaths.py b/src/common/configpaths.py index 9350903c3..a81275cf0 100644 --- a/src/common/configpaths.py +++ b/src/common/configpaths.py @@ -77,8 +77,8 @@ def init(): paths = ConfigPaths() # LOG is deprecated - k = ( 'LOG', 'LOG_DB', 'VCARD', 'AVATAR', 'MY_EMOTS', 'SECRETS' ) - v = (u'logs', u'logs.db', u'vcards', u'avatars', u'emoticons', u'secrets') + k = ( 'LOG', 'LOG_DB', 'VCARD', 'AVATAR', 'MY_EMOTS' ) + v = (u'logs', u'logs.db', u'vcards', u'avatars', u'emoticons') if os.name == 'nt': v = map(lambda x: x.capitalize(), v) @@ -106,13 +106,17 @@ gajimpaths = init() def init_profile(profile, paths=gajimpaths): conffile = windowsify(u'config') pidfile = windowsify(u'gajim') + secretsfile = windowsify(u'secrets') if len(profile) > 0: conffile += u'.' + profile pidfile += u'.' + profile + secretsfile += u'.' + profile + pidfile += u'.pid' paths.add_from_root('CONFIG_FILE', conffile) paths.add_from_root('PID_FILE', pidfile) + paths.add_from_root('SECRETS_FILE', secretsfile) # for k, v in paths.iteritems(): # print "%s: %s" % (repr(k), repr(v)) diff --git a/src/common/gajim.py b/src/common/gajim.py index cd7715592..fc109e8d0 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -71,7 +71,6 @@ LOGPATH = gajimpaths['LOG'] # deprecated VCARD_PATH = gajimpaths['VCARD'] AVATAR_PATH = gajimpaths['AVATAR'] MY_EMOTS_PATH = gajimpaths['MY_EMOTS'] -SECRETS_PATH = gajimpaths['SECRETS'] TMP = gajimpaths['TMP'] DATA_DIR = gajimpaths['DATA'] HOME_DIR = gajimpaths['HOME'] From fdef1c3d72ffafa724930cfc89bf869c9b7eb106 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Tue, 17 Jul 2007 08:08:27 +0000 Subject: [PATCH 28/40] 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() From 8adbac7997fa137411e891cd8a25ec39d609007e Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Tue, 17 Jul 2007 08:26:14 +0000 Subject: [PATCH 29/40] don't prompt to negotiate if we have mutual subscription --- src/gajim.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/gajim.py b/src/gajim.py index 3514273e4..b18d4c792 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1744,9 +1744,11 @@ class Interface: def ignore_negotiation(widget): self.dialog.destroy() return - - contact = gajim.contacts.get_contact_with_highest_priority(account, str(jid)) - + + contact = gajim.contacts.get_contact(account, jid.getStripped(), jid.getResource()) + + # FIXME: shouldn't prompt if i don't have a subscription for remote but + # he has one for me. get_contact() returns None in this case? if gajim.SHOW_LIST[gajim.connections[account].connected] == 'invisible' or not contact or\ contact.sub not in ('from', 'both'): self.dialog = dialogs.YesNoDialog(_('Start session?'), From 59358691361b5688ccac7fd528e4e26628a1289c Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Sun, 29 Jul 2007 21:02:15 +0000 Subject: [PATCH 30/40] test: 1 < d < (p -1) --- src/common/stanza_session.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index b1848091a..08e867163 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -512,7 +512,6 @@ class EncryptedStanzaSession(StanzaSession): 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())) accept = xmpp.Message() @@ -532,6 +531,9 @@ class EncryptedStanzaSession(StanzaSession): e = self.es[mod_p] self.d = self.decode_mpi(base64.b64decode(form['dhkeys'])) + + if (not 1 < self.d < (p - 1)): + raise exceptions.NegotiationError, "invalid DH value 'd'" self.k = self.sha256(self.encode_mpi(self.powmod(self.d, x, p))) From 8d79d320023703a7a7a18a103878231474c89b44 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Tue, 7 Aug 2007 07:21:29 +0000 Subject: [PATCH 31/40] three message negotiation --- src/common/stanza_session.py | 239 +++++++++++++++++++++-------------- 1 file changed, 146 insertions(+), 93 deletions(-) diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 08e867163..1c3d30328 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -305,8 +305,104 @@ class EncryptedStanzaSession(StanzaSession): return ["may", "mustnot"] else: return ["mustnot", "may"] + + def get_shared_secret(self, e, y, p): + if (not 1 < e < (p - 1)): + raise exceptions.NegotiationError, "invalid DH value" - def negotiate_e2e(self): + return self.sha256(self.encode_mpi(self.powmod(e, y, p))) + + def c7lize_mac_id(self, form): + kids = form.getChildren() + macable = filter(lambda x: x.getVar() not in ('mac', 'identity'), kids) + return ''.join(map(lambda el: xmpp.c14n.c14n(el), macable)) + + def verify_alices_identity(self, form, e): + m_a = base64.b64decode(form['mac']) + id_a = base64.b64decode(form['identity']) + + m_a_calculated = self.hmac(self.km_o, self.encode_mpi(self.c_o) + id_a) + + if m_a_calculated != m_a: + raise exceptions.NegotiationError, 'calculated m_a differs from received m_a' + + # 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) + + mac_a = self.decrypt(id_a) + + form_a2 = self.c7lize_mac_id(form) + + mac_a_calculated = self.hmac(self.ks_o, self.n_s + self.n_o + self.encode_mpi(e) + self.form_a + form_a2) + + if mac_a_calculated != mac_a: + raise exceptions.NegotiationError, 'calculated mac_a differs from received mac_a' + + def verify_bobs_identity(self, form, sigmai): + m_b = base64.b64decode(form['mac']) + id_b = base64.b64decode(form['identity']) + + m_b_calculated = self.hmac(self.km_o, self.encode_mpi(self.c_o) + id_b) + + if m_b_calculated != m_b: + raise exceptions.NegotiationError, 'calculated m_b differs from received m_b' + + mac_b = self.decrypt(id_b) + pubkey_b = '' + + c7l_form = self.c7lize_mac_id(form) + + content = self.n_s + self.n_o + self.encode_mpi(self.d) + pubkey_b + + if sigmai: + form_b = c7l_form + content += form_b + else: + form_b2 = c7l_form + content += self.form_b + form_b2 + + mac_b_calculated = self.hmac(self.ks_o, content) + + if mac_b_calculated != mac_b: + raise exceptions.NegotiationError, 'calculated mac_b differs from received mac_b' + + def make_alices_identity(self, form, e): + form_a2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) + + old_c_s = self.c_s + content = self.n_o + self.n_s + self.encode_mpi(e) + self.form_a + form_a2 + + mac_a = self.hmac(self.ks_s, content) + id_a = self.encrypt(mac_a) + + 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': + self.sas = self.sas_28x5(m_a, self.form_b) + + return (xmpp.DataField(name='identity', value=base64.b64encode(id_a)), \ + xmpp.DataField(name='mac', value=base64.b64encode(m_a))) + + def make_bobs_identity(self, form, d): + pubkey_b = '' + + form_b2 = ''.join(map(lambda el: c14n.c14n(el), form.getChildren())) + content = self.n_o + self.n_s + self.encode_mpi(d) + pubkey_b + self.form_b + form_b2 + + old_c_s = self.c_s + mac_b = self.hmac(self.ks_s, content) + id_b = self.encrypt(mac_b) + + m_b = self.hmac(self.km_s, self.encode_mpi(old_c_s) + id_b) + + return (xmpp.DataField(name='identity', value=base64.b64encode(id_b)), \ + xmpp.DataField(name='mac', value=base64.b64encode(m_b))) + + def negotiate_e2e(self, sigmai): self.negotiated = {} request = xmpp.Message() @@ -347,8 +443,8 @@ class EncryptedStanzaSession(StanzaSession): x.addChild(node=xmpp.DataField(name='modp', typ='list-single', options=map(lambda x: [ None, x ], modp_options))) - dhhashes = map(lambda x: self.make_dhhash(x), modp_options) - x.addChild(node=xmpp.DataField(name='dhhashes', typ='hidden', value=dhhashes)) + x.addChild(node=self.make_dhfield(modp_options, sigmai)) + self.sigmai = sigmai self.form_a = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren())) @@ -413,7 +509,7 @@ class EncryptedStanzaSession(StanzaSession): if not 'logging' in ask_user: not_acceptable.append(name) else: - # some things are handled elsewhere, some things are not-implemented + # XXX some things are handled elsewhere, some things are not-implemented pass return (negotiated, not_acceptable, ask_user) @@ -487,8 +583,6 @@ class EncryptedStanzaSession(StanzaSession): # 'Alice Accepts' def verify_options_alice(self, form): -# 1. Verify that the ESession options selected by Bob are acceptable - negotiated = {} ask_user = {} not_acceptable = [] @@ -531,52 +625,42 @@ class EncryptedStanzaSession(StanzaSession): e = self.es[mod_p] self.d = self.decode_mpi(base64.b64decode(form['dhkeys'])) - - if (not 1 < self.d < (p - 1)): - raise exceptions.NegotiationError, "invalid DH value 'd'" - self.k = self.sha256(self.encode_mpi(self.powmod(self.d, x, p))) + self.k = self.get_shared_secret(self.d, x, p) result.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn')) result.addChild(node=xmpp.DataField(name='accept', value='1')) 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)))) - - 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())) self.kc_s, self.km_s, self.ks_s = self.generate_initiator_keys(self.k) - + + if self.sigmai: + self.kc_o, self.km_o, self.ks_o = self.generate_responder_keys(self.k) + self.verify_bobs_identity(form, True) + else: + 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)) + result.addChild(node=xmpp.DataField(name='dhkeys', value=base64.b64encode(self.encode_mpi(e)))) + # MUST securely destroy K unless it will be used later to generate the final shared secret - old_c_s = self.c_s - - mac_a = self.hmac(self.ks_s, self.n_o + self.n_s + self.encode_mpi(e) + self.form_a + form_a2) - id_a = self.encrypt(mac_a) - - 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': - self.sas = 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))) + for datafield in self.make_alices_identity(result, e): + result.addChild(node=datafield) feature.addChild(node=result) self.send(accept) - - self.status = 'identified-alice' + + if self.sigmai: + self.status = 'active' + self.enable_encryption = True + else: + self.status = 'identified-alice' # 4.5 esession accept (bob) def accept_e2e_bob(self, form): @@ -594,31 +678,13 @@ class EncryptedStanzaSession(StanzaSession): e = self.decode_mpi(base64.b64decode(form['dhkeys'])) p = dh.primes[self.modp] - if (self.sha256(self.encode_mpi(e)) != self.He) or (not 1 < e < (p - 1)): - raise exceptions.NegotiationError, "invalid DH value 'e'" - - k = self.sha256(self.encode_mpi(self.powmod(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) # 4.5.2 verifying alice's identity - id_a = base64.b64decode(form['identity']) - m_a = base64.b64decode(form['mac']) - m_a_calculated = self.hmac(self.km_o, self.encode_mpi(self.c_o) + id_a) - - if m_a_calculated != m_a: - raise exceptions.NegotiationError, 'calculated m_a differs from received m_a' - - mac_a = self.decrypt(id_a) - - macable_children = filter(lambda x: x.getVar() not in ('mac', 'identity'), form.getChildren()) - form_a2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), macable_children)) - - mac_a_calculated = self.hmac(self.ks_o, self.n_s + self.n_o + self.encode_mpi(e) + self.form_a + form_a2) - - if mac_a_calculated != mac_a: - raise exceptions.NegotiationError, 'calculated mac_a differs from received mac_a' + self.verify_alices_identity(form) # 4.5.4 generating bob's final session keys @@ -635,11 +701,6 @@ class EncryptedStanzaSession(StanzaSession): # 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 + srs + oss) # XXX I can skip generating ks_o here @@ -704,24 +765,7 @@ class EncryptedStanzaSession(StanzaSession): # 4.6.2 Verifying Bob's Identity - m_b = base64.b64decode(form['mac']) - id_b = base64.b64decode(form['identity']) - - m_b_calculated = self.hmac(self.km_o, self.encode_mpi(self.c_o) + id_b) - - if m_b_calculated != m_b: - raise exceptions.NegotiationError, 'calculated m_b differs from received m_b' - - mac_b = self.decrypt(id_b) - - macable_children = filter(lambda x: x.getVar() not in ('mac', 'identity'), form.getChildren()) - form_b2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), macable_children)) - - mac_b_calculated = self.hmac(self.ks_o, self.n_s + self.n_o + self.encode_mpi(self.d) + self.form_b + form_b2) - - if mac_b_calculated != mac_b: - raise exceptions.NegotiationError, 'calculated mac_b differs from received mac_b' - + self.verify_bobs_identity(form, False) # Note: If Alice discovers an error then she SHOULD ignore any encrypted content she received in the stanza. if self.negotiated['logging'] == 'mustnot': @@ -751,21 +795,30 @@ class EncryptedStanzaSession(StanzaSession): # in retrospect, this is horribly inadequate. return (self.decode_mpi(self.random_bytes(bytes)) % (top - bottom)) + bottom - def make_dhhash(self, modp): - p = dh.primes[modp] - g = dh.generators[modp] + def make_dhfield(self, modp_options, sigmai): + dhs = [] - x = self.srand(2 ** (2 * self.n - 1), p - 1) + for modp in modp_options: + p = dh.primes[modp] + g = dh.generators[modp] - # XXX this may be a source of performance issues - e = self.powmod(g, x, p) + x = self.srand(2 ** (2 * self.n - 1), p - 1) - self.xes[modp] = x - self.es[modp] = e + # XXX this may be a source of performance issues + e = self.powmod(g, x, p) - He = self.sha256(self.encode_mpi(e)) + self.xes[modp] = x + self.es[modp] = e - return base64.b64encode(He) + if sigmai: + dhs.append(base64.b64encode(self.encode_mpi(e))) + name = 'dhkeys' + else: + He = self.sha256(self.encode_mpi(e)) + dhs.append(base64.b64encode(He)) + name = 'dhhashes' + + return xmpp.DataField(name=name, typ='hidden', value=dhs) # a faster version of (base ** exp) % mod # taken from From 4bfe14d3cfce03d77c61d6d257319779d9fe4d75 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Tue, 7 Aug 2007 07:42:31 +0000 Subject: [PATCH 32/40] prompt with SAS during 3 message negotiation --- src/chat_control.py | 3 ++- src/common/stanza_session.py | 14 +++++++++----- src/gajim.py | 4 ++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index ec04c7bb7..bc21d4599 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1957,7 +1957,8 @@ class ChatControl(ChatControlBase): if not self.session: self.session = gajim.connections[self.account].make_new_session(self.contact.jid) - self.session.negotiate_e2e() + # XXX decide whether to use 4 or 3 message negotiation + self.session.negotiate_e2e(False) def got_connected(self): ChatControlBase.got_connected(self) diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 1c3d30328..d2a2dc4eb 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -330,7 +330,7 @@ class EncryptedStanzaSession(StanzaSession): # if none exists, prompt the user with the SAS if self.sas_algs == 'sas28x5': self.sas = self.sas_28x5(m_a, self.form_b) - + mac_a = self.decrypt(id_a) form_a2 = self.c7lize_mac_id(form) @@ -357,8 +357,8 @@ class EncryptedStanzaSession(StanzaSession): content = self.n_s + self.n_o + self.encode_mpi(self.d) + pubkey_b if sigmai: - form_b = c7l_form - content += form_b + self.form_b = c7l_form + content += self.form_b else: form_b2 = c7l_form content += self.form_b + form_b2 @@ -383,6 +383,9 @@ class EncryptedStanzaSession(StanzaSession): # if none exists, prompt the user with the SAS if self.sas_algs == 'sas28x5': self.sas = self.sas_28x5(m_a, self.form_b) + + if self.sigmai: + self.check_identity() return (xmpp.DataField(name='identity', value=base64.b64encode(id_a)), \ xmpp.DataField(name='mac', value=base64.b64encode(m_a))) @@ -606,8 +609,6 @@ class EncryptedStanzaSession(StanzaSession): self.negotiated = negotiated - self.form_b = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) - accept = xmpp.Message() feature = accept.NT.feature feature.setNamespace(xmpp.NS_FEATURE) @@ -647,6 +648,9 @@ class EncryptedStanzaSession(StanzaSession): rshashes = [base64.b64encode(rshash) for rshash in rshashes] result.addChild(node=xmpp.DataField(name='rshashes', value=rshashes)) result.addChild(node=xmpp.DataField(name='dhkeys', value=base64.b64encode(self.encode_mpi(e)))) + + self.form_b = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) + # MUST securely destroy K unless it will be used later to generate the final shared secret diff --git a/src/gajim.py b/src/gajim.py index b18d4c792..15647a26a 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1765,11 +1765,15 @@ class Interface: elif session.status == 'requested-e2e' and form.getType() == 'submit': negotiated, not_acceptable, ask_user = session.verify_options_alice(form) + if session.sigmai: + session.check_identity = lambda: negotiation.show_sas_dialog(jid, session.sas) + if ask_user: def accept_nondefault_options(widget): dialog.destroy() negotiated.update(ask_user) + session.accept_e2e_alice(form, negotiated) def reject_nondefault_options(widget): From 8675731f8dd61d1d2088c7ed0b9251736e3849e9 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Fri, 17 Aug 2007 08:39:47 +0000 Subject: [PATCH 33/40] refactored identity verification public key authentication --- src/common/stanza_session.py | 185 +++++++++++++++++++++++------------ src/gajim.py | 7 ++ 2 files changed, 131 insertions(+), 61 deletions(-) diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index d2a2dc4eb..dee5b3cf4 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -16,9 +16,12 @@ import xmpp.c14n from Crypto.Cipher import AES from Crypto.Hash import HMAC, SHA256 +from Crypto.PublicKey import RSA import base64 +XmlDsig = 'http://www.w3.org/2000/09/xmldsig#' + class StanzaSession(object): def __init__(self, conn, jid, thread_id, type): self.conn = conn @@ -189,6 +192,11 @@ class EncryptedStanzaSession(StanzaSession): self.c_o = (self.c_o + 1) % (2 ** self.n) return self.encode_mpi_with_padding(self.c_o) + def sign(self, string): + if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'): + hash = self.sha256(string) + return self.encode_mpi(gajim.interface.pubkey.sign(hash, '')[0]) + def encrypt_stanza(self, stanza): encryptable = filter(lambda x: x.getName() not in ('error', 'amp', 'thread'), stanza.getChildren()) @@ -317,84 +325,117 @@ class EncryptedStanzaSession(StanzaSession): macable = filter(lambda x: x.getVar() not in ('mac', 'identity'), kids) return ''.join(map(lambda el: xmpp.c14n.c14n(el), macable)) - def verify_alices_identity(self, form, e): - m_a = base64.b64decode(form['mac']) - id_a = base64.b64decode(form['identity']) + def verify_identity(self, form, dh_i, sigmai, i_o): + m_o = base64.b64decode(form['mac']) + id_o = base64.b64decode(form['identity']) - m_a_calculated = self.hmac(self.km_o, self.encode_mpi(self.c_o) + id_a) + m_o_calculated = self.hmac(self.km_o, self.encode_mpi(self.c_o) + id_o) - if m_a_calculated != m_a: - raise exceptions.NegotiationError, 'calculated m_a differs from received m_a' + if m_o_calculated != m_o: + raise exceptions.NegotiationError, 'calculated m_%s differs from received m_%s' % (i_o, i_o) - # 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) + if i_o == 'a' and self.sas_algs == 'sas28x5': + # XXX not necessary if there's a verified retained secret + self.sas = self.sas_28x5(m_o, self.form_s) - mac_a = self.decrypt(id_a) + if self.negotiated['recv_pubkey']: + plaintext = self.decrypt(id_o) + parsed = xmpp.Node(node='' + plaintext + '') - form_a2 = self.c7lize_mac_id(form) + if self.negotiated['recv_pubkey'] == 'hash': + fingerprint = parsed.getTagData('fingerprint') - mac_a_calculated = self.hmac(self.ks_o, self.n_s + self.n_o + self.encode_mpi(e) + self.form_a + form_a2) + # XXX find stored pubkey or terminate session + raise 'unimplemented' + else: + if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'): + keyvalue = parsed.getTag(name='RSAKeyValue', namespace=XmlDsig) - if mac_a_calculated != mac_a: - raise exceptions.NegotiationError, 'calculated mac_a differs from received mac_a' + n, e = map(lambda x: self.decode_mpi(base64.b64decode(keyvalue.getTagData(x))), ('Modulus', 'Exponent')) + eir_pubkey = RSA.construct((n,long(e))) - def verify_bobs_identity(self, form, sigmai): - m_b = base64.b64decode(form['mac']) - id_b = base64.b64decode(form['identity']) + pubkey_o = xmpp.c14n.c14n(keyvalue) + else: + # XXX DSA, etc. + raise 'unimplemented' - m_b_calculated = self.hmac(self.km_o, self.encode_mpi(self.c_o) + id_b) - - if m_b_calculated != m_b: - raise exceptions.NegotiationError, 'calculated m_b differs from received m_b' - - mac_b = self.decrypt(id_b) - pubkey_b = '' + enc_sig = parsed.getTag(name='SignatureValue', namespace=XmlDsig).getData() + signature = (self.decode_mpi(base64.b64decode(enc_sig)),) + else: + mac_o = self.decrypt(id_o) + pubkey_o = '' c7l_form = self.c7lize_mac_id(form) - content = self.n_s + self.n_o + self.encode_mpi(self.d) + pubkey_b + content = self.n_s + self.n_o + self.encode_mpi(dh_i) + pubkey_o if sigmai: - self.form_b = c7l_form - content += self.form_b + self.form_o = c7l_form + content += self.form_o else: - form_b2 = c7l_form - content += self.form_b + form_b2 + form_o2 = c7l_form + content += self.form_o + form_o2 - mac_b_calculated = self.hmac(self.ks_o, content) + mac_o_calculated = self.hmac(self.ks_o, content) + + if self.negotiated['recv_pubkey']: + hash = self.sha256(mac_o_calculated) - if mac_b_calculated != mac_b: - raise exceptions.NegotiationError, 'calculated mac_b differs from received mac_b' + if not eir_pubkey.verify(hash, signature): + raise exceptions.NegotiationError, 'public key signature verification failed!' + + elif mac_o_calculated != mac_o: + raise exceptions.NegotiationError, 'calculated mac_%s differs from received mac_%s' % (i_o, i_o) def make_alices_identity(self, form, e): - form_a2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) + if self.negotiated['send_pubkey']: + if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'): + fields = (gajim.interface.pubkey.n, gajim.interface.pubkey.e) + + cb_fields = map(lambda f: base64.b64encode(self.encode_mpi(f)), fields) + + pubkey_s = '%s%s' % tuple(cb_fields) + else: + pubkey_s = '' + + form_s2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) old_c_s = self.c_s - content = self.n_o + self.n_s + self.encode_mpi(e) + self.form_a + form_a2 + content = self.n_o + self.n_s + self.encode_mpi(e) + pubkey_s + self.form_s + form_s2 mac_a = self.hmac(self.ks_s, content) - id_a = self.encrypt(mac_a) + + if self.negotiated['send_pubkey']: + signature = self.sign(mac_a) + + sign_s = '%s' % base64.b64encode(signature) + + if self.negotiated['send_pubkey'] == 'hash': + b64ed = base64.b64encode(self.hash(pubkey_s)) + pubkey_s = '%s' % b64ed + + id_a = self.encrypt(pubkey_s + sign_s) + else: + id_a = self.encrypt(mac_a) 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': - self.sas = self.sas_28x5(m_a, self.form_b) + self.sas = self.sas_28x5(m_a, self.form_o) if self.sigmai: self.check_identity() - + return (xmpp.DataField(name='identity', value=base64.b64encode(id_a)), \ xmpp.DataField(name='mac', value=base64.b64encode(m_a))) def make_bobs_identity(self, form, d): pubkey_b = '' - form_b2 = ''.join(map(lambda el: c14n.c14n(el), form.getChildren())) - content = self.n_o + self.n_s + self.encode_mpi(d) + pubkey_b + self.form_b + form_b2 + form_s2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) + content = self.n_o + self.n_s + self.encode_mpi(d) + pubkey_b + self.form_s + form_s2 old_c_s = self.c_s mac_b = self.hmac(self.ks_s, content) @@ -430,13 +471,17 @@ class EncryptedStanzaSession(StanzaSession): # unsupported options: 'iq', 'presence' x.addChild(node=xmpp.DataField(name='stanzas', typ='list-multi', options=['message'])) - x.addChild(node=xmpp.DataField(name='init_pubkey', value='none', typ='hidden')) - x.addChild(node=xmpp.DataField(name='resp_pubkey', value='none', typ='hidden')) + x.addChild(node=xmpp.DataField(name='init_pubkey', options=['none', 'key', 'hash'], typ='list-single')) + + # XXX store key, use hash + x.addChild(node=xmpp.DataField(name='resp_pubkey', options=['none', 'key'], typ='list-single')) + x.addChild(node=xmpp.DataField(name='ver', value='1.0', typ='hidden')) x.addChild(node=xmpp.DataField(name='rekey_freq', value='4294967295', typ='hidden')) x.addChild(node=xmpp.DataField(name='sas_algs', value='sas28x5', typ='hidden')) + x.addChild(node=xmpp.DataField(name='sign_algs', value='http://www.w3.org/2000/09/xmldsig#rsa-sha256', typ='hidden')) self.n_s = self.generate_nonce() @@ -449,7 +494,7 @@ class EncryptedStanzaSession(StanzaSession): x.addChild(node=self.make_dhfield(modp_options, sigmai)) self.sigmai = sigmai - self.form_a = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren())) + self.form_s = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren())) feature.addChild(node=x) @@ -459,7 +504,7 @@ class EncryptedStanzaSession(StanzaSession): # 4.3 esession response (bob) def verify_options_bob(self, form): - negotiated = {} + negotiated = {'recv_pubkey': None, 'send_pubkey': None} not_acceptable = [] ask_user = {} @@ -511,6 +556,19 @@ class EncryptedStanzaSession(StanzaSession): if not 'logging' in ask_user: not_acceptable.append(name) + elif name == 'init_pubkey': + for x in ('key'): + if x in options: + negotiated['recv_pubkey'] = x + break + elif name == 'resp_pubkey': + for x in ('hash', 'key'): + if x in options: + negotiated['send_pubkey'] = x + break + elif name == 'sign_algs': + if (XmlDsig + 'rsa-sha256') in options: + negotiated['sign_algs'] = XmlDsig + 'rsa-sha256' else: # XXX some things are handled elsewhere, some things are not-implemented pass @@ -565,8 +623,8 @@ class EncryptedStanzaSession(StanzaSession): 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), form.getChildren())) - self.form_b = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren())) + self.form_o = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) + self.form_s = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren())) self.status = 'responded-e2e' @@ -597,6 +655,18 @@ class EncryptedStanzaSession(StanzaSession): else: negotiated['logging'] = self.logging_preference()[0] + for r,a in (('recv_pubkey', 'resp_pubkey'), ('send_pubkey', 'init_pubkey')): + negotiated[r] = None + + if a in form.asDict() and form[a] in ('key', 'hash'): + negotiated[r] = form[a] + + if 'sign_algs' in form.asDict(): + if form['sign_algs'] in (XmlDsig + 'rsa-sha256',): + negotiated['sign_algs'] = form['sign_algs'] + else: + not_acceptable.append(form['sign_algs']) + return (negotiated, not_acceptable, ask_user) # 'Alice Accepts', continued @@ -637,7 +707,7 @@ class EncryptedStanzaSession(StanzaSession): if self.sigmai: self.kc_o, self.km_o, self.ks_o = self.generate_responder_keys(self.k) - self.verify_bobs_identity(form, True) + self.verify_identity(form, self.d, True, 'b') else: secrets = gajim.interface.list_secrets(self.conn.name, self.jid.getStripped()) rshashes = [self.hmac(self.n_s, rs) for rs in secrets] @@ -649,7 +719,7 @@ class EncryptedStanzaSession(StanzaSession): result.addChild(node=xmpp.DataField(name='rshashes', value=rshashes)) result.addChild(node=xmpp.DataField(name='dhkeys', value=base64.b64encode(self.encode_mpi(e)))) - self.form_b = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) + self.form_o = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) # MUST securely destroy K unless it will be used later to generate the final shared secret @@ -682,13 +752,15 @@ class EncryptedStanzaSession(StanzaSession): e = self.decode_mpi(base64.b64decode(form['dhkeys'])) p = dh.primes[self.modp] + # XXX return if hash(e) != He + k = self.get_shared_secret(e, self.y, p) self.kc_o, self.km_o, self.ks_o = self.generate_initiator_keys(k) # 4.5.2 verifying alice's identity - self.verify_alices_identity(form) + self.verify_identity(form, e, False, 'a') # 4.5.4 generating bob's final session keys @@ -721,17 +793,8 @@ class EncryptedStanzaSession(StanzaSession): x.addChild(node=xmpp.DataField(name='nonce', value=base64.b64encode(self.n_o))) x.addChild(node=xmpp.DataField(name='srshash', value=base64.b64encode(srshash))) - form_b2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren())) - - old_c_s = self.c_s - - mac_b = self.hmac(self.ks_s, self.n_o + self.n_s + self.encode_mpi(self.d) + self.form_b + form_b2) - id_b = self.encrypt(mac_b) - - m_b = self.hmac(self.km_s, self.encode_mpi(old_c_s) + id_b) - - x.addChild(node=xmpp.DataField(name='identity', value=base64.b64encode(id_b))) - x.addChild(node=xmpp.DataField(name='mac', value=base64.b64encode(m_b))) + for datafield in self.make_bobs_identity(x, self.d): + x.addChild(node=datafield) init.addChild(node=x) @@ -769,7 +832,7 @@ class EncryptedStanzaSession(StanzaSession): # 4.6.2 Verifying Bob's Identity - self.verify_bobs_identity(form, False) + self.verify_identity(form, self.d, False, 'b') # Note: If Alice discovers an error then she SHOULD ignore any encrypted content she received in the stanza. if self.negotiated['logging'] == 'mustnot': diff --git a/src/gajim.py b/src/gajim.py index 15647a26a..59f412b74 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -104,6 +104,7 @@ from chat_control import ChatControlBase from atom_window import AtomWindow import negotiation +import Crypto.PublicKey.RSA from common import exceptions from common.zeroconf import connection_zeroconf @@ -1393,6 +1394,7 @@ class Interface: if os.path.isfile(path_to_original_file): os.remove(path_to_original_file) + # list the retained secrets we have for a local account and a remote jid def list_secrets(self, account, jid): f = open(secrets_filename) @@ -1404,6 +1406,7 @@ class Interface: f.close() return s + # save a new retained secret def save_new_secret(self, account, jid, secret): f = open(secrets_filename, 'r') secrets = pickle.load(f) @@ -2559,6 +2562,10 @@ class Interface: gobject.timeout_add(200, self.process_connections) gobject.timeout_add(500, self.read_sleepy) + # public key for XEP-0116 + # XXX os.urandom is not a cryptographic PRNG + self.pubkey = Crypto.PublicKey.RSA.generate(384, os.urandom) + if __name__ == '__main__': def sigint_cb(num, stack): sys.exit(5) From f93276c1a9762c892324d48e8d41037c79ec906c Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Fri, 17 Aug 2007 09:02:53 +0000 Subject: [PATCH 34/40] refactored make_identity --- src/common/stanza_session.py | 45 ++++++++++++------------------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index dee5b3cf4..baae1431e 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -387,7 +387,7 @@ class EncryptedStanzaSession(StanzaSession): elif mac_o_calculated != mac_o: raise exceptions.NegotiationError, 'calculated mac_%s differs from received mac_%s' % (i_o, i_o) - def make_alices_identity(self, form, e): + def make_identity(self, form, dh_i): if self.negotiated['send_pubkey']: if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'): fields = (gajim.interface.pubkey.n, gajim.interface.pubkey.e) @@ -401,12 +401,12 @@ class EncryptedStanzaSession(StanzaSession): form_s2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) old_c_s = self.c_s - content = self.n_o + self.n_s + self.encode_mpi(e) + pubkey_s + self.form_s + form_s2 + content = self.n_o + self.n_s + self.encode_mpi(dh_i) + pubkey_s + self.form_s + form_s2 - mac_a = self.hmac(self.ks_s, content) + mac_s = self.hmac(self.ks_s, content) if self.negotiated['send_pubkey']: - signature = self.sign(mac_a) + signature = self.sign(mac_s) sign_s = '%s' % base64.b64encode(signature) @@ -414,37 +414,22 @@ class EncryptedStanzaSession(StanzaSession): b64ed = base64.b64encode(self.hash(pubkey_s)) pubkey_s = '%s' % b64ed - id_a = self.encrypt(pubkey_s + sign_s) + id_s = self.encrypt(pubkey_s + sign_s) else: - id_a = self.encrypt(mac_a) + id_s = self.encrypt(mac_s) - m_a = self.hmac(self.km_s, self.encode_mpi(old_c_s) + id_a) + m_s = self.hmac(self.km_s, self.encode_mpi(old_c_s) + id_s) - # 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_o) + if self.status == 'requested-e2e' and self.sas_algs == 'sas28x5': + # we're alice; check for a retained secret + # if none exists, prompt the user with the SAS + self.sas = self.sas_28x5(m_s, self.form_o) if self.sigmai: self.check_identity() - return (xmpp.DataField(name='identity', value=base64.b64encode(id_a)), \ - xmpp.DataField(name='mac', value=base64.b64encode(m_a))) - - def make_bobs_identity(self, form, d): - pubkey_b = '' - - form_s2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) - content = self.n_o + self.n_s + self.encode_mpi(d) + pubkey_b + self.form_s + form_s2 - - old_c_s = self.c_s - mac_b = self.hmac(self.ks_s, content) - id_b = self.encrypt(mac_b) - - m_b = self.hmac(self.km_s, self.encode_mpi(old_c_s) + id_b) - - return (xmpp.DataField(name='identity', value=base64.b64encode(id_b)), \ - xmpp.DataField(name='mac', value=base64.b64encode(m_b))) + return (xmpp.DataField(name='identity', value=base64.b64encode(id_s)), \ + xmpp.DataField(name='mac', value=base64.b64encode(m_s))) def negotiate_e2e(self, sigmai): self.negotiated = {} @@ -724,7 +709,7 @@ class EncryptedStanzaSession(StanzaSession): # MUST securely destroy K unless it will be used later to generate the final shared secret - for datafield in self.make_alices_identity(result, e): + for datafield in self.make_identity(result, e): result.addChild(node=datafield) feature.addChild(node=result) @@ -793,7 +778,7 @@ class EncryptedStanzaSession(StanzaSession): x.addChild(node=xmpp.DataField(name='nonce', value=base64.b64encode(self.n_o))) x.addChild(node=xmpp.DataField(name='srshash', value=base64.b64encode(srshash))) - for datafield in self.make_bobs_identity(x, self.d): + for datafield in self.make_identity(x, self.d): x.addChild(node=datafield) init.addChild(node=x) From e1c4d80e65edca842dcfc57b6f384dc5f00d6cae Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Fri, 17 Aug 2007 17:26:05 +0000 Subject: [PATCH 35/40] notify on begin/end encryption --- src/common/connection_handlers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index dbd00e990..587d80786 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1450,8 +1450,11 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if msg.getTag('init') and msg.getTag('init').namespace == 'http://www.xmpp.org/extensions/xep-0116.html#ns-init': self._InitE2ECB(con, msg, session) + encrypted = False + e2eTag = msg.getTag('c', namespace = common.xmpp.NS_STANZA_CRYPTO) if e2eTag: + encrypted = True msg = session.decrypt_stanza(msg) msgtxt = msg.getBody() @@ -1461,7 +1464,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) - encrypted = False chatstate = None encTag = msg.getTag('x', namespace = common.xmpp.NS_ENCRYPTED) decmsg = '' From 9985b784d5dbf6a043a521bcb5fe995c2f1daed8 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Sat, 18 Aug 2007 08:59:36 +0000 Subject: [PATCH 36/40] bugfixes, send not-acceptable for fields with only unsupported options --- src/common/connection_handlers.py | 3 +++ src/common/stanza_session.py | 30 +++++++++++++++++------------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 587d80786..1d75bc437 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1584,6 +1584,9 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if bare_jid != jid: session = self.find_session(bare_jid, thread_id, type) if session: + if not session.received_thread_id: + thread_id = session.thread_id + self.move_session(bare_jid, thread_id, jid.split("/")[1]) return session diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index baae1431e..bb2785e83 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -48,8 +48,8 @@ class StanzaSession(object): return "".join([random.choice(string.letters) for x in xrange(0,32)]) def send(self, msg): - if self.thread_id and isinstance(msg, xmpp.Message): - msg.setThread(self.thread_id) + if self.thread_id: + msg.NT.thread = self.thread_id msg.setAttr('to', self.jid) self.conn.send_stanza(msg) @@ -530,10 +530,10 @@ class EncryptedStanzaSession(StanzaSession): elif name == 'logging': my_prefs = self.logging_preference() - if my_prefs[0] in options: + if my_prefs[0] in options: # our first choice is offered, select it pref = my_prefs[0] negotiated['logging'] = pref - else: + else: # see if other acceptable choices are offered for pref in my_prefs: if pref in options: ask_user['logging'] = pref @@ -572,7 +572,9 @@ class EncryptedStanzaSession(StanzaSession): x.addChild(node=xmpp.DataField(name='accept', value='true')) for name in negotiated: - x.addChild(node=xmpp.DataField(name=name, value=negotiated[name])) + # some fields are internal and should not be sent + if not name in ('send_pubkey', 'recv_pubkey'): + x.addChild(node=xmpp.DataField(name=name, value=negotiated[name])) self.negotiated = negotiated @@ -616,14 +618,16 @@ class EncryptedStanzaSession(StanzaSession): feature.addChild(node=x) if not_acceptable: - pass -# XXX -# -# -# -# -# -# + response = xmpp.Error(response, xmpp.ERR_NOT_ACCEPTABLE) + + feature = xmpp.Node(xmpp.NS_FEATURE + ' feature') + + for f in not_acceptable: + n = xmpp.Node('field') + n['var'] = f + feature.addChild(node=n) + + response.T.error.addChild(node=feature) self.send(response) From 0230c91e4c852ac8b0a0310d8aa3e70c87b65111 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Mon, 20 Aug 2007 08:16:48 +0000 Subject: [PATCH 37/40] esession bugfixes --- src/chat_control.py | 61 +++++++++++++++++++++++++------ src/common/connection_handlers.py | 18 ++++++--- src/common/stanza_session.py | 20 +++++++--- src/gajim.py | 43 ++++++++++++++++++---- 4 files changed, 111 insertions(+), 31 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index bc21d4599..b22bafddc 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -971,6 +971,9 @@ class ChatControl(ChatControlBase): self.session = session + # does this window have an existing, active esession? + self.esessioned = False + self.status_tooltip = gtk.Tooltips() self.update_ui() # restore previous conversation @@ -1236,13 +1239,13 @@ class ChatControl(ChatControlBase): contact = self.contact + encrypted = bool(self.session) and self.session.enable_encryption + keyID = '' - encrypted = False if self.xml.get_widget('gpg_togglebutton').get_active(): keyID = contact.keyID encrypted = True - chatstates_on = gajim.config.get('outgoing_chat_state_notifications') != \ 'disabled' composing_jep = contact.composing_jep @@ -1352,18 +1355,46 @@ class ChatControl(ChatControlBase): kind = 'info' name = '' else: - ec = gajim.encrypted_chats[self.account] - if encrypted and jid not in ec: - msg = _('Encryption enabled') + # ESessions + if self.session.enable_encryption: + if not self.esessioned: + msg = _('Encryption enabled') + ChatControlBase.print_conversation_line(self, msg, + 'status', '', tim) + + if self.session.loggable: + msg = _('Session WILL be logged') + else: + msg = _('Session WILL NOT be logged') + + ChatControlBase.print_conversation_line(self, msg, + 'status', '', tim) + + self.esessioned = True + elif not encrypted: + msg = _('The following message was NOT encrypted') + ChatControlBase.print_conversation_line(self, msg, + 'status', '', tim) + elif self.esessioned: + msg = _('Encryption disabled') ChatControlBase.print_conversation_line(self, msg, 'status', '', tim) - ec.append(jid) - elif not encrypted and jid in ec: - msg = _('Encryption disabled') - ChatControlBase.print_conversation_line(self, msg, - 'status', '', tim) - ec.remove(jid) - self.xml.get_widget('gpg_togglebutton').set_active(encrypted) + self.esessioned = False + else: + # GPG encryption + ec = gajim.encrypted_chats[self.account] + if encrypted and jid not in ec: + msg = _('Encryption enabled') + ChatControlBase.print_conversation_line(self, msg, + 'status', '', tim) + ec.append(jid) + elif not encrypted and jid in ec: + msg = _('Encryption disabled') + ChatControlBase.print_conversation_line(self, msg, + 'status', '', tim) + ec.remove(jid) + self.xml.get_widget('gpg_togglebutton').set_active(encrypted) + if not frm: kind = 'incoming' name = contact.get_shown_name() @@ -1949,9 +1980,15 @@ class ChatControl(ChatControlBase): if self.session and self.session.enable_encryption: self.session.terminate_e2e() + msg = _('Encryption disabled') + ChatControlBase.print_conversation_line(self, msg, + 'status', '', None) + self.esessioned = False + 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: if not self.session: diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 1d75bc437..035ca00bb 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1451,18 +1451,22 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, self._InitE2ECB(con, msg, session) encrypted = False + tim = msg.getTimestamp() + tim = time.strptime(tim, '%Y%m%dT%H:%M:%S') + tim = time.localtime(timegm(tim)) - e2eTag = msg.getTag('c', namespace = common.xmpp.NS_STANZA_CRYPTO) - if e2eTag: + e2e_tag = msg.getTag('c', namespace = common.xmpp.NS_STANZA_CRYPTO) + if e2e_tag: encrypted = True - msg = session.decrypt_stanza(msg) + + try: + msg = session.decrypt_stanza(msg) + except: + self.dispatch('FAILED_DECRYPT', (frm, tim)) msgtxt = msg.getBody() msghtml = msg.getXHTML() subject = msg.getSubject() # if not there, it's None - tim = msg.getTimestamp() - tim = time.strptime(tim, '%Y%m%dT%H:%M:%S') - tim = time.localtime(timegm(tim)) jid = helpers.get_jid_from_iq(msg) chatstate = None encTag = msg.getTag('x', namespace = common.xmpp.NS_ENCRYPTED) @@ -1573,6 +1577,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, def get_session(self, jid, thread_id, type): '''returns an existing session between this connection and 'jid', returns a new one if none exist.''' + print repr(self.sessions) + session = self.find_session(jid, thread_id, type) if session: diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index bb2785e83..09f45e754 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -289,7 +289,7 @@ class EncryptedStanzaSession(StanzaSession): calculated_mac = self.hmac(self.km_o, macable + self.encode_mpi_with_padding(self.c_o)) if not calculated_mac == received_mac: - raise 'bad signature (%s != %s)' % (repr(received_mac), repr(calculated_mac)) + raise exceptions.DecryptionError, 'bad signature' m_final = base64.b64decode(c.getTagData('data')) m_compressed = self.decrypt(m_final) @@ -298,7 +298,7 @@ class EncryptedStanzaSession(StanzaSession): try: parsed = xmpp.Node(node='' + plaintext + '') except: - raise exceptions.DecryptionError + raise exceptions.DecryptionError, 'decrypted not parseable as XML' for child in parsed.getChildren(): stanza.addChild(node=child) @@ -377,12 +377,12 @@ class EncryptedStanzaSession(StanzaSession): content += self.form_o + form_o2 mac_o_calculated = self.hmac(self.ks_o, content) - + if self.negotiated['recv_pubkey']: hash = self.sha256(mac_o_calculated) if not eir_pubkey.verify(hash, signature): - raise exceptions.NegotiationError, 'public key signature verification failed!' + raise exceptions.NegotiationError, 'public key signature verification failed!' elif mac_o_calculated != mac_o: raise exceptions.NegotiationError, 'calculated mac_%s differs from received mac_%s' % (i_o, i_o) @@ -589,7 +589,7 @@ class EncryptedStanzaSession(StanzaSession): self.n_o = base64.b64decode(form['my_nonce']) dhhashes = form.getField('dhhashes').getValues() - self.He = base64.b64decode(dhhashes[group_order].encode("utf8")) + self.negotiated['He'] = base64.b64decode(dhhashes[group_order].encode("utf8")) bytes = int(self.n / 8) @@ -741,7 +741,8 @@ class EncryptedStanzaSession(StanzaSession): e = self.decode_mpi(base64.b64decode(form['dhkeys'])) p = dh.primes[self.modp] - # XXX return if hash(e) != He + if self.sha256(self.encode_mpi(e)) != self.negotiated['He']: + raise exceptions.NegotiationError, 'SHA256(e) != He' k = self.get_shared_secret(e, self.y, p) @@ -903,10 +904,17 @@ class EncryptedStanzaSession(StanzaSession): def fail_bad_negotiation(self, reason): '''they've tried to feed us a bogus value, send an error and cancel everything.''' + err = xmpp.Error(xmpp.Message(), xmpp.ERR_FEATURE_NOT_IMPLEMENTED) err.T.error.T.text.setData(reason) self.send(err) + self.status = None + self.enable_encryption = False + + # this prevents the MAC check on decryption from succeeding, + # preventing falsified messages from going through. + self.km_o = '' def is_loggable(self): account = self.conn.name diff --git a/src/gajim.py b/src/gajim.py index 59f412b74..3c50acfba 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1701,6 +1701,15 @@ class Interface: atom_entry, = data AtomWindow.newAtomEntry(atom_entry) + def handle_event_failed_decrypt(self, account, data): + jid, tim = data + + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + ctrl.print_conversation_line('Unable to decrypt message from %s\nIt may have been tampered with.' % (jid), 'status', '', tim) + else: + print 'failed decrypt, unable to find a control to notify you in.' + def handle_session_negotiation(self, account, data): jid, session, form = data @@ -1713,12 +1722,25 @@ class Interface: # encrypted session states. these are described in stanza_session.py # bob responds - if form.getType() == 'form' and u'e2e' in \ - map(lambda x: x[1], form.getField('security').getOptions()): + if form.getType() == 'form' and 'security' in form.asDict(): def continue_with_negotiation(*args): if len(args): self.dialog.destroy() + # we don't support 3-message negotiation as the responder + if 'dhkeys' in form.asDict(): + err = xmpp.Error(xmpp.Message(), xmpp.ERR_FEATURE_NOT_IMPLEMENTED) + + feature = xmpp.Node(xmpp.NS_FEATURE + ' feature') + field = xmpp.Node('field') + field['var'] = 'dhkeys' + + feature.addChild(node=field) + err.addChild(node=feature) + + session.send(err) + return + negotiated, not_acceptable, ask_user = session.verify_options_bob(form) if ask_user: @@ -1776,8 +1798,11 @@ class Interface: dialog.destroy() negotiated.update(ask_user) - - session.accept_e2e_alice(form, negotiated) + + try: + session.accept_e2e_alice(form, negotiated) + except exceptions.NegotiationError, details: + session.fail_bad_negotiation(details) def reject_nondefault_options(widget): session.reject_negotiation() @@ -1788,7 +1813,10 @@ class Interface: on_response_yes = accept_nondefault_options, on_response_no = reject_nondefault_options) else: - session.accept_e2e_alice(form, negotiated) + try: + session.accept_e2e_alice(form, negotiated) + except exceptions.NegotiationError, details: + session.fail_bad_negotiation(details) return elif session.status == 'responded-e2e' and form.getType() == 'result': @@ -1802,7 +1830,7 @@ class Interface: 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: @@ -1819,7 +1847,7 @@ class Interface: ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account) if ctrl: - ctrl.session = gajim.connections[self.account].make_new_session(str(jid)) + ctrl.session = gajim.connections[account].make_new_session(str(jid)) return @@ -2266,6 +2294,7 @@ class Interface: 'SIGNED_IN': self.handle_event_signed_in, 'METACONTACTS': self.handle_event_metacontacts, 'ATOM_ENTRY': self.handle_atom_entry, + 'FAILED_DECRYPT': self.handle_event_failed_decrypt, 'PRIVACY_LISTS_RECEIVED': self.handle_event_privacy_lists_received, 'PRIVACY_LIST_RECEIVED': self.handle_event_privacy_list_received, 'PRIVACY_LISTS_ACTIVE_DEFAULT': \ From 59b7e83fd5cc7e61e13dc3c47609fa8a102cc966 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Mon, 20 Aug 2007 08:19:29 +0000 Subject: [PATCH 38/40] removed buggy anti-presence-leak test --- src/gajim.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/gajim.py b/src/gajim.py index 3c50acfba..b3770bc9c 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1770,19 +1770,7 @@ class Interface: self.dialog.destroy() return - contact = gajim.contacts.get_contact(account, jid.getStripped(), jid.getResource()) - - # FIXME: shouldn't prompt if i don't have a subscription for remote but - # he has one for me. get_contact() returns None in this case? - if gajim.SHOW_LIST[gajim.connections[account].connected] == 'invisible' or not contact or\ - contact.sub not in ('from', 'both'): - self.dialog = dialogs.YesNoDialog(_('Start session?'), - _('''%s would like to start a session with you. Should I respond?''') % jid, - on_response_yes = continue_with_negotiation, - on_response_no = ignore_negotiation, - ) - else: - continue_with_negotiation() + continue_with_negotiation() return From 88e49ffa46c7d4acf31de7f7944afe216655bc34 Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Mon, 20 Aug 2007 17:33:12 +0000 Subject: [PATCH 39/40] don't create uneccessary sessions (eg. for groupchat messages), bugfix for pms --- src/chat_control.py | 2 +- src/common/connection_handlers.py | 7 +++---- src/common/stanza_session.py | 1 - src/gajim.py | 2 +- src/groupchat_control.py | 12 ++++++------ src/roster_window.py | 10 ++++++---- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index b22bafddc..777245d92 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1356,7 +1356,7 @@ class ChatControl(ChatControlBase): name = '' else: # ESessions - if self.session.enable_encryption: + if self.session and self.session.enable_encryption: if not self.esessioned: msg = _('Encryption enabled') ChatControlBase.print_conversation_line(self, msg, diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 035ca00bb..77e493ca2 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1429,7 +1429,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if not mtype: mtype = 'normal' - session = self.get_session(frm, thread_id, mtype) + if not mtype == 'groupchat': + session = self.get_session(frm, thread_id, mtype) if thread_id and not session.received_thread_id: session.received_thread_id = True @@ -1545,7 +1546,7 @@ 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 session.is_loggable() and not int(float(time.mktime(tim)))\ + if not int(float(time.mktime(tim)))\ <= self.last_history_line[jid] and msgtxt: gajim.logger.write('gc_msg', frm, msgtxt, tim = tim) return @@ -1577,8 +1578,6 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, def get_session(self, jid, thread_id, type): '''returns an existing session between this connection and 'jid', returns a new one if none exist.''' - print repr(self.sessions) - session = self.find_session(jid, thread_id, type) if session: diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 09f45e754..6af56d203 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -769,7 +769,6 @@ class EncryptedStanzaSession(StanzaSession): 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) diff --git a/src/gajim.py b/src/gajim.py index b3770bc9c..6eaea849f 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -776,7 +776,7 @@ class Interface: if pm: nickname = resource groupchat_control.on_private_message(nickname, message, array[2], - xhtml) + xhtml, session) else: # array: (jid, msg, time, encrypted, msg_type, subject) if encrypted: diff --git a/src/groupchat_control.py b/src/groupchat_control.py index bc7b3e7a7..e94c525ce 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -97,12 +97,12 @@ def tree_cell_data_func(column, renderer, model, iter, tv=None): class PrivateChatControl(ChatControl): TYPE_ID = message_control.TYPE_PM - def __init__(self, parent_win, gc_contact, contact, acct): + def __init__(self, parent_win, gc_contact, contact, acct, session): room_jid = contact.jid.split('/')[0] room_ctrl = gajim.interface.msg_win_mgr.get_control(room_jid, acct) self.room_name = room_ctrl.name self.gc_contact = gc_contact - ChatControl.__init__(self, parent_win, contact, acct) + ChatControl.__init__(self, parent_win, contact, acct, session) self.TYPE_ID = 'pm' def send_message(self, message): @@ -519,7 +519,7 @@ class GroupchatControl(ChatControlBase): else: self.print_conversation(msg, nick, tim, xhtml) - def on_private_message(self, nick, msg, tim, xhtml): + def on_private_message(self, nick, msg, tim, xhtml, session): # Do we have a queue? fjid = self.room_jid + '/' + nick no_queue = len(gajim.events.get_events(self.account, fjid)) == 0 @@ -531,7 +531,7 @@ class GroupchatControl(ChatControlBase): return event = gajim.events.create_event('pm', (msg, '', 'incoming', tim, - False, '', None, xhtml)) + False, '', None, xhtml, session)) gajim.events.add_event(self.account, fjid, event) autopopup = gajim.config.get('autopopup') @@ -551,7 +551,7 @@ class GroupchatControl(ChatControlBase): self.parent_win.show_title() self.parent_win.redraw_tab(self) else: - self._start_private_message(nick) + self._start_private_message(nick, session) # Scroll to line self.list_treeview.expand_row(path[0:1], False) self.list_treeview.scroll_to_cell(path) @@ -1812,7 +1812,7 @@ class GroupchatControl(ChatControlBase): menu.show_all() menu.popup(None, None, None, event.button, event.time) - def _start_private_message(self, nick): + def _start_private_message(self, nick, session = None): gc_c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) nick_jid = gc_c.get_full_jid() diff --git a/src/roster_window.py b/src/roster_window.py index cc7c89364..95873a211 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1191,8 +1191,10 @@ class RosterWindow: for jid in gajim.contacts.get_jid_list(account): results = gajim.logger.get_unread_msgs_for_jid(jid) - # XXX unread messages should probably have their session with them - session = gajim.connections[account].make_new_session(jid) + # XXX unread messages should probably have their session saved with them + if results: + session = gajim.connections[account].make_new_session(jid) + for result in results: tim = time.localtime(float(result[2])) self.on_message(jid, result[1], tim, account, msg_type = 'chat', @@ -3466,7 +3468,7 @@ class RosterWindow: self.actions_menu_needs_rebuild = True self.update_status_combobox() - def new_private_chat(self, gc_contact, account): + def new_private_chat(self, gc_contact, account, session = None): contact = gajim.contacts.contact_from_gc_contact(gc_contact) type_ = message_control.TYPE_PM fjid = gc_contact.room_jid + '/' + gc_contact.name @@ -3474,7 +3476,7 @@ class RosterWindow: if not mw: mw = gajim.interface.msg_win_mgr.create_window(contact, account, type_) - chat_control = PrivateChatControl(mw, gc_contact, contact, account) + chat_control = PrivateChatControl(mw, gc_contact, contact, account, session) mw.new_tab(chat_control) if len(gajim.events.get_events(account, fjid)): # We call this here to avoid race conditions with widget validation From eb93f9a17259f9047bb378ea1a5fae269809b4ae Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Mon, 20 Aug 2007 19:02:58 +0000 Subject: [PATCH 40/40] advertise xep-0116 support via disco --- src/common/connection_handlers.py | 6 ++++-- src/common/stanza_session.py | 2 +- src/common/xmpp/protocol.py | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 77e493ca2..0e07e01f4 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -733,6 +733,7 @@ class ConnectionDisco: q.addChild('feature', attrs = {'var': common.xmpp.NS_MUC}) q.addChild('feature', attrs = {'var': common.xmpp.NS_COMMANDS}) q.addChild('feature', attrs = {'var': common.xmpp.NS_DISCO_INFO}) + q.addChild('feature', attrs = {'var': common.xmpp.NS_ESESSION_INIT}) if (node is None or extension == 'cstates') and gajim.config.get('outgoing_chat_state_notifactions') != 'disabled': q.addChild('feature', attrs = {'var': common.xmpp.NS_CHATSTATES}) @@ -1220,7 +1221,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, def _InitE2ECB(self, con, stanza, session): gajim.log.debug('InitE2ECB') - init = stanza.getTag(name='init', namespace='http://www.xmpp.org/extensions/xep-0116.html#ns-init') + init = stanza.getTag(name='init', namespace=common.xmpp.NS_ESESSION_INIT) form = common.xmpp.DataForm(node=init.getTag('x')) self.dispatch('SESSION_NEG', (stanza.getFrom(), session, form)) @@ -1448,7 +1449,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, common.xmpp.NS_FEATURE: self._FeatureNegCB(con, msg, session) return - if msg.getTag('init') and msg.getTag('init').namespace == 'http://www.xmpp.org/extensions/xep-0116.html#ns-init': + if msg.getTag('init') and msg.getTag('init').namespace == \ + common.xmpp.NS_ESESSION_INIT: self._InitE2ECB(con, msg, session) encrypted = False diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 6af56d203..e1ffc53f9 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -730,7 +730,7 @@ class EncryptedStanzaSession(StanzaSession): response = xmpp.Message() init = response.NT.init - init.setNamespace('http://www.xmpp.org/extensions/xep-0116.html#ns-init') + init.setNamespace(xmpp.NS_ESESSION_INIT) x = xmpp.DataForm(typ='result') diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index 7d903a292..aa43126f8 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -45,6 +45,7 @@ NS_DISCO ='http://jabber.org/protocol/disco' NS_DISCO_INFO =NS_DISCO+'#info' NS_DISCO_ITEMS =NS_DISCO+'#items' NS_ENCRYPTED ='jabber:x:encrypted' # JEP-0027 +NS_ESESSION_INIT='http://www.xmpp.org/extensions/xep-0116.html#ns-init' NS_EVENT ='jabber:x:event' # JEP-0022 NS_FEATURE ='http://jabber.org/protocol/feature-neg' NS_FILE ='http://jabber.org/protocol/si/profile/file-transfer' # JEP-0096