diff --git a/unshackle/services/Netflix/MSL/__init__.py b/unshackle/services/Netflix/MSL/__init__.py index ccc7704..e76e33b 100644 --- a/unshackle/services/Netflix/MSL/__init__.py +++ b/unshackle/services/Netflix/MSL/__init__.py @@ -10,6 +10,7 @@ import time import zlib from datetime import datetime from io import BytesIO +from typing import Optional, Any import jsonpickle import requests @@ -26,7 +27,8 @@ from .schemes import EntityAuthenticationSchemes # noqa: F401 from .schemes import KeyExchangeSchemes from .schemes.EntityAuthentication import EntityAuthentication from .schemes.KeyExchangeRequest import KeyExchangeRequest -# from vinetrimmer.utils.widevine.device import RemoteDevice +from pywidevine.cdm import Cdm +from pywidevine.pssh import PSSH class MSL: log = logging.getLogger("MSL") @@ -40,7 +42,7 @@ class MSL: self.message_id = message_id @classmethod - def handshake(cls, scheme: KeyExchangeSchemes, session: requests.Session, endpoint: str, sender: str, cache: Cacher): + def handshake(cls, scheme: KeyExchangeSchemes, session: requests.Session, endpoint: str, sender: str, cache: Cacher, cdm: Optional[Cdm] = None, config: Any = None): cache = cache.get(sender) message_id = random.randint(0, pow(2, 52)) msl_keys = MSL.load_cache_data(cache) @@ -52,31 +54,33 @@ class MSL: if scheme != KeyExchangeSchemes.Widevine: msl_keys.rsa = RSA.generate(2048) - # if not cdm: - # raise cls.log.exit("- No cached data and no CDM specified") - # if not msl_keys_path: - # raise cls.log.exit("- No cached data and no MSL key path specified") + if scheme == KeyExchangeSchemes.Widevine: + if not cdm: + raise Exception('Key exchange scheme Widevine but CDM instance is None.') + + session_id = cdm.open() + msl_keys.cdm_session = session_id + cdm.set_service_certificate(session_id, config["certificate"]) + challenge = cdm.get_license_challenge( + session_id=session_id, + pssh=PSSH("AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQAAAAAAPSZ0kAAAAAAAAAAA=="), + license_type="OFFLINE", + privacy_mode=True, - # Key Exchange Scheme Widevine currently not implemented - # if scheme == KeyExchangeSchemes.Widevine: - # msl_keys.cdm_session = cdm.open( - # pssh=b"\x0A\x7A\x00\x6C\x38\x2B", - # raw=True, - # offline=True - # ) - # keyrequestdata = KeyExchangeRequest.Widevine( - # keyrequest=cdm.get_license_challenge(msl_keys.cdm_session) - # ) - # else: - keyrequestdata = KeyExchangeRequest.AsymmetricWrapped( - keypairid="superKeyPair", - mechanism="JWK_RSA", - publickey=msl_keys.rsa.publickey().exportKey(format="DER") - ) + ) + keyrequestdata = KeyExchangeRequest.Widevine(challenge) + entityauthdata = EntityAuthentication.Widevine("TV", base64.b64encode(challenge).decode()) + else: + entityauthdata = EntityAuthentication.Unauthenticated(sender) + keyrequestdata = KeyExchangeRequest.AsymmetricWrapped( + keypairid="superKeyPair", + mechanism="JWK_RSA", + publickey=msl_keys.rsa.publickey().exportKey(format="DER") + ) data = jsonpickle.encode({ - "entityauthdata": EntityAuthentication.Unauthenticated(sender), + "entityauthdata": entityauthdata, "headerdata": base64.b64encode(MSL.generate_msg_header( message_id=message_id, sender=sender, @@ -101,11 +105,11 @@ class MSL: data=data ) except requests.HTTPError as e: - raise cls.log.exit(f"- Key exchange failed, response data is unexpected: {e.response.text}") + raise Exception(f"- Key exchange failed, response data is unexpected: {e.response.text}") key_exchange = r.json() # expecting no payloads, so this is fine if "errordata" in key_exchange: - raise cls.log.exit("- Key exchange failed: " + json.loads(base64.b64decode( + raise Exception("- Key exchange failed: " + json.loads(base64.b64decode( key_exchange["errordata"] ).decode())["errormsg"]) @@ -115,7 +119,7 @@ class MSL: ).decode("utf-8"))["keyresponsedata"] if key_response_data["scheme"] != str(scheme): - raise cls.log.exit("- Key exchange scheme mismatch occurred") + raise Exception("- Key exchange scheme mismatch occurred") key_data = key_response_data["keydata"] # if scheme == KeyExchangeSchemes.Widevine: diff --git a/unshackle/services/Netflix/__init__.py b/unshackle/services/Netflix/__init__.py index c806141..970cdeb 100644 --- a/unshackle/services/Netflix/__init__.py +++ b/unshackle/services/Netflix/__init__.py @@ -104,6 +104,10 @@ class Netflix(Service): self.subs_only = ctx.parent.params.get("subs_only") self.chapters_only = ctx.parent.params.get("chapters_only") + # Inherited from unshackle + self.cdm: Cdm = ctx.obj.cdm + # self.ctx = ctx + def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None: # Configure first before download @@ -382,6 +386,9 @@ class Netflix(Service): self.profiles = self.get_profiles() self.log.info("Intializing a MSL client") self.get_esn() + # if self.cdm.security_level == 1: + # scheme = KeyExchangeSchemes.Widevine + # else: scheme = KeyExchangeSchemes.AsymmetricWrapped self.log.info(f"Scheme: {scheme}") @@ -391,7 +398,9 @@ class Netflix(Service): session=self.session, endpoint=self.config["endpoints"]["manifest"], sender=self.esn.data, - cache=self.cache.get("MSL") + cache=self.cache.get("MSL"), + cdm=self.cdm, + config=self.config, ) cookie = self.session.cookies.get_dict() self.userauthdata = UserAuthentication.NetflixIDCookies( @@ -425,15 +434,20 @@ class Netflix(Service): return result_profiles def get_esn(self): - ESN_GEN = "".join(random.choice("0123456789ABCDEF") for _ in range(30)) - esn_value = f"NFCDIE-03-{ESN_GEN}" - # Check if ESN is expired or doesn't exist - if self.esn.data is None or self.esn.data == {} or (hasattr(self.esn, 'expired') and self.esn.expired): - # Set new ESN with 6-hour expiration - self.esn.set(esn_value, 1 * 60 * 60) # 6 hours in seconds - self.log.info(f"Generated new ESN with 1-hour expiration") + self.log.info(f"Security level: {self.cdm.security_level}") + if int(self.cdm.security_level) == 1: + # Use ESN map from config.yaml instead of generating a new one + self.esn.set(self.config["esn_map"][self.cdm.system_id]) else: - self.log.info(f"Using cached ESN.") + ESN_GEN = "".join(random.choice("0123456789ABCDEF") for _ in range(30)) + esn_value = f"NFCDIE-03-{ESN_GEN}" + # Check if ESN is expired or doesn't exist + if self.esn.data is None or self.esn.data == {} or (hasattr(self.esn, 'expired') and self.esn.expired): + # Set new ESN with 6-hour expiration + self.esn.set(esn_value, 1 * 60 * 60) # 6 hours in seconds + self.log.info(f"Generated new ESN with 1-hour expiration") + else: + self.log.info(f"Using cached ESN.") self.log.info(f"ESN: {self.esn.data}") @@ -516,6 +530,9 @@ class Netflix(Service): "reqPriority": 10, "reqName": "manifest", } + # session_id = self.cdm.open() + # self.cdm.set_service_certificate(session_id, self.config["certificate"]) + # challenge = self.cdm.get_license_challenge(session_id, PSSH("AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQAAAAAAPSZ0kAAAAAAAAAAA==")) _, payload_chunks = self.msl.send_message( endpoint=self.config["endpoints"]["manifest"], params=params, @@ -529,7 +546,9 @@ class Netflix(Service): "params": { "clientVersion": "6.0051.090.911", "challenge": self.config["payload_challenge_pr"] if self.drm_system == 'playready' else self.config["payload_challenge"], + # "challenge": base64.b64encode(challenge).decode(), "challanges": { + # "default": base64.b64encode(challenge).decode() "default": self.config["payload_challenge_pr"] if self.drm_system == 'playready' else self.config["payload_challenge"] }, "contentPlaygraph": ["v2"], @@ -588,6 +607,7 @@ class Netflix(Service): }, userauthdata=self.userauthdata ) + # self.cdm.close(session_id) if "errorDetails" in payload_chunks: self.log.error(f"Manifest call failed for title_id: {title_id}, required_audio_track_id: {required_audio_track_id}, required_text_track_id: {required_text_track_id}, error: {payload_chunks['errorDetails']}") return self._get_empty_manifest() diff --git a/unshackle/services/Netflix/config.yaml b/unshackle/services/Netflix/config.yaml index b988fe1..0899c89 100644 --- a/unshackle/services/Netflix/config.yaml +++ b/unshackle/services/Netflix/config.yaml @@ -14,6 +14,7 @@ payload_challenge_pr: "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c29hc esn_map: # key map of CDM WVD `SystemID = 'ESN you want to use for that CDM WVD'` 8159: "NFANDROID1-PRV-P-GOOGLEPIXEL" + 8131: "HISETVK84500000000000000000000000007401422" endpoints: website: "https://www.netflix.com/nq/website/memberapi/{build_id}/pathEvaluator" manifest: "https://www.netflix.com/msl/playapi/cadmium/licensedmanifest/1"