Mercurial > libervia-backend
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, |