Update Netflix

This commit is contained in:
kenzuya
2026-03-17 14:04:36 +07:00
parent dfd3cdb8a2
commit b308669221
5 changed files with 137 additions and 111 deletions

View File

@@ -10,7 +10,7 @@ import time
import zlib import zlib
from datetime import datetime from datetime import datetime
from io import BytesIO from io import BytesIO
from typing import Optional, Any from typing import Any, Optional
import jsonpickle import jsonpickle
import requests import requests
@@ -19,15 +19,18 @@ from Cryptodome.Hash import HMAC, SHA256
from Cryptodome.PublicKey import RSA from Cryptodome.PublicKey import RSA
from Cryptodome.Random import get_random_bytes from Cryptodome.Random import get_random_bytes
from Cryptodome.Util import Padding from Cryptodome.Util import Padding
from pywidevine import PSSH, Cdm, Key
from unshackle.core.cacher import Cacher from unshackle.core.cacher import Cacher
from .MSLKeys import MSLKeys from .MSLKeys import MSLKeys
from .schemes import EntityAuthenticationSchemes # noqa: F401 from .schemes import (
from .schemes import KeyExchangeSchemes EntityAuthenticationSchemes, # noqa: F401
KeyExchangeSchemes,
)
from .schemes.EntityAuthentication import EntityAuthentication from .schemes.EntityAuthentication import EntityAuthentication
from .schemes.KeyExchangeRequest import KeyExchangeRequest from .schemes.KeyExchangeRequest import KeyExchangeRequest
from pywidevine import Cdm, PSSH, Key
class MSL: class MSL:
log = logging.getLogger("MSL") log = logging.getLogger("MSL")
@@ -41,7 +44,16 @@ class MSL:
self.message_id = message_id self.message_id = message_id
@classmethod @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) cache = cache.get(sender)
message_id = random.randint(0, pow(2, 52)) message_id = random.randint(0, pow(2, 52))
msl_keys = MSL.load_cache_data(cache) msl_keys = MSL.load_cache_data(cache)
@@ -53,20 +65,18 @@ class MSL:
if scheme != KeyExchangeSchemes.Widevine: if scheme != KeyExchangeSchemes.Widevine:
msl_keys.rsa = RSA.generate(2048) msl_keys.rsa = RSA.generate(2048)
if scheme == KeyExchangeSchemes.Widevine: if scheme == KeyExchangeSchemes.Widevine:
if not cdm: 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() session_id = cdm.open()
msl_keys.cdm_session = session_id msl_keys.cdm_session = session_id
cdm.set_service_certificate(session_id, config["certificate"]) cdm.set_service_certificate(session_id, config["certificate"])
challenge = cdm.get_license_challenge( challenge = cdm.get_license_challenge(
session_id=session_id, session_id,
pssh=PSSH("AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQAAAAAAPSZ0kAAAAAAAAAAA=="), PSSH("AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQAAAAAAPSZ0kAAAAAAAAAAA=="),
license_type="OFFLINE", "OFFLINE",
privacy_mode=True, True,
) )
keyrequestdata = KeyExchangeRequest.Widevine(challenge) keyrequestdata = KeyExchangeRequest.Widevine(challenge)
entityauthdata = EntityAuthentication.Unauthenticated(sender) entityauthdata = EntityAuthentication.Unauthenticated(sender)
@@ -76,47 +86,48 @@ class MSL:
keyrequestdata = KeyExchangeRequest.AsymmetricWrapped( keyrequestdata = KeyExchangeRequest.AsymmetricWrapped(
keypairid="superKeyPair", keypairid="superKeyPair",
mechanism="JWK_RSA", mechanism="JWK_RSA",
publickey=msl_keys.rsa.publickey().exportKey(format="DER") publickey=msl_keys.rsa.publickey().exportKey(format="DER"),
) )
data = jsonpickle.encode({ data = jsonpickle.encode(
"entityauthdata": entityauthdata, {
"headerdata": base64.b64encode(MSL.generate_msg_header( "entityauthdata": entityauthdata,
message_id=message_id, "headerdata": base64.b64encode(
sender=sender, MSL.generate_msg_header(
is_handshake=True, message_id=message_id, sender=sender, is_handshake=True, keyrequestdata=keyrequestdata
keyrequestdata=keyrequestdata ).encode("utf-8")
).encode("utf-8")).decode("utf-8"), ).decode("utf-8"),
"signature": "" "signature": "",
}, unpicklable=False) },
data += json.dumps({ unpicklable=False,
"payload": base64.b64encode(json.dumps({ )
"messageid": message_id, data += json.dumps(
"data": "", {
"sequencenumber": 1, "payload": base64.b64encode(
"endofmsg": True json.dumps({"messageid": message_id, "data": "", "sequencenumber": 1, "endofmsg": True}).encode(
}).encode("utf-8")).decode("utf-8"), "utf-8"
"signature": "" )
}) ).decode("utf-8"),
"signature": "",
}
)
try: try:
r = session.post( r = session.post(url=endpoint, data=data)
url=endpoint,
data=data
)
except requests.HTTPError as e: except requests.HTTPError as e:
raise Exception(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 key_exchange = r.json() # expecting no payloads, so this is fine
if "errordata" in key_exchange: if "errordata" in key_exchange:
raise Exception("- Key exchange failed: " + json.loads(base64.b64decode( raise Exception(
key_exchange["errordata"] "- Key exchange failed: "
).decode())["errormsg"]) + json.loads(base64.b64decode(key_exchange["errordata"]).decode())["errormsg"]
)
# parse the crypto keys # parse the crypto keys
key_response_data = json.JSONDecoder().decode(base64.b64decode( key_response_data = json.JSONDecoder().decode(base64.b64decode(key_exchange["headerdata"]).decode("utf-8"))[
key_exchange["headerdata"] "keyresponsedata"
).decode("utf-8"))["keyresponsedata"] ]
if key_response_data["scheme"] != str(scheme): if key_response_data["scheme"] != str(scheme):
raise Exception("- Key exchange scheme mismatch occurred") raise Exception("- Key exchange scheme mismatch occurred")
@@ -133,42 +144,36 @@ class MSL:
encryption_key = MSL.get_widevine_key( encryption_key = MSL.get_widevine_key(
kid=base64.b64decode(key_data["encryptionkeyid"]), kid=base64.b64decode(key_data["encryptionkeyid"]),
keys=keys, keys=keys,
permissions=["allow_encrypt", "allow_decrypt"] permissions=["allow_encrypt", "allow_decrypt"],
) )
msl_keys.encryption = encryption_key msl_keys.encryption = encryption_key
cls.log.debug(f"Encryption key: {encryption_key}") cls.log.debug(f"Encryption key: {encryption_key}")
sign = MSL.get_widevine_key( sign = MSL.get_widevine_key(
kid=base64.b64decode(key_data["hmackeyid"]), kid=base64.b64decode(key_data["hmackeyid"]),
keys=keys, keys=keys,
permissions=["allow_sign", "allow_signature_verify"] permissions=["allow_sign", "allow_signature_verify"],
) )
cls.log.debug(f"Sign key: {sign}") cls.log.debug(f"Sign key: {sign}")
msl_keys.sign = sign msl_keys.sign = sign
elif scheme == KeyExchangeSchemes.AsymmetricWrapped: 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(
base64.b64decode(key_data["encryptionkey"]) cipher_rsa.decrypt(base64.b64decode(key_data["encryptionkey"])).decode("utf-8")
).decode("utf-8"))["k"] )["k"]
) )
msl_keys.sign = MSL.base64key_decode( msl_keys.sign = MSL.base64key_decode(
json.JSONDecoder().decode(cipher_rsa.decrypt( json.JSONDecoder().decode(
base64.b64decode(key_data["hmackey"]) cipher_rsa.decrypt(base64.b64decode(key_data["hmackey"])).decode("utf-8")
).decode("utf-8"))["k"] )["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)
cls.log.info("MSL handshake successful") cls.log.info("MSL handshake successful")
return cls( return cls(session=session, endpoint=endpoint, sender=sender, keys=msl_keys, message_id=message_id)
session=session,
endpoint=endpoint,
sender=sender,
keys=msl_keys,
message_id=message_id
)
@staticmethod @staticmethod
def load_cache_data(cacher: Cacher): def load_cache_data(cacher: Cacher):
@@ -184,9 +189,24 @@ class MSL:
# to an RsaKey :) # to an RsaKey :)
msl_keys.rsa = RSA.importKey(msl_keys.rsa) msl_keys.rsa = RSA.importKey(msl_keys.rsa)
# If it's expired or close to, return None as it's unusable # If it's expired or close to, return None as it's unusable
if msl_keys.mastertoken and ((datetime.utcfromtimestamp(int(json.JSONDecoder().decode( if (
base64.b64decode(msl_keys.mastertoken["tokendata"]).decode("utf-8") msl_keys.mastertoken
)["expiration"])) - datetime.now()).total_seconds() / 60 / 60) < 10: 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 None
return msl_keys return msl_keys
@@ -204,8 +224,9 @@ class MSL:
msl_keys.rsa = RSA.importKey(msl_keys.rsa) msl_keys.rsa = RSA.importKey(msl_keys.rsa)
@staticmethod @staticmethod
def generate_msg_header(message_id, sender, is_handshake, userauthdata=None, keyrequestdata=None, def generate_msg_header(
compression="GZIP"): 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 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. and verification, and service tokens. Portions of the MSL header are encrypted.
@@ -228,7 +249,7 @@ class MSL:
"capabilities": { "capabilities": {
"compressionalgos": [compression] if compression else [], "compressionalgos": [compression] if compression else [],
"languages": ["en-US"], # bcp-47 "languages": ["en-US"], # bcp-47
"encoderformats": ["JSON"] "encoderformats": ["JSON"],
}, },
"timestamp": int(time.time()), "timestamp": int(time.time()),
# undocumented or unused: # undocumented or unused:
@@ -272,33 +293,42 @@ class MSL:
def create_message(self, application_data, userauthdata=None): def create_message(self, application_data, userauthdata=None):
self.message_id += 1 # new message must ue a new message id self.message_id += 1 # new message must ue a new message id
headerdata = self.encrypt(self.generate_msg_header( headerdata = self.encrypt(
message_id=self.message_id, self.generate_msg_header(
sender=self.sender, message_id=self.message_id, sender=self.sender, is_handshake=False, userauthdata=userauthdata
is_handshake=False, )
userauthdata=userauthdata )
))
header = json.dumps({ header = json.dumps(
"headerdata": base64.b64encode(headerdata.encode("utf-8")).decode("utf-8"), {
"signature": self.sign(headerdata).decode("utf-8"), "headerdata": base64.b64encode(headerdata.encode("utf-8")).decode("utf-8"),
"mastertoken": self.keys.mastertoken "signature": self.sign(headerdata).decode("utf-8"),
}) "mastertoken": self.keys.mastertoken,
}
)
payload_chunks = [self.encrypt(json.dumps({ payload_chunks = [
"messageid": self.message_id, self.encrypt(
"data": self.gzip_compress(json.dumps(application_data).encode("utf-8")).decode("utf-8"), json.dumps(
"compressionalgo": "GZIP", {
"sequencenumber": 1, # todo ; use sequence_number from master token instead? "messageid": self.message_id,
"endofmsg": True "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 message = header
for payload_chunk in payload_chunks: for payload_chunk in payload_chunks:
message += json.dumps({ message += json.dumps(
"payload": base64.b64encode(payload_chunk.encode("utf-8")).decode("utf-8"), {
"signature": self.sign(payload_chunk).decode("utf-8") "payload": base64.b64encode(payload_chunk.encode("utf-8")).decode("utf-8"),
}) "signature": self.sign(payload_chunk).decode("utf-8"),
}
)
return message return message
@@ -316,9 +346,7 @@ class MSL:
payload_chunk = json.loads(base64.b64decode(payload_chunk["payload"]).decode("utf-8")) payload_chunk = json.loads(base64.b64decode(payload_chunk["payload"]).decode("utf-8"))
# decrypt the payload # decrypt the payload
payload_decrypted = AES.new( payload_decrypted = AES.new(
key=self.keys.encryption, key=self.keys.encryption, mode=AES.MODE_CBC, iv=base64.b64decode(payload_chunk["iv"])
mode=AES.MODE_CBC,
iv=base64.b64decode(payload_chunk["iv"])
).decrypt(base64.b64decode(payload_chunk["ciphertext"])) ).decrypt(base64.b64decode(payload_chunk["ciphertext"]))
payload_decrypted = Padding.unpad(payload_decrypted, 16) payload_decrypted = Padding.unpad(payload_decrypted, 16)
payload_decrypted = json.loads(payload_decrypted.decode("utf-8")) payload_decrypted = json.loads(payload_decrypted.decode("utf-8"))
@@ -335,9 +363,9 @@ class MSL:
error_detail = re.sub(r" \(E3-[^)]+\)", "", error.get("detail", "")) error_detail = re.sub(r" \(E3-[^)]+\)", "", error.get("detail", ""))
if error_display: if error_display:
self.log.critical(f"- {error_display}") self.log.critical(f"- {error_display}")
if error_detail: if error_detail:
self.log.critical(f"- {error_detail}") self.log.critical(f"- {error_detail}")
if not (error_display or error_detail): if not (error_display or error_detail):
self.log.critical(f"- {error}") self.log.critical(f"- {error}")
@@ -391,22 +419,19 @@ class MSL:
:return: Serialized JSON String of the encryption Envelope :return: Serialized JSON String of the encryption Envelope
""" """
iv = get_random_bytes(16) iv = get_random_bytes(16)
return json.dumps({ return json.dumps(
"ciphertext": base64.b64encode( {
AES.new( "ciphertext": base64.b64encode(
self.keys.encryption, AES.new(self.keys.encryption, AES.MODE_CBC, iv).encrypt(Padding.pad(plaintext.encode("utf-8"), 16))
AES.MODE_CBC, ).decode("utf-8"),
iv "keyid": "{}_{}".format(
).encrypt( self.sender,
Padding.pad(plaintext.encode("utf-8"), 16) json.loads(base64.b64decode(self.keys.mastertoken["tokendata"]).decode("utf-8"))["sequencenumber"],
) ),
).decode("utf-8"), "sha256": "AA==",
"keyid": "{}_{}".format(self.sender, json.loads( "iv": base64.b64encode(iv).decode("utf-8"),
base64.b64decode(self.keys.mastertoken["tokendata"]).decode("utf-8") }
)["sequencenumber"]), )
"sha256": "AA==",
"iv": base64.b64encode(iv).decode("utf-8")
})
def sign(self, text): def sign(self, text):
""" """

View File

@@ -567,6 +567,7 @@ class Netflix(Service):
self.log.debug(f"Android sign-in header keys: {list(header_data.keys())}") self.log.debug(f"Android sign-in header keys: {list(header_data.keys())}")
sign_in_value = self.extract_android_sign_in_value(payload_data) sign_in_value = self.extract_android_sign_in_value(payload_data)
error_code = self.extract_android_sign_in_error_code(sign_in_value) 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: if error_code:
raise click.ClickException(f"Android sign-in failed: {error_code}") raise click.ClickException(f"Android sign-in failed: {error_code}")
raise click.ClickException("Android sign-in did not return a useridtoken.") raise click.ClickException("Android sign-in did not return a useridtoken.")

View File

@@ -19,7 +19,7 @@ playready_kdekdh:
esn_map: esn_map:
# key map of CDM WVD `SystemID = 'ESN you want to use for that CDM WVD'` # key map of CDM WVD `SystemID = 'ESN you want to use for that CDM WVD'`
8159: "NFANDROID1-PRV-P-GOOGLEPIXEL" 8159: "NFANDROID1-PRV-P-GOOGLEPIXEL"
8131: "HISETVK84500000000000000000000000007401422" 12603: "HISETVK84500000000000000000000000007401422"
22589: "NFANDROID1-PRV-P-SAMSUNG-SM-G975F-22589-RYFPGCV4K7QNZJFT5EK3YZ25TYOC1B1MSFEXR4L8JM99YOFXKZLFLOBTATE2AA2UJ" 22589: "NFANDROID1-PRV-P-SAMSUNG-SM-G975F-22589-RYFPGCV4K7QNZJFT5EK3YZ25TYOC1B1MSFEXR4L8JM99YOFXKZLFLOBTATE2AA2UJ"
22590: "NFANDROID1-PXA-P-L3-XIAOMM2102J20SG-22590-020NTB086HJPGG70MDDMR0306MR0NNO5G3DJGFCKS9HJF58ER9QA21VFG4I0246JRN6TF16L9I627EPK708SH42UUMG1ASFVG20F3" 22590: "NFANDROID1-PXA-P-L3-XIAOMM2102J20SG-22590-020NTB086HJPGG70MDDMR0306MR0NNO5G3DJGFCKS9HJF58ER9QA21VFG4I0246JRN6TF16L9I627EPK708SH42UUMG1ASFVG20F3"
12063: "NFANDROID1-PRV-P-SHENZHENKTC-49B1U-12063-2PAENERYJWY35H7F24163TMUCBBA4VRHQ2XZX4OBU4MUTKYFW50BMFBVGTUMN6IM0" 12063: "NFANDROID1-PRV-P-SHENZHENKTC-49B1U-12063-2PAENERYJWY35H7F24163TMUCBBA4VRHQ2XZX4OBU4MUTKYFW50BMFBVGTUMN6IM0"

View File

@@ -70,7 +70,7 @@ remote_cdm:
security_level: 3 security_level: 3
type: "decrypt_labs" type: "decrypt_labs"
host: https://keyxtractor.decryptlabs.com host: https://keyxtractor.decryptlabs.com
secret: 7547150416_41da0a32d6237d83_KeyXtractor_api_ext secret: 919240143_41d9c3fac9a5f82e_KeyXtractor_ultimate
- name: "android" - name: "android"
device_name: andorid device_name: andorid
device_type: ANDROID device_type: ANDROID