diff src/plugins/plugin_sec_otr.py @ 1141:7fcafc3206b1

plugin OTR: authentication management + fixed a bug in setState (due to a wrong docstring in potr.context.getCurrentTrust)
author Goffi <goffi@goffi.org>
date Wed, 27 Aug 2014 00:23:14 +0200
parents 768f1f1ef12c
children 2481fa96ac1c
line wrap: on
line diff
--- a/src/plugins/plugin_sec_otr.py	Tue Aug 26 13:33:12 2014 +0200
+++ b/src/plugins/plugin_sec_otr.py	Wed Aug 27 00:23:14 2014 +0200
@@ -25,6 +25,7 @@
 from sat.core.log import getLogger
 from sat.core import exceptions
 log = getLogger(__name__)
+from sat.tools import xml_tools
 from twisted.words.protocols.jabber import jid
 from twisted.python import failure
 from twisted.internet import defer
@@ -34,6 +35,7 @@
 NS_OTR = "otr_plugin"
 PRIVATE_KEY = "PRIVATE KEY"
 MAIN_MENU = D_('OTR')
+AUTH_TXT = D_("To authenticate your correspondent, you need to give your below fingerprint *BY AN EXTERNAL CANAL* (i.e. not in this chat), and check that the one he give you is the same as below. If there is a mismatch, there can be a spy between you !")
 
 DEFAULT_POLICY_FLAGS = {
   'ALLOW_V1':False,
@@ -54,7 +56,6 @@
 
 
 class Context(potr.context.Context):
-
     def __init__(self, host, account, other_jid):
         super(Context, self).__init__(account, other_jid)
         self.host = host
@@ -88,7 +89,7 @@
             feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {'other_jid': self.peer.full()}
         elif state == potr.context.STATE_ENCRYPTED:
             try:
-                fingerprint, trusted = self.getCurrentTrust()
+                trusted = self.getCurrentTrust()
             except TypeError:
                 trusted = False
             trusted_str = _(u"trusted") if trusted else _(u"untrusted")
@@ -138,6 +139,28 @@
             self.client.otr_data[PRIVATE_KEY] = encrypted_priv_key
         d.addCallback(save_encrypted_key)
 
+    def loadTrusts(self):
+        trust_data = self.client.otr_data.get('trust', {})
+        for jid_, jid_data in trust_data.iteritems():
+            for fingerprint, trust_level in jid_data.iteritems():
+                log.debug('setting trust for {jid}: [{fingerprint}] = "{trust_level}"'.format(jid=jid_, fingerprint=fingerprint, trust_level=trust_level))
+                self.trusts.setdefault(jid.JID(jid_), {})[fingerprint] = trust_level
+
+    def saveTrusts(self):
+        log.debug("saving trusts for {profile}".format(profile=self.client.profile))
+        log.debug("trusts = {}".format(self.client.otr_data['trust']))
+        self.client.otr_data.force('trust')
+
+    def setTrust(self, other_jid, fingerprint, trustLevel):
+        try:
+            trust_data = self.client.otr_data['trust']
+        except KeyError:
+            trust_data = {}
+            self.client.otr_data['trust'] = trust_data
+        jid_data = trust_data.setdefault(other_jid.full(), {})
+        jid_data[fingerprint] = trustLevel
+        super(Account, self).setTrust(other_jid, fingerprint, trustLevel)
+
 
 class ContextManager(object):
 
@@ -169,6 +192,7 @@
         host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000)
         host.importMenu((MAIN_MENU, D_("Start/Refresh")), self._startRefresh, security_limit=0, help_string=D_("Start or refresh an OTR session"), type_=C.MENU_SINGLE)
         host.importMenu((MAIN_MENU, D_("End session")), self._endSession, security_limit=0, help_string=D_("Finish an OTR session"), type_=C.MENU_SINGLE)
+        host.importMenu((MAIN_MENU, D_("Authenticate")), self._authenticate, security_limit=0, help_string=D_("Authenticate user/see your fingerprint"), type_=C.MENU_SINGLE)
 
     def _fixPotr(self):
         # FIXME: potr fix for bad unicode handling
@@ -189,7 +213,7 @@
     @defer.inlineCallbacks
     def profileConnected(self, profile):
         client = self.host.getClient(profile)
