# HG changeset patch # User Goffi # Date 1667220411 -3600 # Node ID 5fbdf986670cc6772534aac8eebd190425957c20 # Parent 9b1d74a6b48cb2a0e936be78cbe53c108fe71515 plugin pte: Pubsub Target Encryption implementation: This plugin lets encrypt a few items for a specific set of entities. rel 382 diff -r 9b1d74a6b48c -r 5fbdf986670c sat/plugins/plugin_sec_pte.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat/plugins/plugin_sec_pte.py Mon Oct 31 13:46:51 2022 +0100 @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 + +# Libervia plugin for Pubsub Targeted Encryption +# Copyright (C) 2009-2022 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from typing import Any, Dict, List, Optional + +from twisted.internet import defer +from twisted.words.protocols.jabber import jid, xmlstream +from twisted.words.xish import domish +from wokkel import disco, iwokkel +from wokkel import rsm +from zope.interface import implementer + +from sat.core import exceptions +from sat.core.constants import Const as C +from sat.core.core_types import SatXMPPEntity +from sat.core.i18n import _ +from sat.core.log import getLogger + + +log = getLogger(__name__) + +IMPORT_NAME = "PTE" + +PLUGIN_INFO = { + C.PI_NAME: "Pubsub Targeted Encryption", + C.PI_IMPORT_NAME: IMPORT_NAME, + C.PI_TYPE: C.PLUG_TYPE_XEP, + C.PI_MODES: C.PLUG_MODE_BOTH, + C.PI_PROTOCOLS: [], + C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0384"], + C.PI_MAIN: "PTE", + C.PI_HANDLER: "yes", + C.PI_DESCRIPTION: _("""Encrypt some items to specific entities"""), +} +NS_PTE = "urn:xmpp:pte:0" + + +class PTE: + namespace = NS_PTE + + def __init__(self, host): + log.info(_("Pubsub Targeted Encryption plugin initialization")) + host.registerNamespace("pte", NS_PTE) + self.host = host + self._o = host.plugins["XEP-0384"] + host.trigger.add("XEP-0060_publish", self._publish_trigger) + host.trigger.add("XEP-0060_items", self._items_trigger) + + def getHandler(self, client): + return PTE_Handler() + + async def _publish_trigger( + self, + client: SatXMPPEntity, + service: jid.JID, + node: str, + items: Optional[List[domish.Element]], + options: Optional[dict], + sender: jid.JID, + extra: Dict[str, Any] + ) -> bool: + if not items or extra.get("encrypted_for") is None: + return True + encrypt_data = extra["encrypted_for"] + try: + targets = {jid.JID(t) for t in encrypt_data["targets"]} + except (KeyError, RuntimeError): + raise exceptions.DataError(f"Invalid encryption data: {encrypt_data}") + for item in items: + log.debug( + f"encrypting item {item.getAttribute('id', '')} for " + f"{', '.join(t.full() for t in targets)}" + ) + encryption_type = encrypt_data.get("type", self._o.NS_TWOMEMO) + if encryption_type != self._o.NS_TWOMEMO: + raise NotImplementedError("only TWOMEMO is supported for now") + await self._o.encrypt( + client, + self._o.NS_TWOMEMO, + item, + targets, + is_muc_message=False, + stanza_id=None + ) + item_elts = list(item.elements()) + if len(item_elts) != 1: + raise ValueError( + f"there should be exactly one item payload: {item.toXml()}" + ) + encrypted_payload = item_elts[0] + item.children.clear() + encrypted_elt = item.addElement((NS_PTE, "encrypted")) + encrypted_elt["by"] = sender.userhost() + encrypted_elt["type"] = encryption_type + encrypted_elt.addChild(encrypted_payload) + + return True + + async def _items_trigger( + self, + client: SatXMPPEntity, + service: Optional[jid.JID], + node: str, + items: List[domish.Element], + rsm_response: rsm.RSMResponse, + extra: Dict[str, Any], + ) -> bool: + if not extra.get(C.KEY_DECRYPT, True): + return True + if service is None: + service = client.jid.userhostJID() + for item in items: + payload = item.firstChildElement() + if (payload is not None + and payload.name == "encrypted" + and payload.uri == NS_PTE): + encrypted_elt = payload + item.children.clear() + try: + encryption_type = encrypted_elt.getAttribute("type") + encrypted_by = jid.JID(encrypted_elt["by"]) + except (KeyError, RuntimeError): + raise exceptions.DataError( + f"invalid element: {encrypted_elt.toXml()}" + ) + if encryption_type!= self._o.NS_TWOMEMO: + raise NotImplementedError("only TWOMEMO is supported for now") + log.debug(f"decrypting item {item.getAttribute('id', '')}") + + # FIXME: we do use _message_received_trigger now to decrypt the stanza, a + # cleaner separated decrypt method should be used + encrypted_elt["from"] = encrypted_by.full() + if not await self._o._message_received_trigger( + client, + encrypted_elt, + defer.Deferred() + ) or not encrypted_elt.children: + raise exceptions.EncryptionError("can't decrypt the message") + + item.addChild(encrypted_elt.firstChildElement()) + + extra.setdefault("encrypted", {})[item["id"]] = { + "type": NS_PTE, + "algorithm": encryption_type + } + return True + + +@implementer(iwokkel.IDisco) +class PTE_Handler(xmlstream.XMPPHandler): + + def getDiscoInfo(self, requestor, service, nodeIdentifier=""): + return [disco.DiscoFeature(NS_PTE)] + + def getDiscoItems(self, requestor, service, nodeIdentifier=""): + return [] diff -r 9b1d74a6b48c -r 5fbdf986670c sat/plugins/plugin_xep_0277.py --- a/sat/plugins/plugin_xep_0277.py Mon Oct 31 04:09:38 2022 +0100 +++ b/sat/plugins/plugin_xep_0277.py Mon Oct 31 13:46:51 2022 +0100 @@ -984,7 +984,7 @@ return None extra = {} - for key in ("encrypted", "signed"): + for key in ("encrypted", "encrypted_for", "signed"): value = data.get(key) if value is not None: extra[key] = value diff -r 9b1d74a6b48c -r 5fbdf986670c sat/plugins/plugin_xep_0384.py --- a/sat/plugins/plugin_xep_0384.py Mon Oct 31 04:09:38 2022 +0100 +++ b/sat/plugins/plugin_xep_0384.py Mon Oct 31 13:46:51 2022 +0100 @@ -22,7 +22,7 @@ import logging import time from typing import \ - Any, Dict, FrozenSet, List, Literal, NamedTuple, Optional, Set, Type, cast + Any, Dict, FrozenSet, List, Literal, NamedTuple, Optional, Set, Type, Union, cast import uuid import xml.etree.ElementTree as ET from xml.sax.saxutils import quoteattr @@ -1581,7 +1581,7 @@ # added, the messageReceived trigger is also used for twomemo. sat.trigger.add( "messageReceived", - self.__message_received_trigger, + self._message_received_trigger, priority=100050 ) sat.trigger.add( @@ -2098,7 +2098,7 @@ frozenset(applied_trust_updates) ) - async def __message_received_trigger( + async def _message_received_trigger( self, client: SatXMPPClient, message_elt: domish.Element, @@ -2113,13 +2113,12 @@ encrypted. @return: Whether to continue the message received flow. """ - muc_plaintext_cache_key: Optional[MUCPlaintextCacheKey] = None sender_jid = jid.JID(message_elt["from"]) feedback_jid: jid.JID - message_type = message_elt.getAttribute("type", "unknown") + message_type = message_elt.getAttribute("type", C.MESS_TYPE_NORMAL) is_muc_message = message_type == C.MESS_TYPE_GROUPCHAT if is_muc_message: if self.__xep_0045 is None: @@ -2420,12 +2419,12 @@ return True # All pre-checks done, we can start encrypting! - await self.__encrypt( + await self.encrypt( client, twomemo.twomemo.NAMESPACE, stanza, recipient_bare_jid, - stanza.getAttribute("type", "unkown") == C.MESS_TYPE_GROUPCHAT, + stanza.getAttribute("type", C.MESS_TYPE_NORMAL) == C.MESS_TYPE_GROUPCHAT, stanza.getAttribute("id", None) ) @@ -2462,7 +2461,7 @@ is_muc_message = mess_data["type"] == C.MESS_TYPE_GROUPCHAT stanza_id = mess_data["uid"] - await self.__encrypt( + await self.encrypt( client, oldmemo.oldmemo.NAMESPACE, stanza, @@ -2474,12 +2473,12 @@ # Add a store hint self.__xep_0334.addHintElements(stanza, [ "store" ]) - async def __encrypt( + async def encrypt( self, client: SatXMPPClient, namespace: Literal["urn:xmpp:omemo:2", "eu.siacs.conversations.axolotl"], stanza: domish.Element, - recipient_jid: jid.JID, + recipient_jids: Union[jid.JID, Set[jid.JID]], is_muc_message: bool, stanza_id: Optional[str] ) -> None: @@ -2488,8 +2487,9 @@ @param namespace: The namespace of the OMEMO version to use. @param stanza: The stanza. Twomemo will encrypt the whole stanza using SCE, oldmemo will encrypt only the body. The stanza is modified by this call. - @param recipient_jid: The JID of the recipient. Can be a bare (aka "userhost") JID - but doesn't have to. + @param recipient_jid: The JID of the recipients. + Can be a bare (aka "userhost") JIDs but doesn't have to. + A single JID can be used. @param is_muc_message: Whether the stanza is a message stanza to a MUC room. @param stanza_id: The id of this stanza. Especially relevant for message stanzas to MUC rooms such that the outgoing plaintext can be cached for MUC message @@ -2499,6 +2499,11 @@ hint to the stanza if applicable! This can be done before or after this call, the order doesn't matter. """ + if isinstance(recipient_jids, jid.JID): + recipient_jids = {recipient_jids} + if not recipient_jids: + raise exceptions.InternalError("At least one JID must be specified") + recipient_jid = next(iter(recipient_jids)) muc_plaintext_cache_key: Optional[MUCPlaintextCacheKey] = None @@ -2506,6 +2511,10 @@ feedback_jid: jid.JID if is_muc_message: + if len(recipient_jids) != 1: + raise exceptions.InternalError( + 'Only one JID can be set when "is_muc_message" is set' + ) if self.__xep_0045 is None: raise exceptions.InternalError( "Encryption of MUC message requested, but plugin XEP-0045 is not" @@ -2531,7 +2540,7 @@ message_uid=stanza_id ) else: - recipient_bare_jids = { recipient_jid.userhost() } + recipient_bare_jids = {r.userhost() for r in recipient_jids} feedback_jid = recipient_jid.userhostJID() log.debug(