forked from kenzuya/unshackle
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:
@@ -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"))
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user