From 6449b5c764f58b5985283a55ef8f18d8fae8c244 Mon Sep 17 00:00:00 2001 From: Kjell Braden Date: Fri, 10 Jun 2011 13:54:24 +0200 Subject: [PATCH] added gajim-otr plugin. needs python-potr > 1.0.0beta1 (which I will release soon) --- gotr/__init__.py | 1 + gotr/config_dialog.ui | 367 +++++++++++++++++++++++++ gotr/contact_otr_window.ui | 376 +++++++++++++++++++++++++ gotr/manifest.ini | 7 + gotr/otrmodule.py | 516 +++++++++++++++++++++++++++++++++++ gotr/ui.py | 544 +++++++++++++++++++++++++++++++++++++ 6 files changed, 1811 insertions(+) create mode 100644 gotr/__init__.py create mode 100644 gotr/config_dialog.ui create mode 100644 gotr/contact_otr_window.ui create mode 100644 gotr/manifest.ini create mode 100644 gotr/otrmodule.py create mode 100644 gotr/ui.py diff --git a/gotr/__init__.py b/gotr/__init__.py new file mode 100644 index 0000000..8fa452d --- /dev/null +++ b/gotr/__init__.py @@ -0,0 +1 @@ +from otrmodule import OtrPlugin diff --git a/gotr/config_dialog.ui b/gotr/config_dialog.ui new file mode 100644 index 0000000..4d9d4a2 --- /dev/null +++ b/gotr/config_dialog.ui @@ -0,0 +1,367 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + + + True + vertical + + + True + 12 + 0 + none + + + True + 12 + + + True + vertical + 5 + + + True + 5 + + + True + Fingerprint: + + + False + 0 + + + + + True + <tt>-------- -------- -------- -------- -------- </tt> + True + True + end + + + 1 + + + + + False + 0 + + + + + (Re-)generate + True + True + True + + + + False + 1 + + + + + True + 12 + 0 + none + + + True + 12 + + + True + vertical + + + Enable private (Off-the-Record) messaging + True + True + False + True + + + + 0 + + + + + + 1 + + + + + Automatically start private messaging + True + True + False + True + + + + 2 + + + + + Require private messaging + True + True + False + True + + + + 3 + + + + + + + + + True + <b>Default OTR Settings</b> + True + + + + + False + 2 + + + + + + + + + True + 5 + + + True + <b>Off-the-Record settings for:</b> + True + + + False + 0 + + + + + True + account_store + + + + + 0 + + + + + 1 + + + + + + + False + 0 + + + + + + + True + OTR Settings + + + False + + + + + True + 12 + vertical + + + True + True + automatic + automatic + + + True + True + fingerprint_store + 0 + 5 + + + True + Name + + + + 0 + + + + + + + True + Status + + + + 1 + + + + + + + True + Validated + + + + 2 + + + + + + + True + Fingerprint + + + + 3 + + + + + + + True + Account + + + + 4 + + + + + + + + + 0 + + + + + True + 5 + True + + + Verify Fingerprint + True + True + True + + + + 0 + + + + + Forget Fingerprint + True + True + True + + + + 1 + + + + + False + 1 + + + + + 1 + + + + + True + Known Fingerprints + + + 1 + False + + + + diff --git a/gotr/contact_otr_window.ui b/gotr/contact_otr_window.ui new file mode 100644 index 0000000..09feaa0 --- /dev/null +++ b/gotr/contact_otr_window.ui @@ -0,0 +1,376 @@ + + + + + + True + True + + + True + 5 + vertical + 5 + True + + + True + 0 + Your fingerprint: +<span weight="bold" face="monospace">01234567 89ABCDEF 01234567 89ABCDEF 01234567</span> + True + True + + + 0 + + + + + True + 0 + Purported fingerprint for asdfasdf@xyzxyzxyz.de: +<span weight="bold" face="monospace">01234567 89ABCDEF 01234567 89ABCDEF 01234567</span> + True + True + + + 1 + + + + + True + + + True + True + verifiedmodel + 0 + + + + 0 + + + + + False + 0 + + + + + True + 0.20000000298023224 + verified that the purported fingerprint is in fact the correct fingerprint for that contact. + True + + + False + 1 + + + + + 2 + + + + + + + True + Authentication + + + 1 + False + + + + + True + 0 + none + + + True + 5 + vertical + 5 + True + + + OTR version 2 allowed + True + True + False + True + True + + + 1 + + + + + Encryption required + True + True + False + True + + + 2 + + + + + Show others we understand OTR + True + True + False + True + True + + + 3 + + + + + Automatically initiate encryption if partner understands OTR + True + True + False + True + True + + + 4 + + + + + + + Use the default settings + True + True + False + True + True + + + + + 1 + + + + + True + OTR Settings + + + 1 + False + + + + + + + + + + + I have NOT + + + I have + + + + + False + + + True + vertical + + + True + 5 + vertical + 5 + + + True + label + True + True + + + 0 + + + + + True + + + Use question: + True + True + False + True + + + False + 0 + + + + + True + True + + + 1 + + + + + False + 1 + + + + + True + label + + + 2 + + + + + True + True + + + False + 3 + + + + + 0 + + + + + True + 5 + + + True + + + False + 0 + + + + + True + end + + + gtk-cancel + True + True + True + True + + + False + False + 0 + + + + + gtk-ok + True + True + True + True + + + False + False + 1 + + + + + 1 + + + + + False + 1 + + + + + + + True + False + Off-the-Record Encryption + True + + + + + True + OTR settings / fingerprint + True + + + + + + True + Authenticate contact + True + + + + + + True + Start / Refresh OTR + True + + + + + + True + False + End OTR + True + + + + + + + diff --git a/gotr/manifest.ini b/gotr/manifest.ini new file mode 100644 index 0000000..3348e64 --- /dev/null +++ b/gotr/manifest.ini @@ -0,0 +1,7 @@ +[info] +name: Off-The-Record Encryption +short_name: gotr +version: 1 +description: See http://www.cypherpunks.ca/otr/ +authors: Kjell Braden +homepage: http://gajim-otr.pentabarf.de diff --git a/gotr/otrmodule.py b/gotr/otrmodule.py new file mode 100644 index 0000000..0a3bb2a --- /dev/null +++ b/gotr/otrmodule.py @@ -0,0 +1,516 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +## otrmodule.py +## +## Copyright (C) 2008-2010 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 . +## + + +''' +Off-The-Record encryption plugin. + +:author: Kjell self.Braden +:since: 20 May 2011 +:copyright: Copyright (2011) Kjell Braden +:license: GPL +''' + +MINVERSION = (1,0,0,'beta1') # 1.0-alpha1 +IGNORE = True +PASS = False + +DEFAULTFLAGS = { + 'ALLOW_V1':False, + 'ALLOW_V2':True, + 'REQUIRE_ENCRYPTION':False, + 'SEND_TAG':True, + 'WHITESPACE_START_AKE':True, + 'ERROR_START_AKE':True, + } + +MMS = 1024 +PROTOCOL = 'xmpp' + +enc_tip = 'A private chat session is established to this contact ' \ + 'with this fingerprint' +unused_tip = 'A private chat session is established to this contact using ' \ + 'another fingerprint' +ended_tip = 'The private chat session to this contact has ended' +inactive_tip = 'Communication to this contact is currently ' \ + 'unencrypted' + +import os +import time + +import common.xmpp +from common import gajim +from common import ged +from common.connection_handlers_events import MessageOutgoingEvent +from plugins import GajimPlugin +from message_control import TYPE_CHAT, MessageControl +from plugins.helpers import log_calls, log + +import ui + + +import pickle +import potr +if not hasattr(potr, 'VERSION') or potr.VERSION < MINVERSION: + raise ImportError('old / unsupported python-otr version') + +class GajimContext(potr.context.Context): + __slots__ = ['smpWindow'] + + def __init__(self, account, peer): + super(GajimContext, self).__init__(account, peer) + self.smpWindow = ui.ContactOtrSmpWindow(self) + + def inject(self, msg, appdata=None): + log.warning('inject(appdata=%s)', appdata) + msg = unicode(msg) + account = self.user.accountname + + stanza = common.xmpp.Message(to=self.peer, body=msg, typ='chat') + if appdata and 'session' in appdata: + session = appdata['session'] + stanza.setThread(session.thread_id) + gajim.connections[account].connection.send(stanza, now=True) + return + + def setState(self, newstate): + if self.state == potr.context.STATE_ENCRYPTED: + # we were encrypted + if newstate == potr.context.STATE_ENCRYPTED: + # and are still -> it's just a refresh + OtrPlugin.gajim_log( + _('Private conversation with %s refreshed.') % self.peer, + self.user.accountname, self.peer) + elif newstate == potr.context.STATE_FINISHED: + # and aren't anymore -> other side disconnected + OtrPlugin.gajim_log(_('%s has ended his/her private ' + 'conversation with you. You should do the same.') + % self.peer, self.user.accountname, self.peer) + else: + if newstate == potr.context.STATE_ENCRYPTED: + # we are now encrypted + trust = self.getCurrentTrust() + if trust is None: + fpr = str(self.getCurrentKey()) + OtrPlugin.gajim_log(_('New fingerprint for %s: %s') + % (self.peer, fpr), self.user.accountname, self.peer) + self.setCurrentTrust('') + trustStr = 'authenticated' if bool(trust) else '*unauthenticated*' + OtrPlugin.gajim_log( + _('%s secured OTR conversation with %s started') + % (trustStr, self.peer), self.user.accountname, self.peer) + + if self.state != potr.context.STATE_PLAINTEXT and \ + newstate == potr.context.STATE_PLAINTEXT: + # we are now plaintext + OtrPlugin.gajim_log( + _('Private conversation with %s lost.') % self.peer, + self.user.accountname, self.peer) + + super(GajimContext, self).setState(newstate) + OtrPlugin.update_otr(self.peer, self.user.accountname) + self.user.plugin.update_context_list() + + def getPolicy(self, key): + jid = gajim.get_room_and_nick_from_fjid(self.peer)[0] + ret = self.user.plugin.get_flags(self.user.accountname, jid)[key] + log.warning('getPolicy(key=%s) = %s', key, ret) + return ret + +class GajimOtrAccount(potr.context.Account): + contextclass = GajimContext + def __init__(self, plugin, accountname): + global PROTOCOL, MMS + self.plugin = plugin + self.accountname = accountname + name = gajim.get_jid_from_account(accountname) + super(GajimOtrAccount, self).__init__(name, PROTOCOL, MMS) + self.keyFilePath = os.path.join(gajim.gajimpaths.data_root, accountname) + + def loadPrivkey(self): + try: + with open(self.keyFilePath + '.key2', 'r') as keyFile: + return pickle.load(keyFile) + except IOError, e: + log.exception('IOError occurred when loading key file for %s', + self.name) + return None + + def savePrivkey(self): + try: + with open(self.keyFilePath + '.key2', 'w') as keyFile: + pickle.dump(self.getPrivkey(), keyFile) + except IOError, e: + log.exception('IOError occurred when loading key file for %s', + self.name) + + def loadTrusts(self, newCtxCb=None): + ''' load the fingerprint trustdb ''' + # it has the same format as libotr, therefore the + # redundant account / proto field + try: + with open(self.keyFilePath + '.fpr', 'r') as fprFile: + for line in fprFile: + ctx, acc, proto, fpr, trust = line[:-1].split('\t') + + if acc != self.name or proto != PROTOCOL: + continue + + self.getContext(ctx, newCtxCb).setTrust(fpr, trust) + except IOError, e: + log.exception('IOError occurred when loading fpr file for %s', + self.name) + + def saveTrusts(self): + try: + with open(self.keyFilePath + '.fpr', 'w') as fprFile: + for uid, ctx in self.ctxs.iteritems(): + for fpr, trust in ctx.trust.iteritems(): + fprFile.write('\t'.join( + (uid, self.name, PROTOCOL, fpr, trust))) + fprFile.write('\n') + except IOError, e: + log.exception('IOError occurred when loading fpr file for %s', + self.name) + + +def otr_dialog_destroy(widget, *args, **kwargs): + widget.destroy() + +class OtrPlugin(GajimPlugin): + otr = None + def init(self): + + self.us = {} + self.config_dialog = ui.OtrPluginConfigDialog(self) + self.events_handlers = {} + self.events_handlers['message-received'] = (ged.PRECORE, + self.handle_incoming_msg) + self.events_handlers['message-outgoing'] = (ged.OUT_PRECORE, + self.handle_outgoing_msg) + + self.gui_extension_points = { + 'chat_control' : (self.cc_connect, self.cc_disconnect) + } + + for acc in gajim.contacts.get_accounts(): + self.us[acc] = GajimOtrAccount(self, acc) + self.us[acc].loadTrusts() + + acc = str(acc) + if acc not in self.config or None not in self.config[acc]: + self.config[acc] = {None:DEFAULTFLAGS.copy()} + + def get_otr_status(self, account, contact): + ctx = self.us[account].getContext(contact.get_full_jid()) + + finished = ctx.state == potr.context.STATE_FINISHED + encrypted = finished or ctx.state == potr.context.STATE_ENCRYPTED + trusted = encrypted and bool(ctx.getCurrentTrust()) + return (encrypted, trusted, finished) + + def cc_connect(self, cc): + def update_otr(print_status=False): + enc_status, authenticated, finished = \ + self.get_otr_status(cc.account, cc.contact) + otr_status_text = '' + + if finished: + otr_status_text = u'finished OTR connection' + elif authenticated: + otr_status_text = u'authenticated secure OTR connection' + elif enc_status: + otr_status_text = u'*unauthenticated* secure OTR connection' + + cc._show_lock_image(enc_status, u'OTR', enc_status, True, + authenticated) + if print_status and otr_status_text: + cc.print_conversation_line(u'[OTR] %s' % otr_status_text, + 'status', '', None) + cc.update_otr = update_otr + cc.update_otr(True) + + # hijack authentication button with our submenu + def authbutton_cb(widget): + if not cc.gpg_is_active and not (cc.session and + cc.session.enable_encryption): + ui.get_otr_submenu(self, cc).get_submenu().popup(None, + None, None, 0, 0) + else: + cc._on_authentication_button_clicked(cc, widget) + self.overwrite_handler(cc, cc.authentication_button, authbutton_cb) + + # hijack context menu + cc.orig_prepare_context_menu = cc.prepare_context_menu + def inject_menu(hide_buttonbar_items=False): + menu = cc.orig_prepare_context_menu(hide_buttonbar_items) + menu.insert(ui.get_otr_submenu(self, cc), 8) + return menu + cc.prepare_context_menu = inject_menu + + def cc_disconnect(self, cc): + try: + self.overwrite_handler(cc, cc.authentication_button, + cc._on_authentication_button_clicked) + cc.prepare_context_menu = cc.orig_prepare_context_menu + del cc.update_otr + except AttributeError: + pass + + def menu_settings_cb(self, item, control): + ctx = self.us[control.account].getContext(control.contact.get_full_jid()) + dlg = ui.ContactOtrWindow(self, ctx) + dlg.run() + dlg.destroy() + + def menu_start_cb(self, item, control): + gajim.nec.push_outgoing_event(MessageOutgoingEvent(None, + account=control.account, jid=control.contact.jid, + message=u'?OTRv?', type_='chat', + resource=control.contact.resource, is_loggable=False)) + + def menu_end_cb(self, item, control): + fjid = control.contact.get_full_jid() + thread_id = control.session.thread_id if control.session else None + + self.us[control.account].getContext(fjid).disconnect( + appdata={'session':control.session}) + + def menu_smp_cb(self, item, control): + ctx = self.us[control.account].getContext(control.contact.get_full_jid()) + ctx.smpWindow.show(False) + + @staticmethod + def overwrite_handler(window, control, handler): + for id_, v in window.handlers.iteritems(): + if v == control: + break + else: + raise LookupError + + del window.handlers[id_] + control.disconnect(id_) + id_ = control.connect('clicked', handler) + window.handlers[id_] = control + + def set_flags(self, value, account=None, contact=None): + if isinstance(account, unicode): + account = account.encode() + + if account not in self.config: + self.config[account] = {None:DEFAULTFLAGS.copy()} + + if account is None and contact is not None: + # don't set per-contact options without account + raise Exception("can't set contact flags without account") + + config = self.config[account] + config[contact] = value + + self.config[account] = config + + def get_flags(self, account=None, contact=None, fallback=True): + if isinstance(account, unicode): + account = account.encode() + + setting = DEFAULTFLAGS.copy() + if account in self.config: + setting.update(self.config[account][None]) + if contact in self.config[account] \ + and self.config[account][contact] is not None: + setting.update(self.config[account][contact]) + elif not fallback: + return None + return setting + + def update_context_list(self): + self.config_dialog.fpr_model.clear() + for us in self.us.itervalues(): + for uid, ctx in us.ctxs.iteritems(): + for fpr, trust in ctx.trust.iteritems(): + trust = False + if ctx.state == potr.context.STATE_ENCRYPTED: + if ctx.getCurrentKey().cfingerprint() == fpr: + state = "encrypted" + tip = enc_tip + trust = bool(ctx.getCurrentTrust()) + else: + state = "unused" + tip = unused_tip + elif ctx.state == potr.context.STATE_FINISHED: + state = "finished" + tip = ended_tip + else: + state = 'inactive' + tip = inactive_tip + + human_hash = potr.human_hash(fpr) + + self.config_dialog.fpr_model.append((uid, state, trust, + '%s' % human_hash, us.name, tip, fpr)) + + @classmethod + def gajim_log(cls, msg, account, fjid, no_print=False, + is_status_message=True, thread_id=None): + if not isinstance(fjid, unicode): + fjid = unicode(fjid) + if not isinstance(account, unicode): + account = unicode(account) + + resource = gajim.get_resource_from_jid(fjid) + jid = gajim.get_jid_without_resource(fjid) + tim = time.localtime() + + if is_status_message is True: + if not no_print: + ctrl = cls.get_control(fjid, account) + if ctrl: + ctrl.print_conversation_line(u'[OTR] %s' % msg, 'status', + '', None) + id = gajim.logger.write('chat_msg_recv', fjid, + message=u'[OTR: %s]' % msg, tim=tim) + # gajim.logger.write() only marks a message as unread (and so + # only returns an id) when fjid is a real contact (NOT if it's a + # GC private chat) + if id: + gajim.logger.set_read_messages([id]) + else: + session = gajim.connections[account].get_or_create_session(fjid, + thread_id) + session.received_thread_id |= bool(thread_id) + session.last_receive = time.time() + + if not session.control: + # look for an existing chat control without a session + ctrl = cls.get_control(fjid, account) + if ctrl: + session.control = ctrl + session.control.set_session(session) + + msg_id = gajim.logger.write('chat_msg_recv', fjid, + message=u'[OTR: %s]' % msg, tim=tim) + session.roster_message(jid, msg, tim=tim, msg_id=msg_id, + msg_type='chat', resource=resource) + + @classmethod + def update_otr(cls, user, acc, print_status=False): + ctrl = cls.get_control(user, acc) + if ctrl: + ctrl.update_otr(print_status) + + @staticmethod + def get_control(fjid, account): + # first try to get the window with the full jid + ctrl = gajim.interface.msg_win_mgr.get_control(fjid, account) + if ctrl: + # got one, be happy + return ctrl + + # otherwise try without the resource + ctrl = gajim.interface.msg_win_mgr.get_control( + gajim.get_jid_without_resource(fjid), account) + # but only use it when it's not a GC window + if ctrl and ctrl.TYPE_ID == TYPE_CHAT: + return ctrl + + def handle_incoming_msg(self, event): + ctx = None + account = event.conn.name + accjid = gajim.get_jid_from_account(account) + + if event.encrypted is not False or not event.stanza.getTag('body') \ + or not isinstance(event.stanza.getBody(), unicode): + return PASS + + try: + ctx = self.us[account].getContext(event.fjid) + msgtxt, tlvs = ctx.receiveMessage(event.msgtxt, + appdata={'session':event.session}) + except potr.context.UnencryptedMessage, e: + tlvs = [] + msgtxt = _('The following message received from %s was ' + '*not encrypted*: [%s]') % (event.fjid, e.args[0]) + except potr.context.NotEncryptedError, e: + self.gajim_log(_('The encrypted message received from %s is ' + 'unreadable, as you are not currently communicating ' + 'privately') % event.fjid, account, event.fjid) + return IGNORE + except potr.context.ErrorReceived, e: + self.gajim_log(_('We received the following OTR error ' + 'message from %s: [%s]') % (event.fjid, e.args[0].error), + account, event.fjid) + return IGNORE + except RuntimeError, e: + self.gajim_log(_('The following error occurred when trying to ' + 'decrypt a message from %s: [%s]') % (event.fjid, e), + account, event.fjid) + return IGNORE + event.msgtxt = unicode(msgtxt) + event.stanza.setBody(event.msgtxt) + + html_node = event.stanza.getTag('html') + if html_node: + event.stanza.delChild(html_node) + + if ctx is not None: + ctx.smpWindow.handle_tlv(tlvs) + + if not msgtxt: + return IGNORE + + return PASS + + def handle_outgoing_msg(self, event): + if hasattr(event, 'otrmessage'): + return PASS + + xep_200 = bool(event.session) and event.session.enable_encryption + if xep_200 or not event.message: + return PASS + + print event + + if event.session: + fjid = event.session.get_to() + else: + fjid = event.jid + if event.resource: + fjid += '/' + event.resource + print (fjid, event.session, event.jid, event.resource) + + try: + newmsg = self.us[event.account].getContext(fjid).sendMessage( + potr.context.FRAGMENT_SEND_ALL_BUT_LAST, event.message, + appdata={'session':event.session}) + except potr.context.NotEncryptedError, e: + if e.args[0] == potr.context.EXC_FINISHED: + self.gajim_log(_('Your message was not send. Either end ' + 'your private conversation, or restart it'), event.account, + fjid) + return IGNORE + else: + raise e + event.message = newmsg + + return PASS + +## TODO: +## - disconnect ctxs on disconnect diff --git a/gotr/ui.py b/gotr/ui.py new file mode 100644 index 0000000..dd878b8 --- /dev/null +++ b/gotr/ui.py @@ -0,0 +1,544 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +## ui.py +## +## Copyright (C) 2008-2010 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 gobject +import gtk +from common import i18n +from common import gajim +from plugins.gui import GajimPluginConfigDialog + +import otrmodule +import potr + + +class OtrPluginConfigDialog(GajimPluginConfigDialog): + def init(self): + self.GTK_BUILDER_FILE_PATH = \ + self.plugin.local_file_path('config_dialog.ui') + self.B = gtk.Builder() + self.B.set_translation_domain(i18n.APP) + self.B.add_from_file(self.GTK_BUILDER_FILE_PATH) + + self.fpr_model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, + gobject.TYPE_BOOLEAN, gobject.TYPE_STRING, gobject.TYPE_STRING, + gobject.TYPE_STRING, gobject.TYPE_STRING) + + self.otr_account_store = self.B.get_object('account_store') + + for account in sorted(gajim.contacts.get_accounts()): + self.otr_account_store.append(row=(account,)) + + fpr_view = self.B.get_object('fingerprint_view') + fpr_view.set_model(self.fpr_model) + fpr_view.get_selection().set_mode(gtk.SELECTION_MULTIPLE) + + if len(self.otr_account_store) > 0: + self.B.get_object('account_combobox').set_active(0) + + self.child.pack_start(self.B.get_object('notebook1')) + + self.flags = dict() + flagList = ( + ('ALLOW_V2', 'enable_check'), + ('SEND_TAG', 'advertise_check'), + ('WHITESPACE_START_AKE', 'autoinitiate_check'), + ('REQUIRE_ENCRYPTION', 'require_check') + ) + for flagName, checkBoxName in flagList: + self.flags[flagName] = self.B.get_object(checkBoxName) + + self.B.connect_signals(self) + self.account_combobox_changed_cb(self.B.get_object('account_combobox')) + + def on_run(self): + self.plugin.update_context_list() + + def flags_toggled_cb(self, button): + if button == self.B.get_object('enable_check'): + new_status = button.get_active() + self.B.get_object('advertise_check').set_sensitive(new_status) + self.B.get_object('autoinitiate_check').set_sensitive(new_status) + self.B.get_object('require_check').set_sensitive(new_status) + + if new_status is False: + self.B.get_object('advertise_check').set_active(False) + self.B.get_object('autoinitiate_check').set_active(False) + self.B.get_object('require_check').set_active(False) + + box = self.B.get_object('account_combobox') + active = box.get_active() + if active > -1: + account = self.otr_account_store[active][0] + + flagValues = {} + for key, box in self.flags.iteritems(): + flagValues[key] = box.get_active() + self.plugin.set_flags(flagValues, account) + + def account_combobox_changed_cb(self, box, *args): + fpr_label = self.B.get_object('fingerprint_label') + regen_button = self.B.get_object('regenerate_button') + + active = box.get_active() + fpr = '-------- -------- -------- -------- --------' + try: + if active > -1: + regen_button.set_sensitive(True) + account = self.otr_account_store[active][0] + + otr_flags = self.plugin.get_flags(account) + for key, box in self.flags.iteritems(): + box.set_active(otr_flags[key]) + + fpr = str(self.plugin.us[account].getPrivkey()) + regen_button.set_label('Regenerate') + else: + regen_button.set_sensitive(False) + except LookupError, e: + # Account not found, no private key available - display the + # empty one + regen_button.set_label('Generate') + finally: + self.B.get_object('fingerprint_label').set_markup('%s'%fpr) + + def forget_button_clicked_cb(self, button, *args): + accounts = {} + for acc in gajim.connections.iterkeys(): + accounts[gajim.get_jid_from_account(acc)] = acc + + tw = self.B.get_object('fingerprint_view') + + mod, paths = tw.get_selection().get_selected_rows() + + for path in paths: + it = mod.get_iter(path) + user, human_fpr, a, fpr = mod.get(it, 0, 3, 4, 6) + + dlg = gtk.Dialog('Confirm removal of fingerprint', self, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_YES, gtk.RESPONSE_YES, + gtk.STOCK_NO, gtk.RESPONSE_NO) + ) + l = gtk.Label() + l.set_markup('Are you sure you want remove the following ' + 'fingerprint for the contact %s on the account %s?' + '\n\n%s' % (user, a, human_fpr)) + l.set_line_wrap(True) + dlg.vbox.pack_start(l) + dlg.show_all() + + if dlg.run() == gtk.RESPONSE_YES: + ctx = self.plugin.us[accounts[a]].getContext(user) + ctx.removeFingerprint(fpr) + dlg.destroy() + self.plugin.us[accounts[a]].saveTrusts() + + self.plugin.update_context_list() + + def verify_button_clicked_cb(self, button, *args): + accounts = {} + for acc in gajim.connections.iterkeys(): + accounts[gajim.get_jid_from_account(acc)] = acc + + tw = self.B.get_object('fingerprint_view') + + mod, paths = tw.get_selection().get_selected_rows() + + # open the window for the first selected row + for path in paths[0:1]: + it = mod.get_iter(path) + fjid, fpr, a = mod.get(it, 0, 6, 4) + + ctx = self.plugin.us[accounts[a]].getContext(fjid) + + dlg = ContactOtrWindow(self.plugin, ctx, fpr=fpr, parent=self) + dlg.run() + dlg.destroy() + break + + def regenerate_button_clicked_cb(self, button, *args): + box = self.B.get_object('account_combobox') + active = box.get_active() + if active > -1: + account = self.otr_account_store[active][0] + button.set_sensitive(False) + self.plugin.us[account].privkey = None + self.account_combobox_changed_cb(box, *args) + button.set_sensitive(True) + + +import gtkgui_helpers +from common import gajim + +our_fp_text = _('Your fingerprint:\n' \ + '%s') +their_fp_text = _('Purported fingerprint for %s:\n' \ + '%s') + +another_q = _('You may want to authenticate your buddy as well by asking'\ + 'your own question.') +smp_query = _('%s is trying to authenticate you using a secret only known '\ + 'to him/her and you.') +smp_q_query = _('%s has chosen a question for you to answer to '\ + 'authenticate yourself:') +enter_secret = _('Please enter your secret below.') + +smp_init = _('You are trying to authenticate %s using a secret only known ' \ + 'to him/her and yourself.') +choose_q = _('You can choose a question as a hint for your buddy below.') + +class ContactOtrSmpWindow: + def gw(self, n): + return self.xml.get_object(n) + + def __init__(self, ctx): + self.question = None + self.ctx = ctx + self.account = ctx.user.accountname + + self.plugin = ctx.user.plugin + + self.GTK_BUILDER_FILE_PATH = \ + self.plugin.local_file_path('contact_otr_window.ui') + self.xml = gtk.Builder() + self.xml.set_translation_domain(i18n.APP) + self.xml.add_from_file(self.GTK_BUILDER_FILE_PATH) + + self.window = self.gw('otr_smp_window') + self.window.set_title(_('OTR settings for %s') % ctx.peer) + + # the lambda thing is an anonymous helper that just discards the + # parameters and calls hide_on_delete on clicking the window's + # close button + self.window.connect('delete-event', lambda d,o: + self.window.hide_on_delete()) + + self.gw('smp_cancel_button').connect('clicked', self._on_destroy) + self.gw('smp_ok_button').connect('clicked', self._apply) + self.gw('qcheckbutton').connect('toggled', self._toggle) + + self.gw('qcheckbutton').set_no_show_all(False) + self.gw('qentry').set_no_show_all(False) + self.gw('desclabel2').set_no_show_all(False) + + def _toggle(self, w, *args): + self.gw('qentry').set_sensitive(w.get_active()) + + def show(self, response): + self.smp_running = False + self.finished = False + + self.gw('smp_cancel_button').set_sensitive(True) + self.gw('smp_ok_button').set_sensitive(True) + self.gw('progressbar').set_fraction(0) + self.gw('secret_entry').set_text('') + + self.response = response + self.window.show_all() + if response: + self.gw('qcheckbutton').set_sensitive(False) + if self.question is None: + self.gw('qcheckbutton').set_active(False) + self.gw('qcheckbutton').hide() + self.gw('qentry').hide() + self.gw('desclabel2').hide() + self.gw('qcheckbutton').set_sensitive(False) + self.gw('desclabel1').set_markup((smp_query % self.ctx.peer) + + ' ' + enter_secret) + else: + self.gw('qcheckbutton').set_active(True) + self.gw('qcheckbutton').show() + self.gw('qentry').show() + self.gw('qentry').set_sensitive(True) + self.gw('qentry').set_editable(False) + self.gw('desclabel2').show() + self.gw('qentry').set_text(self.question) + + self.gw('desclabel1').set_markup(smp_q_query % self.ctx.peer) + self.gw('desclabel2').set_markup(enter_secret) + else: + self.gw('qcheckbutton').show() + self.gw('qcheckbutton').set_active(True) + self.gw('qcheckbutton').set_mode(True) + self.gw('qcheckbutton').set_sensitive(True) + self.gw('qentry').set_sensitive(True) + self.gw('qentry').show() + self.gw('qentry').set_text("") + + self.gw('qentry').set_editable(True) + self.gw('qentry').set_sensitive(True) + + self.gw('desclabel2').show() + self.gw('desclabel1').set_markup((smp_init % self.ctx.peer) + ' ' + + choose_q) + self.gw('desclabel2').set_markup(enter_secret) + + def _abort(self, text=None, appdata=None): + self.smp_running = False + + self.ctx.smpAbort(appdata=appdata) + if text: + self.plugin.gajim_log(text, self.account, self.ctx.peer) + + def _finish(self, text): + self.smp_running = False + self.finished = True + + self.gw('qcheckbutton').set_active(False) + self.gw('qcheckbutton').hide() + self.gw('qentry').hide() + self.gw('desclabel2').hide() + + self.gw('qcheckbutton').set_sensitive(False) + self.gw('smp_cancel_button').set_sensitive(False) + self.gw('smp_ok_button').set_sensitive(True) + self.gw('progressbar').set_fraction(1) + self.plugin.gajim_log(text, self.account, self.ctx.peer) + self.gw('desclabel1').set_markup(text) + + self.plugin.update_otr(self.ctx.peer, self.account, True) + self.ctx.user.saveTrusts() + self.plugin.update_context_list() + + def get_tlv(self, tlvs, check): + print (tlvs, check) + for tlv in tlvs: + if isinstance(tlv, check): + return tlv + return None + + def handle_tlv(self, tlvs): + if tlvs: + is1qtlv = self.get_tlv(tlvs, potr.proto.SMP1QTLV) + # check for TLV_SMP_ABORT or state = CHEATED + if not self.ctx.smpIsValid(): + self._abort() + self._finish(_('SMP verifying aborted')) + + # check for TLV_SMP1 + elif self.get_tlv(tlvs, potr.proto.SMP1TLV): + self.question = None + self.show(True) + self.gw('progressbar').set_fraction(0.3) + + # check for TLV_SMP1Q + elif is1qtlv: + self.question = is1qtlv.msg + self.show(True) + self.gw('progressbar').set_fraction(0.3) + + # check for TLV_SMP2 + elif self.get_tlv(tlvs, potr.proto.SMP2TLV): + self.gw('progressbar').set_fraction(0.6) + + # check for TLV_SMP3 + elif self.get_tlv(tlvs, potr.proto.SMP3TLV): + if self.ctx.smpIsSuccess(): + text = _('SMP verifying succeeded') + if self.question is not None: + text += ' '+another_q + self._finish(text) + else: + self._finish(_('SMP verifying failed')) + + # check for TLV_SMP4 + elif self.get_tlv(tlvs, potr.proto.SMP4TLV): + if self.ctx.smpIsSuccess(): + text = _('SMP verifying succeeded') + if self.question is not None: + text += ' '+another_q + self._finish(text) + else: + self._finish(_('SMP verifying failed')) + + def _on_destroy(self, widget): + if self.smp_running: + self._abort(_('user aborted SMP authentication')) + self.window.hide_all() + + def _apply(self, widget, appdata=None): + if self.finished: + self.window.hide_all() + return + secret = self.gw('secret_entry').get_text() + if self.response: + self.ctx.smpGotSecret(secret, appdata=appdata) + else: + if self.gw('qcheckbutton').get_active(): + qtext = self.gw('qentry').get_text() + self.ctx.smpInit(secret, question=qtext, appdata=appdata) + else: + self.ctx.smpInit(secret, appdata=appdata) + self.gw('progressbar').set_fraction(0.3) + self.smp_running = True + widget.set_sensitive(False) + +class ContactOtrWindow(gtk.Dialog): + def gw(self, n): + return self.xml.get_object(n) + + def __init__(self, plugin, ctx, fpr=None, parent=None): + fjid = ctx.peer + gtk.Dialog.__init__(self, title=_('OTR settings for %s') % fjid, + parent=parent, + flags=gtk.DIALOG_DESTROY_WITH_PARENT, + buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, + gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) + + self.ctx = ctx + self.fjid = fjid + self.jid = gajim.get_room_and_nick_from_fjid(self.fjid)[0] + self.account = ctx.user.accountname + self.fpr = fpr + self.plugin = plugin + + if self.fpr is None: + key = self.ctx.getCurrentKey() + if key is not None: + self.fpr = key.cfingerprint() + + self.GTK_BUILDER_FILE_PATH = \ + self.plugin.local_file_path('contact_otr_window.ui') + self.xml = gtk.Builder() + self.xml.set_translation_domain(i18n.APP) + self.xml.add_from_file(self.GTK_BUILDER_FILE_PATH) + self.notebook = self.gw('otr_settings_notebook') + self.child.pack_start(self.notebook) + + self.connect('response', self.on_response) + self.gw('otr_default_checkbutton').connect('toggled', + self._otr_default_checkbutton_toggled) + + # always set the label containing our fingerprint + self.gw('our_fp_label').set_markup(our_fp_text % ctx.user.getPrivkey()) + + if self.fpr is None: + # 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.fjid, + _('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 + fp = potr.human_hash(self.fpr) + self.gw('their_fp_label').set_markup(their_fp_text%(self.fjid, fp)) + # set the trust combobox + if ctx.getCurrentTrust(): + self.gw('verified_combobox').set_active(1) + else: + self.gw('verified_combobox').set_active(0) + + otr_flags = self.plugin.get_flags(self.account, self.jid, fallback=False) + + if otr_flags is not None: + 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 = self.plugin.get_flags(self.account) + 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_v2_checkbutton').set_active( + otr_flags['ALLOW_V2']) + self.gw('otr_policy_require_checkbutton').set_active( + otr_flags['REQUIRE_ENCRYPTION']) + self.gw('otr_policy_send_tag_checkbutton').set_active( + otr_flags['SEND_TAG']) + self.gw('otr_policy_start_on_tag_checkbutton').set_active( + otr_flags['WHITESPACE_START_AKE']) + + self.child.show_all() + + def on_response(self, dlg, response, *args): + if response != gtk.RESPONSE_ACCEPT: + return + + + # -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.getTrust(self.fpr): + self.ctx.setTrust(self.fpr, 'verified') + self.ctx.user.saveTrusts() + self.plugin.update_context_list() + elif trust_state == 0: + self.ctx.setTrust(self.fpr, '') + self.ctx.user.saveTrusts() + self.plugin.update_context_list() + + self.plugin.update_otr(self.ctx.peer, self.ctx.user.accountname, True) + + if self.gw('otr_default_checkbutton').get_active(): + # default is enabled, so remove any user-specific + # settings if available + self.plugin.set_flags(None, self.account, self.jid) + else: + print "got per-contact settings" + # build the flags using the checkboxes + flags = {} + flags['ALLOW_V2'] = \ + self.gw('otr_policy_allow_v2_checkbutton').get_active() + flags['REQUIRE_ENCRYPTION'] = \ + self.gw('otr_policy_require_checkbutton').get_active() + flags['SEND_TAG'] = \ + self.gw('otr_policy_send_tag_checkbutton').get_active() + flags['WHITESPACE_START_AKE'] = \ + self.gw('otr_policy_start_on_tag_checkbutton').get_active() + + print "per-contact settings: ", flags + self.plugin.set_flags(flags, self.account, self.jid) + + def _otr_default_checkbutton_toggled(self, widget): + for w in self.gw('otr_settings_vbox').get_children(): + w.set_sensitive(not widget.get_active()) + +def get_otr_submenu(plugin, control): + GTK_BUILDER_FILE_PATH = \ + plugin.local_file_path('contact_otr_window.ui') + xml = gtk.Builder() + xml.set_translation_domain(i18n.APP) + xml.add_from_file(GTK_BUILDER_FILE_PATH) + + otr_submenu = xml.get_object('otr_submenu') + otr_settings_menuitem, smp_otr_menuitem, start_otr_menuitem, \ + end_otr_menuitem = otr_submenu.get_submenu().get_children() + + otr_submenu.set_sensitive(True) + otr_settings_menuitem.connect('activate', plugin.menu_settings_cb, control) + start_otr_menuitem.connect('activate', plugin.menu_start_cb, control) + end_otr_menuitem.connect('activate', plugin.menu_end_cb, control) + smp_otr_menuitem.connect('activate', plugin.menu_smp_cb, control) + + enc, _, fin = plugin.get_otr_status(control.account, control.contact) + # can end only when not in PLAINTEXT + end_otr_menuitem.set_sensitive(enc) + # can SMP only when ENCRYPTED + smp_otr_menuitem.set_sensitive(enc and not fin) + return otr_submenu