Source code for aeat.application.modelo._review_package_recipient_encryption

"""Encrypt-for-recipient transport for review packages (X25519 ECIES).

This module adds a CONFIDENTIALITY layer on top of the review-package
checksum-integrity (:mod:`~application.modelo._review_package`) and
authenticity (:mod:`~application.modelo._review_package_signing`,
:mod:`~application.modelo._review_package_counter_sign`) layers: a
review package sealed with :func:`encrypt_review_package_for_recipient`
can be opened only by the holder of the matching X25519 private key --
unlike ``sign``/``counter-sign``, which leave the archive itself in
plaintext ZIP form.

Construction (ECIES over the primitives already vetted and already
shipped by this project -- no new dependency, see
``2026-07-04-recipient-encryption-adr``):

1. A fresh EPHEMERAL X25519 keypair is generated for this one message
   (``cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.generate()``).
2. ECDH is performed between the ephemeral private key and the
   recipient's long-term public key
   (:class:`~application.modelo.RecipientFingerprintRecord`),
   producing a 32-byte shared secret.
3. The shared secret is NEVER used directly as an AEAD key. It is run
   through :func:`~adapters.persistence.storage.crypto.derive_key`
   (HKDF-SHA256), with the HKDF ``salt`` set to the ephemeral public key
   and the ``context`` (HKDF ``info``) bound to a fixed domain-separation
   string PLUS the recipient's public key -- so a derived key can never
   be reused across a different ephemeral sender key or a different
   recipient.
4. The package bytes are AEAD-encrypted via
   :func:`~adapters.persistence.storage.crypto.encrypt_record`
   (AES-256-GCM), with the recipient's public key bound into the
   associated data -- so a ciphertext cannot be silently re-targeted at
   a different recipient's key without the AEAD tag failing to verify.
5. The returned :class:`RecipientEncryptedPackage` envelope carries the
   ephemeral public key, the recipient's public key, and the AEAD wire
   bytes -- everything the recipient needs to reverse the ECDH and
   decrypt, and nothing that identifies the sender (no long-term sender
   keypair is required or persisted for this direction).

Key custody (``sensitive-financial-data-secure-storage-only``): the
review-package bytes are read into memory, encrypted in memory, and the
CIPHERTEXT envelope is the only artefact this module returns to the
caller; the caller is responsible for writing the envelope bytes to its
requested output path. Nothing is staged to a temp file. The recipient's
public key carries no secrecy requirement (it is looked up from
:class:`~application.modelo.RecipientFingerprintRegistryRepository`);
the ephemeral sender private key exists only for the duration of one
call and is never persisted.

Expiry and replay defence (``2026-07-04-recipient-encryption-adr``, the
review-only/expiry/replay follow-up slice): every envelope carries an
``issued_at`` timestamp, an optional ``valid_until`` deadline, a random
``envelope_nonce_hex`` (independent of the AEAD nonce embedded in
``ciphertext``, minted purely as a replay-detection token), and a
``review_only`` flag.

* **Expiry** is checked entirely inside :func:`decrypt_review_package_for_recipient`
  against an explicit, caller-supplied ``now`` (never the wall clock read
  directly by this module, so the check is deterministic and testable): a
  package presented after its ``valid_until`` deadline is refused before AEAD
  decryption is even attempted. A ``valid_until=None`` envelope never expires.
* **Replay defence** is a TWO-PARTY contract this module only half-owns: the
  envelope's ``envelope_nonce_hex`` is the token a caller checks against
  :class:`~application.modelo.RecipientReplayGuardRepository` (a
  persisted, bucket-scoped consumed-nonce ledger) before or after calling
  :func:`decrypt_review_package_for_recipient` -- this module mints and
  carries the nonce but performs no persistence itself (this is the
  ``composition-service-no-parallel-write-path`` boundary: encryption and
  decryption stay pure in-memory primitives, and the CLI decrypt-side
  composition owns the ledger check).
* **Review-only mode** (``review_only=True``) asserts the sealed package
  carries no filing authority: the recipient may read and verify it, but it
  is NOT evidence that the underlying revision has been (or will be) filed
  with AEAT. :func:`decrypt_review_package_for_recipient` returns a typed
  :class:`RecipientDecryptedPackage` (bytes plus the ``review_only`` flag)
  rather than bare ``bytes``, so a downstream consumer cannot lose the flag
  and mistake a review-only handoff for a filing artefact.

Recipient's own keypair (mint-or-load, symmetric to
:func:`~application.modelo.ensure_review_package_signing_keypair`): a
recipient (the accountant running :func:`decrypt_review_package_for_recipient`
against a package sealed for them) needs their OWN X25519 private key, matching
the public key a taxpayer registered via
:class:`~application.modelo.RecipientFingerprintRegistryRepository`.
:func:`ensure_recipient_encryption_keypair` mints one on first use and persists
it -- private key included -- ONLY as ciphertext through a
:class:`~adapters.persistence.storage.SecureObjectRepository`, at
:class:`~adapters.persistence.storage.SensitivityClass` ``SECRET``
(:data:`~adapters.persistence.storage.MODELO_REVIEW_PACKAGE_RECIPIENT_ENCRYPTION_KEY_NAMESPACE`),
exactly as the Ed25519 signing keypair is minted and stored. It is never
logged, never written to a plaintext file, and never leaves this module as raw
bytes except transiently in process memory to decrypt. The exportable public
half (:func:`recipient_encryption_public_key`) is what a taxpayer registers via
the fingerprint registry -- never the private key.

See Also:
    :mod:`~application.modelo._review_package_recipient_registry`
        Where a recipient's trusted public key is registered and looked
        up before calling this module.
    :mod:`~application.modelo._review_package_recipient_replay_guard`
        The consumed-nonce ledger a caller composes around
        :func:`decrypt_review_package_for_recipient` for replay defence.
    :mod:`~application.modelo._review_package`
        Builds and integrity-verifies the review package this module
        encrypts.
    :func:`~application.modelo.ensure_review_package_signing_keypair`
        The Ed25519 signing-keypair primitive this module's
        :func:`ensure_recipient_encryption_keypair` mirrors exactly (mint-once,
        persist-as-ciphertext, idempotent-reuse), for a distinct purpose
        (encryption, never signing -- see the ADR's rejection of key reuse
        across purposes).
"""

