core: new EncryptionHandler class which manage encrypted session as a core feature:

Plugin handling encryption can now register using host.registerEncryptionPlugin, and an encryption session can now be started using messageEncryptionStart bridge method.

This will make encryption handling more easy, as we now know if a session is clear or e2e encrypted, and which plugin handle it.
This commit is contained in:
Goffi 2018-07-29 19:22:56 +02:00
commit 32b4db60c9
8 changed files with 181 additions and 0 deletions

View file

@ -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

View file

@ -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'))

View file

@ -124,6 +124,8 @@ class Const(object):
MESS_EXTRA_INFO = "info_type"
MESS_KEY_ENCRYPTION = "ENCRYPTION"
## Chat ##
CHAT_ONE2ONE = "one2one"
CHAT_GROUP = "group"

View file

@ -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)

View file

@ -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,

119
sat/memory/encryption.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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

View file

@ -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

View file

@ -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: