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
This commit is contained in:
Goffi 2018-08-11 18:24:55 +02:00
commit e4bda681b8
7 changed files with 310 additions and 54 deletions

View file

@ -438,14 +438,48 @@ doc_param_5=%(doc_profile_key)s
[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 encoded for all devices)
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=%(doc_profile_key)s
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 (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

View file

@ -271,6 +271,12 @@ class DbusObject(dbus.service.Object):
def disconnect(self, profile_key="@DEFAULT@", callback=None, errback=None):
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)
@ -398,10 +404,22 @@ class DbusObject(dbus.service.Object):
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 messageEncryptionStart(self, to_jid, encryption_ns='', profile_key="@NONE@"):
return self._callback("messageEncryptionStart", unicode(to_jid), unicode(encryption_ns), unicode(profile_key))
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='', 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='',

View file

@ -113,6 +113,11 @@ class SAT(service.Service):
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 @@ class SAT(service.Service):
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 @@ class SAT(service.Service):
{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 = {}

View file

@ -61,7 +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)
self.encryption = encryption.EncryptionHandler(self)
## initialisation ##
@ -919,6 +919,8 @@ class SatMessageProtocol(xmppim.MessageProtocol):
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"],

View file

@ -17,69 +17,120 @@
# 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/>.
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
@param entity(jid.JID): entity to start an encrypted session with
@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
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 @@ class EncryptionHandler(object):
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 @@ class EncryptionHandler(object):
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 @@ class EncryptionHandler(object):
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

View file

@ -245,6 +245,20 @@ class Bridge(object):
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 @@ 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):
def messageEncryptionGet(self, to_jid, profile_key, callback=None, errback=None):
if callback is None:
error_handler = None
else:
@ -516,7 +530,35 @@ class Bridge(object):
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:

View file

@ -214,6 +214,14 @@ class Bridge(object):
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 @@ 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)
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: