Mercurial > libervia-backend
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( |