diff libervia/backend/plugins/plugin_sec_otr.py @ 4071:4b842c1fb686

refactoring: renamed `sat` package to `libervia.backend`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 11:49:51 +0200
parents sat/plugins/plugin_sec_otr.py@c23cad65ae99
children 0d7bb4df2343
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/backend/plugins/plugin_sec_otr.py	Fri Jun 02 11:49:51 2023 +0200
@@ -0,0 +1,839 @@
+#!/usr/bin/env python3
+
+
+# SAT plugin for OTR encryption
+# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program 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 Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# XXX: thanks to Darrik L Mazey for his documentation
+#      (https://blog.darmasoft.net/2013/06/30/using-pure-python-otr.html)
+#      this implentation is based on it
+
+import copy
+import time
+import uuid
+from binascii import hexlify, unhexlify
+from libervia.backend.core.i18n import _, D_
+from libervia.backend.core.constants import Const as C
+from libervia.backend.core.log import getLogger
+from libervia.backend.core import exceptions
+from libervia.backend.tools import xml_tools
+from twisted.words.protocols.jabber import jid
+from twisted.python import failure
+from twisted.internet import defer
+from libervia.backend.memory import persistent
+import potr
+
+log = getLogger(__name__)
+
+
+PLUGIN_INFO = {
+    C.PI_NAME: "OTR",
+    C.PI_IMPORT_NAME: "OTR",
+    C.PI_MODES: [C.PLUG_MODE_CLIENT],
+    C.PI_TYPE: "SEC",
+    C.PI_PROTOCOLS: ["XEP-0364"],
+    C.PI_DEPENDENCIES: ["XEP-0280", "XEP-0334"],
+    C.PI_MAIN: "OTR",
+    C.PI_HANDLER: "no",
+    C.PI_DESCRIPTION: _("""Implementation of OTR"""),
+}
+
+NS_OTR = "urn:xmpp:otr:0"
+PRIVATE_KEY = "PRIVATE KEY"
+OTR_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 gives "
+    "you is the same as below. If there is a mismatch, there can be a spy between you!"
+)
+DROP_TXT = D_(
+    "You private key is used to encrypt messages for your correspondent, nobody except "
+    "you must know it, if you are in doubt, you should drop it!\n\nAre you sure you "
+    "want to drop your private key?"
+)
+# NO_LOG_AND = D_(u"/!\\Your history is not logged anymore, and")   # FIXME: not used at the moment
+NO_ADV_FEATURES = D_("Some of advanced features are disabled !")
+
+DEFAULT_POLICY_FLAGS = {"ALLOW_V1": False, "ALLOW_V2": True, "REQUIRE_ENCRYPTION": True}
+
+OTR_STATE_TRUSTED = "trusted"
+OTR_STATE_UNTRUSTED = "untrusted"
+OTR_STATE_UNENCRYPTED = "unencrypted"
+OTR_STATE_ENCRYPTED = "encrypted"
+
+
+class Context(potr.context.Context):
+    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 get_policy(self, key):
+        if key in DEFAULT_POLICY_FLAGS:
+            return DEFAULT_POLICY_FLAGS[key]
+        else:
+            return False
+
+    def inject(self, msg_str, appdata=None):
+        """Inject encrypted data in the stream
+
+        if appdata is not None, we are sending a message in sendMessageDataTrigger
+        stanza will be injected directly if appdata is None,
+        else we just update the element and follow normal workflow
+        @param msg_str(str): encrypted message body
+        @param appdata(None, dict): None for signal message,
+            message data when an encrypted message is going to be sent
+        """
+        assert isinstance(self.peer, jid.JID)
+        msg = msg_str.decode('utf-8')
+        client = self.user.client
+        log.debug("injecting encrypted message to {to}".format(to=self.peer))
+        if appdata is None:
+            mess_data = {
+                "from": client.jid,
+                "to": self.peer,
+                "uid": str(uuid.uuid4()),
+                "message": {"": msg},
+                "subject": {},
+                "type": "chat",
+                "extra": {},
+                "timestamp": time.time(),
+            }
+            client.generate_message_xml(mess_data)
+            xml = mess_data['xml']
+            self._p_carbons.set_private(xml)
+            self._p_hints.add_hint_elements(xml, [
+                self._p_hints.HINT_NO_COPY,
+                self._p_hints.HINT_NO_PERMANENT_STORE])
+            client.send(mess_data["xml"])
+        else:
+            message_elt = appdata["xml"]
+            assert message_elt.name == "message"
+            message_elt.addElement("body", content=msg)
+
+    def stop_cb(self, __, feedback):
+        client = self.user.client
+        self.host.bridge.otr_state(
+            OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile
+        )
+        client.feedback(self.peer, feedback)
+
+    def stop_eb(self, failure_):
+        # encryption may be already stopped in case of manual stop
+        if not failure_.check(exceptions.NotFound):
+            log.error("Error while stopping OTR encryption: {msg}".format(msg=failure_))
+
+    def is_trusted(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 == 'trusted':
+            return True
+        else:
+            log.error("Unexpected getCurrentTrust() value: {value}".format(
+                value=trusted))
+            return False
+
+    def set_state(self, state):
+        client = self.user.client
+        old_state = self.state
+        super(Context, self).set_state(state)
+        log.debug("set_state: %s (old_state=%s)" % (state, old_state))
+
+        if state == potr.context.STATE_PLAINTEXT:
+            feedback = _("/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {
+                "other_jid": self.peer.full()
+            }
+            d = defer.ensureDeferred(client.encryption.stop(self.peer, NS_OTR))
+            d.addCallback(self.stop_cb, feedback=feedback)
+            d.addErrback(self.stop_eb)
+            return
+        elif state == potr.context.STATE_ENCRYPTED:
+            defer.ensureDeferred(client.encryption.start(self.peer, NS_OTR))
+            try:
+                trusted = self.is_trusted()
+            except TypeError:
+                trusted = False
+            trusted_str = _("trusted") if trusted else _("untrusted")
+
+            if old_state == potr.context.STATE_ENCRYPTED:
+                feedback = D_(
+                    "{trusted} OTR conversation with {other_jid} REFRESHED"
+                ).format(trusted=trusted_str, other_jid=self.peer.full())
+            else:
+                feedback = D_(
+                    "{trusted} encrypted OTR conversation started with {other_jid}\n"
+                    "{extra_info}"
+                ).format(
+                    trusted=trusted_str,
+                    other_jid=self.peer.full(),
+                    extra_info=NO_ADV_FEATURES,
+                )
+            self.host.bridge.otr_state(
+                OTR_STATE_ENCRYPTED, self.peer.full(), client.profile
+            )
+        elif state == potr.context.STATE_FINISHED:
+            feedback = D_("OTR conversation with {other_jid} is FINISHED").format(
+                other_jid=self.peer.full()
+            )
+            d = defer.ensureDeferred(client.encryption.stop(self.peer, NS_OTR))
+            d.addCallback(self.stop_cb, feedback=feedback)
+            d.addErrback(self.stop_eb)
+            return
+        else:
+            log.error(D_("Unknown OTR state"))
+            return
+
+        client.feedback(self.peer, feedback)
+
+    def disconnect(self):
+        """Disconnect the session."""
+        if self.state != potr.context.STATE_PLAINTEXT:
+            super(Context, self).disconnect()
+
+    def finish(self):
+        """Finish the session
+
+        avoid to send any message but the user still has to end the session himself.
+        """
+        if self.state == potr.context.STATE_ENCRYPTED:
+            self.processTLVs([potr.proto.DisconnectTLV()])
+
+
+class Account(potr.context.Account):
+    # TODO: manage trusted keys: if a fingerprint is not used anymore,
+    #       we have no way to remove it from database yet (same thing for a
+    #       correspondent jid)
+    # TODO: manage explicit message encryption
+
+    def __init__(self, host, client):
+        log.debug("new account: %s" % client.jid)
+        if not client.jid.resource:
+            log.warning("Account created without resource")
+        super(Account, self).__init__(str(client.jid), "xmpp", 1024)
+        self.host = host
+        self.client = client
+
+    def load_privkey(self):
+        log.debug("load_privkey")
+        return self.privkey
+
+    def save_privkey(self):
+        log.debug("save_privkey")
+        if self.privkey is None:
+            raise exceptions.InternalError(_("Save is called but privkey is None !"))
+        priv_key = hexlify(self.privkey.serializePrivateKey())
+        encrypted_priv_key = self.host.memory.encrypt_value(priv_key, self.client.profile)
+        self.client._otr_data[PRIVATE_KEY] = encrypted_priv_key
+
+    def load_trusts(self):
+        trust_data = self.client._otr_data.get("trust", {})
+        for jid_, jid_data in trust_data.items():
+            for fingerprint, trust_level in jid_data.items():
+                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 save_trusts(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 set_trust(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).set_trust(other_jid, fingerprint, trustLevel)
+
+
+class ContextManager(object):
+    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 start_context(self, other_jid):
+        assert isinstance(other_jid, jid.JID)
+        context = self.contexts.setdefault(
+            other_jid, Context(self, other_jid)
+        )
+        return context
+
+    def get_context_for_user(self, other):
+        log.debug("get_context_for_user [%s]" % other)
+        if not other.resource:
+            log.warning("get_context_for_user called with a bare jid: %s" % other.full())
+        return self.start_context(other)
+
+
+class OTR(object):
+
+    def __init__(self, host):
+        log.info(_("OTR plugin initialization"))
+        self.host = host
+        self.context_managers = {}
+        self.skipped_profiles = (
+            set()
+        )  #  FIXME: OTR should not be skipped per profile, this need to be refactored
+        self._p_hints = host.plugins["XEP-0334"]
+        self._p_carbons = host.plugins["XEP-0280"]
+        host.trigger.add("message_received", self.message_received_trigger, priority=100000)
+        host.trigger.add("sendMessage", self.send_message_trigger, priority=100000)
+        host.trigger.add("send_message_data", self._send_message_data_trigger)
+        host.bridge.add_method(
+            "skip_otr", ".plugin", in_sign="s", out_sign="", method=self._skip_otr
+        )  # FIXME: must be removed, must be done on per-message basis
+        host.bridge.add_signal(
+            "otr_state", ".plugin", signature="sss"
+        )  # args: state, destinee_jid, profile
+        # XXX: menus are disabled in favor to the new more generic encryption menu
+        #      there are let here commented for a little while as a reference
+        # host.import_menu(
+        #     (OTR_MENU, D_(u"Start/Refresh")),
+        #     self._otr_start_refresh,
+        #     security_limit=0,
+        #     help_string=D_(u"Start or refresh an OTR session"),
+        #     type_=C.MENU_SINGLE,
+        # )
+        # host.import_menu(
+        #     (OTR_MENU, D_(u"End session")),
+        #     self._otr_session_end,
+        #     security_limit=0,
+        #     help_string=D_(u"Finish an OTR session"),
+        #     type_=C.MENU_SINGLE,
+        # )
+        # host.import_menu(
+        #     (OTR_MENU, D_(u"Authenticate")),
+        #     self._otr_authenticate,
+        #     security_limit=0,
+        #     help_string=D_(u"Authenticate user/see your fingerprint"),
+        #     type_=C.MENU_SINGLE,
+        # )
+        # host.import_menu(
+        #     (OTR_MENU, D_(u"Drop private key")),
+        #     self._drop_priv_key,
+        #     security_limit=0,
+        #     type_=C.MENU_SINGLE,
+        # )
+        host.trigger.add("presence_received", self._presence_received_trigger)
+        self.host.register_encryption_plugin(self, "OTR", NS_OTR, directed=True)
+
+    def _skip_otr(self, profile):
+        """Tell the backend to not handle OTR for this profile.
+
+        @param profile (str): %(doc_profile)s
+        """
+        # FIXME: should not be done per profile but per message, using extra data
+        #        for message received, profile wide hook may be need, but client
+        #        should be used anyway instead of a class attribute
+        self.skipped_profiles.add(profile)
+
+    @defer.inlineCallbacks
+    def profile_connecting(self, client):
+        if client.profile in self.skipped_profiles:
+            return
+        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)
+        if encrypted_priv_key is not None:
+            priv_key = self.host.memory.decrypt_value(
+                encrypted_priv_key, client.profile
+            )
+            ctxMng.account.privkey = potr.crypt.PK.parsePrivateKey(
+                unhexlify(priv_key.encode('utf-8'))
+            )[0]
+        else:
+            ctxMng.account.privkey = None
+        ctxMng.account.load_trusts()
+
+    def profile_disconnected(self, client):
+        if client.profile in self.skipped_profiles:
+            self.skipped_profiles.remove(client.profile)
+            return
+        for context in list(client._otr_context_manager.contexts.values()):
+            context.disconnect()
+        del client._otr_context_manager
+
+    # encryption plugin methods
+
+    def start_encryption(self, client, entity_jid):
+        self.start_refresh(client, entity_jid)
+
+    def stop_encryption(self, client, entity_jid):
+        self.end_session(client, entity_jid)
+
+    def get_trust_ui(self, client, entity_jid):
+        if not entity_jid.resource:
+            entity_jid.resource = self.host.memory.main_resource_get(
+                client, entity_jid
+            )  # FIXME: temporary and unsecure, must be changed when frontends
+               #        are refactored
+        ctxMng = client._otr_context_manager
+        otrctx = ctxMng.get_context_for_user(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: _(
+                        "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 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: _(
+                        "Your fingerprint is:\n{fingerprint}\n\n"
+                        "Start an OTR conversation to have your correspondent one."
+                    ).format(fingerprint=priv_key),
+                    C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO,
+                },
+                title=_("Fingerprint"),
+            )
+            return dialog
+
+        def set_trust(raw_data, profile):
+            if xml_tools.is_xmlui_cancelled(raw_data):
+                return {}
+            # This method is called when authentication form is submited
+            data = xml_tools.xmlui_result_2_data_form_result(raw_data)
+            if data["match"] == "yes":
+                otrctx.setCurrentTrust(OTR_STATE_TRUSTED)
+                note_msg = _("Your correspondent {correspondent} is now TRUSTED")
+                self.host.bridge.otr_state(
+                    OTR_STATE_TRUSTED, entity_jid.full(), client.profile
+                )
+            else:
+                otrctx.setCurrentTrust("")
+                note_msg = _("Your correspondent {correspondent} is now UNTRUSTED")
+                self.host.bridge.otr_state(
+                    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.register_callback(set_trust, with_data=True, one_shot=True)
+        trusted = otrctx.is_trusted()
+
+        xmlui = xml_tools.XMLUI(
+            C.XMLUI_FORM,
+            title=_("Authentication ({entity_jid})").format(entity_jid=entity_jid.full()),
+            submit_id=submit_id,
+        )
+        xmlui.addText(_(AUTH_TXT))
+        xmlui.addDivider()
+        xmlui.addText(
+            D_("Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key)
+        )
+        xmlui.addText(
+            D_("Your correspondent fingerprint should be:\n{fingerprint}").format(
+                fingerprint=other_fingerprint
+            )
+        )
+        xmlui.addDivider("blank")
+        xmlui.change_container("pairs")
+        xmlui.addLabel(D_("Is your correspondent fingerprint the same as here ?"))
+        xmlui.addList(
+            "match", [("yes", _("yes")), ("no", _("no"))], ["yes" if trusted else "no"]
+        )
+        return xmlui
+
+    def _otr_start_refresh(self, menu_data, profile):
+        """Start or refresh an OTR session
+
+        @param menu_data: %(menu_data)s
+        @param profile: %(doc_profile)s
+        """
+        client = self.host.get_client(profile)
+        try:
+            to_jid = jid.JID(menu_data["jid"])
+        except KeyError:
+            log.error(_("jid key is not present !"))
+            return defer.fail(exceptions.DataError)
+        self.start_refresh(client, to_jid)
+        return {}
+
+    def start_refresh(self, client, to_jid):
+        """Start or refresh an OTR session
+
+        @param to_jid(jid.JID): jid to start encrypted session with
+        """
+        encrypted_session = client.encryption.getSession(to_jid.userhostJID())
+        if encrypted_session and encrypted_session['plugin'].namespace != NS_OTR:
+            raise exceptions.ConflictError(_(
+                "Can't start an OTR session, there is already an encrypted session "
+                "with {name}").format(name=encrypted_session['plugin'].name))
+        if not to_jid.resource:
+            to_jid.resource = self.host.memory.main_resource_get(
+                client, to_jid
+            )  # FIXME: temporary and unsecure, must be changed when frontends
+               #        are refactored
+        otrctx = client._otr_context_manager.get_context_for_user(to_jid)
+        query = otrctx.sendMessage(0, b"?OTRv?")
+        otrctx.inject(query)
+
+    def _otr_session_end(self, menu_data, profile):
+        """End an OTR session
+
+        @param menu_data: %(menu_data)s
+        @param profile: %(doc_profile)s
+        """
+        client = self.host.get_client(profile)
+        try:
+            to_jid = jid.JID(menu_data["jid"])
+        except KeyError:
+            log.error(_("jid key is not present !"))
+            return defer.fail(exceptions.DataError)
+        self.end_session(client, to_jid)
+        return {}
+
+    def end_session(self, client, to_jid):
+        """End an OTR session"""
+        if not to_jid.resource:
+            to_jid.resource = self.host.memory.main_resource_get(
+                client, to_jid
+            )  # FIXME: temporary and unsecure, must be changed when frontends
+               #        are refactored
+        otrctx = client._otr_context_manager.get_context_for_user(to_jid)
+        otrctx.disconnect()
+        return {}
+
+    def _otr_authenticate(self, menu_data, profile):
+        """End an OTR session
+
+        @param menu_data: %(menu_data)s
+        @param profile: %(doc_profile)s
+        """
+        client = self.host.get_client(profile)
+        try:
+            to_jid = jid.JID(menu_data["jid"])
+        except KeyError:
+            log.error(_("jid key is not present !"))
+            return defer.fail(exceptions.DataError)
+        return self.authenticate(client, to_jid)
+
+    def authenticate(self, client, to_jid):
+        """Authenticate other user and see our own fingerprint"""
+        xmlui = self.get_trust_ui(client, to_jid)
+        return {"xmlui": xmlui.toXml()}
+
+    def _drop_priv_key(self, menu_data, profile):
+        """Drop our private Key
+
+        @param menu_data: %(menu_data)s
+        @param profile: %(doc_profile)s
+        """
+        client = self.host.get_client(profile)
+        try:
+            to_jid = jid.JID(menu_data["jid"])
+            if not to_jid.resource:
+                to_jid.resource = self.host.memory.main_resource_get(
+                    client, to_jid
+                )  # 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)
+
+        ctxMng = client._otr_context_manager
+        if ctxMng.account.privkey is None:
+            return {
+                "xmlui": xml_tools.note(_("You don't have a private key yet !")).toXml()
+            }
+
+        def drop_key(data, profile):
+            if C.bool(data["answer"]):
+                # we end all sessions
+                for context in list(ctxMng.contexts.values()):
+                    context.disconnect()
+                ctxMng.account.privkey = None
+                ctxMng.account.getPrivkey()  # as account.privkey is None, getPrivkey
+                                             # will generate a new key, and save it
+                return {
+                    "xmlui": xml_tools.note(
+                        D_("Your private key has been dropped")
+                    ).toXml()
+                }
+            return {}
+
+        submit_id = self.host.register_callback(drop_key, with_data=True, one_shot=True)
+
+        confirm = xml_tools.XMLUI(
+            C.XMLUI_DIALOG,
+            title=_("Confirm private key drop"),
+            dialog_opt={"type": C.XMLUI_DIALOG_CONFIRM, "message": _(DROP_TXT)},
+            submit_id=submit_id,
+        )
+        return {"xmlui": confirm.toXml()}
+
+    def _received_treatment(self, data, client):
+        from_jid = data["from"]
+        log.debug("_received_treatment [from_jid = %s]" % from_jid)
+        otrctx = client._otr_context_manager.get_context_for_user(from_jid)
+
+        try:
+            message = (
+                next(iter(data["message"].values()))
+            )  # FIXME: Q&D fix for message refactoring, message is now a dict
+            res = otrctx.receiveMessage(message.encode("utf-8"))
+        except (potr.context.UnencryptedMessage, potr.context.NotOTRMessage):
+            # potr has a bug with Python 3 and test message against str while bytes are
+            # expected, resulting in a NoOTRMessage raised instead of UnencryptedMessage;
+            # so we catch NotOTRMessage as a workaround
+            # TODO: report this upstream
+            encrypted = False
+            if otrctx.state == potr.context.STATE_ENCRYPTED:
+                log.warning(
+                    "Received unencrypted message in an encrypted context (from {jid})"
+                    .format(jid=from_jid.full())
+                )
+
+                feedback = (
+                    D_(
+                        "WARNING: received unencrypted data in a supposedly encrypted "
+                        "context"
+                    ),
+                )
+                client.feedback(from_jid, feedback)
+        except potr.context.NotEncryptedError:
+            msg = D_("WARNING: received OTR encrypted data in an unencrypted context")
+            log.warning(msg)
+            feedback = msg
+            client.feedback(from_jid, msg)
+            raise failure.Failure(exceptions.CancelError(msg))
+        except potr.context.ErrorReceived as e:
+            msg = D_("WARNING: received OTR error message: {msg}".format(msg=e))
+            log.warning(msg)
+            feedback = msg
+            client.feedback(from_jid, msg)
+            raise failure.Failure(exceptions.CancelError(msg))
+        except potr.crypt.InvalidParameterError as e:
+            msg = D_("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:
+            encrypted = True
+
+        if encrypted:
+            if res[0] != None:
+                # decrypted messages handling.
+                # receiveMessage() will return a tuple,
+                # the first part of which will be the decrypted message
+                data["message"] = {
+                    "": res[0]
+                }  # FIXME: Q&D fix for message refactoring, message is now a dict
+                try:
+                    # we want to keep message in history, even if no store is
+                    # requested in message hints
+                    del data["history"]
+                except KeyError:
+                    pass
+                # TODO: add skip history as an option, but by default we don't skip it
+                # data[u'history'] = C.HISTORY_SKIP # we send the decrypted message to
+                                                    # frontends, but we don't want it in
+                                                    # history
+            else:
+                raise failure.Failure(
+                    exceptions.CancelError("Cancelled by OTR")
+                )  # no message at all (no history, no signal)
+
+            client.encryption.mark_as_encrypted(data, namespace=NS_OTR)
+            trusted = otrctx.is_trusted()
+
+            if trusted:
+                client.encryption.mark_as_trusted(data)
+            else:
+                client.encryption.mark_as_untrusted(data)
+
+        return data
+
+    def _received_treatment_for_skipped_profiles(self, data):
+        """This profile must be skipped because the frontend manages OTR itself,
+
+        but we still need to check if the message must be stored in history or not
+        """
+        #  XXX: FIXME: this should not be done on a per-profile basis, but  per-message
+        try:
+            message = (
+                iter(data["message"].values()).next().encode("utf-8")
+            )  # FIXME: Q&D fix for message refactoring, message is now a dict
+        except StopIteration:
+            return data
+        if message.startswith(potr.proto.OTRTAG):
+            #  FIXME: it may be better to cancel the message and send it direclty to
+            #         bridge
+            #        this is used by Libervia, but this may send garbage message to
+            #        other frontends
+            #        if they are used at the same time as Libervia.
+            #        Hard to avoid with decryption on Libervia though.
+            data["history"] = C.HISTORY_SKIP
+        return data
+
+    def message_received_trigger(self, client, message_elt, post_treat):
+        if client.is_component:
+            return True
+        if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
+            # OTR is not possible in group chats
+            return True
+        from_jid = jid.JID(message_elt['from'])
+        if not from_jid.resource or from_jid.userhostJID() == client.jid.userhostJID():
+            # OTR is only usable when resources are present
+            return True
+        if client.profile in self.skipped_profiles:
+            post_treat.addCallback(self._received_treatment_for_skipped_profiles)
+        else:
+            post_treat.addCallback(self._received_treatment, client)
+        return True
+
+    def _send_message_data_trigger(self, client, mess_data):
+        if client.is_component:
+            return True
+        encryption = mess_data.get(C.MESS_KEY_ENCRYPTION)
+        if encryption is None or encryption['plugin'].namespace != NS_OTR:
+            return
+        to_jid = mess_data['to']
+        if not to_jid.resource:
+            to_jid.resource = self.host.memory.main_resource_get(
+                client, to_jid
+            )  # FIXME: temporary and unsecure, must be changed when frontends
+        otrctx = client._otr_context_manager.get_context_for_user(to_jid)
+        message_elt = mess_data["xml"]
+        if otrctx.state == potr.context.STATE_ENCRYPTED:
+            log.debug("encrypting message")
+            body = None
+            for child in list(message_elt.children):
+                if child.name == "body":
+                    # we remove all unencrypted body,
+                    # and will only encrypt the first one
+                    if body is None:
+                        body = child
+                    message_elt.children.remove(child)
+                elif child.name == "html":
+                    # we don't want any XHTML-IM element
+                    message_elt.children.remove(child)
+            if body is None:
+                log.warning("No message found")
+            else:
+                self._p_carbons.set_private(message_elt)
+                self._p_hints.add_hint_elements(message_elt, [
+                    self._p_hints.HINT_NO_COPY,
+                    self._p_hints.HINT_NO_PERMANENT_STORE])
+                otrctx.sendMessage(0, str(body).encode("utf-8"), appdata=mess_data)
+        else:
+            feedback = D_(
+                "Your message was not sent because your correspondent closed the "
+                "encrypted conversation on his/her side. "
+                "Either close your own side, or refresh the session."
+            )
+            log.warning(_("Message discarded because closed encryption channel"))
+            client.feedback(to_jid, feedback)
+            raise failure.Failure(exceptions.CancelError("Cancelled by OTR plugin"))
+
+    def send_message_trigger(self, client, mess_data, pre_xml_treatments,
+                           post_xml_treatments):
+        if client.is_component:
+            return True
+        if mess_data["type"] == "groupchat":
+            return True
+
+        if client.profile in self.skipped_profiles:
+            #  FIXME: should not be done on a per-profile basis
+            return True
+
+        to_jid = copy.copy(mess_data["to"])
+        if client.encryption.getSession(to_jid.userhostJID()):
+            # there is already an encrypted session with this entity
+            return True
+
+        if not to_jid.resource:
+            to_jid.resource = self.host.memory.main_resource_get(
+                client, to_jid
+            )  # FIXME: full jid may not be known
+
+        otrctx = client._otr_context_manager.get_context_for_user(to_jid)
+
+        if otrctx.state != potr.context.STATE_PLAINTEXT:
+            defer.ensureDeferred(client.encryption.start(to_jid, NS_OTR))
+            client.encryption.set_encryption_flag(mess_data)
+            if not mess_data["to"].resource:
+                # if not resource was given, we force it here
+                mess_data["to"] = to_jid
+        return True
+
+    def _presence_received_trigger(self, client, entity, show, priority, statuses):
+        if show != C.PRESENCE_UNAVAILABLE:
+            return True
+        if not entity.resource:
+            try:
+                entity.resource = self.host.memory.main_resource_get(
+                    client, entity
+                )  # FIXME: temporary and unsecure, must be changed when frontends
+                   #        are refactored
+            except exceptions.UnknownEntityError:
+                return True  #  entity was not connected
+        if entity in client._otr_context_manager.contexts:
+            otrctx = client._otr_context_manager.get_context_for_user(entity)
+            otrctx.disconnect()
+        return True