352 lines
13 KiB
Python
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
|