Skip to content

Commit

Permalink
fix: #1454 remove python-jose package dependency from BCSC integratio…
Browse files Browse the repository at this point in the history
…n. (#1500)
  • Loading branch information
ianliuwk1019 authored Jul 25, 2024
1 parent f6f22bb commit c6c7776
Show file tree
Hide file tree
Showing 7 changed files with 520 additions and 21 deletions.
107 changes: 107 additions & 0 deletions server/backend/api/app/integration/bcsc/bcsc_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import hashlib


class JWEError(Exception):
"""Base error for all JWE errors"""
pass


class JWEParseError(JWEError):
"""Could not parse the JWE string provided"""
pass


class JOSEError(Exception):
pass


class JWKError(JOSEError):
pass


class Algorithms:
# DS Algorithms
NONE = "none"
HS256 = "HS256"
HS384 = "HS384"
HS512 = "HS512"
RS256 = "RS256"
RS384 = "RS384"
RS512 = "RS512"
ES256 = "ES256"
ES384 = "ES384"
ES512 = "ES512"

# Content Encryption Algorithms
A128CBC_HS256 = "A128CBC-HS256"
A192CBC_HS384 = "A192CBC-HS384"
A256CBC_HS512 = "A256CBC-HS512"
A128GCM = "A128GCM"
A192GCM = "A192GCM"
A256GCM = "A256GCM"

# Pseudo algorithm for encryption
A128CBC = "A128CBC"
A192CBC = "A192CBC"
A256CBC = "A256CBC"

# CEK Encryption Algorithms
DIR = "dir"
RSA1_5 = "RSA1_5"
RSA_OAEP = "RSA-OAEP"
RSA_OAEP_256 = "RSA-OAEP-256"
A128KW = "A128KW"
A192KW = "A192KW"
A256KW = "A256KW"
ECDH_ES = "ECDH-ES"
ECDH_ES_A128KW = "ECDH-ES+A128KW"
ECDH_ES_A192KW = "ECDH-ES+A192KW"
ECDH_ES_A256KW = "ECDH-ES+A256KW"
A128GCMKW = "A128GCMKW"
A192GCMKW = "A192GCMKW"
A256GCMKW = "A256GCMKW"
PBES2_HS256_A128KW = "PBES2-HS256+A128KW"
PBES2_HS384_A192KW = "PBES2-HS384+A192KW"
PBES2_HS512_A256KW = "PBES2-HS512+A256KW"

# Compression Algorithms
DEF = "DEF"

HMAC = {HS256, HS384, HS512}
RSA_DS = {RS256, RS384, RS512}
RSA_KW = {RSA1_5, RSA_OAEP, RSA_OAEP_256}
RSA = RSA_DS.union(RSA_KW)
EC_DS = {ES256, ES384, ES512}
EC_KW = {ECDH_ES, ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW}
EC = EC_DS.union(EC_KW)
AES_PSEUDO = {A128CBC, A192CBC, A256CBC, A128GCM, A192GCM, A256GCM}
AES_JWE_ENC = {A128CBC_HS256, A192CBC_HS384, A256CBC_HS512, A128GCM, A192GCM, A256GCM}
AES_ENC = AES_JWE_ENC.union(AES_PSEUDO)
AES_KW = {A128KW, A192KW, A256KW}
AEC_GCM_KW = {A128GCMKW, A192GCMKW, A256GCMKW}
AES = AES_ENC.union(AES_KW)
PBES2_KW = {PBES2_HS256_A128KW, PBES2_HS384_A192KW, PBES2_HS512_A256KW}

HMAC_AUTH_TAG = {A128CBC_HS256, A192CBC_HS384, A256CBC_HS512}
GCM = {A128GCM, A192GCM, A256GCM}

SUPPORTED = HMAC.union(RSA_DS).union(EC_DS).union([DIR]).union(AES_JWE_ENC).union(RSA_KW).union(AES_KW)

ALL = SUPPORTED.union([NONE]).union(AEC_GCM_KW).union(EC_KW).union(PBES2_KW)

HASHES = {
HS256: hashlib.sha256,
HS384: hashlib.sha384,
HS512: hashlib.sha512,
RS256: hashlib.sha256,
RS384: hashlib.sha384,
RS512: hashlib.sha512,
ES256: hashlib.sha256,
ES384: hashlib.sha384,
ES512: hashlib.sha512,
}

KEYS = {}


ALGORITHMS = Algorithms()
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
import binascii
import json
import logging
from collections.abc import Mapping
from struct import pack
from jose import jwk
from jose.constants import ALGORITHMS
from jose.exceptions import JWEError, JWEParseError
from jose.utils import base64url_decode, ensure_binary

from api.app.integration.bcsc import bcsc_jwk
from api.app.integration.bcsc.bcsc_constants import (ALGORITHMS, JWEError,
JWEParseError)
from api.app.utils import utils

LOGGER = logging.getLogger(__name__)

"""
Note:
# - All integration files under "integration/bcsc" path are based on previous "python-jose" package
and ported library code to FAM with modification in order to make FAM-BCSC login flow
encryption-decryptoin works
- To get rid of "python-jose" package (which has security vulnerability and did not have updated version
and currently is not maintained for years), attempts to use other libraries with jwe capabilities were
not successful (authlib and joserfc libraries), so temporary solution for now is to ported more code to
be able to remove dependency. This is far more than ideal and code is ugly.
# - The difficult decision for why copying library code to modify for fitting BCSC authentication can be
found on ticket: (#1454 - https://github.com/orgs/bcgov/projects/65/views/1?pane=issue&itemId=67731674)
# - It would be better later if it is still possble to resolve jwe token decryption difficulties with
BCSC team to standardize the JWE token encryption-decryption with common known practice and not with
BCSC customized internal token spec.
"""


def decrypt(jwe_str, decrypted_key):
Expand All @@ -27,7 +47,7 @@ def decrypt(jwe_str, decrypted_key):
>>> jwe.decrypt(jwe_string, 'asecret128bitkey')
'Hello, World!'
"""
header, encoded_header, encrypted_key, iv, cipher_text, auth_tag = _jwe_compact_deserialize(jwe_str)
header, encoded_header, _encrypted_key, iv, cipher_text, auth_tag = _jwe_compact_deserialize(jwe_str)

try:
# Determine the Key Management Mode employed by the algorithm
Expand Down Expand Up @@ -116,8 +136,10 @@ def _decrypt_and_auth(cek_bytes, enc, cipher_text, iv, aad, auth_tag):
if enc in ALGORITHMS.HMAC_AUTH_TAG:
encryption_key, mac_key, key_len = _get_encryption_key_mac_key_and_key_length_from_cek(cek_bytes, enc)
auth_tag_check = _auth_tag(cipher_text, iv, aad, mac_key, key_len)

# BCSC enc uses algorithm in ALGORITHMS.HMAC_AUTH_TAG, below will not run.
elif enc in ALGORITHMS.GCM:
encryption_key = jwk.construct(cek_bytes, enc)
encryption_key = bcsc_jwk.jwk_construct(cek_bytes, enc)
auth_tag_check = auth_tag # GCM check auth on decrypt
else:
raise NotImplementedError(f"enc {enc} is not implemented!")
Expand All @@ -141,7 +163,7 @@ def _get_hmac_key(enc, mac_key_bytes):
(HMACKey): The key to perform HMAC actions
"""
_, hash_alg = enc.split("-")
mac_key = jwk.construct(mac_key_bytes, hash_alg)
mac_key = bcsc_jwk.jwk_construct(mac_key_bytes, hash_alg)
return mac_key


Expand All @@ -151,7 +173,7 @@ def _get_encryption_key_mac_key_and_key_length_from_cek(cek_bytes, enc):
mac_key = _get_hmac_key(enc, mac_key_bytes)
encryption_key_bytes = cek_bytes[-derived_key_len:]
encryption_alg, _ = enc.split("-")
encryption_key = jwk.construct(encryption_key_bytes, encryption_alg)
encryption_key = bcsc_jwk.jwk_construct(encryption_key_bytes, encryption_alg)
return encryption_key, mac_key, derived_key_len


Expand All @@ -170,12 +192,12 @@ def _jwe_compact_deserialize(jwe_bytes):
# Vector, the JWE Ciphertext, the JWE Authentication Tag, and the
# JWE AAD, following the restriction that no line breaks,
# whitespace, or other additional characters have been used.
jwe_bytes = ensure_binary(jwe_bytes)
jwe_bytes = utils.ensure_binary(jwe_bytes)
try:
header_segment, encrypted_key_segment, iv_segment, cipher_text_segment, auth_tag_segment = jwe_bytes.split(
b".", 4
)
header_data = base64url_decode(header_segment)
header_data = utils.base64url_decode(header_segment)
except ValueError:
raise JWEParseError("Not enough segments")
except (TypeError):
Expand Down Expand Up @@ -207,22 +229,22 @@ def _jwe_compact_deserialize(jwe_bytes):
raise JWEParseError("Invalid header string: must be a json object")

try:
encrypted_key = base64url_decode(encrypted_key_segment)
encrypted_key = utils.base64url_decode(encrypted_key_segment)
except (TypeError, binascii.Error):
raise JWEParseError("Invalid encrypted key")

try:
iv = base64url_decode(iv_segment)
iv = utils.base64url_decode(iv_segment)
except (TypeError, binascii.Error):
raise JWEParseError("Invalid IV")

try:
ciphertext = base64url_decode(cipher_text_segment)
ciphertext = utils.base64url_decode(cipher_text_segment)
except (TypeError, binascii.Error):
raise JWEParseError("Invalid cyphertext")

try:
auth_tag = base64url_decode(auth_tag_segment)
auth_tag = utils.base64url_decode(auth_tag_segment)
except (TypeError, binascii.Error):
raise JWEParseError("Invalid auth tag")

Expand Down
Loading

0 comments on commit c6c7776

Please sign in to comment.