from typing import cast, overload, Union from collections.abc import Collection from ._rfc7516.types import ( GeneralJSONSerialization, FlattenedJSONSerialization, ) from ._rfc7516.models import ( Recipient, CompactEncryption, GeneralJSONEncryption, FlattenedJSONEncryption, ) from ._rfc7516.registry import ( JWERegistry, default_registry, ) from ._rfc7516.message import perform_encrypt, perform_decrypt from ._rfc7516.compact import represent_compact, extract_compact from ._rfc7516.json import ( represent_general_json, represent_flattened_json, extract_general_json, extract_flattened_json, ) from .jwa import setup_jwe_algorithms from .jwk import Key, KeySet, ECKey, OKPKey, KeyFlexible, guess_key from .util import to_bytes from .registry import Header, reject_unprotected_crit_header __all__ = [ # types "GeneralJSONSerialization", "FlattenedJSONSerialization", # modules "JWERegistry", "Recipient", "CompactEncryption", "GeneralJSONEncryption", "FlattenedJSONEncryption", # methods "encrypt_compact", "decrypt_compact", "encrypt_json", "decrypt_json", # consts "default_registry", ] setup_jwe_algorithms() def encrypt_compact( protected: Header, plaintext: bytes | str, public_key: KeyFlexible, algorithms: Collection[str] | None = None, registry: JWERegistry | None = None, sender_key: ECKey | OKPKey | KeySet | None = None, ) -> str: """Generate a JWE Compact Serialization. The JWE Compact Serialization represents encrypted content as a compact, URL-safe string. This string is:: BASE64URL(UTF8(JWE Protected Header)) || '.' || BASE64URL(JWE Encrypted Key) || '.' || BASE64URL(JWE Initialization Vector) || '.' || BASE64URL(JWE Ciphertext) || '.' || BASE64URL(JWE Authentication Tag) :param protected: protected header part of the JWE, in dict :param plaintext: the content (message) to be encrypted :param public_key: a public key used to encrypt the CEK :param algorithms: a collection (list, tuple, or set) of allowed algorithms :param registry: a JWERegistry to use :param sender_key: only required when using ECDH-1PU :return: JWE Compact Serialization in bytes """ if algorithms: registry = JWERegistry(algorithms=algorithms) elif registry is None: registry = default_registry obj = CompactEncryption(protected, to_bytes(plaintext)) recipient: Recipient[Key] = Recipient(obj) key = guess_key(public_key, recipient, True, use="enc") key.check_use("enc") recipient.recipient_key = key if sender_key: recipient.sender_key = _guess_sender_key(recipient, sender_key, True) obj.recipient = recipient perform_encrypt(obj, registry) out = represent_compact(obj) return out.decode("utf-8") def decrypt_compact( value: bytes | str, private_key: KeyFlexible, algorithms: Collection[str] | None = None, registry: JWERegistry | None = None, sender_key: ECKey | OKPKey | KeySet | None = None, ) -> CompactEncryption: """Extract and validate the JWE Compact Serialization (in string, or bytes) with the given key. An JWE Compact Serialization looks like: .. code-block:: text :caption: line breaks for display purposes only OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb Sv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaV mqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je8 1860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi 6UklfCpIMfIjf7iGdXKHzg :param value: a string (or bytes) of the JWE Compact Serialization :param private_key: a flexible private key to decrypt the serialization :param algorithms: a collection (list, tuple, or set) of allowed algorithms :param registry: a JWERegistry to use :param sender_key: only required when using ECDH-1PU :return: object of the ``CompactEncryption`` """ if algorithms: registry = JWERegistry(algorithms=algorithms) elif registry is None: registry = default_registry obj = extract_compact(to_bytes(value), registry) recipient = obj.recipient assert recipient is not None key = guess_key(private_key, recipient, use="enc") key.check_use("enc") recipient.recipient_key = key if sender_key: recipient.sender_key = _guess_sender_key(recipient, sender_key) perform_decrypt(obj, registry) return obj @overload def encrypt_json( obj: GeneralJSONEncryption, public_key: KeyFlexible | None, algorithms: Collection[str] | None = None, registry: JWERegistry | None = None, sender_key: ECKey | OKPKey | KeySet | None = None, ) -> GeneralJSONSerialization: ... @overload def encrypt_json( obj: FlattenedJSONEncryption, public_key: KeyFlexible | None, algorithms: Collection[str] | None = None, registry: JWERegistry | None = None, sender_key: ECKey | OKPKey | KeySet | None = None, ) -> FlattenedJSONSerialization: ... def encrypt_json( obj: GeneralJSONEncryption | FlattenedJSONEncryption, public_key: KeyFlexible | None, algorithms: Collection[str] | None = None, registry: JWERegistry | None = None, sender_key: ECKey | OKPKey | KeySet | None = None, ) -> GeneralJSONSerialization | FlattenedJSONSerialization: """Generate a JWE JSON Serialization (in dict). The JWE JSON Serialization represents encrypted content as a JSON object. This representation is neither optimized for compactness nor URL safe. When calling this method, developers MUST construct an instance of a ``GeneralJSONEncryption`` or ``FlattenedJSONEncryption`` object. Here is an example:: from joserfc.jwe import GeneralJSONEncryption protected = {"enc": "A128CBC-HS256"} plaintext = b"hello world" header = {"jku": "https://server.example.com/keys.jwks"} # optional shared header obj = GeneralJSONEncryption(protected, plaintext, header) # add the recipients obj.add_recipient({"kid": "alice", "alg": "RSA1_5"}) # not configured a key bob_key = OctKey.import_key("bob secret") obj.add_recipient({"kid": "bob", "alg": "A128KW"}, bob_key) :param obj: an instance of ``GeneralJSONEncryption`` or ``FlattenedJSONEncryption`` :param public_key: a public key used to encrypt the CEK :param algorithms: a collection (list, tuple, or set) of allowed algorithms :param registry: a JWERegistry to use :param sender_key: only required when using ECDH-1PU :return: JWE JSON Serialization in dict """ if algorithms: registry = JWERegistry(algorithms=algorithms) elif registry is None: registry = default_registry reject_unprotected_crit_header(obj.unprotected) for recipient in obj.recipients: if sender_key and not recipient.sender_key: recipient.sender_key = _guess_sender_key(recipient, sender_key, True) if not recipient.recipient_key: assert public_key is not None key = guess_key(public_key, recipient, True, use="enc") key.check_use("enc") recipient.recipient_key = key perform_encrypt(obj, registry) if isinstance(obj, GeneralJSONEncryption): return represent_general_json(obj) return represent_flattened_json(obj) def decrypt_json( data: GeneralJSONSerialization | FlattenedJSONSerialization, private_key: KeyFlexible, algorithms: Collection[str] | None = None, registry: JWERegistry | None = None, sender_key: ECKey | OKPKey | KeySet | None = None, ) -> GeneralJSONEncryption | FlattenedJSONEncryption: """Decrypt the JWE JSON Serialization (in dict) to a ``GeneralJSONEncryption`` or ``FlattenedJSONEncryption`` object. :param data: JWE JSON Serialization in dict :param private_key: a flexible private key to decrypt the CEK :param algorithms: a collection (list, tuple, or set) of allowed algorithms :param registry: a JWERegistry to use :param sender_key: only required when using ECDH-1PU :return: an instance of ``GeneralJSONEncryption`` or ``FlattenedJSONEncryption`` """ if algorithms: registry = JWERegistry(algorithms=algorithms) elif registry is None: registry = default_registry reject_unprotected_crit_header(data.get("unprotected")) if "recipients" in data: general_obj = extract_general_json(cast(GeneralJSONSerialization, data), registry) _attach_recipient_keys(general_obj.recipients, private_key, sender_key) perform_decrypt(general_obj, registry) return general_obj else: flattened_obj = extract_flattened_json(cast(FlattenedJSONSerialization, data), registry) _attach_recipient_keys(flattened_obj.recipients, private_key, sender_key) perform_decrypt(flattened_obj, registry) return flattened_obj def _attach_recipient_keys( recipients: list[Recipient[Key]], private_key: KeyFlexible, sender_key: ECKey | OKPKey | KeySet | None = None ) -> None: for recipient in recipients: key = guess_key(private_key, recipient, use="enc") key.check_use("enc") recipient.recipient_key = key if sender_key: recipient.sender_key = _guess_sender_key(recipient, sender_key) def _guess_sender_key( recipient: Recipient[Key], key: ECKey | OKPKey | KeySet, use_random: bool = False ) -> ECKey | OKPKey: if not isinstance(key, KeySet): return key headers = recipient.headers() skid = headers.get("skid") if skid: skey = cast(Union[ECKey, OKPKey], key.get_by_kid(skid)) return skey if use_random: skey = cast(Union[ECKey, OKPKey], key.pick_random_key(headers["alg"])) if skey is not None: recipient.add_header("skid", skey.kid) return skey raise ValueError("Invalid key")