diff --git a/unshackle/WVDs/hisense_vidaa_tv_14.0.0_92747e74_7110_l1.wvd b/unshackle/WVDs/hisense_vidaa_tv_14.0.0_92747e74_7110_l1.wvd new file mode 100644 index 0000000..978734f Binary files /dev/null and b/unshackle/WVDs/hisense_vidaa_tv_14.0.0_92747e74_7110_l1.wvd differ diff --git a/unshackle/services/Netflix/MSL/__init__.py b/unshackle/services/Netflix/MSL/__init__.py index 3b65d4d..2d74657 100644 --- a/unshackle/services/Netflix/MSL/__init__.py +++ b/unshackle/services/Netflix/MSL/__init__.py @@ -10,7 +10,7 @@ import time import zlib from datetime import datetime from io import BytesIO -from typing import Optional, Any +from typing import Any, Optional import jsonpickle import requests @@ -19,15 +19,18 @@ from Cryptodome.Hash import HMAC, SHA256 from Cryptodome.PublicKey import RSA from Cryptodome.Random import get_random_bytes from Cryptodome.Util import Padding +from pywidevine import PSSH, Cdm, Key from unshackle.core.cacher import Cacher from .MSLKeys import MSLKeys -from .schemes import EntityAuthenticationSchemes # noqa: F401 -from .schemes import KeyExchangeSchemes +from .schemes import ( + EntityAuthenticationSchemes, # noqa: F401 + KeyExchangeSchemes, +) from .schemes.EntityAuthentication import EntityAuthentication from .schemes.KeyExchangeRequest import KeyExchangeRequest -from pywidevine import Cdm, PSSH, Key + class MSL: log = logging.getLogger("MSL") @@ -41,7 +44,16 @@ class MSL: self.message_id = message_id @classmethod - def handshake(cls, scheme: KeyExchangeSchemes, session: requests.Session, endpoint: str, sender: str, cache: Cacher, cdm: Optional[Cdm] = None, config: Any = None): + 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) @@ -53,20 +65,18 @@ class MSL: if scheme != KeyExchangeSchemes.Widevine: msl_keys.rsa = RSA.generate(2048) - if scheme == KeyExchangeSchemes.Widevine: if not cdm: - raise Exception('Key exchange scheme Widevine but CDM instance is None.') - + 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, - + session_id, + PSSH("AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQAAAAAAPSZ0kAAAAAAAAAAA=="), + "OFFLINE", + True, ) keyrequestdata = KeyExchangeRequest.Widevine(challenge) entityauthdata = EntityAuthentication.Unauthenticated(sender) @@ -76,47 +86,48 @@ class MSL: keyrequestdata = KeyExchangeRequest.AsymmetricWrapped( keypairid="superKeyPair", mechanism="JWK_RSA", - publickey=msl_keys.rsa.publickey().exportKey(format="DER") + publickey=msl_keys.rsa.publickey().exportKey(format="DER"), ) - data = jsonpickle.encode({ - "entityauthdata": entityauthdata, - "headerdata": base64.b64encode(MSL.generate_msg_header( - message_id=message_id, - sender=sender, - is_handshake=True, - keyrequestdata=keyrequestdata - ).encode("utf-8")).decode("utf-8"), - "signature": "" - }, unpicklable=False) - data += json.dumps({ - "payload": base64.b64encode(json.dumps({ - "messageid": message_id, - "data": "", - "sequencenumber": 1, - "endofmsg": True - }).encode("utf-8")).decode("utf-8"), - "signature": "" - }) + data = jsonpickle.encode( + { + "entityauthdata": entityauthdata, + "headerdata": base64.b64encode( + MSL.generate_msg_header( + message_id=message_id, sender=sender, is_handshake=True, keyrequestdata=keyrequestdata + ).encode("utf-8") + ).decode("utf-8"), + "signature": "", + }, + unpicklable=False, + ) + data += json.dumps( + { + "payload": base64.b64encode( + json.dumps({"messageid": message_id, "data": "", "sequencenumber": 1, "endofmsg": True}).encode( + "utf-8" + ) + ).decode("utf-8"), + "signature": "", + } + ) try: - r = session.post( - url=endpoint, - data=data - ) + r = session.post(url=endpoint, data=data) except requests.HTTPError as e: 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 Exception("- Key exchange failed: " + json.loads(base64.b64decode( - key_exchange["errordata"] - ).decode())["errormsg"]) + raise Exception( + "- Key exchange failed: " + + json.loads(base64.b64decode(key_exchange["errordata"]).decode())["errormsg"] + ) # parse the crypto keys - key_response_data = json.JSONDecoder().decode(base64.b64decode( - key_exchange["headerdata"] - ).decode("utf-8"))["keyresponsedata"] + key_response_data = json.JSONDecoder().decode(base64.b64decode(key_exchange["headerdata"]).decode("utf-8"))[ + "keyresponsedata" + ] if key_response_data["scheme"] != str(scheme): raise Exception("- Key exchange scheme mismatch occurred") @@ -133,42 +144,36 @@ class MSL: encryption_key = MSL.get_widevine_key( kid=base64.b64decode(key_data["encryptionkeyid"]), keys=keys, - permissions=["allow_encrypt", "allow_decrypt"] + permissions=["allow_encrypt", "allow_decrypt"], ) msl_keys.encryption = 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"] + permissions=["allow_sign", "allow_signature_verify"], ) cls.log.debug(f"Sign key: {sign}") msl_keys.sign = sign - + elif scheme == KeyExchangeSchemes.AsymmetricWrapped: cipher_rsa = PKCS1_OAEP.new(msl_keys.rsa) msl_keys.encryption = MSL.base64key_decode( - json.JSONDecoder().decode(cipher_rsa.decrypt( - base64.b64decode(key_data["encryptionkey"]) - ).decode("utf-8"))["k"] + json.JSONDecoder().decode( + cipher_rsa.decrypt(base64.b64decode(key_data["encryptionkey"])).decode("utf-8") + )["k"] ) msl_keys.sign = MSL.base64key_decode( - json.JSONDecoder().decode(cipher_rsa.decrypt( - base64.b64decode(key_data["hmackey"]) - ).decode("utf-8"))["k"] + json.JSONDecoder().decode( + cipher_rsa.decrypt(base64.b64decode(key_data["hmackey"])).decode("utf-8") + )["k"] ) - + msl_keys.mastertoken = key_response_data["mastertoken"] MSL.cache_keys(msl_keys, cache) cls.log.info("MSL handshake successful") - return cls( - session=session, - endpoint=endpoint, - sender=sender, - keys=msl_keys, - message_id=message_id - ) + return cls(session=session, endpoint=endpoint, sender=sender, keys=msl_keys, message_id=message_id) @staticmethod def load_cache_data(cacher: Cacher): @@ -184,9 +189,24 @@ class MSL: # to an RsaKey :) msl_keys.rsa = RSA.importKey(msl_keys.rsa) # If it's expired or close to, return None as it's unusable - if msl_keys.mastertoken and ((datetime.utcfromtimestamp(int(json.JSONDecoder().decode( - base64.b64decode(msl_keys.mastertoken["tokendata"]).decode("utf-8") - )["expiration"])) - datetime.now()).total_seconds() / 60 / 60) < 10: + if ( + msl_keys.mastertoken + and ( + ( + datetime.utcfromtimestamp( + int( + json.JSONDecoder().decode( + base64.b64decode(msl_keys.mastertoken["tokendata"]).decode("utf-8") + )["expiration"] + ) + ) + - datetime.now() + ).total_seconds() + / 60 + / 60 + ) + < 10 + ): return None return msl_keys @@ -204,8 +224,9 @@ class MSL: msl_keys.rsa = RSA.importKey(msl_keys.rsa) @staticmethod - def generate_msg_header(message_id, sender, is_handshake, userauthdata=None, keyrequestdata=None, - compression="GZIP"): + def generate_msg_header( + message_id, sender, is_handshake, userauthdata=None, keyrequestdata=None, compression="GZIP" + ): """ The MSL header carries all MSL data used for entity and user authentication, message encryption and verification, and service tokens. Portions of the MSL header are encrypted. @@ -228,7 +249,7 @@ class MSL: "capabilities": { "compressionalgos": [compression] if compression else [], "languages": ["en-US"], # bcp-47 - "encoderformats": ["JSON"] + "encoderformats": ["JSON"], }, "timestamp": int(time.time()), # undocumented or unused: @@ -272,33 +293,42 @@ class MSL: def create_message(self, application_data, userauthdata=None): self.message_id += 1 # new message must ue a new message id - headerdata = self.encrypt(self.generate_msg_header( - message_id=self.message_id, - sender=self.sender, - is_handshake=False, - userauthdata=userauthdata - )) + headerdata = self.encrypt( + self.generate_msg_header( + message_id=self.message_id, sender=self.sender, is_handshake=False, userauthdata=userauthdata + ) + ) - header = json.dumps({ - "headerdata": base64.b64encode(headerdata.encode("utf-8")).decode("utf-8"), - "signature": self.sign(headerdata).decode("utf-8"), - "mastertoken": self.keys.mastertoken - }) + header = json.dumps( + { + "headerdata": base64.b64encode(headerdata.encode("utf-8")).decode("utf-8"), + "signature": self.sign(headerdata).decode("utf-8"), + "mastertoken": self.keys.mastertoken, + } + ) - payload_chunks = [self.encrypt(json.dumps({ - "messageid": self.message_id, - "data": self.gzip_compress(json.dumps(application_data).encode("utf-8")).decode("utf-8"), - "compressionalgo": "GZIP", - "sequencenumber": 1, # todo ; use sequence_number from master token instead? - "endofmsg": True - }))] + payload_chunks = [ + self.encrypt( + json.dumps( + { + "messageid": self.message_id, + "data": self.gzip_compress(json.dumps(application_data).encode("utf-8")).decode("utf-8"), + "compressionalgo": "GZIP", + "sequencenumber": 1, # todo ; use sequence_number from master token instead? + "endofmsg": True, + } + ) + ) + ] message = header for payload_chunk in payload_chunks: - message += json.dumps({ - "payload": base64.b64encode(payload_chunk.encode("utf-8")).decode("utf-8"), - "signature": self.sign(payload_chunk).decode("utf-8") - }) + message += json.dumps( + { + "payload": base64.b64encode(payload_chunk.encode("utf-8")).decode("utf-8"), + "signature": self.sign(payload_chunk).decode("utf-8"), + } + ) return message @@ -316,9 +346,7 @@ class MSL: payload_chunk = json.loads(base64.b64decode(payload_chunk["payload"]).decode("utf-8")) # decrypt the payload payload_decrypted = AES.new( - key=self.keys.encryption, - mode=AES.MODE_CBC, - iv=base64.b64decode(payload_chunk["iv"]) + key=self.keys.encryption, mode=AES.MODE_CBC, iv=base64.b64decode(payload_chunk["iv"]) ).decrypt(base64.b64decode(payload_chunk["ciphertext"])) payload_decrypted = Padding.unpad(payload_decrypted, 16) payload_decrypted = json.loads(payload_decrypted.decode("utf-8")) @@ -335,9 +363,9 @@ class MSL: error_detail = re.sub(r" \(E3-[^)]+\)", "", error.get("detail", "")) if error_display: - self.log.critical(f"- {error_display}") + self.log.critical(f"- {error_display}") if error_detail: - self.log.critical(f"- {error_detail}") + self.log.critical(f"- {error_detail}") if not (error_display or error_detail): self.log.critical(f"- {error}") @@ -391,22 +419,19 @@ class MSL: :return: Serialized JSON String of the encryption Envelope """ iv = get_random_bytes(16) - return json.dumps({ - "ciphertext": base64.b64encode( - AES.new( - self.keys.encryption, - AES.MODE_CBC, - iv - ).encrypt( - Padding.pad(plaintext.encode("utf-8"), 16) - ) - ).decode("utf-8"), - "keyid": "{}_{}".format(self.sender, json.loads( - base64.b64decode(self.keys.mastertoken["tokendata"]).decode("utf-8") - )["sequencenumber"]), - "sha256": "AA==", - "iv": base64.b64encode(iv).decode("utf-8") - }) + return json.dumps( + { + "ciphertext": base64.b64encode( + AES.new(self.keys.encryption, AES.MODE_CBC, iv).encrypt(Padding.pad(plaintext.encode("utf-8"), 16)) + ).decode("utf-8"), + "keyid": "{}_{}".format( + self.sender, + json.loads(base64.b64decode(self.keys.mastertoken["tokendata"]).decode("utf-8"))["sequencenumber"], + ), + "sha256": "AA==", + "iv": base64.b64encode(iv).decode("utf-8"), + } + ) def sign(self, text): """ diff --git a/unshackle/services/Netflix/__init__.py b/unshackle/services/Netflix/__init__.py index 1c55f70..265b27b 100644 --- a/unshackle/services/Netflix/__init__.py +++ b/unshackle/services/Netflix/__init__.py @@ -567,6 +567,7 @@ class Netflix(Service): 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) + self.log.fatal(f"Android sign-in failed: {header_data}") if error_code: raise click.ClickException(f"Android sign-in failed: {error_code}") raise click.ClickException("Android sign-in did not return a useridtoken.") diff --git a/unshackle/services/Netflix/config.yaml b/unshackle/services/Netflix/config.yaml index 2802567..498ae54 100644 --- a/unshackle/services/Netflix/config.yaml +++ b/unshackle/services/Netflix/config.yaml @@ -19,7 +19,7 @@ playready_kdekdh: esn_map: # key map of CDM WVD `SystemID = 'ESN you want to use for that CDM WVD'` 8159: "NFANDROID1-PRV-P-GOOGLEPIXEL" - 8131: "HISETVK84500000000000000000000000007401422" + 12603: "HISETVK84500000000000000000000000007401422" 22589: "NFANDROID1-PRV-P-SAMSUNG-SM-G975F-22589-RYFPGCV4K7QNZJFT5EK3YZ25TYOC1B1MSFEXR4L8JM99YOFXKZLFLOBTATE2AA2UJ" 22590: "NFANDROID1-PXA-P-L3-XIAOMM2102J20SG-22590-020NTB086HJPGG70MDDMR0306MR0NNO5G3DJGFCKS9HJF58ER9QA21VFG4I0246JRN6TF16L9I627EPK708SH42UUMG1ASFVG20F3" 12063: "NFANDROID1-PRV-P-SHENZHENKTC-49B1U-12063-2PAENERYJWY35H7F24163TMUCBBA4VRHQ2XZX4OBU4MUTKYFW50BMFBVGTUMN6IM0" diff --git a/unshackle/unshackle.yaml b/unshackle/unshackle.yaml index 553173a..c05560a 100644 --- a/unshackle/unshackle.yaml +++ b/unshackle/unshackle.yaml @@ -70,7 +70,7 @@ remote_cdm: security_level: 3 type: "decrypt_labs" host: https://keyxtractor.decryptlabs.com - secret: 7547150416_41da0a32d6237d83_KeyXtractor_api_ext + secret: 919240143_41d9c3fac9a5f82e_KeyXtractor_ultimate - name: "android" device_name: andorid device_type: ANDROID