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 )