diff --git a/sat/bridge/bridge_constructor/bridge_template.ini b/sat/bridge/bridge_constructor/bridge_template.ini index 1c0b0c26..bf19f4b0 100644 --- a/sat/bridge/bridge_constructor/bridge_template.ini +++ b/sat/bridge/bridge_constructor/bridge_template.ini @@ -410,6 +410,18 @@ doc_param_3=mess_type: Type of the message (cf RFC 6121 §5.2.2) or "auto" for a doc_param_4=extra: optional data that can be used by a plugin to build more specific messages doc_param_5=%(doc_profile_key)s +[messageEncryptionStart] +type=method +category=core +sig_in=sss +sig_out= +param_1_default='' +param_2_default="@NONE@" +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 + [setPresence] type=method category=core diff --git a/sat/bridge/dbus_bridge.py b/sat/bridge/dbus_bridge.py index c2059a44..0458a64f 100644 --- a/sat/bridge/dbus_bridge.py +++ b/sat/bridge/dbus_bridge.py @@ -387,6 +387,12 @@ class DbusObject(dbus.service.Object): def menusGet(self, language, security_limit): return self._callback("menusGet", unicode(language), security_limit) + @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, + in_signature='sss', 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)) + @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sa{ss}a{ss}sa{ss}s', out_signature='', async_callbacks=('callback', 'errback')) diff --git a/sat/core/constants.py b/sat/core/constants.py index 66b87e4e..ce920c84 100644 --- a/sat/core/constants.py +++ b/sat/core/constants.py @@ -124,6 +124,8 @@ class Const(object): MESS_EXTRA_INFO = "info_type" + MESS_KEY_ENCRYPTION = "ENCRYPTION" + ## Chat ## CHAT_ONE2ONE = "one2one" CHAT_GROUP = "group" diff --git a/sat/core/sat_main.py b/sat/core/sat_main.py index 003c9e5c..12400487 100644 --- a/sat/core/sat_main.py +++ b/sat/core/sat_main.py @@ -32,6 +32,7 @@ log = getLogger(__name__) from sat.core.constants import Const as C from sat.memory import memory from sat.memory import cache +from sat.memory import encryption from sat.tools import async_trigger as trigger from sat.tools import utils from sat.tools.common import dynamic_import @@ -110,6 +111,8 @@ class SAT(service.Service): ) self.bridge.register_method("getWaitingSub", self.memory.getWaitingSub) self.bridge.register_method("messageSend", self._messageSend) + self.bridge.register_method("messageEncryptionStart", + self._messageEncryptionStart) self.bridge.register_method("getConfig", self._getConfig) self.bridge.register_method("setParam", self.setParam) self.bridge.register_method("getParamA", self.memory.getStringParamA) @@ -649,6 +652,11 @@ class SAT(service.Service): return False return self.profiles[profile].isConnected() + ## Encryption ## + + def registerEncryptionPlugin(self, *args, **kwargs): + return encryption.EncryptionHandler.registerPlugin(*args, **kwargs) + ## XMPP methods ## def _messageSend(self, to_jid_s, message, subject=None, mess_type="auto", extra=None, @@ -665,6 +673,12 @@ class SAT(service.Service): {unicode(key): unicode(value) for key, value in extra.items()}, ) + def _messageEncryptionStart(self, to_jid_s, encryption_ns, + 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) + 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) diff --git a/sat/core/xmpp.py b/sat/core/xmpp.py index 1e7f4c0f..18e3ec45 100644 --- a/sat/core/xmpp.py +++ b/sat/core/xmpp.py @@ -34,6 +34,7 @@ from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions +from sat.memory import encryption from zope.interface import implements import time import calendar @@ -60,6 +61,7 @@ class SatXMPPEntity(object): 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) ## initialisation ## @@ -403,6 +405,7 @@ class SatXMPPEntity(object): if None, an uuid will be generated @param no_trigger (bool): if True, sendMessage[suffix] trigger will no be used useful when a message need to be sent without any modification + /!\ this will also skip encryption methods! """ if subject is None: subject = {} @@ -455,6 +458,9 @@ class SatXMPPEntity(object): send_only = data["extra"].get("send_only", False) if not no_trigger and not send_only: + # is the session encrypted? If so we indicate it in data + self.encryption.setEncryptionFlag(data) + if not self.host_app.trigger.point( "sendMessage" + self.trigger_suffix, self, diff --git a/sat/memory/encryption.py b/sat/memory/encryption.py new file mode 100644 index 00000000..cce82d87 --- /dev/null +++ b/sat/memory/encryption.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# SAT: a jabber client +# Copyright (C) 2009-2018 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 . + +from sat.core.i18n import _ +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__) + + +EncryptionPlugin = namedtuple("EncryptionPlugin", ("instance", + "name", + "namespace", + "priority")) + + +class EncryptionHandler(object): + """Class to handle encryption sessions for a client""" + plugins = [] # plugin able to encrypt messages + + def __init__(self, host): + self._sessions = {} # bare_jid ==> encryption_data + + @classmethod + def registerPlugin(cls, plg_instance, name, namespace, priority=0): + """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 namespace(unicode): namespace of the encryption algorithm + @param priority(int): priority of this plugin to encrypt an message when not + selected manually + """ + existing_ns = [p.namespace for p in cls.plugins] + if namespace in existing_ns: + raise exceptions.ConflictError("A plugin with this namespace already exists!") + plg = EncryptionPlugin( + instance=plg_instance, + name=name, + namespace=namespace, + priority=priority) + cls.plugins.append(plg) + cls.plugins.sort(key=lambda p: p.priority) + + def start(self, entity, namespace=None): + """Start an encrypted session with an entity + + @param entity(jid.JID): entity to start an encrypted 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 + """ + if not self.plugins: + raise exceptions.NotFound(_(u"No encryption plugin is registered, " + u"an encryption session can't be started")) + + bare_jid = entity.userhostJID() + if bare_jid in self._sessions: + plg = self._sessions[bare_jid]['plugin'] + + 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 namespace is None: + plg = self.plugins[0] + else: + try: + plg = 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( + namespace=namespace)) + + data = {"plugin": plg} + if entity.resource: + # indicate that we encrypt only for some devices + data['directed_devices'] = [entity.resource] + + self._sessions[entity.userhostJID()] = data + log.info(_(u"Encryption session as been set for {bare_jid} with " + u"{encryption_name}").format( + bare_jid=bare_jid.userhost(), encryption_name=plg.name)) + + ## Triggers ## + + def setEncryptionFlag(self, mess_data): + """Set "encryption" key in mess_data if session with destinee is encrypted""" + + if mess_data["type"] == "groupchat": + # FIXME: to change when group chat encryption will be handled + return + + to_jid = mess_data['to'] + encryption = self._sessions.get(to_jid.userhostJID()) + if encryption is not None: + mess_data[C.MESS_KEY_ENCRYPTION] = encryption diff --git a/sat_frontends/bridge/dbus_bridge.py b/sat_frontends/bridge/dbus_bridge.py index a49a6942..8e078b6b 100644 --- a/sat_frontends/bridge/dbus_bridge.py +++ b/sat_frontends/bridge/dbus_bridge.py @@ -504,6 +504,20 @@ class Bridge(object): 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): + 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, 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: error_handler = None diff --git a/sat_frontends/bridge/pb.py b/sat_frontends/bridge/pb.py index 7e435f38..779fa1fb 100644 --- a/sat_frontends/bridge/pb.py +++ b/sat_frontends/bridge/pb.py @@ -382,6 +382,14 @@ class Bridge(object): 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) + if callback is not None: + d.addCallback(lambda dummy: callback()) + if errback is None: + errback = self._generic_errback + d.addErrback(errback) + def messageSend(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None): d = self.root.callRemote("messageSend", to_jid, message, subject, mess_type, extra, profile_key) if callback is not None: