plugin XEP-0384: code cleanup and failure tracking

This commit is contained in:
Syndace 2026-06-08 14:23:35 +02:00 • committed by Goffi
commit 5d9f2d2da9

View file

@ -16,6 +16,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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.")