diff sat/plugins/plugin_sec_otr.py @ 2743:da59ff099b32

core (memory/encryption), plugin OTR: finished OTR integration in encryption: - messageEncryptionStart and messageEncryptionStop are now async - an encryption plugin can now have a startEncryption and endEncryption method, which are called automatically when suitable - trust is now put in "trusted" key of message_data, and markAsTrusted has been added, it is used in OTR - getTrustUI implemented for OTR, using legacy authenticate code - catch potr.crypt.InvalidParameterError on decryption - fixed some service OTR messages which were not correctly marked as not for storing, and thus were added to MAM archive/carbon copied - other bug fixes
author Goffi <goffi@goffi.org>
date Thu, 03 Jan 2019 21:00:00 +0100
parents a86f494457c2
children 3dd265d281e1
line wrap: on
line diff
--- a/sat/plugins/plugin_sec_otr.py	Thu Jan 03 20:51:08 2019 +0100
+++ b/sat/plugins/plugin_sec_otr.py	Thu Jan 03 21:00:00 2019 +0100
@@ -74,9 +74,21 @@
 
 
 class Context(potr.context.Context):
-    def __init__(self, host, account, other_jid):
-        super(Context, self).__init__(account, other_jid)
-        self.host = host
+    def __init__(self, context_manager, other_jid):
+        self.context_manager = context_manager
+        super(Context, self).__init__(context_manager.account, other_jid)
+
+    @property
+    def host(self):
+        return self.context_manager.host
+
+    @property
+    def _p_hints(self):
+        return self.context_manager.parent._p_hints
+
+    @property
+    def _p_carbons(self):
+        return self.context_manager.parent._p_carbons
 
     def getPolicy(self, key):
         if key in DEFAULT_POLICY_FLAGS:
@@ -110,12 +122,42 @@
                 "timestamp": time.time(),
             }
             client.generateMessageXML(mess_data)
+            xml = mess_data[u'xml']
+            self._p_carbons.setPrivate(xml)
+            self._p_hints.addHintElements(xml, [
+                self._p_hints.HINT_NO_COPY,
+                self._p_hints.HINT_NO_PERMANENT_STORE])
             client.send(mess_data["xml"])
         else:
             message_elt = appdata[u"xml"]
             assert message_elt.name == u"message"
             message_elt.addElement("body", content=msg)
 
+    def stopCb(self, __, feedback):
+        client = self.user.client
+        self.host.bridge.otrState(
+            OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile
+        )
+        client.feedback(self.peer, feedback)
+
+    def stopEb(self, failure_):
+        # encryption may be already stopped in case of manual stop
+        if not failure_.check(exceptions.NotFound):
+            log.error(u"Error while stopping OTR encryption: {msg}".format(msg=failure_))
+
+    def isTrusted(self):
+        # we have to check value because potr code says that a 2-tuples should be
+        # returned while in practice it's either None or u"trusted"
+        trusted = self.getCurrentTrust()
+        if trusted is None:
+            return False
+        elif trusted == u'trusted':
+            return True
+        else:
+            log.error(u"Unexpected getCurrentTrust() value: {value}".format(
+                value=trusted))
+            return False
+
     def setState(self, state):
         client = self.user.client
         old_state = self.state
@@ -123,17 +165,17 @@
         log.debug(u"setState: %s (old_state=%s)" % (state, old_state))
 
         if state == potr.context.STATE_PLAINTEXT:
-            client.encryption.stop(self.peer, NS_OTR)
             feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {
                 "other_jid": self.peer.full()
             }
-            self.host.bridge.otrState(
-                OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile
-            )
+            d = client.encryption.stop(self.peer, NS_OTR)
+            d.addCallback(self.stopCb, feedback=feedback)
+            d.addErrback(self.stopEb)
+            return
         elif state == potr.context.STATE_ENCRYPTED:
             client.encryption.start(self.peer, NS_OTR)
             try:
-                trusted = self.getCurrentTrust()
+                trusted = self.isTrusted()
             except TypeError:
                 trusted = False
             trusted_str = _(u"trusted") if trusted else _(u"untrusted")
@@ -155,13 +197,13 @@
                 OTR_STATE_ENCRYPTED, self.peer.full(), client.profile
             )
         elif state == potr.context.STATE_FINISHED:
-            client.encryption.stop(self.peer, NS_OTR)
             feedback = D_(u"OTR conversation with {other_jid} is FINISHED").format(
                 other_jid=self.peer.full()
             )
-            self.host.bridge.otrState(
-                OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile
-            )
+            d = client.encryption.stop(self.peer, NS_OTR)
+            d.addCallback(self.stopCb, feedback=feedback)
+            d.addErrback(self.stopEb)
+            return
         else:
             log.error(D_(u"Unknown OTR state"))
             return
@@ -240,15 +282,19 @@
 
 
 class ContextManager(object):
-    def __init__(self, host, client):
-        self.host = host
-        self.account = Account(host, client)
+    def __init__(self, parent, client):
+        self.parent = parent
+        self.account = Account(parent.host, client)
         self.contexts = {}
 
+    @property
+    def host(self):
+        return self.parent.host
+
     def startContext(self, other_jid):
         assert isinstance(other_jid, jid.JID)
         context = self.contexts.setdefault(
-            other_jid, Context(self.host, self.account, other_jid)
+            other_jid, Context(self, other_jid)
         )
         return context
 
@@ -323,7 +369,7 @@
     def profileConnected(self, client):
         if client.profile in self.skipped_profiles:
             return
-        ctxMng = client._otr_context_manager = ContextManager(self.host, client)
+        ctxMng = client._otr_context_manager = ContextManager(self, client)
         client._otr_data = persistent.PersistentBinaryDict(NS_OTR, client.profile)
         yield client._otr_data.load()
         encrypted_priv_key = client._otr_data.get(PRIVATE_KEY, None)
@@ -346,6 +392,110 @@
             context.disconnect()
         del client._otr_context_manager
 
+    # encryption plugin methods
+
+    def startEncryption(self, client, entity_jid):
+        self.startRefresh(client, entity_jid)
+
+    def stopEncryption(self, client, entity_jid):
+        self.endSession(client, entity_jid)
+
+    def getTrustUI(self, client, entity_jid):
+        if not entity_jid.resource:
+            entity_jid.resource = self.host.memory.getMainResource(
+                client, entity_jid
+            )  # FIXME: temporary and unsecure, must be changed when frontends
+               #        are refactored
+        ctxMng = client._otr_context_manager
+        otrctx = ctxMng.getContextForUser(entity_jid)
+        priv_key = ctxMng.account.privkey
+
+        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: _(
+                        u"You have no private key yet, start an OTR conversation to "
+                        u"have one"
+                    ),
+                    C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING,
+                },
+                title=_(u"No private key"),
+            )
+            return dialog
+
+        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: _(
+                        u"Your fingerprint is:\n{fingerprint}\n\n"
+                        u"Start an OTR conversation to have your correspondent one."
+                    ).format(fingerprint=priv_key),
+                    C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO,
+                },
+                title=_(u"Fingerprint"),
+            )
+            return dialog
+
+        def setTrust(raw_data, profile):
+            if xml_tools.isXMLUICancelled(raw_data):
+                return {}
+            # This method is called when authentication form is submited
+            data = xml_tools.XMLUIResult2DataFormResult(raw_data)
+            if data["match"] == "yes":
+                otrctx.setCurrentTrust(OTR_STATE_TRUSTED)
+                note_msg = _(u"Your correspondent {correspondent} is now TRUSTED")
+                self.host.bridge.otrState(
+                    OTR_STATE_TRUSTED, entity_jid.full(), client.profile
+                )
+            else:
+                otrctx.setCurrentTrust("")
+                note_msg = _(u"Your correspondent {correspondent} is now UNTRUSTED")
+                self.host.bridge.otrState(
+                    OTR_STATE_UNTRUSTED, entity_jid.full(), client.profile
+                )
+            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.peer),
+                },
+            )
+            return {"xmlui": note.toXml()}
+
+        submit_id = self.host.registerCallback(setTrust, with_data=True, one_shot=True)
+        trusted = otrctx.isTrusted()
+
+        xmlui = xml_tools.XMLUI(
+            C.XMLUI_FORM,
+            title=_(u"Authentication ({entity_jid})").format(entity_jid=entity_jid.full()),
+            submit_id=submit_id,
+        )
+        xmlui.addText(_(AUTH_TXT))
+        xmlui.addDivider()
+        xmlui.addText(
+            D_(u"Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key)
+        )
+        xmlui.addText(
+            D_(u"Your correspondent fingerprint should be:\n{fingerprint}").format(
+                fingerprint=other_fingerprint
+            )
+        )
+        xmlui.addDivider("blank")
+        xmlui.changeContainer("pairs")
+        xmlui.addLabel(D_(u"Is your correspondent fingerprint the same as here ?"))
+        xmlui.addList(
+            "match", [("yes", _("yes")), ("no", _("no"))], ["yes" if trusted else "no"]
+        )
+        return xmlui
+
     def _otrStartRefresh(self, menu_data, profile):
         """Start or refresh an OTR session
 
@@ -422,97 +572,7 @@
 
     def authenticate(self, client, to_jid):
         """Authenticate other user and see our own fingerprint"""
-        if not to_jid.resource:
-            to_jid.resource = self.host.memory.getMainResource(
-                client, to_jid
-            )  # FIXME: temporary and unsecure, must be changed when frontends
-               #        are refactored
-        ctxMng = client._otr_context_manager
-        otrctx = ctxMng.getContextForUser(to_jid)
-        priv_key = ctxMng.account.privkey
-
-        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: _(
-                        u"You have no private key yet, start an OTR conversation to "
-                        u"have one"
-                    ),
-                    C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING,
-                },
-                title=_(u"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: _(
-                        u"Your fingerprint is:\n{fingerprint}\n\n"
-                        u"Start an OTR conversation to have your correspondent one."
-                    ).format(fingerprint=priv_key),
-                    C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO,
-                },
-                title=_(u"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(OTR_STATE_TRUSTED)
-                note_msg = _(u"Your correspondent {correspondent} is now TRUSTED")
-                self.host.bridge.otrState(
-                    OTR_STATE_TRUSTED, to_jid.full(), client.profile
-                )
-            else:
-                otrctx.setCurrentTrust("")
-                note_msg = _(u"Your correspondent {correspondent} is now UNTRUSTED")
-                self.host.bridge.otrState(
-                    OTR_STATE_UNTRUSTED, to_jid.full(), client.profile
-                )
-            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.peer),
-                },
-            )
-            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(
-            D_(u"Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key)
-        )
-        xmlui.addText(
-            D_(u"Your correspondent fingerprint should be:\n{fingerprint}").format(
-                fingerprint=other_fingerprint
-            )
-        )
-        xmlui.addDivider("blank")
-        xmlui.changeContainer("pairs")
-        xmlui.addLabel(D_(u"Is your correspondent fingerprint the same as here ?"))
-        xmlui.addList(
-            "match", [("yes", _("yes")), ("no", _("no"))], ["yes" if trusted else "no"]
-        )
+        xmlui = self.getTrustUI(client, to_jid)
         return {"xmlui": xmlui.toXml()}
 
     def _dropPrivKey(self, menu_data, profile):
@@ -601,6 +661,12 @@
             feedback = msg
             client.feedback(from_jid, msg)
             raise failure.Failure(exceptions.CancelError(msg))
+        except potr.crypt.InvalidParameterError as e:
+            msg = D_(u"Error while trying de decrypt OTR message: {msg}".format(msg=e))
+            log.warning(msg)
+            feedback = msg
+            client.feedback(from_jid, msg)
+            raise failure.Failure(exceptions.CancelError(msg))
         except StopIteration:
             return data
         else:
@@ -629,6 +695,13 @@
                     exceptions.CancelError("Cancelled by OTR")
                 )  # no message at all (no history, no signal)
         client.encryption.markAsEncrypted(data)
+        trusted = otrctx.isTrusted()
+
+        if trusted:
+            client.encryption.markAsTrusted(data)
+        else:
+            client.encryption.markAsUntrusted(data)
+
         return data
 
     def _receivedTreatmentForSkippedProfiles(self, data):