refactor(msl): improve key exchange handling and code cleanup

- Replace commented Widevine key exchange code with active parsing and key extraction
- Add checks for CDM session and CDM availability before license parsing
- Update key permission strings to lowercase and align with key extraction logic
- Handle AsymmetricWrapped scheme separately with RSA decryption of keys
- Change EntityAuthentication to Unauthenticated for certain challenge cases
- Remove redundant jsonpickle encoding/decoding for cached keys, store raw data instead
- Add detailed logging for key UUIDs and extracted Widevine keys
- Fix key comparison by converting kid bytes to UUID on comparison
- Raise Exception instead of using logger exit on MSL response error
- Update imports to consolidate pywidevine package classes used
This commit is contained in:
2025-08-29 20:52:30 +07:00
parent fcd1ebcf83
commit 3c24d83293

View File

@@ -27,8 +27,7 @@ from .schemes import EntityAuthenticationSchemes # noqa: F401
from .schemes import KeyExchangeSchemes from .schemes import KeyExchangeSchemes
from .schemes.EntityAuthentication import EntityAuthentication from .schemes.EntityAuthentication import EntityAuthentication
from .schemes.KeyExchangeRequest import KeyExchangeRequest from .schemes.KeyExchangeRequest import KeyExchangeRequest
from pywidevine.cdm import Cdm from pywidevine import Cdm, PSSH, Key
from pywidevine.pssh import PSSH
class MSL: class MSL:
log = logging.getLogger("MSL") log = logging.getLogger("MSL")
@@ -70,7 +69,8 @@ class MSL:
) )
keyrequestdata = KeyExchangeRequest.Widevine(challenge) keyrequestdata = KeyExchangeRequest.Widevine(challenge)
entityauthdata = EntityAuthentication.Widevine("TV", base64.b64encode(challenge).decode()) entityauthdata = EntityAuthentication.Unauthenticated(sender)
# entityauthdata = EntityAuthentication.Widevine("TV", base64.b64encode(challenge).decode())
else: else:
entityauthdata = EntityAuthentication.Unauthenticated(sender) entityauthdata = EntityAuthentication.Unauthenticated(sender)
keyrequestdata = KeyExchangeRequest.AsymmetricWrapped( keyrequestdata = KeyExchangeRequest.AsymmetricWrapped(
@@ -122,29 +122,30 @@ class MSL:
raise Exception("- Key exchange scheme mismatch occurred") raise Exception("- Key exchange scheme mismatch occurred")
key_data = key_response_data["keydata"] key_data = key_response_data["keydata"]
# if scheme == KeyExchangeSchemes.Widevine: if scheme == KeyExchangeSchemes.Widevine:
# if isinstance(cdm.device, RemoteDevice): if not msl_keys.cdm_session:
# msl_keys.encryption, msl_keys.sign = cdm.device.exchange( raise Exception("- No CDM session available")
# cdm.sessions[msl_keys.cdm_session], if not cdm:
# license_res=key_data["cdmkeyresponse"], raise Exception("- No CDM available")
# enc_key_id=base64.b64decode(key_data["encryptionkeyid"]), cdm.parse_license(msl_keys.cdm_session, key_data["cdmkeyresponse"])
# hmac_key_id=base64.b64decode(key_data["hmackeyid"]) keys = cdm.get_keys(msl_keys.cdm_session)
# ) cls.log.info(f"Keys: {keys}")
# cdm.parse_license(msl_keys.cdm_session, key_data["cdmkeyresponse"]) encryption_key = MSL.get_widevine_key(
# else: kid=base64.b64decode(key_data["encryptionkeyid"]),
# cdm.parse_license(msl_keys.cdm_session, key_data["cdmkeyresponse"]) keys=keys,
# keys = cdm.get_keys(msl_keys.cdm_session) permissions=["allow_encrypt", "allow_decrypt"]
# msl_keys.encryption = MSL.get_widevine_key( )
# kid=base64.b64decode(key_data["encryptionkeyid"]), msl_keys.encryption = encryption_key
# keys=keys, cls.log.info(f"Encryption key: {encryption_key}")
# permissions=["AllowEncrypt", "AllowDecrypt"] sign = MSL.get_widevine_key(
# ) kid=base64.b64decode(key_data["hmackeyid"]),
# msl_keys.sign = MSL.get_widevine_key( keys=keys,
# kid=base64.b64decode(key_data["hmackeyid"]), permissions=["allow_sign", "allow_signature_verify"]
# keys=keys, )
# permissions=["AllowSign", "AllowSignatureVerify"] cls.log.info(f"Sign key: {sign}")
# ) msl_keys.sign = sign
# else:
elif scheme == KeyExchangeSchemes.AsymmetricWrapped:
cipher_rsa = PKCS1_OAEP.new(msl_keys.rsa) cipher_rsa = PKCS1_OAEP.new(msl_keys.rsa)
msl_keys.encryption = MSL.base64key_decode( msl_keys.encryption = MSL.base64key_decode(
json.JSONDecoder().decode(cipher_rsa.decrypt( json.JSONDecoder().decode(cipher_rsa.decrypt(
@@ -156,6 +157,7 @@ class MSL:
base64.b64decode(key_data["hmackey"]) base64.b64decode(key_data["hmackey"])
).decode("utf-8"))["k"] ).decode("utf-8"))["k"]
) )
msl_keys.mastertoken = key_response_data["mastertoken"] msl_keys.mastertoken = key_response_data["mastertoken"]
MSL.cache_keys(msl_keys, cache) MSL.cache_keys(msl_keys, cache)
@@ -174,7 +176,7 @@ class MSL:
return None return None
# with open(msl_keys_path, encoding="utf-8") as fd: # with open(msl_keys_path, encoding="utf-8") as fd:
# msl_keys = jsonpickle.decode(fd.read()) # msl_keys = jsonpickle.decode(fd.read())
msl_keys = jsonpickle.decode(cacher.data) msl_keys = cacher.data
if msl_keys.rsa: if msl_keys.rsa:
# noinspection PyTypeChecker # noinspection PyTypeChecker
# expects RsaKey, but is a string, this is because jsonpickle can't pickle RsaKey object # expects RsaKey, but is a string, this is because jsonpickle can't pickle RsaKey object
@@ -196,7 +198,7 @@ class MSL:
msl_keys.rsa = msl_keys.rsa.export_key() msl_keys.rsa = msl_keys.rsa.export_key()
# with open(cache, "w", encoding="utf-8") as fd: # with open(cache, "w", encoding="utf-8") as fd:
# fd.write() # fd.write()
cache.set(jsonpickle.encode(msl_keys)) cache.set(msl_keys)
if msl_keys.rsa: if msl_keys.rsa:
# re-import now # re-import now
msl_keys.rsa = RSA.importKey(msl_keys.rsa) msl_keys.rsa = RSA.importKey(msl_keys.rsa)
@@ -241,9 +243,11 @@ class MSL:
return jsonpickle.encode(header_data, unpicklable=False) return jsonpickle.encode(header_data, unpicklable=False)
@classmethod @classmethod
def get_widevine_key(cls, kid, keys, permissions): def get_widevine_key(cls, kid, keys: list[Key], permissions):
cls.log.info(f"KID: {Key.kid_to_uuid(kid)}")
for key in keys: for key in keys:
if key.kid != kid: # cls.log.info(f"KEY: {key.kid_to_uuid}")
if key.kid != Key.kid_to_uuid(kid):
continue continue
if key.type != "OPERATOR_SESSION": if key.type != "OPERATOR_SESSION":
cls.log.warning(f"Widevine Key Exchange: Wrong key type (not operator session) key {key}") cls.log.warning(f"Widevine Key Exchange: Wrong key type (not operator session) key {key}")
@@ -259,7 +263,7 @@ class MSL:
res = self.session.post(url=endpoint, data=message, params=params) res = self.session.post(url=endpoint, data=message, params=params)
header, payload_data = self.parse_message(res.text) header, payload_data = self.parse_message(res.text)
if "errordata" in header: if "errordata" in header:
raise self.log.exit( raise Exception(
"- MSL response message contains an error: {}".format( "- MSL response message contains an error: {}".format(
json.loads(base64.b64decode(header["errordata"].encode("utf-8")).decode("utf-8")) json.loads(base64.b64decode(header["errordata"].encode("utf-8")).decode("utf-8"))
) )