Mercurial > libervia-backend
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