# HG changeset patch # User Goffi # Date 1546545600 -3600 # Node ID da59ff099b324f918aa2d821baa2a36ea352071f # Parent 1f5b0262335512f8a28e99fab19f7f0a7a4829a9 core (memory/encryption), plugin OTR: finished OTR integration in encryption: - messageEncryptionStart and messageEncryptionStop are now async - an encryption plugin can now have a startEncryption and endEncryption method, which are called automatically when suitable - trust is now put in "trusted" key of message_data, and markAsTrusted has been added, it is used in OTR - getTrustUI implemented for OTR, using legacy authenticate code - catch potr.crypt.InvalidParameterError on decryption - fixed some service OTR messages which were not correctly marked as not for storing, and thus were added to MAM archive/carbon copied - other bug fixes diff -r 1f5b02623355 -r da59ff099b32 sat/bridge/bridge_constructor/bridge_template.ini --- a/sat/bridge/bridge_constructor/bridge_template.ini Thu Jan 03 20:51:08 2019 +0100 +++ b/sat/bridge/bridge_constructor/bridge_template.ini Thu Jan 03 21:00:00 2019 +0100 @@ -436,6 +436,7 @@ doc_param_5=%(doc_profile_key)s [messageEncryptionStart] +async= type=method category=core sig_in=ssbs @@ -451,6 +452,7 @@ doc_param_3=%(doc_profile_key)s [messageEncryptionStop] +async= type=method category=core sig_in=ss diff -r 1f5b02623355 -r da59ff099b32 sat/bridge/dbus_bridge.py --- a/sat/bridge/dbus_bridge.py Thu Jan 03 20:51:08 2019 +0100 +++ b/sat/bridge/dbus_bridge.py Thu Jan 03 21:00:00 2019 +0100 @@ -423,15 +423,15 @@ @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ssbs', out_signature='', - async_callbacks=None) - def messageEncryptionStart(self, to_jid, namespace='', replace=False, profile_key="@NONE@"): - return self._callback("messageEncryptionStart", unicode(to_jid), unicode(namespace), replace, unicode(profile_key)) + async_callbacks=('callback', 'errback')) + def messageEncryptionStart(self, to_jid, namespace='', replace=False, profile_key="@NONE@", callback=None, errback=None): + return self._callback("messageEncryptionStart", unicode(to_jid), unicode(namespace), replace, unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ss', out_signature='', - async_callbacks=None) - def messageEncryptionStop(self, to_jid, profile_key): - return self._callback("messageEncryptionStop", unicode(to_jid), unicode(profile_key)) + async_callbacks=('callback', 'errback')) + def messageEncryptionStop(self, to_jid, profile_key, callback=None, errback=None): + return self._callback("messageEncryptionStop", unicode(to_jid), unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sa{ss}a{ss}sa{ss}s', out_signature='', diff -r 1f5b02623355 -r da59ff099b32 sat/memory/encryption.py --- a/sat/memory/encryption.py Thu Jan 03 20:51:08 2019 +0100 +++ b/sat/memory/encryption.py Thu Jan 03 21:00:00 2019 +0100 @@ -22,9 +22,13 @@ from sat.core import exceptions from collections import namedtuple from sat.core.log import getLogger +from sat.tools.common import data_format +from twisted.internet import defer +from twisted.python import failure +import copy log = getLogger(__name__) -from sat.tools.common import data_format +log = getLogger(__name__) EncryptionPlugin = namedtuple("EncryptionPlugin", ("instance", "name", @@ -54,6 +58,12 @@ - getTrustUI(entity): return a XMLUI for trust management entity(jid.JID): entity to manage The returned XMLUI must be a form + if may have the following methods: + - startEncryption(entity): start encrypted session + entity(jid.JID): entity to start encrypted session with + - stopEncryption(entity): start encrypted session + entity(jid.JID): entity to stop encrypted session with + if they don't exists, those 2 methods will be ignored. @param name(unicode): human readable name of the encryption algorithm @param namespace(unicode): namespace of the encryption algorithm @@ -125,6 +135,39 @@ return data_format.serialise(bridge_data) + def _startEncryption(self, plugin, entity): + """Start encryption with a plugin + + This method must be called just before adding a plugin session. + StartEncryptionn method of plugin will be called if it exists. + """ + try: + start_encryption = plugin.instance.startEncryption + except AttributeError: + log.debug(u"No startEncryption method found for {plugin}".format( + plugin = plugin.namespace)) + return defer.succeed(None) + else: + # we copy entity to avoid having the resource changed by stop_encryption + return defer.maybeDeferred(start_encryption, self.client, copy.copy(entity)) + + def _stopEncryption(self, plugin, entity): + """Stop encryption with a plugin + + This method must be called just before removing a plugin session. + StopEncryptionn method of plugin will be called if it exists. + """ + try: + stop_encryption = plugin.instance.stopEncryption + except AttributeError: + log.debug(u"No stopEncryption method found for {plugin}".format( + plugin = plugin.namespace)) + return defer.succeed(None) + else: + # we copy entity to avoid having the resource changed by stop_encryption + return defer.maybeDeferred(stop_encryption, self.client, copy.copy(entity)) + + @defer.inlineCallbacks def start(self, entity, namespace=None, replace=False): """Start an encryption session with an entity @@ -148,7 +191,7 @@ bare_jid = entity.userhostJID() if bare_jid in self._sessions: # we have already an encryption session with this contact - former_plugin = self._sessions[bare_jid]['plugin'] + former_plugin = self._sessions[bare_jid][u"plugin"] if former_plugin.namespace == namespace: log.info(_(u"Session with {bare_jid} is already encrypted with {name}. " u"Nothing to do.").format( @@ -159,6 +202,7 @@ # there is a conflict, but replacement is requested # so we stop previous encryption to use new one del self._sessions[bare_jid] + yield self._stopEncryption(former_plugin, entity) else: msg = (_(u"Session with {bare_jid} is already encrypted with {name}. " u"Please stop encryption session before changing algorithm.") @@ -182,6 +226,7 @@ elif entity.resource: raise ValueError(_(u"{name} encryption must be used with bare jids.")) + yield self._startEncryption(plugin, entity) self._sessions[entity.userhostJID()] = data log.info(_(u"Encryption session has been set for {entity_jid} with " u"{encryption_name}").format( @@ -202,6 +247,7 @@ self.client.feedback(bare_jid, msg) + @defer.inlineCallbacks def stop(self, entity, namespace=None): """Stop an encryption session with an entity @@ -212,8 +258,9 @@ """ session = self.getSession(entity.userhostJID()) if not session: - raise exceptions.NotFound(_(u"There is no encryption session with this " - u"entity.")) + raise failure.Failure( + exceptions.NotFound(_(u"There is no encryption session with this " + u"entity."))) plugin = session['plugin'] if namespace is not None and plugin.namespace != namespace: raise exceptions.InternalError(_( @@ -237,9 +284,16 @@ u"entity.")) else: if not directed_devices: - del session[u'directed_devices'] + # if we have no more directed device sessions, + # we stop the whole session + # see comment below for deleting session before stopping encryption + del self._sessions[entity.userhostJID()] + yield self._stopEncryption(plugin, entity) else: - del self._sessions[entity] + # plugin's stopEncryption may call stop again (that's the case with OTR) + # so we need to remove plugin from session before calling self._stopEncryption + del self._sessions[entity.userhostJID()] + yield self._stopEncryption(plugin, entity) log.info(_(u"encryption session stopped with entity {entity}").format( entity=entity.full())) @@ -296,7 +350,7 @@ raise NotImplementedError( u"Encryption plugin doesn't handle trust management UI") else: - return get_trust_ui(self.client, entity_jid) + return defer.maybeDeferred(get_trust_ui, self.client, entity_jid) ## Triggers ## @@ -324,6 +378,16 @@ mess_data['encrypted'] = True return mess_data + def markAsTrusted(self, mess_data): + """Helper methor to mark a message as sent from a trusted entity. + + This should be used in the post_treat workflow of MessageReceived trigger of + the plugin + @param mess_data(dict): message data as used in post treat workflow + """ + mess_data['trusted'] = True + return mess_data + def markAsUntrusted(self, mess_data): """Helper methor to mark a message as sent from an untrusted entity. @@ -331,5 +395,5 @@ the plugin @param mess_data(dict): message data as used in post treat workflow """ - mess_data['untrusted'] = True + mess_data['trusted'] = False return mess_data diff -r 1f5b02623355 -r da59ff099b32 sat/plugins/plugin_sec_otr.py --- a/sat/plugins/plugin_sec_otr.py Thu Jan 03 20:51:08 2019 +0100 +++ b/sat/plugins/plugin_sec_otr.py Thu Jan 03 21:00:00 2019 +0100 @@ -74,9 +74,21 @@ class Context(potr.context.Context): - def __init__(self, host, account, other_jid): - super(Context, self).__init__(account, other_jid) - self.host = host + def __init__(self, context_manager, other_jid): + self.context_manager = context_manager + super(Context, self).__init__(context_manager.account, other_jid) + + @property + def host(self): + return self.context_manager.host + + @property + def _p_hints(self): + return self.context_manager.parent._p_hints + + @property + def _p_carbons(self): + return self.context_manager.parent._p_carbons def getPolicy(self, key): if key in DEFAULT_POLICY_FLAGS: @@ -110,12 +122,42 @@ "timestamp": time.time(), } client.generateMessageXML(mess_data) + xml = mess_data[u'xml'] + self._p_carbons.setPrivate(xml) + self._p_hints.addHintElements(xml, [ + self._p_hints.HINT_NO_COPY, + self._p_hints.HINT_NO_PERMANENT_STORE]) client.send(mess_data["xml"]) else: message_elt = appdata[u"xml"] assert message_elt.name == u"message" message_elt.addElement("body", content=msg) + def stopCb(self, __, feedback): + client = self.user.client + self.host.bridge.otrState( + OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile + ) + client.feedback(self.peer, feedback) + + def stopEb(self, failure_): + # encryption may be already stopped in case of manual stop + if not failure_.check(exceptions.NotFound): + log.error(u"Error while stopping OTR encryption: {msg}".format(msg=failure_)) + + def isTrusted(self): + # we have to check value because potr code says that a 2-tuples should be + # returned while in practice it's either None or u"trusted" + trusted = self.getCurrentTrust() + if trusted is None: + return False + elif trusted == u'trusted': + return True + else: + log.error(u"Unexpected getCurrentTrust() value: {value}".format( + value=trusted)) + return False + def setState(self, state): client = self.user.client old_state = self.state @@ -123,17 +165,17 @@ log.debug(u"setState: %s (old_state=%s)" % (state, old_state)) if state == potr.context.STATE_PLAINTEXT: - client.encryption.stop(self.peer, NS_OTR) feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % { "other_jid": self.peer.full() } - self.host.bridge.otrState( - OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile - ) + d = client.encryption.stop(self.peer, NS_OTR) + d.addCallback(self.stopCb, feedback=feedback) + d.addErrback(self.stopEb) + return elif state == potr.context.STATE_ENCRYPTED: client.encryption.start(self.peer, NS_OTR) try: - trusted = self.getCurrentTrust() + trusted = self.isTrusted() except TypeError: trusted = False trusted_str = _(u"trusted") if trusted else _(u"untrusted") @@ -155,13 +197,13 @@ OTR_STATE_ENCRYPTED, self.peer.full(), client.profile ) elif state == potr.context.STATE_FINISHED: - client.encryption.stop(self.peer, NS_OTR) feedback = D_(u"OTR conversation with {other_jid} is FINISHED").format( other_jid=self.peer.full() ) - self.host.bridge.otrState( - OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile - ) + d = client.encryption.stop(self.peer, NS_OTR) + d.addCallback(self.stopCb, feedback=feedback) + d.addErrback(self.stopEb) + return else: log.error(D_(u"Unknown OTR state")) return @@ -240,15 +282,19 @@ class ContextManager(object): - def __init__(self, host, client): - self.host = host - self.account = Account(host, client) + def __init__(self, parent, client): + self.parent = parent + self.account = Account(parent.host, client) self.contexts = {} + @property + def host(self): + return self.parent.host + def startContext(self, other_jid): assert isinstance(other_jid, jid.JID) context = self.contexts.setdefault( - other_jid, Context(self.host, self.account, other_jid) + other_jid, Context(self, other_jid) ) return context @@ -323,7 +369,7 @@ def profileConnected(self, client): if client.profile in self.skipped_profiles: return - ctxMng = client._otr_context_manager = ContextManager(self.host, client) + ctxMng = client._otr_context_manager = ContextManager(self, client) client._otr_data = persistent.PersistentBinaryDict(NS_OTR, client.profile) yield client._otr_data.load() encrypted_priv_key = client._otr_data.get(PRIVATE_KEY, None) @@ -346,6 +392,110 @@ context.disconnect() del client._otr_context_manager + # encryption plugin methods + + def startEncryption(self, client, entity_jid): + self.startRefresh(client, entity_jid) + + def stopEncryption(self, client, entity_jid): + self.endSession(client, entity_jid) + + def getTrustUI(self, client, entity_jid): + if not entity_jid.resource: + entity_jid.resource = self.host.memory.getMainResource( + client, entity_jid + ) # FIXME: temporary and unsecure, must be changed when frontends + # are refactored + ctxMng = client._otr_context_manager + otrctx = ctxMng.getContextForUser(entity_jid) + priv_key = ctxMng.account.privkey + + if priv_key is None: + # we have no private key yet + dialog = xml_tools.XMLUI( + C.XMLUI_DIALOG, + dialog_opt={ + C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE, + C.XMLUI_DATA_MESS: _( + u"You have no private key yet, start an OTR conversation to " + u"have one" + ), + C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING, + }, + title=_(u"No private key"), + ) + return dialog + + other_fingerprint = otrctx.getCurrentKey() + + if other_fingerprint is None: + # we have a private key, but not the fingerprint of our correspondent + dialog = xml_tools.XMLUI( + C.XMLUI_DIALOG, + dialog_opt={ + C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE, + C.XMLUI_DATA_MESS: _( + u"Your fingerprint is:\n{fingerprint}\n\n" + u"Start an OTR conversation to have your correspondent one." + ).format(fingerprint=priv_key), + C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO, + }, + title=_(u"Fingerprint"), + ) + return dialog + + def setTrust(raw_data, profile): + if xml_tools.isXMLUICancelled(raw_data): + return {} + # This method is called when authentication form is submited + data = xml_tools.XMLUIResult2DataFormResult(raw_data) + if data["match"] == "yes": + otrctx.setCurrentTrust(OTR_STATE_TRUSTED) + note_msg = _(u"Your correspondent {correspondent} is now TRUSTED") + self.host.bridge.otrState( + OTR_STATE_TRUSTED, entity_jid.full(), client.profile + ) + else: + otrctx.setCurrentTrust("") + note_msg = _(u"Your correspondent {correspondent} is now UNTRUSTED") + self.host.bridge.otrState( + OTR_STATE_UNTRUSTED, entity_jid.full(), client.profile + ) + note = xml_tools.XMLUI( + C.XMLUI_DIALOG, + dialog_opt={ + C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE, + C.XMLUI_DATA_MESS: note_msg.format(correspondent=otrctx.peer), + }, + ) + return {"xmlui": note.toXml()} + + submit_id = self.host.registerCallback(setTrust, with_data=True, one_shot=True) + trusted = otrctx.isTrusted() + + xmlui = xml_tools.XMLUI( + C.XMLUI_FORM, + title=_(u"Authentication ({entity_jid})").format(entity_jid=entity_jid.full()), + submit_id=submit_id, + ) + xmlui.addText(_(AUTH_TXT)) + xmlui.addDivider() + xmlui.addText( + D_(u"Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key) + ) + xmlui.addText( + D_(u"Your correspondent fingerprint should be:\n{fingerprint}").format( + fingerprint=other_fingerprint + ) + ) + xmlui.addDivider("blank") + xmlui.changeContainer("pairs") + xmlui.addLabel(D_(u"Is your correspondent fingerprint the same as here ?")) + xmlui.addList( + "match", [("yes", _("yes")), ("no", _("no"))], ["yes" if trusted else "no"] + ) + return xmlui + def _otrStartRefresh(self, menu_data, profile): """Start or refresh an OTR session @@ -422,97 +572,7 @@ def authenticate(self, client, to_jid): """Authenticate other user and see our own fingerprint""" - if not to_jid.resource: - to_jid.resource = self.host.memory.getMainResource( - client, to_jid - ) # FIXME: temporary and unsecure, must be changed when frontends - # are refactored - ctxMng = client._otr_context_manager - otrctx = ctxMng.getContextForUser(to_jid) - priv_key = ctxMng.account.privkey - - if priv_key is None: - # we have no private key yet - dialog = xml_tools.XMLUI( - C.XMLUI_DIALOG, - dialog_opt={ - C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE, - C.XMLUI_DATA_MESS: _( - u"You have no private key yet, start an OTR conversation to " - u"have one" - ), - C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING, - }, - title=_(u"No private key"), - ) - return {"xmlui": dialog.toXml()} - - other_fingerprint = otrctx.getCurrentKey() - - if other_fingerprint is None: - # we have a private key, but not the fingerprint of our correspondent - dialog = xml_tools.XMLUI( - C.XMLUI_DIALOG, - dialog_opt={ - C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE, - C.XMLUI_DATA_MESS: _( - u"Your fingerprint is:\n{fingerprint}\n\n" - u"Start an OTR conversation to have your correspondent one." - ).format(fingerprint=priv_key), - C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO, - }, - title=_(u"Fingerprint"), - ) - return {"xmlui": dialog.toXml()} - - def setTrust(raw_data, profile): - # This method is called when authentication form is submited - data = xml_tools.XMLUIResult2DataFormResult(raw_data) - if data["match"] == "yes": - otrctx.setCurrentTrust(OTR_STATE_TRUSTED) - note_msg = _(u"Your correspondent {correspondent} is now TRUSTED") - self.host.bridge.otrState( - OTR_STATE_TRUSTED, to_jid.full(), client.profile - ) - else: - otrctx.setCurrentTrust("") - note_msg = _(u"Your correspondent {correspondent} is now UNTRUSTED") - self.host.bridge.otrState( - OTR_STATE_UNTRUSTED, to_jid.full(), client.profile - ) - note = xml_tools.XMLUI( - C.XMLUI_DIALOG, - dialog_opt={ - C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE, - C.XMLUI_DATA_MESS: note_msg.format(correspondent=otrctx.peer), - }, - ) - return {"xmlui": note.toXml()} - - submit_id = self.host.registerCallback(setTrust, with_data=True, one_shot=True) - trusted = bool(otrctx.getCurrentTrust()) - - xmlui = xml_tools.XMLUI( - C.XMLUI_FORM, - title=_("Authentication (%s)") % to_jid.full(), - submit_id=submit_id, - ) - xmlui.addText(_(AUTH_TXT)) - xmlui.addDivider() - xmlui.addText( - D_(u"Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key) - ) - xmlui.addText( - D_(u"Your correspondent fingerprint should be:\n{fingerprint}").format( - fingerprint=other_fingerprint - ) - ) - xmlui.addDivider("blank") - xmlui.changeContainer("pairs") - xmlui.addLabel(D_(u"Is your correspondent fingerprint the same as here ?")) - xmlui.addList( - "match", [("yes", _("yes")), ("no", _("no"))], ["yes" if trusted else "no"] - ) + xmlui = self.getTrustUI(client, to_jid) return {"xmlui": xmlui.toXml()} def _dropPrivKey(self, menu_data, profile): @@ -601,6 +661,12 @@ feedback = msg client.feedback(from_jid, msg) raise failure.Failure(exceptions.CancelError(msg)) + except potr.crypt.InvalidParameterError as e: + msg = D_(u"Error while trying de decrypt OTR message: {msg}".format(msg=e)) + log.warning(msg) + feedback = msg + client.feedback(from_jid, msg) + raise failure.Failure(exceptions.CancelError(msg)) except StopIteration: return data else: @@ -629,6 +695,13 @@ exceptions.CancelError("Cancelled by OTR") ) # no message at all (no history, no signal) client.encryption.markAsEncrypted(data) + trusted = otrctx.isTrusted() + + if trusted: + client.encryption.markAsTrusted(data) + else: + client.encryption.markAsUntrusted(data) + return data def _receivedTreatmentForSkippedProfiles(self, data): diff -r 1f5b02623355 -r da59ff099b32 sat_frontends/bridge/dbus_bridge.py --- a/sat_frontends/bridge/dbus_bridge.py Thu Jan 03 20:51:08 2019 +0100 +++ b/sat_frontends/bridge/dbus_bridge.py Thu Jan 03 21:00:00 2019 +0100 @@ -562,12 +562,7 @@ if errback is None: errback = log.error error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.messageEncryptionStart(to_jid, namespace, replace, profile_key, **kwargs) + return self.db_core_iface.messageEncryptionStart(to_jid, namespace, replace, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) def messageEncryptionStop(self, to_jid, profile_key, callback=None, errback=None): if callback is None: @@ -576,12 +571,7 @@ if errback is None: errback = log.error error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.messageEncryptionStop(to_jid, profile_key, **kwargs) + return self.db_core_iface.messageEncryptionStop(to_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) def messageSend(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None): if callback is None: