Mercurial > libervia-backend
comparison sat/plugins/plugin_xep_0391.py @ 3969:8e7d5796fb23
plugin XEP-0391: implement XEP-0391 (Jingle Encrypted Transports) + XEP-0396 (JET-OMEMO):
rel 378
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 31 Oct 2022 04:09:34 +0100 |
parents | |
children | 524856bd7b19 |
comparison
equal
deleted
inserted
replaced
3968:0dd79c6cc1d2 | 3969:8e7d5796fb23 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # Libervia plugin for Jingle Encrypted Transports | |
4 # Copyright (C) 2009-2022 Jérôme Poisson (goffi@goffi.org) | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 from base64 import b64encode | |
20 from functools import partial | |
21 import io | |
22 from typing import Any, Callable, Dict, List, Optional, Tuple, Union | |
23 | |
24 from twisted.words.protocols.jabber import error, jid, xmlstream | |
25 from twisted.words.xish import domish | |
26 from wokkel import disco, iwokkel | |
27 from zope.interface import implementer | |
28 from cryptography.exceptions import AlreadyFinalized | |
29 from cryptography.hazmat import backends | |
30 from cryptography.hazmat.primitives import ciphers | |
31 from cryptography.hazmat.primitives.ciphers import Cipher, CipherContext, modes | |
32 from cryptography.hazmat.primitives.padding import PKCS7, PaddingContext | |
33 | |
34 from sat.core import exceptions | |
35 from sat.core.constants import Const as C | |
36 from sat.core.core_types import SatXMPPEntity | |
37 from sat.core.i18n import _ | |
38 from sat.core.log import getLogger | |
39 from sat.tools import xml_tools | |
40 | |
41 try: | |
42 import oldmemo | |
43 import oldmemo.etree | |
44 except ImportError as import_error: | |
45 raise exceptions.MissingModule( | |
46 "You are missing one or more package required by the OMEMO plugin. Please" | |
47 " download/install the pip packages 'oldmemo'." | |
48 ) from import_error | |
49 | |
50 | |
51 log = getLogger(__name__) | |
52 | |
53 IMPORT_NAME = "XEP-0391" | |
54 | |
55 PLUGIN_INFO = { | |
56 C.PI_NAME: "Jingle Encrypted Transports", | |
57 C.PI_IMPORT_NAME: IMPORT_NAME, | |
58 C.PI_TYPE: C.PLUG_TYPE_XEP, | |
59 C.PI_MODES: C.PLUG_MODE_BOTH, | |
60 C.PI_PROTOCOLS: ["XEP-0391", "XEP-0396"], | |
61 C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0384"], | |
62 C.PI_MAIN: "JET", | |
63 C.PI_HANDLER: "yes", | |
64 C.PI_DESCRIPTION: _("""End-to-end encryption of Jingle transports"""), | |
65 } | |
66 | |
67 NS_JET = "urn:xmpp:jingle:jet:0" | |
68 NS_JET_OMEMO = "urn:xmpp:jingle:jet-omemo:0" | |
69 | |
70 | |
71 class JET: | |
72 namespace = NS_JET | |
73 | |
74 def __init__(self, host): | |
75 log.info(_("XEP-0391 (Pubsub Attachments) plugin initialization")) | |
76 host.registerNamespace("jet", NS_JET) | |
77 self.host = host | |
78 self._o = host.plugins["XEP-0384"] | |
79 self._j = host.plugins["XEP-0166"] | |
80 host.trigger.add( | |
81 "XEP-0166_initiate_elt_built", | |
82 self._on_initiate_elt_build | |
83 ) | |
84 host.trigger.add( | |
85 "XEP-0166_on_session_initiate", | |
86 self._on_session_initiate | |
87 ) | |
88 host.trigger.add( | |
89 "XEP-0234_jingle_handler", | |
90 self._add_encryption_filter | |
91 ) | |
92 host.trigger.add( | |
93 "XEP-0234_file_receiving_request_conf", | |
94 self._add_encryption_filter | |
95 ) | |
96 | |
97 def getHandler(self, client): | |
98 return JET_Handler() | |
99 | |
100 async def _on_initiate_elt_build( | |
101 self, | |
102 client: SatXMPPEntity, | |
103 session: Dict[str, Any], | |
104 iq_elt: domish.Element, | |
105 jingle_elt: domish.Element | |
106 ) -> bool: | |
107 if client.encryption.get_namespace( | |
108 session["peer_jid"].userhostJID() | |
109 ) != self._o.NS_OLDMEMO: | |
110 return True | |
111 for content_elt in jingle_elt.elements(self._j.namespace, "content"): | |
112 content_data = session["contents"][content_elt["name"]] | |
113 security_elt = content_elt.addElement((NS_JET, "security")) | |
114 security_elt["name"] = content_elt["name"] | |
115 # XXX: for now only OLDMEMO is supported, thus we do it directly here. If some | |
116 # other are supported in the future, a plugin registering mechanism will be | |
117 # implemented. | |
118 cipher = "urn:xmpp:ciphers:aes-128-gcm-nopadding" | |
119 enc_type = "eu.siacs.conversations.axolotl" | |
120 security_elt["cipher"] = cipher | |
121 security_elt["type"] = enc_type | |
122 encryption_data = content_data["encryption"] = { | |
123 "cipher": cipher, | |
124 "type": enc_type | |
125 } | |
126 session_manager = await self._o.get_session_manager(client.profile) | |
127 try: | |
128 messages, encryption_errors = await session_manager.encrypt( | |
129 frozenset({session["peer_jid"].userhost()}), | |
130 # the value seems to be the commonly used value | |
131 { self._o.NS_OLDMEMO: b" " }, | |
132 backend_priority_order=[ self._o.NS_OLDMEMO ], | |
133 identifier = client.jid.userhost() | |
134 ) | |
135 except Exception as e: | |
136 log.error("Can't generate IV and keys: {e}") | |
137 raise e | |
138 message, plain_key_material = next(iter(messages.items())) | |
139 iv, key = message.content.initialization_vector, plain_key_material.key | |
140 content_data["encryption"].update({ | |
141 "iv": iv, | |
142 "key": key | |
143 }) | |
144 encrypted_elt = xml_tools.et_elt_2_domish_elt( | |
145 oldmemo.etree.serialize_message(message) | |
146 ) | |
147 security_elt.addChild(encrypted_elt) | |
148 return True | |
149 | |
150 async def _on_session_initiate( | |
151 self, | |
152 client: SatXMPPEntity, | |
153 session: Dict[str, Any], | |
154 iq_elt: domish.Element, | |
155 jingle_elt: domish.Element | |
156 ) -> bool: | |
157 if client.encryption.get_namespace( | |
158 session["peer_jid"].userhostJID() | |
159 ) != self._o.NS_OLDMEMO: | |
160 return True | |
161 for content_elt in jingle_elt.elements(self._j.namespace, "content"): | |
162 content_data = session["contents"][content_elt["name"]] | |
163 security_elt = next(content_elt.elements(NS_JET, "security"), None) | |
164 if security_elt is None: | |
165 continue | |
166 encrypted_elt = next( | |
167 security_elt.elements(self._o.NS_OLDMEMO, "encrypted"), None | |
168 ) | |
169 if encrypted_elt is None: | |
170 log.warning( | |
171 "missing <encrypted> element, can't decrypt: {security_elt.toXml()}" | |
172 ) | |
173 continue | |
174 session_manager = await self._o.get_session_manager(client.profile) | |
175 try: | |
176 message = await oldmemo.etree.parse_message( | |
177 xml_tools.domish_elt_2_et_elt(encrypted_elt, False), | |
178 session["peer_jid"].userhost(), | |
179 client.jid.userhost(), | |
180 session_manager | |
181 ) | |
182 __, __, plain_key_material = await session_manager.decrypt(message) | |
183 except Exception as e: | |
184 log.warning(f"Can't get IV and key: {e}\n{security_elt.toXml()}") | |
185 continue | |
186 try: | |
187 content_data["encryption"] = { | |
188 "cipher": security_elt["cipher"], | |
189 "type": security_elt["type"], | |
190 "iv": message.content.initialization_vector, | |
191 "key": plain_key_material.key | |
192 } | |
193 except KeyError as e: | |
194 log.warning(f"missing data, can't decrypt: {e}") | |
195 continue | |
196 | |
197 return True | |
198 | |
199 def __encrypt( | |
200 self, | |
201 data: bytes, | |
202 encryptor: CipherContext, | |
203 data_cb: Callable | |
204 ) -> bytes: | |
205 data_cb(data) | |
206 if data: | |
207 return encryptor.update(data) | |
208 else: | |
209 try: | |
210 return encryptor.finalize() + encryptor.tag | |
211 except AlreadyFinalized: | |
212 return b'' | |
213 | |
214 def __decrypt( | |
215 self, | |
216 data: bytes, | |
217 buffer: list[bytes], | |
218 decryptor: CipherContext, | |
219 data_cb: Callable | |
220 ) -> bytes: | |
221 buffer.append(data) | |
222 data = b''.join(buffer) | |
223 buffer.clear() | |
224 if len(data) > 16: | |
225 decrypted = decryptor.update(data[:-16]) | |
226 data_cb(decrypted) | |
227 else: | |
228 decrypted = b'' | |
229 buffer.append(data[-16:]) | |
230 return decrypted | |
231 | |
232 def __decrypt_finalize( | |
233 self, | |
234 file_obj: io.BytesIO, | |
235 buffer: list[bytes], | |
236 decryptor: CipherContext, | |
237 ) -> None: | |
238 tag = b''.join(buffer) | |
239 file_obj.write(decryptor.finalize_with_tag(tag)) | |
240 | |
241 async def _add_encryption_filter( | |
242 self, | |
243 client: SatXMPPEntity, | |
244 session: Dict[str, Any], | |
245 content_data: Dict[str, Any], | |
246 elt: domish.Element | |
247 ) -> bool: | |
248 file_obj = content_data["stream_object"].file_obj | |
249 try: | |
250 encryption_data=content_data["encryption"] | |
251 except KeyError: | |
252 return True | |
253 cipher = ciphers.Cipher( | |
254 ciphers.algorithms.AES(encryption_data["key"]), | |
255 modes.GCM(encryption_data["iv"]), | |
256 backend=backends.default_backend(), | |
257 ) | |
258 if file_obj.mode == "wb": | |
259 # we are receiving a file | |
260 buffer = [] | |
261 decryptor = cipher.decryptor() | |
262 file_obj.pre_close_cb = partial( | |
263 self.__decrypt_finalize, | |
264 file_obj=file_obj, | |
265 buffer=buffer, | |
266 decryptor=decryptor | |
267 ) | |
268 file_obj.data_cb = partial( | |
269 self.__decrypt, | |
270 buffer=buffer, | |
271 decryptor=decryptor, | |
272 data_cb=file_obj.data_cb | |
273 ) | |
274 else: | |
275 # we are sending a file | |
276 file_obj.data_cb = partial( | |
277 self.__encrypt, | |
278 encryptor=cipher.encryptor(), | |
279 data_cb=file_obj.data_cb | |
280 ) | |
281 | |
282 return True | |
283 | |
284 | |
285 @implementer(iwokkel.IDisco) | |
286 class JET_Handler(xmlstream.XMPPHandler): | |
287 | |
288 def getDiscoInfo(self, requestor, service, nodeIdentifier=""): | |
289 return [ | |
290 disco.DiscoFeature(NS_JET), | |
291 disco.DiscoFeature(NS_JET_OMEMO), | |
292 ] | |
293 | |
294 def getDiscoItems(self, requestor, service, nodeIdentifier=""): | |
295 return [] |