comparison sat/plugins/plugin_xep_0384.py @ 3967:f461f11ea176

plugin XEP-0384: Implementation of Automatic Trust Management: - Implementation of Trust Messages (XEP-0434) - Implementation of Automatic Trust Management (XEP-0450) - Implementations directly as part of the OMEMO plugin, since omemo:2 is the only protocol supported by ATM at the moment - Trust system selection updated to allow choice between manual trust with ATM and BTBV - dev-requirements.txt updated to include additional requirements for the e2e tests fix 376
author Syndace <me@syndace.dev>
date Fri, 28 Oct 2022 18:50:06 +0200
parents 748094d5a74d
children 8e7d5796fb23
comparison
equal deleted inserted replaced
3966:9f85369294f3 3967:f461f11ea176
14 # GNU Affero General Public License for more details. 14 # GNU Affero General Public License for more details.
15 15
16 # You should have received a copy of the GNU Affero General Public License 16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. 17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 18
19 import base64
20 from datetime import datetime
19 import enum 21 import enum
20 import logging 22 import logging
21 import time 23 import time
22 from typing import ( 24 from typing import \
23 Any, Callable, Dict, FrozenSet, List, Literal, NamedTuple, Optional, Set, Type, cast 25 Any, Dict, FrozenSet, List, Literal, NamedTuple, Optional, Set, Type, cast
24 )
25 import uuid 26 import uuid
26 import xml.etree.ElementTree as ET 27 import xml.etree.ElementTree as ET
27 from xml.sax.saxutils import quoteattr 28 from xml.sax.saxutils import quoteattr
28 29
29 from typing_extensions import Never, assert_never 30 from typing_extensions import Final, Never, assert_never
30 from wokkel import muc, pubsub # type: ignore[import] 31 from wokkel import muc, pubsub # type: ignore[import]
32 import xmlschema
31 33
32 from sat.core import exceptions 34 from sat.core import exceptions
33 from sat.core.constants import Const as C 35 from sat.core.constants import Const as C
34 from sat.core.core_types import MessageData, SatXMPPEntity 36 from sat.core.core_types import MessageData, SatXMPPEntity
35 from sat.core.i18n import _, D_ 37 from sat.core.i18n import _, D_
41 from sat.plugins.plugin_xep_0045 import XEP_0045 43 from sat.plugins.plugin_xep_0045 import XEP_0045
42 from sat.plugins.plugin_xep_0060 import XEP_0060 44 from sat.plugins.plugin_xep_0060 import XEP_0060
43 from sat.plugins.plugin_xep_0163 import XEP_0163 45 from sat.plugins.plugin_xep_0163 import XEP_0163
44 from sat.plugins.plugin_xep_0334 import XEP_0334 46 from sat.plugins.plugin_xep_0334 import XEP_0334
45 from sat.plugins.plugin_xep_0359 import XEP_0359 47 from sat.plugins.plugin_xep_0359 import XEP_0359
46 from sat.plugins.plugin_xep_0420 import XEP_0420, SCEAffixPolicy, SCEProfile 48 from sat.plugins.plugin_xep_0420 import \
49 XEP_0420, SCEAffixPolicy, SCEAffixValues, SCEProfile
47 from sat.tools import xml_tools 50 from sat.tools import xml_tools
48 from twisted.internet import defer 51 from twisted.internet import defer
49 from twisted.words.protocols.jabber import error, jid 52 from twisted.words.protocols.jabber import error, jid
50 from twisted.words.xish import domish 53 from twisted.words.xish import domish
51 54
74 "PLUGIN_INFO", 77 "PLUGIN_INFO",
75 "OMEMO" 78 "OMEMO"
76 ] 79 ]
77 80
78 log = cast(Logger, getLogger(__name__)) # type: ignore[no-untyped-call] 81 log = cast(Logger, getLogger(__name__)) # type: ignore[no-untyped-call]
79
80
81 string_to_domish = cast(Callable[[str], domish.Element], xml_tools.ElementParser())
82 82
83 83
84 PLUGIN_INFO = { 84 PLUGIN_INFO = {
85 C.PI_NAME: "OMEMO", 85 C.PI_NAME: "OMEMO",
86 C.PI_IMPORT_NAME: "XEP-0384", 86 C.PI_IMPORT_NAME: "XEP-0384",
132 client: SatXMPPClient 132 client: SatXMPPClient
133 room_jid: jid.JID 133 room_jid: jid.JID
134 message_uid: str 134 message_uid: str
135 135
136 136
137 # TODO: Convert without serialization/parsing
138 # On a medium-to-large-sized oldmemo message stanza, 10000 runs of this function took
139 # around 0.6 seconds on my setup.
140 def etree_to_domish(element: ET.Element) -> domish.Element:
141 """
142 @param element: An ElementTree element.
143 @return: The ElementTree element converted to a domish element.
144 """
145
146 return string_to_domish(ET.tostring(element, encoding="unicode"))
147
148
149 # TODO: Convert without serialization/parsing
150 # On a medium-to-large-sized oldmemo message stanza, 10000 runs of this function took less
151 # than one second on my setup.
152 def domish_to_etree(element: domish.Element) -> ET.Element:
153 """
154 @param element: A domish element.
155 @return: The domish element converted to an ElementTree element.
156 """
157
158 return ET.fromstring(element.toXml())
159
160
161 def domish_to_etree2(element: domish.Element) -> ET.Element:
162 """
163 WIP
164 """
165
166 element_name = element.name
167 if element.uri is not None:
168 element_name = "{" + element.uri + "}" + element_name
169
170 attrib: Dict[str, str] = {}
171 for qname, value in element.attributes.items():
172 attribute_name = qname[1] if isinstance(qname, tuple) else qname
173 attribute_namespace = qname[0] if isinstance(qname, tuple) else None
174 if attribute_namespace is not None:
175 attribute_name = "{" + attribute_namespace + "}" + attribute_name
176
177 attrib[attribute_name] = value
178
179 result = ET.Element(element_name, attrib)
180
181 last_child: Optional[ET.Element] = None
182 for child in element.children:
183 if isinstance(child, str):
184 if last_child is None:
185 result.text = child
186 else:
187 last_child.tail = child
188 else:
189 last_child = domish_to_etree2(child)
190 result.append(last_child)
191
192 return result
193
194
195 @enum.unique 137 @enum.unique
196 class TrustLevel(enum.Enum): 138 class TrustLevel(enum.Enum):
197 """ 139 """
198 The trust levels required for BTBV and manual trust. 140 The trust levels required for ATM and BTBV.
199 """ 141 """
200 142
201 TRUSTED: str = "TRUSTED" 143 TRUSTED: str = "TRUSTED"
202 BLINDLY_TRUSTED: str = "BLINDLY_TRUSTED" 144 BLINDLY_TRUSTED: str = "BLINDLY_TRUSTED"
203 UNDECIDED: str = "UNDECIDED" 145 UNDECIDED: str = "UNDECIDED"
204 DISTRUSTED: str = "DISTRUSTED" 146 DISTRUSTED: str = "DISTRUSTED"
205
206 def to_omemo_trust_level(self) -> omemo.TrustLevel:
207 """
208 @return: This custom trust level evaluated to one of the OMEMO trust levels.
209 """
210
211 if self is TrustLevel.TRUSTED or self is TrustLevel.BLINDLY_TRUSTED:
212 return omemo.TrustLevel.TRUSTED
213 if self is TrustLevel.UNDECIDED:
214 return omemo.TrustLevel.UNDECIDED
215 if self is TrustLevel.DISTRUSTED:
216 return omemo.TrustLevel.DISTRUSTED
217
218 return assert_never(self)
219 147
220 148
221 TWOMEMO_DEVICE_LIST_NODE = "urn:xmpp:omemo:2:devices" 149 TWOMEMO_DEVICE_LIST_NODE = "urn:xmpp:omemo:2:devices"
222 OLDMEMO_DEVICE_LIST_NODE = "eu.siacs.conversations.axolotl.devicelist" 150 OLDMEMO_DEVICE_LIST_NODE = "eu.siacs.conversations.axolotl.devicelist"
223 151
429 raise omemo.BundleDownloadFailed( 357 raise omemo.BundleDownloadFailed(
430 f"Bundle download failed for {bare_jid}: {device_id} under namespace" 358 f"Bundle download failed for {bare_jid}: {device_id} under namespace"
431 f" {namespace}: Unexpected number of items retrieved: {len(items)}." 359 f" {namespace}: Unexpected number of items retrieved: {len(items)}."
432 ) 360 )
433 361
434 element = next(iter(xml_tools.domish_elt_2_et_elt(cast(domish.Element, items[0]))), None) 362 element = \
363 next(iter(xml_tools.domish_elt_2_et_elt(cast(domish.Element, items[0]))), None)
435 if element is None: 364 if element is None:
436 raise omemo.BundleDownloadFailed( 365 raise omemo.BundleDownloadFailed(
437 f"Bundle download failed for {bare_jid}: {device_id} under namespace" 366 f"Bundle download failed for {bare_jid}: {device_id} under namespace"
438 f" {namespace}: Item download succeeded but parsing failed: {element}." 367 f" {namespace}: Item download succeeded but parsing failed: {element}."
439 ) 368 )
443 except Exception as e: 372 except Exception as e:
444 raise omemo.BundleDownloadFailed( 373 raise omemo.BundleDownloadFailed(
445 f"Bundle parsing failed for {bare_jid}: {device_id} under namespace" 374 f"Bundle parsing failed for {bare_jid}: {device_id} under namespace"
446 f" {namespace}" 375 f" {namespace}"
447 ) from e 376 ) from e
377
378
379 # ATM only supports protocols based on SCE, which is currently only omemo:2, and relies on
380 # so many implementation details of the encryption protocol that it makes more sense to
381 # add ATM to the OMEMO plugin directly instead of having it a separate Libervia plugin.
382 NS_TM: Final = "urn:xmpp:tm:1"
383 NS_ATM: Final = "urn:xmpp:atm:1"
384
385
386 TRUST_MESSAGE_SCHEMA = xmlschema.XMLSchema("""<?xml version='1.0' encoding='UTF-8'?>
387 <xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'
388 targetNamespace='urn:xmpp:tm:1'
389 xmlns='urn:xmpp:tm:1'
390 elementFormDefault='qualified'>
391
392 <xs:element name='trust-message'>
393 <xs:complexType>
394 <xs:sequence>
395 <xs:element ref='key-owner' minOccurs='1' maxOccurs='unbounded'/>
396 </xs:sequence>
397 <xs:attribute name='usage' type='xs:string' use='required'/>
398 <xs:attribute name='encryption' type='xs:string' use='required'/>
399 </xs:complexType>
400 </xs:element>
401
402 <xs:element name='key-owner'>
403 <xs:complexType>
404 <xs:sequence>
405 <xs:element
406 name='trust' type='xs:base64Binary' minOccurs='0' maxOccurs='unbounded'/>
407 <xs:element
408 name='distrust' type='xs:base64Binary' minOccurs='0' maxOccurs='unbounded'/>
409 </xs:sequence>
410 <xs:attribute name='jid' type='xs:string' use='required'/>
411 </xs:complexType>
412 </xs:element>
413 </xs:schema>
414 """)
415
416
417 # This is compatible with omemo:2's SCE profile
418 TM_SCE_PROFILE = SCEProfile(
419 rpad_policy=SCEAffixPolicy.REQUIRED,
420 time_policy=SCEAffixPolicy.REQUIRED,
421 to_policy=SCEAffixPolicy.OPTIONAL,
422 from_policy=SCEAffixPolicy.OPTIONAL,
423 custom_policies={}
424 )
425
426
427 class TrustUpdate(NamedTuple):
428 # pylint: disable=invalid-name
429 """
430 An update to the trust status of an identity key, used by Automatic Trust Management.
431 """
432
433 target_jid: jid.JID
434 target_key: bytes
435 target_trust: bool
436
437
438 class TrustMessageCacheEntry(NamedTuple):
439 # pylint: disable=invalid-name
440 """
441 An entry in the trust message cache used by ATM.
442 """
443
444 sender_jid: jid.JID
445 sender_key: bytes
446 timestamp: datetime
447 trust_update: TrustUpdate
448
449
450 class PartialTrustMessage(NamedTuple):
451 # pylint: disable=invalid-name
452 """
453 A structure representing a partial trust message, used by :func:`send_trust_messages`
454 to build trust messages.
455 """
456
457 recipient_jid: jid.JID
458 updated_jid: jid.JID
459 trust_updates: FrozenSet[TrustUpdate]
460
461
462 async def manage_trust_message_cache(
463 client: SatXMPPClient,
464 session_manager: omemo.SessionManager,
465 applied_trust_updates: FrozenSet[TrustUpdate]
466 ) -> None:
467 """Manage the ATM trust message cache after trust updates have been applied.
468
469 @param client: The client this operation runs under.
470 @param session_manager: The session manager to use.
471 @param applied_trust_updates: The trust updates that have already been applied,
472 triggering this cache management run.
473 """
474
475 trust_message_cache = persistent.LazyPersistentBinaryDict(
476 "XEP-0384/TM",
477 client.profile
478 )
479
480 # Load cache entries
481 cache_entries = cast(
482 Set[TrustMessageCacheEntry],
483 await trust_message_cache.get("cache", set())
484 )
485
486 # Expire cache entries that were overwritten by the applied trust updates
487 cache_entries_by_target = {
488 (
489 cache_entry.trust_update.target_jid.userhostJID(),
490 cache_entry.trust_update.target_key
491 ): cache_entry
492 for cache_entry
493 in cache_entries
494 }
495
496 for trust_update in applied_trust_updates:
497 cache_entry = cache_entries_by_target.get(
498 (trust_update.target_jid.userhostJID(), trust_update.target_key),
499 None
500 )
501
502 if cache_entry is not None:
503 cache_entries.remove(cache_entry)
504
505 # Apply cached Trust Messages by newly trusted devices
506 new_trust_updates: Set[TrustUpdate] = set()
507
508 for trust_update in applied_trust_updates:
509 if trust_update.target_trust:
510 # Iterate over a copy such that cache_entries can be modified
511 for cache_entry in set(cache_entries):
512 if (
513 cache_entry.sender_jid.userhostJID()
514 == trust_update.target_jid.userhostJID()
515 and cache_entry.sender_key == trust_update.target_key
516 ):
517 trust_level = (
518 TrustLevel.TRUSTED
519 if cache_entry.trust_update.target_trust
520 else TrustLevel.DISTRUSTED
521 )
522
523 # Apply the trust update
524 await session_manager.set_trust(
525 cache_entry.trust_update.target_jid.userhost(),
526 cache_entry.trust_update.target_key,
527 trust_level.name
528 )
529
530 # Track the fact that this trust update has been applied
531 new_trust_updates.add(cache_entry.trust_update)
532
533 # Remove the corresponding cache entry
534 cache_entries.remove(cache_entry)
535
536 # Store the updated cache entries
537 await trust_message_cache.force("cache", cache_entries)
538
539 # TODO: Notify the user ("feedback") about automatically updated trust?
540
541 if len(new_trust_updates) > 0:
542 # If any trust has been updated, recursively perform another run of cache
543 # management
544 await manage_trust_message_cache(
545 client,
546 session_manager,
547 frozenset(new_trust_updates)
548 )
549
550
551 async def get_trust_as_trust_updates(
552 session_manager: omemo.SessionManager,
553 target_jid: jid.JID
554 ) -> FrozenSet[TrustUpdate]:
555 """Get the trust status of all known keys of a JID as trust updates for use with ATM.
556
557 @param session_manager: The session manager to load the trust from.
558 @param target_jid: The JID to load the trust for.
559 @return: The trust updates encoding the trust status of all known keys of the JID that
560 are either explicitly trusted or distrusted. Undecided keys are not included in
561 the trust updates.
562 """
563
564 devices = await session_manager.get_device_information(target_jid.userhost())
565
566 trust_updates: Set[TrustUpdate] = set()
567
568 for device in devices:
569 trust_level = TrustLevel(device.trust_level_name)
570 target_trust: bool
571
572 if trust_level is TrustLevel.TRUSTED:
573 target_trust = True
574 elif trust_level is TrustLevel.DISTRUSTED:
575 target_trust = False
576 else:
577 # Skip devices that are not explicitly trusted or distrusted
578 continue
579
580 trust_updates.add(TrustUpdate(
581 target_jid=target_jid.userhostJID(),
582 target_key=device.identity_key,
583 target_trust=target_trust
584 ))
585
586 return frozenset(trust_updates)
587
588
589 async def send_trust_messages(
590 client: SatXMPPClient,
591 session_manager: omemo.SessionManager,
592 applied_trust_updates: FrozenSet[TrustUpdate]
593 ) -> None:
594 """Send information about updated trust to peers via ATM (XEP-0450).
595
596 @param client: The client.
597 @param session_manager: The session manager.
598 @param applied_trust_updates: The trust updates that have already been applied, to
599 notify other peers about.
600 """
601 # NOTE: This currently sends information about oldmemo trust too. This is not
602 # specified and experimental, but since twomemo and oldmemo share the same identity
603 # keys and trust systems, this could be a cool side effect.
604
605 # Send Trust Messages for newly trusted and distrusted devices
606 own_jid = client.jid.userhostJID()
607 own_trust_updates = await get_trust_as_trust_updates(session_manager, own_jid)
608
609 # JIDs of which at least one device's trust has been updated
610 updated_jids = frozenset({
611 trust_update.target_jid.userhostJID()
612 for trust_update
613 in applied_trust_updates
614 })
615
616 trust_messages: Set[PartialTrustMessage] = set()
617
618 for updated_jid in updated_jids:
619 # Get the trust updates for that JID
620 trust_updates = frozenset({
621 trust_update for trust_update in applied_trust_updates
622 if trust_update.target_jid.userhostJID() == updated_jid
623 })
624
625 if updated_jid == own_jid:
626 # If the own JID is updated, _all_ peers have to be notified
627 # TODO: Using my author's privilege here to shamelessly access private fields
628 # and storage keys until I've added public API to get a list of peers to
629 # python-omemo.
630 storage: omemo.Storage = getattr(session_manager, "_SessionManager__storage")
631 peer_jids = frozenset({
632 jid.JID(bare_jid).userhostJID() for bare_jid in (await storage.load_list(
633 f"/{OMEMO.NS_TWOMEMO}/bare_jids",
634 str
635 )).maybe([])
636 })
637
638 if len(peer_jids) == 0:
639 # If there are no peers to notify, notify our other devices about the
640 # changes directly
641 trust_messages.add(PartialTrustMessage(
642 recipient_jid=own_jid,
643 updated_jid=own_jid,
644 trust_updates=trust_updates
645 ))
646 else:
647 # Otherwise, notify all peers about the changes in trust and let carbons
648 # handle the copy to our own JID
649 for peer_jid in peer_jids:
650 trust_messages.add(PartialTrustMessage(
651 recipient_jid=peer_jid,
652 updated_jid=own_jid,
653 trust_updates=trust_updates
654 ))
655
656 # Also send full trust information about _every_ peer to our newly
657 # trusted devices
658 peer_trust_updates = \
659 await get_trust_as_trust_updates(session_manager, peer_jid)
660
661 trust_messages.add(PartialTrustMessage(
662 recipient_jid=own_jid,
663 updated_jid=peer_jid,
664 trust_updates=peer_trust_updates
665 ))
666
667 # Send information about our own devices to our newly trusted devices
668 trust_messages.add(PartialTrustMessage(
669 recipient_jid=own_jid,
670 updated_jid=own_jid,
671 trust_updates=own_trust_updates
672 ))
673 else:
674 # Notify our other devices about the changes in trust
675 trust_messages.add(PartialTrustMessage(
676 recipient_jid=own_jid,
677 updated_jid=updated_jid,
678 trust_updates=trust_updates
679 ))
680
681 # Send a summary of our own trust to newly trusted devices
682 trust_messages.add(PartialTrustMessage(
683 recipient_jid=updated_jid,
684 updated_jid=own_jid,
685 trust_updates=own_trust_updates
686 ))
687
688 # All trust messages prepared. Merge all trust messages directed at the same
689 # recipient.
690 recipient_jids = { trust_message.recipient_jid for trust_message in trust_messages }
691
692 for recipient_jid in recipient_jids:
693 updated: Dict[jid.JID, Set[TrustUpdate]] = {}
694
695 for trust_message in trust_messages:
696 # Merge trust messages directed at that recipient
697 if trust_message.recipient_jid == recipient_jid:
698 # Merge the trust updates
699 updated[trust_message.updated_jid] = \
700 updated.get(trust_message.updated_jid, set())
701
702 updated[trust_message.updated_jid] |= trust_message.trust_updates
703
704 # Build the trust message
705 trust_message_elt = domish.Element((NS_TM, "trust-message"))
706 trust_message_elt["usage"] = NS_ATM
707 trust_message_elt["encryption"] = twomemo.twomemo.NAMESPACE
708
709 for updated_jid, trust_updates in updated.items():
710 key_owner_elt = trust_message_elt.addElement((NS_TM, "key-owner"))
711 key_owner_elt["jid"] = updated_jid.userhost()
712
713 for trust_update in trust_updates:
714 serialized_identity_key = \
715 base64.b64encode(trust_update.target_key).decode("ASCII")
716
717 if trust_update.target_trust:
718 key_owner_elt.addElement(
719 (NS_TM, "trust"),
720 content=serialized_identity_key
721 )
722 else:
723 key_owner_elt.addElement(
724 (NS_TM, "distrust"),
725 content=serialized_identity_key
726 )
727
728 # Finally, encrypt and send the trust message!
729 message_data = client.generateMessageXML(MessageData({
730 "from": own_jid,
731 "to": recipient_jid,
732 "uid": str(uuid.uuid4()),
733 "message": {},
734 "subject": {},
735 "type": C.MESS_TYPE_CHAT,
736 "extra": {},
737 "timestamp": time.time()
738 }))
739
740 message_data["xml"].addChild(trust_message_elt)
741
742 plaintext = XEP_0420.pack_stanza(TM_SCE_PROFILE, message_data["xml"])
743
744 feedback_jid = recipient_jid
745
746 # TODO: The following is mostly duplicate code
747 try:
748 messages, encryption_errors = await session_manager.encrypt(
749 frozenset({ own_jid.userhost(), recipient_jid.userhost() }),
750 { OMEMO.NS_TWOMEMO: plaintext },
751 backend_priority_order=[ OMEMO.NS_TWOMEMO ],
752 identifier=feedback_jid.userhost()
753 )
754 except Exception as e:
755 msg = _(
756 # pylint: disable=consider-using-f-string
757 "Can't encrypt message for {entities}: {reason}".format(
758 entities=', '.join({ own_jid.userhost(), recipient_jid.userhost() }),
759 reason=e
760 )
761 )
762 log.warning(msg)
763 client.feedback(feedback_jid, msg, {
764 C.MESS_EXTRA_INFO: C.EXTRA_INFO_ENCR_ERR
765 })
766 raise e
767
768 if len(encryption_errors) > 0:
769 log.warning(
770 f"Ignored the following non-critical encryption errors:"
771 f" {encryption_errors}"
772 )
773
774 encrypted_errors_stringified = ", ".join([
775 f"device {err.device_id} of {err.bare_jid} under namespace"
776 f" {err.namespace}"
777 for err
778 in encryption_errors
779 ])
780
781 client.feedback(
782 feedback_jid,
783 D_(
784 "There were non-critical errors during encryption resulting in some"
785 " of your destinees' devices potentially not receiving the message."
786 " This happens when the encryption data/key material of a device is"
787 " incomplete or broken, which shouldn't happen for actively used"
788 " devices, and can usually be ignored. The following devices are"
789 f" affected: {encrypted_errors_stringified}."
790 )
791 )
792
793 message = next(
794 message for message in messages
795 if message.namespace == OMEMO.NS_TWOMEMO
796 )
797
798 # Add the encrypted element
799 message_data["xml"].addChild(xml_tools.et_elt_2_domish_elt(
800 twomemo.etree.serialize_message(message)
801 ))
802
803 await client.a_send(message_data["xml"])
448 804
449 805
450 def make_session_manager(sat: SAT, profile: str) -> Type[omemo.SessionManager]: 806 def make_session_manager(sat: SAT, profile: str) -> Type[omemo.SessionManager]:
451 """ 807 """
452 @param sat: The SAT instance. 808 @param sat: The SAT instance.
703 f" {namespace}" 1059 f" {namespace}"
704 ) from e 1060 ) from e
705 1061
706 if len(items) == 0: 1062 if len(items) == 0:
707 return {} 1063 return {}
708 elif len(items) != 1: 1064
1065 if len(items) != 1:
709 raise omemo.DeviceListDownloadFailed( 1066 raise omemo.DeviceListDownloadFailed(
710 f"Device list download failed for {bare_jid} under namespace" 1067 f"Device list download failed for {bare_jid} under namespace"
711 f" {namespace}: Unexpected number of items retrieved: {len(items)}." 1068 f" {namespace}: Unexpected number of items retrieved: {len(items)}."
712 ) 1069 )
713 1070
714 element = next(iter(xml_tools.domish_elt_2_et_elt(cast(domish.Element, items[0]))), None) 1071 element = next(
1072 iter(xml_tools.domish_elt_2_et_elt(cast(domish.Element, items[0]))),
1073 None
1074 )
1075
715 if element is None: 1076 if element is None:
716 raise omemo.DeviceListDownloadFailed( 1077 raise omemo.DeviceListDownloadFailed(
717 f"Device list download failed for {bare_jid} under namespace" 1078 f"Device list download failed for {bare_jid} under namespace"
718 f" {namespace}: Item download succeeded but parsing failed:" 1079 f" {namespace}: Item download succeeded but parsing failed:"
719 f" {element}." 1080 f" {element}."
730 f" {namespace}" 1091 f" {namespace}"
731 ) from e 1092 ) from e
732 1093
733 raise omemo.UnknownNamespace(f"Unknown namespace: {namespace}") 1094 raise omemo.UnknownNamespace(f"Unknown namespace: {namespace}")
734 1095
735 @staticmethod 1096 async def _evaluate_custom_trust_level(
736 def _evaluate_custom_trust_level(trust_level_name: str) -> omemo.TrustLevel: 1097 self,
1098 device: omemo.DeviceInformation
1099 ) -> omemo.TrustLevel:
1100 # Get the custom trust level
737 try: 1101 try:
738 return TrustLevel(trust_level_name).to_omemo_trust_level() 1102 trust_level = TrustLevel(device.trust_level_name)
739 except ValueError as e: 1103 except ValueError as e:
740 raise omemo.UnknownTrustLevel( 1104 raise omemo.UnknownTrustLevel(
741 f"Unknown trust level name {trust_level_name}" 1105 f"Unknown trust level name {device.trust_level_name}"
742 ) from e 1106 ) from e
1107
1108 # The first three cases are a straight-forward mapping
1109 if trust_level is TrustLevel.TRUSTED:
1110 return omemo.TrustLevel.TRUSTED
1111 if trust_level is TrustLevel.UNDECIDED:
1112 return omemo.TrustLevel.UNDECIDED
1113 if trust_level is TrustLevel.DISTRUSTED:
1114 return omemo.TrustLevel.DISTRUSTED
1115
1116 # The blindly trusted case is more complicated, since its evaluation depends
1117 # on the trust system and phase
1118 if trust_level is TrustLevel.BLINDLY_TRUSTED:
1119 # Get the name of the active trust system
1120 trust_system = cast(str, sat.memory.getParamA(
1121 PARAM_NAME,
1122 PARAM_CATEGORY,
1123 profile_key=profile
1124 ))
1125
1126 # If the trust model is BTBV, blind trust is always enabled
1127 if trust_system == "btbv":
1128 return omemo.TrustLevel.TRUSTED
1129
1130 # If the trust model is ATM, blind trust is disabled in the second phase
1131 # and counts as undecided
1132 if trust_system == "atm":
1133 # Find out whether we are in phase one or two
1134 devices = await self.get_device_information(device.bare_jid)
1135
1136 phase_one = all(TrustLevel(device.trust_level_name) in {
1137 TrustLevel.UNDECIDED,
1138 TrustLevel.BLINDLY_TRUSTED
1139 } for device in devices)
1140
1141 if phase_one:
1142 return omemo.TrustLevel.TRUSTED
1143
1144 return omemo.TrustLevel.UNDECIDED
1145
1146 raise exceptions.InternalError(
1147 f"Unknown trust system active: {trust_system}"
1148 )
1149
1150 assert_never(trust_level)
743 1151
744 async def _make_trust_decision( 1152 async def _make_trust_decision(
745 self, 1153 self,
746 undecided: FrozenSet[omemo.DeviceInformation], 1154 undecided: FrozenSet[omemo.DeviceInformation],
747 identifier: Optional[str] 1155 identifier: Optional[str]
752 ) 1160 )
753 1161
754 # The feedback JID is transferred via the identifier 1162 # The feedback JID is transferred via the identifier
755 feedback_jid = jid.JID(identifier).userhostJID() 1163 feedback_jid = jid.JID(identifier).userhostJID()
756 1164
757 # Get the name of the trust model to use 1165 # Both the ATM and the BTBV trust models work with blind trust before the
758 trust_model = cast(str, sat.memory.getParamA( 1166 # first manual verification is performed. Thus, we can separate bare JIDs into
759 PARAM_NAME, 1167 # two pools here, one pool of bare JIDs for which blind trust is active, and
760 PARAM_CATEGORY, 1168 # one pool of bare JIDs for which manual trust is used instead.
761 profile_key=cast(str, client.profile)
762 ))
763
764 # Under the BTBV trust model, if at least one device of a bare JID is manually
765 # trusted or distrusted, the trust model is "downgraded" to manual trust.
766 # Thus, we can separate bare JIDs into two pools here, one pool of bare JIDs
767 # for which BTBV is active, and one pool of bare JIDs for which manual trust
768 # is used.
769 bare_jids = { device.bare_jid for device in undecided } 1169 bare_jids = { device.bare_jid for device in undecided }
770 1170
771 btbv_bare_jids: Set[str] = set() 1171 blind_trust_bare_jids: Set[str] = set()
772 manual_trust_bare_jids: Set[str] = set() 1172 manual_trust_bare_jids: Set[str] = set()
773 1173
774 if trust_model == "btbv": 1174 # For each bare JID, decide whether blind trust applies
775 # For each bare JID, decide whether BTBV or manual trust applies 1175 for bare_jid in bare_jids:
776 for bare_jid in bare_jids: 1176 # Get all known devices belonging to the bare JID
777 # Get all known devices belonging to the bare JID 1177 devices = await self.get_device_information(bare_jid)
778 devices = await self.get_device_information(bare_jid) 1178
779 1179 # If the trust levels of all devices correspond to those used by blind
780 # If the trust levels of all devices correspond to those used by BTBV, 1180 # trust, blind trust applies. Otherwise, fall back to manual trust.
781 # BTBV applies. Otherwise, fall back to manual trust. 1181 if all(TrustLevel(device.trust_level_name) in {
782 if all(TrustLevel(device.trust_level_name) in { 1182 TrustLevel.UNDECIDED,
783 TrustLevel.UNDECIDED, 1183 TrustLevel.BLINDLY_TRUSTED
784 TrustLevel.BLINDLY_TRUSTED 1184 } for device in devices):
785 } for device in devices): 1185 blind_trust_bare_jids.add(bare_jid)
786 btbv_bare_jids.add(bare_jid) 1186 else:
787 else: 1187 manual_trust_bare_jids.add(bare_jid)
788 manual_trust_bare_jids.add(bare_jid)
789
790 if trust_model == "manual":
791 manual_trust_bare_jids = bare_jids
792 1188
793 # With the JIDs sorted into their respective pools, the undecided devices can 1189 # With the JIDs sorted into their respective pools, the undecided devices can
794 # be categorized too 1190 # be categorized too
795 blindly_trusted_devices = \ 1191 blindly_trusted_devices = \
796 { dev for dev in undecided if dev.bare_jid in btbv_bare_jids } 1192 { dev for dev in undecided if dev.bare_jid in blind_trust_bare_jids }
797 manually_trusted_devices = \ 1193 manually_trusted_devices = \
798 { dev for dev in undecided if dev.bare_jid in manual_trust_bare_jids } 1194 { dev for dev in undecided if dev.bare_jid in manual_trust_bare_jids }
799 1195
800 # Blindly trust devices handled by BTBV 1196 # Blindly trust devices handled by blind trust
801 if len(blindly_trusted_devices) > 0: 1197 if len(blindly_trusted_devices) > 0:
802 for device in blindly_trusted_devices: 1198 for device in blindly_trusted_devices:
803 await self.set_trust( 1199 await self.set_trust(
804 device.bare_jid, 1200 device.bare_jid,
805 device.identity_key, 1201 device.identity_key,
815 1211
816 client.feedback( 1212 client.feedback(
817 feedback_jid, 1213 feedback_jid,
818 D_( 1214 D_(
819 "Not all destination devices are trusted, unknown devices will be" 1215 "Not all destination devices are trusted, unknown devices will be"
820 " blindly trusted due to the Blind Trust Before Verification" 1216 " blindly trusted.\nFollowing devices have been automatically"
821 " policy. If you want a more secure workflow, please activate the" 1217 f" trusted: {blindly_trusted_devices_stringified}."
822 " \"manual\" policy in the settings' \"Security\" tab.\nFollowing"
823 " devices have been automatically trusted:"
824 f" {blindly_trusted_devices_stringified}."
825 ) 1218 )
826 ) 1219 )
827 1220
828 # Prompt the user for manual trust decisions on the devices handled by manual 1221 # Prompt the user for manual trust decisions on the devices handled by manual
829 # trust 1222 # trust
852 element = oldmemo.etree.serialize_message(message) 1245 element = oldmemo.etree.serialize_message(message)
853 1246
854 if element is None: 1247 if element is None:
855 raise omemo.UnknownNamespace(f"Unknown namespace: {message.namespace}") 1248 raise omemo.UnknownNamespace(f"Unknown namespace: {message.namespace}")
856 1249
857 # TODO: Untested
858 message_data = client.generateMessageXML(MessageData({ 1250 message_data = client.generateMessageXML(MessageData({
859 "from": client.jid, 1251 "from": client.jid,
860 "to": jid.JID(bare_jid), 1252 "to": jid.JID(bare_jid),
861 "uid": str(uuid.uuid4()), 1253 "uid": str(uuid.uuid4()),
862 "message": {}, 1254 "message": {},
957 1349
958 data_form_result = cast(Dict[str, str], xml_tools.XMLUIResult2DataFormResult( 1350 data_form_result = cast(Dict[str, str], xml_tools.XMLUIResult2DataFormResult(
959 trust_ui_result 1351 trust_ui_result
960 )) 1352 ))
961 1353
1354 trust_updates: Set[TrustUpdate] = set()
1355
962 for key, value in data_form_result.items(): 1356 for key, value in data_form_result.items():
963 if not key.startswith("trust_"): 1357 if not key.startswith("trust_"):
964 continue 1358 continue
965 1359
966 device = undecided_ordered[int(key[len("trust_"):])] 1360 device = undecided_ordered[int(key[len("trust_"):])]
967 trust = C.bool(value) 1361 target_trust = C.bool(value)
1362 trust_level = \
1363 TrustLevel.TRUSTED if target_trust else TrustLevel.DISTRUSTED
968 1364
969 await self.set_trust( 1365 await self.set_trust(
970 device.bare_jid, 1366 device.bare_jid,
971 device.identity_key, 1367 device.identity_key,
972 TrustLevel.TRUSTED.name if trust else TrustLevel.DISTRUSTED.name 1368 trust_level.name
973 ) 1369 )
1370
1371 trust_updates.add(TrustUpdate(
1372 target_jid=jid.JID(device.bare_jid).userhostJID(),
1373 target_key=device.identity_key,
1374 target_trust=target_trust
1375 ))
1376
1377 # Check whether ATM is enabled and handle everything in case it is
1378 trust_system = cast(str, sat.memory.getParamA(
1379 PARAM_NAME,
1380 PARAM_CATEGORY,
1381 profile_key=profile
1382 ))
1383
1384 if trust_system == "atm":
1385 await manage_trust_message_cache(client, self, frozenset(trust_updates))
1386 await send_trust_messages(client, self, frozenset(trust_updates))
974 1387
975 return SessionManagerImpl 1388 return SessionManagerImpl
976 1389
977 1390
978 async def prepare_for_profile( 1391 async def prepare_for_profile(
1084 <individual> 1497 <individual>
1085 <category name="{PARAM_CATEGORY}" label={quoteattr(D_('Security'))}> 1498 <category name="{PARAM_CATEGORY}" label={quoteattr(D_('Security'))}>
1086 <param name="{PARAM_NAME}" 1499 <param name="{PARAM_NAME}"
1087 label={quoteattr(D_('OMEMO default trust policy'))} 1500 label={quoteattr(D_('OMEMO default trust policy'))}
1088 type="list" security="3"> 1501 type="list" security="3">
1089 <option value="manual" label={quoteattr(D_('Manual trust (more secure)'))} /> 1502 <option value="atm"
1503 label={quoteattr(D_('Automatic Trust Management (more secure)'))} />
1090 <option value="btbv" 1504 <option value="btbv"
1091 label={quoteattr(D_('Blind Trust Before Verification (more user friendly)'))} 1505 label={quoteattr(D_('Blind Trust Before Verification (more user friendly)'))}
1092 selected="true" /> 1506 selected="true" />
1093 </param> 1507 </param>
1094 </category> 1508 </category>
1101 """ 1515 """
1102 Plugin equipping Libervia with OMEMO capabilities under the (modern) 1516 Plugin equipping Libervia with OMEMO capabilities under the (modern)
1103 ``urn:xmpp:omemo:2`` namespace and the (legacy) ``eu.siacs.conversations.axolotl`` 1517 ``urn:xmpp:omemo:2`` namespace and the (legacy) ``eu.siacs.conversations.axolotl``
1104 namespace. Both versions of the protocol are handled by this plugin and compatibility 1518 namespace. Both versions of the protocol are handled by this plugin and compatibility
1105 between the two is maintained. MUC messages are supported next to one to one messages. 1519 between the two is maintained. MUC messages are supported next to one to one messages.
1106 For trust management, the two trust models "BTBV" and "manual" are supported. 1520 For trust management, the two trust models "ATM" and "BTBV" are supported.
1107 """ 1521 """
1108 NS_TWOMEMO = twomemo.twomemo.NAMESPACE 1522 NS_TWOMEMO = twomemo.twomemo.NAMESPACE
1109 NS_OLDMEMO = oldmemo.oldmemo.NAMESPACE 1523 NS_OLDMEMO = oldmemo.oldmemo.NAMESPACE
1110 1524
1111 # For MUC/MIX message stanzas, the <to/> affix is a MUST 1525 # For MUC/MIX message stanzas, the <to/> affix is a MUST
1161 1575
1162 # Calls waiting for a specific session manager to be built 1576 # Calls waiting for a specific session manager to be built
1163 self.__session_manager_waiters: Dict[str, List[defer.Deferred]] = {} 1577 self.__session_manager_waiters: Dict[str, List[defer.Deferred]] = {}
1164 1578
1165 # These triggers are used by oldmemo, which doesn't do SCE and only applies to 1579 # These triggers are used by oldmemo, which doesn't do SCE and only applies to
1166 # messages 1580 # messages. Temporarily, until a more fitting trigger for SCE-based encryption is
1581 # added, the messageReceived trigger is also used for twomemo.
1167 sat.trigger.add( 1582 sat.trigger.add(
1168 "messageReceived", 1583 "messageReceived",
1169 self.__message_received_trigger, 1584 self.__message_received_trigger,
1170 priority=100050 1585 priority=100050
1171 ) 1586 )
1296 in bare_jids 1711 in bare_jids
1297 ]), key=lambda device: device.bare_jid) 1712 ]), key=lambda device: device.bare_jid)
1298 1713
1299 async def callback( 1714 async def callback(
1300 data: Any, 1715 data: Any,
1301 profile: str # pylint: disable=unused-argument 1716 profile: str
1302 ) -> Dict[Never, Never]: 1717 ) -> Dict[Never, Never]:
1303 """ 1718 """
1304 @param data: The XMLUI result produces by the trust UI form. 1719 @param data: The XMLUI result produces by the trust UI form.
1305 @param profile: The profile. 1720 @param profile: The profile.
1306 @return: An empty dictionary. The type of the return value was chosen 1721 @return: An empty dictionary. The type of the return value was chosen
1312 1727
1313 data_form_result = cast( 1728 data_form_result = cast(
1314 Dict[str, str], 1729 Dict[str, str],
1315 xml_tools.XMLUIResult2DataFormResult(data) 1730 xml_tools.XMLUIResult2DataFormResult(data)
1316 ) 1731 )
1732
1733 trust_updates: Set[TrustUpdate] = set()
1734
1317 for key, value in data_form_result.items(): 1735 for key, value in data_form_result.items():
1318 if not key.startswith("trust_"): 1736 if not key.startswith("trust_"):
1319 continue 1737 continue
1320 1738
1321 device = devices[int(key[len("trust_"):])] 1739 device = devices[int(key[len("trust_"):])]
1322 trust = TrustLevel(value) 1740 trust_level_name = value
1323 1741
1324 if TrustLevel(device.trust_level_name) is not trust: 1742 if device.trust_level_name != trust_level_name:
1325 await session_manager.set_trust( 1743 await session_manager.set_trust(
1326 device.bare_jid, 1744 device.bare_jid,
1327 device.identity_key, 1745 device.identity_key,
1328 value 1746 trust_level_name
1747 )
1748
1749 target_trust: Optional[bool] = None
1750
1751 if TrustLevel(trust_level_name) is TrustLevel.TRUSTED:
1752 target_trust = True
1753 if TrustLevel(trust_level_name) is TrustLevel.DISTRUSTED:
1754 target_trust = False
1755
1756 if target_trust is not None:
1757 trust_updates.add(TrustUpdate(
1758 target_jid=jid.JID(device.bare_jid).userhostJID(),
1759 target_key=device.identity_key,
1760 target_trust=target_trust
1761 ))
1762
1763 # Check whether ATM is enabled and handle everything in case it is
1764 trust_system = cast(str, self.__sat.memory.getParamA(
1765 PARAM_NAME,
1766 PARAM_CATEGORY,
1767 profile_key=profile
1768 ))
1769
1770 if trust_system == "atm":
1771 if len(trust_updates) > 0:
1772 await manage_trust_message_cache(
1773 client,
1774 session_manager,
1775 frozenset(trust_updates)
1776 )
1777
1778 await send_trust_messages(
1779 client,
1780 session_manager,
1781 frozenset(trust_updates)
1329 ) 1782 )
1330 1783
1331 return {} 1784 return {}
1332 1785
1333 submit_id = self.__sat.registerCallback(callback, with_data=True, one_shot=True) 1786 submit_id = self.__sat.registerCallback(callback, with_data=True, one_shot=True)
1339 ) 1792 )
1340 # Casting this to Any, otherwise all calls on the variable cause type errors 1793 # Casting this to Any, otherwise all calls on the variable cause type errors
1341 # pylint: disable=no-member 1794 # pylint: disable=no-member
1342 trust_ui = cast(Any, result) 1795 trust_ui = cast(Any, result)
1343 trust_ui.addText(D_( 1796 trust_ui.addText(D_(
1344 "This is OMEMO trusting system. You'll see below the devices of your " 1797 "This is OMEMO trusting system. You'll see below the devices of your"
1345 "contacts, and a list selection to trust them or not. A trusted device " 1798 " contacts, and a list selection to trust them or not. A trusted device"
1346 "can read your messages in plain text, so be sure to only validate " 1799 " can read your messages in plain text, so be sure to only validate"
1347 "devices that you are sure are belonging to your contact. It's better " 1800 " devices that you are sure are belonging to your contact. It's better"
1348 "to do this when you are next to your contact and their device, so " 1801 " to do this when you are next to your contact and their device, so"
1349 "you can check the \"fingerprint\" (the number next to the device) " 1802 " you can check the \"fingerprint\" (the number next to the device)"
1350 "yourself. Do *not* validate a device if the fingerprint is wrong!" 1803 " yourself. Do *not* validate a device if the fingerprint is wrong!"
1804 " Note that manually validating a fingerprint disables any form of automatic"
1805 " trust."
1351 )) 1806 ))
1352 1807
1353 own_device, __ = await session_manager.get_own_device_information() 1808 own_device, __ = await session_manager.get_own_device_information()
1354 1809
1355 trust_ui.changeContainer("label") 1810 trust_ui.changeContainer("label")
1488 for waiter in self.__session_manager_waiters[profile]: 1943 for waiter in self.__session_manager_waiters[profile]:
1489 waiter.callback(session_manager) 1944 waiter.callback(session_manager)
1490 del self.__session_manager_waiters[profile] 1945 del self.__session_manager_waiters[profile]
1491 1946
1492 return session_manager 1947 return session_manager
1948
1949 async def __message_received_trigger_atm(
1950 self,
1951 client: SatXMPPClient,
1952 message_elt: domish.Element,
1953 session_manager: omemo.SessionManager,
1954 sender_device_information: omemo.DeviceInformation,
1955 timestamp: datetime
1956 ) -> None:
1957 """Check a newly decrypted message stanza for ATM content and perform ATM in case.
1958
1959 @param client: The client which received the message.
1960 @param message_elt: The message element. Can be modified.
1961 @param session_manager: The session manager.
1962 @param sender_device_information: Information about the device that sent/encrypted
1963 the message.
1964 @param timestamp: Timestamp extracted from the SCE time affix.
1965 """
1966
1967 trust_message_cache = persistent.LazyPersistentBinaryDict(
1968 "XEP-0384/TM",
1969 client.profile
1970 )
1971
1972 new_cache_entries: Set[TrustMessageCacheEntry] = set()
1973
1974 for trust_message_elt in message_elt.elements(NS_TM, "trust-message"):
1975 assert isinstance(trust_message_elt, domish.Element)
1976
1977 try:
1978 TRUST_MESSAGE_SCHEMA.validate(trust_message_elt.toXml())
1979 except xmlschema.XMLSchemaValidationError as e:
1980 raise exceptions.ParsingError(
1981 "<trust-message/> element doesn't pass schema validation."
1982 ) from e
1983
1984 if trust_message_elt["usage"] != NS_ATM:
1985 # Skip non-ATM trust message
1986 continue
1987
1988 if trust_message_elt["encryption"] != OMEMO.NS_TWOMEMO:
1989 # Skip non-twomemo trust message
1990 continue
1991
1992 for key_owner_elt in trust_message_elt.elements(NS_TM, "key-owner"):
1993 assert isinstance(key_owner_elt, domish.Element)
1994
1995 key_owner_jid = jid.JID(key_owner_elt["jid"]).userhostJID()
1996
1997 for trust_elt in key_owner_elt.elements(NS_TM, "trust"):
1998 assert isinstance(trust_elt, domish.Element)
1999
2000 new_cache_entries.add(TrustMessageCacheEntry(
2001 sender_jid=jid.JID(sender_device_information.bare_jid),
2002 sender_key=sender_device_information.identity_key,
2003 timestamp=timestamp,
2004 trust_update=TrustUpdate(
2005 target_jid=key_owner_jid,
2006 target_key=base64.b64decode(str(trust_elt)),
2007 target_trust=True
2008 )
2009 ))
2010
2011 for distrust_elt in key_owner_elt.elements(NS_TM, "distrust"):
2012 assert isinstance(distrust_elt, domish.Element)
2013
2014 new_cache_entries.add(TrustMessageCacheEntry(
2015 sender_jid=jid.JID(sender_device_information.bare_jid),
2016 sender_key=sender_device_information.identity_key,
2017 timestamp=timestamp,
2018 trust_update=TrustUpdate(
2019 target_jid=key_owner_jid,
2020 target_key=base64.b64decode(str(distrust_elt)),
2021 target_trust=False
2022 )
2023 ))
2024
2025 # Load existing cache entries
2026 existing_cache_entries = cast(
2027 Set[TrustMessageCacheEntry],
2028 await trust_message_cache.get("cache", set())
2029 )
2030
2031 # Discard cache entries by timestamp comparison
2032 existing_by_target = {
2033 (
2034 cache_entry.trust_update.target_jid.userhostJID(),
2035 cache_entry.trust_update.target_key
2036 ): cache_entry
2037 for cache_entry
2038 in existing_cache_entries
2039 }
2040
2041 # Iterate over a copy here, such that new_cache_entries can be modified
2042 for new_cache_entry in set(new_cache_entries):
2043 existing_cache_entry = existing_by_target.get(
2044 (
2045 new_cache_entry.trust_update.target_jid.userhostJID(),
2046 new_cache_entry.trust_update.target_key
2047 ),
2048 None
2049 )
2050
2051 if existing_cache_entry is not None:
2052 if existing_cache_entry.timestamp > new_cache_entry.timestamp:
2053 # If the existing cache entry is newer than the new cache entry,
2054 # discard the new one in favor of the existing one
2055 new_cache_entries.remove(new_cache_entry)
2056 else:
2057 # Otherwise, discard the existing cache entry. This includes the case
2058 # when both cache entries have matching timestamps.
2059 existing_cache_entries.remove(existing_cache_entry)
2060
2061 # If the sending device is trusted, apply the new cache entries
2062 applied_trust_updates: Set[TrustUpdate] = set()
2063
2064 if TrustLevel(sender_device_information.trust_level_name) is TrustLevel.TRUSTED:
2065 # Iterate over a copy such that new_cache_entries can be modified
2066 for cache_entry in set(new_cache_entries):
2067 trust_update = cache_entry.trust_update
2068
2069 trust_level = (
2070 TrustLevel.TRUSTED
2071 if trust_update.target_trust
2072 else TrustLevel.DISTRUSTED
2073 )
2074
2075 await session_manager.set_trust(
2076 trust_update.target_jid.userhost(),
2077 trust_update.target_key,
2078 trust_level.name
2079 )
2080
2081 applied_trust_updates.add(trust_update)
2082
2083 new_cache_entries.remove(cache_entry)
2084
2085 # Store the remaining existing and new cache entries
2086 await trust_message_cache.force(
2087 "cache",
2088 existing_cache_entries | new_cache_entries
2089 )
2090
2091 # If the trust of at least one device was modified, run the ATM cache update logic
2092 if len(applied_trust_updates) > 0:
2093 await manage_trust_message_cache(
2094 client,
2095 session_manager,
2096 frozenset(applied_trust_updates)
2097 )
1493 2098
1494 async def __message_received_trigger( 2099 async def __message_received_trigger(
1495 self, 2100 self,
1496 client: SatXMPPClient, 2101 client: SatXMPPClient,
1497 message_elt: domish.Element, 2102 message_elt: domish.Element,
1504 message has fully progressed through the message receiving flow. Can be used 2109 message has fully progressed through the message receiving flow. Can be used
1505 to apply treatments to the fully processed message, like marking it as 2110 to apply treatments to the fully processed message, like marking it as
1506 encrypted. 2111 encrypted.
1507 @return: Whether to continue the message received flow. 2112 @return: Whether to continue the message received flow.
1508 """ 2113 """
2114
1509 muc_plaintext_cache_key: Optional[MUCPlaintextCacheKey] = None 2115 muc_plaintext_cache_key: Optional[MUCPlaintextCacheKey] = None
1510 2116
1511 sender_jid = jid.JID(message_elt["from"]) 2117 sender_jid = jid.JID(message_elt["from"])
1512 feedback_jid: jid.JID 2118 feedback_jid: jid.JID
1513 2119
1567 ) 2173 )
1568 else: 2174 else:
1569 # I'm not sure why this check is required, this code is copied from the old 2175 # I'm not sure why this check is required, this code is copied from the old
1570 # plugin. 2176 # plugin.
1571 if sender_jid.userhostJID() == client.jid.userhostJID(): 2177 if sender_jid.userhostJID() == client.jid.userhostJID():
1572 # TODO: I've seen this cause an exception "builtins.KeyError: 'to'", seems
1573 # like "to" isn't always set.
1574 try: 2178 try:
1575 feedback_jid = jid.JID(message_elt["to"]) 2179 feedback_jid = jid.JID(message_elt["to"])
1576 except KeyError: 2180 except KeyError:
1577 feedback_jid = client.server_jid 2181 feedback_jid = client.server_jid
1578 else: 2182 else:
1697 ), 2301 ),
1698 { C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR } 2302 { C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR }
1699 ) 2303 )
1700 # No point in further processing this message 2304 # No point in further processing this message
1701 return False 2305 return False
2306
2307 affix_values: Optional[SCEAffixValues] = None
1702 2308
1703 if message.namespace == twomemo.twomemo.NAMESPACE: 2309 if message.namespace == twomemo.twomemo.NAMESPACE:
1704 if plaintext is not None: 2310 if plaintext is not None:
1705 # XEP_0420.unpack_stanza handles the whole unpacking, including the 2311 # XEP_0420.unpack_stanza handles the whole unpacking, including the
1706 # relevant modifications to the element 2312 # relevant modifications to the element
1724 ), 2330 ),
1725 { C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR } 2331 { C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR }
1726 ) 2332 )
1727 # No point in further processing this message 2333 # No point in further processing this message
1728 return False 2334 return False
1729 2335 else:
1730 if affix_values.timestamp is not None: 2336 if affix_values.timestamp is not None:
1731 # TODO: affix_values.timestamp contains the timestamp included in the 2337 # TODO: affix_values.timestamp contains the timestamp included in
1732 # encrypted element here. The XEP says it SHOULD be displayed with the 2338 # the encrypted element here. The XEP says it SHOULD be displayed
1733 # plaintext by clients. 2339 # with the plaintext by clients.
1734 pass 2340 pass
1735 2341
1736 if message.namespace == oldmemo.oldmemo.NAMESPACE: 2342 if message.namespace == oldmemo.oldmemo.NAMESPACE:
1737 # Remove all body elements from the original element, since those act as 2343 # Remove all body elements from the original element, since those act as
1738 # fallbacks in case the encryption protocol is not supported 2344 # fallbacks in case the encryption protocol is not supported
1739 for child in message_elt.elements(): 2345 for child in message_elt.elements():
1744 # Add the decrypted body 2350 # Add the decrypted body
1745 message_elt.addElement("body", content=plaintext.decode("utf-8")) 2351 message_elt.addElement("body", content=plaintext.decode("utf-8"))
1746 2352
1747 # Mark the message as trusted or untrusted. Undecided counts as untrusted here. 2353 # Mark the message as trusted or untrusted. Undecided counts as untrusted here.
1748 trust_level = \ 2354 trust_level = \
1749 TrustLevel(device_information.trust_level_name).to_omemo_trust_level() 2355 await session_manager._evaluate_custom_trust_level(device_information)
2356
1750 if trust_level is omemo.TrustLevel.TRUSTED: 2357 if trust_level is omemo.TrustLevel.TRUSTED:
1751 post_treat.addCallback(client.encryption.markAsTrusted) 2358 post_treat.addCallback(client.encryption.markAsTrusted)
1752 else: 2359 else:
1753 post_treat.addCallback(client.encryption.markAsUntrusted) 2360 post_treat.addCallback(client.encryption.markAsUntrusted)
1754 2361
1755 # Mark the message as originally encrypted 2362 # Mark the message as originally encrypted
1756 post_treat.addCallback( 2363 post_treat.addCallback(
1757 client.encryption.markAsEncrypted, 2364 client.encryption.markAsEncrypted,
1758 namespace=message.namespace 2365 namespace=message.namespace
1759 ) 2366 )
2367
2368 # Handle potential ATM trust updates
2369 if affix_values is not None and affix_values.timestamp is not None:
2370 await self.__message_received_trigger_atm(
2371 client,
2372 message_elt,
2373 session_manager,
2374 device_information,
2375 affix_values.timestamp
2376 )
1760 2377
1761 # Message processed successfully, continue with the flow 2378 # Message processed successfully, continue with the flow
1762 return True 2379 return True
1763 2380
1764 async def __send_trigger(self, client: SatXMPPClient, stanza: domish.Element) -> bool: 2381 async def __send_trigger(self, client: SatXMPPClient, stanza: domish.Element) -> bool:
1767 @param stanza: The stanza that is about to be sent. Can be modified. 2384 @param stanza: The stanza that is about to be sent. Can be modified.
1768 @return: Whether the send message flow should continue or not. 2385 @return: Whether the send message flow should continue or not.
1769 """ 2386 """
1770 # SCE is only applicable to message and IQ stanzas 2387 # SCE is only applicable to message and IQ stanzas
1771 # FIXME: temporary disabling IQ stanza encryption 2388 # FIXME: temporary disabling IQ stanza encryption
1772 if stanza.name not in { "message" }: # , "iq" }: 2389 if stanza.name not in { "message" }: # , "iq" }:
1773 return True 2390 return True
1774 2391
1775 # Get the intended recipient 2392 # Get the intended recipient
1776 recipient = stanza.getAttribute("to", None) 2393 recipient = stanza.getAttribute("to", None)
1777 if recipient is None: 2394 if recipient is None:
2017 2634
2018 message = next(message for message in messages if message.namespace == namespace) 2635 message = next(message for message in messages if message.namespace == namespace)
2019 2636
2020 if namespace == twomemo.twomemo.NAMESPACE: 2637 if namespace == twomemo.twomemo.NAMESPACE:
2021 # Add the encrypted element 2638 # Add the encrypted element
2022 stanza.addChild(xml_tools.et_elt_2_domish_elt(twomemo.etree.serialize_message(message))) 2639 stanza.addChild(xml_tools.et_elt_2_domish_elt(
2640 twomemo.etree.serialize_message(message)
2641 ))
2023 2642
2024 if namespace == oldmemo.oldmemo.NAMESPACE: 2643 if namespace == oldmemo.oldmemo.NAMESPACE:
2025 # Add the encrypted element 2644 # Add the encrypted element
2026 stanza.addChild(xml_tools.et_elt_2_domish_elt(oldmemo.etree.serialize_message(message))) 2645 stanza.addChild(xml_tools.et_elt_2_domish_elt(
2646 oldmemo.etree.serialize_message(message)
2647 ))
2027 2648
2028 if muc_plaintext_cache_key is not None: 2649 if muc_plaintext_cache_key is not None:
2029 self.__muc_plaintext_cache[muc_plaintext_cache_key] = plaintext 2650 self.__muc_plaintext_cache[muc_plaintext_cache_key] = plaintext
2030 2651
2031 async def __on_device_list_update( 2652 async def __on_device_list_update(