first commit

This commit is contained in:
linkitapps
2026-09-14 23:22:10 +09:00
commit 9f9c8fb69b
4207 changed files with 1333187 additions and 0 deletions
@@ -0,0 +1,75 @@
from .models import CompactEncryption, Recipient
from .registry import JWERegistry
from .._keys import Key
from ..errors import (
MissingAlgorithmError,
MissingEncryptionError,
DecodeError,
)
from ..util import (
json_b64decode,
urlsafe_b64encode,
urlsafe_b64decode,
)
__all__ = [
"represent_compact",
"extract_compact",
]
def represent_compact(obj: CompactEncryption) -> bytes:
assert obj.recipient is not None
encrypted_key = obj.recipient.encrypted_key
assert encrypted_key is not None
return b".".join(
[
obj.base64_segments["aad"],
urlsafe_b64encode(encrypted_key),
obj.base64_segments["iv"],
obj.base64_segments["ciphertext"],
obj.base64_segments["tag"],
]
)
def extract_compact(value: bytes, registry: JWERegistry) -> CompactEncryption:
parts = value.split(b".")
if len(parts) != 5:
raise ValueError("Invalid JSON Web Encryption")
header_segment, ek_segment, iv_segment, ciphertext_segment, tag_segment = parts
registry.validate_protected_header_size(header_segment)
registry.validate_encrypted_key_size(ek_segment)
registry.validate_initialization_vector_size(iv_segment)
registry.validate_ciphertext_size(ciphertext_segment)
registry.validate_auth_tag_size(tag_segment)
try:
protected = json_b64decode(header_segment)
if "alg" not in protected:
raise MissingAlgorithmError()
if "enc" not in protected:
raise MissingEncryptionError()
except (TypeError, ValueError):
raise DecodeError("Invalid header")
obj = CompactEncryption(protected)
obj.base64_segments.update(
{
"aad": header_segment,
"iv": iv_segment,
"ciphertext": ciphertext_segment,
"tag": tag_segment,
}
)
obj.bytes_segments.update(
{
"iv": urlsafe_b64decode(iv_segment),
"ciphertext": urlsafe_b64decode(ciphertext_segment),
"tag": urlsafe_b64decode(tag_segment),
}
)
recipient: Recipient[Key] = Recipient(obj)
recipient.encrypted_key = urlsafe_b64decode(ek_segment)
obj.recipient = recipient
return obj
@@ -0,0 +1,142 @@
from typing import Any
from .models import (
BaseJSONEncryption,
GeneralJSONEncryption,
FlattenedJSONEncryption,
Recipient,
)
from .registry import JWERegistry
from .types import (
JSONRecipientDict,
GeneralJSONSerialization,
FlattenedJSONSerialization,
)
from ..util import (
to_bytes,
to_str,
json_b64encode,
json_b64decode,
urlsafe_b64encode,
urlsafe_b64decode,
)
from .._keys import Key
__all__ = [
"represent_general_json",
"represent_flattened_json",
"extract_general_json",
"extract_flattened_json",
]
def represent_general_json(obj: GeneralJSONEncryption) -> GeneralJSONSerialization:
data: GeneralJSONSerialization = __represent_json_serialization(obj)
recipients = []
for recipient in obj.recipients:
item: JSONRecipientDict = {}
assert recipient.header is not None
assert recipient.encrypted_key is not None
item["header"] = recipient.header
item["encrypted_key"] = to_str(urlsafe_b64encode(recipient.encrypted_key))
recipients.append(item)
data["recipients"] = recipients
return data
def represent_flattened_json(obj: FlattenedJSONEncryption) -> FlattenedJSONSerialization:
data: FlattenedJSONSerialization = __represent_json_serialization(obj)
recipient = obj.recipients[0]
assert recipient is not None
assert recipient.header is not None
assert recipient.encrypted_key is not None
data["header"] = recipient.header
data["encrypted_key"] = to_str(urlsafe_b64encode(recipient.encrypted_key))
return data
def __represent_json_serialization(obj: BaseJSONEncryption) -> Any:
data: dict[str, Any] = {
"protected": to_str(json_b64encode(obj.protected)),
"iv": to_str(obj.base64_segments["iv"]),
"ciphertext": to_str(obj.base64_segments["ciphertext"]),
"tag": to_str(obj.base64_segments["tag"]),
}
if obj.aad:
data["aad"] = to_str(urlsafe_b64encode(obj.aad))
if obj.unprotected:
data["unprotected"] = obj.unprotected
return data
def extract_general_json(data: GeneralJSONSerialization, registry: JWERegistry) -> GeneralJSONEncryption:
protected_segment = to_bytes(data["protected"])
registry.validate_protected_header_size(protected_segment)
protected = json_b64decode(protected_segment)
unprotected = data.get("unprotected")
recipients = data["recipients"]
registry.validate_recipient_count(recipients)
base64_segments, bytes_segments, aad = __extract_segments(data, registry)
obj = GeneralJSONEncryption(protected, None, unprotected, aad)
obj.base64_segments = base64_segments
obj.bytes_segments = bytes_segments
for item in recipients:
recipient = __extract_recipient(obj, item, registry)
obj.recipients.append(recipient)
return obj
def extract_flattened_json(data: FlattenedJSONSerialization, registry: JWERegistry) -> FlattenedJSONEncryption:
protected_segment = to_bytes(data["protected"])
registry.validate_protected_header_size(protected_segment)
protected = json_b64decode(protected_segment)
unprotected = data.get("unprotected")
base64_segments, bytes_segments, aad = __extract_segments(data, registry)
obj = FlattenedJSONEncryption(protected, None, unprotected, aad)
obj.base64_segments = base64_segments
obj.bytes_segments = bytes_segments
recipient = __extract_recipient(obj, data, registry)
obj.recipients.append(recipient)
return obj
def __extract_segments(
data: GeneralJSONSerialization | FlattenedJSONSerialization,
registry: JWERegistry,
) -> tuple[dict[str, bytes], dict[str, bytes], bytes | None]:
base64_segments: dict[str, bytes] = {
"iv": to_bytes(data["iv"]),
"ciphertext": to_bytes(data["ciphertext"]),
"tag": to_bytes(data["tag"]),
}
registry.validate_initialization_vector_size(base64_segments["iv"])
registry.validate_ciphertext_size(base64_segments["ciphertext"])
registry.validate_auth_tag_size(base64_segments["tag"])
bytes_segments: dict[str, bytes] = {
"iv": urlsafe_b64decode(base64_segments["iv"]),
"ciphertext": urlsafe_b64decode(base64_segments["ciphertext"]),
"tag": urlsafe_b64decode(base64_segments["tag"]),
}
if "aad" in data:
base64_segments["aad"] = to_bytes(data["aad"])
aad = urlsafe_b64decode(base64_segments["aad"])
bytes_segments["aad"] = aad
else:
aad = None
return base64_segments, bytes_segments, aad
def __extract_recipient(
obj: FlattenedJSONEncryption | GeneralJSONEncryption,
data: FlattenedJSONSerialization | JSONRecipientDict,
registry: JWERegistry,
) -> Recipient[Key]:
recipient: Recipient[Key] = Recipient(obj, data.get("header"))
if "encrypted_key" in data:
ek_segment = to_bytes(data["encrypted_key"])
registry.validate_encrypted_key_size(ek_segment)
recipient.encrypted_key = urlsafe_b64decode(ek_segment)
return recipient
@@ -0,0 +1,266 @@
import secrets
from typing import Any, Union
from .models import (
CompactEncryption,
BaseJSONEncryption,
GeneralJSONEncryption,
FlattenedJSONEncryption,
Recipient,
JWEAlgModel,
JWEEncModel,
JWEKeyAgreement,
JWEDirectEncryption,
JWEKeyEncryption,
JWEKeyWrapping,
)
from .registry import JWERegistry
from ..errors import (
JoseError,
DecodeError,
InvalidEncryptedKeyError,
InvalidExchangeKeyError,
ConflictAlgorithmError,
)
from ..util import (
json_b64encode,
urlsafe_b64encode,
)
__all__ = [
"EncryptionData",
"perform_encrypt",
"perform_decrypt",
]
EncryptionData = Union[CompactEncryption, GeneralJSONEncryption, FlattenedJSONEncryption]
def perform_encrypt(obj: EncryptionData, registry: JWERegistry) -> None:
enc = registry.get_enc(obj.protected["enc"])
cek, delayed_tasks = pre_encrypt_recipients(enc, obj.recipients, registry)
# Step 9, Generate a random JWE Initialization Vector of the correct size
# for the content encryption algorithm (if required for the algorithm);
# otherwise, let the JWE Initialization Vector be the empty octet sequence.
iv = enc.generate_iv()
# Step 10, Compute the encoded Initialization Vector value
# BASE64URL(JWE Initialization Vector).
obj.base64_segments["iv"] = urlsafe_b64encode(iv)
# Step 11, If a "zip" parameter was included, compress the plaintext using
# the specified compression algorithm and let M be the octet sequence
# representing the compressed plaintext; otherwise, let M be the octet
# sequence representing the plaintext.
assert obj.plaintext is not None
plaintext: bytes
if "zip" in obj.protected:
zip_ = registry.get_zip(obj.protected["zip"])
plaintext = zip_.compress(obj.plaintext)
else:
plaintext = obj.plaintext
# Step 13, Compute the Encoded Protected Header value BASE64URL(UTF8(JWE Protected Header)).
aad = json_b64encode(obj.protected)
# Step 14, Let the Additional Authenticated Data encryption parameter be
# ASCII(Encoded Protected Header). However, if a JWE AAD value is
# present (which can only be the case when using the JWE JSON Serialization),
# instead let the Additional Authenticated Data encryption parameter be
# ASCII(Encoded Protected Header || '.' || BASE64URL(JWE AAD)).
if isinstance(obj, BaseJSONEncryption) and obj.aad:
aad = aad + b"." + urlsafe_b64encode(obj.aad)
obj.base64_segments["aad"] = aad
# encrypting plaintext
ciphertext, tag = enc.encrypt(plaintext, cek, iv, aad)
# delay encrypting every recipient
post_encrypt_recipients(enc, delayed_tasks, cek, tag)
obj.base64_segments["ciphertext"] = urlsafe_b64encode(ciphertext)
obj.base64_segments["tag"] = urlsafe_b64encode(tag)
def perform_decrypt(obj: EncryptionData, registry: JWERegistry) -> None:
try:
_perform_decrypt(obj, registry)
except InvalidExchangeKeyError as error:
raise DecodeError(error.description)
def _perform_decrypt(obj: EncryptionData, registry: JWERegistry) -> None:
enc = registry.get_enc(obj.protected["enc"])
iv = obj.bytes_segments["iv"]
enc.check_iv(iv)
tag = obj.bytes_segments["tag"]
ciphertext = obj.bytes_segments["ciphertext"]
cek_set = set()
for recipient in obj.recipients:
try:
headers = recipient.headers()
registry.check_header(headers, True)
# Step 6, Determine the Key Management Mode employed by the algorithm
# specified by the "alg" (algorithm) Header Parameter.
alg = registry.get_alg(headers["alg"])
cek = decrypt_recipient(alg, enc, recipient, tag)
cek_set.add(cek)
except (AssertionError, JoseError) as error:
if registry.verify_all_recipients:
raise error
else:
if not registry.verify_all_recipients:
break
if not cek_set:
raise DecodeError("Invalid recipients")
if len(cek_set) > 1: # pragma: no cover
raise DecodeError("Multiple 'cek' found")
cek = cek_set.pop()
if len(cek) * 8 != enc.cek_size: # pragma: no cover
cek = secrets.token_bytes(enc.cek_size // 8)
if isinstance(obj, BaseJSONEncryption):
aad = json_b64encode(obj.protected)
if obj.aad:
aad = aad + b"." + obj.base64_segments["aad"]
else:
aad = obj.base64_segments["aad"]
msg = enc.decrypt(ciphertext, tag, cek, iv, aad)
if "zip" in obj.protected:
zip_ = registry.get_zip(obj.protected["zip"])
obj.plaintext = zip_.decompress(msg)
else:
obj.plaintext = msg
def pre_encrypt_recipients(
enc: JWEEncModel, recipients: list[Recipient[Any]], registry: JWERegistry
) -> tuple[bytes, list[tuple[JWEKeyAgreement, Recipient[Any]]]]:
cek: bytes = b""
delayed_tasks: list[tuple[JWEKeyAgreement, Recipient[Any]]] = []
for recipient in recipients:
alg = __prepare_recipient_algorithm(recipient, registry)
if alg.direct_mode:
if len(recipients) > 1:
raise ConflictAlgorithmError(f"Algorithm {alg.name} SHOULD have 1 recipient only")
cek = __pre_encrypt_direct_mode(alg, enc, recipient)
else:
if not cek:
# 2. When Key Wrapping, Key Encryption, or Key Agreement with Key
# Wrapping are employed, generate a random CEK value. See RFC
# 4086 [RFC4086] for considerations on generating random values.
# The CEK MUST have a length equal to that required for the
# content encryption algorithm.
cek = enc.generate_cek()
if isinstance(alg, JWEKeyAgreement):
delayed_tasks.append((alg, recipient))
else:
# 4. When Key Wrapping, or Key Encryption are employed, encrypt the CEK
# to the recipient and let the result be the JWE Encrypted Key.
assert isinstance(alg, (JWEKeyWrapping, JWEKeyEncryption))
recipient.encrypted_key = alg.encrypt_cek(cek, recipient)
return cek, delayed_tasks
def __prepare_recipient_algorithm(recipient: Recipient[Any], registry: JWERegistry) -> JWEAlgModel:
headers = recipient.headers()
registry.check_header(headers)
# 1. Determine the Key Management Mode employed by the algorithm used
# to determine the Content Encryption Key value. (This is the
# algorithm recorded in the "alg" (algorithm) Header Parameter of
# the resulting JWE.)
alg = registry.get_alg(headers["alg"])
if isinstance(alg, JWEKeyAgreement):
alg.prepare_ephemeral_key(recipient)
return alg
def __pre_encrypt_direct_mode(alg: JWEAlgModel, enc: JWEEncModel, recipient: Recipient[Any]) -> bytes:
cek: bytes
if isinstance(alg, JWEKeyAgreement):
# 3. When Direct Key Agreement is employed,
# let the CEK be the agreed upon key.
cek = alg.encrypt_agreed_upon_key(enc, recipient)
if len(cek) * 8 != enc.cek_size: # pragma: no cover
cek = secrets.token_bytes(enc.cek_size // 8)
else:
# 6. When Direct Encryption is employed, let the CEK be the shared
# symmetric key.
assert isinstance(alg, JWEDirectEncryption)
cek = alg.compute_cek(enc.cek_size, recipient)
# 5. When Direct Key Agreement or Direct Encryption are employed, let
# the JWE Encrypted Key be the empty octet sequence.
recipient.encrypted_key = b""
return cek
def post_encrypt_recipients(
enc: JWEEncModel, tasks: list[tuple[JWEKeyAgreement, Recipient[Any]]], cek: bytes, tag: bytes
) -> None:
for alg, recipient in tasks:
if alg.tag_aware:
agreed_upon_key = alg.encrypt_agreed_upon_key_with_tag(enc, recipient, tag)
else:
agreed_upon_key = alg.encrypt_agreed_upon_key(enc, recipient)
# 4. When Key Agreement with Key Wrapping is employed, encrypt the CEK
# to the recipient and let the result be the JWE Encrypted Key.
recipient.encrypted_key = alg.wrap_cek_with_auk(cek, agreed_upon_key)
def decrypt_recipient(alg: JWEAlgModel, enc: JWEEncModel, recipient: Recipient[Any], tag: bytes) -> bytes:
cek: bytes
if alg.direct_mode:
# 10. When Direct Key Agreement or Direct Encryption are employed,
# verify that the JWE Encrypted Key value is an empty octet
# sequence.
if recipient.encrypted_key: # pragma: no cover
raise InvalidEncryptedKeyError()
if isinstance(alg, JWEKeyAgreement):
# 8. When Direct Key Agreement is employed, let the CEK be the agreed upon key.
cek = alg.decrypt_agreed_upon_key(enc, recipient)
else:
# 11. When Direct Encryption is employed, let the CEK be the shared
# symmetric key.
assert isinstance(alg, JWEDirectEncryption)
cek = alg.compute_cek(enc.cek_size, recipient)
elif isinstance(alg, JWEKeyAgreement):
agreed_upon_key: bytes
if alg.tag_aware:
agreed_upon_key = alg.decrypt_agreed_upon_key_with_tag(enc, recipient, tag)
else:
agreed_upon_key = alg.decrypt_agreed_upon_key(enc, recipient)
# 8. When Key Agreement with Key Wrapping is employed, the agreed upon key
# will be used to decrypt the JWE Encrypted Key.
if recipient.encrypted_key is None: # pragma: no cover
raise DecodeError("Invalid encrypted key")
cek = alg.unwrap_cek_with_auk(recipient.encrypted_key, agreed_upon_key)
else:
# 9. When Key Wrapping, Key Encryption, or Key Agreement with Key
# Wrapping are employed, decrypt the JWE Encrypted Key to produce
# the CEK. The CEK MUST have a length equal to that required for
# the content encryption algorithm. Note that when there are
# multiple recipients, each recipient will only be able to decrypt
# JWE Encrypted Key values that were encrypted to a key in that
# recipient's possession. It is therefore normal to only be able
# to decrypt one of the per-recipient JWE Encrypted Key values to
# obtain the CEK value.
assert isinstance(alg, (JWEKeyWrapping, JWEKeyEncryption))
if recipient.encrypted_key is None:
raise DecodeError("Invalid encrypted key")
cek = alg.decrypt_cek(recipient)
return cek
@@ -0,0 +1,345 @@
import typing as t
import secrets
from abc import ABCMeta, abstractmethod
from ..registry import Header, HeaderRegistryDict
from ..errors import InvalidKeyTypeError, InvalidKeyLengthError
from .._keys import Key, ECKey, OctKey
__all__ = [
"Recipient",
"CompactEncryption",
"BaseJSONEncryption",
"GeneralJSONEncryption",
"FlattenedJSONEncryption",
"JWEEncModel",
"JWEZipModel",
"KeyManagement",
"JWEDirectEncryption",
"JWEKeyEncryption",
"JWEKeyWrapping",
"JWEKeyAgreement",
"JWEAlgModel",
]
KeyType = t.TypeVar("KeyType")
class Recipient(t.Generic[KeyType]):
def __init__(
self,
parent: t.Union["CompactEncryption", "GeneralJSONEncryption", "FlattenedJSONEncryption"],
header: Header | None = None,
recipient_key: KeyType | None = None,
):
self.__parent = parent
self.header = header
self.recipient_key = recipient_key
self.sender_key: KeyType | None = None
self.encrypted_key: bytes | None = None
self.ephemeral_key: KeyType | None = None
def headers(self) -> Header:
rv: Header = {}
if isinstance(self.__parent, BaseJSONEncryption) and self.__parent.unprotected:
rv.update(self.__parent.unprotected)
if self.header:
rv.update(self.header)
rv.update(self.__parent.protected)
return rv
def add_header(self, k: str, v: t.Any) -> None:
if isinstance(self.__parent, CompactEncryption):
self.__parent.protected.update({k: v})
elif self.header:
self.header.update({k: v})
else:
self.header = {k: v}
def set_kid(self, kid: str) -> None:
self.add_header("kid", kid)
class CompactEncryption:
"""An object to represent the JWE Compact Serialization. It is usually returned by
``decrypt_compact`` method.
"""
def __init__(self, protected: Header, plaintext: bytes | None = None):
#: protected header in dict
self.protected = protected
#: the plaintext in bytes
self.plaintext = plaintext
self.recipient: Recipient[t.Any] | None = None
self.bytes_segments: dict[str, bytes] = {} # store the decoded segments
self.base64_segments: dict[str, bytes] = {} # store the encoded segments
def headers(self) -> Header:
"""Returns the protected header values in dict."""
return self.protected
def attach_recipient(self, key: Key, header: Header | None = None) -> None:
"""Add a recipient to the JWE Compact Serialization. Please add a key that
comply with the given "alg" value.
:param key: an instance of a key, e.g. (OctKey, RSAKey, ECKey, and etc)
:param header: extra header in dict
"""
recipient = Recipient(self, None, key)
if header:
self.protected.update(header)
self.recipient = recipient
@property
def recipients(self) -> list[Recipient[t.Any]]:
if self.recipient is not None:
return [self.recipient]
return []
class BaseJSONEncryption(metaclass=ABCMeta):
#: represents if the object is in flatten syntax
flattened: t.ClassVar[bool]
#: protected header in dict
protected: Header
#: the plaintext in bytes
plaintext: t.Optional[bytes]
#: unprotected header in dict
unprotected: t.Optional[Header]
#: an optional additional authenticated data
aad: t.Optional[bytes]
#: a list of recipients
recipients: list[Recipient[t.Any]]
def __init__(
self,
protected: Header,
plaintext: bytes | None = None,
unprotected: Header | None = None,
aad: bytes | None = None,
):
self.protected = protected
self.plaintext = plaintext
self.unprotected = unprotected
self.aad = aad
self.recipients = []
self.bytes_segments: dict[str, bytes] = {} # store the decoded segments
self.base64_segments: dict[str, bytes] = {} # store the encoded segments
@abstractmethod
def add_recipient(self, header: Header | None = None, key: Key | None = None) -> None:
"""Add a recipient to the JWE JSON Serialization. Please add a key that
comply with the "alg" to this recipient.
:param header: recipient's own (unprotected) header
:param key: an instance of a key, e.g. (OctKey, RSAKey, ECKey, and etc)
"""
class GeneralJSONEncryption(BaseJSONEncryption):
"""An object to represent the JWE General JSON Serialization. It is used by
``encrypt_json``, and it is usually returned by ``decrypt_json`` method.
To construct an object of ``GeneralJSONEncryption``:
.. code-block:: python
protected = {"enc": "A128CBC-HS256"}
plaintext = b"hello world"
obj = GeneralJSONEncryption(protected, plaintext)
# then add each recipient
obj.add_recipient({"alg": "A128KW"})
"""
flattened = False
def add_recipient(self, header: Header | None = None, key: Key | None = None) -> None:
recipient = Recipient(self, header, key)
if key and key.kid:
recipient.set_kid(key.kid)
self.recipients.append(recipient)
class FlattenedJSONEncryption(BaseJSONEncryption):
"""An object to represent the JWE Flattened JSON Serialization. It is used by
``encrypt_json``, and it is usually returned by ``decrypt_json`` method.
To construct an object of ``FlattenedJSONEncryption``:
.. code-block:: python
protected = {"enc": "A128CBC-HS256"}
plaintext = b"hello world"
obj = FlattenedJSONEncryption(protected, plaintext)
# then add each recipient
obj.add_recipient({"alg": "A128KW"})
"""
flattened = True
def add_recipient(self, header: Header | None = None, key: Key | None = None) -> None:
recipient = Recipient(self, header, key)
if key and key.kid:
recipient.set_kid(key.kid)
self.recipients = [recipient]
class JWEEncModel(metaclass=ABCMeta):
name: str
description: str
recommended: bool = False
algorithm_type: t.Literal["JWE"] = "JWE"
algorithm_location: t.Literal["enc"] = "enc"
iv_size: int
cek_size: int
def generate_cek(self) -> bytes:
return secrets.token_bytes(self.cek_size // 8)
def generate_iv(self) -> bytes:
return secrets.token_bytes(self.iv_size // 8)
def check_iv(self, iv: bytes) -> bytes:
if len(iv) * 8 != self.iv_size: # pragma: no cover
raise ValueError("Invalid 'iv' size")
return iv
@abstractmethod
def encrypt(self, plaintext: bytes, cek: bytes, iv: bytes, aad: bytes) -> tuple[bytes, bytes]:
pass
@abstractmethod
def decrypt(self, ciphertext: bytes, tag: bytes, cek: bytes, iv: bytes, aad: bytes) -> bytes:
pass
class JWEZipModel(metaclass=ABCMeta):
name: str
description: str
recommended: bool = True
algorithm_type: t.Literal["JWE"] = "JWE"
algorithm_location: t.Literal["zip"] = "zip"
@abstractmethod
def compress(self, s: bytes) -> bytes:
pass
@abstractmethod
def decompress(self, s: bytes) -> bytes:
pass
class KeyManagement:
name: str
description: str
recommended: bool = False
key_size: int | None = None
key_types: list[str]
security_warning: str | None = None
algorithm_type: t.Literal["JWE"] = "JWE"
algorithm_location: t.Literal["alg"] = "alg"
more_header_registry: HeaderRegistryDict = {}
@property
def direct_mode(self) -> bool:
return self.key_size is None
def check_key_type(self, key: Key) -> None:
if key.key_type not in self.key_types:
raise InvalidKeyTypeError()
def prepare_recipient_header(self, recipient: Recipient[t.Any]) -> None:
raise NotImplementedError()
class JWEDirectEncryption(KeyManagement, metaclass=ABCMeta):
key_types = ["oct"]
@abstractmethod
def compute_cek(self, size: int, recipient: Recipient[OctKey]) -> bytes:
pass
class JWEKeyEncryption(KeyManagement, metaclass=ABCMeta):
@property
def direct_mode(self) -> bool:
return False
@abstractmethod
def encrypt_cek(self, cek: bytes, recipient: Recipient[t.Any]) -> bytes:
pass
@abstractmethod
def decrypt_cek(self, recipient: Recipient[t.Any]) -> bytes:
pass
class JWEKeyWrapping(KeyManagement, metaclass=ABCMeta):
key_size: int
key_types = ["oct"]
@property
def direct_mode(self) -> bool:
return False
def check_op_key(self, op_key: bytes) -> None:
if len(op_key) * 8 != self.key_size:
raise InvalidKeyLengthError(f"A key of size {self.key_size} bits MUST be used")
@abstractmethod
def wrap_cek(self, cek: bytes, key: bytes) -> bytes:
pass
@abstractmethod
def unwrap_cek(self, ek: bytes, key: bytes) -> bytes:
pass
@abstractmethod
def encrypt_cek(self, cek: bytes, recipient: Recipient[OctKey]) -> bytes:
pass
@abstractmethod
def decrypt_cek(self, recipient: Recipient[OctKey]) -> bytes:
pass
class JWEKeyAgreement(KeyManagement, metaclass=ABCMeta):
key_types = ["EC", "OKP"]
tag_aware: bool = False
key_wrapping: t.Optional[JWEKeyWrapping]
def prepare_ephemeral_key(self, recipient: Recipient[ECKey]) -> None:
recipient_key = recipient.recipient_key
assert recipient_key is not None
self.check_key_type(recipient_key)
if recipient.ephemeral_key is None:
ephemeral_key = recipient_key.generate_key(recipient_key.curve_name, private=True)
recipient.ephemeral_key = ephemeral_key
recipient.add_header("epk", recipient.ephemeral_key.as_dict(private=False))
@abstractmethod
def encrypt_agreed_upon_key(self, enc: JWEEncModel, recipient: Recipient[ECKey]) -> bytes:
pass
@abstractmethod
def decrypt_agreed_upon_key(self, enc: JWEEncModel, recipient: Recipient[ECKey]) -> bytes:
pass
def wrap_cek_with_auk(self, cek: bytes, key: bytes) -> bytes:
assert self.key_wrapping is not None
return self.key_wrapping.wrap_cek(cek, key)
def unwrap_cek_with_auk(self, ek: bytes, key: bytes) -> bytes:
assert self.key_wrapping is not None
return self.key_wrapping.unwrap_cek(ek, key)
def encrypt_agreed_upon_key_with_tag(self, enc: JWEEncModel, recipient: Recipient[ECKey], tag: bytes) -> bytes:
raise NotImplementedError()
def decrypt_agreed_upon_key_with_tag(self, enc: JWEEncModel, recipient: Recipient[ECKey], tag: bytes) -> bytes:
raise NotImplementedError()
JWEAlgModel = t.Union[JWEKeyEncryption, JWEKeyWrapping, JWEKeyAgreement, JWEDirectEncryption]
@@ -0,0 +1,177 @@
import warnings
import typing as t
from collections.abc import Collection
from .models import JWEAlgModel, JWEEncModel, JWEZipModel
from ..errors import (
UnsupportedAlgorithmError,
SecurityWarning,
ExceededSizeError,
)
from ..registry import (
Header,
HeaderRegistryDict,
JWE_HEADER_REGISTRY,
check_supported_header,
check_registry_header,
check_crit_header,
)
__all__ = [
"JWEAlgorithm",
"JWERegistry",
"default_registry",
]
JWEAlgorithm = t.Union[JWEAlgModel, JWEEncModel, JWEZipModel]
AlgorithmsDict = t.TypedDict(
"AlgorithmsDict",
{
"alg": dict[str, JWEAlgModel],
"enc": dict[str, JWEEncModel],
"zip": dict[str, JWEZipModel],
},
)
class JWERegistry:
"""A registry for JSON Web Encryption to keep all the supported algorithms.
An instance of ``JWERegistry`` is usually used together with methods in
``joserfc.jwe``.
:param header_registry: extra header parameters registry
:param algorithms: allowed algorithms to be used
:param verify_all_recipients: validating all recipients in a JSON serialization
:param strict_check_header: only allow header key in the registry to be used
:param max_recipients: max number of recipients in JWE General JSON Serialization
"""
algorithms: AlgorithmsDict = {
"alg": {},
"enc": {},
"zip": {},
}
recommended: t.ClassVar[list[str]] = []
#: max protected header content's size in bytes
max_protected_header_length: int = 1024
#: max encrypted key's size in bytes
max_encrypted_key_length: int = 1024
#: max initialization vector's size in bytes
max_initialization_vector_length: int = 64
#: max ciphertext's size in bytes
max_ciphertext_length: int = 65536 # 64KB
#: max auth tag's size in bytes
max_auth_tag_length: int = 64
#: max number of recipients in JWE General JSON Serialization
max_recipients: int = 16
def __init__(
self,
header_registry: HeaderRegistryDict | None = None,
algorithms: Collection[str] | None = None,
verify_all_recipients: bool = True,
strict_check_header: bool = True,
max_recipients: int | None = None,
):
self.header_registry: HeaderRegistryDict = {}
self.header_registry.update(JWE_HEADER_REGISTRY)
if header_registry is not None:
self.header_registry.update(header_registry)
self.allowed = algorithms
self.verify_all_recipients = verify_all_recipients
self.strict_check_header = strict_check_header
if max_recipients is not None:
self.max_recipients = max_recipients
@classmethod
def register(cls, model: JWEAlgorithm) -> None:
cls.algorithms[model.algorithm_location][model.name] = model # type: ignore
if model.recommended:
cls.recommended.append(model.name)
def check_header(self, header: Header, check_more: bool = False) -> None:
"""Check and validate the fields in header part of a JWS object."""
check_crit_header(self.header_registry, header)
check_registry_header(self.header_registry, header)
alg = self.get_alg(header["alg"])
if alg.more_header_registry:
check_registry_header(alg.more_header_registry, header, check_more)
if self.strict_check_header:
allowed_registry = self.header_registry.copy()
allowed_registry.update(alg.more_header_registry)
check_supported_header(allowed_registry, header)
elif self.strict_check_header:
check_supported_header(self.header_registry, header)
def validate_protected_header_size(self, header: bytes) -> None:
if header and len(header) > self.max_protected_header_length:
raise ExceededSizeError(f"Header size exceeds {self.max_protected_header_length} bytes.")
def validate_encrypted_key_size(self, ek: bytes) -> None:
if ek and len(ek) > self.max_encrypted_key_length:
raise ExceededSizeError(f"Encrypted key size exceeds {self.max_encrypted_key_length} bytes.")
def validate_initialization_vector_size(self, iv: bytes) -> None:
if iv and len(iv) > self.max_initialization_vector_length:
raise ExceededSizeError(
f"Initialization vector size exceeds {self.max_initialization_vector_length} bytes."
)
def validate_ciphertext_size(self, ciphertext: bytes) -> None:
if ciphertext and len(ciphertext) > self.max_ciphertext_length:
raise ExceededSizeError(f"Ciphertext size exceeds {self.max_ciphertext_length} bytes.")
def validate_auth_tag_size(self, tag: bytes) -> None:
if tag and len(tag) > self.max_auth_tag_length:
raise ExceededSizeError(f"Auth tag size exceeds {self.max_auth_tag_length} bytes.")
def validate_recipient_count(self, recipients: Collection[t.Any]) -> None:
if len(recipients) > self.max_recipients:
raise ExceededSizeError(f"Recipient count exceeds {self.max_recipients}.")
def get_alg(self, name: str) -> JWEAlgModel:
"""Get the allowed ("alg") algorithm instance of the given name.
:param name: value of the ``alg``, e.g. ``ECDH-ES``, ``A128KW``
"""
registry = self.algorithms["alg"]
self._check_algorithm(name, registry)
alg: JWEAlgModel = registry[name]
if alg.security_warning:
warnings.warn(alg.security_warning, SecurityWarning)
return alg
def get_enc(self, name: str) -> JWEEncModel:
"""Get the allowed ("enc") algorithm instance of the given name.
:param name: value of the ``enc``, e.g. ``A128CBC-HS256``, ``A128GCM``
"""
registry = self.algorithms["enc"]
self._check_algorithm(name, registry)
return registry[name]
def get_zip(self, name: str) -> JWEZipModel:
"""Get the allowed ("zip") algorithm instance of the given name.
:param name: value of the ``zip``, e.g. ``DEF``
"""
registry = self.algorithms["zip"]
self._check_algorithm(name, registry)
return registry[name]
def _check_algorithm(self, name: str, registry: dict[str, t.Any]) -> None:
if name not in registry:
raise UnsupportedAlgorithmError(f"Algorithm of '{name}' is not supported")
if self.allowed:
if name not in self.allowed:
raise UnsupportedAlgorithmError(f"Algorithm of '{name}' is not allowed")
else:
if name not in self.recommended:
raise UnsupportedAlgorithmError(f"Algorithm of '{name}' is not recommended")
default_registry = JWERegistry()
@@ -0,0 +1,33 @@
from typing import Any, TypedDict
__all__ = [
"JSONRecipientDict",
"FlattenedJSONSerialization",
"GeneralJSONSerialization",
]
class JSONRecipientDict(TypedDict, total=False):
header: dict[str, Any]
encrypted_key: str
class GeneralJSONSerialization(TypedDict, total=False):
protected: str
unprotected: dict[str, Any]
iv: str
aad: str
ciphertext: str
tag: str
recipients: list[JSONRecipientDict]
class FlattenedJSONSerialization(TypedDict, total=False):
protected: str
unprotected: dict[str, Any]
iv: str
aad: str
ciphertext: str
tag: str
header: dict[str, Any]
encrypted_key: str