comparison sat/plugins/plugin_xep_0420.py @ 3911:8289ac1b34f4

plugin XEP-0384: Fully reworked to adjust to the reworked python-omemo: - support for both (modern) OMEMO under the `urn:xmpp:omemo:2` namespace and (legacy) OMEMO under the `eu.siacs.conversations.axolotl` namespace - maintains one identity across both versions of OMEMO - migrates data from the old plugin - includes more features for protocol stability - uses SCE for modern OMEMO - fully type-checked, linted and format-checked - added type hints to various pieces of backend code used by the plugin - added stubs for some Twisted APIs used by the plugin under stubs/ (use `export MYPYPATH=stubs/` before running mypy) - core (xmpp): enabled `send` trigger and made it an asyncPoint fix 375
author Syndace <me@syndace.dev>
date Tue, 23 Aug 2022 21:06:24 +0200
parents 00212260f659
children 626629781a53
comparison
equal deleted inserted replaced
3910:199598223f82 3911:8289ac1b34f4
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
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
19 # Type-check with `mypy --strict --disable-error-code no-untyped-call`
20 # Lint with `pylint`
21 18
22 from abc import ABC, abstractmethod 19 from abc import ABC, abstractmethod
23 from datetime import datetime 20 from datetime import datetime
24 import enum 21 import enum
25 import secrets 22 import secrets
26 import string 23 import string
27 from typing import Dict, Iterator, List, NamedTuple, Optional, Set, Tuple, Union, cast 24 from typing import Dict, NamedTuple, Optional, Set, Tuple, cast
28 25
29 from lxml import etree 26 from lxml import etree
30 27
31 from sat.core.constants import Const as C 28 from sat.core.constants import Const as C
32 from sat.core.i18n import D_ 29 from sat.core.i18n import D_
53 "SCEProfile", 50 "SCEProfile",
54 "SCEAffixValues" 51 "SCEAffixValues"
55 ] 52 ]
56 53
57 54
58 log = cast(Logger, getLogger(__name__)) 55 log = cast(Logger, getLogger(__name__)) # type: ignore[no-untyped-call]
59 56
60 57
61 PLUGIN_INFO = { 58 PLUGIN_INFO = {
62 C.PI_NAME: "SCE", 59 C.PI_NAME: "SCE",
63 C.PI_IMPORT_NAME: "XEP-0420", 60 C.PI_IMPORT_NAME: "XEP-0420",
289 content = envelope.addElement((NS_SCE, "content")) 286 content = envelope.addElement((NS_SCE, "content"))
290 287
291 # Note the serialized byte size of the content element before adding any children 288 # Note the serialized byte size of the content element before adding any children
292 empty_content_byte_size = len(content.toXml().encode("utf-8")) 289 empty_content_byte_size = len(content.toXml().encode("utf-8"))
293 290
294 # Just for type safety
295 stanza_children = cast(List[Union[domish.Element, str]], stanza.children)
296 content_children = cast(List[Union[domish.Element, str]], content.children)
297
298 # Move elements that are not explicitly forbidden from being encrypted from the 291 # Move elements that are not explicitly forbidden from being encrypted from the
299 # stanza to the content element. 292 # stanza to the content element.
300 for child in list(cast(Iterator[domish.Element], stanza.elements())): 293 for child in list(stanza.elements()):
301 if ( 294 if (
302 child.uri not in XEP_0420.MUST_BE_PLAINTEXT_NAMESPACES 295 child.uri not in XEP_0420.MUST_BE_PLAINTEXT_NAMESPACES
303 and (child.uri, child.name) not in XEP_0420.MUST_BE_PLAINTEXT_ELEMENTS 296 and (child.uri, child.name) not in XEP_0420.MUST_BE_PLAINTEXT_ELEMENTS
304 ): 297 ):
305 # Remove the child from the stanza 298 # Remove the child from the stanza
306 stanza_children.remove(child) 299 stanza.children.remove(child)
307 300
308 # A namespace of ``None`` can be used on domish elements to inherit the 301 # A namespace of ``None`` can be used on domish elements to inherit the
309 # namespace from the parent. When moving elements from the stanza root to 302 # namespace from the parent. When moving elements from the stanza root to
310 # the content element, however, we don't want elements to inherit the 303 # the content element, however, we don't want elements to inherit the
311 # namespace of the content element. Thus, check for elements with ``None`` 304 # namespace of the content element. Thus, check for elements with ``None``
314 if child.uri is None: 307 if child.uri is None:
315 child.uri = C.NS_CLIENT 308 child.uri = C.NS_CLIENT
316 child.defaultUri = C.NS_CLIENT 309 child.defaultUri = C.NS_CLIENT
317 310
318 # Add the child with corrected namespaces to the content element 311 # Add the child with corrected namespaces to the content element
319 content_children.append(child) 312 content.addChild(child)
320 313
321 # Add the affixes requested by the profile 314 # Add the affixes requested by the profile
322 if profile.rpad_policy is not SCEAffixPolicy.NOT_NEEDED: 315 if profile.rpad_policy is not SCEAffixPolicy.NOT_NEEDED:
323 # The specification defines the rpad affix to contain "[...] a randomly 316 # The specification defines the rpad affix to contain "[...] a randomly
324 # generated sequence of random length between 0 and 200 characters." This 317 # generated sequence of random length between 0 and 200 characters." This
343 if profile.time_policy is not SCEAffixPolicy.NOT_NEEDED: 336 if profile.time_policy is not SCEAffixPolicy.NOT_NEEDED:
344 time_element = envelope.addElement((NS_SCE, "time")) 337 time_element = envelope.addElement((NS_SCE, "time"))
345 time_element["stamp"] = XEP_0082.format_datetime() 338 time_element["stamp"] = XEP_0082.format_datetime()
346 339
347 if profile.to_policy is not SCEAffixPolicy.NOT_NEEDED: 340 if profile.to_policy is not SCEAffixPolicy.NOT_NEEDED:
348 recipient = cast(Optional[str], stanza.getAttribute("to", None)) 341 recipient = stanza.getAttribute("to", None)
349 if recipient is None: 342 if recipient is None:
350 raise ValueError( 343 raise ValueError(
351 "<to/> affix requested, but stanza doesn't have the 'to' attribute" 344 "<to/> affix requested, but stanza doesn't have the 'to' attribute"
352 " set." 345 " set."
353 ) 346 )
354 347
355 to_element = envelope.addElement((NS_SCE, "to")) 348 to_element = envelope.addElement((NS_SCE, "to"))
356 to_element["jid"] = jid.JID(recipient).userhost() 349 to_element["jid"] = jid.JID(recipient).userhost()
357 350
358 if profile.from_policy is not SCEAffixPolicy.NOT_NEEDED: 351 if profile.from_policy is not SCEAffixPolicy.NOT_NEEDED:
359 sender = cast(Optional[str], stanza.getAttribute("from", None)) 352 sender = stanza.getAttribute("from", None)
360 if sender is None: 353 if sender is None:
361 raise ValueError( 354 raise ValueError(
362 "<from/> affix requested, but stanza doesn't have the 'from'" 355 "<from/> affix requested, but stanza doesn't have the 'from'"
363 " attribute set." 356 " attribute set."
364 ) 357 )
368 361
369 for affix, policy in profile.custom_policies.items(): 362 for affix, policy in profile.custom_policies.items():
370 if policy is not SCEAffixPolicy.NOT_NEEDED: 363 if policy is not SCEAffixPolicy.NOT_NEEDED:
371 envelope.addChild(affix.create(stanza)) 364 envelope.addChild(affix.create(stanza))
372 365
373 return cast(str, envelope.toXml()).encode("utf-8") 366 return envelope.toXml().encode("utf-8")
374 367
375 @staticmethod 368 @staticmethod
376 def unpack_stanza( 369 def unpack_stanza(
377 profile: SCEProfile, 370 profile: SCEProfile,
378 stanza: domish.Element, 371 stanza: domish.Element,
429 except etree.XMLSyntaxError as e: 422 except etree.XMLSyntaxError as e:
430 raise ValueError("Serialized envelope doesn't pass schema validation.") from e 423 raise ValueError("Serialized envelope doesn't pass schema validation.") from e
431 424
432 # Prepare the envelope and content elements 425 # Prepare the envelope and content elements
433 envelope = cast(domish.Element, ElementParser()(envelope_serialized_string)) 426 envelope = cast(domish.Element, ElementParser()(envelope_serialized_string))
434 content = cast(domish.Element, next(envelope.elements(NS_SCE, "content"))) 427 content = next(envelope.elements(NS_SCE, "content"))
435 428
436 # Verify the affixes 429 # Verify the affixes
437 rpad_element = cast( 430 rpad_element = cast(
438 Optional[domish.Element], 431 Optional[domish.Element],
439 next(envelope.elements(NS_SCE, "rpad"), None) 432 next(envelope.elements(NS_SCE, "rpad"), None)
466 # specification. 459 # specification.
467 recipient_value: Optional[jid.JID] = None 460 recipient_value: Optional[jid.JID] = None
468 if to_element is not None: 461 if to_element is not None:
469 recipient_value = jid.JID(to_element["jid"]) 462 recipient_value = jid.JID(to_element["jid"])
470 463
471 recipient_actual = cast(Optional[str], stanza.getAttribute("to", None)) 464 recipient_actual = stanza.getAttribute("to", None)
472 if recipient_actual is None: 465 if recipient_actual is None:
473 raise AffixVerificationFailed( 466 raise AffixVerificationFailed(
474 "'To' affix is included in the envelope, but the stanza is lacking a" 467 "'To' affix is included in the envelope, but the stanza is lacking a"
475 " 'to' attribute to compare the value to." 468 " 'to' attribute to compare the value to."
476 ) 469 )
489 # the specification. 482 # the specification.
490 sender_value: Optional[jid.JID] = None 483 sender_value: Optional[jid.JID] = None
491 if from_element is not None: 484 if from_element is not None:
492 sender_value = jid.JID(from_element["jid"]) 485 sender_value = jid.JID(from_element["jid"])
493 486
494 sender_actual = cast(Optional[str], stanza.getAttribute("from", None)) 487 sender_actual = stanza.getAttribute("from", None)
495 if sender_actual is None: 488 if sender_actual is None:
496 raise AffixVerificationFailed( 489 raise AffixVerificationFailed(
497 "'From' affix is included in the envelope, but the stanza is lacking" 490 "'From' affix is included in the envelope, but the stanza is lacking"
498 " a 'from' attribute to compare the value to." 491 " a 'from' attribute to compare the value to."
499 ) 492 )
549 f", to={'missing' if to_missing else 'present'}" 542 f", to={'missing' if to_missing else 'present'}"
550 f", from={'missing' if from_missing else 'present'}" 543 f", from={'missing' if from_missing else 'present'}"
551 + custom_missing_string 544 + custom_missing_string
552 ) 545 )
553 546
554 # Just for type safety
555 content_children = cast(List[Union[domish.Element, str]], content.children)
556 stanza_children = cast(List[Union[domish.Element, str]], stanza.children)
557
558 # Move elements that are not explicitly forbidden from being encrypted from the 547 # Move elements that are not explicitly forbidden from being encrypted from the
559 # content element to the stanza. 548 # content element to the stanza.
560 for child in list(cast(Iterator[domish.Element], content.elements())): 549 for child in list(content.elements()):
561 if ( 550 if (
562 child.uri in XEP_0420.MUST_BE_PLAINTEXT_NAMESPACES 551 child.uri in XEP_0420.MUST_BE_PLAINTEXT_NAMESPACES
563 or (child.uri, child.name) in XEP_0420.MUST_BE_PLAINTEXT_ELEMENTS 552 or (child.uri, child.name) in XEP_0420.MUST_BE_PLAINTEXT_ELEMENTS
564 ): 553 ):
565 log.warning( 554 log.warning(
566 f"An element that MUST be transferred in plaintext was found in an" 555 f"An element that MUST be transferred in plaintext was found in an"
567 f" SCE envelope: {child.toXml()}" 556 f" SCE envelope: {child.toXml()}"
568 ) 557 )
569 else: 558 else:
570 # Remove the child from the content element 559 # Remove the child from the content element
571 content_children.remove(child) 560 content.children.remove(child)
572 561
573 # Add the child to the stanza 562 # Add the child to the stanza
574 stanza_children.append(child) 563 stanza.addChild(child)
575 564
576 return SCEAffixValues( 565 return SCEAffixValues(
577 rpad_value, 566 rpad_value,
578 timestamp_value, 567 timestamp_value,
579 recipient_value, 568 recipient_value,