From 435042968e5e85951f1cba811c464779d0636695 Mon Sep 17 00:00:00 2001 From: js Date: Fri, 9 May 2008 12:35:25 +0000 Subject: [PATCH] Added OTR support. Work done by Kjell Braden . Some fixes done by me. --- data/glade/chat_control_popup_menu.glade | 44 ++++ data/glade/contact_otr_window.glade | 322 +++++++++++++++++++++++ src/chat_control.py | 81 ++++++ src/common/config.py | 2 + src/common/connection.py | 7 +- src/common/connection_handlers.py | 28 ++ src/common/gajim.py | 4 + src/gajim.py | 185 +++++++++++++ src/message_control.py | 31 ++- src/otr_windows.py | 301 +++++++++++++++++++++ src/roster_window.py | 14 + 11 files changed, 1016 insertions(+), 3 deletions(-) create mode 100644 data/glade/contact_otr_window.glade create mode 100644 src/otr_windows.py diff --git a/data/glade/chat_control_popup_menu.glade b/data/glade/chat_control_popup_menu.glade index 36dd58801..66da16509 100644 --- a/data/glade/chat_control_popup_menu.glade +++ b/data/glade/chat_control_popup_menu.glade @@ -71,6 +71,50 @@ + + + True + Off-the-Record Encryption + True + + + + + True + OTR settings / fingerprint + True + + + + + + True + Authenticate contact + True + + + + + + True + Start / Refresh OTR + True + + + + + + True + False + End OTR + True + + + + + + + True diff --git a/data/glade/contact_otr_window.glade b/data/glade/contact_otr_window.glade new file mode 100644 index 000000000..e0f5b865c --- /dev/null +++ b/data/glade/contact_otr_window.glade @@ -0,0 +1,322 @@ + + + + + + False + + + True + + + True + True + + + True + 5 + 5 + True + + + True + 0 + Your fingerprint: +<span weight="bold" face="monospace">01234567 89ABCDEF 01234567 89ABCDEF 01234567</span> + True + + + + + True + 0 + Purported fingerprint for asdfasdf@xyzxyzxyz.de: +<span weight="bold" face="monospace">01234567 89ABCDEF 01234567 89ABCDEF 01234567</span> + True + + + 1 + + + + + True + + + True + True + 1 + I have NOT +I have + + + False + + + + + True + 0.20000000298023224 + verified that the purported fingerprint is in fact the correct fingerprint for that contact. + True + + + 1 + + + + + 2 + + + + + + + True + Authentication + + + tab + False + + + + + True + 0 + GTK_SHADOW_NONE + + + True + 5 + 5 + True + + + True + True + OTR version 1 allowed + 0 + True + True + + + + + True + True + OTR version 2 allowed + 0 + True + True + + + 1 + + + + + True + True + Encryption required + 0 + True + + + 2 + + + + + True + True + Show others we understand OTR + 0 + True + True + + + 3 + + + + + True + True + Automatically initiate encryption if partner understands OTR + 0 + True + True + + + 4 + + + + + True + True + Automatically start OTR when an OTR error occured + 0 + True + + + 5 + + + + + + + True + True + Use the default settings + 0 + True + True + + + label_item + + + + + 1 + + + + + True + OTR Settings + + + tab + 1 + False + + + + + + + True + GTK_BUTTONBOX_END + + + True + True + True + gtk-cancel + True + 0 + + + + + True + True + True + True + True + gtk-ok + True + 0 + + + 1 + + + + + 1 + + + + + + + False + + + True + + + True + 5 + 5 + + + True + label + True + True + + + + + True + True + + + False + 1 + + + + + + + True + 5 + + + True + + + + False + + + + + True + GTK_BUTTONBOX_END + + + True + True + True + gtk-cancel + True + 0 + + + + + True + True + True + gtk-ok + True + 0 + + + 1 + + + + + 1 + + + + + False + 1 + + + + + + diff --git a/src/chat_control.py b/src/chat_control.py index 37e6eec76..66e4627ad 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1176,9 +1176,37 @@ class ChatControl(ChatControlBase): self.check_for_possible_inactive_chatstate, None) def update_ui(self): + if gajim.otr_module: + self.update_otr(True) # The name banner is drawn here ChatControlBase.update_ui(self) + def update_otr(self, print_status=False): + # retrieve the OTR context from the chat's contact data + ctx = gajim.otr_module.otrl_context_find(gajim.otr_userstates[self.account], + self.contact.get_full_jid().encode(), + gajim.get_jid_from_account(self.account).encode(), gajim.OTR_PROTO, 1, + (gajim.otr_add_appdata, self.account))[0] + + enc_status = False + otr_status_text = "" + if ctx.msgstate == gajim.otr_module.OTRL_MSGSTATE_ENCRYPTED: + enc_status = True + if ctx.active_fingerprint.trust: + otr_status_text = u"authenticated secure OTR connection" + else: + otr_status_text = u'*unauthenticated* secure OTR connection' + elif ctx.msgstate == gajim.otr_module.OTRL_MSGSTATE_FINISHED: + enc_status = True + otr_status_text = u"finished OTR connection" + else: + # nothing to print + print_status = False + self._show_lock_image(enc_status, u'OTR', enc_status, True) + if print_status: + self.print_conversation_line(u" [OTR] %s"%otr_status_text, 'status', + '', None) + def _update_banner_state_image(self): contact = gajim.contacts.get_contact_with_highest_priority(self.account, self.contact.jid) @@ -1696,6 +1724,11 @@ 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') + otr_submenu = xml.get_widget('otr_submenu') + otr_settings_menuitem = xml.get_widget('otr_settings_menuitem') + smp_otr_menuitem = xml.get_widget('smp_otr_menuitem') + start_otr_menuitem = xml.get_widget('start_otr_menuitem') + end_otr_menuitem = xml.get_widget('end_otr_menuitem') add_to_roster_menuitem = xml.get_widget('add_to_roster_menuitem') send_file_menuitem = xml.get_widget('send_file_menuitem') information_menuitem = xml.get_widget('information_menuitem') @@ -1781,6 +1814,33 @@ class ChatControl(ChatControlBase): id = convert_to_gc_menuitem.connect('activate', self._on_convert_to_gc_menuitem_activate) self.handlers[id] = convert_to_gc_menuitem + + if gajim.otr_module: + otr_submenu.show() + id = otr_settings_menuitem.connect('activate', + self._on_otr_settings_menuitem_activate) + self.handlers[id] = otr_settings_menuitem + id = start_otr_menuitem.connect('activate', + self._on_start_otr_menuitem_activate) + self.handlers[id] = start_otr_menuitem + id = end_otr_menuitem.connect('activate', + self._on_end_otr_menuitem_activate) + self.handlers[id] = end_otr_menuitem + id = smp_otr_menuitem.connect('activate', + self._on_smp_otr_menuitem_activate) + self.handlers[id] = smp_otr_menuitem + + ctx = gajim.otr_module.otrl_context_find(gajim.otr_userstates[self.account], + self.contact.get_full_jid().encode(), + gajim.get_jid_from_account(self.account).encode(), gajim.OTR_PROTO, 1, + (gajim.otr_add_appdata, self.account))[0] + # can end only when PLAINTEXT + end_otr_menuitem.set_sensitive(ctx.msgstate != + gajim.otr_module.OTRL_MSGSTATE_PLAINTEXT) + # can SMP only when ENCRYPTED + smp_otr_menuitem.set_sensitive(ctx.msgstate == + gajim.otr_module.OTRL_MSGSTATE_ENCRYPTED) + menu.connect('selection-done', self.destroy_menu, history_menuitem, information_menuitem) return menu @@ -2259,6 +2319,27 @@ class ChatControl(ChatControlBase): # XXX decide whether to use 4 or 3 message negotiation self.session.negotiate_e2e(False) + def _on_start_otr_menuitem_activate(self, widget): + # ?OTR? gets replaced with a better message internally in otrl_message_sending + MessageControl.send_message(self, u"?OTR?", type="chat") + def _on_end_otr_menuitem_activate(self, widget): + fjid = self.contact.get_full_jid() + gajim.otr_module.otrl_message_disconnect(gajim.otr_userstates[self.account], + (gajim.otr_ui_ops, {'account':self.account,'urgent':True}), + gajim.get_jid_from_account(self.account).encode(), gajim.OTR_PROTO, + fjid.encode()) + gajim.otr_ui_ops.gajim_log("Private conversation with %s lost."%fjid, + self.account, fjid.encode()) + self.update_ui() + def _on_otr_settings_menuitem_activate(self, widget): + gajim.otr_windows.ContactOtrWindow(self.contact, self.account, self) + def _on_smp_otr_menuitem_activate(self, widget): + ctx = gajim.otr_module.otrl_context_find(gajim.otr_userstates[self.account], + self.contact.get_full_jid().encode(), + gajim.get_jid_from_account(self.account).encode(), gajim.OTR_PROTO, 1, + (gajim.otr_add_appdata, self.account))[0] + ctx.app_data.show(False) + def got_connected(self): ChatControlBase.got_connected(self) # Refreshing contact diff --git a/src/common/config.py b/src/common/config.py index 5ced3829f..ac2ef9105 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -319,6 +319,7 @@ class Config: 'zeroconf_jabber_id': [ opt_str, '', '', True ], 'zeroconf_email': [ opt_str, '', '', True ], 'use_env_http_proxy' : [opt_bool, False], + 'otr_flags': [opt_int, 59 ], }, {}), 'statusmsg': ({ 'message': [ opt_str, '' ], @@ -369,6 +370,7 @@ class Config: 'contacts': ({ 'gpg_enabled': [ opt_bool, False, _('Is OpenPGP enabled for this contact?')], 'speller_language': [ opt_str, '', _('Language for which we want to check misspelled words')], + 'otr_flags': [opt_int, -1 ], }, {}), 'rooms': ({ 'speller_language': [ opt_str, '', _('Language for which we want to check misspelled words')], diff --git a/src/common/connection.py b/src/common/connection.py index 83080f270..94c99494e 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -1024,7 +1024,7 @@ class Connection(ConnectionHandlers): def send_message(self, jid, msg, keyID, type='chat', subject='', chatstate=None, msg_id=None, composing_xep=None, resource=None, - user_nick=None, xhtml=None, session=None, forward_from=None, form_node=None): + user_nick=None, xhtml=None, session=None, forward_from=None, form_node=None, original_message=None): if not self.connection: return 1 if msg and not xhtml and gajim.config.get('rst_formatting_outgoing_messages'): @@ -1116,13 +1116,16 @@ class Connection(ConnectionHandlers): if session.enable_encryption: msg_iq = session.encrypt_stanza(msg_iq) + self.connection.send(msg_iq) - if not forward_from and session.is_loggable(): + if not forward_from and session and session.is_loggable(): 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: log_msg = msg + if original_message != None: + log_msg = original_message if subject: log_msg = _('Subject: %s\n%s') % (subject, msg) if log_msg: diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index bf624655e..0d881e694 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1677,6 +1677,34 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, self.dispatch('ERROR', (_('Disk Write Error'), str(e))) return elif mtype == 'chat': # it's type 'chat' + + if gajim.otr_module and isinstance(msgtxt, unicode): + otr_msg_tuple = gajim.otr_module.otrl_message_receiving( + gajim.otr_userstates[self.name], + (gajim.otr_ui_ops, {'account':self.name}), + gajim.get_jid_from_account(self.name).encode(), + gajim.OTR_PROTO, frm.encode(), msgtxt.encode(), + (gajim.otr_add_appdata, self.name)) + msgtxt = unicode(otr_msg_tuple[1]) + # OTR messages are unformatted, or rather contain the same + # text in and + msghtml = msgtxt + + if gajim.otr_module.otrl_tlv_find(otr_msg_tuple[2], + gajim.otr_module.OTRL_TLV_DISCONNECTED) != None: + gajim.otr_ui_ops.gajim_log("%s has ended his/her private conversation" + " with you; you should do the same."%frm, self.name, + frm) + ctrl = gajim.interface.msg_win_mgr.get_control(frm, self.name) + if ctrl: + ctrl.update_ui() + + ctx = gajim.otr_module.otrl_context_find(gajim.otr_userstates[self.name], frm.encode(), + gajim.get_jid_from_account(self.name).encode(), gajim.OTR_PROTO, 1, + (gajim.otr_add_appdata, self.name))[0] + tlvs = otr_msg_tuple[2] + ctx.app_data.handle_tlv(tlvs) + if not msg.getTag('body') and chatstate is None: #no return if msg.getTag('body') and session.is_loggable() and msgtxt: diff --git a/src/common/gajim.py b/src/common/gajim.py index 978f67c81..747e9e8b6 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -165,6 +165,10 @@ else: if system('gpg -h >/dev/null 2>&1'): HAVE_GPG = False +OTR_PROTO = "xmpp" +otr_userstates = {} +otr_policy = {} + gajim_identity = {'type': 'pc', 'category': 'client', 'name': 'Gajim'} gajim_common_features = [xmpp.NS_BYTESTREAM, xmpp.NS_SI, xmpp.NS_FILE, xmpp.NS_MUC, xmpp.NS_MUC_USER, diff --git a/src/gajim.py b/src/gajim.py index 0d7f76b48..435c78957 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -253,6 +253,172 @@ from common import helpers from common import optparser from common import dataforms +from common.xmpp import Message as XmppMessage + +try: + import otr, otr_windows + + gajim.otr_module = otr + gajim.otr_windows = otr_windows +except ImportError: + gajim.otr_module = None + gajim.otr_windows = None + +def add_appdata(data=None, context=None): + account = data + context.app_data = otr_windows.ContactOtrSMPWindow(unicode(context.username), + account) + +gajim.otr_add_appdata = add_appdata + + +def otr_dialog_destroy(widget, *args, **kwargs): + widget.destroy() + +class OtrlMessageAppOps: + + def gajim_log(self, msg, account, fjid, no_print=False): + if not isinstance(fjid, unicode): + fjid = unicode(fjid) + if not isinstance(account, unicode): + account = unicode(account) + resource=gajim.get_resource_from_jid(fjid) + tim = time.localtime() + + if not no_print: + ctrl = gajim.interface.msg_win_mgr.get_control( + gajim.get_jid_without_resource(fjid), account) + if ctrl: + ctrl.print_conversation_line(u" [OTR] %s"%msg, 'status', '', None) + id = gajim.logger.write('chat_msg_recv', fjid, message=msg, tim=tim) + gajim.logger.set_read_messages([id]) + + def policy(self, opdata=None, context=None): + policy = gajim.config.get_per("contacts", + gajim.get_jid_without_resource(context.username), "otr_flags") + if policy <= 0: + policy = gajim.config.get_per("accounts", opdata['account'], "otr_flags") + return policy + + def create_privkey(self, opdata="", accountname="", protocol=""): + dialog = gtk.Dialog(title=_("Generating..."), parent=gajim.interface.roster.window, + flags=gtk.DIALOG_MODAL, buttons=(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)) + permlabel = gtk.Label("Generating a private key for %s..."%accountname) + permlabel.set_padding(20,20) + dialog.set_response_sensitive(gtk.RESPONSE_CLOSE, False) + dialog.connect("destroy", otr_dialog_destroy) + dialog.connect("response", otr_dialog_destroy) + dialog.vbox.pack_start(permlabel) + dialog.get_root_window().raise_() + dialog.show_all() + dialog.map() + for c in dialog.get_children(): + c.show_now() + c.map() + + while gtk.events_pending(): + gtk.main_iteration(block=False) + + otr.otrl_privkey_generate(gajim.otr_userstates[opdata['account']], + os.path.join(gajimpaths.root, "%s.key"%opdata['account']).encode(), + accountname, gajim.OTR_PROTO) + permlabel.set_text("Generating a private key for %s...\ndone."%accountname) + dialog.set_response_sensitive(gtk.RESPONSE_CLOSE, True) + + def is_logged_in(self, opdata={}, accountname="", protocol="", recipient=""): + return gajim.contacts.get_contact_from_full_jid(opdata['account'], recipient).show \ + in ['dnd', 'xa', 'chat', 'online', 'away', 'invisible'] + + def inject_message(self, opdata=None, accountname="", protocol="", recipient="", + message=""): + msg_type = otr.otrl_proto_message_type(message) + + if 'kwargs' not in opdata or 'urgent' in opdata: + # don't use send_message here to have the message sent immediatly. + # this results in being able to disconnect from OTR sessions before + # quitting + stanza = XmppMessage(to=recipient, body=message, typ="chat") + gajim.connections[opdata['account']].connection.send(stanza, now=True) + return + + if msg_type == otr.OTRL_MSGTYPE_QUERY: + # split away XHTML-contaminated explanatory message + message = unicode(message.splitlines()[0]) + message += u"\n%s has requested an Off-the-Record private " \ + "conversation. However, you do not have a plugin to " \ + "support that.\nSee http://otr.cypherpunks.ca/ for more "\ + "information."%gajim.get_jid_from_account(opdata['account']) + + gajim.connections[opdata['account']].send_message(recipient, message, + **opdata['kwargs']) + + def notify(sef, opdata=None, username="", **kwargs): + self.gajim_log("Notify: "+str(kwargs), opdata['account'], username) + + def display_otr_message(self, opdata=None, username="", msg="", **kwargs): + self.gajim_log("OTR Message: "+msg, opdata['account'], username) + return 0 + + def update_context_list(self, **kwargs): + # FIXME stub FIXME # + pass + + def protocol_name(self, opdata=None, protocol=""): + return "XMPP" + + def new_fingerprint(self, opdata=None, username="", fingerprint="", **kwargs): + self.gajim_log("New fingerprint for %s: %s"%(username, + otr.otrl_privkey_hash_to_human(fingerprint)), opdata['account'], username) + + def write_fingerprints(self, opdata=""): + otr.otrl_privkey_write_fingerprints(gajim.otr_userstates[opdata['account']], + os.path.join(gajimpaths.root, "%s.fpr"%opdata['account']).encode()) + + def gone_secure(self, opdata="", context=None): + trust = context.active_fingerprint.trust + if trust: + trust = "verified" + else: + trust = "unverified" + self.gajim_log("%s secured OTR connection started"%trust, + opdata['account'], context.username, no_print=True) + + ctrl = gajim.interface.msg_win_mgr.get_control( + gajim.get_jid_without_resource(unicode(context.username)), + opdata['account']) + if ctrl: + ctrl.update_otr(True) + + def gone_insecure(self, opdata="", context=None): + self.gajim_log("Private conversation with %s lost.", opdata['account'], context.username) + + ctrl = gajim.interface.msg_win_mgr.get_control( + gajim.get_jid_without_resource(unicode(context.username)), + opdata['account']) + if ctrl: + ctrl.update_otr() + + def still_secure(self, opdata=None, context=None, is_reply=0): + ctrl = gajim.interface.msg_win_mgr.get_control( + gajim.get_jid_without_resource(unicode(context.username)), + opdata['account']) + if ctrl: + ctrl.update_otr(True) + + self.gajim_log("OTR connection was refreshed", opdata['account'], + context.username) + + def log_message(self, opdata=None, message=""): + gajim.log.debug(message) + + def max_message_size(self, **kwargs): + return 0 + + def account_name(self, opdata=None, account="",protocol=""): + return gajim.get_name_from_jid(opdata['account'], unicode(account)) + +gajim.otr_ui_ops = OtrlMessageAppOps() + if verbose: gajim.verbose = True del verbose @@ -3160,6 +3326,25 @@ class Interface: gajim.status_before_autoaway[a] = '' gajim.transport_avatar[a] = {} + if gajim.otr_module: + gajim.otr_userstates[a] = otr.otrl_userstate_create() + try: + otr.otrl_privkey_read(gajim.otr_userstates[a], + os.path.join(gajimpaths.root, "%s.key"%a).encode()) + except Exception, e: + if hasattr(e,"os_errno") and e.os_errno == 2: + print "didn't find otr keyfile "+ \ + (os.path.join(gajimpaths.root, "%s.key"%a).encode()) + pass + try: + otr.otrl_privkey_read_fingerprints(gajim.otr_userstates[a], + os.path.join(gajimpaths.root, "%s.fpr"%a).encode(), (add_appdata, a)) + except Exception, e: + if hasattr(e,"os_errno") and e.os_errno == 2: + print "didn't find otr fingerprint file "+ \ + (os.path.join(gajimpaths.root, "%s.fpr"%a).encode()) + pass + if gajim.config.get('remote_control'): try: import remote_control diff --git a/src/message_control.py b/src/message_control.py index a81532bc1..907ef0ba2 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -143,6 +143,7 @@ class MessageControl: '''Send the given message to the active tab. Doesn't return None if error ''' jid = self.contact.jid + original_message = message if not self.session: fjid = self.contact.get_full_jid() @@ -150,8 +151,36 @@ class MessageControl: self.set_session(new_session) + if gajim.otr_module: + if type == 'chat' and isinstance(message, unicode): + d = {'kwargs':{'keyID':keyID, 'type':type, + 'chatstate':chatstate, 'msg_id':msg_id, + 'composing_xep':composing_xep, 'resource':self.resource, + 'user_nick':user_nick, 'session':self.session, + 'original_message':original_message}, 'account':self.account} + + new_msg = gajim.otr_module.otrl_message_sending( + gajim.otr_userstates[self.account], + (gajim.otr_ui_ops, d), + gajim.get_jid_from_account(self.account).encode(), gajim.OTR_PROTO, + self.contact.get_full_jid().encode(), message.encode(), None) + + context = gajim.otr_module.otrl_context_find( + gajim.otr_userstates[self.account], + self.contact.get_full_jid().encode(), + gajim.get_jid_from_account(self.account).encode(), + gajim.OTR_PROTO, 1)[0] + + print repr(context.accountname), repr(context.username) + + # we send all because inject_message can filter on HTML stuff then + gajim.otr_module.otrl_message_fragment_and_send( + (gajim.otr_ui_ops, d), + context, new_msg, gajim.otr_module.OTRL_FRAGMENT_SEND_ALL) + return + # Send and update history return gajim.connections[self.account].send_message(jid, message, keyID, type = type, chatstate = chatstate, msg_id = msg_id, composing_xep = composing_xep, resource = self.resource, - user_nick = user_nick, session = self.session) + user_nick = user_nick, session = self.session, original_message = original_message) diff --git a/src/otr_windows.py b/src/otr_windows.py new file mode 100644 index 000000000..b0486dfc2 --- /dev/null +++ b/src/otr_windows.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python +## otr_windows.py +## +## +## Copyright (C) 2008 Kjell Braden +## +## This file is part of Gajim. +## +## Gajim is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## Gajim is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see . +## + +import gtkgui_helpers +from common import gajim + +our_fp_text = """Your fingerprint: +%s""" + +their_fp_text = """Purported fingerprint for %s: +%s""" + +class ContactOtrSMPWindow: + + def gw(self, n): + """ shorthand for self.xml.get_widget(n)""" + return self.xml.get_widget(n) + + def __init__(self, fjid, account): + self.fjid = fjid + self.account = account + + self.xml = gtkgui_helpers.get_glade("contact_otr_window.glade") + self.window = self.xml.get_widget("otr_smp_window") + + + # the contact may be unknown to gajim if ContactOtrSMPWindow is created very early + self.contact = gajim.contacts.get_contact_from_full_jid(account, fjid) + if self.contact: + self.window.set_title("OTR settings for %s"% + self.contact.get_full_jid()) + + self.ctx = gajim.otr_module.otrl_context_find( + gajim.otr_userstates[self.account], + self.fjid.encode(), gajim.get_jid_from_account(self.account).encode(), + gajim.OTR_PROTO, 1, (gajim.otr_add_appdata, self.account))[0] + + self.gw("smp_cancel_button").connect("clicked", self._on_destroy) + self.gw("smp_ok_button").connect("clicked", self._apply) + + def show(self, response): + # re-initialize if contact was unknown when we initially initialized + if not self.contact: + self.__init__(self.fjid, self.account) + assert(self.contact) # the contact MUST be known when showing the dialog + + self.smp_running = False + self.finished = False + + self.response = response + if response: + self.gw("desc_label").set_markup("%s is trying to authenticate you " + "using a secret only known to him/her and you. Please enter your secret " + "below."% self.contact.get_full_jid()) + else: + self.gw("desc_label").set_markup("You are trying to authenticate %s " + "using a secret only known to him/her and yourself. Please enter your " + "secret below."% self.contact.get_full_jid()) + + self.window.show_all() + + def _abort(self, text=None): + self.smp_running = False + gajim.otr_module.otrl_message_abort_smp(gajim.otr_userstates[self.account], + (gajim.otr_ui_ops, {'account':self.account}), self.ctx) + if text: + gajim.otr_ui_ops.gajim_log(text, self.account, self.contact.get_full_jid()) + + def _finish(self, text): + self.smp_running = False + self.finished = True + self.gw("smp_cancel_button").set_sensitive(False) + self.gw("smp_ok_button").set_sensitive(True) + self.gw("progressbar").set_fraction(1) + gajim.otr_ui_ops.gajim_log(text, self.account, self.contact.get_full_jid()) + self.gw("desc_label").set_markup(text) + ctrl = gajim.interface.msg_win_mgr.get_control(self.contact.jid, self.account) + if ctrl: + ctrl.update_ui() + gajim.otr_ui_ops.write_fingerprints({'account':self.account}) + + def handle_tlv(self, tlvs): + if not self.contact: + self.__init__(self.fjid, self.account) + + if tlvs: + nextTLV = self.ctx.smstate.nextExpected; + tlv = gajim.otr_module.otrl_tlv_find(tlvs, gajim.otr_module.OTRL_TLV_SMP1) + if tlv: + if nextTLV != gajim.otr_module.OTRL_SMP_EXPECT1: + self._abort() + else: + self.show(True) + self.gw("progressbar").set_fraction(0.3) + tlv = gajim.otr_module.otrl_tlv_find(tlvs, gajim.otr_module.OTRL_TLV_SMP2) + if tlv: + if nextTLV != gajim.otr_module.OTRL_SMP_EXPECT2: + self._abort() + else: + self.ctx.smstate.nextExpected = gajim.otr_module.OTRL_SMP_EXPECT4; + self.gw("progressbar").set_fraction(0.6) + tlv = gajim.otr_module.otrl_tlv_find(tlvs, gajim.otr_module.OTRL_TLV_SMP3) + if tlv: + if nextTLV != gajim.otr_module.OTRL_SMP_EXPECT3: + self._abort() + else: + self.ctx.smstate.nextExpected = gajim.otr_module.OTRL_SMP_EXPECT1; + if self.ctx.active_fingerprint.trust: + self._finish("SMP verifying succeeded") + else: + self._finish("SMP verifying failed") + tlv = gajim.otr_module.otrl_tlv_find(tlvs, gajim.otr_module.OTRL_TLV_SMP4) + if tlv: + if nextTLV != gajim.otr_module.OTRL_SMP_EXPECT4: + self._abort() + else: + self.ctx.smstate.nextExpected = gajim.otr_module.OTRL_SMP_EXPECT1; + if self.ctx.active_fingerprint.trust: + self._finish("SMP verifying succeeded") + else: + self._finish("SMP verifying failed") + tlv = gajim.otr_module.otrl_tlv_find(tlvs, gajim.otr_module.OTRL_TLV_SMP_ABORT) + if tlv: + self._finish("SMP verifying aborted") + + def _on_destroy(self, widget): + if self.smp_running: + self._abort("user aborted SMP authentication") + self.window.hide_all() + + def _apply(self, widget): + if self.finished: + self.window.hide_all() + return + secret = self.gw("secret_entry").get_text() + if self.response: + gajim.otr_module.otrl_message_respond_smp(gajim.otr_userstates[self.account], + (gajim.otr_ui_ops, {'account':self.account}), self.ctx, secret) + else: + gajim.otr_module.otrl_message_initiate_smp(gajim.otr_userstates[self.account], + (gajim.otr_ui_ops, {'account':self.account}), self.ctx, secret) + self.gw("progressbar").set_fraction(0.3) + self.smp_running = True + widget.set_sensitive(False) + + +class ContactOtrWindow: + + def gw(self, n): + """ shorthand for self.xml.get_widget(n)""" + return self.xml.get_widget(n) + + def __init__(self, contact, account, ctrl=None): + self.contact = contact + self.account = account + self.ctrl = ctrl + + self.ctx = gajim.otr_module.otrl_context_find( + gajim.otr_userstates[self.account], + self.contact.get_full_jid().encode(), + gajim.get_jid_from_account(self.account).encode(), + gajim.OTR_PROTO, 1, (gajim.otr_add_appdata, self.account))[0] + + self.xml = gtkgui_helpers.get_glade("contact_otr_window.glade") + self.window = self.xml.get_widget("otr_settings_window") + + self.gw("settings_cancel_button").connect("clicked", self._on_destroy) + self.gw("settings_ok_button").connect("clicked", self._apply) + self.gw("otr_default_checkbutton").connect("toggled", + self._otr_default_checkbutton_toggled) + + self.window.set_title("OTR settings for %s"% + self.contact.get_full_jid()) + + # always set the label containing our fingerprint + self.gw("our_fp_label").set_markup(our_fp_text% + gajim.otr_module.otrl_privkey_fingerprint( + gajim.otr_userstates[self.account], + gajim.get_jid_from_account(self.account).encode(), + gajim.OTR_PROTO)) + + if self.ctx.msgstate != gajim.otr_module.OTRL_MSGSTATE_ENCRYPTED: + # make the fingerprint widgets insensitive when not encrypted + for widget in self.gw("otr_fp_vbox").get_children(): + widget.set_sensitive(False) + # show that the fingerprint is unknown + self.gw("their_fp_label").set_markup( + their_fp_text%(self.contact.get_full_jid(), + "unknown")) + self.gw("verified_combobox").set_active(-1) + else: + # make the fingerprint widgets sensitive when encrypted + for widget in self.gw("otr_fp_vbox").get_children(): + widget.set_sensitive(True) + # show their fingerprint + self.gw("their_fp_label").set_markup( + their_fp_text%(self.contact.get_full_jid(), + gajim.otr_module.otrl_privkey_hash_to_human( + self.ctx.active_fingerprint.fingerprint))) + # set the trust combobox + if self.ctx.active_fingerprint.trust: + self.gw("verified_combobox").set_active(1) + else: + self.gw("verified_combobox").set_active(0) + + otr_flags = gajim.config.get_per("contacts", self.contact.jid, + "otr_flags") + + if otr_flags > 0: + self.gw("otr_default_checkbutton").set_active(0) + for w in self.gw("otr_settings_vbox").get_children(): + w.set_sensitive(True) + else: + # per-user settings not available, using default settings + otr_flags = gajim.config.get_per("accounts", self.account, + "otr_flags") + self.gw("otr_default_checkbutton").set_active(1) + for w in self.gw("otr_settings_vbox").get_children(): + w.set_sensitive(False) + + self.gw("otr_policy_allow_v1_checkbutton").set_active( + otr_flags & gajim.otr_module.OTRL_POLICY_ALLOW_V1) + self.gw("otr_policy_allow_v2_checkbutton").set_active( + otr_flags & gajim.otr_module.OTRL_POLICY_ALLOW_V2) + self.gw("otr_policy_require_checkbutton").set_active( + otr_flags & gajim.otr_module.OTRL_POLICY_REQUIRE_ENCRYPTION) + self.gw("otr_policy_send_tag_checkbutton").set_active( + otr_flags & gajim.otr_module.OTRL_POLICY_SEND_WHITESPACE_TAG) + self.gw("otr_policy_start_on_tag_checkbutton").set_active( + otr_flags & gajim.otr_module.OTRL_POLICY_WHITESPACE_START_AKE) + self.gw("otr_policy_start_on_error_checkbutton").set_active( + otr_flags & gajim.otr_module.OTRL_POLICY_ERROR_START_AKE) + + self.window.show_all() + + def _on_destroy(self, widget): + self.window.destroy() + + def _apply(self, widget): + # -1 when nothing is selected (ie. the connection is not encrypted) + trust_state = self.gw("verified_combobox").get_active() + if trust_state == 1 and not self.ctx.active_fingerprint.trust: + gajim.otr_module.otrl_context_set_trust( + self.ctx.active_fingerprint, "verified") + gajim.otr_ui_ops.write_fingerprints({'account':self.account}) + elif trust_state == 0: + gajim.otr_module.otrl_context_set_trust( + self.ctx.active_fingerprint, "") + gajim.otr_ui_ops.write_fingerprints({'account':self.account}) + + if not self.ctrl: + self.ctrl = gajim.interface.msg_win_mgr.get_control( + self.contact.jid, self.account) + if self.ctrl: + self.ctrl.update_ui() + + if self.gw("otr_default_checkbutton").get_active(): + # default is enabled, so remove any user-specific settings if available + gajim.config.set_per("contacts", self.contact.jid, "otr_flags", -1) + else: + # build the flags using the checkboxes + flags = 0 + flags += self.gw("otr_policy_allow_v1_checkbutton").get_active() \ + and gajim.otr_module.OTRL_POLICY_ALLOW_V1 + flags += self.gw("otr_policy_allow_v2_checkbutton").get_active() \ + and gajim.otr_module.OTRL_POLICY_ALLOW_V2 + flags += self.gw("otr_policy_require_checkbutton").get_active() \ + and gajim.otr_module.OTRL_POLICY_REQUIRE_ENCRYPTION + flags += self.gw("otr_policy_send_tag_checkbutton").get_active() \ + and gajim.otr_module.OTRL_POLICY_SEND_WHITESPACE_TAG + flags += self.gw("otr_policy_start_on_tag_checkbutton").get_active() \ + and gajim.otr_module.OTRL_POLICY_WHITESPACE_START_AKE + flags += self.gw("otr_policy_start_on_error_checkbutton").get_active() \ + and gajim.otr_module.OTRL_POLICY_ERROR_START_AKE + + gajim.config.add_per("contacts", self.contact.jid) + gajim.config.set_per("contacts", self.contact.jid, "otr_flags", flags) + + self._on_destroy(widget) + + def _otr_default_checkbutton_toggled(self, widget): + for w in self.gw("otr_settings_vbox").get_children(): + w.set_sensitive(not widget.get_active()) diff --git a/src/roster_window.py b/src/roster_window.py index 19e41c05e..2140e2e7c 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1771,6 +1771,20 @@ class RosterWindow: gajim.sleeper_state[account] = 'online' elif gajim.sleeper_state[account] not in ('autoaway', 'autoxa'): gajim.sleeper_state[account] = 'off' + + if gajim.otr_module: + # disconnect from ENCRYPTED OTR contexts when going + # offline/invisible + if status == 'offline' or status == 'invisible': + ctx = gajim.otr_userstates[account].context_root + while ctx is not None: + if ctx.msgstate == gajim.otr_module.OTRL_MSGSTATE_ENCRYPTED: + disconnected = True + gajim.otr_module.otrl_message_disconnect(gajim.otr_userstates[account], + (gajim.otr_ui_ops, + {'account':account,'urgent':True}), ctx.accountname, + ctx.protocol, ctx.username) + ctx = ctx.next if to: gajim.connections[account].send_custom_status(status, txt, to) else: