updates
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
"""authlib.jose.rfc7516.
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module represents a direct implementation of
|
||||
JSON Web Encryption (JWE).
|
||||
|
||||
https://tools.ietf.org/html/rfc7516
|
||||
"""
|
||||
|
||||
from .jwe import JsonWebEncryption
|
||||
from .models import JWEAlgorithm
|
||||
from .models import JWEAlgorithmWithTagAwareKeyAgreement
|
||||
from .models import JWEEncAlgorithm
|
||||
from .models import JWEZipAlgorithm
|
||||
|
||||
__all__ = [
|
||||
"JsonWebEncryption",
|
||||
"JWEAlgorithm",
|
||||
"JWEAlgorithmWithTagAwareKeyAgreement",
|
||||
"JWEEncAlgorithm",
|
||||
"JWEZipAlgorithm",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,759 @@
|
||||
from collections import OrderedDict
|
||||
from copy import deepcopy
|
||||
|
||||
from authlib.common.encoding import json_b64encode
|
||||
from authlib.common.encoding import to_bytes
|
||||
from authlib.common.encoding import to_unicode
|
||||
from authlib.common.encoding import urlsafe_b64encode
|
||||
from authlib.jose.errors import DecodeError
|
||||
from authlib.jose.errors import InvalidAlgorithmForMultipleRecipientsMode
|
||||
from authlib.jose.errors import InvalidHeaderParameterNameError
|
||||
from authlib.jose.errors import KeyMismatchError
|
||||
from authlib.jose.errors import MissingAlgorithmError
|
||||
from authlib.jose.errors import MissingEncryptionAlgorithmError
|
||||
from authlib.jose.errors import UnsupportedAlgorithmError
|
||||
from authlib.jose.errors import UnsupportedCompressionAlgorithmError
|
||||
from authlib.jose.errors import UnsupportedEncryptionAlgorithmError
|
||||
from authlib.jose.rfc7516.models import JWEAlgorithmWithTagAwareKeyAgreement
|
||||
from authlib.jose.rfc7516.models import JWEHeader
|
||||
from authlib.jose.rfc7516.models import JWESharedHeader
|
||||
from authlib.jose.util import ensure_dict
|
||||
from authlib.jose.util import extract_header
|
||||
from authlib.jose.util import extract_segment
|
||||
|
||||
|
||||
class JsonWebEncryption:
|
||||
#: Registered Header Parameter Names defined by Section 4.1
|
||||
REGISTERED_HEADER_PARAMETER_NAMES = frozenset(
|
||||
[
|
||||
"alg",
|
||||
"enc",
|
||||
"zip",
|
||||
"jku",
|
||||
"jwk",
|
||||
"kid",
|
||||
"x5u",
|
||||
"x5c",
|
||||
"x5t",
|
||||
"x5t#S256",
|
||||
"typ",
|
||||
"cty",
|
||||
"crit",
|
||||
]
|
||||
)
|
||||
|
||||
ALG_REGISTRY = {}
|
||||
ENC_REGISTRY = {}
|
||||
ZIP_REGISTRY = {}
|
||||
|
||||
def __init__(self, algorithms=None, private_headers=None):
|
||||
self._algorithms = algorithms
|
||||
self._private_headers = private_headers
|
||||
|
||||
@classmethod
|
||||
def register_algorithm(cls, algorithm):
|
||||
"""Register an algorithm for ``alg`` or ``enc`` or ``zip`` of JWE."""
|
||||
if not algorithm or algorithm.algorithm_type != "JWE":
|
||||
raise ValueError(f"Invalid algorithm for JWE, {algorithm!r}")
|
||||
|
||||
if algorithm.algorithm_location == "alg":
|
||||
cls.ALG_REGISTRY[algorithm.name] = algorithm
|
||||
elif algorithm.algorithm_location == "enc":
|
||||
cls.ENC_REGISTRY[algorithm.name] = algorithm
|
||||
elif algorithm.algorithm_location == "zip":
|
||||
cls.ZIP_REGISTRY[algorithm.name] = algorithm
|
||||
|
||||
def serialize_compact(self, protected, payload, key, sender_key=None):
|
||||
"""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)
|
||||
|
||||
Only one recipient is supported by the JWE Compact Serialization and
|
||||
it provides no syntax to represent JWE Shared Unprotected Header, JWE
|
||||
Per-Recipient Unprotected Header, or JWE AAD values.
|
||||
|
||||
:param protected: A dict of protected header
|
||||
:param payload: Payload (bytes or a value convertible to bytes)
|
||||
:param key: Public key used to encrypt payload
|
||||
:param sender_key: Sender's private key in case
|
||||
JWEAlgorithmWithTagAwareKeyAgreement is used
|
||||
:return: JWE compact serialization as bytes
|
||||
"""
|
||||
# step 1: Prepare algorithms & key
|
||||
alg = self.get_header_alg(protected)
|
||||
enc = self.get_header_enc(protected)
|
||||
zip_alg = self.get_header_zip(protected)
|
||||
|
||||
self._validate_sender_key(sender_key, alg)
|
||||
self._validate_private_headers(protected, alg)
|
||||
|
||||
key = prepare_key(alg, protected, key)
|
||||
if sender_key is not None:
|
||||
sender_key = alg.prepare_key(sender_key)
|
||||
|
||||
# self._post_validate_header(protected, algorithm)
|
||||
|
||||
# step 2: Generate a random Content Encryption Key (CEK)
|
||||
# use enc_alg.generate_cek() in scope of upcoming .wrap
|
||||
# or .generate_keys_and_prepare_headers call
|
||||
|
||||
# step 3: Encrypt the CEK with the recipient's public key
|
||||
if (
|
||||
isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement)
|
||||
and alg.key_size is not None
|
||||
):
|
||||
# For a JWE algorithm with tag-aware key agreement in case key agreement
|
||||
# with key wrapping mode is used:
|
||||
# Defer key agreement with key wrapping until
|
||||
# authentication tag is computed
|
||||
prep = alg.generate_keys_and_prepare_headers(enc, key, sender_key)
|
||||
epk = prep["epk"]
|
||||
cek = prep["cek"]
|
||||
protected.update(prep["header"])
|
||||
else:
|
||||
# In any other case:
|
||||
# Keep the normal steps order defined by RFC 7516
|
||||
if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement):
|
||||
wrapped = alg.wrap(enc, protected, key, sender_key)
|
||||
else:
|
||||
wrapped = alg.wrap(enc, protected, key)
|
||||
cek = wrapped["cek"]
|
||||
ek = wrapped["ek"]
|
||||
if "header" in wrapped:
|
||||
protected.update(wrapped["header"])
|
||||
|
||||
# step 4: Generate a random JWE Initialization Vector
|
||||
iv = enc.generate_iv()
|
||||
|
||||
# step 5: Let the Additional Authenticated Data encryption parameter
|
||||
# be ASCII(BASE64URL(UTF8(JWE Protected Header)))
|
||||
protected_segment = json_b64encode(protected)
|
||||
aad = to_bytes(protected_segment, "ascii")
|
||||
|
||||
# step 6: compress message if required
|
||||
if zip_alg:
|
||||
msg = zip_alg.compress(to_bytes(payload))
|
||||
else:
|
||||
msg = to_bytes(payload)
|
||||
|
||||
# step 7: perform encryption
|
||||
ciphertext, tag = enc.encrypt(msg, aad, iv, cek)
|
||||
|
||||
if (
|
||||
isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement)
|
||||
and alg.key_size is not None
|
||||
):
|
||||
# For a JWE algorithm with tag-aware key agreement in case key agreement
|
||||
# with key wrapping mode is used:
|
||||
# Perform key agreement with key wrapping deferred at step 3
|
||||
wrapped = alg.agree_upon_key_and_wrap_cek(
|
||||
enc, protected, key, sender_key, epk, cek, tag
|
||||
)
|
||||
ek = wrapped["ek"]
|
||||
|
||||
# step 8: build resulting message
|
||||
return b".".join(
|
||||
[
|
||||
protected_segment,
|
||||
urlsafe_b64encode(ek),
|
||||
urlsafe_b64encode(iv),
|
||||
urlsafe_b64encode(ciphertext),
|
||||
urlsafe_b64encode(tag),
|
||||
]
|
||||
)
|
||||
|
||||
def serialize_json(self, header_obj, payload, keys, sender_key=None): # noqa: C901
|
||||
"""Generate a JWE JSON Serialization (in fully general syntax).
|
||||
|
||||
The JWE JSON Serialization represents encrypted content as a JSON
|
||||
object. This representation is neither optimized for compactness nor
|
||||
URL safe.
|
||||
|
||||
The following members are defined for use in top-level JSON objects
|
||||
used for the fully general JWE JSON Serialization syntax:
|
||||
|
||||
protected
|
||||
The "protected" member MUST be present and contain the value
|
||||
BASE64URL(UTF8(JWE Protected Header)) when the JWE Protected
|
||||
Header value is non-empty; otherwise, it MUST be absent. These
|
||||
Header Parameter values are integrity protected.
|
||||
|
||||
unprotected
|
||||
The "unprotected" member MUST be present and contain the value JWE
|
||||
Shared Unprotected Header when the JWE Shared Unprotected Header
|
||||
value is non-empty; otherwise, it MUST be absent. This value is
|
||||
represented as an unencoded JSON object, rather than as a string.
|
||||
These Header Parameter values are not integrity protected.
|
||||
|
||||
iv
|
||||
The "iv" member MUST be present and contain the value
|
||||
BASE64URL(JWE Initialization Vector) when the JWE Initialization
|
||||
Vector value is non-empty; otherwise, it MUST be absent.
|
||||
|
||||
aad
|
||||
The "aad" member MUST be present and contain the value
|
||||
BASE64URL(JWE AAD)) when the JWE AAD value is non-empty;
|
||||
otherwise, it MUST be absent. A JWE AAD value can be included to
|
||||
supply a base64url-encoded value to be integrity protected but not
|
||||
encrypted.
|
||||
|
||||
ciphertext
|
||||
The "ciphertext" member MUST be present and contain the value
|
||||
BASE64URL(JWE Ciphertext).
|
||||
|
||||
tag
|
||||
The "tag" member MUST be present and contain the value
|
||||
BASE64URL(JWE Authentication Tag) when the JWE Authentication Tag
|
||||
value is non-empty; otherwise, it MUST be absent.
|
||||
|
||||
recipients
|
||||
The "recipients" member value MUST be an array of JSON objects.
|
||||
Each object contains information specific to a single recipient.
|
||||
This member MUST be present with exactly one array element per
|
||||
recipient, even if some or all of the array element values are the
|
||||
empty JSON object "{}" (which can happen when all Header Parameter
|
||||
values are shared between all recipients and when no encrypted key
|
||||
is used, such as when doing Direct Encryption).
|
||||
|
||||
The following members are defined for use in the JSON objects that
|
||||
are elements of the "recipients" array:
|
||||
|
||||
header
|
||||
The "header" member MUST be present and contain the value JWE Per-
|
||||
Recipient Unprotected Header when the JWE Per-Recipient
|
||||
Unprotected Header value is non-empty; otherwise, it MUST be
|
||||
absent. This value is represented as an unencoded JSON object,
|
||||
rather than as a string. These Header Parameter values are not
|
||||
integrity protected.
|
||||
|
||||
encrypted_key
|
||||
The "encrypted_key" member MUST be present and contain the value
|
||||
BASE64URL(JWE Encrypted Key) when the JWE Encrypted Key value is
|
||||
non-empty; otherwise, it MUST be absent.
|
||||
|
||||
This implementation assumes that "alg" and "enc" header fields are
|
||||
contained in the protected or shared unprotected header.
|
||||
|
||||
:param header_obj: A dict of headers (in addition optionally contains JWE AAD)
|
||||
:param payload: Payload (bytes or a value convertible to bytes)
|
||||
:param keys: Public keys (or a single public key) used to encrypt payload
|
||||
:param sender_key: Sender's private key in case
|
||||
JWEAlgorithmWithTagAwareKeyAgreement is used
|
||||
:return: JWE JSON serialization (in fully general syntax) as dict
|
||||
|
||||
Example of `header_obj`::
|
||||
|
||||
{
|
||||
"protected": {
|
||||
"alg": "ECDH-1PU+A128KW",
|
||||
"enc": "A256CBC-HS512",
|
||||
"apu": "QWxpY2U",
|
||||
"apv": "Qm9iIGFuZCBDaGFybGll",
|
||||
},
|
||||
"unprotected": {"jku": "https://alice.example.com/keys.jwks"},
|
||||
"recipients": [
|
||||
{"header": {"kid": "bob-key-2"}},
|
||||
{"header": {"kid": "2021-05-06"}},
|
||||
],
|
||||
"aad": b"Authenticate me too.",
|
||||
}
|
||||
"""
|
||||
if not isinstance(keys, list): # single key
|
||||
keys = [keys]
|
||||
|
||||
if not keys:
|
||||
raise ValueError("No keys have been provided")
|
||||
|
||||
header_obj = deepcopy(header_obj)
|
||||
|
||||
shared_header = JWESharedHeader.from_dict(header_obj)
|
||||
|
||||
recipients = header_obj.get("recipients")
|
||||
if recipients is None:
|
||||
recipients = [{} for _ in keys]
|
||||
for i in range(len(recipients)):
|
||||
if recipients[i] is None:
|
||||
recipients[i] = {}
|
||||
if "header" not in recipients[i]:
|
||||
recipients[i]["header"] = {}
|
||||
|
||||
jwe_aad = header_obj.get("aad")
|
||||
|
||||
if len(keys) != len(recipients):
|
||||
raise ValueError(
|
||||
f"Count of recipient keys {len(keys)} does not equal to count of recipients {len(recipients)}"
|
||||
)
|
||||
|
||||
# step 1: Prepare algorithms & key
|
||||
alg = self.get_header_alg(shared_header)
|
||||
enc = self.get_header_enc(shared_header)
|
||||
zip_alg = self.get_header_zip(shared_header)
|
||||
|
||||
self._validate_sender_key(sender_key, alg)
|
||||
self._validate_private_headers(shared_header, alg)
|
||||
for recipient in recipients:
|
||||
self._validate_private_headers(recipient["header"], alg)
|
||||
|
||||
for i in range(len(keys)):
|
||||
keys[i] = prepare_key(alg, recipients[i]["header"], keys[i])
|
||||
if sender_key is not None:
|
||||
sender_key = alg.prepare_key(sender_key)
|
||||
|
||||
# self._post_validate_header(protected, algorithm)
|
||||
|
||||
# step 2: Generate a random Content Encryption Key (CEK)
|
||||
# use enc_alg.generate_cek() in scope of upcoming .wrap
|
||||
# or .generate_keys_and_prepare_headers call
|
||||
|
||||
# step 3: Encrypt the CEK with the recipient's public key
|
||||
preset = alg.generate_preset(enc, keys[0])
|
||||
if "cek" in preset:
|
||||
cek = preset["cek"]
|
||||
else:
|
||||
cek = None
|
||||
if len(keys) > 1 and cek is None:
|
||||
raise InvalidAlgorithmForMultipleRecipientsMode(alg.name)
|
||||
if "header" in preset:
|
||||
shared_header.update_protected(preset["header"])
|
||||
|
||||
if (
|
||||
isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement)
|
||||
and alg.key_size is not None
|
||||
):
|
||||
# For a JWE algorithm with tag-aware key agreement in case key agreement
|
||||
# with key wrapping mode is used:
|
||||
# Defer key agreement with key wrapping until authentication tag is computed
|
||||
epks = []
|
||||
for i in range(len(keys)):
|
||||
prep = alg.generate_keys_and_prepare_headers(
|
||||
enc, keys[i], sender_key, preset
|
||||
)
|
||||
if cek is None:
|
||||
cek = prep["cek"]
|
||||
epks.append(prep["epk"])
|
||||
recipients[i]["header"].update(prep["header"])
|
||||
else:
|
||||
# In any other case:
|
||||
# Keep the normal steps order defined by RFC 7516
|
||||
for i in range(len(keys)):
|
||||
if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement):
|
||||
wrapped = alg.wrap(enc, shared_header, keys[i], sender_key, preset)
|
||||
else:
|
||||
wrapped = alg.wrap(enc, shared_header, keys[i], preset)
|
||||
if cek is None:
|
||||
cek = wrapped["cek"]
|
||||
recipients[i]["encrypted_key"] = wrapped["ek"]
|
||||
if "header" in wrapped:
|
||||
recipients[i]["header"].update(wrapped["header"])
|
||||
|
||||
# step 4: Generate a random JWE Initialization Vector
|
||||
iv = enc.generate_iv()
|
||||
|
||||
# step 5: Compute the Encoded Protected Header value
|
||||
# BASE64URL(UTF8(JWE Protected Header)). If the JWE Protected Header
|
||||
# is not present, let this value be the empty string.
|
||||
# Let the Additional Authenticated Data encryption parameter be
|
||||
# ASCII(Encoded Protected Header). However, if a JWE AAD value is
|
||||
# present, instead let the Additional Authenticated Data encryption
|
||||
# parameter be ASCII(Encoded Protected Header || '.' || BASE64URL(JWE AAD)).
|
||||
aad = (
|
||||
json_b64encode(shared_header.protected) if shared_header.protected else b""
|
||||
)
|
||||
if jwe_aad is not None:
|
||||
aad += b"." + urlsafe_b64encode(jwe_aad)
|
||||
aad = to_bytes(aad, "ascii")
|
||||
|
||||
# step 6: compress message if required
|
||||
if zip_alg:
|
||||
msg = zip_alg.compress(to_bytes(payload))
|
||||
else:
|
||||
msg = to_bytes(payload)
|
||||
|
||||
# step 7: perform encryption
|
||||
ciphertext, tag = enc.encrypt(msg, aad, iv, cek)
|
||||
|
||||
if (
|
||||
isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement)
|
||||
and alg.key_size is not None
|
||||
):
|
||||
# For a JWE algorithm with tag-aware key agreement in case key agreement
|
||||
# with key wrapping mode is used:
|
||||
# Perform key agreement with key wrapping deferred at step 3
|
||||
for i in range(len(keys)):
|
||||
wrapped = alg.agree_upon_key_and_wrap_cek(
|
||||
enc, shared_header, keys[i], sender_key, epks[i], cek, tag
|
||||
)
|
||||
recipients[i]["encrypted_key"] = wrapped["ek"]
|
||||
|
||||
# step 8: build resulting message
|
||||
obj = OrderedDict()
|
||||
|
||||
if shared_header.protected:
|
||||
obj["protected"] = to_unicode(json_b64encode(shared_header.protected))
|
||||
|
||||
if shared_header.unprotected:
|
||||
obj["unprotected"] = shared_header.unprotected
|
||||
|
||||
for recipient in recipients:
|
||||
if not recipient["header"]:
|
||||
del recipient["header"]
|
||||
recipient["encrypted_key"] = to_unicode(
|
||||
urlsafe_b64encode(recipient["encrypted_key"])
|
||||
)
|
||||
for member in set(recipient.keys()):
|
||||
if member not in {"header", "encrypted_key"}:
|
||||
del recipient[member]
|
||||
obj["recipients"] = recipients
|
||||
|
||||
if jwe_aad is not None:
|
||||
obj["aad"] = to_unicode(urlsafe_b64encode(jwe_aad))
|
||||
|
||||
obj["iv"] = to_unicode(urlsafe_b64encode(iv))
|
||||
|
||||
obj["ciphertext"] = to_unicode(urlsafe_b64encode(ciphertext))
|
||||
|
||||
obj["tag"] = to_unicode(urlsafe_b64encode(tag))
|
||||
|
||||
return obj
|
||||
|
||||
def serialize(self, header, payload, key, sender_key=None):
|
||||
"""Generate a JWE Serialization.
|
||||
|
||||
It will automatically generate a compact or JSON serialization depending
|
||||
on `header` argument. If `header` is a dict with "protected",
|
||||
"unprotected" and/or "recipients" keys, it will call `serialize_json`,
|
||||
otherwise it will call `serialize_compact`.
|
||||
|
||||
:param header: A dict of header(s)
|
||||
:param payload: Payload (bytes or a value convertible to bytes)
|
||||
:param key: Public key(s) used to encrypt payload
|
||||
:param sender_key: Sender's private key in case
|
||||
JWEAlgorithmWithTagAwareKeyAgreement is used
|
||||
:return: JWE compact serialization as bytes or
|
||||
JWE JSON serialization as dict
|
||||
"""
|
||||
if "protected" in header or "unprotected" in header or "recipients" in header:
|
||||
return self.serialize_json(header, payload, key, sender_key)
|
||||
|
||||
return self.serialize_compact(header, payload, key, sender_key)
|
||||
|
||||
def deserialize_compact(self, s, key, decode=None, sender_key=None):
|
||||
"""Extract JWE Compact Serialization.
|
||||
|
||||
:param s: JWE Compact Serialization as bytes
|
||||
:param key: Private key used to decrypt payload
|
||||
(optionally can be a tuple of kid and essentially key)
|
||||
:param decode: Function to decode payload data
|
||||
:param sender_key: Sender's public key in case
|
||||
JWEAlgorithmWithTagAwareKeyAgreement is used
|
||||
:return: dict with `header` and `payload` keys where `header` value is
|
||||
a dict containing protected header fields
|
||||
"""
|
||||
try:
|
||||
s = to_bytes(s)
|
||||
protected_s, ek_s, iv_s, ciphertext_s, tag_s = s.rsplit(b".")
|
||||
except ValueError as exc:
|
||||
raise DecodeError("Not enough segments") from exc
|
||||
|
||||
protected = extract_header(protected_s, DecodeError)
|
||||
ek = extract_segment(ek_s, DecodeError, "encryption key")
|
||||
iv = extract_segment(iv_s, DecodeError, "initialization vector")
|
||||
ciphertext = extract_segment(ciphertext_s, DecodeError, "ciphertext")
|
||||
tag = extract_segment(tag_s, DecodeError, "authentication tag")
|
||||
|
||||
alg = self.get_header_alg(protected)
|
||||
enc = self.get_header_enc(protected)
|
||||
zip_alg = self.get_header_zip(protected)
|
||||
|
||||
self._validate_sender_key(sender_key, alg)
|
||||
self._validate_private_headers(protected, alg)
|
||||
|
||||
if isinstance(key, tuple) and len(key) == 2:
|
||||
# Ignore separately provided kid, extract essentially key only
|
||||
key = key[1]
|
||||
|
||||
key = prepare_key(alg, protected, key)
|
||||
|
||||
if sender_key is not None:
|
||||
sender_key = alg.prepare_key(sender_key)
|
||||
|
||||
if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement):
|
||||
# For a JWE algorithm with tag-aware key agreement:
|
||||
if alg.key_size is not None:
|
||||
# In case key agreement with key wrapping mode is used:
|
||||
# Provide authentication tag to .unwrap method
|
||||
cek = alg.unwrap(enc, ek, protected, key, sender_key, tag)
|
||||
else:
|
||||
# Otherwise, don't provide authentication tag to .unwrap method
|
||||
cek = alg.unwrap(enc, ek, protected, key, sender_key)
|
||||
else:
|
||||
# For any other JWE algorithm:
|
||||
# Don't provide authentication tag to .unwrap method
|
||||
cek = alg.unwrap(enc, ek, protected, key)
|
||||
|
||||
aad = to_bytes(protected_s, "ascii")
|
||||
msg = enc.decrypt(ciphertext, aad, iv, tag, cek)
|
||||
|
||||
if zip_alg:
|
||||
payload = zip_alg.decompress(to_bytes(msg))
|
||||
else:
|
||||
payload = msg
|
||||
|
||||
if decode:
|
||||
payload = decode(payload)
|
||||
return {"header": protected, "payload": payload}
|
||||
|
||||
def deserialize_json(self, obj, key, decode=None, sender_key=None): # noqa: C901
|
||||
"""Extract JWE JSON Serialization.
|
||||
|
||||
:param obj: JWE JSON Serialization as dict or str
|
||||
:param key: Private key used to decrypt payload
|
||||
(optionally can be a tuple of kid and essentially key)
|
||||
:param decode: Function to decode payload data
|
||||
:param sender_key: Sender's public key in case
|
||||
JWEAlgorithmWithTagAwareKeyAgreement is used
|
||||
:return: dict with `header` and `payload` keys where `header` value is
|
||||
a dict containing `protected`, `unprotected`, `recipients` and/or
|
||||
`aad` keys
|
||||
"""
|
||||
obj = ensure_dict(obj, "JWE")
|
||||
obj = deepcopy(obj)
|
||||
|
||||
if "protected" in obj:
|
||||
protected = extract_header(to_bytes(obj["protected"]), DecodeError)
|
||||
else:
|
||||
protected = None
|
||||
|
||||
unprotected = obj.get("unprotected")
|
||||
|
||||
recipients = obj["recipients"]
|
||||
for recipient in recipients:
|
||||
if "header" not in recipient:
|
||||
recipient["header"] = {}
|
||||
recipient["encrypted_key"] = extract_segment(
|
||||
to_bytes(recipient["encrypted_key"]), DecodeError, "encrypted key"
|
||||
)
|
||||
|
||||
if "aad" in obj:
|
||||
jwe_aad = extract_segment(to_bytes(obj["aad"]), DecodeError, "JWE AAD")
|
||||
else:
|
||||
jwe_aad = None
|
||||
|
||||
iv = extract_segment(to_bytes(obj["iv"]), DecodeError, "initialization vector")
|
||||
|
||||
ciphertext = extract_segment(
|
||||
to_bytes(obj["ciphertext"]), DecodeError, "ciphertext"
|
||||
)
|
||||
|
||||
tag = extract_segment(to_bytes(obj["tag"]), DecodeError, "authentication tag")
|
||||
|
||||
shared_header = JWESharedHeader(protected, unprotected)
|
||||
|
||||
alg = self.get_header_alg(shared_header)
|
||||
enc = self.get_header_enc(shared_header)
|
||||
zip_alg = self.get_header_zip(shared_header)
|
||||
|
||||
self._validate_sender_key(sender_key, alg)
|
||||
self._validate_private_headers(shared_header, alg)
|
||||
for recipient in recipients:
|
||||
self._validate_private_headers(recipient["header"], alg)
|
||||
|
||||
kid = None
|
||||
if isinstance(key, tuple) and len(key) == 2:
|
||||
# Extract separately provided kid and essentially key
|
||||
kid = key[0]
|
||||
key = key[1]
|
||||
|
||||
key = alg.prepare_key(key)
|
||||
|
||||
if kid is None:
|
||||
# If kid has not been provided separately, try to get it from key itself
|
||||
kid = key.kid
|
||||
|
||||
if sender_key is not None:
|
||||
sender_key = alg.prepare_key(sender_key)
|
||||
|
||||
def _unwrap_with_sender_key_and_tag(ek, header):
|
||||
return alg.unwrap(enc, ek, header, key, sender_key, tag)
|
||||
|
||||
def _unwrap_with_sender_key_and_without_tag(ek, header):
|
||||
return alg.unwrap(enc, ek, header, key, sender_key)
|
||||
|
||||
def _unwrap_without_sender_key_and_tag(ek, header):
|
||||
return alg.unwrap(enc, ek, header, key)
|
||||
|
||||
def _unwrap_for_matching_recipient(unwrap_func):
|
||||
if kid is not None:
|
||||
for recipient in recipients:
|
||||
if recipient["header"].get("kid") == kid:
|
||||
header = JWEHeader(protected, unprotected, recipient["header"])
|
||||
return unwrap_func(recipient["encrypted_key"], header)
|
||||
|
||||
# Since no explicit match has been found, iterate over all the recipients
|
||||
error = None
|
||||
for recipient in recipients:
|
||||
header = JWEHeader(protected, unprotected, recipient["header"])
|
||||
try:
|
||||
return unwrap_func(recipient["encrypted_key"], header)
|
||||
except Exception as e:
|
||||
error = e
|
||||
else:
|
||||
if error is None:
|
||||
raise KeyMismatchError()
|
||||
else:
|
||||
raise error
|
||||
|
||||
if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement):
|
||||
# For a JWE algorithm with tag-aware key agreement:
|
||||
if alg.key_size is not None:
|
||||
# In case key agreement with key wrapping mode is used:
|
||||
# Provide authentication tag to .unwrap method
|
||||
cek = _unwrap_for_matching_recipient(_unwrap_with_sender_key_and_tag)
|
||||
else:
|
||||
# Otherwise, don't provide authentication tag to .unwrap method
|
||||
cek = _unwrap_for_matching_recipient(
|
||||
_unwrap_with_sender_key_and_without_tag
|
||||
)
|
||||
else:
|
||||
# For any other JWE algorithm:
|
||||
# Don't provide authentication tag to .unwrap method
|
||||
cek = _unwrap_for_matching_recipient(_unwrap_without_sender_key_and_tag)
|
||||
|
||||
aad = to_bytes(obj.get("protected", ""))
|
||||
if "aad" in obj:
|
||||
aad += b"." + to_bytes(obj["aad"])
|
||||
aad = to_bytes(aad, "ascii")
|
||||
|
||||
msg = enc.decrypt(ciphertext, aad, iv, tag, cek)
|
||||
|
||||
if zip_alg:
|
||||
payload = zip_alg.decompress(to_bytes(msg))
|
||||
else:
|
||||
payload = msg
|
||||
|
||||
if decode:
|
||||
payload = decode(payload)
|
||||
|
||||
for recipient in recipients:
|
||||
if not recipient["header"]:
|
||||
del recipient["header"]
|
||||
for member in set(recipient.keys()):
|
||||
if member != "header":
|
||||
del recipient[member]
|
||||
|
||||
header = {}
|
||||
if protected:
|
||||
header["protected"] = protected
|
||||
if unprotected:
|
||||
header["unprotected"] = unprotected
|
||||
header["recipients"] = recipients
|
||||
if jwe_aad is not None:
|
||||
header["aad"] = jwe_aad
|
||||
|
||||
return {"header": header, "payload": payload}
|
||||
|
||||
def deserialize(self, obj, key, decode=None, sender_key=None):
|
||||
"""Extract a JWE Serialization.
|
||||
|
||||
It supports both compact and JSON serialization.
|
||||
|
||||
:param obj: JWE compact serialization as bytes or
|
||||
JWE JSON serialization as dict or str
|
||||
:param key: Private key used to decrypt payload
|
||||
(optionally can be a tuple of kid and essentially key)
|
||||
:param decode: Function to decode payload data
|
||||
:param sender_key: Sender's public key in case
|
||||
JWEAlgorithmWithTagAwareKeyAgreement is used
|
||||
:return: dict with `header` and `payload` keys
|
||||
"""
|
||||
if isinstance(obj, dict):
|
||||
return self.deserialize_json(obj, key, decode, sender_key)
|
||||
|
||||
obj = to_bytes(obj)
|
||||
if obj.startswith(b"{") and obj.endswith(b"}"):
|
||||
return self.deserialize_json(obj, key, decode, sender_key)
|
||||
|
||||
return self.deserialize_compact(obj, key, decode, sender_key)
|
||||
|
||||
@staticmethod
|
||||
def parse_json(obj):
|
||||
"""Parse JWE JSON Serialization.
|
||||
|
||||
:param obj: JWE JSON Serialization as str or dict
|
||||
:return: Parsed JWE JSON Serialization as dict if `obj` is an str,
|
||||
or `obj` as is if `obj` is already a dict
|
||||
"""
|
||||
return ensure_dict(obj, "JWE")
|
||||
|
||||
def get_header_alg(self, header):
|
||||
if "alg" not in header:
|
||||
raise MissingAlgorithmError()
|
||||
|
||||
alg = header["alg"]
|
||||
if self._algorithms is not None and alg not in self._algorithms:
|
||||
raise UnsupportedAlgorithmError()
|
||||
if alg not in self.ALG_REGISTRY:
|
||||
raise UnsupportedAlgorithmError()
|
||||
return self.ALG_REGISTRY[alg]
|
||||
|
||||
def get_header_enc(self, header):
|
||||
if "enc" not in header:
|
||||
raise MissingEncryptionAlgorithmError()
|
||||
enc = header["enc"]
|
||||
if self._algorithms is not None and enc not in self._algorithms:
|
||||
raise UnsupportedEncryptionAlgorithmError()
|
||||
if enc not in self.ENC_REGISTRY:
|
||||
raise UnsupportedEncryptionAlgorithmError()
|
||||
return self.ENC_REGISTRY[enc]
|
||||
|
||||
def get_header_zip(self, header):
|
||||
if "zip" in header:
|
||||
z = header["zip"]
|
||||
if self._algorithms is not None and z not in self._algorithms:
|
||||
raise UnsupportedCompressionAlgorithmError()
|
||||
if z not in self.ZIP_REGISTRY:
|
||||
raise UnsupportedCompressionAlgorithmError()
|
||||
return self.ZIP_REGISTRY[z]
|
||||
|
||||
def _validate_sender_key(self, sender_key, alg):
|
||||
if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement):
|
||||
if sender_key is None:
|
||||
raise ValueError(
|
||||
f"{alg.name} algorithm requires sender_key but passed sender_key value is None"
|
||||
)
|
||||
else:
|
||||
if sender_key is not None:
|
||||
raise ValueError(
|
||||
f"{alg.name} algorithm does not use sender_key but passed sender_key value is not None"
|
||||
)
|
||||
|
||||
def _validate_private_headers(self, header, alg):
|
||||
# only validate private headers when developers set
|
||||
# private headers explicitly
|
||||
if self._private_headers is None:
|
||||
return
|
||||
|
||||
names = self.REGISTERED_HEADER_PARAMETER_NAMES.copy()
|
||||
names = names.union(self._private_headers)
|
||||
|
||||
if alg.EXTRA_HEADERS:
|
||||
names = names.union(alg.EXTRA_HEADERS)
|
||||
|
||||
for k in header:
|
||||
if k not in names:
|
||||
raise InvalidHeaderParameterNameError(k)
|
||||
|
||||
|
||||
def prepare_key(alg, header, key):
|
||||
if callable(key):
|
||||
key = key(header, None)
|
||||
elif key is None and "jwk" in header:
|
||||
key = header["jwk"]
|
||||
return alg.prepare_key(key)
|
||||
@@ -0,0 +1,157 @@
|
||||
import os
|
||||
from abc import ABCMeta
|
||||
|
||||
|
||||
class JWEAlgorithmBase(metaclass=ABCMeta): # noqa: B024
|
||||
"""Base interface for all JWE algorithms."""
|
||||
|
||||
EXTRA_HEADERS = None
|
||||
|
||||
name = None
|
||||
description = None
|
||||
algorithm_type = "JWE"
|
||||
algorithm_location = "alg"
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
raise NotImplementedError
|
||||
|
||||
def generate_preset(self, enc_alg, key):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class JWEAlgorithm(JWEAlgorithmBase, metaclass=ABCMeta):
|
||||
"""Interface for JWE algorithm conforming to RFC7518.
|
||||
JWA specification (RFC7518) SHOULD implement the algorithms for JWE
|
||||
with this base implementation.
|
||||
"""
|
||||
|
||||
def wrap(self, enc_alg, headers, key, preset=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def unwrap(self, enc_alg, ek, headers, key):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class JWEAlgorithmWithTagAwareKeyAgreement(JWEAlgorithmBase, metaclass=ABCMeta):
|
||||
"""Interface for JWE algorithm with tag-aware key agreement (in key agreement
|
||||
with key wrapping mode).
|
||||
ECDH-1PU is an example of such an algorithm.
|
||||
"""
|
||||
|
||||
def generate_keys_and_prepare_headers(self, enc_alg, key, sender_key, preset=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def agree_upon_key_and_wrap_cek(
|
||||
self, enc_alg, headers, key, sender_key, epk, cek, tag
|
||||
):
|
||||
raise NotImplementedError
|
||||
|
||||
def wrap(self, enc_alg, headers, key, sender_key, preset=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def unwrap(self, enc_alg, ek, headers, key, sender_key, tag=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class JWEEncAlgorithm:
|
||||
name = None
|
||||
description = None
|
||||
algorithm_type = "JWE"
|
||||
algorithm_location = "enc"
|
||||
|
||||
IV_SIZE = None
|
||||
CEK_SIZE = None
|
||||
|
||||
def generate_cek(self):
|
||||
return os.urandom(self.CEK_SIZE // 8)
|
||||
|
||||
def generate_iv(self):
|
||||
return os.urandom(self.IV_SIZE // 8)
|
||||
|
||||
def check_iv(self, iv):
|
||||
if len(iv) * 8 != self.IV_SIZE:
|
||||
raise ValueError('Invalid "iv" size')
|
||||
|
||||
def encrypt(self, msg, aad, iv, key):
|
||||
"""Encrypt the given "msg" text.
|
||||
|
||||
:param msg: text to be encrypt in bytes
|
||||
:param aad: additional authenticated data in bytes
|
||||
:param iv: initialization vector in bytes
|
||||
:param key: encrypted key in bytes
|
||||
:return: (ciphertext, tag)
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def decrypt(self, ciphertext, aad, iv, tag, key):
|
||||
"""Decrypt the given cipher text.
|
||||
|
||||
:param ciphertext: ciphertext in bytes
|
||||
:param aad: additional authenticated data in bytes
|
||||
:param iv: initialization vector in bytes
|
||||
:param tag: authentication tag in bytes
|
||||
:param key: encrypted key in bytes
|
||||
:return: message
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class JWEZipAlgorithm:
|
||||
name = None
|
||||
description = None
|
||||
algorithm_type = "JWE"
|
||||
algorithm_location = "zip"
|
||||
|
||||
def compress(self, s):
|
||||
raise NotImplementedError
|
||||
|
||||
def decompress(self, s):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class JWESharedHeader(dict):
|
||||
"""Shared header object for JWE.
|
||||
|
||||
Combines protected header and shared unprotected header together.
|
||||
"""
|
||||
|
||||
def __init__(self, protected, unprotected):
|
||||
obj = {}
|
||||
if unprotected:
|
||||
obj.update(unprotected)
|
||||
if protected:
|
||||
obj.update(protected)
|
||||
super().__init__(obj)
|
||||
self.protected = protected if protected else {}
|
||||
self.unprotected = unprotected if unprotected else {}
|
||||
|
||||
def update_protected(self, addition):
|
||||
self.update(addition)
|
||||
self.protected.update(addition)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj):
|
||||
if isinstance(obj, cls):
|
||||
return obj
|
||||
return cls(obj.get("protected"), obj.get("unprotected"))
|
||||
|
||||
|
||||
class JWEHeader(dict):
|
||||
"""Header object for JWE.
|
||||
|
||||
Combines protected header, shared unprotected header
|
||||
and specific recipient's unprotected header together.
|
||||
"""
|
||||
|
||||
def __init__(self, protected, unprotected, header):
|
||||
obj = {}
|
||||
if unprotected:
|
||||
obj.update(unprotected)
|
||||
if header:
|
||||
obj.update(header)
|
||||
if protected:
|
||||
obj.update(protected)
|
||||
super().__init__(obj)
|
||||
self.protected = protected if protected else {}
|
||||
self.unprotected = unprotected if unprotected else {}
|
||||
self.header = header if header else {}
|
||||
Reference in New Issue
Block a user