diff --git a/libervia/backend/plugins/plugin_xep_0384.py b/libervia/backend/plugins/plugin_xep_0384.py index f771d7f0..91722e13 100644 --- a/libervia/backend/plugins/plugin_xep_0384.py +++ b/libervia/backend/plugins/plugin_xep_0384.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from asyncio import Future import base64 from datetime import datetime import enum @@ -64,7 +65,7 @@ from libervia.backend.plugins.plugin_xep_0420 import ( SCEProfile, ) from libervia.backend.tools import xml_tools -from twisted.internet import defer +from twisted.internet.defer import Deferred, ensureDeferred from twisted.words.protocols.jabber import error, jid from twisted.words.xish import domish @@ -155,10 +156,10 @@ class TrustLevel(enum.Enum): The trust levels required for ATM and BTBV. """ - TRUSTED: str = "TRUSTED" - BLINDLY_TRUSTED: str = "BLINDLY_TRUSTED" - UNDECIDED: str = "UNDECIDED" - DISTRUSTED: str = "DISTRUSTED" + TRUSTED = "TRUSTED" + BLINDLY_TRUSTED = "BLINDLY_TRUSTED" + UNDECIDED = "UNDECIDED" + DISTRUSTED = "DISTRUSTED" TWOMEMO_DEVICE_LIST_NODE = "urn:xmpp:omemo:2:devices" @@ -224,7 +225,8 @@ class LegacyStorageImpl(oldmemo.migrations.LegacyStorage): def __init__(self, profile: str, own_bare_jid: str) -> None: """ @param profile: The profile this OMEMO data belongs to. - @param own_bare_jid: The own bare JID, to return by the :meth:`load_own_data` call. + @param own_bare_jid: The own bare JID, to return by the :meth:`load_own_data` + call. """ self.__storage = persistent.LazyPersistentBinaryDict("XEP-0384", profile) @@ -1361,10 +1363,11 @@ def make_session_manager( "This is OMEMO trusting system. You'll see below the devices of your " "contacts, and a checkbox to trust them or not. A trusted device " "can read your messages in plain text, so be sure to only validate " - "devices that you are sure are belonging to your contact. It's better " - "to do this when you are next to your contact and their device, so " - 'you can check the "fingerprint" (the number next to the device) ' - "yourself. Do *not* validate a device if the fingerprint is wrong!" + "devices that you are sure are belonging to your contact. It's " + "better to do this when you are next to your contact and their " + 'device, so you can check the "fingerprint" (the number next to the ' + "device) yourself. Do *not* validate a device if the fingerprint is " + "wrong!" ) ) @@ -1626,11 +1629,14 @@ class OMEMO: # really necessary. self.__muc_plaintext_cache: Dict[MUCPlaintextCacheKey, bytes] = {} + # A set containing all profiles for which OMEMO init failed. + self.__init_failed: Set[str] = set() + # Mapping from profile name to corresponding session manager self.__session_managers: Dict[str, omemo.SessionManager] = {} # Calls waiting for a specific session manager to be built - self.__session_manager_waiters: Dict[str, List[defer.Deferred]] = {} + self.__session_manager_waiters: Dict[str, Future[omemo.SessionManager]] = {} # These triggers are used by oldmemo, which doesn't do SCE and only applies to # messages. Temporarily, until a more fitting trigger for SCE-based encryption is @@ -1653,14 +1659,14 @@ class OMEMO: xep_0163.add_pep_event( "TWOMEMO_DEVICES", TWOMEMO_DEVICE_LIST_NODE, - lambda items_event, profile: defer.ensureDeferred( + lambda items_event, profile: ensureDeferred( self.__on_device_list_update(items_event, profile) ), ) xep_0163.add_pep_event( "OLDMEMO_DEVICES", OLDMEMO_DEVICE_LIST_NODE, - lambda items_event, profile: defer.ensureDeferred( + lambda items_event, profile: ensureDeferred( self.__on_device_list_update(items_event, profile) ), ) @@ -1679,7 +1685,7 @@ class OMEMO: @param client: The client. """ - defer.ensureDeferred(self.get_session_manager(cast(str, client.profile))) + ensureDeferred(self.get_session_manager(client.profile)) async def cmd_omemo_reset( self, client: SatXMPPClient, mess_data: MessageDataLegacy @@ -1697,6 +1703,11 @@ class OMEMO: the message is not supposed to be sent. """ + if client.profile in self.__init_failed: + raise exceptions.InternalError( + f"Can't reset OMEMO for {client.profile}, OMEMO init failed." + ) + twomemo_requested = client.encryption.is_encryption_requested( mess_data, twomemo.twomemo.NAMESPACE ) @@ -1737,6 +1748,11 @@ class OMEMO: devices belonging to the entity. """ + if client.profile in self.__init_failed: + raise exceptions.InternalError( + f"Can't get trust UI for {client.profile}, OMEMO init failed." + ) + if entity.resource: raise ValueError("A bare JID is expected.") @@ -1844,8 +1860,8 @@ class OMEMO: " to do this when you are next to your contact and their device, so" ' you can check the "fingerprint" (the number next to the device)' " yourself. Do *not* validate a device if the fingerprint is wrong!" - " Note that manually validating a fingerprint disables any form of automatic" - " trust." + " Note that manually validating a fingerprint disables any form of" + " automatic trust." ) ) @@ -1948,46 +1964,50 @@ class OMEMO: none was prepared before. """ - try: - # Try to return the session manager + if profile in self.__init_failed: + raise exceptions.InternalError( + f"Can't get SessionManager, OMEMO init failed for {profile}." + ) + + # Try to return the session manager + if profile in self.__session_managers: return self.__session_managers[profile] - except KeyError: - # If a session manager for that profile doesn't exist yet, check whether it is - # currently being built. A session manager being built is signified by the - # profile key existing on __session_manager_waiters. - if profile in self.__session_manager_waiters: - # If the session manager is being built, add ourselves to the waiting - # queue - deferred = defer.Deferred() - self.__session_manager_waiters[profile].append(deferred) - return cast(omemo.SessionManager, await deferred) - # If the session manager is not being built, do so here. - self.__session_manager_waiters[profile] = [] + # If a session manager for that profile doesn't exist yet, check whether it is + # currently being built. A session manager being built is signified by the + # profile key existing on __session_manager_waiters. + if profile in self.__session_manager_waiters: + # If the session manager is being built, add ourselves to the waiting queue + return await Deferred.fromFuture(self.__session_manager_waiters[profile]) - # Build and store the session manager - try: - session_manager = await prepare_for_profile( - self.host, profile, initial_own_label="Libervia" - ) - except Exception as e: - # In case of an error during initalization, notify the waiters accordingly - # and delete them - for waiter in self.__session_manager_waiters[profile]: - waiter.errback(e) - del self.__session_manager_waiters[profile] + # If the session manager is not being built, do so here. + waiter = self.__session_manager_waiters[profile] = Future() - # Re-raise the exception - raise + # Build and store the session manager + try: + session_manager = await prepare_for_profile( + self.host, profile, initial_own_label="Libervia" + ) + except Exception as e: + # Keep track of the failure to avoid re-attempting initialization + self.__init_failed.add(profile) + log.error(f"error while preparing profile for {profile}: {e}", exc_info=e) - self.__session_managers[profile] = session_manager - - # Notify the waiters and delete them - for waiter in self.__session_manager_waiters[profile]: - waiter.callback(session_manager) + # In case of an error during initalization, notify the waiters accordingly + # and delete them + waiter.set_exception(e) del self.__session_manager_waiters[profile] - return session_manager + # Re-raise the exception + raise + + self.__session_managers[profile] = session_manager + + # Notify the waiters and delete them + waiter.set_result(session_manager) + del self.__session_manager_waiters[profile] + + return session_manager async def __message_received_trigger_atm( self, @@ -2142,7 +2162,7 @@ class OMEMO: self, client: SatXMPPClient, message_elt: domish.Element, - post_treat: defer.Deferred, + post_treat: Deferred, ) -> bool: """ @param client: The client which received the message. @@ -2238,10 +2258,17 @@ class OMEMO: next(message_elt.elements(oldmemo.oldmemo.NAMESPACE, "encrypted"), None), ) + if twomemo_encrypted_elt is None and oldmemo_encrypted_elt is None: + # None of our business, let the flow continue + return True + + if client.profile in self.__init_failed: + log.error(f"Can't decrypt message, OMEMO init failed for {client.profile}") + return True + try: - session_manager = await self.get_session_manager(cast(str, client.profile)) + session_manager = await self.get_session_manager(client.profile) except Exception as e: - log.error(f"error while preparing profile for {client.profile}: {e}") # we don't want to block the workflow return True @@ -2453,6 +2480,11 @@ class OMEMO: # Encryption is not requested for this recipient return True + if client.profile in self.__init_failed: + raise exceptions.InternalError( + f"Can't encrypt stanza, OMEMO init failed for {client.profile}." + ) + encryption_ns = encryption["plugin"].namespace # All pre-checks done, we can start encrypting! if encryption_ns in (twomemo.twomemo.NAMESPACE, oldmemo.oldmemo.NAMESPACE): @@ -2540,6 +2572,11 @@ class OMEMO: hint to the stanza if applicable! This can be done before or after this call, the order doesn't matter. """ + if client.profile in self.__init_failed: + raise exceptions.InternalError( + f"Can't encrypt stanza, OMEMO init failed for {client.profile}." + ) + if isinstance(recipient_jids, jid.JID): recipient_jids = {recipient_jids} if not recipient_jids: @@ -2706,13 +2743,6 @@ class OMEMO: @param items_event: The event. @param profile: The profile this event belongs to. """ - # Skip if session manager is already being built to prevent reentrancy loop - # (device list refresh during init triggers this handler again) - if profile in self.__session_manager_waiters: - log.debug( - f"Ignoring device list update for {profile} (session manager being built)" - ) - return sender = cast(jid.JID, items_event.sender) items = cast(List[domish.Element], items_event.items) @@ -2722,6 +2752,11 @@ class OMEMO: async def _update_device_list( self, client: SatXMPPEntity, sender: jid.JID, items: list[domish.Element] ) -> None: + if client.profile in self.__init_failed: + # It should be fine to silently ignore this here in case OMEMO failed + # to initialize, as the data will be freshly fetched during the next + # library init attempt (after restart). + return if len(items) > 1: log.warning("Ignoring device list update with more than one element.")