Mercurial > libervia-backend
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 |