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 []