view sat/memory/encryption.py @ 2651:ebcff5423465

core (memory/encryption): start improvments, stop and getSession: - "start" won't do anything if session is already encrypted with requested algorithm, and will raise a ConflictError if it's already encrypted but with an other algorithm - implemented "stop", with an optional namespace to check we are stopping the expected algorithm - "getSession" retrieve the current encryption session of a jid, if any
author Goffi <goffi@goffi.org>
date Sat, 11 Aug 2018 18:24:55 +0200
parents 712cb4ff3e13
children 4e130cc9bfc0
line wrap: on
line source

#!/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)
        log.info(_(u"Encryption plugin registered: {name}").format(name=name))

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

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

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

        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 has been set for {bare_jid} with "
                   u"{encryption_name}").format(
                   bare_jid=bare_jid.userhost(), encryption_name=plg.name))

    def stop(self, entity, namespace=None):
        """Stop an encrypted session with an entity

        @param entity(jid.JID): entity with who the encrypted 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 "
                                        u"entity."))
        if namespace is not None and session[u'plugin'].namespace != namespace:
            raise exceptions.InternalError(_(
                u"The encrypted 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))
        if entity.resource:
            try:
                directed_devices = session[u'directed_devices']
            except KeyError:
                raise exceptions.NotFound(_(
                    u"There is a session for the whole entity (i.e. all devices of the "
                    u"entity), not a directed one. Please use bare jid if you want to "
                    u"stop the whole encryption with this entity."))

            try:
                directed_devices.remove(entity.resource)
            except ValueError:
                raise exceptions.NotFound(_(u"There is no directed session with this "
                                            u"entity."))
        else:
            del self._sessions[entity]

        log.info(_(u"Encrypted session stopped with entity {entity}").format(
            entity=entity.full()))

    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
            None if there is not encryption for this session with this jid
        """
        if entity.resource:
            raise exceptions.InternalError(u"Full jid given when expecting bare jid")
        return self._sessions.get(entity)

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