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 @@
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
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