comparison sat/memory/encryption.py @ 2658:4e130cc9bfc0

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
author Goffi <goffi@goffi.org>
date Sat, 11 Aug 2018 18:24:55 +0200
parents ebcff5423465
children e347e32aa07f
comparison
equal deleted inserted replaced
2657:9190874a8ac5 2658:4e130cc9bfc0
15 # GNU Affero General Public License for more details. 15 # GNU Affero General Public License for more details.
16 16
17 # You should have received a copy of the GNU Affero General Public License 17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from sat.core.i18n import _ 20 from sat.core.i18n import D_, _
21 from sat.core.constants import Const as C 21 from sat.core.constants import Const as C
22 from sat.core import exceptions 22 from sat.core import exceptions
23 from collections import namedtuple 23 from collections import namedtuple
24 from sat.core.log import getLogger 24 from sat.core.log import getLogger
25 log = getLogger(__name__) 25 log = getLogger(__name__)
26 from sat.tools.common import data_format
26 27
27 28
28 EncryptionPlugin = namedtuple("EncryptionPlugin", ("instance", 29 EncryptionPlugin = namedtuple("EncryptionPlugin", ("instance",
29 "name", 30 "name",
30 "namespace", 31 "namespace",
31 "priority")) 32 "priority",
33 "directed"))
32 34
33 35
34 class EncryptionHandler(object): 36 class EncryptionHandler(object):
35 """Class to handle encryption sessions for a client""" 37 """Class to handle encryption sessions for a client"""
36 plugins = [] # plugin able to encrypt messages 38 plugins = [] # plugin able to encrypt messages
37 39
38 def __init__(self, host): 40 def __init__(self, client):
41 self.client = client
39 self._sessions = {} # bare_jid ==> encryption_data 42 self._sessions = {} # bare_jid ==> encryption_data
40 43
44 @property
45 def host(self):
46 return self.client.host_app
47
41 @classmethod 48 @classmethod
42 def registerPlugin(cls, plg_instance, name, namespace, priority=0): 49 def registerPlugin(cls, plg_instance, name, namespace, priority=0, directed=False):
43 """Register a plugin handling an encryption algorithm 50 """Register a plugin handling an encryption algorithm
44 51
45 @param plg_instance(object): instance of the plugin 52 @param plg_instance(object): instance of the plugin
46 it must have the following methods: 53 it must have the following methods:
47 - startEncryption(jid.JID): start an encryption session with a bare jid 54 - startEncryption(jid.JID): start an encryption session with a bare jid
48 - stopEncryption(jid.JID): stop an encryption session with a bare jid 55 - stopEncryption(jid.JID): stop an encryption session with a bare jid
49 @param name(unicode): human readable name of the encryption alrgorithm 56 @param name(unicode): human readable name of the encryption algorithm
50 @param namespace(unicode): namespace of the encryption algorithm 57 @param namespace(unicode): namespace of the encryption algorithm
51 @param priority(int): priority of this plugin to encrypt an message when not 58 @param priority(int): priority of this plugin to encrypt an message when not
52 selected manually 59 selected manually
53 """ 60 @param directed(bool): True if this plugin is directed (if it works with one
54 existing_ns = [p.namespace for p in cls.plugins] 61 device only at a time)
55 if namespace in existing_ns: 62 """
63 existing_ns = set()
64 existing_names = set()
65 for p in cls.plugins:
66 existing_ns.add(p.namespace.lower())
67 existing_names.add(p.name.lower())
68 if namespace.lower() in existing_ns:
56 raise exceptions.ConflictError("A plugin with this namespace already exists!") 69 raise exceptions.ConflictError("A plugin with this namespace already exists!")
57 plg = EncryptionPlugin( 70 if name.lower() in existing_names:
71 raise exceptions.ConflictError("A plugin with this name already exists!")
72 plugin = EncryptionPlugin(
58 instance=plg_instance, 73 instance=plg_instance,
59 name=name, 74 name=name,
60 namespace=namespace, 75 namespace=namespace,
61 priority=priority) 76 priority=priority,
62 cls.plugins.append(plg) 77 directed=directed)
78 cls.plugins.append(plugin)
63 cls.plugins.sort(key=lambda p: p.priority) 79 cls.plugins.sort(key=lambda p: p.priority)
64 log.info(_(u"Encryption plugin registered: {name}").format(name=name)) 80 log.info(_(u"Encryption plugin registered: {name}").format(name=name))
65 81
66 def start(self, entity, namespace=None): 82 @classmethod
67 """Start an encrypted session with an entity 83 def getPlugins(cls):
68 84 return cls.plugins
69 @param entity(jid.JID): entity to start an encrypted session with 85
86 @classmethod
87 def getNSFromName(cls, name):
88 """Retrieve plugin namespace from its name
89
90 @param name(unicode): name of the plugin (case insensitive)
91 @return (unicode): namespace of the plugin
92 @raise exceptions.NotFound: there is not encryption plugin of this name
93 """
94 for p in cls.plugins:
95 if p.name.lower() == name.lower():
96 return p.namespace
97 raise exceptions.NotFound
98
99 def getBridgeData(self, session):
100 """Retrieve session data serialized for bridge.
101
102 @param session(dict): encryption session
103 @return (unicode): serialized data for bridge
104 """
105 if session is None:
106 return u''
107 plugin = session[u'plugin']
108 bridge_data = {'name': plugin.name,
109 'namespace': plugin.namespace}
110 if u'directed_devices' in session:
111 bridge_data[u'directed_devices'] = session[u'directed_devices']
112
113 return data_format.serialise(bridge_data)
114
115 def start(self, entity, namespace=None, replace=False):
116 """Start an encryption session with an entity
117
118 @param entity(jid.JID): entity to start an encryption session with
70 must be bare jid is the algorithm encrypt for all devices 119 must be bare jid is the algorithm encrypt for all devices
71 @param namespace(unicode, None): namespace of the encryption algorithm to use 120 @param namespace(unicode, None): namespace of the encryption algorithm to use
72 None to select automatically an algorithm 121 None to select automatically an algorithm
122 @param replace(bool): if True and an encrypted session already exists,
123 it will be replaced by the new one
73 """ 124 """
74 if not self.plugins: 125 if not self.plugins:
75 raise exceptions.NotFound(_(u"No encryption plugin is registered, " 126 raise exceptions.NotFound(_(u"No encryption plugin is registered, "
76 u"an encryption session can't be started")) 127 u"an encryption session can't be started"))
77 128
78 if namespace is None: 129 if namespace is None:
79 plg = self.plugins[0] 130 plugin = self.plugins[0]
80 else: 131 else:
81 try: 132 try:
82 plg = next(p for p in self.plugins if p.namespace == namespace) 133 plugin = next(p for p in self.plugins if p.namespace == namespace)
83 except StopIteration: 134 except StopIteration:
84 raise exceptions.NotFound(_( 135 raise exceptions.NotFound(_(
85 u"Can't find requested encryption plugin: {namespace}").format( 136 u"Can't find requested encryption plugin: {namespace}").format(
86 namespace=namespace)) 137 namespace=namespace))
87 138
88 bare_jid = entity.userhostJID() 139 bare_jid = entity.userhostJID()
89 if bare_jid in self._sessions: 140 if bare_jid in self._sessions:
90 plg = self._sessions[bare_jid]['plugin'] 141 # we have already an encryption session with this contact
91 if plg.namespace == namespace: 142 former_plugin = self._sessions[bare_jid]['plugin']
92 log.info(_(u"Session with {bare_jid} is already encrypted with {name}." 143 if former_plugin.namespace == namespace:
93 u"Nothing to do.") 144 log.info(_(u"Session with {bare_jid} is already encrypted with {name}. "
94 .format(bare_jid=bare_jid, name=plg.name)) 145 u"Nothing to do.").format(bare_jid=bare_jid, name=plugin.name))
95 return 146 return
96 147
97 msg = (_(u"Session with {bare_jid} is already encrypted with {name}. " 148 if replace:
98 u"Please stop encryption session before changing algorithm.") 149 # there is a conflict, but replacement is requested
99 .format(bare_jid=bare_jid, name=plg.name)) 150 # so we stop previous encryption to use new one
100 log.warning(msg) 151 del self._sessions[bare_jid]
101 raise exceptions.ConflictError(msg) 152 else:
102 153 msg = (_(u"Session with {bare_jid} is already encrypted with {name}. "
103 data = {"plugin": plg} 154 u"Please stop encryption session before changing algorithm.")
104 if entity.resource: 155 .format(bare_jid=bare_jid, name=plugin.name))
156 log.warning(msg)
157 raise exceptions.ConflictError(msg)
158
159 data = {"plugin": plugin}
160 if plugin.directed:
161 if not entity.resource:
162 entity.resource = self.host.memory.getMainResource(self.client, entity)
163 if not entity.resource:
164 raise exceptions.NotFound(
165 _(u"No resource found for {destinee}, can't encrypt with {name}")
166 .format(destinee=entity.full(), name=plugin.name))
167 log.info(_(u"No resource specified to encrypt with {name}, using "
168 u"{destinee}.").format(destinee=entity.full(),
169 name=plugin.name))
105 # indicate that we encrypt only for some devices 170 # indicate that we encrypt only for some devices
106 data['directed_devices'] = [entity.resource] 171 directed_devices = data[u'directed_devices'] = [entity.resource]
172 elif entity.resource:
173 raise ValueError(_(u"{name} encryption must be used with bare jids."))
107 174
108 self._sessions[entity.userhostJID()] = data 175 self._sessions[entity.userhostJID()] = data
109 log.info(_(u"Encryption session has been set for {bare_jid} with " 176 log.info(_(u"Encryption session has been set for {entity_jid} with "
110 u"{encryption_name}").format( 177 u"{encryption_name}").format(
111 bare_jid=bare_jid.userhost(), encryption_name=plg.name)) 178 entity_jid=entity.full(), encryption_name=plugin.name))
179 self.host.bridge.messageEncryptionStarted(
180 entity.full(),
181 self.getBridgeData(data),
182 self.client.profile)
183 msg = D_(u"Encryption session started: your messages with {destinee} are "
184 u"now end to end encrypted using {name} algorithm.").format(
185 destinee=entity.full(), name=plugin.name)
186 directed_devices = data.get(u'directed_devices')
187 if directed_devices:
188 msg += u"\n" + D_(u"Message are encrypted only for {nb_devices} device(s): "
189 u"{devices_list}.").format(
190 nb_devices=len(directed_devices),
191 devices_list = u', '.join(directed_devices))
192
193 self.client.feedback(bare_jid, msg)
112 194
113 def stop(self, entity, namespace=None): 195 def stop(self, entity, namespace=None):
114 """Stop an encrypted session with an entity 196 """Stop an encryption session with an entity
115 197
116 @param entity(jid.JID): entity with who the encrypted session must be stopped 198 @param entity(jid.JID): entity with who the encryption session must be stopped
117 must be bare jid is the algorithm encrypt for all devices 199 must be bare jid is the algorithm encrypt for all devices
118 @param namespace(unicode): namespace of the session to stop 200 @param namespace(unicode): namespace of the session to stop
119 when specified, used to check we stop the right encryption session 201 when specified, used to check we stop the right encryption session
120 """ 202 """
121 session = self.getSession(entity.userhostJID()) 203 session = self.getSession(entity.userhostJID())
122 if not session: 204 if not session:
123 raise exceptions.NotFound(_(u"There is no encrypted session with this " 205 raise exceptions.NotFound(_(u"There is no encryption session with this "
124 u"entity.")) 206 u"entity."))
125 if namespace is not None and session[u'plugin'].namespace != namespace: 207 plugin = session['plugin']
208 if namespace is not None and plugin.namespace != namespace:
126 raise exceptions.InternalError(_( 209 raise exceptions.InternalError(_(
127 u"The encrypted session is not run with the expected plugin: encrypted " 210 u"The encryption session is not run with the expected plugin: encrypted "
128 u"with {current_name} and was expecting {expected_name}").format( 211 u"with {current_name} and was expecting {expected_name}").format(
129 current_name=session[u'plugin'].namespace, 212 current_name=session[u'plugin'].namespace,
130 expected_name=namespace)) 213 expected_name=namespace))
131 if entity.resource: 214 if entity.resource:
132 try: 215 try:
140 try: 223 try:
141 directed_devices.remove(entity.resource) 224 directed_devices.remove(entity.resource)
142 except ValueError: 225 except ValueError:
143 raise exceptions.NotFound(_(u"There is no directed session with this " 226 raise exceptions.NotFound(_(u"There is no directed session with this "
144 u"entity.")) 227 u"entity."))
228 else:
229 if not directed_devices:
230 del session[u'directed_devices']
145 else: 231 else:
146 del self._sessions[entity] 232 del self._sessions[entity]
147 233
148 log.info(_(u"Encrypted session stopped with entity {entity}").format( 234 log.info(_(u"encryption session stopped with entity {entity}").format(
149 entity=entity.full())) 235 entity=entity.full()))
236 self.host.bridge.messageEncryptionStopped(
237 entity.full(),
238 {'name': plugin.name,
239 'namespace': plugin.namespace,
240 },
241 self.client.profile)
242 msg = D_(u"Encryption session finished: your messages with {destinee} are "
243 u"NOT end to end encrypted anymore.\nYour server administrators or "
244 u"{destinee} server administrators will be able to read them.").format(
245 destinee=entity.full())
246
247 self.client.feedback(entity, msg)
150 248
151 def getSession(self, entity): 249 def getSession(self, entity):
152 """Get encryption session for this contact 250 """Get encryption session for this contact
153 251
154 @param entity(jid.JID): get the session for this entity 252 @param entity(jid.JID): get the session for this entity
155 must be a bare jid 253 must be a bare jid
156 @return (dict, None): encrypted session data 254 @return (dict, None): encryption session data
157 None if there is not encryption for this session with this jid 255 None if there is not encryption for this session with this jid
158 """ 256 """
159 if entity.resource: 257 if entity.resource:
160 raise exceptions.InternalError(u"Full jid given when expecting bare jid") 258 raise exceptions.InternalError(u"Full jid given when expecting bare jid")
161 return self._sessions.get(entity) 259 return self._sessions.get(entity)
171 269
172 to_jid = mess_data['to'] 270 to_jid = mess_data['to']
173 encryption = self._sessions.get(to_jid.userhostJID()) 271 encryption = self._sessions.get(to_jid.userhostJID())
174 if encryption is not None: 272 if encryption is not None:
175 mess_data[C.MESS_KEY_ENCRYPTION] = encryption 273 mess_data[C.MESS_KEY_ENCRYPTION] = encryption
274
275 ## Misc ##
276
277 def markAsEncrypted(self, mess_data):
278 """Helper method to mark a message as having been e2e encrypted.
279
280 This should be used in the post_treat workflow of MessageReceived trigger of
281 the plugin
282 @param mess_data(dict): message data as used in post treat workflow
283 """
284 mess_data['encrypted'] = True
285 return mess_data