from __future__ import annotations

import secrets
from datetime import datetime, timedelta
from typing import TYPE_CHECKING

from cryptography.hazmat.primitives.asymmetric.x25519 import (
    X25519PrivateKey,
    X25519PublicKey,
)
from pydantic import BaseModel, Field, field_serializer, field_validator, model_validator

from ...adapters.persistence.storage import (
    MODELO_REVIEW_PACKAGE_RECIPIENT_ENCRYPTION_KEY_NAMESPACE as _NAMESPACE,
)
from ...adapters.persistence.storage import DecryptionError, SensitivityClass
from ...adapters.persistence.storage.crypto import EncryptedBlob, decrypt_record, derive_key, encrypt_record
from ...core import STRICT_FROZEN_CONFIG as _STRICT_FROZEN
from ...core.errors import AeatError
from ...core.external_constants import UTF_8_ENCODING
from ...core.time import now as _utc_now

if TYPE_CHECKING:
    from ...adapters.persistence.storage import SecureObjectRepository

#: Wire-format version of the recipient-encryption envelope. Bumped when
#: the envelope schema changes shape.
_RECIPIENT_ENCRYPTION_ENVELOPE_VERSION = 1

#: HKDF domain-separation context. Distinct from any other HKDF context
#: string used elsewhere in the codebase (e.g. secure-object row keys,
#: DEK wrapping) so a key derived here can never collide with a key
#: derived for an unrelated purpose even if the same shared secret were
#: (incorrectly) reused.
_HKDF_CONTEXT_PREFIX = b"aeat.review_package.recipient_encryption.v1"

_HEX_PATTERN_64 = r"^[0-9a-f]{64}$"

#: Byte length of the replay-detection nonce (independent of, and never
#: reused as, the AEAD nonce embedded inside ``ciphertext``).
_REPLAY_NONCE_BYTES = 32


[docs] class RecipientEncryptionError(AeatError): """Base error for review-package recipient-encryption failures."""
[docs] class RecipientDecryptionError(RecipientEncryptionError): """Raised when a recipient-encrypted package fails to decrypt. Covers cryptographic AEAD-tag failure (tampered ciphertext or wrong private key), a mismatched recipient public key (the caller's private key does not correspond to the envelope's declared recipient public key), and an expired ``valid_until`` deadline -- never distinguished further, so an attacker cannot use error content to learn which check failed. """
[docs] class RecipientEncryptionKeyNotFoundError(RecipientEncryptionError): """Raised when no encryption keypair has been minted for a bucket yet. Callers should mint one via :func:`ensure_recipient_encryption_keypair` before loading it explicitly. """
[docs] class RecipientEncryptionKeypair(BaseModel): """A bucket's X25519 encryption keypair, private key included. This model is the PLAINTEXT in-memory shape used only transiently around generation, persistence, and decryption; :meth:`private_key` / :meth:`public_key` reconstruct live ``cryptography`` key objects from the stored raw hex bytes. The caller (:func:`ensure_recipient_encryption_keypair`) is responsible for persisting it only through :class:`~adapters.persistence.storage.SecureObjectRepository`, mirroring :class:`~application.modelo.ReviewPackageSigningKeypair` exactly -- a distinct keypair, for a distinct purpose (encryption, never signing). """ model_config = _STRICT_FROZEN bucket_id: str = Field(min_length=1) private_key_hex: str = Field(pattern=_HEX_PATTERN_64) public_key_hex: str = Field(pattern=_HEX_PATTERN_64) created_at: datetime
[docs] def private_key(self) -> X25519PrivateKey: """Reconstruct the live :class:`X25519PrivateKey` from stored raw bytes.""" return X25519PrivateKey.from_private_bytes(bytes.fromhex(self.private_key_hex))
[docs] def public_key(self) -> X25519PublicKey: """Reconstruct the live :class:`X25519PublicKey` from stored raw bytes.""" return X25519PublicKey.from_public_bytes(bytes.fromhex(self.public_key_hex))
[docs] class RecipientEncryptionPublicKey(BaseModel): """The exportable, non-secret half of a bucket's encryption keypair. Safe to hand to a taxpayer so they can register it via :class:`~application.modelo.RecipientFingerprintRegistryRepository`. Carries no secrecy requirement -- unlike :class:`RecipientEncryptionKeypair`, this model is fine to print, write to a plaintext file, or read aloud for out-of-band fingerprint verification. """ model_config = _STRICT_FROZEN bucket_id: str = Field(min_length=1) public_key_hex: str = Field(pattern=_HEX_PATTERN_64) created_at: datetime
def _recipient_encryption_key_object_key(bucket_id: str) -> str: """Return the natural :class:`~adapters.persistence.storage.SecureObjectRepository` key for ``bucket_id``'s keypair. Matches the namespace's declared ``object_key_grammar="review-package-recipient-encryption-key:{bucket_id}"``. """ return f"review-package-recipient-encryption-key:{bucket_id.strip()}"
[docs] def ensure_recipient_encryption_keypair( *, bucket_id: str, repository: SecureObjectRepository, generated_at: datetime | None = None, ) -> RecipientEncryptionKeypair: """Return the bucket's X25519 encryption keypair, minting one on first use. Mirrors :func:`~application.modelo.ensure_review_package_signing_keypair` exactly: loads the existing keypair from :data:`~adapters.persistence.storage.MODELO_REVIEW_PACKAGE_RECIPIENT_ENCRYPTION_KEY_NAMESPACE` when present; otherwise generates a fresh keypair via ``X25519PrivateKey.generate()``, persists it (private key included) as ciphertext, and returns it. Idempotent: a second call against the same bucket returns the SAME keypair rather than rotating it, so a package sealed for the recipient's public key today still decrypts next week. Args: bucket_id: The bucket this keypair is scoped to (the recipient's own profile bucket, resolved the same way the signing keypair is). repository: The bucket's :class:`~adapters.persistence.storage.SecureObjectRepository`. generated_at: Optional override for the keypair's ``created_at`` timestamp (tests only); defaults to the current UTC time. """ object_key = _recipient_encryption_key_object_key(bucket_id) existing = repository.load( _NAMESPACE.namespace, object_key, expected_class=SensitivityClass.SECRET, max_supported_version=_NAMESPACE.schema_version, ) if existing is not None: return RecipientEncryptionKeypair.model_validate_json(existing.payload) private_key = X25519PrivateKey.generate() public_key = private_key.public_key() keypair = RecipientEncryptionKeypair( bucket_id=bucket_id, private_key_hex=private_key.private_bytes_raw().hex(), public_key_hex=public_key.public_bytes_raw().hex(), created_at=generated_at or _utc_now(), ) repository.save( namespace=_NAMESPACE.namespace, object_key=object_key, classification=SensitivityClass.SECRET, schema_version=_NAMESPACE.schema_version, written_at=keypair.created_at, payload=keypair.model_dump_json().encode(UTF_8_ENCODING), write_provenance="application.modelo.review_package_recipient_encryption.ensure_keypair", ) return keypair
[docs] def load_recipient_encryption_keypair( *, bucket_id: str, repository: SecureObjectRepository, ) -> RecipientEncryptionKeypair: """Load the bucket's existing X25519 encryption keypair. Args: bucket_id: The bucket this keypair is scoped to. repository: The bucket's :class:`~adapters.persistence.storage.SecureObjectRepository`. Raises: RecipientEncryptionKeyNotFoundError: If no keypair has been minted yet for ``bucket_id``. Call :func:`ensure_recipient_encryption_keypair` first. """ object_key = _recipient_encryption_key_object_key(bucket_id) record = repository.load( _NAMESPACE.namespace, object_key, expected_class=SensitivityClass.SECRET, max_supported_version=_NAMESPACE.schema_version, ) if record is None: raise RecipientEncryptionKeyNotFoundError( translated_message="application.modelo.errors.recipient_encryption_key_not_found", context={"bucket_id": bucket_id}, ) return RecipientEncryptionKeypair.model_validate_json(record.payload)
[docs] def recipient_encryption_public_key( keypair: RecipientEncryptionKeypair, ) -> RecipientEncryptionPublicKey: """Project the exportable public half out of a full keypair. The projection never touches ``private_key_hex``; the returned model is safe to hand to a taxpayer to register via the fingerprint registry. """ return RecipientEncryptionPublicKey( bucket_id=keypair.bucket_id, public_key_hex=keypair.public_key_hex, created_at=keypair.created_at, )
[docs] class RecipientEncryptedPackage(BaseModel): """Wire envelope for a review package encrypted for one recipient. ``ephemeral_public_key_hex`` and ``recipient_public_key_hex`` are both raw 32-byte X25519 public keys, hex-encoded. ``ciphertext`` is the AEAD wire form (``nonce || ciphertext_with_tag``) produced by :func:`~adapters.persistence.storage.crypto.encrypt_record`, held as raw ``bytes`` on the Python object (matching every in-process caller in this module) but hex-encoded on the JSON boundary (:meth:`model_dump_json` / ``model_dump(mode="json")``) -- pydantic's default JSON encoding for ``bytes`` assumes valid UTF-8, which arbitrary AEAD ciphertext is not, so a bare ``bytes`` field would raise ``PydanticSerializationError`` the first time a caller (e.g. the CLI ``encrypt-for-recipient`` verb) writes the envelope to disk as JSON. :meth:`model_validate_json` accepts the hex form it produced; the plain-Python constructor still accepts raw ``bytes`` directly. ``envelope_nonce_hex`` is a replay-detection token, independent of the AEAD nonce embedded in ``ciphertext``: a caller checks it against :class:`~application.modelo.RecipientReplayGuardRepository` to refuse a package presented more than once. ``issued_at`` / ``valid_until`` bound the envelope's validity window (``valid_until`` of ``None`` means the envelope never expires); the deadline is checked inside :func:`decrypt_review_package_for_recipient` against an explicit caller-supplied ``now``, never the wall clock read by this module. ``review_only`` asserts the sealed package carries no filing authority -- see the module docstring. """ model_config = _STRICT_FROZEN envelope_version: int = Field(default=_RECIPIENT_ENCRYPTION_ENVELOPE_VERSION, ge=1) ephemeral_public_key_hex: str = Field(pattern=_HEX_PATTERN_64) recipient_public_key_hex: str = Field(pattern=_HEX_PATTERN_64) ciphertext: bytes = Field(min_length=1) envelope_nonce_hex: str = Field(pattern=_HEX_PATTERN_64) issued_at: datetime valid_until: datetime | None = Field(default=None) review_only: bool = Field(default=False) @field_validator("ciphertext", mode="before") @classmethod def _ciphertext_accepts_hex_or_raw_bytes(cls, value: object) -> object: if isinstance(value, str): return bytes.fromhex(value) return value @field_serializer("ciphertext", when_used="json") def _ciphertext_as_hex_for_json(self, value: bytes) -> str: return value.hex() @model_validator(mode="after") def _valid_until_is_after_issued_at(self) -> RecipientEncryptedPackage: if self.valid_until is not None and self.valid_until <= self.issued_at: raise ValueError("valid_until must be strictly after issued_at") return self
def _hkdf_context(recipient_public_key_hex: str) -> bytes: """Return the HKDF ``info`` bytes binding a derived key to one recipient. Domain-separates the derived AEAD key from any other HKDF use in the codebase, and from a derivation for a *different* recipient's public key, so the same ephemeral keypair could never (even by caller error) yield the same derived key for two different recipients. """ return _HKDF_CONTEXT_PREFIX + b":" + recipient_public_key_hex.encode("ascii") def _associated_data(recipient_public_key_hex: str) -> bytes: """Return the AEAD associated data binding ciphertext to one recipient. Any attempt to present this ciphertext as encrypted for a different recipient's public key fails AEAD authentication, because the associated data would no longer match. """ return b"aeat.review_package.recipient:" + recipient_public_key_hex.encode("ascii")
[docs] def encrypt_review_package_for_recipient( package_bytes: bytes, *, recipient_public_key_hex: str, review_only: bool = False, valid_for: timedelta | None = None, issued_at: datetime | None = None, ) -> RecipientEncryptedPackage: """Seal ``package_bytes`` so only ``recipient_public_key_hex``'s holder can open it. Generates a fresh ephemeral X25519 keypair, performs ECDH against the recipient's public key, derives a per-message AES-256-GCM key via HKDF-SHA256, and encrypts. See the module docstring for the full construction. Never writes ``package_bytes`` or the derived key to disk; the caller is responsible for persisting the returned envelope's bytes. Args: package_bytes: The plaintext review-package archive bytes to seal (read into memory by the caller; this function performs no filesystem I/O). recipient_public_key_hex: The recipient's raw 32-byte X25519 public key, hex-encoded (see :class:`~application.modelo.RecipientFingerprintRecord`). review_only: When ``True``, marks the sealed package as carrying no filing authority -- see the module docstring. Defaults to ``False`` (a normal filing-grade handoff). valid_for: Optional validity window measured from ``issued_at``. When supplied, the envelope's ``valid_until`` is ``issued_at + valid_for`` and :func:`decrypt_review_package_for_recipient` refuses the package once that deadline has passed. ``None`` (the default) produces an envelope that never expires. issued_at: Optional override for the envelope's ``issued_at`` timestamp (tests only); defaults to the current UTC time. Raises: RecipientEncryptionError: If ``recipient_public_key_hex`` is not a well-formed X25519 public key, or if ``valid_for`` is not a strictly positive duration. """ try: recipient_public_key = X25519PublicKey.from_public_bytes(bytes.fromhex(recipient_public_key_hex)) except (ValueError, TypeError) as exc: raise RecipientEncryptionError( "recipient_public_key_hex is not a well-formed X25519 public key", translated_message="application.modelo.errors.recipient_encryption_invalid_key", ) from exc if valid_for is not None and valid_for <= timedelta(0): raise RecipientEncryptionError( "valid_for must be a strictly positive duration", translated_message="application.modelo.errors.recipient_encryption_invalid_key", ) ephemeral_private_key = X25519PrivateKey.generate() ephemeral_public_key = ephemeral_private_key.public_key() ephemeral_public_key_hex = ephemeral_public_key.public_bytes_raw().hex() shared_secret = ephemeral_private_key.exchange(recipient_public_key) derived_key = derive_key( key_material=shared_secret, salt=ephemeral_public_key.public_bytes_raw(), context=_hkdf_context(recipient_public_key_hex), ) encrypted = encrypt_record( package_bytes, key=derived_key, associated_data=_associated_data(recipient_public_key_hex), ) envelope_issued_at = issued_at or _utc_now() envelope_valid_until = envelope_issued_at + valid_for if valid_for is not None else None return RecipientEncryptedPackage( ephemeral_public_key_hex=ephemeral_public_key_hex, recipient_public_key_hex=recipient_public_key_hex, ciphertext=encrypted.to_wire(), envelope_nonce_hex=secrets.token_hex(_REPLAY_NONCE_BYTES), issued_at=envelope_issued_at, valid_until=envelope_valid_until, review_only=review_only, )
[docs] class RecipientPackageExpiredError(RecipientDecryptionError): """Raised when a recipient-encrypted package is presented past its ``valid_until`` deadline. A subclass of :class:`RecipientDecryptionError` (rather than a sibling) so an existing ``except RecipientDecryptionError`` catch-all keeps working verbatim; callers that need to distinguish expiry from a cryptographic failure may catch this subclass specifically, though the ADR's undifferentiated-failure posture means the rendered message is identical either way. """
[docs] class RecipientDecryptedPackage(BaseModel): """Recovered plaintext bytes plus the envelope's carried disposition flags. Returned by :func:`decrypt_review_package_for_recipient` instead of bare ``bytes`` so a downstream consumer cannot lose the ``review_only`` flag and mistake a review-only handoff for a filing-grade artefact. """ model_config = _STRICT_FROZEN package_bytes: bytes = Field(min_length=1) review_only: bool
[docs] def decrypt_review_package_for_recipient( envelope: RecipientEncryptedPackage, *, recipient_private_key: X25519PrivateKey, now: datetime | None = None, ) -> RecipientDecryptedPackage: """Reverse :func:`encrypt_review_package_for_recipient` and return the package bytes. Reconstructs the same derived AEAD key by performing ECDH between ``recipient_private_key`` and the envelope's ephemeral public key, then decrypts and authenticates. A wrong ``recipient_private_key`` (or any tampering of ``envelope.ciphertext`` or the declared public keys) fails AEAD authentication. Before any cryptographic work, the envelope's ``valid_until`` deadline (when set) is checked against ``now``; an expired envelope is refused without attempting decryption. Replay defence is NOT performed here: this function is a pure encrypt/decrypt primitive with no persistence dependency (``composition-service-no-parallel-write-path``). A caller that needs replay defence composes :class:`~application.modelo.RecipientReplayGuardRepository` around this call, keyed on ``envelope.envelope_nonce_hex``. Args: envelope: The :class:`RecipientEncryptedPackage` produced by :func:`encrypt_review_package_for_recipient`. recipient_private_key: The recipient's own X25519 private key. now: The instant to evaluate ``envelope.valid_until`` against. Defaults to the current UTC time; tests inject an explicit value rather than relying on the wall clock. Returns: A :class:`RecipientDecryptedPackage` carrying the original plaintext review-package archive bytes and the envelope's ``review_only`` disposition. Raises: RecipientPackageExpiredError: If ``envelope.valid_until`` is set and ``now`` is at or past that deadline. RecipientDecryptionError: If ``recipient_private_key`` does not match the envelope's declared recipient public key, or the ciphertext fails AEAD authentication for any reason (tampering, corruption, wrong key). """ evaluated_at = now or _utc_now() if envelope.valid_until is not None and evaluated_at >= envelope.valid_until: raise RecipientPackageExpiredError( "recipient-encrypted package has expired; the recipient must request a fresh package", translated_message="application.modelo.errors.recipient_decryption_failed", ) recipient_public_key_hex = recipient_private_key.public_key().public_bytes_raw().hex() if recipient_public_key_hex != envelope.recipient_public_key_hex: raise RecipientDecryptionError( "recipient_private_key does not match the envelope's declared recipient public key", translated_message="application.modelo.errors.recipient_decryption_failed", ) ephemeral_public_key = X25519PublicKey.from_public_bytes(bytes.fromhex(envelope.ephemeral_public_key_hex)) shared_secret = recipient_private_key.exchange(ephemeral_public_key) derived_key = derive_key( key_material=shared_secret, salt=bytes.fromhex(envelope.ephemeral_public_key_hex), context=_hkdf_context(envelope.recipient_public_key_hex), ) try: recovered = decrypt_record( EncryptedBlob.from_wire(envelope.ciphertext), key=derived_key, associated_data=_associated_data(envelope.recipient_public_key_hex), ) except DecryptionError as exc: raise RecipientDecryptionError( "recipient-encrypted package failed AEAD authentication", translated_message="application.modelo.errors.recipient_decryption_failed", ) from exc return RecipientDecryptedPackage(package_bytes=recovered, review_only=envelope.review_only)
__all__ = [ "RecipientDecryptedPackage", "RecipientDecryptionError", "RecipientEncryptedPackage", "RecipientEncryptionError", "RecipientEncryptionKeyNotFoundError", "RecipientEncryptionKeypair", "RecipientEncryptionPublicKey", "RecipientPackageExpiredError", "decrypt_review_package_for_recipient", "encrypt_review_package_for_recipient", "ensure_recipient_encryption_keypair", "load_recipient_encryption_keypair", "recipient_encryption_public_key", ]