-        self.context_managers[profile] = ContextManager(self.host, client)
+        ctxMng = self.context_managers[profile] = ContextManager(self.host, client)
         client.otr_data = persistent.PersistentBinaryDict(NS_OTR, profile)
         yield client.otr_data.load()
         encrypted_priv_key = client.otr_data.get(PRIVATE_KEY, None)
@@ -198,6 +222,7 @@
             client.otr_priv_key = potr.crypt.PK.parsePrivateKey(priv_key.decode('hex'))[0]
         else:
             client.otr_priv_key = None
+        ctxMng.account.loadTrusts()
 
     def _startRefresh(self, menu_data, profile):
         """Start or refresh an OTR session
@@ -234,6 +259,76 @@
         otrctx.disconnect()
         return {}
 
+    def _authenticate(self, menu_data, profile):
+        """Authenticate other user and see our own fingerprint
+
+        @param menu_data: %(menu_data)s
+        @param profile: %(doc_profile)s
+        """
+        try:
+            to_jid = jid.JID(menu_data['jid'])
+            if not to_jid.resource:
+                to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
+        except KeyError:
+            log.error(_("jid key is not present !"))
+            return defer.fail(exceptions.DataError)
+        otrctx = self.context_managers[profile].getContextForUser(to_jid)
+
+        priv_key = otrctx.user.client.otr_priv_key
+
+        if priv_key is None:
+            # we have no private key yet
+            dialog = xml_tools.XMLUI(C.XMLUI_DIALOG,
+                     dialog_opt = {C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE,
+                                   C.XMLUI_DATA_MESS: _("You have no private key yet, start an OTR conversation to have one"),
+                                   C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING
+                                  },
+                     title = _("No private key"),
+                     )
+            return {'xmlui': dialog.toXml()}
+
+        other_fingerprint = otrctx.getCurrentKey()
+
+        if other_fingerprint is None:
+            # we have a private key, but not the fingerprint of our correspondent
+            dialog = xml_tools.XMLUI(C.XMLUI_DIALOG,
+                     dialog_opt = {C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE,
+                                   C.XMLUI_DATA_MESS: _("Your fingerprint is\n{fingerprint}\n\nStart an OTR conversation to have your correspondent one.").format(fingerprint=priv_key),
+                                   C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO
+                                  },
+                     title = _("Fingerprint"),
+                     )
+            return {'xmlui': dialog.toXml()}
+
+        def setTrust(raw_data, profile):
+            # This method is called when authentication form is submited
+            data = xml_tools.XMLUIResult2DataFormResult(raw_data)
+            if data['match'] == 'yes':
+                otrctx.setCurrentTrust('verified')
+                note_msg = _("Your correspondant {correspondent} is now TRUSTED")
+            else:
+                otrctx.setCurrentTrust('')
+                note_msg = _("Your correspondant {correspondent} is now UNTRUSTED")
+            note = xml_tools.XMLUI(C.XMLUI_DIALOG, dialog_opt = {
+                                   C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE,
+                                   C.XMLUI_DATA_MESS: note_msg.format(correspondent=otrctx.user.name)}
+                                   )
+            return {'xmlui': note.toXml()}
+
+        submit_id = self.host.registerCallback(setTrust, with_data=True, one_shot=True)
+        trusted = bool(otrctx.getCurrentTrust())
+
+        xmlui = xml_tools.XMLUI(C.XMLUI_FORM, title=_('Authentication (%s)') % to_jid.full(), submit_id=submit_id)
+        xmlui.addText(_(AUTH_TXT))
+        xmlui.addDivider()
+        xmlui.addText(_("Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key))
+        xmlui.addText(_("Your correspondent fingerprint should be:\n{fingerprint}").format(fingerprint=other_fingerprint))
+        xmlui.addDivider('blank')
+        xmlui.changeContainer('pairs')
+        xmlui.addLabel(_('Is your correspondent fingerprint the same as here ?'))
+        xmlui.addList("match", [('yes', _('yes')),('no', _('no'))], ['yes' if trusted else 'no'])
+        return {'xmlui': xmlui.toXml()}
+
     def _receivedTreatment(self, data, profile):
         from_jid = jid.JID(data['from'])
         log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid)