574 lines
23 KiB
Python
574 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# Libervia plugin for Stanza Content Encryption
|
|
# Copyright (C) 2022-2022 Tim Henkes (me@syndace.dev)
|
|
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
from abc import ABC, abstractmethod
|
|
from datetime import datetime
|
|
import enum
|
|
import secrets
|
|
import string
|
|
from typing import Dict, NamedTuple, Optional, Set, Tuple, cast
|
|
from typing_extensions import Final
|
|
|
|
from lxml import etree
|
|
from libervia.backend.core import exceptions
|
|
|
|
from libervia.backend.core.constants import Const as C
|
|
from libervia.backend.core.i18n import D_
|
|
from libervia.backend.core.log import Logger, getLogger
|
|
from libervia.backend.core.main import LiberviaBackend
|
|
from libervia.backend.tools.xml_tools import ElementParser
|
|
from libervia.backend.plugins.plugin_xep_0033 import NS_ADDRESS
|
|
from libervia.backend.plugins.plugin_xep_0082 import XEP_0082
|
|
from libervia.backend.plugins.plugin_xep_0334 import NS_HINTS
|
|
from libervia.backend.plugins.plugin_xep_0359 import NS_SID
|
|
from libervia.backend.plugins.plugin_xep_0380 import NS_EME
|
|
from twisted.words.protocols.jabber import jid
|
|
from twisted.words.xish import domish
|
|
|
|
|
|
__all__ = [ # pylint: disable=unused-variable
|
|
"PLUGIN_INFO",
|
|
"NS_SCE",
|
|
"XEP_0420",
|
|
"ProfileRequirementsNotMet",
|
|
"AffixVerificationFailed",
|
|
"SCECustomAffix",
|
|
"SCEAffixPolicy",
|
|
"SCEProfile",
|
|
"SCEAffixValues",
|
|
]
|
|
|
|
|
|
log = cast(Logger, getLogger(__name__)) # type: ignore[no-untyped-call]
|
|
|
|
|
|
PLUGIN_INFO = {
|
|
C.PI_NAME: "SCE",
|
|
C.PI_IMPORT_NAME: "XEP-0420",
|
|
C.PI_TYPE: "SEC",
|
|
C.PI_PROTOCOLS: ["XEP-0420"],
|
|
C.PI_DEPENDENCIES: ["XEP-0334", "XEP-0082"],
|
|
C.PI_RECOMMENDATIONS: ["XEP-0045", "XEP-0033", "XEP-0359"],
|
|
C.PI_MAIN: "XEP_0420",
|
|
C.PI_HANDLER: "no",
|
|
C.PI_DESCRIPTION: D_("Implementation of Stanza Content Encryption"),
|
|
}
|
|
|
|
|
|
NS_SCE: Final = "urn:xmpp:sce:1"
|
|
|
|
|
|
class ProfileRequirementsNotMet(Exception):
|
|
"""
|
|
Raised by :meth:`XEP_0420.unpack_stanza` in case the requirements formulated by the
|
|
profile are not met.
|
|
"""
|
|
|
|
|
|
class AffixVerificationFailed(Exception):
|
|
"""
|
|
Raised by :meth:`XEP_0420.unpack_stanza` in case of affix verification failure.
|
|
"""
|
|
|
|
|
|
class SCECustomAffix(ABC):
|
|
"""
|
|
Interface for custom affixes of SCE profiles.
|
|
"""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def element_name(self) -> str:
|
|
"""
|
|
@return: The name of the affix's XML element.
|
|
"""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def element_schema(self) -> str:
|
|
"""
|
|
@return: The XML schema definition of the affix element's XML structure, i.e. the
|
|
``<xs:element/>`` schema element. This element will be referenced using
|
|
``<xs:element ref="{element_name}"/>``.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def create(self, stanza: domish.Element) -> domish.Element:
|
|
"""
|
|
@param stanza: The stanza element which has been processed by
|
|
:meth:`XEP_0420.pack_stanza`, i.e. all encryptable children have been removed
|
|
and only the root ``<message/>`` or ``<iq/>`` and unencryptable children
|
|
remain. Do not modify.
|
|
@return: An affix element to include in the envelope. The element must have the
|
|
name :attr:`element_name` and must validate using :attr:`element_schema`.
|
|
@raise ValueError: if the affix couldn't be built due to missing information on
|
|
the stanza.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def verify(self, stanza: domish.Element, element: domish.Element) -> None:
|
|
"""
|
|
@param stanza: The stanza element before being processed by
|
|
:meth:`XEP_0420.unpack_stanza`, i.e. all encryptable children have been
|
|
removed and only the root ``<message/>`` or ``<iq/>`` and unencryptable
|
|
children remain. Do not modify.
|
|
@param element: The affix element to verify.
|
|
@raise AffixVerificationFailed: on verification failure.
|
|
"""
|
|
|
|
|
|
@enum.unique
|
|
class SCEAffixPolicy(enum.Enum):
|
|
"""
|
|
Policy for the presence of an affix in an SCE envelope.
|
|
"""
|
|
|
|
REQUIRED: str = "REQUIRED"
|
|
OPTIONAL: str = "OPTIONAL"
|
|
NOT_NEEDED: str = "NOT_NEEDED"
|
|
|
|
|
|
class SCEProfile(NamedTuple):
|
|
# pylint: disable=invalid-name
|
|
"""
|
|
An SCE profile, i.e. the definition which affixes are required, optional or not needed
|
|
at all by an SCE-enabled encryption protocol.
|
|
"""
|
|
|
|
rpad_policy: SCEAffixPolicy
|
|
time_policy: SCEAffixPolicy
|
|
to_policy: SCEAffixPolicy
|
|
from_policy: SCEAffixPolicy
|
|
custom_policies: Dict[SCECustomAffix, SCEAffixPolicy]
|
|
|
|
|
|
class SCEAffixValues(NamedTuple):
|
|
# pylint: disable=invalid-name
|
|
"""
|
|
Structure returned by :meth:`XEP_0420.unpack_stanza` with the parsed/processes values
|
|
of all affixes included in the envelope. For custom affixes, the whole affix element
|
|
is returned.
|
|
"""
|
|
|
|
rpad: Optional[str]
|
|
timestamp: Optional[datetime]
|
|
recipient: Optional[jid.JID]
|
|
sender: Optional[jid.JID]
|
|
custom: Dict[SCECustomAffix, domish.Element]
|
|
|
|
|
|
ENVELOPE_SCHEMA = """<?xml version="1.0" encoding="utf8"?>
|
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
targetNamespace="urn:xmpp:sce:1"
|
|
xmlns="urn:xmpp:sce:1">
|
|
|
|
<xs:element name="envelope">
|
|
<xs:complexType>
|
|
<xs:all>
|
|
<xs:element ref="content"/>
|
|
<xs:element ref="rpad" minOccurs="0"/>
|
|
<xs:element ref="time" minOccurs="0"/>
|
|
<xs:element ref="to" minOccurs="0"/>
|
|
<xs:element ref="from" minOccurs="0"/>
|
|
{custom_affix_references}
|
|
</xs:all>
|
|
</xs:complexType>
|
|
</xs:element>
|
|
|
|
<xs:element name="content">
|
|
<xs:complexType>
|
|
<xs:sequence>
|
|
<xs:any minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
|
|
</xs:sequence>
|
|
</xs:complexType>
|
|
</xs:element>
|
|
|
|
<xs:element name="rpad" type="xs:string"/>
|
|
|
|
<xs:element name="time">
|
|
<xs:complexType>
|
|
<xs:attribute name="stamp" type="xs:dateTime"/>
|
|
</xs:complexType>
|
|
</xs:element>
|
|
|
|
<xs:element name="to">
|
|
<xs:complexType>
|
|
<xs:attribute name="jid" type="xs:string"/>
|
|
</xs:complexType>
|
|
</xs:element>
|
|
|
|
<xs:element name="from">
|
|
<xs:complexType>
|
|
<xs:attribute name="jid" type="xs:string"/>
|
|
</xs:complexType>
|
|
</xs:element>
|
|
|
|
{custom_affix_definitions}
|
|
</xs:schema>
|
|
"""
|
|
|
|
|
|
class XEP_0420: # pylint: disable=invalid-name
|
|
"""
|
|
Implementation of XEP-0420: Stanza Content Encryption under namespace
|
|
``urn:xmpp:sce:1``.
|
|
|
|
This is a passive plugin, i.e. it doesn't hook into any triggers to process stanzas
|
|
actively, but offers API for other plugins to use.
|
|
"""
|
|
|
|
# Set of namespaces whose elements are never allowed to be transferred in an encrypted
|
|
# envelope.
|
|
MUST_BE_PLAINTEXT_NAMESPACES: Set[str] = {
|
|
NS_HINTS,
|
|
NS_SID, # TODO: Not sure whether this ban applies to both stanza-id and origin-id
|
|
NS_ADDRESS,
|
|
# Not part of the specification (yet), but just doesn't make sense in an encrypted
|
|
# envelope:
|
|
NS_EME,
|
|
}
|
|
|
|
# Set of (namespace, element name) tuples that define elements which are never allowed
|
|
# to be transferred in an encrypted envelope. If all elements under a certain
|
|
# namespace are forbidden, the namespace can be added to
|
|
# :attr:`MUST_BE_PLAINTEXT_NAMESPACES` instead.
|
|
# Note: only full namespaces are forbidden by the spec for now, the following is for
|
|
# potential future use.
|
|
MUST_BE_PLAINTEXT_ELEMENTS: Set[Tuple[str, str]] = set()
|
|
|
|
def __init__(self, sat: LiberviaBackend) -> None:
|
|
"""
|
|
@param sat: The SAT instance.
|
|
"""
|
|
|
|
@staticmethod
|
|
def pack_stanza(profile: SCEProfile, stanza: domish.Element) -> bytes:
|
|
"""Pack a stanza according to Stanza Content Encryption.
|
|
|
|
Removes all elements from the stanza except for a few exceptions that explicitly
|
|
need to be transferred in plaintext, e.g. because they contain hints/instructions
|
|
for the server on how to process the stanza. Together with the affix elements as
|
|
requested by the profile, the removed elements are added to an envelope XML
|
|
structure that builds the plaintext to be encrypted by the SCE-enabled encryption
|
|
scheme. Optional affixes are always added to the structure, i.e. they are treated
|
|
by the packing code as if they were required.
|
|
|
|
Once built, the envelope structure is serialized to a byte string and returned for
|
|
the encryption scheme to encrypt and add to the stanza.
|
|
|
|
@param profile: The SCE profile, i.e. the definition of affixes to include in the
|
|
envelope.
|
|
@param stanza: The stanza to process. Will be modified by the call.
|
|
@return: The serialized envelope structure that builds the plaintext for the
|
|
encryption scheme to process.
|
|
@raise ValueError: if the <to/> or <from/> affixes are requested but the stanza
|
|
doesn't have the "to"/"from" attribute set to extract the value from. Can also
|
|
be raised by custom affixes.
|
|
|
|
@warning: It is up to the calling code to add a <store/> message processing hint
|
|
if applicable.
|
|
"""
|
|
|
|
# Prepare the envelope and content elements
|
|
envelope = domish.Element((NS_SCE, "envelope"))
|
|
content = envelope.addElement((NS_SCE, "content"))
|
|
|
|
# Note the serialized byte size of the content element before adding any children
|
|
empty_content_byte_size = len(content.toXml().encode("utf-8"))
|
|
|
|
# Move elements that are not explicitly forbidden from being encrypted from the
|
|
# stanza to the content element.
|
|
for child in list(stanza.elements()):
|
|
if (
|
|
child.uri not in XEP_0420.MUST_BE_PLAINTEXT_NAMESPACES
|
|
and (child.uri, child.name) not in XEP_0420.MUST_BE_PLAINTEXT_ELEMENTS
|
|
):
|
|
# Remove the child from the stanza
|
|
stanza.children.remove(child)
|
|
|
|
# A namespace of ``None`` can be used on domish elements to inherit the
|
|
# namespace from the parent. When moving elements from the stanza root to
|
|
# the content element, however, we don't want elements to inherit the
|
|
# namespace of the content element. Thus, check for elements with ``None``
|
|
# for their namespace and set the namespace to jabber:client, which is the
|
|
# namespace of the parent element.
|
|
if child.uri is None:
|
|
child.uri = C.NS_CLIENT
|
|
child.defaultUri = C.NS_CLIENT
|
|
|
|
# Add the child with corrected namespaces to the content element
|
|
content.addChild(child)
|
|
|
|
# Add the affixes requested by the profile
|
|
if profile.rpad_policy is not SCEAffixPolicy.NOT_NEEDED:
|
|
# The specification defines the rpad affix to contain "[...] a randomly
|
|
# generated sequence of random length between 0 and 200 characters." This
|
|
# implementation differs a bit from the specification in that a minimum size
|
|
# other than 0 is chosen depending on the serialized size of the content
|
|
# element. This is to prevent the scenario where the encrypted content is
|
|
# short and the rpad is also randomly chosen to be short, which could allow
|
|
# guessing the content of a short message. To do so, the rpad length is first
|
|
# chosen to pad the content to at least 53 bytes, then afterwards another 0 to
|
|
# 200 bytes are added. Note that single-byte characters are used by this
|
|
# implementation, thus the number of characters equals the number of bytes.
|
|
content_byte_size = len(content.toXml().encode("utf-8"))
|
|
content_byte_size_diff = content_byte_size - empty_content_byte_size
|
|
rpad_length = max(0, 53 - content_byte_size_diff) + secrets.randbelow(201)
|
|
rpad_content = "".join(
|
|
secrets.choice(string.digits + string.ascii_letters + string.punctuation)
|
|
for __ in range(rpad_length)
|
|
)
|
|
envelope.addElement((NS_SCE, "rpad"), content=rpad_content)
|
|
|
|
if profile.time_policy is not SCEAffixPolicy.NOT_NEEDED:
|
|
time_element = envelope.addElement((NS_SCE, "time"))
|
|
time_element["stamp"] = XEP_0082.format_datetime()
|
|
|
|
if profile.to_policy is not SCEAffixPolicy.NOT_NEEDED:
|
|
recipient = stanza.getAttribute("to", None)
|
|
if recipient is not None:
|
|
to_element = envelope.addElement((NS_SCE, "to"))
|
|
to_element["jid"] = jid.JID(recipient).userhost()
|
|
elif profile.to_policy is SCEAffixPolicy.REQUIRED:
|
|
raise ValueError(
|
|
"<to/> affix requested, but stanza doesn't have the 'to' attribute"
|
|
" set."
|
|
)
|
|
|
|
if profile.from_policy is not SCEAffixPolicy.NOT_NEEDED:
|
|
sender = stanza.getAttribute("from", None)
|
|
if sender is not None:
|
|
from_element = envelope.addElement((NS_SCE, "from"))
|
|
from_element["jid"] = jid.JID(sender).userhost()
|
|
elif profile.from_policy is SCEAffixPolicy.REQUIRED:
|
|
raise ValueError(
|
|
"<from/> affix requested, but stanza doesn't have the 'from'"
|
|
" attribute set."
|
|
)
|
|
|
|
for affix, policy in profile.custom_policies.items():
|
|
if policy is not SCEAffixPolicy.NOT_NEEDED:
|
|
envelope.addChild(affix.create(stanza))
|
|
|
|
return envelope.toXml().encode("utf-8")
|
|
|
|
@staticmethod
|
|
def unpack_stanza(
|
|
profile: SCEProfile, stanza: domish.Element, envelope_serialized: bytes
|
|
) -> SCEAffixValues:
|
|
"""Unpack a stanza packed according to Stanza Content Encryption.
|
|
|
|
Parses the serialized envelope as XML, verifies included affixes and makes sure
|
|
the requirements of the profile are met, and restores the stanza by moving
|
|
decrypted elements from the envelope back to the stanza top level.
|
|
|
|
@param profile: The SCE profile, i.e. the definition of affixes that have to/may
|
|
be included in the envelope.
|
|
@param stanza: The stanza to process. Will be modified by the call.
|
|
@param envelope_serialized: The serialized envelope, i.e. the plaintext produced
|
|
by the decryption scheme utilizing SCE.
|
|
@return: The parsed and processed values of all affixes that were present on the
|
|
envelope, notably including the timestamp.
|
|
@raise exceptions.ParsingError: if the serialized envelope element is malformed.
|
|
@raise ProfileRequirementsNotMet: if one or more affixes required by the profile
|
|
are missing from the envelope.
|
|
@raise AffixVerificationFailed: if an affix included in the envelope fails to
|
|
validate. It doesn't matter whether the affix is required by the profile or
|
|
not, all affixes included in the envelope are validated and cause this
|
|
exception to be raised on failure.
|
|
|
|
@warning: It is up to the calling code to verify the timestamp, if returned, since
|
|
the requirements on the timestamp may vary between SCE-enabled protocols.
|
|
"""
|
|
|
|
try:
|
|
envelope_serialized_string = envelope_serialized.decode("utf-8")
|
|
except UnicodeError as e:
|
|
raise exceptions.ParsingError(
|
|
"Serialized envelope can't bare parsed as utf-8."
|
|
) from e
|
|
|
|
custom_affixes = set(profile.custom_policies.keys())
|
|
|
|
# Make sure the envelope adheres to the schema
|
|
parser = etree.XMLParser(
|
|
schema=etree.XMLSchema(
|
|
etree.XML(
|
|
ENVELOPE_SCHEMA.format(
|
|
custom_affix_references="".join(
|
|
f'<xs:element ref="{custom_affix.element_name}" minOccurs="0"/>'
|
|
for custom_affix in custom_affixes
|
|
),
|
|
custom_affix_definitions="".join(
|
|
custom_affix.element_schema for custom_affix in custom_affixes
|
|
),
|
|
).encode("utf-8")
|
|
)
|
|
)
|
|
)
|
|
|
|
try:
|
|
etree.fromstring(envelope_serialized_string, parser)
|
|
except etree.XMLSyntaxError as e:
|
|
raise exceptions.ParsingError(
|
|
"Serialized envelope doesn't pass schema validation."
|
|
) from e
|
|
|
|
# Prepare the envelope and content elements
|
|
envelope = cast(domish.Element, ElementParser()(envelope_serialized_string))
|
|
content = next(envelope.elements(NS_SCE, "content"))
|
|
|
|
# Verify the affixes
|
|
rpad_element = cast(
|
|
Optional[domish.Element], next(envelope.elements(NS_SCE, "rpad"), None)
|
|
)
|
|
time_element = cast(
|
|
Optional[domish.Element], next(envelope.elements(NS_SCE, "time"), None)
|
|
)
|
|
to_element = cast(
|
|
Optional[domish.Element], next(envelope.elements(NS_SCE, "to"), None)
|
|
)
|
|
from_element = cast(
|
|
Optional[domish.Element], next(envelope.elements(NS_SCE, "from"), None)
|
|
)
|
|
|
|
# The rpad doesn't need verification.
|
|
rpad_value = None if rpad_element is None else str(rpad_element)
|
|
|
|
# The time affix isn't verified other than that the timestamp is parseable.
|
|
try:
|
|
timestamp_value = (
|
|
None
|
|
if time_element is None
|
|
else XEP_0082.parse_datetime(time_element["stamp"])
|
|
)
|
|
except ValueError as e:
|
|
raise AffixVerificationFailed("Malformed time affix.") from e
|
|
|
|
# The to affix is verified by comparing the to attribute of the stanza with the
|
|
# JID referenced by the affix. Note that only bare JIDs are compared as per the
|
|
# specification.
|
|
recipient_value: Optional[jid.JID] = None
|
|
if to_element is not None:
|
|
recipient_value = jid.JID(to_element["jid"])
|
|
|
|
recipient_actual = stanza.getAttribute("to", None)
|
|
if recipient_actual is None:
|
|
raise AffixVerificationFailed(
|
|
"'To' affix is included in the envelope, but the stanza is lacking a"
|
|
" 'to' attribute to compare the value to."
|
|
)
|
|
|
|
recipient_actual_bare_jid = jid.JID(recipient_actual).userhost()
|
|
recipient_target_bare_jid = recipient_value.userhost()
|
|
|
|
if recipient_actual_bare_jid != recipient_target_bare_jid:
|
|
raise AffixVerificationFailed(
|
|
f"Mismatch between actual and target recipient bare JIDs:"
|
|
f" {recipient_actual_bare_jid} vs {recipient_target_bare_jid}."
|
|
)
|
|
|
|
# The from affix is verified by comparing the from attribute of the stanza with
|
|
# the JID referenced by the affix. Note that only bare JIDs are compared as per
|
|
# the specification.
|
|
sender_value: Optional[jid.JID] = None
|
|
if from_element is not None:
|
|
sender_value = jid.JID(from_element["jid"])
|
|
|
|
sender_actual = stanza.getAttribute("from", None)
|
|
if sender_actual is None:
|
|
raise AffixVerificationFailed(
|
|
"'From' affix is included in the envelope, but the stanza is lacking"
|
|
" a 'from' attribute to compare the value to."
|
|
)
|
|
|
|
sender_actual_bare_jid = jid.JID(sender_actual).userhost()
|
|
sender_target_bare_jid = sender_value.userhost()
|
|
|
|
if sender_actual_bare_jid != sender_target_bare_jid:
|
|
raise AffixVerificationFailed(
|
|
f"Mismatch between actual and target sender bare JIDs:"
|
|
f" {sender_actual_bare_jid} vs {sender_target_bare_jid}."
|
|
)
|
|
|
|
# Find and verify custom affixes
|
|
custom_values: Dict[SCECustomAffix, domish.Element] = {}
|
|
for affix in custom_affixes:
|
|
element_name = affix.element_name
|
|
element = cast(
|
|
Optional[domish.Element],
|
|
next(envelope.elements(NS_SCE, element_name), None),
|
|
)
|
|
if element is not None:
|
|
affix.verify(stanza, element)
|
|
custom_values[affix] = element
|
|
|
|
# Check whether all affixes required by the profile are present
|
|
rpad_missing = (
|
|
profile.rpad_policy is SCEAffixPolicy.REQUIRED and rpad_element is None
|
|
)
|
|
time_missing = (
|
|
profile.time_policy is SCEAffixPolicy.REQUIRED and time_element is None
|
|
)
|
|
to_missing = profile.to_policy is SCEAffixPolicy.REQUIRED and to_element is None
|
|
from_missing = (
|
|
profile.from_policy is SCEAffixPolicy.REQUIRED and from_element is None
|
|
)
|
|
custom_missing = any(
|
|
affix not in custom_values
|
|
for affix, policy in profile.custom_policies.items()
|
|
if policy is SCEAffixPolicy.REQUIRED
|
|
)
|
|
|
|
if rpad_missing or time_missing or to_missing or from_missing or custom_missing:
|
|
custom_missing_string = ""
|
|
for custom_affix in custom_affixes:
|
|
value = "present" if custom_affix in custom_values else "missing"
|
|
custom_missing_string += f", [custom]{custom_affix.element_name}={value}"
|
|
|
|
raise ProfileRequirementsNotMet(
|
|
f"SCE envelope is missing affixes required by the profile {profile}."
|
|
f" Affix presence:"
|
|
f" rpad={'missing' if rpad_missing else 'present'}"
|
|
f", time={'missing' if time_missing else 'present'}"
|
|
f", to={'missing' if to_missing else 'present'}"
|
|
f", from={'missing' if from_missing else 'present'}"
|
|
+ custom_missing_string
|
|
)
|
|
|
|
# Move elements that are not explicitly forbidden from being encrypted from the
|
|
# content element to the stanza.
|
|
for child in list(content.elements()):
|
|
if (
|
|
child.uri in XEP_0420.MUST_BE_PLAINTEXT_NAMESPACES
|
|
or (child.uri, child.name) in XEP_0420.MUST_BE_PLAINTEXT_ELEMENTS
|
|
):
|
|
log.warning(
|
|
f"An element that MUST be transferred in plaintext was found in an"
|
|
f" SCE envelope: {child.toXml()}"
|
|
)
|
|
else:
|
|
# Remove the child from the content element
|
|
content.children.remove(child)
|
|
|
|
# Add the child to the stanza
|
|
stanza.addChild(child)
|
|
|
|
return SCEAffixValues(
|
|
rpad_value, timestamp_value, recipient_value, sender_value, custom_values
|
|
)
|