diff --git a/unshackle/services/Netflix/MSL/__init__.py b/unshackle/services/Netflix/MSL/__init__.py index b549630..3b65d4d 100644 --- a/unshackle/services/Netflix/MSL/__init__.py +++ b/unshackle/services/Netflix/MSL/__init__.py @@ -129,20 +129,20 @@ class MSL: raise Exception("- No CDM available") cdm.parse_license(msl_keys.cdm_session, key_data["cdmkeyresponse"]) keys = cdm.get_keys(msl_keys.cdm_session) - cls.log.info(f"Keys: {keys}") + cls.log.debug(f"Keys: {keys}") encryption_key = MSL.get_widevine_key( kid=base64.b64decode(key_data["encryptionkeyid"]), keys=keys, permissions=["allow_encrypt", "allow_decrypt"] ) msl_keys.encryption = encryption_key - cls.log.info(f"Encryption key: {encryption_key}") + cls.log.debug(f"Encryption key: {encryption_key}") sign = MSL.get_widevine_key( kid=base64.b64decode(key_data["hmackeyid"]), keys=keys, permissions=["allow_sign", "allow_signature_verify"] ) - cls.log.info(f"Sign key: {sign}") + cls.log.debug(f"Sign key: {sign}") msl_keys.sign = sign elif scheme == KeyExchangeSchemes.AsymmetricWrapped: @@ -244,7 +244,7 @@ class MSL: @classmethod def get_widevine_key(cls, kid, keys: list[Key], permissions): - cls.log.info(f"KID: {Key.kid_to_uuid(kid)}") + cls.log.debug(f"KID: {Key.kid_to_uuid(kid)}") for key in keys: # cls.log.info(f"KEY: {key.kid_to_uuid}") if key.kid != Key.kid_to_uuid(kid): @@ -258,10 +258,10 @@ class MSL: return key.key return None - def send_message(self, endpoint, params, application_data, userauthdata=None): + def send_message(self, endpoint, params, application_data, userauthdata=None, headers=None, unwrap_result=True): message = self.create_message(application_data, userauthdata) - res = self.session.post(url=endpoint, data=message, params=params) - header, payload_data = self.parse_message(res.text) + res = self.session.post(url=endpoint, data=message, params=params, headers=headers) + header, payload_data = self.parse_message(res.text, unwrap_result=unwrap_result) if "errordata" in header: raise Exception( "- MSL response message contains an error: {}".format( @@ -302,7 +302,7 @@ class MSL: return message - def decrypt_payload_chunks(self, payload_chunks): + def decrypt_payload_chunks(self, payload_chunks, unwrap_result=True): """ Decrypt and extract data from payload chunks @@ -310,7 +310,6 @@ class MSL: :return: json object """ raw_data = "" - for payload_chunk in payload_chunks: # todo ; verify signature of payload_chunk["signature"] against payload_chunk["payload"] # expecting base64-encoded json string @@ -344,10 +343,12 @@ class MSL: self.log.critical(f"- {error}") raise Exception(f"- MSL response message contains an error: {error}") # sys.exit(1) + self.log.debug(f"Payload Chunks: {data}") + if unwrap_result: + return data["result"] + return data - return data["result"] - - def parse_message(self, message): + def parse_message(self, message, unwrap_result=True): """ Parse an MSL message into a header and list of payload chunks @@ -359,7 +360,7 @@ class MSL: header = parsed_message[0] encrypted_payload_chunks = parsed_message[1:] if len(parsed_message) > 1 else [] if encrypted_payload_chunks: - payload_chunks = self.decrypt_payload_chunks(encrypted_payload_chunks) + payload_chunks = self.decrypt_payload_chunks(encrypted_payload_chunks, unwrap_result=unwrap_result) else: payload_chunks = {} diff --git a/unshackle/services/Netflix/MSL/schemes/UserAuthentication.py b/unshackle/services/Netflix/MSL/schemes/UserAuthentication.py index 15dabb2..83f1a64 100644 --- a/unshackle/services/Netflix/MSL/schemes/UserAuthentication.py +++ b/unshackle/services/Netflix/MSL/schemes/UserAuthentication.py @@ -31,6 +31,19 @@ class UserAuthentication(MSLObject): } ) + @classmethod + def UserIDToken(cls, token_data, signature, master_token): + return cls( + scheme=UserAuthenticationSchemes.UserIDToken, + authdata={ + "useridtoken": { + "tokendata": token_data, + "signature": signature + }, + "mastertoken": master_token + } + ) + @classmethod def NetflixIDCookies(cls, netflixid, securenetflixid): """ diff --git a/unshackle/services/Netflix/MSL/schemes/__init__.py b/unshackle/services/Netflix/MSL/schemes/__init__.py index a1f61d7..08b4f97 100644 --- a/unshackle/services/Netflix/MSL/schemes/__init__.py +++ b/unshackle/services/Netflix/MSL/schemes/__init__.py @@ -15,6 +15,7 @@ class EntityAuthenticationSchemes(Scheme): class UserAuthenticationSchemes(Scheme): """https://github.com/Netflix/msl/wiki/User-Authentication-%28Configuration%29""" EmailPassword = "EMAIL_PASSWORD" + UserIDToken = "USER_ID_TOKEN" NetflixIDCookies = "NETFLIXID" diff --git a/unshackle/services/Netflix/__init__.py b/unshackle/services/Netflix/__init__.py index a4dc436..81a7420 100644 --- a/unshackle/services/Netflix/__init__.py +++ b/unshackle/services/Netflix/__init__.py @@ -20,6 +20,8 @@ from pymp4.parser import Box from pywidevine import PSSH, Cdm as WidevineCDM, DeviceTypes from pyplayready import PSSH as PlayReadyPSSH import requests +from Cryptodome.Cipher import AES +from Cryptodome.Util.Padding import unpad from langcodes import Language from unshackle.core.constants import AnyTrack @@ -60,6 +62,7 @@ class Netflix(Service): "es": "es-419", "pt": "pt-PT", } + ANDROID_CONFIG_ENDPOINT = "https://android.prod.ftl.netflix.com/nq/androidui/samurai/v1/config" @staticmethod @click.command(name="Netflix", short_help="https://netflix.com") @@ -481,14 +484,166 @@ class Netflix(Service): securenetflixid=cookie["SecureNetflixId"] ) else: - # Android like way login to Netflix using email and password if not self.credential: - raise Exception(" - Credentials are required for Android CDMs, and none were provided.") - self.userauthdata = UserAuthentication.EmailPassword( - email=self.credential.username, - password=self.credential.password + raise click.ClickException("Android sign-in requires credentials.") + self.userauthdata = self.get_android_userauthdata() + + def get_android_userauthdata(self) -> UserAuthentication: + token_cache = self.get_android_user_token_cache() + token_data = token_cache.data if token_cache and isinstance(token_cache.data, dict) else None + + if not token_data or not token_data.get("tokendata") or not token_data.get("signature"): + self.log.info("Requesting Android useridtoken") + token_data = self.fetch_android_user_id_token() + token_cache.set(token_data, expiration=self.resolve_android_token_expiration(token_data)) + else: + self.log.info("Using cached Android useridtoken") + + return UserAuthentication.UserIDToken( + token_data=token_data["tokendata"], + signature=token_data["signature"], + master_token=self.msl.keys.mastertoken + ) + + def get_android_user_token_cache(self): + return self.cache.get(f"ANDROID_USER_ID_TOKEN/{self.credential.sha1}/{self.esn.data['esn']}") + + def fetch_android_user_id_token(self) -> dict: + try: + header, payload_data = self.msl.send_message( + endpoint=self.ANDROID_CONFIG_ENDPOINT, + params=self.build_android_sign_in_query(), + application_data="", + headers=self.build_android_sign_in_headers(), + unwrap_result=False ) - self.log.info(f"userauthdata: {self.userauthdata}") + except Exception as exc: + raise click.ClickException(f"Android sign-in request failed: {exc}") from exc + header_data = self.decrypt_android_header(header["headerdata"]) + tokens = header_data.get("useridtoken") + if not tokens: + self.log.debug(f"Android sign-in header keys: {list(header_data.keys())}") + sign_in_value = self.extract_android_sign_in_value(payload_data) + error_code = self.extract_android_sign_in_error_code(sign_in_value) + if error_code: + raise click.ClickException(f"Android sign-in failed: {error_code}") + raise click.ClickException("Android sign-in did not return a useridtoken.") + return tokens + + @staticmethod + def extract_android_sign_in_value(payload_data: dict) -> Optional[dict]: + if not isinstance(payload_data, dict): + return None + json_graph = payload_data.get("jsonGraph") + if not isinstance(json_graph, dict): + return None + sign_in_verify = json_graph.get("signInVerify") + if not isinstance(sign_in_verify, dict): + return None + value = sign_in_verify.get("value") + return value if isinstance(value, dict) else None + + @staticmethod + def extract_android_sign_in_error_code(sign_in_value: Optional[dict]) -> Optional[str]: + if not isinstance(sign_in_value, dict): + return None + fields = sign_in_value.get("fields") + if not isinstance(fields, dict): + return None + error_code = fields.get("errorCode") + if not isinstance(error_code, dict): + return None + value = error_code.get("value") + return value if isinstance(value, str) and value else None + + def build_android_sign_in_query(self) -> dict: + cookie = self.session.cookies.get_dict() + return { + "api": "33", + "appType": "samurai", + "appVer": "62902", + "appVersion": "9.18.0", + "chipset": "sm6150", + "chipsetHardware": "qcom", + "clientAppState": "FOREGROUND", + "clientAppVersionState": "NORMAL", + "countryCode": "+385", + "countryIsoCode": "HR", + "ctgr": "phone", + "dbg": "false", + "deviceLocale": "hr", + "devmod": "samsung_SM-A705FN", + "ffbc": "phone", + "flwssn": "c3100219-d002-40c5-80a7-055c00407246", + "installType": "regular", + "isAutomation": "false", + "isConsumptionOnly": "true", + "isNetflixPreloaded": "false", + "isPlayBillingEnabled": "true", + "isStubInSystemPartition": "false", + "lackLocale": "false", + "landingOrigin": "https://www.netflix.com", + "mId": "SAMSUSM-A705FNS", + "memLevel": "HIGH", + "method": "get", + "mnf": "samsung", + "model": "SM-A705FN", + "netflixClientPlatform": "androidNative", + "netflixId": cookie["NetflixId"], + "networkType": "wifi", + "osBoard": "sm6150", + "osDevice": "a70q", + "osDisplay": "TQ1A.230205.002", + "password": self.credential.password, + "path": "[\"signInVerify\"]", + "pathFormat": "hierarchical", + "platform": "android", + "preloadSignupRoValue": "", + "progressive": "false", + "qlty": "hd", + "recaptchaResponseTime": 244, + "recaptchaResponseToken": "", + "responseFormat": "json", + "roBspVer": "Q6150-17263-1", + "secureNetflixId": cookie["SecureNetflixId"], + "sid": "7176", + "store": "google", + "userLoginId": self.credential.username + } + + def build_android_sign_in_headers(self) -> dict: + return { + "X-Netflix.Request.NqTracking": "VerifyLoginMslRequest", + "X-Netflix.Client.Request.Name": "VerifyLoginMslRequest", + "X-Netflix.Request.Client.Context": "{\"appState\":\"foreground\"}", + "X-Netflix-Esn": self.esn.data["esn"], + "X-Netflix.EsnPrefix": "NFANDROID1-PRV-P-", + "X-Netflix.msl-header-friendly-client": "true", + "content-encoding": "msl_v1" + } + + def decrypt_android_header(self, encrypted_header_b64: str) -> dict: + encrypted_header = json.loads(base64.b64decode(encrypted_header_b64)) + iv = base64.b64decode(encrypted_header["iv"]) + ciphertext = base64.b64decode(encrypted_header["ciphertext"]) + cipher = AES.new(self.msl.keys.encryption, AES.MODE_CBC, iv) + decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size) + return json.loads(decrypted.decode("utf-8")) + + def resolve_android_token_expiration(self, token_data: dict): + for source in (token_data, self.msl.keys.mastertoken): + if not isinstance(source, dict): + continue + tokendata = source.get("tokendata") + if not tokendata: + continue + try: + parsed = json.loads(base64.b64decode(tokendata).decode("utf-8")) + except (TypeError, ValueError, json.JSONDecodeError): + continue + if parsed.get("expiration"): + return parsed["expiration"] + return None def get_profiles(self): diff --git a/unshackle/services/Netflix/config.yaml b/unshackle/services/Netflix/config.yaml index d904457..a872f28 100644 --- a/unshackle/services/Netflix/config.yaml +++ b/unshackle/services/Netflix/config.yaml @@ -11,12 +11,18 @@ certificate: | payload_challenge: "CAESyiASLAoqChQIARIQAAAAAAPSZ0kAAAAAAAAAABABGhBIEaExMI/bXBDHG1CO/P94GAEg+aSPzQYwFjiU5vKVDUL8HwoQdGVzdC5uZXRmbGl4LmNvbRIQ5US6QAvBDzfTtjb4tU/7QxrAHef/hDMBvFq8LEHp2c4f5Ahc8pRjEgJMHTrwH2GDsAeuctYG+V0cjK0bvwWxgAU2XQjSyfehPSdySufFVHaYcntw6U55JNwZ0g5SikEwIiLzdSSHHW1+h6n/xpG6GWFFEFmgJ/vnIKAOr7PuJOkYXHrBiWzpq0ckJpxTwoUEZv3+qiLqZ8BkVazfOxDVnT6AYmwVIEgtkQWFFWZphWJduPmKF6D0RydIx1LOHpwsHGsx9zkbSOXrLzGTUX6+8EB1CrFOhZnYkPiDxdiKqbstDD4v4+DWBw6eRTHpuyn0ECvzVYQoKYsrUGFa7CetvufSQV0pMKqlBECWGwO1d54k+tIrptarE0ADM5E57dwD/0Z2ts+mWFoOqOJgKEaY6Mi6uj+Kyj08DrEgUezkx3ooz2Dl0/BKY5BIgcSHUiqzLJ09cYjFTg9U9cqcE3cNtgy9fL7NFEg1bhu6Pggs8eZehH0uOj1iFNMXxDQYKSghkXfCY0NGqO38AEFT6qAjzagI9DFlMsRPeD7++Sky9wBoXNq5eoNjdOx3y2OAp2y/T57GQvcFe7MVPZV+1ZlCRNlKP+H2CU50UZIYhWUFkVvEyFIpMFWJPpAvHzybfRrNjAfkok3fMyd+0dQm/llsoy0Rzgo7/MG5VSfu2c5qTR+oC7GeddcmLBgB/8xzu+yiZk/4ZJ62z9JtKk6dc41kRnOlt4xce3zE6ebAMMRUIxaCtKUrnRETq6dwngiD8AGSU6FBKcomP9r3Swyd6tHmUx9nFe1Xt7HwS6vxuEtU1HIs64TZdbxUqcPs1Hlm6xH8WlkDa5s5PK0LWxnJ0ScYbzTTr0UJ1JmC37tid5mOZHYKLS3hcNkdzeeU5R7rqAxOr7akiFLv1bO0jYypfnElfZtTzlsWs765Y9aho/CBPQZ1lu/MS4ZkK4hOKLM0US9kG4F1oMNTeAOP7gkzjdqltsumvhnaOJhjOxgNprBXGQBsG1olzer8AtfMrpEOLKJ9lYPDSIuVrv46rgXNUFzwcyOs2dQ7zVNF06pumSj2nxYgk3ZRZXSAhcRKgZA7FiE5pjdRtWncWerv6EyflEEkfnrCUADiiVtT25ZFvFZ0pQIkLejGRpdAJQ4pQWuaUZuhFZ+JXI8DljeQ8eVD+JE+SRbRkSD8dtNzTYMnzB/skoMDwi8AsD3+ZyyS7J0DWbehSUrHeHlcI5MxNucWS7vt9NSYUITjbgpv+t+PL30tzGG/SyIoKwK2IZJVoExSuvq7C7eyLX42fN6J6ZDeSs8I8JSEzON5GA1EwEET5HfqdqiIGLOaSWjps3FVRLg4B13XanvEmJ1UNmM/jW0/hPhEAL0DShLH9XxCpEmQh7jjWaICkUIXP0ufSQHLtXIWqa1sVQgrnrXtNrbgeHv5afBakL8NYxrjkezUCT3CNp2qterOP9PjGe7DQnJMiC7/xF97ybiBS/+LmGSn2mBuScT/OEGa4QkdG9WEqH/XRjlMSwEsVGIm5Dhawcy2+flgoFrIIjnpPjpPWOk4yTgqOiv6oL9G3THAHGD4GOyXfFjS660I7bSj7AtDo4Ds98k9gWQIlndjPXLTGwQ5iQtl9xtZPGOx2SHeAJ9RU5AVzRIgHNVOsbx/Pu2+c1vWu+9Q+5n557SEV4ITKsH4VCrs/cHqDz3ajcnYMOu3zy3nyxz22fS6PhljrjlAAIWbxCAURv5D8sPUz2r3uaS2GIryAAr7dIYHJG6QOYI9AwzUbKURqvX9Dl+bd/6a+V2yan7WSNFZJNJ7TS2W02lZjQJnUE8YsY2o2GYAwJKE8sTKasbp8+hAOaVKEgGI1DHv1hr/zhdWgQrGIF9eV8hNjzAcpD1j/FW/lkNbyx+89NfsRtGalbiU4ZRh4t7cXOo3FAh1Eao1YdelxqacKecL845yJN/L/F5YEg8laX8fWQ60hO/Ih2xevJhFPu2jzvQX9h5dOQPCybPPkJ/quwYRst82m7s8eOIL0w8nyeucF0T8qRV83GIO63pG7jDZCR4xTGoU+JdP6DB1NIhQy6p0ZesPT2oJYGwFpFG+J7r93B8jgwW7L9qhNQO8YdvEs9fXU7e+hY066ba9lGePIctAYwSRC//4QvnfHu7F8kX028T4rhndw0BJs6mARKKahvkRAo6GQnIrzZoBGh+TGyRYF/NldR8dHxV5djvtAvo1imVYlEaEsbB8mHQFGnjjIKBgA715I3iMLoaYBGlQauqQ2vTDko14mCEVnMZNpPhDnrEKM1ybhUAw3wRDDo1b79wwkgumP6qRcDJD9ldOpifUQ/Tql4zQrs0BYY895mXkp71i3J0qxexHQi37SN2dkUbCprxJIslB9HxxXDFJAsMK3STT8gK9Nu/Kl2ahYan0zaaiOgTOkqa3wID06pTxcL95a5Kkuc2q2MPXgvAOJEE2ixmYC8xgnB/8ajf+eX9UQRLgJmb34xjK07Ck/0Mri1ILTP/FRFRdecJ69xOF3s6uDN6PGCBDKgrnWDAU/wmJ6fyUkDd3GG9368qx77SYhDEgSmdWQuSMdgngLdw5/+COF6IrdI2zb/sP/gX32SmZSEfVgmZfPpmLGKW3+KmRbXtptQcdec/z8QmgWzVJ3Djt0NZ/EWpHRyKUfJhboOZVcF46iCA15i8ClnIW8fZuAjUDvghKV1O1bCpyZ/6sKWrGLK3HJAtZ5lE5jrqkeWteyvb1j/HEAMpQqkL5uCJF+8ZMDwsdxqrLYF4W+LHN/5CMrp0IvW4NopMY/9oRzQatA9dARRBJrXoESe0ma+2oEI2cOBp1maLxEQHlu8IYFZDsY4F0iGENYOq/9GaEkLeUrRn/ntWGU6G1Nv52stlW9y6WGsuDxLKjCFxWTYj+rHo5OVp9lsvg973Tk89h+eV6lVTdKkHfJK9l2m8t7Zt2e8kvDzgdUJE7TDKGbRjOMyniPZVZTn79C9RpIGVchHKnGk7tnvlcMpk3k1rJH1TAQJTDitmpOBuvwoftEmETcUWgDhThuNglsaYdrSZQWtZ7nE2A/YYXN06z5CihCQajWS+qs6HpYNqLpZddyxwkCLjkkGqHGbjjZHB2dYqoBWDU3Sp45gSxljDbEi4StZ/SBW6SazAWbCdJHwG7TIgb3B9PJ6M9xmdASCXdin+2lpyobKi0xc11FaAl04DJ1fDVkJ4kbj2AZ+JDn91yDEE2O/Gh5nXPqmpeaIuiVTMzvii7DwfBk9NlOy0OmMM9Zjf2K/e44Mt9Q62qix9GrSawDDu0u4EV3mJqyRTnmWx6ChAbpMYoKsbDKVrcTcrJf2wyjynzo064ZDSRaTRELDeL1zZjWGfa+oOYkEY3n9dUkrivU9HHrcf48zTCNeoCukKJ7n/yrGav6VcDCzzdOcPeksVJroaLy93anIcxI/zkBuRQ51yCtbbUCOs4Gp9QVOkKc5/qJ8GHQGboSrYpq7Ks3Jl4GsoMny16zuOQ9d+ABOU9yu70VyxT2uFmQKlk7w+2Mji+eBLwK8ao2RQP01t9PTIRIkxlsgw4z2pdX4oflmMQFl5JQaBP6PTQVlMfpu2LkO7IPTGRVshDEMEtXVrP/xMOfLFLcWtJzr2GxUmji1aSVUUeNK+BbNy4Jgl3RoDr6scnTsaZhzMwMmuteLAKtByp3xSHJz5DRzlokFKrvaYqx/ANIe4rdDeV8Pp0RqzXMB/mbApwGBnaZDBP6loCT/B/AFrMX9K3OkwO68yxtavBNsmHg7Jel0GPrtLb0MSAfAChAx5zAAR4/yazz1CaZLsBsAAveilVZJi84xbNLW5FfjneBSse7UsFu1K9I4dcuy7EVY5gMLc2r+kdt7BzJ7dSx8677iyc48BEslvifqLZb9avOrJQXcKmRRLVXLa1EnTOkvvhBkWUVdGcYDqDQtC231Wo5TXQ4oBRKJQVOH7DCZYHpxV0kuf2v8SQ0nX/ORTKTnvitpa580jzhHSJIJDhtQb+DZ+u8239U3yGli2YwWr1CSKDRji88jg2OuTzlstPnHDveKj00aceEm4Efs/DlOQqrrrTvKR7XcGcc42c8EEcolzyM5Rc9zij4HMAfRmc+uGzmNywX5++JBxi1IlMGVJpr2X3LVvdS4SW28AWm07fqyZ9jWz3lYx30f0jVzyPJXaM9EDaag4/fExIceiW8xUO3vmFsZYyRgyP2FC0Wp+GZcsLzyTwKnXk4UnTtttQu94ZCqPQiv4mjYtyKEjPO1pTA2TKWGx81Azh0A3JCEgeP9il1ZzB97RVMtdrXIxwi7ynjK7Y4kinOml2u3TkBrgtdl8Sdtmi1qU07QV06X8JugDAY6+nu9uK0VPLn8KKsBtJIN3t9fasnbpnXU0QXgJ4gf4qx3J4g+pCWBnzliJfzIwfZS1QZHh7FhmMKs7BzJ96tLH0R92sfjB8b328Nm9zTVdLZiObZn4P1bLFQ2p2tEiizWzJReRQ8gxkGmShHlO6wbXO9VyK7lNEwR5EyljLaFFsJrsog0hUS/8sx1oraEQQe0LmD0gkUqu+jtTnhZGjoxaT6hqz76mnI2pLNKFEOz20eaeYevescEIHF3+n4jX/F7G6pTS05jBjCYeQ0ByL2g826QIXr3oX611QT3GRektZ8kgQC5w4J7Sr3QGk5ADFDTJPxvQay/Mk0gkpDA5NSOQ4p0bZZDL/D6FW9DLspysB3LNiwAwrwETaIGYQnqvexEXwDdYbZ1cHaTmR1C2wvzcFL/4hDj82vQTmap4+JvGWT09T4ZJZ1MiRT9IAg+sA0NbxMlsuXlmXWbdrwZawsObzGadwLx9Nbpx7/u82nL0OxaOy5UN2JTLjWukwqVZnGzBPSDSYAo6EeQd2GebKJM4ACeqJFkqk1fZvkU9v4JWmSf5//OowhES+VqTW23EIBSf7hzhkaB9TSvXR2dp/2dBqxCdWcstfhBW/6/ZSf/vTLP00ct9qrrD5cK6LEg6F2fyy53g37kcN3LxiprUVxx8CEGunnSx94GTuABXT5RuYWFjRvb4eN5RYp94SiYF+lAOWxvqhOMr6ufQkVrpqZNNkSl8YIhAiHiW6JTfU1OsPuFDBHy+zKoACIQzJuw8R4Q1EXZeCner8WNQXXpj8Vgl7S0S0cKc63NpoLCtWCHiJCQmGCYoZscK+KtFQdO4BBlfx1od06TRyA12nmupXOp5k91KcjOe3950HcCoHIJSeiFcBkjTnhlX9WEwq2KCiC5F1sjAVaf8KBkgNn77twLMp7B6Azm0FiVb7bRIVsb8I6P+Tua0YalSBHn+tWX7DQBmiDb2Qz9xBG/Ez33QaoFGNLZ9oZNvoI5C87Obr9xc6oNElSze3XHIMUlCA1yOaYPspjxGXYIeMZMH5LYbnpu9UsesQcICInN19qEcIUo4v7Zl6rd/hQIt2g/9yNX9vT7Y4o73+LrYN5EoLNC4xMC4yOTM0LjAagAEZ2In1JM+3QTtkNZOljQMMZvNvR3GXrLrsbwlQ/wSfyIjapcXOFutrD+wgCA5N5B4QcuMuLYhi+Oc7uzXc4fTUUGEM3y3wV9zqsVJKcaUGXFWy8e3qNQQnrd9WdwlJRCr2mQ/0TpNKsOF3on31907OW5HqHGnMrKUy83ZbtyeFRUoUAAAAAQAAABQABQAQ0ryzFMylJVo=" payload_challenge_pr: "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c29hcDpFbnZlbG9wZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4c2Q9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczpzb2FwPSJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy9zb2FwL2VudmVsb3BlLyI+PHNvYXA6Qm9keT48QWNxdWlyZUxpY2Vuc2UgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vRFJNLzIwMDcvMDMvcHJvdG9jb2xzIj48Y2hhbGxlbmdlPjxDaGFsbGVuZ2UgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vRFJNLzIwMDcvMDMvcHJvdG9jb2xzL21lc3NhZ2VzIj48TEEgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vRFJNLzIwMDcvMDMvcHJvdG9jb2xzIiBJZD0iU2lnbmVkRGF0YSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PFZlcnNpb24+NDwvVmVyc2lvbj48Q29udGVudEhlYWRlcj48V1JNSEVBREVSIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL0RSTS8yMDA3LzAzL1BsYXlSZWFkeUhlYWRlciIgdmVyc2lvbj0iNC4yLjAuMCI+PERBVEE+PFBST1RFQ1RJTkZPPjxLSURTPjxLSUQgQUxHSUQ9IkFFU0NUUiIgVkFMVUU9IkFBQUFBTVlFeElFQUFBQUFBQUFBQUE9PSI+PC9LSUQ+PC9LSURTPjwvUFJPVEVDVElORk8+PExBX1VSTD5odHRwOi8vY2FwcHJzdnIwNi9zaWx2ZXJsaWdodDUvcmlnaHRzbWFuYWdlci5hc214PC9MQV9VUkw+PExVSV9VUkw+aHR0cDovL2NhcHByc3ZyMDYvc2lsdmVybGlnaHQ1L3JpZ2h0c21hbmFnZXIuYXNteDwvTFVJX1VSTD48REVDUllQVE9SU0VUVVA+T05ERU1BTkQ8L0RFQ1JZUFRPUlNFVFVQPjwvREFUQT48L1dSTUhFQURFUj48L0NvbnRlbnRIZWFkZXI+PENMSUVOVElORk8+PENMSUVOVFZFUlNJT04+MTAuMC4xNjM4NC4xMDAxMTwvQ0xJRU5UVkVSU0lPTj48L0NMSUVOVElORk8+PExpY2Vuc2VOb25jZT4yd3BIdnhydHZHQVdNSGYvVUhHaGRnPT08L0xpY2Vuc2VOb25jZT48Q2xpZW50VGltZT4xNzQyOTI3OTIxPC9DbGllbnRUaW1lPjxFbmNyeXB0ZWREYXRhIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjxFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMTI4LWNiYyI+PC9FbmNyeXB0aW9uTWV0aG9kPjxLZXlJbmZvIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48RW5jcnlwdGVkS2V5IHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9EUk0vMjAwNy8wMy9wcm90b2NvbHMjZWNjMjU2Ij48L0VuY3J5cHRpb25NZXRob2Q+PEtleUluZm8geG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxLZXlOYW1lPldNUk1TZXJ2ZXI8L0tleU5hbWU+PC9LZXlJbmZvPjxDaXBoZXJEYXRhPjxDaXBoZXJWYWx1ZT5ZbEVYZGdWVXhsa2J3aWdFR3dESUJCRldpQnliU2dJRG96WStHOXgzMXhGR3NubnlLSjlHQlVkc0hrenZITGlZRnBseTI1VUpydlJVVDd0T3ZlOTZIcmlvY0tnK2hIZnAxV3RPRmhFakkyZFhweWs0amZjS21DRmlRN3cvTHpDTlBUVkhBV0liNFl2ZzNsbXFnNlM1R1dYSWgwOERaVndtN1NWNmQ3R05iTFE9PC9DaXBoZXJWYWx1ZT48L0NpcGhlckRhdGE+PC9FbmNyeXB0ZWRLZXk+PC9LZXlJbmZvPjxDaXBoZXJEYXRhPjxDaXBoZXJWYWx1ZT5PeVEyVkl5UEI0aEFwWDZ6dE9Gcmg3eDB0ZXI4ZDBXbk9rQnB6QWQ5Lzg1VzR0SUhpSkQybnZSNlNqYVZUNUhxZUkwYzFEK09QZ2dKa2VQYlpqV3ZuVmp0NkV2MVJ5SDQ4UWp5NFZZUnNTblhvbkJDdzdGVzFyTFhnMXd6QnArZ2pzTmZDcHZqUCtFT0pvejRFR24wd1VNQytmL1Z4OUt1VlBBNGZOTDFuMmFIaGp4QmhkeG4zWG1ZL05pamZXREphdVdrOEJZZ0lnNlczL2krd0dIdWFVbUxDeXBqcSszeVdwQ2JyN1VsTnFmVUdaRkp3UFhENi82bVVlKy82ZWJ1VlZFc29rcWxYakpqdWtCUE9rcTNKT1NnL0Z3MlUya3dMalhwNUtlNHhlTkF5UE1XSjVYdndZL0hxQi91c2FUSGZUTTh2U2ZINU9yRWRNeHZEMFM4ZWV2Ti9HVVd2N25GaVNlOXZXQVRkSjMrWTY4N1kzWDYrQkVxWWZ4UmVORnJoZFh6Mit5d1dnVStWMVJyanVpZ01QUERsRDVKQ1VQRnQrYnFOejI0OVVuMWJJcG4vOThDRFk5WTJWUTB5aUdPdVg4M2ZuRDdJcE0rVnNKbjZhRUtwbGxYTlpQSEFzM1ptZXJkaXNNb2F4RC9UZFpBUEo4SzVudStTS1dIalJPL1ZUTzRHNXVaTGM3SENHVE9ybXpUWFhiZE84QlRiWUcwRVVibnNmUVVPQkpiMElYcjA5UXlLZ3RKQjlqcWhvN3lXcEUyZWdHMXllSmV1QldSQzQ5Y1NrWHJGOVJrUldZZ3JCMmtGekd1eXo5d0ZUZjdMQ3F0YVgzcXRXbDlCMTJPSStpZy9NbGNMeGx2VTY2S01YanQ3TUxSS0VlNy81TTNSN0NZRnIwdUt3MUs3NDhNM0V1Tit1RnpuaEsyK3Ywb0lPWGFoeEZ4eHN3RlV2YmMvam1ndTJxZm5JVW5jSElQR1hOMjR3OEdONVpHQmJZRzZrSENTZE9ObmpLK0Y2VklkUjNPbU5QMzY4YVUrZCtpOVVtOGhQYjROZ2ZEK1Fmd1orTnZXZ01taXA5SmRVN1hyMmcyTnJFMnFoN2ZleS8xLzNITlUxTTdHWWVNOSsxM3UxOTdzZVlMeDZyOURPcWhZT3YyYUxhUnVvTS8wNmo5bVFGbGFsTzdOVHhrV21mQmltMXhXandRejF4ZnJZNzBodW9XRHBVa3ZJb1d4Z3VHZ0NyNGJlQS9PT3EyMkNHUWJwRVRHOHNLbFJNVUphRzB4SnNlcGxzOVY1bVdJbUJ1bFFpL3lOTzdsR0JsZmRiWGIzcVY4R3VmcnNZV0tQczNMa2VsS2NRNi93SUM1NlJ2cy9iRDNFOFl3QTlVSFpTMkxUdFZ1Njc1VWVrVXJvcHBqeUpKRkNtR3BSMCtoMkFSRjBESnQyQkJmc1Z3Wk9tTDAvdk1RdGwyVERKVml2VjhacE5PS0l2dHFSMnpnejlFaVpGV2xwL05pcHlUWjN1NUFZUGUvNENhazROM2hXUFV6NEw3dkllZmh4NUgrSXlEZWVtU1NRdHZkQUtMSmZzMDVSMzJHQmlnTGdYaXlqdmQ1azZYSTljNDJjV0liRmR6MzhXVGt5aUpEeHBBYWRQRE1hS011RGU1alUxUXZvVEtPU01hTkZhYVN4U00zNmhLRVB3T2xiOGFsenVDTjdsZEVwbzFpQlo1SHlSSzhRNFF3WnZ2dW5sdEgwbGQxTnVLOXpPa1NPVXVkUlJDK2JFQkJ1Umdra0ZxRDRjd2FESlVXbzZxZXRwL0VTSXkyZlAyY3Bkd1hGVTYvNGpKRVl6R2c1amN3dTZnMlVSaG9Md2hkcFRKZ0tENGpVb25kbWl6RmNwZE9pd1BoeDBxMEM3bnQ4ZjlnYWlaLzFySSs0UTFTQW1uZytqenMzRFJVc1BqYW9aSVpON21nMGZlaTNaWnRaZXpVekVReEhEQUp3UFlYYW9IQWJpVFgrdGtZcGhNYTRlM3BqOEk2WnVQUmlzOHNiamFFaXhGWW9sTDFlR0N1WUJvZHY0aHRCSDdLbncvMVhwYVVZcVFLdVBJZ1o5WnVLcVF1VUpsblgxWVJQK1JVQmQ0NzhmOFVwMURMd2VjeTNrdjBPL1V0SFFRN2pMUEQxRWFoT0hwV3ZPdWp4RGU5OW9kUndvcFRMemxUblJhT3pERGtEVFV6NHliWGp6K3pjTEtES2c4RUhQRFRpbnp3V3hJU2F5VllPYjh2Q2hYMjRGZFFCbDQ4emJQeEFTZDhDS082eW9CRE90dEFJTkVYYWp2SUZZV2hyK0grVkpmc3BvNHJIemN6TFc0bUVLUVJXWGNhVTNrM0c5bTNyaWxBc25SVVFQTnZPRTh6L05VNXd6cUNzMUpXYzFLd1hzVDNvbkRyWnlkSGgyQUZXUXRoL3IwT0V1TnRPd2lDTzkyenZhM3FwTm9YUFZtZkpWMy9FUWpTR3FLV0dSeG5xejFvdEVTYmMvc2lNUStSZ05XRGE5UG54RUNiOWhlUVpwSURXMHJ5eHdjTXVIMDdRdEV0TFh2QlBQVEV5SlZZSmR1b25zUHQyVDdjZjhTQUxRT25od2M5YW9tcENmMGRIcG1HK01ZRjgyaVVDbGdzbjJyWnVHZzY2VnRjdURXMXVLL1g1RWx2bjZDSTVzOU1tbGp1Z09SSGpEelZISk4yQ25jTmloMFVNcDJjYmRhTmV5b1o2ZmhzajRsWVRYMHpwcDJDZUlOOGFWcXc0Y0FIK1ZWWnFOODNkNUFFYUxHVVJvbDkzZFQxKzJ0VGFiKzBIcS9NRjd5TXkyZG5seVVIb2dZZTMyVDQ1azZ6dUxZaksyeWdaNHlmTmZCVlc4OWpWQWtydm9oTmNCSVB3MStlMnFjcG9sOCtMVDNEayszVEV2L1J2TGlLTVl4Vi9LeVMralFvRjk2U3I0dXNpVktVeDRJbWRKNW9VNlpmTlpqSFY2eHhhOHN2UVd5aDN0Nksva1BRR1FURFh0aGpMTXdjVGp2ZUxBRDU5RWdhclNaQzFjdWtWeGdyc2JGR1V2QXNzT24zVjd6SzBZaGFiNTVhTGdaZ3VPNWJ1bWR0R2R6Sjc3Myt6ZzN2MjdWcTF0SGR5QWk2d0FnSmQzOEg3eVhKU3k5Wi9hdUszclArdGFiTHdQVURwUVJKOXhRNVMwTW42aUljNU1Ldm9xd2pxMHZNbzhlYXFDU05xNDBmcFFJYVk3UUpkeFdaMFdweWZ6ckJ2TFBKTlQrZDhZQlZIL1ZmUTlQZTFLZlZ3ejg2Kzh6Y2tWQ2dBUXJVZml2NFBiRGU2d0V0SUFiKy8vVUs0OHh3Z1VnaFBqcHZpaEFNSGk4enFrRFd6bW5kRFpmZTFJNDZlaWN0WUJGZXo4N2ViaGxGS1FZemRYa3Fqa080bHI2dFhJQ3JXMHVIVHpndlJtYnBjYUZ0R2h2dWpKWnJZcm1haWJRN2lHUDRJUWxOc3BOeXRxMnU4SFJNOUFHWW8wYjAycGpDWFlNVlorbW8zVmM0aHJRd1pCcjhjM1VSN2c0N1VJemh0VTZCaENVOExHazB1VTZNZ3NuREdYUTNDN2RuckhqRFpheHBJWlRlRi85MHhpSjVHM1lDWnA1aVVwMjhkb3RxK29ySkZMaWNLTGxXYVplV1QzbWxGVXhtamNIWDlPMUV0L3V2Slp5RWNWdmRaZGYwTnBybkNKRWVOYythMDh5S2d0ZkI0VktKVENPa1lKV3Y0MXVNajV4M2ZSYlpSYkxVdXE0NCtUN2NweTVGUHk2NlUrOEFGcTQ3aWxUTW9lUWsyczhaMHJqYm9nRGhHSjdLbUQ4dmUwaEdVeXpQUmcwRFB3M1lhRmxwaWtNdWRNNm9sTkQ5TndDNVVlL0pxTnU0Q2JIekx5L1QwOHVLbXRtb2N4WHA2dUhncTdldXorcnB2OWxVVnkvZHJLVTJTNVpDc2ViaDJ2ZStaUVgweUphcncwOFQrYTBsNlBBMmFRQ013M203RWFUellueDJvS0piRFhrenVadFczOUo2T1pWQkQyR0paV1pxYThZdXZYTldqNkgrZDNWek12TXlVRmZ6M0wxdmxMeUIvYTBaTDJWcXBuaStxbFVOSHVvUnU1ZGVzVExyU1d3bW9FdC9NVXRrQlJBRVZJTENnc1dxNUlnWXN6RGNMb3FXRFk5Rk9vRFBlL3pvam0zZHM4ZHFuSUloZVdsQk5OeEVHLzJnSGtFUkJTamczY0ZzTEVRdmR0QkZyY2hidjdnMmc1KzEwNmZ6TWhweGJHZmNrc1JGaDUrUTgwUEVJT3dHMFlPa2cwdHFENlBKUkdPVGFMV2lxd2lWTitUVFE2ZVBsQ1g2NHY2Uk1GSE9JTFVWdkwvMzB3UERPVTVIa0F3Vmt3K0hjOFF5Tzg2cEU2SUpublp5WmRRVURPQ0RHd0x6Snc3Smt1bjJ4QldvVDh0U2I4QVNoTzQwRVJkMUY4L1ZIaEI3NW9Na0tsSlJaaWVEVDhzM0lmNFBEYld6c1FIMGJXT25wYS9rYW94WlpZMjhGdmRhWlhLaHF4V00wZytweXlHMEc5eUlFbENHZTJqSmFaV3pyaEVsaFViMVRCb1NCNys2QTdrNEcvYXhnPT08L0NpcGhlclZhbHVlPjwvQ2lwaGVyRGF0YT48L0VuY3J5cHRlZERhdGE+PC9MQT48U2lnbmF0dXJlIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48U2lnbmVkSW5mbyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PENhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy9UUi8yMDAxL1JFQy14bWwtYzE0bi0yMDAxMDMxNSI+PC9DYW5vbmljYWxpemF0aW9uTWV0aG9kPjxTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL0RSTS8yMDA3LzAzL3Byb3RvY29scyNlY2RzYS1zaGEyNTYiPjwvU2lnbmF0dXJlTWV0aG9kPjxSZWZlcmVuY2UgVVJJPSIjU2lnbmVkRGF0YSI+PERpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vRFJNLzIwMDcvMDMvcHJvdG9jb2xzI3NoYTI1NiI+PC9EaWdlc3RNZXRob2Q+PERpZ2VzdFZhbHVlPmJyMVB0SjNPamVEWlNKOEJYVkxyeWRIZnl0Wmg4TjdSRU5EQmUyZ0xzVnM9PC9EaWdlc3RWYWx1ZT48L1JlZmVyZW5jZT48L1NpZ25lZEluZm8+PFNpZ25hdHVyZVZhbHVlPlNZSVBzcjQ0M0I0a29CWGhWaUVLcUxZUHNWTTY1RXIwYTI3U0ExVmt4TW5md2MzMDBvN1IrU21KRnVkNTQwbE1OeDc3L3ZQRUJiWnh1S2VqaTVmcml3PT08L1NpZ25hdHVyZVZhbHVlPjxLZXlJbmZvIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48S2V5VmFsdWU+PEVDQ0tleVZhbHVlPjxQdWJsaWNLZXk+WG8yNVEydWI5UmpDZzZtWnlHR1h6WmQvNkdsV1M2Sm11eHR0Ri9vWXlEeXdlN000ZmJuQzZzOEFWandlL24vOXVCaklOQ3RoSUNxa2l4WmxpS2w3cUE9PTwvUHVibGljS2V5PjwvRUNDS2V5VmFsdWU+PC9LZXlWYWx1ZT48L0tleUluZm8+PC9TaWduYXR1cmU+PC9DaGFsbGVuZ2U+PC9jaGFsbGVuZ2U+PC9BY3F1aXJlTGljZW5zZT48L3NvYXA6Qm9keT48L3NvYXA6RW52ZWxvcGU+" +playready_esn: "NFANDROID2-PRV-REFPLUSCOQ4KT51PGTV-MGK000000043430" +playready_kdekdh: + - "5VMBpSpvyiwQPdaZ99PX6Q==" + - "u8ldDaWPL7kFW7hkc84/QN4yDxnbeHAw13Uk7uu50EE=" + esn_map: # key map of CDM WVD `SystemID = 'ESN you want to use for that CDM WVD'` 8159: "NFANDROID1-PRV-P-GOOGLEPIXEL" 8131: "HISETVK84500000000000000000000000007401422" - 22590: "NFANDROID1-PXA-P-L3-XIAOMM2102J20SG-22590-0202084EBTP55D0HO2TOCSM3VR9MOSTTJT2L97EKVN9E8PFA1QQ439QC70QTTTV82LC7KUSD3O0HUB0HKH51DH0N7A7GFJKSJ5S6FFE0" + 22590: "NFANDROID1-PXA-P-L3-XIAOMM2102J20SG-22590-020NTB086HJPGG70MDDMR0306MR0NNO5G3DJGFCKS9HJF58ER9QA21VFG4I0246JRN6TF16L9I627EPK708SH42UUMG1ASFVG20F3" 12063: "NFANDROID1-PRV-P-SHENZHENKTC-49B1U-12063-2PAENERYJWY35H7F24163TMUCBBA4VRHQ2XZX4OBU4MUTKYFW50BMFBVGTUMN6IM0" + 7110: "NFANDROID1-PRV-P-MSD6886602GUHDANDROIDTV-HISENHISMARTTV-A4-7110-D34E1ECACCDBE518DBA0CCE8A4D1D48248ACB5C1A2BAEB2ADFF1040C5AE3FF42" endpoints: website: "https://www.netflix.com/nq/website/memberapi/{build_id}/pathEvaluator" manifest: "https://www.netflix.com/msl/playapi/cadmium/licensedmanifest/1" @@ -27,7 +33,7 @@ endpoints: configuration: drm_system: "widevine" # chrome and android: widevine, edge: playready - drm_version: 30 # widevine: 25, playready: 30 + drm_version: 25 # widevine: 25, playready: 30 supported_hdcp_versions: ["2.2"] # 720p-max: 1.4, chrome: empty, 4k: 2.2 is_hdcp_engaged: false # chrome: false diff --git a/unshackle/unshackle.yaml b/unshackle/unshackle.yaml index 8b54828..df50b84 100644 --- a/unshackle/unshackle.yaml +++ b/unshackle/unshackle.yaml @@ -64,7 +64,7 @@ directories: cache: Cache # cookies: Cookies dcsl: DCSL # Device Certificate Status List - downloads: Downloads + downloads: /home/kenzuya/Mounts/ketuakenzuya/Downloads/ logs: Logs temp: Temp # wvds: WVDs @@ -79,7 +79,7 @@ directories: # Pre-define which Widevine or PlayReady device to use for each Service cdm: # Global default CDM device (fallback for all services/profiles) - default: chromecdm + default: lg_50ut73006la.cekukh_17.0.0_22163355_7110_l1 # Direct service-specific CDM DIFFERENT_EXAMPLE: PRD_1