Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_xep_0384.py @ 4270:0d7bb4df2343
Reformatted code base using black.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 19 Jun 2024 18:44:57 +0200 |
parents | e11b13418ba6 |
children |
comparison
equal
deleted
inserted
replaced
4269:64a85ce8be70 | 4270:0d7bb4df2343 |
---|---|
19 import base64 | 19 import base64 |
20 from datetime import datetime | 20 from datetime import datetime |
21 import enum | 21 import enum |
22 import logging | 22 import logging |
23 import time | 23 import time |
24 from typing import \ | 24 from typing import ( |
25 Any, Dict, FrozenSet, Iterable, List, Literal, NamedTuple, Optional, Set, Type, Union, cast | 25 Any, |
26 Dict, | |
27 FrozenSet, | |
28 Iterable, | |
29 List, | |
30 Literal, | |
31 NamedTuple, | |
32 Optional, | |
33 Set, | |
34 Type, | |
35 Union, | |
36 cast, | |
37 ) | |
26 import uuid | 38 import uuid |
27 import xml.etree.ElementTree as ET | 39 import xml.etree.ElementTree as ET |
28 from xml.sax.saxutils import quoteattr | 40 from xml.sax.saxutils import quoteattr |
29 | 41 |
30 from typing_extensions import Final, Never, assert_never | 42 from typing_extensions import Final, Never, assert_never |
43 from libervia.backend.plugins.plugin_xep_0045 import XEP_0045 | 55 from libervia.backend.plugins.plugin_xep_0045 import XEP_0045 |
44 from libervia.backend.plugins.plugin_xep_0060 import XEP_0060 | 56 from libervia.backend.plugins.plugin_xep_0060 import XEP_0060 |
45 from libervia.backend.plugins.plugin_xep_0163 import XEP_0163 | 57 from libervia.backend.plugins.plugin_xep_0163 import XEP_0163 |
46 from libervia.backend.plugins.plugin_xep_0334 import XEP_0334 | 58 from libervia.backend.plugins.plugin_xep_0334 import XEP_0334 |
47 from libervia.backend.plugins.plugin_xep_0359 import XEP_0359 | 59 from libervia.backend.plugins.plugin_xep_0359 import XEP_0359 |
48 from libervia.backend.plugins.plugin_xep_0420 import \ | 60 from libervia.backend.plugins.plugin_xep_0420 import ( |
49 XEP_0420, SCEAffixPolicy, SCEAffixValues, SCEProfile | 61 XEP_0420, |
62 SCEAffixPolicy, | |
63 SCEAffixValues, | |
64 SCEProfile, | |
65 ) | |
50 from libervia.backend.tools import xml_tools | 66 from libervia.backend.tools import xml_tools |
51 from twisted.internet import defer | 67 from twisted.internet import defer |
52 from twisted.words.protocols.jabber import error, jid | 68 from twisted.words.protocols.jabber import error, jid |
53 from twisted.words.xish import domish | 69 from twisted.words.xish import domish |
54 | 70 |
71 " download/install the pip packages 'omemo', 'twomemo', 'oldmemo' and" | 87 " download/install the pip packages 'omemo', 'twomemo', 'oldmemo' and" |
72 f" 'xmlschema'.\nexception: {import_error}" | 88 f" 'xmlschema'.\nexception: {import_error}" |
73 ) from import_error | 89 ) from import_error |
74 | 90 |
75 | 91 |
76 __all__ = [ # pylint: disable=unused-variable | 92 __all__ = ["PLUGIN_INFO", "OMEMO"] # pylint: disable=unused-variable |
77 "PLUGIN_INFO", | |
78 "OMEMO" | |
79 ] | |
80 | 93 |
81 log = cast(Logger, getLogger(__name__)) # type: ignore[no-untyped-call] | 94 log = cast(Logger, getLogger(__name__)) # type: ignore[no-untyped-call] |
82 | 95 |
83 | 96 |
84 PLUGIN_INFO = { | 97 PLUGIN_INFO = { |
85 C.PI_NAME: "OMEMO", | 98 C.PI_NAME: "OMEMO", |
86 C.PI_IMPORT_NAME: "XEP-0384", | 99 C.PI_IMPORT_NAME: "XEP-0384", |
87 C.PI_TYPE: "SEC", | 100 C.PI_TYPE: "SEC", |
88 C.PI_PROTOCOLS: [ "XEP-0384" ], | 101 C.PI_PROTOCOLS: ["XEP-0384"], |
89 C.PI_DEPENDENCIES: [ "XEP-0163", "XEP-0280", "XEP-0334", "XEP-0060", "XEP-0420" ], | 102 C.PI_DEPENDENCIES: ["XEP-0163", "XEP-0280", "XEP-0334", "XEP-0060", "XEP-0420"], |
90 C.PI_RECOMMENDATIONS: [ "XEP-0045", "XEP-0359", C.TEXT_CMDS ], | 103 C.PI_RECOMMENDATIONS: ["XEP-0045", "XEP-0359", C.TEXT_CMDS], |
91 C.PI_MAIN: "OMEMO", | 104 C.PI_MAIN: "OMEMO", |
92 C.PI_HANDLER: "no", | 105 C.PI_HANDLER: "no", |
93 C.PI_DESCRIPTION: _("""Implementation of OMEMO"""), | 106 C.PI_DESCRIPTION: _("""Implementation of OMEMO"""), |
94 } | 107 } |
95 | 108 |
217 own_device_id = await self.__storage.get(LegacyStorageImpl.KEY_DEVICE_ID, None) | 230 own_device_id = await self.__storage.get(LegacyStorageImpl.KEY_DEVICE_ID, None) |
218 if own_device_id is None: | 231 if own_device_id is None: |
219 return None | 232 return None |
220 | 233 |
221 return oldmemo.migrations.OwnData( | 234 return oldmemo.migrations.OwnData( |
222 own_bare_jid=self.__own_bare_jid, | 235 own_bare_jid=self.__own_bare_jid, own_device_id=own_device_id |
223 own_device_id=own_device_id | |
224 ) | 236 ) |
225 | 237 |
226 async def deleteOwnData(self) -> None: | 238 async def deleteOwnData(self) -> None: |
227 try: | 239 try: |
228 await self.__storage.remove(LegacyStorageImpl.KEY_DEVICE_ID) | 240 await self.__storage.remove(LegacyStorageImpl.KEY_DEVICE_ID) |
230 pass | 242 pass |
231 | 243 |
232 async def loadState(self) -> Optional[oldmemo.migrations.State]: | 244 async def loadState(self) -> Optional[oldmemo.migrations.State]: |
233 return cast( | 245 return cast( |
234 Optional[oldmemo.migrations.State], | 246 Optional[oldmemo.migrations.State], |
235 await self.__storage.get(LegacyStorageImpl.KEY_STATE, None) | 247 await self.__storage.get(LegacyStorageImpl.KEY_STATE, None), |
236 ) | 248 ) |
237 | 249 |
238 async def deleteState(self) -> None: | 250 async def deleteState(self) -> None: |
239 try: | 251 try: |
240 await self.__storage.remove(LegacyStorageImpl.KEY_STATE) | 252 await self.__storage.remove(LegacyStorageImpl.KEY_STATE) |
241 except KeyError: | 253 except KeyError: |
242 pass | 254 pass |
243 | 255 |
244 async def loadSession( | 256 async def loadSession( |
245 self, | 257 self, bare_jid: str, device_id: int |
246 bare_jid: str, | |
247 device_id: int | |
248 ) -> Optional[oldmemo.migrations.Session]: | 258 ) -> Optional[oldmemo.migrations.Session]: |
249 key = "\n".join([ LegacyStorageImpl.KEY_SESSION, bare_jid, str(device_id) ]) | 259 key = "\n".join([LegacyStorageImpl.KEY_SESSION, bare_jid, str(device_id)]) |
250 | 260 |
251 return cast( | 261 return cast( |
252 Optional[oldmemo.migrations.Session], | 262 Optional[oldmemo.migrations.Session], await self.__storage.get(key, None) |
253 await self.__storage.get(key, None) | |
254 ) | 263 ) |
255 | 264 |
256 async def deleteSession(self, bare_jid: str, device_id: int) -> None: | 265 async def deleteSession(self, bare_jid: str, device_id: int) -> None: |
257 key = "\n".join([ LegacyStorageImpl.KEY_SESSION, bare_jid, str(device_id) ]) | 266 key = "\n".join([LegacyStorageImpl.KEY_SESSION, bare_jid, str(device_id)]) |
258 | 267 |
259 try: | 268 try: |
260 await self.__storage.remove(key) | 269 await self.__storage.remove(key) |
261 except KeyError: | 270 except KeyError: |
262 pass | 271 pass |
263 | 272 |
264 async def loadActiveDevices(self, bare_jid: str) -> Optional[List[int]]: | 273 async def loadActiveDevices(self, bare_jid: str) -> Optional[List[int]]: |
265 key = "\n".join([ LegacyStorageImpl.KEY_ACTIVE_DEVICES, bare_jid ]) | 274 key = "\n".join([LegacyStorageImpl.KEY_ACTIVE_DEVICES, bare_jid]) |
266 | 275 |
267 return cast( | 276 return cast(Optional[List[int]], await self.__storage.get(key, None)) |
268 Optional[List[int]], | |
269 await self.__storage.get(key, None) | |
270 ) | |
271 | 277 |
272 async def loadInactiveDevices(self, bare_jid: str) -> Optional[Dict[int, int]]: | 278 async def loadInactiveDevices(self, bare_jid: str) -> Optional[Dict[int, int]]: |
273 key = "\n".join([ LegacyStorageImpl.KEY_INACTIVE_DEVICES, bare_jid ]) | 279 key = "\n".join([LegacyStorageImpl.KEY_INACTIVE_DEVICES, bare_jid]) |
274 | 280 |
275 return cast( | 281 return cast(Optional[Dict[int, int]], await self.__storage.get(key, None)) |
276 Optional[Dict[int, int]], | |
277 await self.__storage.get(key, None) | |
278 ) | |
279 | 282 |
280 async def deleteActiveDevices(self, bare_jid: str) -> None: | 283 async def deleteActiveDevices(self, bare_jid: str) -> None: |
281 key = "\n".join([ LegacyStorageImpl.KEY_ACTIVE_DEVICES, bare_jid ]) | 284 key = "\n".join([LegacyStorageImpl.KEY_ACTIVE_DEVICES, bare_jid]) |
282 | 285 |
283 try: | 286 try: |
284 await self.__storage.remove(key) | 287 await self.__storage.remove(key) |
285 except KeyError: | 288 except KeyError: |
286 pass | 289 pass |
287 | 290 |
288 async def deleteInactiveDevices(self, bare_jid: str) -> None: | 291 async def deleteInactiveDevices(self, bare_jid: str) -> None: |
289 key = "\n".join([ LegacyStorageImpl.KEY_INACTIVE_DEVICES, bare_jid ]) | 292 key = "\n".join([LegacyStorageImpl.KEY_INACTIVE_DEVICES, bare_jid]) |
290 | 293 |
291 try: | 294 try: |
292 await self.__storage.remove(key) | 295 await self.__storage.remove(key) |
293 except KeyError: | 296 except KeyError: |
294 pass | 297 pass |
295 | 298 |
296 async def loadTrust( | 299 async def loadTrust( |
297 self, | 300 self, bare_jid: str, device_id: int |
298 bare_jid: str, | |
299 device_id: int | |
300 ) -> Optional[oldmemo.migrations.Trust]: | 301 ) -> Optional[oldmemo.migrations.Trust]: |
301 key = "\n".join([ LegacyStorageImpl.KEY_TRUST, bare_jid, str(device_id) ]) | 302 key = "\n".join([LegacyStorageImpl.KEY_TRUST, bare_jid, str(device_id)]) |
302 | 303 |
303 return cast( | 304 return cast( |
304 Optional[oldmemo.migrations.Trust], | 305 Optional[oldmemo.migrations.Trust], await self.__storage.get(key, None) |
305 await self.__storage.get(key, None) | |
306 ) | 306 ) |
307 | 307 |
308 async def deleteTrust(self, bare_jid: str, device_id: int) -> None: | 308 async def deleteTrust(self, bare_jid: str, device_id: int) -> None: |
309 key = "\n".join([ LegacyStorageImpl.KEY_TRUST, bare_jid, str(device_id) ]) | 309 key = "\n".join([LegacyStorageImpl.KEY_TRUST, bare_jid, str(device_id)]) |
310 | 310 |
311 try: | 311 try: |
312 await self.__storage.remove(key) | 312 await self.__storage.remove(key) |
313 except KeyError: | 313 except KeyError: |
314 pass | 314 pass |
324 except KeyError: | 324 except KeyError: |
325 pass | 325 pass |
326 | 326 |
327 | 327 |
328 async def download_oldmemo_bundle( | 328 async def download_oldmemo_bundle( |
329 client: SatXMPPClient, | 329 client: SatXMPPClient, xep_0060: XEP_0060, bare_jid: str, device_id: int |
330 xep_0060: XEP_0060, | |
331 bare_jid: str, | |
332 device_id: int | |
333 ) -> oldmemo.oldmemo.BundleImpl: | 330 ) -> oldmemo.oldmemo.BundleImpl: |
334 """Download the oldmemo bundle corresponding to a specific device. | 331 """Download the oldmemo bundle corresponding to a specific device. |
335 | 332 |
336 @param client: The client. | 333 @param client: The client. |
337 @param xep_0060: The XEP-0060 plugin instance to use for pubsub interactions. | 334 @param xep_0060: The XEP-0060 plugin instance to use for pubsub interactions. |
359 raise omemo.BundleDownloadFailed( | 356 raise omemo.BundleDownloadFailed( |
360 f"Bundle download failed for {bare_jid}: {device_id} under namespace" | 357 f"Bundle download failed for {bare_jid}: {device_id} under namespace" |
361 f" {namespace}: Unexpected number of items retrieved: {len(items)}." | 358 f" {namespace}: Unexpected number of items retrieved: {len(items)}." |
362 ) | 359 ) |
363 | 360 |
364 element = \ | 361 element = next( |
365 next(iter(xml_tools.domish_elt_2_et_elt(cast(domish.Element, items[0]))), None) | 362 iter(xml_tools.domish_elt_2_et_elt(cast(domish.Element, items[0]))), None |
363 ) | |
366 if element is None: | 364 if element is None: |
367 raise omemo.BundleDownloadFailed( | 365 raise omemo.BundleDownloadFailed( |
368 f"Bundle download failed for {bare_jid}: {device_id} under namespace" | 366 f"Bundle download failed for {bare_jid}: {device_id} under namespace" |
369 f" {namespace}: Item download succeeded but parsing failed: {element}." | 367 f" {namespace}: Item download succeeded but parsing failed: {element}." |
370 ) | 368 ) |
383 # add ATM to the OMEMO plugin directly instead of having it a separate Libervia plugin. | 381 # add ATM to the OMEMO plugin directly instead of having it a separate Libervia plugin. |
384 NS_TM: Final = "urn:xmpp:tm:1" | 382 NS_TM: Final = "urn:xmpp:tm:1" |
385 NS_ATM: Final = "urn:xmpp:atm:1" | 383 NS_ATM: Final = "urn:xmpp:atm:1" |
386 | 384 |
387 | 385 |
388 TRUST_MESSAGE_SCHEMA = xmlschema.XMLSchema("""<?xml version='1.0' encoding='UTF-8'?> | 386 TRUST_MESSAGE_SCHEMA = xmlschema.XMLSchema( |
387 """<?xml version='1.0' encoding='UTF-8'?> | |
389 <xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema' | 388 <xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema' |
390 targetNamespace='urn:xmpp:tm:1' | 389 targetNamespace='urn:xmpp:tm:1' |
391 xmlns='urn:xmpp:tm:1' | 390 xmlns='urn:xmpp:tm:1' |
392 elementFormDefault='qualified'> | 391 elementFormDefault='qualified'> |
393 | 392 |
411 </xs:sequence> | 410 </xs:sequence> |
412 <xs:attribute name='jid' type='xs:string' use='required'/> | 411 <xs:attribute name='jid' type='xs:string' use='required'/> |
413 </xs:complexType> | 412 </xs:complexType> |
414 </xs:element> | 413 </xs:element> |
415 </xs:schema> | 414 </xs:schema> |
416 """) | 415 """ |
416 ) | |
417 | 417 |
418 | 418 |
419 # This is compatible with omemo:2's SCE profile | 419 # This is compatible with omemo:2's SCE profile |
420 TM_SCE_PROFILE = SCEProfile( | 420 TM_SCE_PROFILE = SCEProfile( |
421 rpad_policy=SCEAffixPolicy.REQUIRED, | 421 rpad_policy=SCEAffixPolicy.REQUIRED, |
422 time_policy=SCEAffixPolicy.REQUIRED, | 422 time_policy=SCEAffixPolicy.REQUIRED, |
423 to_policy=SCEAffixPolicy.OPTIONAL, | 423 to_policy=SCEAffixPolicy.OPTIONAL, |
424 from_policy=SCEAffixPolicy.OPTIONAL, | 424 from_policy=SCEAffixPolicy.OPTIONAL, |
425 custom_policies={} | 425 custom_policies={}, |
426 ) | 426 ) |
427 | 427 |
428 | 428 |
429 class TrustUpdate(NamedTuple): | 429 class TrustUpdate(NamedTuple): |
430 # pylint: disable=invalid-name | 430 # pylint: disable=invalid-name |
468 """Convert the instance to a serialised dictionary""" | 468 """Convert the instance to a serialised dictionary""" |
469 data = { | 469 data = { |
470 "sender_jid": self.sender_jid.full(), | 470 "sender_jid": self.sender_jid.full(), |
471 "sender_key": self.sender_key.hex(), | 471 "sender_key": self.sender_key.hex(), |
472 "timestamp": self.timestamp.isoformat(), | 472 "timestamp": self.timestamp.isoformat(), |
473 "trust_update": self.trust_update.to_dict() | 473 "trust_update": self.trust_update.to_dict(), |
474 } | 474 } |
475 return data | 475 return data |
476 | 476 |
477 @staticmethod | 477 @staticmethod |
478 def from_dict(data: dict[str, Any]) -> "TrustMessageCacheEntry": | 478 def from_dict(data: dict[str, Any]) -> "TrustMessageCacheEntry": |
497 | 497 |
498 | 498 |
499 async def manage_trust_message_cache( | 499 async def manage_trust_message_cache( |
500 client: SatXMPPClient, | 500 client: SatXMPPClient, |
501 session_manager: omemo.SessionManager, | 501 session_manager: omemo.SessionManager, |
502 applied_trust_updates: FrozenSet[TrustUpdate] | 502 applied_trust_updates: FrozenSet[TrustUpdate], |
503 ) -> None: | 503 ) -> None: |
504 """Manage the ATM trust message cache after trust updates have been applied. | 504 """Manage the ATM trust message cache after trust updates have been applied. |
505 | 505 |
506 @param client: The client this operation runs under. | 506 @param client: The client this operation runs under. |
507 @param session_manager: The session manager to use. | 507 @param session_manager: The session manager to use. |
508 @param applied_trust_updates: The trust updates that have already been applied, | 508 @param applied_trust_updates: The trust updates that have already been applied, |
509 triggering this cache management run. | 509 triggering this cache management run. |
510 """ | 510 """ |
511 | 511 |
512 trust_message_cache = persistent.LazyPersistentBinaryDict( | 512 trust_message_cache = persistent.LazyPersistentBinaryDict( |
513 "XEP-0384/TM", | 513 "XEP-0384/TM", client.profile |
514 client.profile | |
515 ) | 514 ) |
516 | 515 |
517 # Load cache entries | 516 # Load cache entries |
518 cache_entries = { | 517 cache_entries = { |
519 TrustMessageCacheEntry.from_dict(d) | 518 TrustMessageCacheEntry.from_dict(d) |
522 | 521 |
523 # Expire cache entries that were overwritten by the applied trust updates | 522 # Expire cache entries that were overwritten by the applied trust updates |
524 cache_entries_by_target = { | 523 cache_entries_by_target = { |
525 ( | 524 ( |
526 cache_entry.trust_update.target_jid.userhostJID(), | 525 cache_entry.trust_update.target_jid.userhostJID(), |
527 cache_entry.trust_update.target_key | 526 cache_entry.trust_update.target_key, |
528 ): cache_entry | 527 ): cache_entry |
529 for cache_entry | 528 for cache_entry in cache_entries |
530 in cache_entries | |
531 } | 529 } |
532 | 530 |
533 for trust_update in applied_trust_updates: | 531 for trust_update in applied_trust_updates: |
534 cache_entry = cache_entries_by_target.get( | 532 cache_entry = cache_entries_by_target.get( |
535 (trust_update.target_jid.userhostJID(), trust_update.target_key), | 533 (trust_update.target_jid.userhostJID(), trust_update.target_key), None |
536 None | |
537 ) | 534 ) |
538 | 535 |
539 if cache_entry is not None: | 536 if cache_entry is not None: |
540 cache_entries.remove(cache_entry) | 537 cache_entries.remove(cache_entry) |
541 | 538 |
559 | 556 |
560 # Apply the trust update | 557 # Apply the trust update |
561 await session_manager.set_trust( | 558 await session_manager.set_trust( |
562 cache_entry.trust_update.target_jid.userhost(), | 559 cache_entry.trust_update.target_jid.userhost(), |
563 cache_entry.trust_update.target_key, | 560 cache_entry.trust_update.target_key, |
564 trust_level.name | 561 trust_level.name, |
565 ) | 562 ) |
566 | 563 |
567 # Track the fact that this trust update has been applied | 564 # Track the fact that this trust update has been applied |
568 new_trust_updates.add(cache_entry.trust_update) | 565 new_trust_updates.add(cache_entry.trust_update) |
569 | 566 |
570 # Remove the corresponding cache entry | 567 # Remove the corresponding cache entry |
571 cache_entries.remove(cache_entry) | 568 cache_entries.remove(cache_entry) |
572 | 569 |
573 # Store the updated cache entries | 570 # Store the updated cache entries |
574 await trust_message_cache.force( | 571 await trust_message_cache.force("cache", [tm.to_dict() for tm in cache_entries]) |
575 "cache", | |
576 [tm.to_dict() for tm in cache_entries] | |
577 ) | |
578 | 572 |
579 # TODO: Notify the user ("feedback") about automatically updated trust? | 573 # TODO: Notify the user ("feedback") about automatically updated trust? |
580 | 574 |
581 if len(new_trust_updates) > 0: | 575 if len(new_trust_updates) > 0: |
582 # If any trust has been updated, recursively perform another run of cache | 576 # If any trust has been updated, recursively perform another run of cache |
583 # management | 577 # management |
584 await manage_trust_message_cache( | 578 await manage_trust_message_cache( |
585 client, | 579 client, session_manager, frozenset(new_trust_updates) |
586 session_manager, | |
587 frozenset(new_trust_updates) | |
588 ) | 580 ) |
589 | 581 |
590 | 582 |
591 async def get_trust_as_trust_updates( | 583 async def get_trust_as_trust_updates( |
592 session_manager: omemo.SessionManager, | 584 session_manager: omemo.SessionManager, target_jid: jid.JID |
593 target_jid: jid.JID | |
594 ) -> FrozenSet[TrustUpdate]: | 585 ) -> FrozenSet[TrustUpdate]: |
595 """Get the trust status of all known keys of a JID as trust updates for use with ATM. | 586 """Get the trust status of all known keys of a JID as trust updates for use with ATM. |
596 | 587 |
597 @param session_manager: The session manager to load the trust from. | 588 @param session_manager: The session manager to load the trust from. |
598 @param target_jid: The JID to load the trust for. | 589 @param target_jid: The JID to load the trust for. |
615 target_trust = False | 606 target_trust = False |
616 else: | 607 else: |
617 # Skip devices that are not explicitly trusted or distrusted | 608 # Skip devices that are not explicitly trusted or distrusted |
618 continue | 609 continue |
619 | 610 |
620 trust_updates.add(TrustUpdate( | 611 trust_updates.add( |
621 target_jid=target_jid.userhostJID(), | 612 TrustUpdate( |
622 target_key=device.identity_key, | 613 target_jid=target_jid.userhostJID(), |
623 target_trust=target_trust | 614 target_key=device.identity_key, |
624 )) | 615 target_trust=target_trust, |
616 ) | |
617 ) | |
625 | 618 |
626 return frozenset(trust_updates) | 619 return frozenset(trust_updates) |
627 | 620 |
628 | 621 |
629 async def send_trust_messages( | 622 async def send_trust_messages( |
630 client: SatXMPPClient, | 623 client: SatXMPPClient, |
631 session_manager: omemo.SessionManager, | 624 session_manager: omemo.SessionManager, |
632 applied_trust_updates: FrozenSet[TrustUpdate] | 625 applied_trust_updates: FrozenSet[TrustUpdate], |
633 ) -> None: | 626 ) -> None: |
634 """Send information about updated trust to peers via ATM (XEP-0450). | 627 """Send information about updated trust to peers via ATM (XEP-0450). |
635 | 628 |
636 @param client: The client. | 629 @param client: The client. |
637 @param session_manager: The session manager. | 630 @param session_manager: The session manager. |
645 # Send Trust Messages for newly trusted and distrusted devices | 638 # Send Trust Messages for newly trusted and distrusted devices |
646 own_jid = client.jid.userhostJID() | 639 own_jid = client.jid.userhostJID() |
647 own_trust_updates = await get_trust_as_trust_updates(session_manager, own_jid) | 640 own_trust_updates = await get_trust_as_trust_updates(session_manager, own_jid) |
648 | 641 |
649 # JIDs of which at least one device's trust has been updated | 642 # JIDs of which at least one device's trust has been updated |
650 updated_jids = frozenset({ | 643 updated_jids = frozenset( |
651 trust_update.target_jid.userhostJID() | 644 {trust_update.target_jid.userhostJID() for trust_update in applied_trust_updates} |
652 for trust_update | 645 ) |
653 in applied_trust_updates | |
654 }) | |
655 | 646 |
656 trust_messages: Set[PartialTrustMessage] = set() | 647 trust_messages: Set[PartialTrustMessage] = set() |
657 | 648 |
658 for updated_jid in updated_jids: | 649 for updated_jid in updated_jids: |
659 # Get the trust updates for that JID | 650 # Get the trust updates for that JID |
660 trust_updates = frozenset({ | 651 trust_updates = frozenset( |
661 trust_update for trust_update in applied_trust_updates | 652 { |
662 if trust_update.target_jid.userhostJID() == updated_jid | 653 trust_update |
663 }) | 654 for trust_update in applied_trust_updates |
655 if trust_update.target_jid.userhostJID() == updated_jid | |
656 } | |
657 ) | |
664 | 658 |
665 if updated_jid == own_jid: | 659 if updated_jid == own_jid: |
666 # If the own JID is updated, _all_ peers have to be notified | 660 # If the own JID is updated, _all_ peers have to be notified |
667 # TODO: Using my author's privilege here to shamelessly access private fields | 661 # TODO: Using my author's privilege here to shamelessly access private fields |
668 # and storage keys until I've added public API to get a list of peers to | 662 # and storage keys until I've added public API to get a list of peers to |
669 # python-omemo. | 663 # python-omemo. |
670 storage: omemo.Storage = getattr(session_manager, "_SessionManager__storage") | 664 storage: omemo.Storage = getattr(session_manager, "_SessionManager__storage") |
671 peer_jids = frozenset({ | 665 peer_jids = frozenset( |
672 jid.JID(bare_jid).userhostJID() for bare_jid in (await storage.load_list( | 666 { |
673 f"/{OMEMO.NS_TWOMEMO}/bare_jids", | 667 jid.JID(bare_jid).userhostJID() |
674 str | 668 for bare_jid in ( |
675 )).maybe([]) | 669 await storage.load_list(f"/{OMEMO.NS_TWOMEMO}/bare_jids", str) |
676 }) | 670 ).maybe([]) |
671 } | |
672 ) | |
677 | 673 |
678 if len(peer_jids) == 0: | 674 if len(peer_jids) == 0: |
679 # If there are no peers to notify, notify our other devices about the | 675 # If there are no peers to notify, notify our other devices about the |
680 # changes directly | 676 # changes directly |
681 trust_messages.add(PartialTrustMessage( | 677 trust_messages.add( |
682 recipient_jid=own_jid, | 678 PartialTrustMessage( |
683 updated_jid=own_jid, | 679 recipient_jid=own_jid, |
684 trust_updates=trust_updates | 680 updated_jid=own_jid, |
685 )) | 681 trust_updates=trust_updates, |
682 ) | |
683 ) | |
686 else: | 684 else: |
687 # Otherwise, notify all peers about the changes in trust and let carbons | 685 # Otherwise, notify all peers about the changes in trust and let carbons |
688 # handle the copy to our own JID | 686 # handle the copy to our own JID |
689 for peer_jid in peer_jids: | 687 for peer_jid in peer_jids: |
690 trust_messages.add(PartialTrustMessage( | 688 trust_messages.add( |
691 recipient_jid=peer_jid, | 689 PartialTrustMessage( |
692 updated_jid=own_jid, | 690 recipient_jid=peer_jid, |
693 trust_updates=trust_updates | 691 updated_jid=own_jid, |
694 )) | 692 trust_updates=trust_updates, |
693 ) | |
694 ) | |
695 | 695 |
696 # Also send full trust information about _every_ peer to our newly | 696 # Also send full trust information about _every_ peer to our newly |
697 # trusted devices | 697 # trusted devices |
698 peer_trust_updates = \ | 698 peer_trust_updates = await get_trust_as_trust_updates( |
699 await get_trust_as_trust_updates(session_manager, peer_jid) | 699 session_manager, peer_jid |
700 | 700 ) |
701 trust_messages.add(PartialTrustMessage( | 701 |
702 recipient_jid=own_jid, | 702 trust_messages.add( |
703 updated_jid=peer_jid, | 703 PartialTrustMessage( |
704 trust_updates=peer_trust_updates | 704 recipient_jid=own_jid, |
705 )) | 705 updated_jid=peer_jid, |
706 trust_updates=peer_trust_updates, | |
707 ) | |
708 ) | |
706 | 709 |
707 # Send information about our own devices to our newly trusted devices | 710 # Send information about our own devices to our newly trusted devices |
708 trust_messages.add(PartialTrustMessage( | 711 trust_messages.add( |
709 recipient_jid=own_jid, | 712 PartialTrustMessage( |
710 updated_jid=own_jid, | 713 recipient_jid=own_jid, |
711 trust_updates=own_trust_updates | 714 updated_jid=own_jid, |
712 )) | 715 trust_updates=own_trust_updates, |
716 ) | |
717 ) | |
713 else: | 718 else: |
714 # Notify our other devices about the changes in trust | 719 # Notify our other devices about the changes in trust |
715 trust_messages.add(PartialTrustMessage( | 720 trust_messages.add( |
716 recipient_jid=own_jid, | 721 PartialTrustMessage( |
717 updated_jid=updated_jid, | 722 recipient_jid=own_jid, |
718 trust_updates=trust_updates | 723 updated_jid=updated_jid, |
719 )) | 724 trust_updates=trust_updates, |
725 ) | |
726 ) | |
720 | 727 |
721 # Send a summary of our own trust to newly trusted devices | 728 # Send a summary of our own trust to newly trusted devices |
722 trust_messages.add(PartialTrustMessage( | 729 trust_messages.add( |
723 recipient_jid=updated_jid, | 730 PartialTrustMessage( |
724 updated_jid=own_jid, | 731 recipient_jid=updated_jid, |
725 trust_updates=own_trust_updates | 732 updated_jid=own_jid, |
726 )) | 733 trust_updates=own_trust_updates, |
734 ) | |
735 ) | |
727 | 736 |
728 # All trust messages prepared. Merge all trust messages directed at the same | 737 # All trust messages prepared. Merge all trust messages directed at the same |
729 # recipient. | 738 # recipient. |
730 recipient_jids = { trust_message.recipient_jid for trust_message in trust_messages } | 739 recipient_jids = {trust_message.recipient_jid for trust_message in trust_messages} |
731 | 740 |
732 for recipient_jid in recipient_jids: | 741 for recipient_jid in recipient_jids: |
733 updated: Dict[jid.JID, Set[TrustUpdate]] = {} | 742 updated: Dict[jid.JID, Set[TrustUpdate]] = {} |
734 | 743 |
735 for trust_message in trust_messages: | 744 for trust_message in trust_messages: |
736 # Merge trust messages directed at that recipient | 745 # Merge trust messages directed at that recipient |
737 if trust_message.recipient_jid == recipient_jid: | 746 if trust_message.recipient_jid == recipient_jid: |
738 # Merge the trust updates | 747 # Merge the trust updates |
739 updated[trust_message.updated_jid] = \ | 748 updated[trust_message.updated_jid] = updated.get( |
740 updated.get(trust_message.updated_jid, set()) | 749 trust_message.updated_jid, set() |
750 ) | |
741 | 751 |
742 updated[trust_message.updated_jid] |= trust_message.trust_updates | 752 updated[trust_message.updated_jid] |= trust_message.trust_updates |
743 | 753 |
744 # Build the trust message | 754 # Build the trust message |
745 trust_message_elt = domish.Element((NS_TM, "trust-message")) | 755 trust_message_elt = domish.Element((NS_TM, "trust-message")) |
749 for updated_jid, trust_updates in updated.items(): | 759 for updated_jid, trust_updates in updated.items(): |
750 key_owner_elt = trust_message_elt.addElement((NS_TM, "key-owner")) | 760 key_owner_elt = trust_message_elt.addElement((NS_TM, "key-owner")) |
751 key_owner_elt["jid"] = updated_jid.userhost() | 761 key_owner_elt["jid"] = updated_jid.userhost() |
752 | 762 |
753 for trust_update in trust_updates: | 763 for trust_update in trust_updates: |
754 serialized_identity_key = \ | 764 serialized_identity_key = base64.b64encode( |
755 base64.b64encode(trust_update.target_key).decode("ASCII") | 765 trust_update.target_key |
766 ).decode("ASCII") | |
756 | 767 |
757 if trust_update.target_trust: | 768 if trust_update.target_trust: |
758 key_owner_elt.addElement( | 769 key_owner_elt.addElement( |
759 (NS_TM, "trust"), | 770 (NS_TM, "trust"), content=serialized_identity_key |
760 content=serialized_identity_key | |
761 ) | 771 ) |
762 else: | 772 else: |
763 key_owner_elt.addElement( | 773 key_owner_elt.addElement( |
764 (NS_TM, "distrust"), | 774 (NS_TM, "distrust"), content=serialized_identity_key |
765 content=serialized_identity_key | |
766 ) | 775 ) |
767 | 776 |
768 # Finally, encrypt and send the trust message! | 777 # Finally, encrypt and send the trust message! |
769 message_data = client.generate_message_xml(MessageData({ | 778 message_data = client.generate_message_xml( |
770 "from": own_jid, | 779 MessageData( |
771 "to": recipient_jid, | 780 { |
772 "uid": str(uuid.uuid4()), | 781 "from": own_jid, |
773 "message": {}, | 782 "to": recipient_jid, |
774 "subject": {}, | 783 "uid": str(uuid.uuid4()), |
775 "type": C.MESS_TYPE_CHAT, | 784 "message": {}, |
776 "extra": {}, | 785 "subject": {}, |
777 "timestamp": time.time() | 786 "type": C.MESS_TYPE_CHAT, |
778 })) | 787 "extra": {}, |
788 "timestamp": time.time(), | |
789 } | |
790 ) | |
791 ) | |
779 | 792 |
780 message_data["xml"].addChild(trust_message_elt) | 793 message_data["xml"].addChild(trust_message_elt) |
781 | 794 |
782 plaintext = XEP_0420.pack_stanza(TM_SCE_PROFILE, message_data["xml"]) | 795 plaintext = XEP_0420.pack_stanza(TM_SCE_PROFILE, message_data["xml"]) |
783 | 796 |
784 feedback_jid = recipient_jid | 797 feedback_jid = recipient_jid |
785 | 798 |
786 # TODO: The following is mostly duplicate code | 799 # TODO: The following is mostly duplicate code |
787 try: | 800 try: |
788 messages, encryption_errors = await session_manager.encrypt( | 801 messages, encryption_errors = await session_manager.encrypt( |
789 frozenset({ own_jid.userhost(), recipient_jid.userhost() }), | 802 frozenset({own_jid.userhost(), recipient_jid.userhost()}), |
790 { OMEMO.NS_TWOMEMO: plaintext }, | 803 {OMEMO.NS_TWOMEMO: plaintext}, |
791 backend_priority_order=[ OMEMO.NS_TWOMEMO ], | 804 backend_priority_order=[OMEMO.NS_TWOMEMO], |
792 identifier=feedback_jid.userhost() | 805 identifier=feedback_jid.userhost(), |
793 ) | 806 ) |
794 except Exception as e: | 807 except Exception as e: |
795 msg = _( | 808 msg = _( |
796 # pylint: disable=consider-using-f-string | 809 # pylint: disable=consider-using-f-string |
797 "Can't encrypt message for {entities}: {reason}".format( | 810 "Can't encrypt message for {entities}: {reason}".format( |
798 entities=', '.join({ own_jid.userhost(), recipient_jid.userhost() }), | 811 entities=", ".join({own_jid.userhost(), recipient_jid.userhost()}), |
799 reason=e | 812 reason=e, |
800 ) | 813 ) |
801 ) | 814 ) |
802 log.warning(msg) | 815 log.warning(msg) |
803 client.feedback(feedback_jid, msg, { | 816 client.feedback(feedback_jid, msg, {C.MESS_EXTRA_INFO: C.EXTRA_INFO_ENCR_ERR}) |
804 C.MESS_EXTRA_INFO: C.EXTRA_INFO_ENCR_ERR | |
805 }) | |
806 raise e | 817 raise e |
807 | 818 |
808 if len(encryption_errors) > 0: | 819 if len(encryption_errors) > 0: |
809 log.warning( | 820 log.warning( |
810 f"Ignored the following non-critical encryption errors:" | 821 f"Ignored the following non-critical encryption errors:" |
811 f" {encryption_errors}" | 822 f" {encryption_errors}" |
812 ) | 823 ) |
813 | 824 |
814 encrypted_errors_stringified = ", ".join([ | 825 encrypted_errors_stringified = ", ".join( |
815 f"device {err.device_id} of {err.bare_jid} under namespace" | 826 [ |
816 f" {err.namespace}" | 827 f"device {err.device_id} of {err.bare_jid} under namespace" |
817 for err | 828 f" {err.namespace}" |
818 in encryption_errors | 829 for err in encryption_errors |
819 ]) | 830 ] |
831 ) | |
820 | 832 |
821 client.feedback( | 833 client.feedback( |
822 feedback_jid, | 834 feedback_jid, |
823 D_( | 835 D_( |
824 "There were non-critical errors during encryption resulting in some" | 836 "There were non-critical errors during encryption resulting in some" |
825 " of your destinees' devices potentially not receiving the message." | 837 " of your destinees' devices potentially not receiving the message." |
826 " This happens when the encryption data/key material of a device is" | 838 " This happens when the encryption data/key material of a device is" |
827 " incomplete or broken, which shouldn't happen for actively used" | 839 " incomplete or broken, which shouldn't happen for actively used" |
828 " devices, and can usually be ignored. The following devices are" | 840 " devices, and can usually be ignored. The following devices are" |
829 f" affected: {encrypted_errors_stringified}." | 841 f" affected: {encrypted_errors_stringified}." |
830 ) | 842 ), |
831 ) | 843 ) |
832 | 844 |
833 message = next( | 845 message = next( |
834 message for message in messages | 846 message for message in messages if message.namespace == OMEMO.NS_TWOMEMO |
835 if message.namespace == OMEMO.NS_TWOMEMO | |
836 ) | 847 ) |
837 | 848 |
838 # Add the encrypted element | 849 # Add the encrypted element |
839 message_data["xml"].addChild(xml_tools.et_elt_2_domish_elt( | 850 message_data["xml"].addChild( |
840 twomemo.etree.serialize_message(message) | 851 xml_tools.et_elt_2_domish_elt(twomemo.etree.serialize_message(message)) |
841 )) | 852 ) |
842 | 853 |
843 await client.a_send(message_data["xml"]) | 854 await client.a_send(message_data["xml"]) |
844 | 855 |
845 | 856 |
846 def make_session_manager(sat: LiberviaBackend, profile: str) -> Type[omemo.SessionManager]: | 857 def make_session_manager( |
858 sat: LiberviaBackend, profile: str | |
859 ) -> Type[omemo.SessionManager]: | |
847 """ | 860 """ |
848 @param sat: The SAT instance. | 861 @param sat: The SAT instance. |
849 @param profile: The profile. | 862 @param profile: The profile. |
850 @return: A non-abstract subclass of :class:`~omemo.session_manager.SessionManager` | 863 @return: A non-abstract subclass of :class:`~omemo.session_manager.SessionManager` |
851 with XMPP interactions and trust handled via the SAT instance. | 864 with XMPP interactions and trust handled via the SAT instance. |
874 xml_tools.et_elt_2_domish_elt(element), | 887 xml_tools.et_elt_2_domish_elt(element), |
875 item_id=str(bundle.device_id), | 888 item_id=str(bundle.device_id), |
876 extra={ | 889 extra={ |
877 XEP_0060.EXTRA_PUBLISH_OPTIONS: { | 890 XEP_0060.EXTRA_PUBLISH_OPTIONS: { |
878 XEP_0060.OPT_ACCESS_MODEL: "open", | 891 XEP_0060.OPT_ACCESS_MODEL: "open", |
879 XEP_0060.OPT_MAX_ITEMS: "max" | 892 XEP_0060.OPT_MAX_ITEMS: "max", |
880 }, | 893 }, |
881 XEP_0060.EXTRA_ON_PRECOND_NOT_MET: "raise" | 894 XEP_0060.EXTRA_ON_PRECOND_NOT_MET: "raise", |
882 } | 895 }, |
883 ) | 896 ) |
884 except (error.StanzaError, Exception) as e: | 897 except (error.StanzaError, Exception) as e: |
885 if ( | 898 if ( |
886 isinstance(e, error.StanzaError) | 899 isinstance(e, error.StanzaError) |
887 and e.condition == "conflict" | 900 and e.condition == "conflict" |
915 xml_tools.et_elt_2_domish_elt(element), | 928 xml_tools.et_elt_2_domish_elt(element), |
916 item_id=xep_0060.ID_SINGLETON, | 929 item_id=xep_0060.ID_SINGLETON, |
917 extra={ | 930 extra={ |
918 XEP_0060.EXTRA_PUBLISH_OPTIONS: { | 931 XEP_0060.EXTRA_PUBLISH_OPTIONS: { |
919 XEP_0060.OPT_ACCESS_MODEL: "open", | 932 XEP_0060.OPT_ACCESS_MODEL: "open", |
920 XEP_0060.OPT_MAX_ITEMS: 1 | 933 XEP_0060.OPT_MAX_ITEMS: 1, |
921 }, | 934 }, |
922 XEP_0060.EXTRA_ON_PRECOND_NOT_MET: "publish_without_options" | 935 XEP_0060.EXTRA_ON_PRECOND_NOT_MET: "publish_without_options", |
923 } | 936 }, |
924 ) | 937 ) |
925 except Exception as e: | 938 except Exception as e: |
926 raise omemo.BundleUploadFailed( | 939 raise omemo.BundleUploadFailed( |
927 f"Bundle upload failed: {bundle}" | 940 f"Bundle upload failed: {bundle}" |
928 ) from e | 941 ) from e |
931 | 944 |
932 raise omemo.UnknownNamespace(f"Unknown namespace: {bundle.namespace}") | 945 raise omemo.UnknownNamespace(f"Unknown namespace: {bundle.namespace}") |
933 | 946 |
934 @staticmethod | 947 @staticmethod |
935 async def _download_bundle( | 948 async def _download_bundle( |
936 namespace: str, | 949 namespace: str, bare_jid: str, device_id: int |
937 bare_jid: str, | |
938 device_id: int | |
939 ) -> omemo.Bundle: | 950 ) -> omemo.Bundle: |
940 if namespace == twomemo.twomemo.NAMESPACE: | 951 if namespace == twomemo.twomemo.NAMESPACE: |
941 node = "urn:xmpp:omemo:2:bundles" | 952 node = "urn:xmpp:omemo:2:bundles" |
942 | 953 |
943 try: | 954 try: |
944 items, __ = await xep_0060.get_items( | 955 items, __ = await xep_0060.get_items( |
945 client, | 956 client, jid.JID(bare_jid), node, item_ids=[str(device_id)] |
946 jid.JID(bare_jid), | |
947 node, | |
948 item_ids=[ str(device_id) ] | |
949 ) | 957 ) |
950 except Exception as e: | 958 except Exception as e: |
951 raise omemo.BundleDownloadFailed( | 959 raise omemo.BundleDownloadFailed( |
952 f"Bundle download failed for {bare_jid}: {device_id} under" | 960 f"Bundle download failed for {bare_jid}: {device_id} under" |
953 f" namespace {namespace}" | 961 f" namespace {namespace}" |
960 f" {len(items)}." | 968 f" {len(items)}." |
961 ) | 969 ) |
962 | 970 |
963 element = next( | 971 element = next( |
964 iter(xml_tools.domish_elt_2_et_elt(cast(domish.Element, items[0]))), | 972 iter(xml_tools.domish_elt_2_et_elt(cast(domish.Element, items[0]))), |
965 None | 973 None, |
966 ) | 974 ) |
967 if element is None: | 975 if element is None: |
968 raise omemo.BundleDownloadFailed( | 976 raise omemo.BundleDownloadFailed( |
969 f"Bundle download failed for {bare_jid}: {device_id} under" | 977 f"Bundle download failed for {bare_jid}: {device_id} under" |
970 f" namespace {namespace}: Item download succeeded but parsing" | 978 f" namespace {namespace}: Item download succeeded but parsing" |
979 f" namespace {namespace}" | 987 f" namespace {namespace}" |
980 ) from e | 988 ) from e |
981 | 989 |
982 if namespace == oldmemo.oldmemo.NAMESPACE: | 990 if namespace == oldmemo.oldmemo.NAMESPACE: |
983 return await download_oldmemo_bundle( | 991 return await download_oldmemo_bundle( |
984 client, | 992 client, xep_0060, bare_jid, device_id |
985 xep_0060, | |
986 bare_jid, | |
987 device_id | |
988 ) | 993 ) |
989 | 994 |
990 raise omemo.UnknownNamespace(f"Unknown namespace: {namespace}") | 995 raise omemo.UnknownNamespace(f"Unknown namespace: {namespace}") |
991 | 996 |
992 @staticmethod | 997 @staticmethod |
997 try: | 1002 try: |
998 await xep_0060.retract_items( | 1003 await xep_0060.retract_items( |
999 client, | 1004 client, |
1000 client.jid.userhostJID(), | 1005 client.jid.userhostJID(), |
1001 node, | 1006 node, |
1002 [ str(device_id) ], | 1007 [str(device_id)], |
1003 notify=False | 1008 notify=False, |
1004 ) | 1009 ) |
1005 except Exception as e: | 1010 except Exception as e: |
1006 raise omemo.BundleDeletionFailed( | 1011 raise omemo.BundleDeletionFailed( |
1007 f"Bundle deletion failed for {device_id} under namespace" | 1012 f"Bundle deletion failed for {device_id} under namespace" |
1008 f" {namespace}" | 1013 f" {namespace}" |
1025 | 1030 |
1026 raise omemo.UnknownNamespace(f"Unknown namespace: {namespace}") | 1031 raise omemo.UnknownNamespace(f"Unknown namespace: {namespace}") |
1027 | 1032 |
1028 @staticmethod | 1033 @staticmethod |
1029 async def _upload_device_list( | 1034 async def _upload_device_list( |
1030 namespace: str, | 1035 namespace: str, device_list: Dict[int, Optional[str]] |
1031 device_list: Dict[int, Optional[str]] | |
1032 ) -> None: | 1036 ) -> None: |
1033 element: Optional[ET.Element] = None | 1037 element: Optional[ET.Element] = None |
1034 node: Optional[str] = None | 1038 node: Optional[str] = None |
1035 | 1039 |
1036 if namespace == twomemo.twomemo.NAMESPACE: | 1040 if namespace == twomemo.twomemo.NAMESPACE: |
1051 xml_tools.et_elt_2_domish_elt(element), | 1055 xml_tools.et_elt_2_domish_elt(element), |
1052 item_id=xep_0060.ID_SINGLETON, | 1056 item_id=xep_0060.ID_SINGLETON, |
1053 extra={ | 1057 extra={ |
1054 XEP_0060.EXTRA_PUBLISH_OPTIONS: { | 1058 XEP_0060.EXTRA_PUBLISH_OPTIONS: { |
1055 XEP_0060.OPT_MAX_ITEMS: 1, | 1059 XEP_0060.OPT_MAX_ITEMS: 1, |
1056 XEP_0060.OPT_ACCESS_MODEL: "open" | 1060 XEP_0060.OPT_ACCESS_MODEL: "open", |
1057 }, | 1061 }, |
1058 XEP_0060.EXTRA_ON_PRECOND_NOT_MET: "raise" | 1062 XEP_0060.EXTRA_ON_PRECOND_NOT_MET: "raise", |
1059 } | 1063 }, |
1060 ) | 1064 ) |
1061 except (error.StanzaError, Exception) as e: | 1065 except (error.StanzaError, Exception) as e: |
1062 if ( | 1066 if ( |
1063 isinstance(e, error.StanzaError) | 1067 isinstance(e, error.StanzaError) |
1064 and e.condition == "conflict" | 1068 and e.condition == "conflict" |
1078 f"Device list upload failed for namespace {namespace}" | 1082 f"Device list upload failed for namespace {namespace}" |
1079 ) from e | 1083 ) from e |
1080 | 1084 |
1081 @staticmethod | 1085 @staticmethod |
1082 async def _download_device_list( | 1086 async def _download_device_list( |
1083 namespace: str, | 1087 namespace: str, bare_jid: str |
1084 bare_jid: str | |
1085 ) -> Dict[int, Optional[str]]: | 1088 ) -> Dict[int, Optional[str]]: |
1086 node: Optional[str] = None | 1089 node: Optional[str] = None |
1087 | 1090 |
1088 if namespace == twomemo.twomemo.NAMESPACE: | 1091 if namespace == twomemo.twomemo.NAMESPACE: |
1089 node = TWOMEMO_DEVICE_LIST_NODE | 1092 node = TWOMEMO_DEVICE_LIST_NODE |
1111 f"Device list download failed for {bare_jid} under namespace" | 1114 f"Device list download failed for {bare_jid} under namespace" |
1112 f" {namespace}: Unexpected number of items retrieved: {len(items)}." | 1115 f" {namespace}: Unexpected number of items retrieved: {len(items)}." |
1113 ) | 1116 ) |
1114 | 1117 |
1115 element = next( | 1118 element = next( |
1116 iter(xml_tools.domish_elt_2_et_elt(cast(domish.Element, items[0]))), | 1119 iter(xml_tools.domish_elt_2_et_elt(cast(domish.Element, items[0]))), None |
1117 None | |
1118 ) | 1120 ) |
1119 | 1121 |
1120 if element is None: | 1122 if element is None: |
1121 raise omemo.DeviceListDownloadFailed( | 1123 raise omemo.DeviceListDownloadFailed( |
1122 f"Device list download failed for {bare_jid} under namespace" | 1124 f"Device list download failed for {bare_jid} under namespace" |
1136 ) from e | 1138 ) from e |
1137 | 1139 |
1138 raise omemo.UnknownNamespace(f"Unknown namespace: {namespace}") | 1140 raise omemo.UnknownNamespace(f"Unknown namespace: {namespace}") |
1139 | 1141 |
1140 async def _evaluate_custom_trust_level( | 1142 async def _evaluate_custom_trust_level( |
1141 self, | 1143 self, device: omemo.DeviceInformation |
1142 device: omemo.DeviceInformation | |
1143 ) -> omemo.TrustLevel: | 1144 ) -> omemo.TrustLevel: |
1144 # Get the custom trust level | 1145 # Get the custom trust level |
1145 try: | 1146 try: |
1146 trust_level = TrustLevel(device.trust_level_name) | 1147 trust_level = TrustLevel(device.trust_level_name) |
1147 except ValueError as e: | 1148 except ValueError as e: |
1159 | 1160 |
1160 # The blindly trusted case is more complicated, since its evaluation depends | 1161 # The blindly trusted case is more complicated, since its evaluation depends |
1161 # on the trust system and phase | 1162 # on the trust system and phase |
1162 if trust_level is TrustLevel.BLINDLY_TRUSTED: | 1163 if trust_level is TrustLevel.BLINDLY_TRUSTED: |
1163 # Get the name of the active trust system | 1164 # Get the name of the active trust system |
1164 trust_system = cast(str, sat.memory.param_get_a( | 1165 trust_system = cast( |
1165 PARAM_NAME, | 1166 str, |
1166 PARAM_CATEGORY, | 1167 sat.memory.param_get_a( |
1167 profile_key=profile | 1168 PARAM_NAME, PARAM_CATEGORY, profile_key=profile |
1168 )) | 1169 ), |
1170 ) | |
1169 | 1171 |
1170 # If the trust model is BTBV, blind trust is always enabled | 1172 # If the trust model is BTBV, blind trust is always enabled |
1171 if trust_system == "btbv": | 1173 if trust_system == "btbv": |
1172 return omemo.TrustLevel.TRUSTED | 1174 return omemo.TrustLevel.TRUSTED |
1173 | 1175 |
1175 # and counts as undecided | 1177 # and counts as undecided |
1176 if trust_system == "atm": | 1178 if trust_system == "atm": |
1177 # Find out whether we are in phase one or two | 1179 # Find out whether we are in phase one or two |
1178 devices = await self.get_device_information(device.bare_jid) | 1180 devices = await self.get_device_information(device.bare_jid) |
1179 | 1181 |
1180 phase_one = all(TrustLevel(device.trust_level_name) in { | 1182 phase_one = all( |
1181 TrustLevel.UNDECIDED, | 1183 TrustLevel(device.trust_level_name) |
1182 TrustLevel.BLINDLY_TRUSTED | 1184 in {TrustLevel.UNDECIDED, TrustLevel.BLINDLY_TRUSTED} |
1183 } for device in devices) | 1185 for device in devices |
1186 ) | |
1184 | 1187 |
1185 if phase_one: | 1188 if phase_one: |
1186 return omemo.TrustLevel.TRUSTED | 1189 return omemo.TrustLevel.TRUSTED |
1187 | 1190 |
1188 return omemo.TrustLevel.UNDECIDED | 1191 return omemo.TrustLevel.UNDECIDED |
1192 ) | 1195 ) |
1193 | 1196 |
1194 assert_never(trust_level) | 1197 assert_never(trust_level) |
1195 | 1198 |
1196 async def _make_trust_decision( | 1199 async def _make_trust_decision( |
1197 self, | 1200 self, undecided: FrozenSet[omemo.DeviceInformation], identifier: Optional[str] |
1198 undecided: FrozenSet[omemo.DeviceInformation], | |
1199 identifier: Optional[str] | |
1200 ) -> None: | 1201 ) -> None: |
1201 if identifier is None: | 1202 if identifier is None: |
1202 raise omemo.TrustDecisionFailed( | 1203 raise omemo.TrustDecisionFailed( |
1203 "The identifier must contain the feedback JID." | 1204 "The identifier must contain the feedback JID." |
1204 ) | 1205 ) |
1208 | 1209 |
1209 # Both the ATM and the BTBV trust models work with blind trust before the | 1210 # Both the ATM and the BTBV trust models work with blind trust before the |
1210 # first manual verification is performed. Thus, we can separate bare JIDs into | 1211 # first manual verification is performed. Thus, we can separate bare JIDs into |
1211 # two pools here, one pool of bare JIDs for which blind trust is active, and | 1212 # two pools here, one pool of bare JIDs for which blind trust is active, and |
1212 # one pool of bare JIDs for which manual trust is used instead. | 1213 # one pool of bare JIDs for which manual trust is used instead. |
1213 bare_jids = { device.bare_jid for device in undecided } | 1214 bare_jids = {device.bare_jid for device in undecided} |
1214 | 1215 |
1215 blind_trust_bare_jids: Set[str] = set() | 1216 blind_trust_bare_jids: Set[str] = set() |
1216 manual_trust_bare_jids: Set[str] = set() | 1217 manual_trust_bare_jids: Set[str] = set() |
1217 | 1218 |
1218 # For each bare JID, decide whether blind trust applies | 1219 # For each bare JID, decide whether blind trust applies |
1220 # Get all known devices belonging to the bare JID | 1221 # Get all known devices belonging to the bare JID |
1221 devices = await self.get_device_information(bare_jid) | 1222 devices = await self.get_device_information(bare_jid) |
1222 | 1223 |
1223 # If the trust levels of all devices correspond to those used by blind | 1224 # If the trust levels of all devices correspond to those used by blind |
1224 # trust, blind trust applies. Otherwise, fall back to manual trust. | 1225 # trust, blind trust applies. Otherwise, fall back to manual trust. |
1225 if all(TrustLevel(device.trust_level_name) in { | 1226 if all( |
1226 TrustLevel.UNDECIDED, | 1227 TrustLevel(device.trust_level_name) |
1227 TrustLevel.BLINDLY_TRUSTED | 1228 in {TrustLevel.UNDECIDED, TrustLevel.BLINDLY_TRUSTED} |
1228 } for device in devices): | 1229 for device in devices |
1230 ): | |
1229 blind_trust_bare_jids.add(bare_jid) | 1231 blind_trust_bare_jids.add(bare_jid) |
1230 else: | 1232 else: |
1231 manual_trust_bare_jids.add(bare_jid) | 1233 manual_trust_bare_jids.add(bare_jid) |
1232 | 1234 |
1233 # With the JIDs sorted into their respective pools, the undecided devices can | 1235 # With the JIDs sorted into their respective pools, the undecided devices can |
1234 # be categorized too | 1236 # be categorized too |
1235 blindly_trusted_devices = \ | 1237 blindly_trusted_devices = { |
1236 { dev for dev in undecided if dev.bare_jid in blind_trust_bare_jids } | 1238 dev for dev in undecided if dev.bare_jid in blind_trust_bare_jids |
1237 manually_trusted_devices = \ | 1239 } |
1238 { dev for dev in undecided if dev.bare_jid in manual_trust_bare_jids } | 1240 manually_trusted_devices = { |
1241 dev for dev in undecided if dev.bare_jid in manual_trust_bare_jids | |
1242 } | |
1239 | 1243 |
1240 # Blindly trust devices handled by blind trust | 1244 # Blindly trust devices handled by blind trust |
1241 if len(blindly_trusted_devices) > 0: | 1245 if len(blindly_trusted_devices) > 0: |
1242 for device in blindly_trusted_devices: | 1246 for device in blindly_trusted_devices: |
1243 await self.set_trust( | 1247 await self.set_trust( |
1244 device.bare_jid, | 1248 device.bare_jid, |
1245 device.identity_key, | 1249 device.identity_key, |
1246 TrustLevel.BLINDLY_TRUSTED.name | 1250 TrustLevel.BLINDLY_TRUSTED.name, |
1247 ) | 1251 ) |
1248 | 1252 |
1249 blindly_trusted_devices_stringified = ", ".join([ | 1253 blindly_trusted_devices_stringified = ", ".join( |
1250 f"device {device.device_id} of {device.bare_jid} under namespace" | 1254 [ |
1251 f" {device.namespaces}" | 1255 f"device {device.device_id} of {device.bare_jid} under namespace" |
1252 for device | 1256 f" {device.namespaces}" |
1253 in blindly_trusted_devices | 1257 for device in blindly_trusted_devices |
1254 ]) | 1258 ] |
1259 ) | |
1255 | 1260 |
1256 client.feedback( | 1261 client.feedback( |
1257 feedback_jid, | 1262 feedback_jid, |
1258 D_( | 1263 D_( |
1259 "Not all destination devices are trusted, unknown devices will be" | 1264 "Not all destination devices are trusted, unknown devices will be" |
1260 " blindly trusted.\nFollowing devices have been automatically" | 1265 " blindly trusted.\nFollowing devices have been automatically" |
1261 f" trusted: {blindly_trusted_devices_stringified}." | 1266 f" trusted: {blindly_trusted_devices_stringified}." |
1262 ) | 1267 ), |
1263 ) | 1268 ) |
1264 | 1269 |
1265 # Prompt the user for manual trust decisions on the devices handled by manual | 1270 # Prompt the user for manual trust decisions on the devices handled by manual |
1266 # trust | 1271 # trust |
1267 if len(manually_trusted_devices) > 0: | 1272 if len(manually_trusted_devices) > 0: |
1270 D_( | 1275 D_( |
1271 "Not all destination devices are trusted, we can't encrypt" | 1276 "Not all destination devices are trusted, we can't encrypt" |
1272 " message in such a situation. Please indicate if you trust" | 1277 " message in such a situation. Please indicate if you trust" |
1273 " those devices or not in the trust manager before we can" | 1278 " those devices or not in the trust manager before we can" |
1274 " send this message." | 1279 " send this message." |
1275 ) | 1280 ), |
1276 ) | 1281 ) |
1277 await self.__prompt_manual_trust( | 1282 await self.__prompt_manual_trust( |
1278 frozenset(manually_trusted_devices), | 1283 frozenset(manually_trusted_devices), feedback_jid |
1279 feedback_jid | |
1280 ) | 1284 ) |
1281 | 1285 |
1282 @staticmethod | 1286 @staticmethod |
1283 async def _send_message(message: omemo.Message, bare_jid: str) -> None: | 1287 async def _send_message(message: omemo.Message, bare_jid: str) -> None: |
1284 element: Optional[ET.Element] = None | 1288 element: Optional[ET.Element] = None |
1289 element = oldmemo.etree.serialize_message(message) | 1293 element = oldmemo.etree.serialize_message(message) |
1290 | 1294 |
1291 if element is None: | 1295 if element is None: |
1292 raise omemo.UnknownNamespace(f"Unknown namespace: {message.namespace}") | 1296 raise omemo.UnknownNamespace(f"Unknown namespace: {message.namespace}") |
1293 | 1297 |
1294 message_data = client.generate_message_xml(MessageData({ | 1298 message_data = client.generate_message_xml( |
1295 "from": client.jid, | 1299 MessageData( |
1296 "to": jid.JID(bare_jid), | 1300 { |
1297 "uid": str(uuid.uuid4()), | 1301 "from": client.jid, |
1298 "message": {}, | 1302 "to": jid.JID(bare_jid), |
1299 "subject": {}, | 1303 "uid": str(uuid.uuid4()), |
1300 "type": C.MESS_TYPE_CHAT, | 1304 "message": {}, |
1301 "extra": {}, | 1305 "subject": {}, |
1302 "timestamp": time.time() | 1306 "type": C.MESS_TYPE_CHAT, |
1303 })) | 1307 "extra": {}, |
1308 "timestamp": time.time(), | |
1309 } | |
1310 ) | |
1311 ) | |
1304 | 1312 |
1305 message_data["xml"].addChild(xml_tools.et_elt_2_domish_elt(element)) | 1313 message_data["xml"].addChild(xml_tools.et_elt_2_domish_elt(element)) |
1306 | 1314 |
1307 try: | 1315 try: |
1308 await client.a_send(message_data["xml"]) | 1316 await client.a_send(message_data["xml"]) |
1309 except Exception as e: | 1317 except Exception as e: |
1310 raise omemo.MessageSendingFailed() from e | 1318 raise omemo.MessageSendingFailed() from e |
1311 | 1319 |
1312 async def __prompt_manual_trust( | 1320 async def __prompt_manual_trust( |
1313 self, | 1321 self, undecided: FrozenSet[omemo.DeviceInformation], feedback_jid: jid.JID |
1314 undecided: FrozenSet[omemo.DeviceInformation], | |
1315 feedback_jid: jid.JID | |
1316 ) -> None: | 1322 ) -> None: |
1317 """Asks the user to decide on the manual trust level of a set of devices. | 1323 """Asks the user to decide on the manual trust level of a set of devices. |
1318 | 1324 |
1319 Blocks until the user has made a decision and updates the trust levels of all | 1325 Blocks until the user has made a decision and updates the trust levels of all |
1320 devices using :meth:`set_trust`. | 1326 devices using :meth:`set_trust`. |
1338 | 1344 |
1339 namespace = encryption["plugin"].namespace | 1345 namespace = encryption["plugin"].namespace |
1340 | 1346 |
1341 # Casting this to Any, otherwise all calls on the variable cause type errors | 1347 # Casting this to Any, otherwise all calls on the variable cause type errors |
1342 # pylint: disable=no-member | 1348 # pylint: disable=no-member |
1343 trust_ui = cast(Any, xml_tools.XMLUI( | 1349 trust_ui = cast( |
1344 panel_type=C.XMLUI_FORM, | 1350 Any, |
1345 title=D_("OMEMO trust management"), | 1351 xml_tools.XMLUI( |
1346 submit_id="" | 1352 panel_type=C.XMLUI_FORM, |
1347 )) | 1353 title=D_("OMEMO trust management"), |
1348 trust_ui.addText(D_( | 1354 submit_id="", |
1349 "This is OMEMO trusting system. You'll see below the devices of your " | 1355 ), |
1350 "contacts, and a checkbox to trust them or not. A trusted device " | 1356 ) |
1351 "can read your messages in plain text, so be sure to only validate " | 1357 trust_ui.addText( |
1352 "devices that you are sure are belonging to your contact. It's better " | 1358 D_( |
1353 "to do this when you are next to your contact and their device, so " | 1359 "This is OMEMO trusting system. You'll see below the devices of your " |
1354 "you can check the \"fingerprint\" (the number next to the device) " | 1360 "contacts, and a checkbox to trust them or not. A trusted device " |
1355 "yourself. Do *not* validate a device if the fingerprint is wrong!" | 1361 "can read your messages in plain text, so be sure to only validate " |
1356 )) | 1362 "devices that you are sure are belonging to your contact. It's better " |
1363 "to do this when you are next to your contact and their device, so " | |
1364 'you can check the "fingerprint" (the number next to the device) ' | |
1365 "yourself. Do *not* validate a device if the fingerprint is wrong!" | |
1366 ) | |
1367 ) | |
1357 | 1368 |
1358 own_device, __ = await self.get_own_device_information() | 1369 own_device, __ = await self.get_own_device_information() |
1359 | 1370 |
1360 trust_ui.change_container("label") | 1371 trust_ui.change_container("label") |
1361 trust_ui.addLabel(D_("This device ID")) | 1372 trust_ui.addLabel(D_("This device ID")) |
1382 trust_ui.addEmpty() | 1393 trust_ui.addEmpty() |
1383 | 1394 |
1384 trust_ui_result = await xml_tools.defer_xmlui( | 1395 trust_ui_result = await xml_tools.defer_xmlui( |
1385 sat, | 1396 sat, |
1386 trust_ui, | 1397 trust_ui, |
1387 action_extra={ "meta_encryption_trust": namespace }, | 1398 action_extra={"meta_encryption_trust": namespace}, |
1388 profile=profile | 1399 profile=profile, |
1389 ) | 1400 ) |
1390 | 1401 |
1391 if C.bool(trust_ui_result.get("cancelled", "false")): | 1402 if C.bool(trust_ui_result.get("cancelled", "false")): |
1392 raise omemo.TrustDecisionFailed("Trust UI cancelled.") | 1403 raise omemo.TrustDecisionFailed("Trust UI cancelled.") |
1393 | 1404 |
1394 data_form_result = cast(Dict[str, str], xml_tools.xmlui_result_2_data_form_result( | 1405 data_form_result = cast( |
1395 trust_ui_result | 1406 Dict[str, str], xml_tools.xmlui_result_2_data_form_result(trust_ui_result) |
1396 )) | 1407 ) |
1397 | 1408 |
1398 trust_updates: Set[TrustUpdate] = set() | 1409 trust_updates: Set[TrustUpdate] = set() |
1399 | 1410 |
1400 for key, value in data_form_result.items(): | 1411 for key, value in data_form_result.items(): |
1401 if not key.startswith("trust_"): | 1412 if not key.startswith("trust_"): |
1402 continue | 1413 continue |
1403 | 1414 |
1404 device = undecided_ordered[int(key[len("trust_"):])] | 1415 device = undecided_ordered[int(key[len("trust_") :])] |
1405 target_trust = C.bool(value) | 1416 target_trust = C.bool(value) |
1406 trust_level = \ | 1417 trust_level = ( |
1407 TrustLevel.TRUSTED if target_trust else TrustLevel.DISTRUSTED | 1418 TrustLevel.TRUSTED if target_trust else TrustLevel.DISTRUSTED |
1419 ) | |
1408 | 1420 |
1409 await self.set_trust( | 1421 await self.set_trust( |
1410 device.bare_jid, | 1422 device.bare_jid, device.identity_key, trust_level.name |
1411 device.identity_key, | 1423 ) |
1412 trust_level.name | 1424 |
1413 ) | 1425 trust_updates.add( |
1414 | 1426 TrustUpdate( |
1415 trust_updates.add(TrustUpdate( | 1427 target_jid=jid.JID(device.bare_jid).userhostJID(), |
1416 target_jid=jid.JID(device.bare_jid).userhostJID(), | 1428 target_key=device.identity_key, |
1417 target_key=device.identity_key, | 1429 target_trust=target_trust, |
1418 target_trust=target_trust | 1430 ) |
1419 )) | 1431 ) |
1420 | 1432 |
1421 # Check whether ATM is enabled and handle everything in case it is | 1433 # Check whether ATM is enabled and handle everything in case it is |
1422 trust_system = cast(str, sat.memory.param_get_a( | 1434 trust_system = cast( |
1423 PARAM_NAME, | 1435 str, |
1424 PARAM_CATEGORY, | 1436 sat.memory.param_get_a(PARAM_NAME, PARAM_CATEGORY, profile_key=profile), |
1425 profile_key=profile | 1437 ) |
1426 )) | |
1427 | 1438 |
1428 if trust_system == "atm": | 1439 if trust_system == "atm": |
1429 await manage_trust_message_cache(client, self, frozenset(trust_updates)) | 1440 await manage_trust_message_cache(client, self, frozenset(trust_updates)) |
1430 await send_trust_messages(client, self, frozenset(trust_updates)) | 1441 await send_trust_messages(client, self, frozenset(trust_updates)) |
1431 | 1442 |
1437 profile: str, | 1448 profile: str, |
1438 initial_own_label: Optional[str], | 1449 initial_own_label: Optional[str], |
1439 signed_pre_key_rotation_period: int = 7 * 24 * 60 * 60, | 1450 signed_pre_key_rotation_period: int = 7 * 24 * 60 * 60, |
1440 pre_key_refill_threshold: int = 99, | 1451 pre_key_refill_threshold: int = 99, |
1441 max_num_per_session_skipped_keys: int = 1000, | 1452 max_num_per_session_skipped_keys: int = 1000, |
1442 max_num_per_message_skipped_keys: Optional[int] = None | 1453 max_num_per_message_skipped_keys: Optional[int] = None, |
1443 ) -> omemo.SessionManager: | 1454 ) -> omemo.SessionManager: |
1444 """Prepare the OMEMO library (storage, backends, core) for a specific profile. | 1455 """Prepare the OMEMO library (storage, backends, core) for a specific profile. |
1445 | 1456 |
1446 @param sat: The SAT instance. | 1457 @param sat: The SAT instance. |
1447 @param profile: The profile. | 1458 @param profile: The profile. |
1490 # TODO: Do we want BLINDLY_TRUSTED or TRUSTED here? | 1501 # TODO: Do we want BLINDLY_TRUSTED or TRUSTED here? |
1491 TrustLevel.BLINDLY_TRUSTED.name, | 1502 TrustLevel.BLINDLY_TRUSTED.name, |
1492 TrustLevel.UNDECIDED.name, | 1503 TrustLevel.UNDECIDED.name, |
1493 TrustLevel.DISTRUSTED.name, | 1504 TrustLevel.DISTRUSTED.name, |
1494 lambda bare_jid, device_id: download_oldmemo_bundle( | 1505 lambda bare_jid, device_id: download_oldmemo_bundle( |
1495 client, | 1506 client, xep_0060, bare_jid, device_id |
1496 xep_0060, | 1507 ), |
1497 bare_jid, | |
1498 device_id | |
1499 ) | |
1500 ) | 1508 ) |
1501 | 1509 |
1502 session_manager = await make_session_manager(sat, profile).create( | 1510 session_manager = await make_session_manager(sat, profile).create( |
1503 [ | 1511 [ |
1504 twomemo.Twomemo( | 1512 twomemo.Twomemo( |
1505 storage, | 1513 storage, |
1506 max_num_per_session_skipped_keys, | 1514 max_num_per_session_skipped_keys, |
1507 max_num_per_message_skipped_keys | 1515 max_num_per_message_skipped_keys, |
1508 ), | 1516 ), |
1509 oldmemo.Oldmemo( | 1517 oldmemo.Oldmemo( |
1510 storage, | 1518 storage, |
1511 max_num_per_session_skipped_keys, | 1519 max_num_per_session_skipped_keys, |
1512 max_num_per_message_skipped_keys | 1520 max_num_per_message_skipped_keys, |
1513 ) | 1521 ), |
1514 ], | 1522 ], |
1515 storage, | 1523 storage, |
1516 client.jid.userhost(), | 1524 client.jid.userhost(), |
1517 initial_own_label, | 1525 initial_own_label, |
1518 TrustLevel.UNDECIDED.value, | 1526 TrustLevel.UNDECIDED.value, |
1519 signed_pre_key_rotation_period, | 1527 signed_pre_key_rotation_period, |
1520 pre_key_refill_threshold, | 1528 pre_key_refill_threshold, |
1521 omemo.AsyncFramework.TWISTED | 1529 omemo.AsyncFramework.TWISTED, |
1522 ) | 1530 ) |
1523 | 1531 |
1524 # This shouldn't hurt here since we're not running on overly constrainted devices. | 1532 # This shouldn't hurt here since we're not running on overly constrainted devices. |
1525 # TODO: Consider ensuring data consistency regularly/in response to certain events | 1533 # TODO: Consider ensuring data consistency regularly/in response to certain events |
1526 await session_manager.ensure_data_consistency() | 1534 await session_manager.ensure_data_consistency() |
1561 ``urn:xmpp:omemo:2`` namespace and the (legacy) ``eu.siacs.conversations.axolotl`` | 1569 ``urn:xmpp:omemo:2`` namespace and the (legacy) ``eu.siacs.conversations.axolotl`` |
1562 namespace. Both versions of the protocol are handled by this plugin and compatibility | 1570 namespace. Both versions of the protocol are handled by this plugin and compatibility |
1563 between the two is maintained. MUC messages are supported next to one to one messages. | 1571 between the two is maintained. MUC messages are supported next to one to one messages. |
1564 For trust management, the two trust models "ATM" and "BTBV" are supported. | 1572 For trust management, the two trust models "ATM" and "BTBV" are supported. |
1565 """ | 1573 """ |
1574 | |
1566 NS_TWOMEMO = twomemo.twomemo.NAMESPACE | 1575 NS_TWOMEMO = twomemo.twomemo.NAMESPACE |
1567 NS_OLDMEMO = oldmemo.oldmemo.NAMESPACE | 1576 NS_OLDMEMO = oldmemo.oldmemo.NAMESPACE |
1568 | 1577 |
1569 # For MUC/MIX message stanzas, the <to/> affix is a MUST | 1578 # For MUC/MIX message stanzas, the <to/> affix is a MUST |
1570 SCE_PROFILE_GROUPCHAT = SCEProfile( | 1579 SCE_PROFILE_GROUPCHAT = SCEProfile( |
1571 rpad_policy=SCEAffixPolicy.REQUIRED, | 1580 rpad_policy=SCEAffixPolicy.REQUIRED, |
1572 time_policy=SCEAffixPolicy.OPTIONAL, | 1581 time_policy=SCEAffixPolicy.OPTIONAL, |
1573 to_policy=SCEAffixPolicy.REQUIRED, | 1582 to_policy=SCEAffixPolicy.REQUIRED, |
1574 from_policy=SCEAffixPolicy.OPTIONAL, | 1583 from_policy=SCEAffixPolicy.OPTIONAL, |
1575 custom_policies={} | 1584 custom_policies={}, |
1576 ) | 1585 ) |
1577 | 1586 |
1578 # For everything but MUC/MIX message stanzas, the <to/> affix is a MAY | 1587 # For everything but MUC/MIX message stanzas, the <to/> affix is a MAY |
1579 SCE_PROFILE = SCEProfile( | 1588 SCE_PROFILE = SCEProfile( |
1580 rpad_policy=SCEAffixPolicy.REQUIRED, | 1589 rpad_policy=SCEAffixPolicy.REQUIRED, |
1581 time_policy=SCEAffixPolicy.OPTIONAL, | 1590 time_policy=SCEAffixPolicy.OPTIONAL, |
1582 to_policy=SCEAffixPolicy.OPTIONAL, | 1591 to_policy=SCEAffixPolicy.OPTIONAL, |
1583 from_policy=SCEAffixPolicy.OPTIONAL, | 1592 from_policy=SCEAffixPolicy.OPTIONAL, |
1584 custom_policies={} | 1593 custom_policies={}, |
1585 ) | 1594 ) |
1586 | 1595 |
1587 def __init__(self, host: LiberviaBackend) -> None: | 1596 def __init__(self, host: LiberviaBackend) -> None: |
1588 """ | 1597 """ |
1589 @param sat: The SAT instance. | 1598 @param sat: The SAT instance. |
1623 | 1632 |
1624 # These triggers are used by oldmemo, which doesn't do SCE and only applies to | 1633 # These triggers are used by oldmemo, which doesn't do SCE and only applies to |
1625 # messages. Temporarily, until a more fitting trigger for SCE-based encryption is | 1634 # messages. Temporarily, until a more fitting trigger for SCE-based encryption is |
1626 # added, the message_received trigger is also used for twomemo. | 1635 # added, the message_received trigger is also used for twomemo. |
1627 host.trigger.add( | 1636 host.trigger.add( |
1628 "message_received", | 1637 "message_received", self._message_received_trigger, priority=100050 |
1629 self._message_received_trigger, | |
1630 priority=100050 | |
1631 ) | 1638 ) |
1632 | 1639 |
1633 host.trigger.add("send", self.__send_trigger, priority=0) | 1640 host.trigger.add("send", self.__send_trigger, priority=0) |
1634 # TODO: Add new triggers here for freshly received and about-to-be-sent stanzas, | 1641 # TODO: Add new triggers here for freshly received and about-to-be-sent stanzas, |
1635 # including IQs. | 1642 # including IQs. |
1644 xep_0163.add_pep_event( | 1651 xep_0163.add_pep_event( |
1645 "TWOMEMO_DEVICES", | 1652 "TWOMEMO_DEVICES", |
1646 TWOMEMO_DEVICE_LIST_NODE, | 1653 TWOMEMO_DEVICE_LIST_NODE, |
1647 lambda items_event, profile: defer.ensureDeferred( | 1654 lambda items_event, profile: defer.ensureDeferred( |
1648 self.__on_device_list_update(items_event, profile) | 1655 self.__on_device_list_update(items_event, profile) |
1649 ) | 1656 ), |
1650 ) | 1657 ) |
1651 xep_0163.add_pep_event( | 1658 xep_0163.add_pep_event( |
1652 "OLDMEMO_DEVICES", | 1659 "OLDMEMO_DEVICES", |
1653 OLDMEMO_DEVICE_LIST_NODE, | 1660 OLDMEMO_DEVICE_LIST_NODE, |
1654 lambda items_event, profile: defer.ensureDeferred( | 1661 lambda items_event, profile: defer.ensureDeferred( |
1655 self.__on_device_list_update(items_event, profile) | 1662 self.__on_device_list_update(items_event, profile) |
1656 ) | 1663 ), |
1657 ) | 1664 ) |
1658 | 1665 |
1659 try: | 1666 try: |
1660 self.__text_commands = cast(TextCommands, host.plugins[C.TEXT_CMDS]) | 1667 self.__text_commands = cast(TextCommands, host.plugins[C.TEXT_CMDS]) |
1661 except KeyError: | 1668 except KeyError: |
1662 log.info(_("Text commands not available")) | 1669 log.info(_("Text commands not available")) |
1663 else: | 1670 else: |
1664 self.__text_commands.register_text_commands(self) | 1671 self.__text_commands.register_text_commands(self) |
1665 | 1672 |
1666 def profile_connected( # pylint: disable=invalid-name | 1673 def profile_connected( # pylint: disable=invalid-name |
1667 self, | 1674 self, client: SatXMPPClient |
1668 client: SatXMPPClient | |
1669 ) -> None: | 1675 ) -> None: |
1670 """ | 1676 """ |
1671 @param client: The client. | 1677 @param client: The client. |
1672 """ | 1678 """ |
1673 | 1679 |
1674 defer.ensureDeferred(self.get_session_manager( | 1680 defer.ensureDeferred(self.get_session_manager(cast(str, client.profile))) |
1675 cast(str, client.profile) | |
1676 )) | |
1677 | 1681 |
1678 async def cmd_omemo_reset( | 1682 async def cmd_omemo_reset( |
1679 self, | 1683 self, client: SatXMPPClient, mess_data: MessageData |
1680 client: SatXMPPClient, | |
1681 mess_data: MessageData | |
1682 ) -> Literal[False]: | 1684 ) -> Literal[False]: |
1683 """Reset all sessions of devices that belong to the recipient of ``mess_data``. | 1685 """Reset all sessions of devices that belong to the recipient of ``mess_data``. |
1684 | 1686 |
1685 This must only be callable manually by the user. Use this when a session is | 1687 This must only be callable manually by the user. Use this when a session is |
1686 apparently broken, i.e. sending and receiving encrypted messages doesn't work and | 1688 apparently broken, i.e. sending and receiving encrypted messages doesn't work and |
1691 reset all sessions with. | 1693 reset all sessions with. |
1692 @return: The constant value ``False``, indicating to the text commands plugin that | 1694 @return: The constant value ``False``, indicating to the text commands plugin that |
1693 the message is not supposed to be sent. | 1695 the message is not supposed to be sent. |
1694 """ | 1696 """ |
1695 | 1697 |
1696 twomemo_requested = \ | 1698 twomemo_requested = client.encryption.is_encryption_requested( |
1697 client.encryption.is_encryption_requested(mess_data, twomemo.twomemo.NAMESPACE) | 1699 mess_data, twomemo.twomemo.NAMESPACE |
1698 oldmemo_requested = \ | 1700 ) |
1699 client.encryption.is_encryption_requested(mess_data, oldmemo.oldmemo.NAMESPACE) | 1701 oldmemo_requested = client.encryption.is_encryption_requested( |
1702 mess_data, oldmemo.oldmemo.NAMESPACE | |
1703 ) | |
1700 | 1704 |
1701 if not (twomemo_requested or oldmemo_requested): | 1705 if not (twomemo_requested or oldmemo_requested): |
1702 self.__text_commands.feed_back( | 1706 self.__text_commands.feed_back( |
1703 client, | 1707 client, |
1704 _("You need to have OMEMO encryption activated to reset the session"), | 1708 _("You need to have OMEMO encryption activated to reset the session"), |
1705 mess_data | 1709 mess_data, |
1706 ) | 1710 ) |
1707 return False | 1711 return False |
1708 | 1712 |
1709 bare_jid = mess_data["to"].userhost() | 1713 bare_jid = mess_data["to"].userhost() |
1710 | 1714 |
1714 for device in devices: | 1718 for device in devices: |
1715 log.debug(f"Replacing sessions with device {device}") | 1719 log.debug(f"Replacing sessions with device {device}") |
1716 await session_manager.replace_sessions(device) | 1720 await session_manager.replace_sessions(device) |
1717 | 1721 |
1718 self.__text_commands.feed_back( | 1722 self.__text_commands.feed_back( |
1719 client, | 1723 client, _("OMEMO session has been reset"), mess_data |
1720 _("OMEMO session has been reset"), | |
1721 mess_data | |
1722 ) | 1724 ) |
1723 | 1725 |
1724 return False | 1726 return False |
1725 | 1727 |
1726 async def get_trust_ui( # pylint: disable=invalid-name | 1728 async def get_trust_ui( # pylint: disable=invalid-name |
1727 self, | 1729 self, client: SatXMPPClient, entity: jid.JID |
1728 client: SatXMPPClient, | |
1729 entity: jid.JID | |
1730 ) -> xml_tools.XMLUI: | 1730 ) -> xml_tools.XMLUI: |
1731 """ | 1731 """ |
1732 @param client: The client. | 1732 @param client: The client. |
1733 @param entity: The entity whose device trust levels to manage. | 1733 @param entity: The entity whose device trust levels to manage. |
1734 @return: An XMLUI instance which opens a form to manage the trust level of all | 1734 @return: An XMLUI instance which opens a form to manage the trust level of all |
1740 | 1740 |
1741 bare_jids: Set[str] | 1741 bare_jids: Set[str] |
1742 if self.__xep_0045 is not None and self.__xep_0045.is_joined_room(client, entity): | 1742 if self.__xep_0045 is not None and self.__xep_0045.is_joined_room(client, entity): |
1743 bare_jids = self.__get_joined_muc_users(client, self.__xep_0045, entity) | 1743 bare_jids = self.__get_joined_muc_users(client, self.__xep_0045, entity) |
1744 else: | 1744 else: |
1745 bare_jids = { entity.userhost() } | 1745 bare_jids = {entity.userhost()} |
1746 | 1746 |
1747 session_manager = await self.get_session_manager(client.profile) | 1747 session_manager = await self.get_session_manager(client.profile) |
1748 | 1748 |
1749 # At least sort the devices by bare JID such that they aren't listed completely | 1749 # At least sort the devices by bare JID such that they aren't listed completely |
1750 # random | 1750 # random |
1751 devices = sorted(cast(Set[omemo.DeviceInformation], set()).union(*[ | 1751 devices = sorted( |
1752 await session_manager.get_device_information(bare_jid) | 1752 cast(Set[omemo.DeviceInformation], set()).union( |
1753 for bare_jid | 1753 *[ |
1754 in bare_jids | 1754 await session_manager.get_device_information(bare_jid) |
1755 ]), key=lambda device: device.bare_jid) | 1755 for bare_jid in bare_jids |
1756 | 1756 ] |
1757 async def callback( | 1757 ), |
1758 data: Any, | 1758 key=lambda device: device.bare_jid, |
1759 profile: str | 1759 ) |
1760 ) -> Dict[Never, Never]: | 1760 |
1761 async def callback(data: Any, profile: str) -> Dict[Never, Never]: | |
1761 """ | 1762 """ |
1762 @param data: The XMLUI result produces by the trust UI form. | 1763 @param data: The XMLUI result produces by the trust UI form. |
1763 @param profile: The profile. | 1764 @param profile: The profile. |
1764 @return: An empty dictionary. The type of the return value was chosen | 1765 @return: An empty dictionary. The type of the return value was chosen |
1765 conservatively since the exact options are neither known not needed here. | 1766 conservatively since the exact options are neither known not needed here. |
1767 | 1768 |
1768 if C.bool(data.get("cancelled", "false")): | 1769 if C.bool(data.get("cancelled", "false")): |
1769 return {} | 1770 return {} |
1770 | 1771 |
1771 data_form_result = cast( | 1772 data_form_result = cast( |
1772 Dict[str, str], | 1773 Dict[str, str], xml_tools.xmlui_result_2_data_form_result(data) |
1773 xml_tools.xmlui_result_2_data_form_result(data) | |
1774 ) | 1774 ) |
1775 | 1775 |
1776 trust_updates: Set[TrustUpdate] = set() | 1776 trust_updates: Set[TrustUpdate] = set() |
1777 | 1777 |
1778 for key, value in data_form_result.items(): | 1778 for key, value in data_form_result.items(): |
1779 if not key.startswith("trust_"): | 1779 if not key.startswith("trust_"): |
1780 continue | 1780 continue |
1781 | 1781 |
1782 device = devices[int(key[len("trust_"):])] | 1782 device = devices[int(key[len("trust_") :])] |
1783 trust_level_name = value | 1783 trust_level_name = value |
1784 | 1784 |
1785 if device.trust_level_name != trust_level_name: | 1785 if device.trust_level_name != trust_level_name: |
1786 await session_manager.set_trust( | 1786 await session_manager.set_trust( |
1787 device.bare_jid, | 1787 device.bare_jid, device.identity_key, trust_level_name |
1788 device.identity_key, | |
1789 trust_level_name | |
1790 ) | 1788 ) |
1791 | 1789 |
1792 target_trust: Optional[bool] = None | 1790 target_trust: Optional[bool] = None |
1793 | 1791 |
1794 if TrustLevel(trust_level_name) is TrustLevel.TRUSTED: | 1792 if TrustLevel(trust_level_name) is TrustLevel.TRUSTED: |
1795 target_trust = True | 1793 target_trust = True |
1796 if TrustLevel(trust_level_name) is TrustLevel.DISTRUSTED: | 1794 if TrustLevel(trust_level_name) is TrustLevel.DISTRUSTED: |
1797 target_trust = False | 1795 target_trust = False |
1798 | 1796 |
1799 if target_trust is not None: | 1797 if target_trust is not None: |
1800 trust_updates.add(TrustUpdate( | 1798 trust_updates.add( |
1801 target_jid=jid.JID(device.bare_jid).userhostJID(), | 1799 TrustUpdate( |
1802 target_key=device.identity_key, | 1800 target_jid=jid.JID(device.bare_jid).userhostJID(), |
1803 target_trust=target_trust | 1801 target_key=device.identity_key, |
1804 )) | 1802 target_trust=target_trust, |
1803 ) | |
1804 ) | |
1805 | 1805 |
1806 # Check whether ATM is enabled and handle everything in case it is | 1806 # Check whether ATM is enabled and handle everything in case it is |
1807 trust_system = cast(str, self.host.memory.param_get_a( | 1807 trust_system = cast( |
1808 PARAM_NAME, | 1808 str, |
1809 PARAM_CATEGORY, | 1809 self.host.memory.param_get_a( |
1810 profile_key=profile | 1810 PARAM_NAME, PARAM_CATEGORY, profile_key=profile |
1811 )) | 1811 ), |
1812 ) | |
1812 | 1813 |
1813 if trust_system == "atm": | 1814 if trust_system == "atm": |
1814 if len(trust_updates) > 0: | 1815 if len(trust_updates) > 0: |
1815 await manage_trust_message_cache( | 1816 await manage_trust_message_cache( |
1816 client, | 1817 client, session_manager, frozenset(trust_updates) |
1817 session_manager, | |
1818 frozenset(trust_updates) | |
1819 ) | 1818 ) |
1820 | 1819 |
1821 await send_trust_messages( | 1820 await send_trust_messages( |
1822 client, | 1821 client, session_manager, frozenset(trust_updates) |
1823 session_manager, | |
1824 frozenset(trust_updates) | |
1825 ) | 1822 ) |
1826 | 1823 |
1827 return {} | 1824 return {} |
1828 | 1825 |
1829 submit_id = self.host.register_callback(callback, with_data=True, one_shot=True) | 1826 submit_id = self.host.register_callback(callback, with_data=True, one_shot=True) |
1830 | 1827 |
1831 result = xml_tools.XMLUI( | 1828 result = xml_tools.XMLUI( |
1832 panel_type=C.XMLUI_FORM, | 1829 panel_type=C.XMLUI_FORM, |
1833 title=D_("OMEMO trust management"), | 1830 title=D_("OMEMO trust management"), |
1834 submit_id=submit_id | 1831 submit_id=submit_id, |
1835 ) | 1832 ) |
1836 # Casting this to Any, otherwise all calls on the variable cause type errors | 1833 # Casting this to Any, otherwise all calls on the variable cause type errors |
1837 # pylint: disable=no-member | 1834 # pylint: disable=no-member |
1838 trust_ui = cast(Any, result) | 1835 trust_ui = cast(Any, result) |
1839 trust_ui.addText(D_( | 1836 trust_ui.addText( |
1840 "This is OMEMO trusting system. You'll see below the devices of your" | 1837 D_( |
1841 " contacts, and a list selection to trust them or not. A trusted device" | 1838 "This is OMEMO trusting system. You'll see below the devices of your" |
1842 " can read your messages in plain text, so be sure to only validate" | 1839 " contacts, and a list selection to trust them or not. A trusted device" |
1843 " devices that you are sure are belonging to your contact. It's better" | 1840 " can read your messages in plain text, so be sure to only validate" |
1844 " to do this when you are next to your contact and their device, so" | 1841 " devices that you are sure are belonging to your contact. It's better" |
1845 " you can check the \"fingerprint\" (the number next to the device)" | 1842 " to do this when you are next to your contact and their device, so" |
1846 " yourself. Do *not* validate a device if the fingerprint is wrong!" | 1843 ' you can check the "fingerprint" (the number next to the device)' |
1847 " Note that manually validating a fingerprint disables any form of automatic" | 1844 " yourself. Do *not* validate a device if the fingerprint is wrong!" |
1848 " trust." | 1845 " Note that manually validating a fingerprint disables any form of automatic" |
1849 )) | 1846 " trust." |
1847 ) | |
1848 ) | |
1850 | 1849 |
1851 own_device, __ = await session_manager.get_own_device_information() | 1850 own_device, __ = await session_manager.get_own_device_information() |
1852 | 1851 |
1853 trust_ui.change_container("label") | 1852 trust_ui.change_container("label") |
1854 trust_ui.addLabel(D_("This device ID")) | 1853 trust_ui.addLabel(D_("This device ID")) |
1855 trust_ui.addText(str(own_device.device_id)) | 1854 trust_ui.addText(str(own_device.device_id)) |
1856 trust_ui.addLabel(D_("This device's fingerprint")) | 1855 trust_ui.addLabel(D_("This device's fingerprint")) |
1857 trust_ui.addText(" ".join(session_manager.format_identity_key( | 1856 trust_ui.addText( |
1858 own_device.identity_key | 1857 " ".join(session_manager.format_identity_key(own_device.identity_key)) |
1859 ))) | 1858 ) |
1860 trust_ui.addEmpty() | 1859 trust_ui.addEmpty() |
1861 trust_ui.addEmpty() | 1860 trust_ui.addEmpty() |
1862 | 1861 |
1863 for index, device in enumerate(devices): | 1862 for index, device in enumerate(devices): |
1864 trust_ui.addLabel(D_("Contact")) | 1863 trust_ui.addLabel(D_("Contact")) |
1865 trust_ui.addJid(jid.JID(device.bare_jid)) | 1864 trust_ui.addJid(jid.JID(device.bare_jid)) |
1866 trust_ui.addLabel(D_("Device ID")) | 1865 trust_ui.addLabel(D_("Device ID")) |
1867 trust_ui.addText(str(device.device_id)) | 1866 trust_ui.addText(str(device.device_id)) |
1868 trust_ui.addLabel(D_("Fingerprint")) | 1867 trust_ui.addLabel(D_("Fingerprint")) |
1869 trust_ui.addText(" ".join(session_manager.format_identity_key( | 1868 trust_ui.addText( |
1870 device.identity_key | 1869 " ".join(session_manager.format_identity_key(device.identity_key)) |
1871 ))) | 1870 ) |
1872 trust_ui.addLabel(D_("Trust this device?")) | 1871 trust_ui.addLabel(D_("Trust this device?")) |
1873 | 1872 |
1874 current_trust_level = TrustLevel(device.trust_level_name) | 1873 current_trust_level = TrustLevel(device.trust_level_name) |
1875 avaiable_trust_levels = \ | 1874 avaiable_trust_levels = { |
1876 { TrustLevel.DISTRUSTED, TrustLevel.TRUSTED, current_trust_level } | 1875 TrustLevel.DISTRUSTED, |
1876 TrustLevel.TRUSTED, | |
1877 current_trust_level, | |
1878 } | |
1877 | 1879 |
1878 trust_ui.addList( | 1880 trust_ui.addList( |
1879 f"trust_{index}", | 1881 f"trust_{index}", |
1880 options=[ trust_level.name for trust_level in avaiable_trust_levels ], | 1882 options=[trust_level.name for trust_level in avaiable_trust_levels], |
1881 selected=current_trust_level.name, | 1883 selected=current_trust_level.name, |
1882 styles=[ "inline" ] | 1884 styles=["inline"], |
1883 ) | 1885 ) |
1884 | 1886 |
1885 twomemo_active = dict(device.active).get(twomemo.twomemo.NAMESPACE) | 1887 twomemo_active = dict(device.active).get(twomemo.twomemo.NAMESPACE) |
1886 if twomemo_active is None: | 1888 if twomemo_active is None: |
1887 trust_ui.addEmpty() | 1889 trust_ui.addEmpty() |
1903 | 1905 |
1904 return result | 1906 return result |
1905 | 1907 |
1906 @staticmethod | 1908 @staticmethod |
1907 def __get_joined_muc_users( | 1909 def __get_joined_muc_users( |
1908 client: SatXMPPClient, | 1910 client: SatXMPPClient, xep_0045: XEP_0045, room_jid: jid.JID |
1909 xep_0045: XEP_0045, | |
1910 room_jid: jid.JID | |
1911 ) -> Set[str]: | 1911 ) -> Set[str]: |
1912 """ | 1912 """ |
1913 @param client: The client. | 1913 @param client: The client. |
1914 @param xep_0045: A MUC plugin instance. | 1914 @param xep_0045: A MUC plugin instance. |
1915 @param room_jid: The room JID. | 1915 @param room_jid: The room JID. |
1964 self.__session_manager_waiters[profile] = [] | 1964 self.__session_manager_waiters[profile] = [] |
1965 | 1965 |
1966 # Build and store the session manager | 1966 # Build and store the session manager |
1967 try: | 1967 try: |
1968 session_manager = await prepare_for_profile( | 1968 session_manager = await prepare_for_profile( |
1969 self.host, | 1969 self.host, profile, initial_own_label="Libervia" |
1970 profile, | |
1971 initial_own_label="Libervia" | |
1972 ) | 1970 ) |
1973 except Exception as e: | 1971 except Exception as e: |
1974 # In case of an error during initalization, notify the waiters accordingly | 1972 # In case of an error during initalization, notify the waiters accordingly |
1975 # and delete them | 1973 # and delete them |
1976 for waiter in self.__session_manager_waiters[profile]: | 1974 for waiter in self.__session_manager_waiters[profile]: |
1993 self, | 1991 self, |
1994 client: SatXMPPClient, | 1992 client: SatXMPPClient, |
1995 message_elt: domish.Element, | 1993 message_elt: domish.Element, |
1996 session_manager: omemo.SessionManager, | 1994 session_manager: omemo.SessionManager, |
1997 sender_device_information: omemo.DeviceInformation, | 1995 sender_device_information: omemo.DeviceInformation, |
1998 timestamp: datetime | 1996 timestamp: datetime, |
1999 ) -> None: | 1997 ) -> None: |
2000 """Check a newly decrypted message stanza for ATM content and perform ATM in case. | 1998 """Check a newly decrypted message stanza for ATM content and perform ATM in case. |
2001 | 1999 |
2002 @param client: The client which received the message. | 2000 @param client: The client which received the message. |
2003 @param message_elt: The message element. Can be modified. | 2001 @param message_elt: The message element. Can be modified. |
2006 the message. | 2004 the message. |
2007 @param timestamp: Timestamp extracted from the SCE time affix. | 2005 @param timestamp: Timestamp extracted from the SCE time affix. |
2008 """ | 2006 """ |
2009 | 2007 |
2010 trust_message_cache = persistent.LazyPersistentBinaryDict( | 2008 trust_message_cache = persistent.LazyPersistentBinaryDict( |
2011 "XEP-0384/TM", | 2009 "XEP-0384/TM", client.profile |
2012 client.profile | |
2013 ) | 2010 ) |
2014 | 2011 |
2015 new_cache_entries: Set[TrustMessageCacheEntry] = set() | 2012 new_cache_entries: Set[TrustMessageCacheEntry] = set() |
2016 | 2013 |
2017 for trust_message_elt in message_elt.elements(NS_TM, "trust-message"): | 2014 for trust_message_elt in message_elt.elements(NS_TM, "trust-message"): |
2038 key_owner_jid = jid.JID(key_owner_elt["jid"]).userhostJID() | 2035 key_owner_jid = jid.JID(key_owner_elt["jid"]).userhostJID() |
2039 | 2036 |
2040 for trust_elt in key_owner_elt.elements(NS_TM, "trust"): | 2037 for trust_elt in key_owner_elt.elements(NS_TM, "trust"): |
2041 assert isinstance(trust_elt, domish.Element) | 2038 assert isinstance(trust_elt, domish.Element) |
2042 | 2039 |
2043 new_cache_entries.add(TrustMessageCacheEntry( | 2040 new_cache_entries.add( |
2044 sender_jid=jid.JID(sender_device_information.bare_jid), | 2041 TrustMessageCacheEntry( |
2045 sender_key=sender_device_information.identity_key, | 2042 sender_jid=jid.JID(sender_device_information.bare_jid), |
2046 timestamp=timestamp, | 2043 sender_key=sender_device_information.identity_key, |
2047 trust_update=TrustUpdate( | 2044 timestamp=timestamp, |
2048 target_jid=key_owner_jid, | 2045 trust_update=TrustUpdate( |
2049 target_key=base64.b64decode(str(trust_elt)), | 2046 target_jid=key_owner_jid, |
2050 target_trust=True | 2047 target_key=base64.b64decode(str(trust_elt)), |
2048 target_trust=True, | |
2049 ), | |
2051 ) | 2050 ) |
2052 )) | 2051 ) |
2053 | 2052 |
2054 for distrust_elt in key_owner_elt.elements(NS_TM, "distrust"): | 2053 for distrust_elt in key_owner_elt.elements(NS_TM, "distrust"): |
2055 assert isinstance(distrust_elt, domish.Element) | 2054 assert isinstance(distrust_elt, domish.Element) |
2056 | 2055 |
2057 new_cache_entries.add(TrustMessageCacheEntry( | 2056 new_cache_entries.add( |
2058 sender_jid=jid.JID(sender_device_information.bare_jid), | 2057 TrustMessageCacheEntry( |
2059 sender_key=sender_device_information.identity_key, | 2058 sender_jid=jid.JID(sender_device_information.bare_jid), |
2060 timestamp=timestamp, | 2059 sender_key=sender_device_information.identity_key, |
2061 trust_update=TrustUpdate( | 2060 timestamp=timestamp, |
2062 target_jid=key_owner_jid, | 2061 trust_update=TrustUpdate( |
2063 target_key=base64.b64decode(str(distrust_elt)), | 2062 target_jid=key_owner_jid, |
2064 target_trust=False | 2063 target_key=base64.b64decode(str(distrust_elt)), |
2064 target_trust=False, | |
2065 ), | |
2065 ) | 2066 ) |
2066 )) | 2067 ) |
2067 | 2068 |
2068 # Load existing cache entries | 2069 # Load existing cache entries |
2069 existing_cache_entries = { | 2070 existing_cache_entries = { |
2070 TrustMessageCacheEntry.from_dict(d) | 2071 TrustMessageCacheEntry.from_dict(d) |
2071 for d in await trust_message_cache.get("cache", []) | 2072 for d in await trust_message_cache.get("cache", []) |
2073 | 2074 |
2074 # Discard cache entries by timestamp comparison | 2075 # Discard cache entries by timestamp comparison |
2075 existing_by_target = { | 2076 existing_by_target = { |
2076 ( | 2077 ( |
2077 cache_entry.trust_update.target_jid.userhostJID(), | 2078 cache_entry.trust_update.target_jid.userhostJID(), |
2078 cache_entry.trust_update.target_key | 2079 cache_entry.trust_update.target_key, |
2079 ): cache_entry | 2080 ): cache_entry |
2080 for cache_entry | 2081 for cache_entry in existing_cache_entries |
2081 in existing_cache_entries | |
2082 } | 2082 } |
2083 | 2083 |
2084 # Iterate over a copy here, such that new_cache_entries can be modified | 2084 # Iterate over a copy here, such that new_cache_entries can be modified |
2085 for new_cache_entry in set(new_cache_entries): | 2085 for new_cache_entry in set(new_cache_entries): |
2086 existing_cache_entry = existing_by_target.get( | 2086 existing_cache_entry = existing_by_target.get( |
2087 ( | 2087 ( |
2088 new_cache_entry.trust_update.target_jid.userhostJID(), | 2088 new_cache_entry.trust_update.target_jid.userhostJID(), |
2089 new_cache_entry.trust_update.target_key | 2089 new_cache_entry.trust_update.target_key, |
2090 ), | 2090 ), |
2091 None | 2091 None, |
2092 ) | 2092 ) |
2093 | 2093 |
2094 if existing_cache_entry is not None: | 2094 if existing_cache_entry is not None: |
2095 if existing_cache_entry.timestamp > new_cache_entry.timestamp: | 2095 if existing_cache_entry.timestamp > new_cache_entry.timestamp: |
2096 # If the existing cache entry is newer than the new cache entry, | 2096 # If the existing cache entry is newer than the new cache entry, |
2116 ) | 2116 ) |
2117 | 2117 |
2118 await session_manager.set_trust( | 2118 await session_manager.set_trust( |
2119 trust_update.target_jid.userhost(), | 2119 trust_update.target_jid.userhost(), |
2120 trust_update.target_key, | 2120 trust_update.target_key, |
2121 trust_level.name | 2121 trust_level.name, |
2122 ) | 2122 ) |
2123 | 2123 |
2124 applied_trust_updates.add(trust_update) | 2124 applied_trust_updates.add(trust_update) |
2125 | 2125 |
2126 new_cache_entries.remove(cache_entry) | 2126 new_cache_entries.remove(cache_entry) |
2127 | 2127 |
2128 # Store the remaining existing and new cache entries | 2128 # Store the remaining existing and new cache entries |
2129 await trust_message_cache.force( | 2129 await trust_message_cache.force( |
2130 "cache", | 2130 "cache", [tm.to_dict() for tm in existing_cache_entries | new_cache_entries] |
2131 [tm.to_dict() for tm in existing_cache_entries | new_cache_entries] | |
2132 ) | 2131 ) |
2133 | 2132 |
2134 # If the trust of at least one device was modified, run the ATM cache update logic | 2133 # If the trust of at least one device was modified, run the ATM cache update logic |
2135 if len(applied_trust_updates) > 0: | 2134 if len(applied_trust_updates) > 0: |
2136 await manage_trust_message_cache( | 2135 await manage_trust_message_cache( |
2137 client, | 2136 client, session_manager, frozenset(applied_trust_updates) |
2138 session_manager, | |
2139 frozenset(applied_trust_updates) | |
2140 ) | 2137 ) |
2141 | 2138 |
2142 async def _message_received_trigger( | 2139 async def _message_received_trigger( |
2143 self, | 2140 self, |
2144 client: SatXMPPClient, | 2141 client: SatXMPPClient, |
2145 message_elt: domish.Element, | 2142 message_elt: domish.Element, |
2146 post_treat: defer.Deferred | 2143 post_treat: defer.Deferred, |
2147 ) -> bool: | 2144 ) -> bool: |
2148 """ | 2145 """ |
2149 @param client: The client which received the message. | 2146 @param client: The client which received the message. |
2150 @param message_elt: The message element. Can be modified. | 2147 @param message_elt: The message element. Can be modified. |
2151 @param post_treat: A deferred which evaluates to a :class:`MessageData` once the | 2148 @param post_treat: A deferred which evaluates to a :class:`MessageData` once the |
2209 message_uid = self.__xep_0359.get_origin_id(message_elt) | 2206 message_uid = self.__xep_0359.get_origin_id(message_elt) |
2210 if message_uid is None: | 2207 if message_uid is None: |
2211 message_uid = message_elt.getAttribute("id") | 2208 message_uid = message_elt.getAttribute("id") |
2212 if message_uid is not None: | 2209 if message_uid is not None: |
2213 muc_plaintext_cache_key = MUCPlaintextCacheKey( | 2210 muc_plaintext_cache_key = MUCPlaintextCacheKey( |
2214 client, | 2211 client, room_jid, message_uid |
2215 room_jid, | |
2216 message_uid | |
2217 ) | 2212 ) |
2218 else: | 2213 else: |
2219 # I'm not sure why this check is required, this code is copied from the old | 2214 # I'm not sure why this check is required, this code is copied from the old |
2220 # plugin. | 2215 # plugin. |
2221 if sender_jid.userhostJID() == client.jid.userhostJID(): | 2216 if sender_jid.userhostJID() == client.jid.userhostJID(): |
2229 sender_bare_jid = sender_jid.userhost() | 2224 sender_bare_jid = sender_jid.userhost() |
2230 | 2225 |
2231 message: Optional[omemo.Message] = None | 2226 message: Optional[omemo.Message] = None |
2232 encrypted_elt: Optional[domish.Element] = None | 2227 encrypted_elt: Optional[domish.Element] = None |
2233 | 2228 |
2234 twomemo_encrypted_elt = cast(Optional[domish.Element], next( | 2229 twomemo_encrypted_elt = cast( |
2235 message_elt.elements(twomemo.twomemo.NAMESPACE, "encrypted"), | 2230 Optional[domish.Element], |
2236 None | 2231 next(message_elt.elements(twomemo.twomemo.NAMESPACE, "encrypted"), None), |
2237 )) | 2232 ) |
2238 | 2233 |
2239 oldmemo_encrypted_elt = cast(Optional[domish.Element], next( | 2234 oldmemo_encrypted_elt = cast( |
2240 message_elt.elements(oldmemo.oldmemo.NAMESPACE, "encrypted"), | 2235 Optional[domish.Element], |
2241 None | 2236 next(message_elt.elements(oldmemo.oldmemo.NAMESPACE, "encrypted"), None), |
2242 )) | 2237 ) |
2243 | 2238 |
2244 try: | 2239 try: |
2245 session_manager = await self.get_session_manager(cast(str, client.profile)) | 2240 session_manager = await self.get_session_manager(cast(str, client.profile)) |
2246 except Exception as e: | 2241 except Exception as e: |
2247 log.error(f"error while preparing profile for {client.profile}: {e}") | 2242 log.error(f"error while preparing profile for {client.profile}: {e}") |
2249 return True | 2244 return True |
2250 | 2245 |
2251 if twomemo_encrypted_elt is not None: | 2246 if twomemo_encrypted_elt is not None: |
2252 try: | 2247 try: |
2253 message = twomemo.etree.parse_message( | 2248 message = twomemo.etree.parse_message( |
2254 xml_tools.domish_elt_2_et_elt(twomemo_encrypted_elt), | 2249 xml_tools.domish_elt_2_et_elt(twomemo_encrypted_elt), sender_bare_jid |
2255 sender_bare_jid | |
2256 ) | 2250 ) |
2257 except (ValueError, XMLSchemaValidationError): | 2251 except (ValueError, XMLSchemaValidationError): |
2258 log.warning( | 2252 log.warning( |
2259 f"Ingoring malformed encrypted message for namespace" | 2253 f"Ingoring malformed encrypted message for namespace" |
2260 f" {twomemo.twomemo.NAMESPACE}: {twomemo_encrypted_elt.toXml()}" | 2254 f" {twomemo.twomemo.NAMESPACE}: {twomemo_encrypted_elt.toXml()}" |
2266 try: | 2260 try: |
2267 message = await oldmemo.etree.parse_message( | 2261 message = await oldmemo.etree.parse_message( |
2268 xml_tools.domish_elt_2_et_elt(oldmemo_encrypted_elt), | 2262 xml_tools.domish_elt_2_et_elt(oldmemo_encrypted_elt), |
2269 sender_bare_jid, | 2263 sender_bare_jid, |
2270 client.jid.userhost(), | 2264 client.jid.userhost(), |
2271 session_manager | 2265 session_manager, |
2272 ) | 2266 ) |
2273 except (ValueError, XMLSchemaValidationError): | 2267 except (ValueError, XMLSchemaValidationError): |
2274 log.warning( | 2268 log.warning( |
2275 f"Ingoring malformed encrypted message for namespace" | 2269 f"Ingoring malformed encrypted message for namespace" |
2276 f" {oldmemo.oldmemo.NAMESPACE}: {oldmemo_encrypted_elt.toXml()}" | 2270 f" {oldmemo.oldmemo.NAMESPACE}: {oldmemo_encrypted_elt.toXml()}" |
2322 feedback_jid, | 2316 feedback_jid, |
2323 D_( | 2317 D_( |
2324 f"An OMEMO message from {sender_jid.full()} has not been" | 2318 f"An OMEMO message from {sender_jid.full()} has not been" |
2325 f" encrypted for our device, we can't decrypt it." | 2319 f" encrypted for our device, we can't decrypt it." |
2326 ), | 2320 ), |
2327 { C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR } | 2321 {C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR}, |
2328 ) | 2322 ) |
2329 log.warning("Message not encrypted for us.") | 2323 log.warning("Message not encrypted for us.") |
2330 else: | 2324 else: |
2331 log.debug("Message not encrypted for us.") | 2325 log.debug("Message not encrypted for us.") |
2332 | 2326 |
2333 # No point in further processing this message. | 2327 # No point in further processing this message. |
2334 return False | 2328 return False |
2335 except Exception as e: | 2329 except Exception as e: |
2336 log.warning(_("Can't decrypt message: {reason}\n{xml}").format( | 2330 log.warning( |
2337 reason=e, | 2331 _("Can't decrypt message: {reason}\n{xml}").format( |
2338 xml=message_elt.toXml() | 2332 reason=e, xml=message_elt.toXml() |
2339 )) | 2333 ) |
2334 ) | |
2340 client.feedback( | 2335 client.feedback( |
2341 feedback_jid, | 2336 feedback_jid, |
2342 D_( | 2337 D_( |
2343 f"An OMEMO message from {sender_jid.full()} can't be decrypted:" | 2338 f"An OMEMO message from {sender_jid.full()} can't be decrypted:" |
2344 f" {e}" | 2339 f" {e}" |
2345 ), | 2340 ), |
2346 { C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR } | 2341 {C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR}, |
2347 ) | 2342 ) |
2348 # No point in further processing this message | 2343 # No point in further processing this message |
2349 return False | 2344 return False |
2350 | 2345 |
2351 affix_values: Optional[SCEAffixValues] = None | 2346 affix_values: Optional[SCEAffixValues] = None |
2352 | 2347 |
2353 if message.namespace == twomemo.twomemo.NAMESPACE: | 2348 if message.namespace == twomemo.twomemo.NAMESPACE: |
2354 if plaintext is not None: | 2349 if plaintext is not None: |
2355 # XEP_0420.unpack_stanza handles the whole unpacking, including the | 2350 # XEP_0420.unpack_stanza handles the whole unpacking, including the |
2356 # relevant modifications to the element | 2351 # relevant modifications to the element |
2357 sce_profile = \ | 2352 sce_profile = ( |
2358 OMEMO.SCE_PROFILE_GROUPCHAT if is_muc_message else OMEMO.SCE_PROFILE | 2353 OMEMO.SCE_PROFILE_GROUPCHAT if is_muc_message else OMEMO.SCE_PROFILE |
2354 ) | |
2359 try: | 2355 try: |
2360 affix_values = self.__xep_0420.unpack_stanza( | 2356 affix_values = self.__xep_0420.unpack_stanza( |
2361 sce_profile, | 2357 sce_profile, message_elt, plaintext |
2362 message_elt, | |
2363 plaintext | |
2364 ) | 2358 ) |
2365 except Exception as e: | 2359 except Exception as e: |
2366 log.warning(D_( | 2360 log.warning( |
2367 f"Error unpacking SCE-encrypted message: {e}\n{plaintext}" | 2361 D_(f"Error unpacking SCE-encrypted message: {e}\n{plaintext}") |
2368 )) | 2362 ) |
2369 client.feedback( | 2363 client.feedback( |
2370 feedback_jid, | 2364 feedback_jid, |
2371 D_( | 2365 D_( |
2372 f"An OMEMO message from {sender_jid.full()} was rejected:" | 2366 f"An OMEMO message from {sender_jid.full()} was rejected:" |
2373 f" {e}" | 2367 f" {e}" |
2374 ), | 2368 ), |
2375 { C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR } | 2369 {C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR}, |
2376 ) | 2370 ) |
2377 # No point in further processing this message | 2371 # No point in further processing this message |
2378 return False | 2372 return False |
2379 else: | 2373 else: |
2380 if affix_values.timestamp is not None: | 2374 if affix_values.timestamp is not None: |
2393 if plaintext is not None: | 2387 if plaintext is not None: |
2394 # Add the decrypted body | 2388 # Add the decrypted body |
2395 message_elt.addElement("body", content=plaintext.decode("utf-8")) | 2389 message_elt.addElement("body", content=plaintext.decode("utf-8")) |
2396 | 2390 |
2397 # Mark the message as trusted or untrusted. Undecided counts as untrusted here. | 2391 # Mark the message as trusted or untrusted. Undecided counts as untrusted here. |
2398 trust_level = \ | 2392 trust_level = await session_manager._evaluate_custom_trust_level( |
2399 await session_manager._evaluate_custom_trust_level(device_information) | 2393 device_information |
2394 ) | |
2400 | 2395 |
2401 if trust_level is omemo.TrustLevel.TRUSTED: | 2396 if trust_level is omemo.TrustLevel.TRUSTED: |
2402 post_treat.addCallback(client.encryption.mark_as_trusted) | 2397 post_treat.addCallback(client.encryption.mark_as_trusted) |
2403 else: | 2398 else: |
2404 post_treat.addCallback(client.encryption.mark_as_untrusted) | 2399 post_treat.addCallback(client.encryption.mark_as_untrusted) |
2405 | 2400 |
2406 # Mark the message as originally encrypted | 2401 # Mark the message as originally encrypted |
2407 post_treat.addCallback( | 2402 post_treat.addCallback( |
2408 client.encryption.mark_as_encrypted, | 2403 client.encryption.mark_as_encrypted, namespace=message.namespace |
2409 namespace=message.namespace | |
2410 ) | 2404 ) |
2411 | 2405 |
2412 # Handle potential ATM trust updates | 2406 # Handle potential ATM trust updates |
2413 if affix_values is not None and affix_values.timestamp is not None: | 2407 if affix_values is not None and affix_values.timestamp is not None: |
2414 await self.__message_received_trigger_atm( | 2408 await self.__message_received_trigger_atm( |
2415 client, | 2409 client, |
2416 message_elt, | 2410 message_elt, |
2417 session_manager, | 2411 session_manager, |
2418 device_information, | 2412 device_information, |
2419 affix_values.timestamp | 2413 affix_values.timestamp, |
2420 ) | 2414 ) |
2421 | 2415 |
2422 # Message processed successfully, continue with the flow | 2416 # Message processed successfully, continue with the flow |
2423 return True | 2417 return True |
2424 | 2418 |
2428 @param stanza: The stanza that is about to be sent. Can be modified. | 2422 @param stanza: The stanza that is about to be sent. Can be modified. |
2429 @return: Whether the send message flow should continue or not. | 2423 @return: Whether the send message flow should continue or not. |
2430 """ | 2424 """ |
2431 # SCE is only applicable to message and IQ stanzas | 2425 # SCE is only applicable to message and IQ stanzas |
2432 # FIXME: temporary disabling IQ stanza encryption | 2426 # FIXME: temporary disabling IQ stanza encryption |
2433 if stanza.name not in { "message" }: # , "iq" }: | 2427 if stanza.name not in {"message"}: # , "iq" }: |
2434 return True | 2428 return True |
2435 | 2429 |
2436 # Get the intended recipient | 2430 # Get the intended recipient |
2437 recipient = stanza.getAttribute("to", None) | 2431 recipient = stanza.getAttribute("to", None) |
2438 if recipient is None: | 2432 if recipient is None: |
2464 client, | 2458 client, |
2465 encryption_ns, | 2459 encryption_ns, |
2466 stanza, | 2460 stanza, |
2467 recipient_bare_jid, | 2461 recipient_bare_jid, |
2468 stanza.getAttribute("type", C.MESS_TYPE_NORMAL) == C.MESS_TYPE_GROUPCHAT, | 2462 stanza.getAttribute("type", C.MESS_TYPE_NORMAL) == C.MESS_TYPE_GROUPCHAT, |
2469 stanza.getAttribute("id", None) | 2463 stanza.getAttribute("id", None), |
2470 ) | 2464 ) |
2471 else: | 2465 else: |
2472 # Encryption is requested for this recipient, but not with twomemo | 2466 # Encryption is requested for this recipient, but not with twomemo |
2473 return True | 2467 return True |
2474 | 2468 |
2475 | |
2476 | |
2477 # Add a store hint if this is a message stanza | 2469 # Add a store hint if this is a message stanza |
2478 if stanza.name == "message": | 2470 if stanza.name == "message": |
2479 self.__xep_0334.add_hint_elements(stanza, [ "store" ]) | 2471 self.__xep_0334.add_hint_elements(stanza, ["store"]) |
2480 | 2472 |
2481 # Let the flow continue. | 2473 # Let the flow continue. |
2482 return True | 2474 return True |
2483 | 2475 |
2484 async def download_missing_device_lists( | 2476 async def download_missing_device_lists( |
2499 not_in_roster = [j for j in recipients if not client.roster.is_jid_in_roster(j)] | 2491 not_in_roster = [j for j in recipients if not client.roster.is_jid_in_roster(j)] |
2500 for bare_jid in not_in_roster: | 2492 for bare_jid in not_in_roster: |
2501 device_information = await session_manager.get_device_information( | 2493 device_information = await session_manager.get_device_information( |
2502 bare_jid.userhost() | 2494 bare_jid.userhost() |
2503 ) | 2495 ) |
2504 if ( | 2496 if not device_information or not all( |
2505 not device_information | 2497 namespace in di.namespaces for di in device_information |
2506 or not all(namespace in di.namespaces for di in device_information) | |
2507 ): | 2498 ): |
2508 if namespace == self.NS_TWOMEMO: | 2499 if namespace == self.NS_TWOMEMO: |
2509 algo, node = "OMEMO", TWOMEMO_DEVICE_LIST_NODE | 2500 algo, node = "OMEMO", TWOMEMO_DEVICE_LIST_NODE |
2510 elif namespace == self.NS_OLDMEMO: | 2501 elif namespace == self.NS_OLDMEMO: |
2511 algo, node = "OMEMO_legacy", OLDMEMO_DEVICE_LIST_NODE | 2502 algo, node = "OMEMO_legacy", OLDMEMO_DEVICE_LIST_NODE |
2526 client: SatXMPPClient, | 2517 client: SatXMPPClient, |
2527 namespace: NamespaceType, | 2518 namespace: NamespaceType, |
2528 stanza: domish.Element, | 2519 stanza: domish.Element, |
2529 recipient_jids: Union[jid.JID, Set[jid.JID]], | 2520 recipient_jids: Union[jid.JID, Set[jid.JID]], |
2530 is_muc_message: bool, | 2521 is_muc_message: bool, |
2531 stanza_id: Optional[str] | 2522 stanza_id: Optional[str], |
2532 ) -> None: | 2523 ) -> None: |
2533 """ | 2524 """ |
2534 @param client: The client. | 2525 @param client: The client. |
2535 @param namespace: The namespace of the OMEMO version to use. | 2526 @param namespace: The namespace of the OMEMO version to use. |
2536 @param stanza: The stanza. Twomemo will encrypt the whole stanza using SCE, | 2527 @param stanza: The stanza. Twomemo will encrypt the whole stanza using SCE, |
2575 ) | 2566 ) |
2576 | 2567 |
2577 room_jid = feedback_jid = recipient_jid.userhostJID() | 2568 room_jid = feedback_jid = recipient_jid.userhostJID() |
2578 | 2569 |
2579 recipient_bare_jids = self.__get_joined_muc_users( | 2570 recipient_bare_jids = self.__get_joined_muc_users( |
2580 client, | 2571 client, self.__xep_0045, room_jid |
2581 self.__xep_0045, | |
2582 room_jid | |
2583 ) | 2572 ) |
2584 | 2573 |
2585 muc_plaintext_cache_key = MUCPlaintextCacheKey( | 2574 muc_plaintext_cache_key = MUCPlaintextCacheKey( |
2586 client=client, | 2575 client=client, room_jid=room_jid, message_uid=stanza_id |
2587 room_jid=room_jid, | |
2588 message_uid=stanza_id | |
2589 ) | 2576 ) |
2590 else: | 2577 else: |
2591 recipient_bare_jids = {r.userhost() for r in recipient_jids} | 2578 recipient_bare_jids = {r.userhost() for r in recipient_jids} |
2592 feedback_jid = recipient_jid.userhostJID() | 2579 feedback_jid = recipient_jid.userhostJID() |
2593 | 2580 |
2609 """ | 2596 """ |
2610 | 2597 |
2611 if namespace == twomemo.twomemo.NAMESPACE: | 2598 if namespace == twomemo.twomemo.NAMESPACE: |
2612 return self.__xep_0420.pack_stanza( | 2599 return self.__xep_0420.pack_stanza( |
2613 OMEMO.SCE_PROFILE_GROUPCHAT if is_muc_message else OMEMO.SCE_PROFILE, | 2600 OMEMO.SCE_PROFILE_GROUPCHAT if is_muc_message else OMEMO.SCE_PROFILE, |
2614 stanza | 2601 stanza, |
2615 ) | 2602 ) |
2616 | 2603 |
2617 if namespace == oldmemo.oldmemo.NAMESPACE: | 2604 if namespace == oldmemo.oldmemo.NAMESPACE: |
2618 plaintext: Optional[bytes] = None | 2605 plaintext: Optional[bytes] = None |
2619 | 2606 |
2620 for child in stanza.elements(): | 2607 for child in stanza.elements(): |
2621 if child.name == "body" and plaintext is None: | 2608 if child.name == "body" and plaintext is None: |
2622 plaintext = str(child).encode("utf-8") | 2609 plaintext = str(child).encode("utf-8") |
2623 | 2610 |
2624 # Any other sensitive elements to remove here? | 2611 # Any other sensitive elements to remove here? |
2625 if child.name in { "body", "html" }: | 2612 if child.name in {"body", "html"}: |
2626 stanza.children.remove(child) | 2613 stanza.children.remove(child) |
2627 | 2614 |
2628 if plaintext is None: | 2615 if plaintext is None: |
2629 log.warning( | 2616 log.warning( |
2630 "No body found in intercepted message to be encrypted with" | 2617 "No body found in intercepted message to be encrypted with" |
2642 return | 2629 return |
2643 | 2630 |
2644 log.debug(f"Plaintext to encrypt: {plaintext}") | 2631 log.debug(f"Plaintext to encrypt: {plaintext}") |
2645 | 2632 |
2646 session_manager = await self.get_session_manager(client.profile) | 2633 session_manager = await self.get_session_manager(client.profile) |
2647 await self.download_missing_device_lists(client, namespace, recipient_jids, session_manager) | 2634 await self.download_missing_device_lists( |
2635 client, namespace, recipient_jids, session_manager | |
2636 ) | |
2648 | 2637 |
2649 try: | 2638 try: |
2650 messages, encryption_errors = await session_manager.encrypt( | 2639 messages, encryption_errors = await session_manager.encrypt( |
2651 frozenset(recipient_bare_jids), | 2640 frozenset(recipient_bare_jids), |
2652 { namespace: plaintext }, | 2641 {namespace: plaintext}, |
2653 backend_priority_order=[ namespace ], | 2642 backend_priority_order=[namespace], |
2654 identifier=feedback_jid.userhost() | 2643 identifier=feedback_jid.userhost(), |
2655 ) | 2644 ) |
2656 except Exception as e: | 2645 except Exception as e: |
2657 msg = _( | 2646 msg = _( |
2658 # pylint: disable=consider-using-f-string | 2647 # pylint: disable=consider-using-f-string |
2659 "Can't encrypt message for {entities}: {reason}".format( | 2648 "Can't encrypt message for {entities}: {reason}".format( |
2660 entities=', '.join(recipient_bare_jids), | 2649 entities=", ".join(recipient_bare_jids), reason=e |
2661 reason=e | |
2662 ) | 2650 ) |
2663 ) | 2651 ) |
2664 log.warning(msg) | 2652 log.warning(msg) |
2665 client.feedback(feedback_jid, msg, { | 2653 client.feedback(feedback_jid, msg, {C.MESS_EXTRA_INFO: C.EXTRA_INFO_ENCR_ERR}) |
2666 C.MESS_EXTRA_INFO: C.EXTRA_INFO_ENCR_ERR | |
2667 }) | |
2668 raise e | 2654 raise e |
2669 | 2655 |
2670 if len(encryption_errors) > 0: | 2656 if len(encryption_errors) > 0: |
2671 log.warning( | 2657 log.warning( |
2672 f"Ignored the following non-critical encryption errors:" | 2658 f"Ignored the following non-critical encryption errors:" |
2673 f" {encryption_errors}" | 2659 f" {encryption_errors}" |
2674 ) | 2660 ) |
2675 | 2661 |
2676 encrypted_errors_stringified = ", ".join([ | 2662 encrypted_errors_stringified = ", ".join( |
2677 f"device {err.device_id} of {err.bare_jid} under namespace" | 2663 [ |
2678 f" {err.namespace}" | 2664 f"device {err.device_id} of {err.bare_jid} under namespace" |
2679 for err | 2665 f" {err.namespace}" |
2680 in encryption_errors | 2666 for err in encryption_errors |
2681 ]) | 2667 ] |
2668 ) | |
2682 | 2669 |
2683 client.feedback( | 2670 client.feedback( |
2684 feedback_jid, | 2671 feedback_jid, |
2685 D_( | 2672 D_( |
2686 "There were non-critical errors during encryption resulting in some" | 2673 "There were non-critical errors during encryption resulting in some" |
2687 " of your destinees' devices potentially not receiving the message." | 2674 " of your destinees' devices potentially not receiving the message." |
2688 " This happens when the encryption data/key material of a device is" | 2675 " This happens when the encryption data/key material of a device is" |
2689 " incomplete or broken, which shouldn't happen for actively used" | 2676 " incomplete or broken, which shouldn't happen for actively used" |
2690 " devices, and can usually be ignored. The following devices are" | 2677 " devices, and can usually be ignored. The following devices are" |
2691 f" affected: {encrypted_errors_stringified}." | 2678 f" affected: {encrypted_errors_stringified}." |
2692 ) | 2679 ), |
2693 ) | 2680 ) |
2694 | 2681 |
2695 message = next(message for message in messages if message.namespace == namespace) | 2682 message = next(message for message in messages if message.namespace == namespace) |
2696 | 2683 |
2697 if namespace == twomemo.twomemo.NAMESPACE: | 2684 if namespace == twomemo.twomemo.NAMESPACE: |
2698 # Add the encrypted element | 2685 # Add the encrypted element |
2699 stanza.addChild(xml_tools.et_elt_2_domish_elt( | 2686 stanza.addChild( |
2700 twomemo.etree.serialize_message(message) | 2687 xml_tools.et_elt_2_domish_elt(twomemo.etree.serialize_message(message)) |
2701 )) | 2688 ) |
2702 | 2689 |
2703 if namespace == oldmemo.oldmemo.NAMESPACE: | 2690 if namespace == oldmemo.oldmemo.NAMESPACE: |
2704 # Add the encrypted element | 2691 # Add the encrypted element |
2705 stanza.addChild(xml_tools.et_elt_2_domish_elt( | 2692 stanza.addChild( |
2706 oldmemo.etree.serialize_message(message) | 2693 xml_tools.et_elt_2_domish_elt(oldmemo.etree.serialize_message(message)) |
2707 )) | 2694 ) |
2708 | 2695 |
2709 if muc_plaintext_cache_key is not None: | 2696 if muc_plaintext_cache_key is not None: |
2710 self.__muc_plaintext_cache[muc_plaintext_cache_key] = plaintext | 2697 self.__muc_plaintext_cache[muc_plaintext_cache_key] = plaintext |
2711 | 2698 |
2712 async def __on_device_list_update( | 2699 async def __on_device_list_update( |
2713 self, | 2700 self, items_event: pubsub.ItemsEvent, profile: str |
2714 items_event: pubsub.ItemsEvent, | |
2715 profile: str | |
2716 ) -> None: | 2701 ) -> None: |
2717 """Handle device list updates fired by PEP. | 2702 """Handle device list updates fired by PEP. |
2718 | 2703 |
2719 @param items_event: The event. | 2704 @param items_event: The event. |
2720 @param profile: The profile this event belongs to. | 2705 @param profile: The profile this event belongs to. |
2724 items = cast(List[domish.Element], items_event.items) | 2709 items = cast(List[domish.Element], items_event.items) |
2725 client = self.host.get_client(profile) | 2710 client = self.host.get_client(profile) |
2726 await self._update_device_list(client, sender, items) | 2711 await self._update_device_list(client, sender, items) |
2727 | 2712 |
2728 async def _update_device_list( | 2713 async def _update_device_list( |
2729 self, | 2714 self, client: SatXMPPEntity, sender: jid.JID, items: list[domish.Element] |
2730 client: SatXMPPEntity, | |
2731 sender: jid.JID, | |
2732 items: list[domish.Element] | |
2733 ) -> None: | 2715 ) -> None: |
2734 | 2716 |
2735 if len(items) > 1: | 2717 if len(items) > 1: |
2736 log.warning("Ignoring device list update with more than one element.") | 2718 log.warning("Ignoring device list update with more than one element.") |
2737 return | 2719 return |
2772 return | 2754 return |
2773 | 2755 |
2774 session_manager = await self.get_session_manager(client.profile) | 2756 session_manager = await self.get_session_manager(client.profile) |
2775 | 2757 |
2776 await session_manager.update_device_list( | 2758 await session_manager.update_device_list( |
2777 namespace, | 2759 namespace, sender.userhost(), device_list |
2778 sender.userhost(), | 2760 ) |
2779 device_list | |
2780 ) |