view sat/plugins/plugin_sec_otr.py @ 3671:9c50d2f812c1

docker (e2e): add `pytest-twisted` to image
author Goffi <goffi@goffi.org>
date Wed, 08 Sep 2021 17:58:48 +0200
parents be6d91572633
children 967a8e109cda
line wrap: on
line source

#!/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 sat.core.i18n import _, D_
from sat.core.constants import Const as C
from sat.core.log import getLogger
from sat.core import exceptions
from sat.tools import xml_tools
from twisted.words.protocols.jabber import jid
from twisted.python import failure
from twisted.internet import defer
from sat.memory import persistent
import potr

log = getLogger(__name__)


PLUGIN_INFO = {
    C.PI_NAME: "OTR",
    C.PI_IMPORT_NAME: "OTR",
    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 getPolicy(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.generateMessageXML(mess_data)
            xml = mess_data['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["xml"]
            assert message_elt.name == "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("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 == 'trusted':
            return True
        else:
            log.error("Unexpected getCurrentTrust() value: {value}".format(
                value=trusted))
            return False

    def setState(self, state):
        client = self.user.client
        old_state = self.state
        super(Context, self).setState(state)
        log.debug("setState: %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.stopCb, feedback=feedback)
            d.addErrback(self.stopEb)
            return
        elif state == potr.context.STATE_ENCRYPTED:
            defer.ensureDeferred(client.encryption.start(self.peer, NS_OTR))
            try:
                trusted = self.isTrusted()
            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.otrState(
                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.stopCb, feedback=feedback)
            d.addErrback(self.stopEb)
            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 loadPrivkey(self):
        log.debug("loadPrivkey")
        return self.privkey

    def savePrivkey(self):
        log.debug("savePrivkey")
        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.encryptValue(priv_key, self.client.profile)
        self.client._otr_data[PRIVATE_KEY] = encrypted_priv_key

    def loadTrusts(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 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):
    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, other_jid)
        )
        return context

    def getContextForUser(self, other):
        log.debug("getContextForUser [%s]" % other)
        if not other.resource:
            log.warning("getContextForUser called with a bare jid: %s" % other.full())
        return self.startContext(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("messageReceived", self.messageReceivedTrigger, priority=100000)
        host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000)
        host.trigger.add("sendMessageData", self._sendMessageDataTrigger)
        host.bridge.addMethod(
            "skipOTR", ".plugin", in_sign="s", out_sign="", method=self._skipOTR
        )  # FIXME: must be removed, must be done on per-message basis
        host.bridge.addSignal(
            "otrState", ".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.importMenu(
        #     (OTR_MENU, D_(u"Start/Refresh")),
        #     self._otrStartRefresh,
        #     security_limit=0,
        #     help_string=D_(u"Start or refresh an OTR session"),
        #     type_=C.MENU_SINGLE,
        # )
        # host.importMenu(
        #     (OTR_MENU, D_(u"End session")),
        #     self._otrSessionEnd,
        #     security_limit=0,
        #     help_string=D_(u"Finish an OTR session"),
        #     type_=C.MENU_SINGLE,
        # )
        # host.importMenu(
        #     (OTR_MENU, D_(u"Authenticate")),
        #     self._otrAuthenticate,
        #     security_limit=0,
        #     help_string=D_(u"Authenticate user/see your fingerprint"),
        #     type_=C.MENU_SINGLE,
        # )
        # host.importMenu(
        #     (OTR_MENU, D_(u"Drop private key")),
        #     self._dropPrivKey,
        #     security_limit=0,
        #     type_=C.MENU_SINGLE,
        # )
        host.trigger.add("presence_received", self._presenceReceivedTrigger)
        self.host.registerEncryptionPlugin(self, "OTR", NS_OTR, directed=True)

    def _skipOTR(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 profileConnecting(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.decryptValue(
                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.loadTrusts()

    def profileDisconnected(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 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: _(
                        "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 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 = _("Your correspondent {correspondent} is now TRUSTED")
                self.host.bridge.otrState(
                    OTR_STATE_TRUSTED, entity_jid.full(), client.profile
                )
            else:
                otrctx.setCurrentTrust("")
                note_msg = _("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=_("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.changeContainer("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 _otrStartRefresh(self, menu_data, profile):
        """Start or refresh an OTR session

        @param menu_data: %(menu_data)s
        @param profile: %(doc_profile)s
        """
        client = self.host.getClient(profile)
        try:
            to_jid = jid.JID(menu_data["jid"])
        except KeyError:
            log.error(_("jid key is not present !"))
            return defer.fail(exceptions.DataError)
        self.startRefresh(client, to_jid)
        return {}

    def startRefresh(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.getMainResource(
                client, to_jid
            )  # FIXME: temporary and unsecure, must be changed when frontends
               #        are refactored
        otrctx = client._otr_context_manager.getContextForUser(to_jid)
        query = otrctx.sendMessage(0, b"?OTRv?")
        otrctx.inject(query)

    def _otrSessionEnd(self, menu_data, profile):
        """End an OTR session

        @param menu_data: %(menu_data)s
        @param profile: %(doc_profile)s
        """
        client = self.host.getClient(profile)
        try:
            to_jid = jid.JID(menu_data["jid"])
        except KeyError:
            log.error(_("jid key is not present !"))
            return defer.fail(exceptions.DataError)
        self.endSession(client, to_jid)
        return {}

    def endSession(self, client, to_jid):
        """End an OTR session"""
        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
        otrctx = client._otr_context_manager.getContextForUser(to_jid)
        otrctx.disconnect()
        return {}

    def _otrAuthenticate(self, menu_data, profile):
        """End an OTR session

        @param menu_data: %(menu_data)s
        @param profile: %(doc_profile)s
        """
        client = self.host.getClient(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.getTrustUI(client, to_jid)
        return {"xmlui": xmlui.toXml()}

    def _dropPrivKey(self, menu_data, profile):
        """Drop our private Key

        @param menu_data: %(menu_data)s
        @param profile: %(doc_profile)s
        """
        client = self.host.getClient(profile)
        try:
            to_jid = jid.JID(menu_data["jid"])
            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
        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 dropKey(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.registerCallback(dropKey, 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 _receivedTreatment(self, data, client):
        from_jid = data["from"]
        log.debug("_receivedTreatment [from_jid = %s]" % from_jid)
        otrctx = client._otr_context_manager.getContextForUser(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.markAsEncrypted(data, namespace=NS_OTR)
            trusted = otrctx.isTrusted()

            if trusted:
                client.encryption.markAsTrusted(data)
            else:
                client.encryption.markAsUntrusted(data)

        return data

    def _receivedTreatmentForSkippedProfiles(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 messageReceivedTrigger(self, client, message_elt, post_treat):
        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._receivedTreatmentForSkippedProfiles)
        else:
            post_treat.addCallback(self._receivedTreatment, client)
        return True

    def _sendMessageDataTrigger(self, client, mess_data):
        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.getMainResource(
                client, to_jid
            )  # FIXME: temporary and unsecure, must be changed when frontends
        otrctx = client._otr_context_manager.getContextForUser(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.setPrivate(message_elt)
                self._p_hints.addHintElements(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 sendMessageTrigger(self, client, mess_data, pre_xml_treatments,
                           post_xml_treatments):
        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.getMainResource(
                client, to_jid
            )  # FIXME: full jid may not be known

        otrctx = client._otr_context_manager.getContextForUser(to_jid)

        if otrctx.state != potr.context.STATE_PLAINTEXT:
            defer.ensureDeferred(client.encryption.start(to_jid, NS_OTR))
            client.encryption.setEncryptionFlag(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 _presenceReceivedTrigger(self, client, entity, show, priority, statuses):
        if show != C.PRESENCE_UNAVAILABLE:
            return True
        if not entity.resource:
            try:
                entity.resource = self.host.memory.getMainResource(
                    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.getContextForUser(entity)
            otrctx.disconnect()
        return True