comparison libervia/backend/plugins/plugin_xep_0448.py @ 4270:0d7bb4df2343

Reformatted code base using black.
author Goffi <goffi@goffi.org>
date Wed, 19 Jun 2024 18:44:57 +0200
parents 4b842c1fb686
children 111dce64dcb5
comparison
equal deleted inserted replaced
4269:64a85ce8be70 4270:0d7bb4df2343
51 C.PI_NAME: "Encryption for Stateless File Sharing", 51 C.PI_NAME: "Encryption for Stateless File Sharing",
52 C.PI_IMPORT_NAME: IMPORT_NAME, 52 C.PI_IMPORT_NAME: IMPORT_NAME,
53 C.PI_TYPE: C.PLUG_TYPE_EXP, 53 C.PI_TYPE: C.PLUG_TYPE_EXP,
54 C.PI_PROTOCOLS: ["XEP-0448"], 54 C.PI_PROTOCOLS: ["XEP-0448"],
55 C.PI_DEPENDENCIES: [ 55 C.PI_DEPENDENCIES: [
56 "XEP-0103", "XEP-0300", "XEP-0334", "XEP-0363", "XEP-0384", "XEP-0447", 56 "XEP-0103",
57 "DOWNLOAD", "ATTACH" 57 "XEP-0300",
58 "XEP-0334",
59 "XEP-0363",
60 "XEP-0384",
61 "XEP-0447",
62 "DOWNLOAD",
63 "ATTACH",
58 ], 64 ],
59 C.PI_MAIN: "XEP_0448", 65 C.PI_MAIN: "XEP_0448",
60 C.PI_HANDLER: "yes", 66 C.PI_HANDLER: "yes",
61 C.PI_DESCRIPTION: dedent(_("""\ 67 C.PI_DESCRIPTION: dedent(
68 _(
69 """\
62 Implementation of e2e encryption for media sharing 70 Implementation of e2e encryption for media sharing
63 """)), 71 """
72 )
73 ),
64 } 74 }
65 75
66 NS_ESFS = "urn:xmpp:esfs:0" 76 NS_ESFS = "urn:xmpp:esfs:0"
67 NS_AES_128_GCM = "urn:xmpp:ciphers:aes-128-gcm-nopadding:0" 77 NS_AES_128_GCM = "urn:xmpp:ciphers:aes-128-gcm-nopadding:0"
68 NS_AES_256_GCM = "urn:xmpp:ciphers:aes-256-gcm-nopadding:0" 78 NS_AES_256_GCM = "urn:xmpp:ciphers:aes-256-gcm-nopadding:0"
135 self, 145 self,
136 client: SatXMPPEntity, 146 client: SatXMPPEntity,
137 attachment: Dict[str, Any], 147 attachment: Dict[str, Any],
138 source: Dict[str, Any], 148 source: Dict[str, Any],
139 dest_path: Union[Path, str], 149 dest_path: Union[Path, str],
140 extra: Optional[Dict[str, Any]] = None 150 extra: Optional[Dict[str, Any]] = None,
141 ) -> Tuple[str, defer.Deferred]: 151 ) -> Tuple[str, defer.Deferred]:
142 # TODO: check hash 152 # TODO: check hash
143 if extra is None: 153 if extra is None:
144 extra = {} 154 extra = {}
145 try: 155 try:
152 try: 162 try:
153 download_url = source["url"] 163 download_url = source["url"]
154 except KeyError: 164 except KeyError:
155 raise ValueError(f"{source} has missing URL") 165 raise ValueError(f"{source} has missing URL")
156 166
157 if extra.get('ignore_tls_errors', False): 167 if extra.get("ignore_tls_errors", False):
158 log.warning( 168 log.warning("TLS certificate check disabled, this is highly insecure")
159 "TLS certificate check disabled, this is highly insecure"
160 )
161 treq_client = treq_client_no_ssl 169 treq_client = treq_client_no_ssl
162 else: 170 else:
163 treq_client = treq 171 treq_client = treq
164 172
165 try: 173 try:
166 file_size = int(attachment["size"]) 174 file_size = int(attachment["size"])
167 except (KeyError, ValueError): 175 except (KeyError, ValueError):
168 head_data = await treq_client.head(download_url) 176 head_data = await treq_client.head(download_url)
169 content_length = int(head_data.headers.getRawHeaders('content-length')[0]) 177 content_length = int(head_data.headers.getRawHeaders("content-length")[0])
170 # the 128 bits tag is put at the end 178 # the 128 bits tag is put at the end
171 file_size = content_length - 16 179 file_size = content_length - 16
172 180
173 file_obj = stream.SatFile( 181 file_obj = stream.SatFile(
174 self.host, 182 self.host,
175 client, 183 client,
176 dest_path, 184 dest_path,
177 mode="wb", 185 mode="wb",
178 size = file_size, 186 size=file_size,
179 ) 187 )
180 188
181 if cipher in (NS_AES_128_GCM, NS_AES_256_GCM): 189 if cipher in (NS_AES_128_GCM, NS_AES_256_GCM):
182 decryptor = ciphers.Cipher( 190 decryptor = ciphers.Cipher(
183 ciphers.algorithms.AES(key), 191 ciphers.algorithms.AES(key),
202 decrypt_cb = partial( 210 decrypt_cb = partial(
203 self.cbc_decrypt, 211 self.cbc_decrypt,
204 client=client, 212 client=client,
205 file_obj=file_obj, 213 file_obj=file_obj,
206 decryptor=decryptor, 214 decryptor=decryptor,
207 unpadder=unpadder 215 unpadder=unpadder,
208 ) 216 )
209 finalize_cb = partial( 217 finalize_cb = partial(
210 self.cbc_decrypt_finalize, 218 self.cbc_decrypt_finalize,
211 file_obj=file_obj, 219 file_obj=file_obj,
212 decryptor=decryptor, 220 decryptor=decryptor,
213 unpadder=unpadder 221 unpadder=unpadder,
214 ) 222 )
215 else: 223 else:
216 msg = f"cipher {cipher!r} is not supported" 224 msg = f"cipher {cipher!r} is not supported"
217 file_obj.close(error=msg) 225 file_obj.close(error=msg)
218 log.warning(msg) 226 log.warning(msg)
251 "iv": secrets.token_bytes(12), 259 "iv": secrets.token_bytes(12),
252 "key": secrets.token_bytes(32), 260 "key": secrets.token_bytes(32),
253 } 261 }
254 attachment["filename"] = filename 262 attachment["filename"] = filename
255 return await self._http_upload.file_http_upload( 263 return await self._http_upload.file_http_upload(
256 client=client, 264 client=client, filepath=filepath, filename="encrypted", extra=extra
257 filepath=filepath,
258 filename="encrypted",
259 extra=extra
260 ) 265 )
261 266
262 async def attach(self, client, data): 267 async def attach(self, client, data):
263 # XXX: for now, XEP-0447/XEP-0448 only allow to send one file per <message/>, thus 268 # XXX: for now, XEP-0447/XEP-0448 only allow to send one file per <message/>, thus
264 # we need to send each file in a separate message, in the same way as for 269 # we need to send each file in a separate message, in the same way as for
265 # plugin_sec_aesgcm. 270 # plugin_sec_aesgcm.
266 attachments = data["extra"][C.KEY_ATTACHMENTS] 271 attachments = data["extra"][C.KEY_ATTACHMENTS]
267 if not data['message'] or data['message'] == {'': ''}: 272 if not data["message"] or data["message"] == {"": ""}:
268 extra_attachments = attachments[1:] 273 extra_attachments = attachments[1:]
269 del attachments[1:] 274 del attachments[1:]
270 else: 275 else:
271 # we have a message, we must send first attachment separately 276 # we have a message, we must send first attachment separately
272 extra_attachments = attachments[:] 277 extra_attachments = attachments[:]
285 file_hash = (attachment["hash_algo"], attachment["hash"]) 290 file_hash = (attachment["hash_algo"], attachment["hash"])
286 file_sharing_elt = self._sfs.get_file_sharing_elt( 291 file_sharing_elt = self._sfs.get_file_sharing_elt(
287 [], 292 [],
288 name=attachment["filename"], 293 name=attachment["filename"],
289 size=attachment["size"], 294 size=attachment["size"],
290 file_hash=file_hash 295 file_hash=file_hash,
291 ) 296 )
292 encrypted_elt = file_sharing_elt.sources.addElement( 297 encrypted_elt = file_sharing_elt.sources.addElement(
293 (NS_ESFS, "encrypted") 298 (NS_ESFS, "encrypted")
294 ) 299 )
295 encrypted_elt["cipher"] = NS_AES_256_GCM 300 encrypted_elt["cipher"] = NS_AES_256_GCM
296 encrypted_elt.addElement( 301 encrypted_elt.addElement(
297 "key", 302 "key", content=base64.b64encode(encryption_data["key"]).decode()
298 content=base64.b64encode(encryption_data["key"]).decode()
299 ) 303 )
300 encrypted_elt.addElement( 304 encrypted_elt.addElement(
301 "iv", 305 "iv", content=base64.b64encode(encryption_data["iv"]).decode()
302 content=base64.b64encode(encryption_data["iv"]).decode() 306 )
303 ) 307 encrypted_elt.addChild(
304 encrypted_elt.addChild(self._h.build_hash_elt( 308 self._h.build_hash_elt(
305 attachment["encrypted_hash"], 309 attachment["encrypted_hash"], attachment["encrypted_hash_algo"]
306 attachment["encrypted_hash_algo"] 310 )
307 )) 311 )
308 encrypted_elt.addChild( 312 encrypted_elt.addChild(
309 self._sfs.get_sources_elt( 313 self._sfs.get_sources_elt(
310 [self._u.get_url_data_elt(attachment["url"])] 314 [self._u.get_url_data_elt(attachment["url"])]
311 ) 315 )
312 ) 316 )
313 data["xml"].addChild(file_sharing_elt) 317 data["xml"].addChild(file_sharing_elt)
314 318
315 for attachment in extra_attachments: 319 for attachment in extra_attachments:
316 # we send all remaining attachment in a separate message 320 # we send all remaining attachment in a separate message
317 await client.sendMessage( 321 await client.sendMessage(
318 to_jid=data['to'], 322 to_jid=data["to"],
319 message={'': ''}, 323 message={"": ""},
320 subject=data['subject'], 324 subject=data["subject"],
321 mess_type=data['type'], 325 mess_type=data["type"],
322 extra={C.KEY_ATTACHMENTS: [attachment]}, 326 extra={C.KEY_ATTACHMENTS: [attachment]},
323 ) 327 )
324 328
325 if ((not data['extra'] 329 if (
326 and (not data['message'] or data['message'] == {'': ''}) 330 not data["extra"]
327 and not data['subject'])): 331 and (not data["message"] or data["message"] == {"": ""})
332 and not data["subject"]
333 ):
328 # nothing left to send, we can cancel the message 334 # nothing left to send, we can cancel the message
329 raise exceptions.CancelError("Cancelled by XEP_0448 attachment handling") 335 raise exceptions.CancelError("Cancelled by XEP_0448 attachment handling")
330 336
331 def gcm_decrypt( 337 def gcm_decrypt(
332 self, 338 self,
333 data: bytes, 339 data: bytes,
334 client: SatXMPPEntity, 340 client: SatXMPPEntity,
335 file_obj: stream.SatFile, 341 file_obj: stream.SatFile,
336 decryptor: CipherContext 342 decryptor: CipherContext,
337 ) -> None: 343 ) -> None:
338 if file_obj.tell() + len(data) > file_obj.size: # type: ignore 344 if file_obj.tell() + len(data) > file_obj.size: # type: ignore
339 # we're reaching end of file with this bunch of data 345 # we're reaching end of file with this bunch of data
340 # we may still have a last bunch if the tag is incomplete 346 # we may still have a last bunch if the tag is incomplete
341 bytes_left = file_obj.size - file_obj.tell() # type: ignore 347 bytes_left = file_obj.size - file_obj.tell() # type: ignore
369 self, 375 self,
370 data: bytes, 376 data: bytes,
371 client: SatXMPPEntity, 377 client: SatXMPPEntity,
372 file_obj: stream.SatFile, 378 file_obj: stream.SatFile,
373 decryptor: CipherContext, 379 decryptor: CipherContext,
374 unpadder: PaddingContext 380 unpadder: PaddingContext,
375 ) -> None: 381 ) -> None:
376 decrypted = decryptor.update(data) 382 decrypted = decryptor.update(data)
377 file_obj.write(unpadder.update(decrypted)) 383 file_obj.write(unpadder.update(decrypted))
378 384
379 def cbc_decrypt_finalize( 385 def cbc_decrypt_finalize(
380 self, 386 self, file_obj: stream.SatFile, decryptor: CipherContext, unpadder: PaddingContext
381 file_obj: stream.SatFile,
382 decryptor: CipherContext,
383 unpadder: PaddingContext
384 ) -> None: 387 ) -> None:
385 decrypted = decryptor.finalize() 388 decrypted = decryptor.finalize()
386 file_obj.write(unpadder.update(decrypted)) 389 file_obj.write(unpadder.update(decrypted))
387 file_obj.write(unpadder.finalize()) 390 file_obj.write(unpadder.finalize())
388 file_obj.close() 391 file_obj.close()
389 392
390 def _upload_pre_slot(self, client, extra, file_metadata): 393 def _upload_pre_slot(self, client, extra, file_metadata):
391 if extra.get('encryption') != IMPORT_NAME: 394 if extra.get("encryption") != IMPORT_NAME:
392 return True 395 return True
393 # the tag is appended to the file 396 # the tag is appended to the file
394 file_metadata["size"] += 16 397 file_metadata["size"] += 16
395 return True 398 return True
396 399
412 encrypted_hasher.update(ret) 415 encrypted_hasher.update(ret)
413 attachment["encrypted_hash"] = encrypted_hasher.hexdigest() 416 attachment["encrypted_hash"] = encrypted_hasher.hexdigest()
414 return ret 417 return ret
415 except AlreadyFinalized: 418 except AlreadyFinalized:
416 # as we have already finalized, we can now send EOF 419 # as we have already finalized, we can now send EOF
417 return b'' 420 return b""
418 421
419 def _upload_trigger(self, client, extra, sat_file, file_producer, slot): 422 def _upload_trigger(self, client, extra, sat_file, file_producer, slot):
420 if extra.get('encryption') != IMPORT_NAME: 423 if extra.get("encryption") != IMPORT_NAME:
421 return True 424 return True
422 attachment = extra["attachment"] 425 attachment = extra["attachment"]
423 encryption_data = extra["encryption_data"] 426 encryption_data = extra["encryption_data"]
424 log.debug("encrypting file with AES-GCM") 427 log.debug("encrypting file with AES-GCM")
425 iv = encryption_data["iv"] 428 iv = encryption_data["iv"]
440 backend=backends.default_backend(), 443 backend=backends.default_backend(),
441 ).encryptor() 444 ).encryptor()
442 445
443 if sat_file.data_cb is not None: 446 if sat_file.data_cb is not None:
444 raise exceptions.InternalError( 447 raise exceptions.InternalError(
445 f"data_cb was expected to be None, it is set to {sat_file.data_cb}") 448 f"data_cb was expected to be None, it is set to {sat_file.data_cb}"
446 449 )
447 attachment.update({ 450
448 "hash_algo": self._h.ALGO_DEFAULT, 451 attachment.update(
449 "hasher": self._h.get_hasher(), 452 {
450 "encrypted_hash_algo": self._h.ALGO_DEFAULT, 453 "hash_algo": self._h.ALGO_DEFAULT,
451 "encrypted_hasher": self._h.get_hasher(), 454 "hasher": self._h.get_hasher(),
452 }) 455 "encrypted_hash_algo": self._h.ALGO_DEFAULT,
456 "encrypted_hasher": self._h.get_hasher(),
457 }
458 )
453 459
454 # with data_cb we encrypt the file on the fly 460 # with data_cb we encrypt the file on the fly
455 sat_file.data_cb = partial( 461 sat_file.data_cb = partial(
456 self._encrypt, encryptor=encryptor, attachment=attachment 462 self._encrypt, encryptor=encryptor, attachment=attachment
457 ) 463 )