Files
sourcehub/.venv/Lib/site-packages/joserfc/_rfc7518/jwe_algs.py
T
2026-09-14 23:22:10 +09:00

352 lines
13 KiB
Python

import secrets
import warnings
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.keywrap import (
aes_key_wrap,
aes_key_unwrap,
InvalidUnwrap,
)
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers.algorithms import AES
from cryptography.hazmat.primitives.ciphers.modes import GCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.exceptions import InvalidTag
from .derive_key import derive_key_for_concat_kdf
from .oct_key import OctKey
from .rsa_key import RSAKey
from .ec_key import ECKey
from .._rfc7516.models import (
JWEAlgModel,
JWEDirectEncryption,
JWEKeyEncryption,
JWEKeyWrapping,
JWEKeyAgreement,
JWEEncModel,
Recipient,
)
from ..util import to_bytes, urlsafe_b64encode, urlsafe_b64decode
from ..registry import HeaderParameter
from ..errors import (
InvalidKeyLengthError,
DecodeError,
SecurityWarning,
)
class DirectAlgEncryption(JWEDirectEncryption):
name = "dir"
description = "Direct use of a shared symmetric key"
recommended = True
def compute_cek(self, size: int, recipient: Recipient[OctKey]) -> bytes:
key = recipient.recipient_key
assert key is not None
self.check_key_type(key)
cek = key.raw_value
if len(cek) * 8 != size:
raise InvalidKeyLengthError(f"A key of size {size} bits MUST be used")
return cek
class RSAAlgKeyEncryption(JWEKeyEncryption):
#: A key of size 2048 bits or larger MUST be used with these algorithms
#: RSA1_5, RSA-OAEP, RSA-OAEP-256
key_size = 2048
key_types = ["RSA"]
def __init__(self, name: str, description: str, pad_fn: padding.AsymmetricPadding, recommended: bool = False):
self.name = name
self.description = description
self.padding = pad_fn
self.recommended = recommended
def encrypt_cek(self, cek: bytes, recipient: Recipient[RSAKey]) -> bytes:
key = recipient.recipient_key
assert key is not None
self.check_key_type(key)
op_key = key.get_op_key("encrypt")
if op_key.key_size < self.key_size:
raise InvalidKeyLengthError(f"A key of size {self.key_size} bits or larger MUST be used")
return op_key.encrypt(cek, self.padding)
def decrypt_cek(self, recipient: Recipient[RSAKey]) -> bytes:
key = recipient.recipient_key
assert key is not None
self.check_key_type(key)
op_key = key.get_op_key("decrypt")
try:
assert recipient.encrypted_key is not None
cek = op_key.decrypt(recipient.encrypted_key, self.padding)
except ValueError as error:
raise DecodeError(str(error))
return cek
class AESAlgKeyWrapping(JWEKeyWrapping):
def __init__(self, key_size: int, recommended: bool = False):
self.name = f"A{key_size}KW"
self.description = f"AES Key Wrap using {key_size}-bit key"
self.key_size = key_size
self.recommended = recommended
def wrap_cek(self, cek: bytes, key: bytes) -> bytes:
self.check_op_key(key)
return aes_key_wrap(key, cek)
def unwrap_cek(self, ek: bytes, key: bytes) -> bytes:
self.check_op_key(key)
try:
cek = aes_key_unwrap(key, ek)
except InvalidUnwrap:
raise DecodeError("Unwrap AES key failed")
return cek
def encrypt_cek(self, cek: bytes, recipient: Recipient[OctKey]) -> bytes:
key = recipient.recipient_key
assert key is not None
self.check_key_type(key)
op_key = key.get_op_key("wrapKey")
return self.wrap_cek(cek, op_key)
def decrypt_cek(self, recipient: Recipient[OctKey]) -> bytes:
key = recipient.recipient_key
assert key is not None
self.check_key_type(key)
op_key = key.get_op_key("unwrapKey")
assert recipient.encrypted_key is not None
return self.unwrap_cek(recipient.encrypted_key, op_key)
class AESGCMAlgKeyWrapping(JWEKeyWrapping):
more_header_registry = {
"iv": HeaderParameter("Initialization vector", "str", True),
"tag": HeaderParameter("Authentication tag", "str", True),
}
def __init__(self, key_size: int):
self.name = f"A{key_size}GCMKW"
self.description = f"Key wrapping with AES GCM using {key_size}-bit key"
self.key_size = key_size
def wrap_cek(self, cek: bytes, key: bytes) -> bytes: # pragma: no cover
raise RuntimeError(f"{self.name} can not be used together with Key Agreement")
def unwrap_cek(self, ek: bytes, key: bytes) -> bytes: # pragma: no cover
raise RuntimeError(f"{self.name} can not be used together with Key Agreement")
def encrypt_cek(self, cek: bytes, recipient: Recipient[OctKey]) -> bytes:
key = recipient.recipient_key
assert key is not None
self.check_key_type(key)
op_key = key.get_op_key("wrapKey")
self.check_op_key(op_key)
#: https://tools.ietf.org/html/rfc7518#section-4.7.1.1
#: The "iv" (initialization vector) Header Parameter value is the
#: base64url-encoded representation of the 96-bit IV value
iv_size = 96
iv = secrets.token_bytes(iv_size // 8)
cipher = Cipher(AES(op_key), GCM(iv))
enc = cipher.encryptor()
encrypted_key = enc.update(cek) + enc.finalize()
recipient.add_header("iv", urlsafe_b64encode(iv).decode("ascii"))
recipient.add_header("tag", urlsafe_b64encode(enc.tag).decode("ascii"))
return encrypted_key
def decrypt_cek(self, recipient: Recipient[OctKey]) -> bytes:
key = recipient.recipient_key
assert key is not None
self.check_key_type(key)
op_key = key.get_op_key("unwrapKey")
self.check_op_key(op_key)
headers = recipient.headers()
assert "iv" in headers
assert "tag" in headers
iv = urlsafe_b64decode(to_bytes(headers["iv"]))
tag = urlsafe_b64decode(to_bytes(headers["tag"]))
cipher = Cipher(AES(op_key), GCM(iv, tag))
d = cipher.decryptor()
try:
assert recipient.encrypted_key is not None
cek = d.update(recipient.encrypted_key) + d.finalize()
except InvalidTag as error:
raise DecodeError(str(error))
return cek
class ECDHESAlgKeyAgreement(JWEKeyAgreement):
key_types = ["EC", "OKP"]
more_header_registry = {
"epk": HeaderParameter("Ephemeral Public Key", "jwk", True),
"apu": HeaderParameter("Agreement PartyUInfo", "str"),
"apv": HeaderParameter("Agreement PartyVInfo", "str"),
}
# https://tools.ietf.org/html/rfc7518#section-4.6
def __init__(self, key_wrapping: JWEKeyWrapping | None = None):
if key_wrapping is None:
self.name = "ECDH-ES"
self.description = "ECDH-ES in the Direct Key Agreement mode"
self.key_size = None
self.recommended = True
else:
self.name = f"ECDH-ES+{key_wrapping.name}"
self.description = f"ECDH-ES using Concat KDF and CEK wrapped with {key_wrapping.name}"
self.key_size = key_wrapping.key_size
self.recommended = key_wrapping.recommended
self.key_wrapping = key_wrapping
def encrypt_agreed_upon_key(self, enc: JWEEncModel, recipient: Recipient[ECKey]) -> bytes:
recipient_key = recipient.recipient_key
assert recipient_key is not None
ephemeral_key = recipient.ephemeral_key
assert ephemeral_key is not None
shared_key = ephemeral_key.exchange_derive_key(recipient_key)
headers = recipient.headers()
return derive_key_for_concat_kdf(shared_key, headers, enc.cek_size, self.key_size)
def decrypt_agreed_upon_key(self, enc: JWEEncModel, recipient: Recipient[ECKey]) -> bytes:
headers = recipient.headers()
assert "epk" in headers
recipient_key = recipient.recipient_key
assert recipient_key is not None
self.check_key_type(recipient_key)
ephemeral_key = recipient_key.import_key(headers["epk"])
shared_key = recipient_key.exchange_derive_key(ephemeral_key)
return derive_key_for_concat_kdf(shared_key, headers, enc.cek_size, self.key_size)
def validate_p2c(value: int) -> None:
if not isinstance(value, int):
raise ValueError("must be an int")
# A minimum iteration count of 1000 is RECOMMENDED.
if value < 1000:
warnings.warn("A minimum iteration count of 1000 is RECOMMENDED", SecurityWarning)
max_value = 300000
if value > max_value:
raise ValueError(f"must be less than {max_value}")
class PBES2HSAlgKeyEncryption(JWEKeyEncryption):
# https://www.rfc-editor.org/rfc/rfc7518#section-4.8
key_size: int
more_header_registry = {
"p2s": HeaderParameter("PBES2 Salt Input", "str", True),
"p2c": HeaderParameter("PBES2 Count", validate_p2c, True),
}
key_types = ["oct"]
DEFAULT_P2C = 2048
def __init__(self, hash_size: int, key_wrapping: JWEKeyWrapping):
self.name = f"PBES2-HS{hash_size}+{key_wrapping.name}"
self.description = f"PBES2 with HMAC SHA-{hash_size} and {key_wrapping.name} wrapping"
self.key_size = key_wrapping.key_size
self.key_wrapping = key_wrapping
self.hash_alg = getattr(hashes, f"SHA{hash_size}")()
def compute_derived_key(self, key: bytes, p2s: bytes, p2c: int) -> bytes:
# The salt value used is (UTF8(Alg) || 0x00 || Salt Input)
salt = to_bytes(self.name) + b"\x00" + p2s
kdf = PBKDF2HMAC(
algorithm=self.hash_alg,
length=self.key_size // 8,
salt=salt,
iterations=p2c,
)
return kdf.derive(key)
def encrypt_cek(self, cek: bytes, recipient: Recipient[OctKey]) -> bytes:
headers = recipient.headers()
if "p2s" not in headers:
p2s = secrets.token_bytes(16)
recipient.add_header("p2s", urlsafe_b64encode(p2s).decode("ascii"))
else:
p2s = urlsafe_b64decode(to_bytes(headers["p2s"]))
if "p2c" not in headers:
# A minimum iteration count of 1000 is RECOMMENDED.
p2c = self.DEFAULT_P2C
recipient.add_header("p2c", p2c)
else:
p2c = headers["p2c"]
key = recipient.recipient_key
assert key is not None
self.check_key_type(key)
kek = self.compute_derived_key(key.get_op_key("deriveKey"), p2s, p2c)
return self.key_wrapping.wrap_cek(cek, kek)
def decrypt_cek(self, recipient: Recipient[OctKey]) -> bytes:
headers = recipient.headers()
assert "p2s" in headers
assert "p2c" in headers
p2s = urlsafe_b64decode(to_bytes(headers["p2s"]))
p2c = headers["p2c"]
key = recipient.recipient_key
assert key is not None
self.check_key_type(key)
kek = self.compute_derived_key(key.get_op_key("deriveKey"), p2s, p2c)
assert recipient.encrypted_key is not None
return self.key_wrapping.unwrap_cek(recipient.encrypted_key, kek)
RSA1_5 = RSAAlgKeyEncryption("RSA1_5", "RSAES-PKCS1-v1_5", padding.PKCS1v15())
RSA1_5.security_warning = 'JWE algorithm "RSA1_5" is deprecated, via draft-ietf-jose-deprecate-none-rsa15-02'
A128KW = AESAlgKeyWrapping(128, True) # A128KW, Recommended
A192KW = AESAlgKeyWrapping(192) # A192KW
A256KW = AESAlgKeyWrapping(256, True) # A256KW, Recommended
#: https://www.rfc-editor.org/rfc/rfc7518#section-4.1
JWE_ALG_MODELS: list[JWEAlgModel] = [
RSA1_5,
RSAAlgKeyEncryption(
"RSA-OAEP",
"RSAES OAEP using default parameters",
padding.OAEP(padding.MGF1(hashes.SHA1()), hashes.SHA1(), None),
True,
), # Recommended+
RSAAlgKeyEncryption(
"RSA-OAEP-256",
"RSAES OAEP using SHA-256 and MGF1 with SHA-256",
padding.OAEP(padding.MGF1(hashes.SHA256()), hashes.SHA256(), None),
),
A128KW,
A192KW,
A256KW,
DirectAlgEncryption(), # dir, Recommended
ECDHESAlgKeyAgreement(None), # ECDH-ES, Recommended+
ECDHESAlgKeyAgreement(A128KW), # ECDH-ES+A128KW, Recommended
ECDHESAlgKeyAgreement(A192KW), # ECDH-ES+A192KW
ECDHESAlgKeyAgreement(A256KW), # ECDH-ES+A256KW, Recommended
AESGCMAlgKeyWrapping(128), # A128GCMKW
AESGCMAlgKeyWrapping(192), # A192GCMKW
AESGCMAlgKeyWrapping(256), # A256GCMKW
PBES2HSAlgKeyEncryption(256, A128KW), # PBES2-HS256+A128KW
PBES2HSAlgKeyEncryption(384, A192KW), # PBES2-HS384+A192KW
PBES2HSAlgKeyEncryption(512, A256KW), # PBES2-HS512+A256KW
]
# compatible alias
DirectAlgModel = DirectAlgEncryption
AESAlgModel = AESAlgKeyWrapping
ECDHESAlgModel = ECDHESAlgKeyAgreement
AESGCMAlgModel = AESGCMAlgKeyWrapping
PBES2HSAlgModel = PBES2HSAlgKeyEncryption