Mercurial > libervia-backend
changeset 2128:aa94f33fd2ad
plugin otr: various improvments:
- messageSend trigger now use pre_xml_treatments so it doesn't block other triggers of higher priority
- text commands now use a very high priority, as it is local command and should not be blocked in most of cases
- new otrState signal, to send state change to frontends
- history is not skipped anymore, a future option may change this behaviour
- OTR trigger are skipped on groupchat messages
- context_manager is now in client instead of being global to plugin
- removed fixPotr as it is fixed upstream
note triggers should be improved for encryption methods, as skipping an encrypter may break security, but putting it in top priority may break nice features.
fix bug 170
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 01 Feb 2017 21:44:24 +0100 |
parents | 8717e9cc95c0 |
children | 6a66c8c5a567 |
files | frontends/src/quick_frontend/quick_app.py src/core/sat_main.py src/plugins/plugin_exp_parrot.py src/plugins/plugin_misc_text_commands.py src/plugins/plugin_sec_otr.py |
diffstat | 5 files changed, 112 insertions(+), 99 deletions(-) [+] |
line wrap: on
line diff
--- a/frontends/src/quick_frontend/quick_app.py Tue Jan 31 22:35:59 2017 +0100 +++ b/frontends/src/quick_frontend/quick_app.py Wed Feb 01 21:44:24 2017 +0100 @@ -323,6 +323,7 @@ @param iface (str): interface of the bridge to use ('core' or 'plugin') @param with_profile (boolean): True if the signal concerns a specific profile, in that case the profile name has to be passed by the caller """ + log.debug(u"registering signal {name}".format(name = function_name)) if handler is None: handler = getattr(self, "{}{}".format(function_name, 'Handler')) if not with_profile: @@ -493,6 +494,8 @@ def newWidget(self, widget): raise NotImplementedError + # bridge signals hanlers + def connectedHandler(self, profile, jid_s): """Called when the connection is made.
--- a/src/core/sat_main.py Tue Jan 31 22:35:59 2017 +0100 +++ b/src/core/sat_main.py Wed Feb 01 21:44:24 2017 +0100 @@ -642,7 +642,7 @@ # FIXME: send_only is used by libervia's OTR plugin to avoid # the triggers from frontend, and no_trigger do the same # thing internally, this could be unified - send_only = data['extra'].get('send_only', None) + send_only = data['extra'].get('send_only', False) if not no_trigger and not send_only: if not self.trigger.point("messageSend", client, data, pre_xml_treatments, post_xml_treatments):
--- a/src/plugins/plugin_exp_parrot.py Tue Jan 31 22:35:59 2017 +0100 +++ b/src/plugins/plugin_exp_parrot.py Wed Feb 01 21:44:24 2017 +0100 @@ -35,16 +35,17 @@ "recommendations": [C.TEXT_CMDS], "main": "Exp_Parrot", "handler": "no", - "description": _("""Implementation of parrot mode (repeat messages between 2 entities)""") + "description": _(u"""Implementation of parrot mode (repeat messages between 2 entities)""") } class Exp_Parrot(object): """Parrot mode plugin: repeat messages from one entity or MUC room to another one""" - #XXX: This plugin can be potentially dangerous if we don't trust entities linked - # this is specially true if we have other triggers. - # messageSendTrigger avoid other triggers execution, it's deactivated to allow - # /unparrot command in text commands plugin. + # XXX: This plugin can be potentially dangerous if we don't trust entities linked + # this is specially true if we have other triggers. + # messageSendTrigger avoid other triggers execution, it's deactivated to allow + # /unparrot command in text commands plugin. + # FIXME: potentially unsecure, specially with e2e encryption def __init__(self, host): log.info(_("Plugin Parrot initialization")) @@ -54,7 +55,7 @@ try: self.host.plugins[C.TEXT_CMDS].registerTextCommands(self) except KeyError: - log.info(_("Text commands not available")) + log.info(_(u"Text commands not available")) #def messageSendTrigger(self, client, mess_data, treatments): # """ Deactivate other triggers if recipient is in parrot links """
--- a/src/plugins/plugin_misc_text_commands.py Tue Jan 31 22:35:59 2017 +0100 +++ b/src/plugins/plugin_misc_text_commands.py Wed Feb 01 21:44:24 2017 +0100 @@ -59,7 +59,8 @@ def __init__(self, host): log.info(_("Text commands initialization")) self.host = host - host.trigger.add("messageSend", self.messageSendTrigger) + # this is internal command, so we set high priority + host.trigger.add("messageSend", self.messageSendTrigger, priority=1000000) self._commands = {} self._whois = [] self.registerTextCommands(self)
--- a/src/plugins/plugin_sec_otr.py Tue Jan 31 22:35:59 2017 +0100 +++ b/src/plugins/plugin_sec_otr.py Wed Feb 01 21:44:24 2017 +0100 @@ -35,19 +35,6 @@ import time import uuid -NS_OTR = "otr_plugin" -PRIVATE_KEY = "PRIVATE KEY" -OTR_MENU = D_(u'OTR') -AUTH_TXT = D_(u"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_(u"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 = D_(u"/!\\Your history is not logged anymore, and most of advanced features are disabled !") # FIXME: not used at the moment - -DEFAULT_POLICY_FLAGS = { - 'ALLOW_V1':False, - 'ALLOW_V2':True, - 'REQUIRE_ENCRYPTION':True, -} - PLUGIN_INFO = { "name": "OTR", @@ -60,6 +47,25 @@ "description": _(u"""Implementation of OTR""") } +NS_OTR = "otr_plugin" +PRIVATE_KEY = "PRIVATE KEY" +OTR_MENU = D_(u'OTR') +AUTH_TXT = D_(u"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_(u"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_(u"Most 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, host, account, other_jid): @@ -91,12 +97,14 @@ client.xmlstream.send(mess_data['xml']) def setState(self, state): + client = self.user.client old_state = self.state super(Context, self).setState(state) log.debug(u"setState: %s (old_state=%s)" % (state, old_state)) if state == potr.context.STATE_PLAINTEXT: feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {'other_jid': self.peer.full()} + self.host.bridge.otrState(OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile) elif state == potr.context.STATE_ENCRYPTED: try: trusted = self.getCurrentTrust() @@ -109,17 +117,18 @@ trusted = trusted_str, other_jid = self.peer.full()) else: - feedback = D_(u"{trusted} Encrypted OTR conversation started with {other_jid}\n{no_log}").format( + feedback = D_(u"{trusted} encrypted OTR conversation started with {other_jid}\n{extra_info}").format( trusted = trusted_str, other_jid = self.peer.full(), - no_log = NO_LOG) + 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_(u"OTR conversation with {other_jid} is FINISHED").format(other_jid = self.peer.full()) + self.host.bridge.otrState(OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile) else: log.error(D_(u"Unknown OTR state")) return - client = self.user.client self.host.bridge.messageNew(uid=unicode(uuid.uuid4()), timestamp=time.time(), from_jid=client.jid.full(), @@ -129,7 +138,6 @@ mess_type=C.MESS_TYPE_INFO, extra={}, profile=client.profile) - # TODO: send signal to frontends def disconnect(self): """Disconnect the session.""" @@ -164,11 +172,11 @@ priv_key = self.privkey.serializePrivateKey().encode('hex') d = self.host.memory.encryptValue(priv_key, self.client.profile) def save_encrypted_key(encrypted_priv_key): - self.client.otr_data[PRIVATE_KEY] = encrypted_priv_key + self.client._otr_data[PRIVATE_KEY] = encrypted_priv_key d.addCallback(save_encrypted_key) def loadTrusts(self): - trust_data = self.client.otr_data.get('trust', {}) + trust_data = self.client._otr_data.get('trust', {}) for jid_, jid_data in trust_data.iteritems(): for fingerprint, trust_level in jid_data.iteritems(): log.debug(u'setting trust for {jid}: [{fingerprint}] = "{trust_level}"'.format(jid=jid_, fingerprint=fingerprint, trust_level=trust_level)) @@ -176,15 +184,15 @@ def saveTrusts(self): log.debug(u"saving trusts for {profile}".format(profile=self.client.profile)) - log.debug(u"trusts = {}".format(self.client.otr_data['trust'])) - self.client.otr_data.force('trust') + log.debug(u"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'] + trust_data = self.client._otr_data['trust'] except KeyError: trust_data = {} - self.client.otr_data['trust'] = 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) @@ -213,35 +221,19 @@ def __init__(self, host): log.info(_(u"OTR plugin initialization")) - self._fixPotr() # FIXME: to be removed when potr will be fixed self.host = host self.context_managers = {} - self.skipped_profiles = set() + self.skipped_profiles = set() # FIXME: OTR should not be skipped per profile, this need to be refactored host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000) host.trigger.add("messageSend", self.messageSendTrigger, priority=100000) 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 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("presenceReceived", self._presenceReceivedTrigger) - def _fixPotr(self): - # FIXME: potr fix for bad unicode handling - # this method monkeypatch it, must be removed when potr - # is fixed - - def getDefaultQueryMessage(self, policy): - defaultQuery = '?OTRv{versions}?\nI would like to start ' \ - 'an Off-the-Record private conversation. However, you ' \ - 'do not have a plugin to support that.\nSee '\ - 'https://otr.cypherpunks.ca/ for more information.' - v = '2' if policy('ALLOW_V2') else '' - msg = defaultQuery.format(versions=v) - return msg.encode('ascii') - - potr.context.Account.getDefaultQueryMessage = getDefaultQueryMessage - def _skipOTR(self, profile): """Tell the backend to not handle OTR for this profile. @@ -257,10 +249,10 @@ if profile in self.skipped_profiles: return client = self.host.getClient(profile) - ctxMng = self.context_managers[profile] = ContextManager(self.host, client) - client.otr_data = persistent.PersistentBinaryDict(NS_OTR, profile) - yield client.otr_data.load() - encrypted_priv_key = client.otr_data.get(PRIVATE_KEY, None) + ctxMng = client._otr_context_manager = ContextManager(self.host, client) + client._otr_data = persistent.PersistentBinaryDict(NS_OTR, profile) + yield client._otr_data.load() + encrypted_priv_key = client._otr_data.get(PRIVATE_KEY, None) if encrypted_priv_key is not None: priv_key = yield self.host.memory.decryptValue(encrypted_priv_key, profile) ctxMng.account.privkey = potr.crypt.PK.parsePrivateKey(priv_key.decode('hex'))[0] @@ -269,16 +261,13 @@ ctxMng.account.loadTrusts() def profileDisconnected(self, profile): - try: - for context in self.context_managers[profile].contexts.values(): - context.disconnect() - del self.context_managers[profile] - except KeyError: - pass - try: + if profile in self.skipped_profiles: self.skipped_profiles.remove(profile) - except KeyError: - pass + return + client = self.host.getClient(profile) + for context in client._otr_context_manager.contexts.values(): + context.disconnect() + del client._otr_context_manager def _otrStartRefresh(self, menu_data, profile): """Start or refresh an OTR session @@ -302,7 +291,7 @@ """ 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 = self.context_managers[client.profile].getContextForUser(to_jid) + otrctx = client._otr_context_manager.getContextForUser(to_jid) query = otrctx.sendMessage(0, '?OTRv?') otrctx.inject(query) @@ -325,7 +314,7 @@ """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 = self.context_managers[client.profile].getContextForUser(to_jid) + otrctx = client._otr_context_manager.getContextForUser(to_jid) otrctx.disconnect() return {} @@ -347,7 +336,7 @@ """Authenticate other user and see our own fingerprint""" if not to_jid.resource: to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored - ctxMng = self.context_managers[client.profile] + ctxMng = client._otr_context_manager otrctx = ctxMng.getContextForUser(to_jid) priv_key = ctxMng.account.privkey @@ -379,11 +368,13 @@ # This method is called when authentication form is submited data = xml_tools.XMLUIResult2DataFormResult(raw_data) if data['match'] == 'yes': - otrctx.setCurrentTrust('verified') + otrctx.setCurrentTrust(OTR_STATE_TRUSTED) note_msg = _(u"Your correspondent {correspondent} is now TRUSTED") + self.host.bridge.otrState(OTR_STATE_TRUSTED, to_jid.full(), client.profile) else: otrctx.setCurrentTrust('') note_msg = _(u"Your correspondent {correspondent} is now UNTRUSTED") + self.host.bridge.otrState(OTR_STATE_UNTRUSTED, to_jid.full(), client.profile) note = xml_tools.XMLUI(C.XMLUI_DIALOG, dialog_opt = { C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE, C.XMLUI_DATA_MESS: note_msg.format(correspondent=otrctx.peer)} @@ -419,7 +410,7 @@ log.error(_(u"jid key is not present !")) return defer.fail(exceptions.DataError) - ctxMng = self.context_managers[profile] + ctxMng = client._otr_context_manager if ctxMng.account.privkey is None: return {'xmlui': xml_tools.note(_(u"You don't have a private key yet !")).toXml()} @@ -430,27 +421,27 @@ 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(_(u"Your private key has been dropped")).toXml()} + return {'xmlui': xml_tools.note(D_(u"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) + confirm = xml_tools.XMLUI(C.XMLUI_DIALOG, title=_(u'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, profile): + def _receivedTreatment(self, data, client): from_jid = data['from'] log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid) - otrctx = self.context_managers[profile].getContextForUser(from_jid) - encrypted = True + otrctx = client._otr_context_manager.getContextForUser(from_jid) try: message = data['message'].itervalues().next() # FIXME: Q&D fix for message refactoring, message is now a dict res = otrctx.receiveMessage(message.encode('utf-8')) except potr.context.UnencryptedMessage: + encrypted = False if otrctx.state == potr.context.STATE_ENCRYPTED: - log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': from_jid.full()}) - client = self.host.getClient(profile) + log.warning(u"Received unencrypted message in an encrypted context (from {jid})".format( + jid = from_jid.full())) feedback=D_(u"WARNING: received unencrypted data in a supposedly encrypted context"), self.host.bridge.messageNew(uid=unicode(uuid.uuid4()), @@ -462,24 +453,29 @@ mess_type=C.MESS_TYPE_INFO, extra={}, profile=client.profile) - encrypted = False except StopIteration: return data + else: + encrypted = True - if not encrypted: - return data - else: + 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].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict - raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history + # TODO: add skip history as an option, but by default we don't skip it + # raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history else: + log.warning(u"An encrypted message was expected, but got {}".format(data['message'])) raise failure.Failure(exceptions.CancelError('Cancelled by OTR')) # no message at all (no history, no signal) + return data - def _receivedTreatmentForSkippedProfiles(self, data, profile): + 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""" + + 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 = data['message'].itervalues().next().encode('utf-8') # FIXME: Q&D fix for message refactoring, message is now a dict except StopIteration: @@ -489,22 +485,21 @@ return data def MessageReceivedTrigger(self, client, message_elt, post_treat): - profile = client.profile - if profile in self.skipped_profiles: - post_treat.addCallback(self._receivedTreatmentForSkippedProfiles, profile) + if message_elt.getAttribute('type') == C.MESS_TYPE_GROUPCHAT: + # OTR is not possible in group chats + return True + if client.profile in self.skipped_profiles: + post_treat.addCallback(self._receivedTreatmentForSkippedProfiles) else: - post_treat.addCallback(self._receivedTreatment, profile) + post_treat.addCallback(self._receivedTreatment, client) return True - def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments): - profile = client.profile - if profile in self.skipped_profiles: - return True + def _messageSendOTR(self, mess_data, client): to_jid = copy.copy(mess_data['to']) - if mess_data['type'] != 'groupchat' and not to_jid.resource: - to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: it's dirty, but frontends don't manage resources correctly now, refactoring is planed - otrctx = self.context_managers[profile].getContextForUser(to_jid) - if mess_data['type'] != 'groupchat' and otrctx.state != potr.context.STATE_PLAINTEXT: + 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: if otrctx.state == potr.context.STATE_ENCRYPTED: log.debug(u"encrypting message") try: @@ -514,8 +509,13 @@ msg = mess_data['message'].itervalues().next() except StopIteration: log.warning(u"No message found") - return False + for key, value in mess_data['extra'].iteritems(): + if key.startswith('rich') or key.startswith('xhtml'): + log.error(u'received rich content while OTR encryption is activated, cancelling') + raise failure.Failure(exceptions.CancelError()) + return mess_data otrctx.sendMessage(0, msg.encode('utf-8')) + self.host.messageAddToHistory(mess_data, client) self.host.messageSendToBridge(mess_data, client) else: feedback = D_(u"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.") @@ -528,21 +528,29 @@ mess_type=C.MESS_TYPE_INFO, extra={}, profile=client.profile) - return False + # we stop treatment here for OTR, as message is already sent + raise failure.Failure(exceptions.CancelError()) else: - log.debug(u"sending message unencrypted") + return mess_data + + def messageSendTrigger(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 + pre_xml_treatments.addCallback(self._messageSendOTR, client) + return True def _presenceReceivedTrigger(self, entity, show, priority, statuses, profile): - client = self.host.getClient(profile) if show != C.PRESENCE_UNAVAILABLE: return True + client = self.host.getClient(profile) 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 self.context_managers[profile].contexts: - otrctx = self.context_managers[profile].getContextForUser(entity) + if entity in client._otr_context_manager.contexts: + otrctx = client._otr_context_manager.getContextForUser(entity) otrctx.disconnect() return True