# HG changeset patch # User Goffi # Date 1534004695 -7200 # Node ID 4e130cc9bfc0142e3ca90584679a384531b4da65 # Parent 9190874a8ac5912fa987ca136d0cf01bf31b50c7 core (memore/encryption): new methods and checks: Following methods are now available though bridge: - messageEncryptionStop - messageEncryptionGet: retrieve encryption data for a message session - encryptionPluginsGet: retrieve all registered encryption plugin Following methods are available for internal use: - getPlugins: retrieve registerd plugins - getNSFromName: retrieve namespace from plugin name - getBridgeData: serialise session data (to be used with bridge) - markAsEncrypted: mark message data as encrypted (to be set by encryption plugin in MessageReceived trigger) Behaviours improvments: - start and stop send messageEncryptionStarted and messageEncryptionStopped signals, and a message feedback - new "replace" arguments in start allows to replace a plugin if one is already running (instead of raising a ConflictError) - plugins declare themselves as "directed" (i.e. working with only one device at a time) or not. This is checked while dealing with jids, an exception is raised when a full jid is received for a non directed encryption. - use of new data_format (de)serialise diff -r 9190874a8ac5 -r 4e130cc9bfc0 sat/bridge/bridge_constructor/bridge_template.ini --- a/sat/bridge/bridge_constructor/bridge_template.ini Sat Aug 11 18:24:55 2018 +0200 +++ b/sat/bridge/bridge_constructor/bridge_template.ini Sat Aug 11 18:24:55 2018 +0200 @@ -438,14 +438,48 @@ [messageEncryptionStart] type=method category=core -sig_in=sss +sig_in=ssbs sig_out= param_1_default='' -param_2_default="@NONE@" +param_2_default=False +param_3_default="@NONE@" +doc=Start an encryption session +doc_param_0=to_jid: JID of the recipient (bare jid if it must be encrypted for all devices) +doc_param_1=encryption_ns: Namespace of the encryption algorithm to use +doc_param_2=replace: If True and an encryption session already exists, it will be replaced by this one + else a ConflictError will be raised +doc_param_3=%(doc_profile_key)s + +[messageEncryptionStop] +type=method +category=core +sig_in=ss +sig_out= doc=Start an encryption session -doc_param_0=to_jid: JID of the recipient (bare_jid if it must be encoded for all devices) -doc_param_1=encryption_ns: Namespace of the encryption algorithm to use -doc_param_2=%(doc_profile_key)s +doc_param_0=to_jid: JID of the recipient (full jid if encryption must be stopped for one device only) +doc_param_1=%(doc_profile_key)s + +[messageEncryptionGet] +type=method +category=core +sig_in=ss +sig_out=s +doc=Retrieve encryption data for a given entity +doc_param_0=to_jid: bare JID of the recipient +doc_param_1=%(doc_profile_key)s +doc_return=[JSON_OBJ] empty string if session is unencrypted, else a json encoded objects. + In case of dict, following keys are always present: + - name: human readable name of the encryption algorithm + - namespace: namespace of the plugin + following key can be present if suitable: + - directed_devices: list or resource where session is encrypted + +[encryptionPluginsGet] +type=method +category=core +sig_in= +sig_out=aa{ss} +doc=Retrieve registered plugins for encryption [setPresence] type=method diff -r 9190874a8ac5 -r 4e130cc9bfc0 sat/bridge/dbus_bridge.py --- a/sat/bridge/dbus_bridge.py Sat Aug 11 18:24:55 2018 +0200 +++ b/sat/bridge/dbus_bridge.py Sat Aug 11 18:24:55 2018 +0200 @@ -272,6 +272,12 @@ return self._callback("disconnect", unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, + in_signature='', out_signature='aa{ss}', + async_callbacks=None) + def encryptionPluginsGet(self, ): + return self._callback("encryptionPluginsGet", ) + + @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ss', out_signature='s', async_callbacks=None) def getConfig(self, section, name): @@ -398,10 +404,22 @@ return self._callback("menusGet", unicode(language), security_limit) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, - in_signature='sss', out_signature='', + in_signature='ss', out_signature='s', + async_callbacks=None) + def messageEncryptionGet(self, to_jid, profile_key): + return self._callback("messageEncryptionGet", unicode(to_jid), unicode(profile_key)) + + @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, + in_signature='ssbs', out_signature='', async_callbacks=None) - def messageEncryptionStart(self, to_jid, encryption_ns='', profile_key="@NONE@"): - return self._callback("messageEncryptionStart", unicode(to_jid), unicode(encryption_ns), unicode(profile_key)) + def messageEncryptionStart(self, to_jid, encryption_ns='', replace=False, profile_key="@NONE@"): + return self._callback("messageEncryptionStart", unicode(to_jid), unicode(encryption_ns), replace, unicode(profile_key)) + + @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)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sa{ss}a{ss}sa{ss}s', out_signature='', diff -r 9190874a8ac5 -r 4e130cc9bfc0 sat/core/sat_main.py --- a/sat/core/sat_main.py Sat Aug 11 18:24:55 2018 +0200 +++ b/sat/core/sat_main.py Sat Aug 11 18:24:55 2018 +0200 @@ -113,6 +113,11 @@ self.bridge.register_method("messageSend", self._messageSend) self.bridge.register_method("messageEncryptionStart", self._messageEncryptionStart) + self.bridge.register_method("messageEncryptionStop", + self._messageEncryptionStop) + self.bridge.register_method("messageEncryptionGet", + self._messageEncryptionGet) + self.bridge.register_method("encryptionPluginsGet", self._encryptionPluginsGet) self.bridge.register_method("getConfig", self._getConfig) self.bridge.register_method("setParam", self.setParam) self.bridge.register_method("getParamA", self.memory.getStringParamA) @@ -657,6 +662,17 @@ def registerEncryptionPlugin(self, *args, **kwargs): return encryption.EncryptionHandler.registerPlugin(*args, **kwargs) + def _encryptionPluginsGet(self): + plugins = encryption.EncryptionHandler.getPlugins() + ret = [] + for p in plugins: + ret.append({ + u"name": p.name, + u"namespace": p.namespace, + u"priority": unicode(p.priority), + }) + return ret + ## XMPP methods ## def _messageSend(self, to_jid_s, message, subject=None, mess_type="auto", extra=None, @@ -673,18 +689,28 @@ {unicode(key): unicode(value) for key, value in extra.items()}, ) - def _messageEncryptionStart(self, to_jid_s, encryption_ns, + def _messageEncryptionStart(self, to_jid_s, encryption_ns, replace=False, profile_key=C.PROF_KEY_NONE): client = self.getClient(profile_key) to_jid = jid.JID(to_jid_s) - return client.encryption.start(to_jid, encryption_ns.strip() or None) + return client.encryption.start(to_jid, encryption_ns.strip() or None, replace) + + def _messageEncryptionStop(self, to_jid_s, profile_key=C.PROF_KEY_NONE): + client = self.getClient(profile_key) + to_jid = jid.JID(to_jid_s) + return client.encryption.stop(to_jid) + + def _messageEncryptionGet(self, to_jid_s, profile_key=C.PROF_KEY_NONE): + client = self.getClient(profile_key) + to_jid = jid.JID(to_jid_s) + session_data = client.encryption.getSession(to_jid) + return client.encryption.getBridgeData(session_data) def _setPresence(self, to="", show="", statuses=None, profile_key=C.PROF_KEY_NONE): return self.setPresence(jid.JID(to) if to else None, show, statuses, profile_key) - def setPresence( - self, to_jid=None, show="", statuses=None, profile_key=C.PROF_KEY_NONE - ): + def setPresence(self, to_jid=None, show="", statuses=None, + profile_key=C.PROF_KEY_NONE): """Send our presence information""" if statuses is None: statuses = {} diff -r 9190874a8ac5 -r 4e130cc9bfc0 sat/core/xmpp.py --- a/sat/core/xmpp.py Sat Aug 11 18:24:55 2018 +0200 +++ b/sat/core/xmpp.py Sat Aug 11 18:24:55 2018 +0200 @@ -61,7 +61,7 @@ self._progress_cb = {} # callback called when a progress is requested # (key = progress id) self.actions = {} # used to keep track of actions for retrieval (key = action_id) - self.encryption = encryption.EncryptionHandler(host_app) + self.encryption = encryption.EncryptionHandler(self) ## initialisation ## @@ -919,6 +919,8 @@ data["extra"]["delay_sender"] = data["delay_sender"] except KeyError: pass + if C.MESS_KEY_ENCRYPTION in data: + data[u"extra"][u"encrypted"] = C.BOOL_TRUE if data is not None: self.host.bridge.messageNew( data["uid"], diff -r 9190874a8ac5 -r 4e130cc9bfc0 sat/memory/encryption.py --- a/sat/memory/encryption.py Sat Aug 11 18:24:55 2018 +0200 +++ b/sat/memory/encryption.py Sat Aug 11 18:24:55 2018 +0200 @@ -17,69 +17,120 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sat.core.i18n import _ +from sat.core.i18n import D_, _ from sat.core.constants import Const as C from sat.core import exceptions from collections import namedtuple from sat.core.log import getLogger log = getLogger(__name__) +from sat.tools.common import data_format EncryptionPlugin = namedtuple("EncryptionPlugin", ("instance", "name", "namespace", - "priority")) + "priority", + "directed")) class EncryptionHandler(object): """Class to handle encryption sessions for a client""" plugins = [] # plugin able to encrypt messages - def __init__(self, host): + def __init__(self, client): + self.client = client self._sessions = {} # bare_jid ==> encryption_data + @property + def host(self): + return self.client.host_app + @classmethod - def registerPlugin(cls, plg_instance, name, namespace, priority=0): + def registerPlugin(cls, plg_instance, name, namespace, priority=0, directed=False): """Register a plugin handling an encryption algorithm @param plg_instance(object): instance of the plugin it must have the following methods: - startEncryption(jid.JID): start an encryption session with a bare jid - stopEncryption(jid.JID): stop an encryption session with a bare jid - @param name(unicode): human readable name of the encryption alrgorithm + @param name(unicode): human readable name of the encryption algorithm @param namespace(unicode): namespace of the encryption algorithm @param priority(int): priority of this plugin to encrypt an message when not selected manually + @param directed(bool): True if this plugin is directed (if it works with one + device only at a time) """ - existing_ns = [p.namespace for p in cls.plugins] - if namespace in existing_ns: + existing_ns = set() + existing_names = set() + for p in cls.plugins: + existing_ns.add(p.namespace.lower()) + existing_names.add(p.name.lower()) + if namespace.lower() in existing_ns: raise exceptions.ConflictError("A plugin with this namespace already exists!") - plg = EncryptionPlugin( + if name.lower() in existing_names: + raise exceptions.ConflictError("A plugin with this name already exists!") + plugin = EncryptionPlugin( instance=plg_instance, name=name, namespace=namespace, - priority=priority) - cls.plugins.append(plg) + priority=priority, + directed=directed) + cls.plugins.append(plugin) cls.plugins.sort(key=lambda p: p.priority) log.info(_(u"Encryption plugin registered: {name}").format(name=name)) - def start(self, entity, namespace=None): - """Start an encrypted session with an entity + @classmethod + def getPlugins(cls): + return cls.plugins + + @classmethod + def getNSFromName(cls, name): + """Retrieve plugin namespace from its name + + @param name(unicode): name of the plugin (case insensitive) + @return (unicode): namespace of the plugin + @raise exceptions.NotFound: there is not encryption plugin of this name + """ + for p in cls.plugins: + if p.name.lower() == name.lower(): + return p.namespace + raise exceptions.NotFound - @param entity(jid.JID): entity to start an encrypted session with + def getBridgeData(self, session): + """Retrieve session data serialized for bridge. + + @param session(dict): encryption session + @return (unicode): serialized data for bridge + """ + if session is None: + return u'' + plugin = session[u'plugin'] + bridge_data = {'name': plugin.name, + 'namespace': plugin.namespace} + if u'directed_devices' in session: + bridge_data[u'directed_devices'] = session[u'directed_devices'] + + return data_format.serialise(bridge_data) + + def start(self, entity, namespace=None, replace=False): + """Start an encryption session with an entity + + @param entity(jid.JID): entity to start an encryption session with must be bare jid is the algorithm encrypt for all devices @param namespace(unicode, None): namespace of the encryption algorithm to use None to select automatically an algorithm + @param replace(bool): if True and an encrypted session already exists, + it will be replaced by the new one """ if not self.plugins: raise exceptions.NotFound(_(u"No encryption plugin is registered, " u"an encryption session can't be started")) if namespace is None: - plg = self.plugins[0] + plugin = self.plugins[0] else: try: - plg = next(p for p in self.plugins if p.namespace == namespace) + plugin = next(p for p in self.plugins if p.namespace == namespace) except StopIteration: raise exceptions.NotFound(_( u"Can't find requested encryption plugin: {namespace}").format( @@ -87,44 +138,76 @@ bare_jid = entity.userhostJID() if bare_jid in self._sessions: - plg = self._sessions[bare_jid]['plugin'] - if plg.namespace == namespace: - log.info(_(u"Session with {bare_jid} is already encrypted with {name}." - u"Nothing to do.") - .format(bare_jid=bare_jid, name=plg.name)) + # we have already an encryption session with this contact + former_plugin = self._sessions[bare_jid]['plugin'] + if former_plugin.namespace == namespace: + log.info(_(u"Session with {bare_jid} is already encrypted with {name}. " + u"Nothing to do.").format(bare_jid=bare_jid, name=plugin.name)) return - msg = (_(u"Session with {bare_jid} is already encrypted with {name}. " - u"Please stop encryption session before changing algorithm.") - .format(bare_jid=bare_jid, name=plg.name)) - log.warning(msg) - raise exceptions.ConflictError(msg) + if replace: + # there is a conflict, but replacement is requested + # so we stop previous encryption to use new one + del self._sessions[bare_jid] + else: + msg = (_(u"Session with {bare_jid} is already encrypted with {name}. " + u"Please stop encryption session before changing algorithm.") + .format(bare_jid=bare_jid, name=plugin.name)) + log.warning(msg) + raise exceptions.ConflictError(msg) - data = {"plugin": plg} - if entity.resource: + data = {"plugin": plugin} + if plugin.directed: + if not entity.resource: + entity.resource = self.host.memory.getMainResource(self.client, entity) + if not entity.resource: + raise exceptions.NotFound( + _(u"No resource found for {destinee}, can't encrypt with {name}") + .format(destinee=entity.full(), name=plugin.name)) + log.info(_(u"No resource specified to encrypt with {name}, using " + u"{destinee}.").format(destinee=entity.full(), + name=plugin.name)) # indicate that we encrypt only for some devices - data['directed_devices'] = [entity.resource] + directed_devices = data[u'directed_devices'] = [entity.resource] + elif entity.resource: + raise ValueError(_(u"{name} encryption must be used with bare jids.")) self._sessions[entity.userhostJID()] = data - log.info(_(u"Encryption session has been set for {bare_jid} with " + log.info(_(u"Encryption session has been set for {entity_jid} with " u"{encryption_name}").format( - bare_jid=bare_jid.userhost(), encryption_name=plg.name)) + entity_jid=entity.full(), encryption_name=plugin.name)) + self.host.bridge.messageEncryptionStarted( + entity.full(), + self.getBridgeData(data), + self.client.profile) + msg = D_(u"Encryption session started: your messages with {destinee} are " + u"now end to end encrypted using {name} algorithm.").format( + destinee=entity.full(), name=plugin.name) + directed_devices = data.get(u'directed_devices') + if directed_devices: + msg += u"\n" + D_(u"Message are encrypted only for {nb_devices} device(s): " + u"{devices_list}.").format( + nb_devices=len(directed_devices), + devices_list = u', '.join(directed_devices)) + + self.client.feedback(bare_jid, msg) def stop(self, entity, namespace=None): - """Stop an encrypted session with an entity + """Stop an encryption session with an entity - @param entity(jid.JID): entity with who the encrypted session must be stopped + @param entity(jid.JID): entity with who the encryption session must be stopped must be bare jid is the algorithm encrypt for all devices @param namespace(unicode): namespace of the session to stop when specified, used to check we stop the right encryption session """ session = self.getSession(entity.userhostJID()) if not session: - raise exceptions.NotFound(_(u"There is no encrypted session with this " + raise exceptions.NotFound(_(u"There is no encryption session with this " u"entity.")) - if namespace is not None and session[u'plugin'].namespace != namespace: + plugin = session['plugin'] + if namespace is not None and plugin.namespace != namespace: raise exceptions.InternalError(_( - u"The encrypted session is not run with the expected plugin: encrypted " + u"The encryption session is not run with the expected plugin: encrypted " u"with {current_name} and was expecting {expected_name}").format( current_name=session[u'plugin'].namespace, expected_name=namespace)) @@ -142,18 +225,33 @@ except ValueError: raise exceptions.NotFound(_(u"There is no directed session with this " u"entity.")) + else: + if not directed_devices: + del session[u'directed_devices'] else: del self._sessions[entity] - log.info(_(u"Encrypted session stopped with entity {entity}").format( + log.info(_(u"encryption session stopped with entity {entity}").format( entity=entity.full())) + self.host.bridge.messageEncryptionStopped( + entity.full(), + {'name': plugin.name, + 'namespace': plugin.namespace, + }, + self.client.profile) + msg = D_(u"Encryption session finished: your messages with {destinee} are " + u"NOT end to end encrypted anymore.\nYour server administrators or " + u"{destinee} server administrators will be able to read them.").format( + destinee=entity.full()) + + self.client.feedback(entity, msg) def getSession(self, entity): """Get encryption session for this contact @param entity(jid.JID): get the session for this entity must be a bare jid - @return (dict, None): encrypted session data + @return (dict, None): encryption session data None if there is not encryption for this session with this jid """ if entity.resource: @@ -173,3 +271,15 @@ encryption = self._sessions.get(to_jid.userhostJID()) if encryption is not None: mess_data[C.MESS_KEY_ENCRYPTION] = encryption + + ## Misc ## + + def markAsEncrypted(self, mess_data): + """Helper method to mark a message as having been e2e encrypted. + + 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['encrypted'] = True + return mess_data diff -r 9190874a8ac5 -r 4e130cc9bfc0 sat_frontends/bridge/dbus_bridge.py --- a/sat_frontends/bridge/dbus_bridge.py Sat Aug 11 18:24:55 2018 +0200 +++ b/sat_frontends/bridge/dbus_bridge.py Sat Aug 11 18:24:55 2018 +0200 @@ -245,6 +245,20 @@ error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.disconnect(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + def encryptionPluginsGet(self, callback=None, errback=None): + if callback is None: + error_handler = None + else: + 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.encryptionPluginsGet(**kwargs) + def getConfig(self, section, name, callback=None, errback=None): if callback is None: error_handler = None @@ -504,7 +518,7 @@ kwargs['error_handler'] = error_handler return self.db_core_iface.menusGet(language, security_limit, **kwargs) - def messageEncryptionStart(self, to_jid, encryption_ns='', profile_key="@NONE@", callback=None, errback=None): + def messageEncryptionGet(self, to_jid, profile_key, callback=None, errback=None): if callback is None: error_handler = None else: @@ -516,7 +530,35 @@ kwargs['timeout'] = const_TIMEOUT kwargs['reply_handler'] = callback kwargs['error_handler'] = error_handler - return self.db_core_iface.messageEncryptionStart(to_jid, encryption_ns, profile_key, **kwargs) + return unicode(self.db_core_iface.messageEncryptionGet(to_jid, profile_key, **kwargs)) + + def messageEncryptionStart(self, to_jid, encryption_ns='', replace=False, profile_key="@NONE@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + 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, encryption_ns, replace, profile_key, **kwargs) + + def messageEncryptionStop(self, to_jid, profile_key, callback=None, errback=None): + if callback is None: + error_handler = None + else: + 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) def messageSend(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None): if callback is None: diff -r 9190874a8ac5 -r 4e130cc9bfc0 sat_frontends/bridge/pb.py --- a/sat_frontends/bridge/pb.py Sat Aug 11 18:24:55 2018 +0200 +++ b/sat_frontends/bridge/pb.py Sat Aug 11 18:24:55 2018 +0200 @@ -214,6 +214,14 @@ errback = self._generic_errback d.addErrback(errback) + def encryptionPluginsGet(self, callback=None, errback=None): + d = self.root.callRemote("encryptionPluginsGet") + if callback is not None: + d.addCallback(callback) + if errback is None: + errback = self._generic_errback + d.addErrback(errback) + def getConfig(self, section, name, callback=None, errback=None): d = self.root.callRemote("getConfig", section, name) if callback is not None: @@ -382,8 +390,24 @@ errback = self._generic_errback d.addErrback(errback) - def messageEncryptionStart(self, to_jid, encryption_ns='', profile_key="@NONE@", callback=None, errback=None): - d = self.root.callRemote("messageEncryptionStart", to_jid, encryption_ns, profile_key) + def messageEncryptionGet(self, to_jid, profile_key, callback=None, errback=None): + d = self.root.callRemote("messageEncryptionGet", to_jid, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + errback = self._generic_errback + d.addErrback(errback) + + def messageEncryptionStart(self, to_jid, encryption_ns='', replace=False, profile_key="@NONE@", callback=None, errback=None): + d = self.root.callRemote("messageEncryptionStart", to_jid, encryption_ns, replace, profile_key) + if callback is not None: + d.addCallback(lambda dummy: callback()) + if errback is None: + errback = self._generic_errback + d.addErrback(errback) + + def messageEncryptionStop(self, to_jid, profile_key, callback=None, errback=None): + d = self.root.callRemote("messageEncryptionStop", to_jid, profile_key) if callback is not None: d.addCallback(lambda dummy: callback()) if errback is None: