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:
parent
7eab898ef3
commit
e4bda681b8
1 changed files with 310 additions and 54 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue