From 8d8eb2d473e6ee9888dc40271c9d2b84c0cef376 Mon Sep 17 00:00:00 2001 From: Quinten0508 <55107945+Quinten0508@users.noreply.github.com> Date: Tue, 30 Jan 2024 19:19:04 +0100 Subject: [PATCH] added parallelization, minor bugfixes, episode numbering in filename --- .gitignore | 19 + README.md | 45 ++ cdm/__pycache__/wks.cpython-311.pyc | Bin 0 -> 57515 bytes cdm/wks.py | 842 ++++++++++++++++++++++++++++ cdrm.py | 35 ++ cdrm_api.py | 56 ++ cdrm_cache.py | 35 ++ extractwvd.py | 92 +++ gettoken.py | 83 +++ init_pssh.py | 66 +++ ism.py | 36 ++ main.py | 43 ++ main_dsnp.py | 58 ++ main_m3u8.py | 42 ++ npo all-in-one.py | 317 +++++++++++ npo.py | 135 +++++ npo_new.py | 78 +++ requirements.txt | 6 + 18 files changed, 1988 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cdm/__pycache__/wks.cpython-311.pyc create mode 100644 cdm/wks.py create mode 100644 cdrm.py create mode 100644 cdrm_api.py create mode 100644 cdrm_cache.py create mode 100644 extractwvd.py create mode 100644 gettoken.py create mode 100644 init_pssh.py create mode 100644 ism.py create mode 100644 main.py create mode 100644 main_dsnp.py create mode 100644 main_m3u8.py create mode 100644 npo all-in-one.py create mode 100644 npo.py create mode 100644 npo_new.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a9de84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Windows +Thumbs.db +desktop.ini + +# OS X +.DS_Store +.Spotlight-V100 +.Trashes +._* + +# Generic +.vscode/ + +# CDM +**/android_generic/** +N_m3u8DL-RE* +mp4decrypt* +env/ +avondshow.txt \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..03782a0 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# PyWKS + +

+ PyWKS Logo +

+ +PyWKS is a Python script designed for obtaining keys to decrypt encrypted videos. This repository contains an improved version of WKS-KEYS. + +> **⚠️ Warning:** +> +> I'm only uploading this for educational purposes. + +## Setup + +**1. Clone the repository:** + ```bash + git clone https://github.com/SASUKE-DUCK/pywks + ``` + +**2. Install dependencies:** + ```bash + pip install -r https://github.com/SASUKE-DUCK/pywks/blob/main/requirements.txt + ``` + +**3. Explore the example scripts:** + - `main.py` + - `main_dsnp.py` + + For detailed instructions, run the scripts with the `-h` flag. + +## Screenshots + +### `main.py` + +![main.py Screenshot](https://cdn.discordapp.com/attachments/826590534151700550/1168910480878870599/image.png?ex=65537bb7&is=654106b7&hm=2fb9262a79996ee463f8b64caf5495ba8b9bda3a5c57c295b3e30d655f58cd40&) + +### `main_dsnp.py` + +![main_dsnp.py Screenshot](https://cdn.discordapp.com/attachments/826590534151700550/1168910344941469767/image.png?ex=65537b97&is=65410697&hm=062bbcae4ead2976d94c812c83936bad99b40e1473090932639349bb2aee3e2d&) + +## Support and Community + +Join our Discord community for support, discussions, and updates! + +[![Discord](https://img.shields.io/discord/your-discord-server-id?color=%237289DA&label=Join%20us%20on%20Discord&logo=discord&logoColor=white)](https://discord.gg/utEG7CsAm5) diff --git a/cdm/__pycache__/wks.cpython-311.pyc b/cdm/__pycache__/wks.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8b20ad4e189b5c3faaf66bb9160bc0352655935 GIT binary patch literal 57515 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 literal 0 HcmV?d00001 diff --git a/cdm/wks.py b/cdm/wks.py new file mode 100644 index 0000000..e71e3cd --- /dev/null +++ b/cdm/wks.py @@ -0,0 +1,842 @@ +import binascii +import os +import base64 +from google.protobuf import descriptor as _descriptor, descriptor_pool as _descriptor_pool, symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +import os +import time +import binascii +import logging +import subprocess +import re +import base64 +import requests +from base64 import b64encode +from google.protobuf.message import DecodeError +from google.protobuf import text_format +import xmltodict +import base64 +import uuid +import requests +from Cryptodome.Random import get_random_bytes +from Cryptodome.Random import random +from Cryptodome.Cipher import PKCS1_OAEP, AES +from Cryptodome.Hash import CMAC, SHA256, HMAC, SHA1 +from Cryptodome.PublicKey import RSA +from Cryptodome.Signature import pss +from Cryptodome.Util import Padding +import logging +from bs4 import BeautifulSoup + +_sym_db = _symbol_database.Default() + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x0fwv_proto2.proto\"\xe7\x05\n\x14\x43lientIdentification\x12-\n\x04Type\x18\x01 \x02(\x0e\x32\x1f.ClientIdentification.TokenType\x12\'\n\x05Token\x18\x02 \x01(\x0b\x32\x18.SignedDeviceCertificate\x12\x33\n\nClientInfo\x18\x03 \x03(\x0b\x32\x1f.ClientIdentification.NameValue\x12\x1b\n\x13ProviderClientToken\x18\x04 \x01(\x0c\x12\x16\n\x0eLicenseCounter\x18\x05 \x01(\r\x12\x45\n\x13_ClientCapabilities\x18\x06 \x01(\x0b\x32(.ClientIdentification.ClientCapabilities\x12 \n\x0b_FileHashes\x18\x07 \x01(\x0b\x32\x0b.FileHashes\x1a(\n\tNameValue\x12\x0c\n\x04Name\x18\x01 \x02(\t\x12\r\n\x05Value\x18\x02 \x02(\t\x1a\xa4\x02\n\x12\x43lientCapabilities\x12\x13\n\x0b\x43lientToken\x18\x01 \x01(\r\x12\x14\n\x0cSessionToken\x18\x02 \x01(\r\x12\"\n\x1aVideoResolutionConstraints\x18\x03 \x01(\r\x12L\n\x0eMaxHdcpVersion\x18\x04 \x01(\x0e\x32\x34.ClientIdentification.ClientCapabilities.HdcpVersion\x12\x1b\n\x13OemCryptoApiVersion\x18\x05 \x01(\r\"T\n\x0bHdcpVersion\x12\r\n\tHDCP_NONE\x10\x00\x12\x0b\n\x07HDCP_V1\x10\x01\x12\x0b\n\x07HDCP_V2\x10\x02\x12\r\n\tHDCP_V2_1\x10\x03\x12\r\n\tHDCP_V2_2\x10\x04\"S\n\tTokenType\x12\n\n\x06KEYBOX\x10\x00\x12\x16\n\x12\x44\x45VICE_CERTIFICATE\x10\x01\x12\"\n\x1eREMOTE_ATTESTATION_CERTIFICATE\x10\x02\"\x9b\x02\n\x11\x44\x65viceCertificate\x12\x30\n\x04Type\x18\x01 \x02(\x0e\x32\".DeviceCertificate.CertificateType\x12\x14\n\x0cSerialNumber\x18\x02 \x01(\x0c\x12\x1b\n\x13\x43reationTimeSeconds\x18\x03 \x01(\r\x12\x11\n\tPublicKey\x18\x04 \x01(\x0c\x12\x10\n\x08SystemId\x18\x05 \x01(\r\x12\x1c\n\x14TestDeviceDeprecated\x18\x06 \x01(\r\x12\x11\n\tServiceId\x18\x07 \x01(\x0c\"K\n\x0f\x43\x65rtificateType\x12\x08\n\x04ROOT\x10\x00\x12\x10\n\x0cINTERMEDIATE\x10\x01\x12\x0f\n\x0bUSER_DEVICE\x10\x02\x12\x0b\n\x07SERVICE\x10\x03\"\xc4\x01\n\x17\x44\x65viceCertificateStatus\x12\x14\n\x0cSerialNumber\x18\x01 \x01(\x0c\x12:\n\x06Status\x18\x02 \x01(\x0e\x32*.DeviceCertificateStatus.CertificateStatus\x12*\n\nDeviceInfo\x18\x04 \x01(\x0b\x32\x16.ProvisionedDeviceInfo\"+\n\x11\x43\x65rtificateStatus\x12\t\n\x05VALID\x10\x00\x12\x0b\n\x07REVOKED\x10\x01\"o\n\x1b\x44\x65viceCertificateStatusList\x12\x1b\n\x13\x43reationTimeSeconds\x18\x01 \x01(\r\x12\x33\n\x11\x43\x65rtificateStatus\x18\x02 \x03(\x0b\x32\x18.DeviceCertificateStatus\"\xaf\x01\n\x1d\x45ncryptedClientIdentification\x12\x11\n\tServiceId\x18\x01 \x02(\t\x12&\n\x1eServiceCertificateSerialNumber\x18\x02 \x01(\x0c\x12\x19\n\x11\x45ncryptedClientId\x18\x03 \x02(\x0c\x12\x1b\n\x13\x45ncryptedClientIdIv\x18\x04 \x02(\x0c\x12\x1b\n\x13\x45ncryptedPrivacyKey\x18\x05 \x02(\x0c\"\x9c\x01\n\x15LicenseIdentification\x12\x11\n\tRequestId\x18\x01 \x01(\x0c\x12\x11\n\tSessionId\x18\x02 \x01(\x0c\x12\x12\n\nPurchaseId\x18\x03 \x01(\x0c\x12\x1a\n\x04Type\x18\x04 \x01(\x0e\x32\x0c.LicenseType\x12\x0f\n\x07Version\x18\x05 \x01(\r\x12\x1c\n\x14ProviderSessionToken\x18\x06 \x01(\x0c\"\xa1\x0e\n\x07License\x12\"\n\x02Id\x18\x01 \x01(\x0b\x32\x16.LicenseIdentification\x12 \n\x07_Policy\x18\x02 \x01(\x0b\x32\x0f.License.Policy\x12\"\n\x03Key\x18\x03 \x03(\x0b\x32\x15.License.KeyContainer\x12\x18\n\x10LicenseStartTime\x18\x04 \x01(\r\x12!\n\x19RemoteAttestationVerified\x18\x05 \x01(\r\x12\x1b\n\x13ProviderClientToken\x18\x06 \x01(\x0c\x12\x18\n\x10ProtectionScheme\x18\x07 \x01(\r\x1a\xbb\x02\n\x06Policy\x12\x0f\n\x07\x43\x61nPlay\x18\x01 \x01(\x08\x12\x12\n\nCanPersist\x18\x02 \x01(\x08\x12\x10\n\x08\x43\x61nRenew\x18\x03 \x01(\x08\x12\x1d\n\x15RentalDurationSeconds\x18\x04 \x01(\r\x12\x1f\n\x17PlaybackDurationSeconds\x18\x05 \x01(\r\x12\x1e\n\x16LicenseDurationSeconds\x18\x06 \x01(\r\x12&\n\x1eRenewalRecoveryDurationSeconds\x18\x07 \x01(\r\x12\x18\n\x10RenewalServerUrl\x18\x08 \x01(\t\x12\x1b\n\x13RenewalDelaySeconds\x18\t \x01(\r\x12#\n\x1bRenewalRetryIntervalSeconds\x18\n \x01(\r\x12\x16\n\x0eRenewWithUsage\x18\x0b \x01(\x08\x1a\xf9\t\n\x0cKeyContainer\x12\n\n\x02Id\x18\x01 \x01(\x0c\x12\n\n\x02Iv\x18\x02 \x01(\x0c\x12\x0b\n\x03Key\x18\x03 \x01(\x0c\x12+\n\x04Type\x18\x04 \x01(\x0e\x32\x1d.License.KeyContainer.KeyType\x12\x32\n\x05Level\x18\x05 \x01(\x0e\x32#.License.KeyContainer.SecurityLevel\x12\x42\n\x12RequiredProtection\x18\x06 \x01(\x0b\x32&.License.KeyContainer.OutputProtection\x12\x43\n\x13RequestedProtection\x18\x07 \x01(\x0b\x32&.License.KeyContainer.OutputProtection\x12\x35\n\x0b_KeyControl\x18\x08 \x01(\x0b\x32 .License.KeyContainer.KeyControl\x12[\n\x1e_OperatorSessionKeyPermissions\x18\t \x01(\x0b\x32\x33.License.KeyContainer.OperatorSessionKeyPermissions\x12S\n\x1aVideoResolutionConstraints\x18\n \x03(\x0b\x32/.License.KeyContainer.VideoResolutionConstraint\x1a\xdb\x01\n\x10OutputProtection\x12\x42\n\x04Hdcp\x18\x01 \x01(\x0e\x32\x34.ClientIdentification.ClientCapabilities.HdcpVersion\x12>\n\tCgmsFlags\x18\x02 \x01(\x0e\x32+.License.KeyContainer.OutputProtection.CGMS\"C\n\x04\x43GMS\x12\r\n\tCOPY_FREE\x10\x00\x12\r\n\tCOPY_ONCE\x10\x02\x12\x0e\n\nCOPY_NEVER\x10\x03\x12\r\n\tCGMS_NONE\x10*\x1a\x31\n\nKeyControl\x12\x17\n\x0fKeyControlBlock\x18\x01 \x02(\x0c\x12\n\n\x02Iv\x18\x02 \x01(\x0c\x1a|\n\x1dOperatorSessionKeyPermissions\x12\x14\n\x0c\x41llowEncrypt\x18\x01 \x01(\r\x12\x14\n\x0c\x41llowDecrypt\x18\x02 \x01(\r\x12\x11\n\tAllowSign\x18\x03 \x01(\r\x12\x1c\n\x14\x41llowSignatureVerify\x18\x04 \x01(\r\x1a\x99\x01\n\x19VideoResolutionConstraint\x12\x1b\n\x13MinResolutionPixels\x18\x01 \x01(\r\x12\x1b\n\x13MaxResolutionPixels\x18\x02 \x01(\r\x12\x42\n\x12RequiredProtection\x18\x03 \x01(\x0b\x32&.License.KeyContainer.OutputProtection\"J\n\x07KeyType\x12\x0b\n\x07SIGNING\x10\x01\x12\x0b\n\x07\x43ONTENT\x10\x02\x12\x0f\n\x0bKEY_CONTROL\x10\x03\x12\x14\n\x10OPERATOR_SESSION\x10\x04\"z\n\rSecurityLevel\x12\x14\n\x10SW_SECURE_CRYPTO\x10\x01\x12\x14\n\x10SW_SECURE_DECODE\x10\x02\x12\x14\n\x10HW_SECURE_CRYPTO\x10\x03\x12\x14\n\x10HW_SECURE_DECODE\x10\x04\x12\x11\n\rHW_SECURE_ALL\x10\x05\"\x98\x01\n\x0cLicenseError\x12&\n\tErrorCode\x18\x01 \x01(\x0e\x32\x13.LicenseError.Error\"`\n\x05\x45rror\x12\x1e\n\x1aINVALID_DEVICE_CERTIFICATE\x10\x01\x12\x1e\n\x1aREVOKED_DEVICE_CERTIFICATE\x10\x02\x12\x17\n\x13SERVICE_UNAVAILABLE\x10\x03\"\xac\x07\n\x0eLicenseRequest\x12\'\n\x08\x43lientId\x18\x01 \x01(\x0b\x32\x15.ClientIdentification\x12\x38\n\tContentId\x18\x02 \x01(\x0b\x32%.LicenseRequest.ContentIdentification\x12)\n\x04Type\x18\x03 \x01(\x0e\x32\x1b.LicenseRequest.RequestType\x12\x13\n\x0bRequestTime\x18\x04 \x01(\r\x12!\n\x19KeyControlNonceDeprecated\x18\x05 \x01(\x0c\x12)\n\x0fProtocolVersion\x18\x06 \x01(\x0e\x32\x10.ProtocolVersion\x12\x17\n\x0fKeyControlNonce\x18\x07 \x01(\r\x12\x39\n\x11\x45ncryptedClientId\x18\x08 \x01(\x0b\x32\x1e.EncryptedClientIdentification\x1a\xa2\x04\n\x15\x43ontentIdentification\x12:\n\x06\x43\x65ncId\x18\x01 \x01(\x0b\x32*.LicenseRequest.ContentIdentification.CENC\x12:\n\x06WebmId\x18\x02 \x01(\x0b\x32*.LicenseRequest.ContentIdentification.WebM\x12\x46\n\x07License\x18\x03 \x01(\x0b\x32\x35.LicenseRequest.ContentIdentification.ExistingLicense\x1a_\n\x04\x43\x45NC\x12!\n\x04Pssh\x18\x01 \x01(\x0b\x32\x13.WidevineCencHeader\x12!\n\x0bLicenseType\x18\x02 \x01(\x0e\x32\x0c.LicenseType\x12\x11\n\tRequestId\x18\x03 \x01(\x0c\x1aL\n\x04WebM\x12\x0e\n\x06Header\x18\x01 \x01(\x0c\x12!\n\x0bLicenseType\x18\x02 \x01(\x0e\x32\x0c.LicenseType\x12\x11\n\tRequestId\x18\x03 \x01(\x0c\x1a\x99\x01\n\x0f\x45xistingLicense\x12)\n\tLicenseId\x18\x01 \x01(\x0b\x32\x16.LicenseIdentification\x12\x1b\n\x13SecondsSinceStarted\x18\x02 \x01(\r\x12\x1e\n\x16SecondsSinceLastPlayed\x18\x03 \x01(\r\x12\x1e\n\x16SessionUsageTableEntry\x18\x04 \x01(\x0c\"0\n\x0bRequestType\x12\x07\n\x03NEW\x10\x01\x12\x0b\n\x07RENEWAL\x10\x02\x12\x0b\n\x07RELEASE\x10\x03\"\xa9\x07\n\x11LicenseRequestRaw\x12\'\n\x08\x43lientId\x18\x01 \x01(\x0b\x32\x15.ClientIdentification\x12;\n\tContentId\x18\x02 \x01(\x0b\x32(.LicenseRequestRaw.ContentIdentification\x12,\n\x04Type\x18\x03 \x01(\x0e\x32\x1e.LicenseRequestRaw.RequestType\x12\x13\n\x0bRequestTime\x18\x04 \x01(\r\x12!\n\x19KeyControlNonceDeprecated\x18\x05 \x01(\x0c\x12)\n\x0fProtocolVersion\x18\x06 \x01(\x0e\x32\x10.ProtocolVersion\x12\x17\n\x0fKeyControlNonce\x18\x07 \x01(\r\x12\x39\n\x11\x45ncryptedClientId\x18\x08 \x01(\x0b\x32\x1e.EncryptedClientIdentification\x1a\x96\x04\n\x15\x43ontentIdentification\x12=\n\x06\x43\x65ncId\x18\x01 \x01(\x0b\x32-.LicenseRequestRaw.ContentIdentification.CENC\x12=\n\x06WebmId\x18\x02 \x01(\x0b\x32-.LicenseRequestRaw.ContentIdentification.WebM\x12I\n\x07License\x18\x03 \x01(\x0b\x32\x38.LicenseRequestRaw.ContentIdentification.ExistingLicense\x1aJ\n\x04\x43\x45NC\x12\x0c\n\x04Pssh\x18\x01 \x01(\x0c\x12!\n\x0bLicenseType\x18\x02 \x01(\x0e\x32\x0c.LicenseType\x12\x11\n\tRequestId\x18\x03 \x01(\x0c\x1aL\n\x04WebM\x12\x0e\n\x06Header\x18\x01 \x01(\x0c\x12!\n\x0bLicenseType\x18\x02 \x01(\x0e\x32\x0c.LicenseType\x12\x11\n\tRequestId\x18\x03 \x01(\x0c\x1a\x99\x01\n\x0f\x45xistingLicense\x12)\n\tLicenseId\x18\x01 \x01(\x0b\x32\x16.LicenseIdentification\x12\x1b\n\x13SecondsSinceStarted\x18\x02 \x01(\r\x12\x1e\n\x16SecondsSinceLastPlayed\x18\x03 \x01(\r\x12\x1e\n\x16SessionUsageTableEntry\x18\x04 \x01(\x0c\"0\n\x0bRequestType\x12\x07\n\x03NEW\x10\x01\x12\x0b\n\x07RENEWAL\x10\x02\x12\x0b\n\x07RELEASE\x10\x03\"\xa6\x02\n\x15ProvisionedDeviceInfo\x12\x10\n\x08SystemId\x18\x01 \x01(\r\x12\x0b\n\x03Soc\x18\x02 \x01(\t\x12\x14\n\x0cManufacturer\x18\x03 \x01(\t\x12\r\n\x05Model\x18\x04 \x01(\t\x12\x12\n\nDeviceType\x18\x05 \x01(\t\x12\x11\n\tModelYear\x18\x06 \x01(\r\x12=\n\rSecurityLevel\x18\x07 \x01(\x0e\x32&.ProvisionedDeviceInfo.WvSecurityLevel\x12\x12\n\nTestDevice\x18\x08 \x01(\r\"O\n\x0fWvSecurityLevel\x12\x15\n\x11LEVEL_UNSPECIFIED\x10\x00\x12\x0b\n\x07LEVEL_1\x10\x01\x12\x0b\n\x07LEVEL_2\x10\x02\x12\x0b\n\x07LEVEL_3\x10\x03\"\x15\n\x13ProvisioningOptions\"\x15\n\x13ProvisioningRequest\"\x16\n\x14ProvisioningResponse\"i\n\x11RemoteAttestation\x12\x33\n\x0b\x43\x65rtificate\x18\x01 \x01(\x0b\x32\x1e.EncryptedClientIdentification\x12\x0c\n\x04Salt\x18\x02 \x01(\t\x12\x11\n\tSignature\x18\x03 \x01(\t\"\r\n\x0bSessionInit\"\x0e\n\x0cSessionState\"\x1d\n\x1bSignedCertificateStatusList\"\x86\x01\n\x17SignedDeviceCertificate\x12.\n\x12_DeviceCertificate\x18\x01 \x01(\x0b\x32\x12.DeviceCertificate\x12\x11\n\tSignature\x18\x02 \x01(\x0c\x12(\n\x06Signer\x18\x03 \x01(\x0b\x32\x18.SignedDeviceCertificate\"\x1b\n\x19SignedProvisioningMessage\"\x9b\x02\n\rSignedMessage\x12(\n\x04Type\x18\x01 \x01(\x0e\x32\x1a.SignedMessage.MessageType\x12\x0b\n\x03Msg\x18\x02 \x01(\x0c\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xc5\x02\n\x12WidevineCencHeader\x12\x30\n\talgorithm\x18\x01 \x01(\x0e\x32\x1d.WidevineCencHeader.Algorithm\x12\x0e\n\x06key_id\x18\x02 \x03(\x0c\x12\x10\n\x08provider\x18\x03 \x01(\t\x12\x12\n\ncontent_id\x18\x04 \x01(\x0c\x12\x1d\n\x15track_type_deprecated\x18\x05 \x01(\t\x12\x0e\n\x06policy\x18\x06 \x01(\t\x12\x1b\n\x13\x63rypto_period_index\x18\x07 \x01(\r\x12\x17\n\x0fgrouped_license\x18\x08 \x01(\x0c\x12\x19\n\x11protection_scheme\x18\t \x01(\r\x12\x1d\n\x15\x63rypto_period_seconds\x18\n \x01(\r\"(\n\tAlgorithm\x12\x0f\n\x0bUNENCRYPTED\x10\x00\x12\n\n\x06\x41\x45SCTR\x10\x01\"\xba\x02\n\x14SignedLicenseRequest\x12/\n\x04Type\x18\x01 \x01(\x0e\x32!.SignedLicenseRequest.MessageType\x12\x1c\n\x03Msg\x18\x02 \x01(\x0b\x32\x0f.LicenseRequest\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xc3\x02\n\x17SignedLicenseRequestRaw\x12\x32\n\x04Type\x18\x01 \x01(\x0e\x32$.SignedLicenseRequestRaw.MessageType\x12\x1f\n\x03Msg\x18\x02 \x01(\x0b\x32\x12.LicenseRequestRaw\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xa5\x02\n\rSignedLicense\x12(\n\x04Type\x18\x01 \x01(\x0e\x32\x1a.SignedLicense.MessageType\x12\x15\n\x03Msg\x18\x02 \x01(\x0b\x32\x08.License\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xcb\x02\n\x18SignedServiceCertificate\x12\x33\n\x04Type\x18\x01 \x01(\x0e\x32%.SignedServiceCertificate.MessageType\x12%\n\x03Msg\x18\x02 \x01(\x0b\x32\x18.SignedDeviceCertificate\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xb5\x01\n\nFileHashes\x12\x0e\n\x06signer\x18\x01 \x01(\x0c\x12)\n\nsignatures\x18\x02 \x03(\x0b\x32\x15.FileHashes.Signature\x1al\n\tSignature\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x14\n\x0ctest_signing\x18\x02 \x01(\x08\x12\x12\n\nSHA512Hash\x18\x03 \x01(\x0c\x12\x10\n\x08main_exe\x18\x04 \x01(\x08\x12\x11\n\tsignature\x18\x05 \x01(\x0c*1\n\x0bLicenseType\x12\x08\n\x04ZERO\x10\x00\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x01\x12\x0b\n\x07OFFLINE\x10\x02*\x1e\n\x0fProtocolVersion\x12\x0b\n\x07\x43URRENT\x10\x15') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'wv_proto2_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _LICENSETYPE._serialized_start = 8339 + _LICENSETYPE._serialized_end = 8388 + _PROTOCOLVERSION._serialized_start = 8390 + _PROTOCOLVERSION._serialized_end = 8420 + _CLIENTIDENTIFICATION._serialized_start = 20 + _CLIENTIDENTIFICATION._serialized_end = 763 + _CLIENTIDENTIFICATION_NAMEVALUE._serialized_start = 343 + _CLIENTIDENTIFICATION_NAMEVALUE._serialized_end = 383 + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES._serialized_start = 386 + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES._serialized_end = 678 + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION._serialized_start = 594 + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION._serialized_end = 678 + _CLIENTIDENTIFICATION_TOKENTYPE._serialized_start = 680 + _CLIENTIDENTIFICATION_TOKENTYPE._serialized_end = 763 + _DEVICECERTIFICATE._serialized_start = 766 + _DEVICECERTIFICATE._serialized_end = 1049 + _DEVICECERTIFICATE_CERTIFICATETYPE._serialized_start = 974 + _DEVICECERTIFICATE_CERTIFICATETYPE._serialized_end = 1049 + _DEVICECERTIFICATESTATUS._serialized_start = 1052 + _DEVICECERTIFICATESTATUS._serialized_end = 1248 + _DEVICECERTIFICATESTATUS_CERTIFICATESTATUS._serialized_start = 1205 + _DEVICECERTIFICATESTATUS_CERTIFICATESTATUS._serialized_end = 1248 + _DEVICECERTIFICATESTATUSLIST._serialized_start = 1250 + _DEVICECERTIFICATESTATUSLIST._serialized_end = 1361 + _ENCRYPTEDCLIENTIDENTIFICATION._serialized_start = 1364 + _ENCRYPTEDCLIENTIDENTIFICATION._serialized_end = 1539 + _LICENSEIDENTIFICATION._serialized_start = 1542 + _LICENSEIDENTIFICATION._serialized_end = 1698 + _LICENSE._serialized_start = 1701 + _LICENSE._serialized_end = 3526 + _LICENSE_POLICY._serialized_start = 1935 + _LICENSE_POLICY._serialized_end = 2250 + _LICENSE_KEYCONTAINER._serialized_start = 2253 + _LICENSE_KEYCONTAINER._serialized_end = 3526 + _LICENSE_KEYCONTAINER_OUTPUTPROTECTION._serialized_start = 2774 + _LICENSE_KEYCONTAINER_OUTPUTPROTECTION._serialized_end = 2993 + _LICENSE_KEYCONTAINER_OUTPUTPROTECTION_CGMS._serialized_start = 2926 + _LICENSE_KEYCONTAINER_OUTPUTPROTECTION_CGMS._serialized_end = 2993 + _LICENSE_KEYCONTAINER_KEYCONTROL._serialized_start = 2995 + _LICENSE_KEYCONTAINER_KEYCONTROL._serialized_end = 3044 + _LICENSE_KEYCONTAINER_OPERATORSESSIONKEYPERMISSIONS._serialized_start = 3046 + _LICENSE_KEYCONTAINER_OPERATORSESSIONKEYPERMISSIONS._serialized_end = 3170 + _LICENSE_KEYCONTAINER_VIDEORESOLUTIONCONSTRAINT._serialized_start = 3173 + _LICENSE_KEYCONTAINER_VIDEORESOLUTIONCONSTRAINT._serialized_end = 3326 + _LICENSE_KEYCONTAINER_KEYTYPE._serialized_start = 3328 + _LICENSE_KEYCONTAINER_KEYTYPE._serialized_end = 3402 + _LICENSE_KEYCONTAINER_SECURITYLEVEL._serialized_start = 3404 + _LICENSE_KEYCONTAINER_SECURITYLEVEL._serialized_end = 3526 + _LICENSEERROR._serialized_start = 3529 + _LICENSEERROR._serialized_end = 3681 + _LICENSEERROR_ERROR._serialized_start = 3585 + _LICENSEERROR_ERROR._serialized_end = 3681 + _LICENSEREQUEST._serialized_start = 3684 + _LICENSEREQUEST._serialized_end = 4624 + _LICENSEREQUEST_CONTENTIDENTIFICATION._serialized_start = 4028 + _LICENSEREQUEST_CONTENTIDENTIFICATION._serialized_end = 4574 + _LICENSEREQUEST_CONTENTIDENTIFICATION_CENC._serialized_start = 4245 + _LICENSEREQUEST_CONTENTIDENTIFICATION_CENC._serialized_end = 4340 + _LICENSEREQUEST_CONTENTIDENTIFICATION_WEBM._serialized_start = 4342 + _LICENSEREQUEST_CONTENTIDENTIFICATION_WEBM._serialized_end = 4418 + _LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE._serialized_start = 4421 + _LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE._serialized_end = 4574 + _LICENSEREQUEST_REQUESTTYPE._serialized_start = 4576 + _LICENSEREQUEST_REQUESTTYPE._serialized_end = 4624 + _LICENSEREQUESTRAW._serialized_start = 4627 + _LICENSEREQUESTRAW._serialized_end = 5564 + _LICENSEREQUESTRAW_CONTENTIDENTIFICATION._serialized_start = 4980 + _LICENSEREQUESTRAW_CONTENTIDENTIFICATION._serialized_end = 5514 + _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_CENC._serialized_start = 5206 + _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_CENC._serialized_end = 5280 + _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_WEBM._serialized_start = 4342 + _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_WEBM._serialized_end = 4418 + _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_EXISTINGLICENSE._serialized_start = 4421 + _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_EXISTINGLICENSE._serialized_end = 4574 + _LICENSEREQUESTRAW_REQUESTTYPE._serialized_start = 4576 + _LICENSEREQUESTRAW_REQUESTTYPE._serialized_end = 4624 + _PROVISIONEDDEVICEINFO._serialized_start = 5567 + _PROVISIONEDDEVICEINFO._serialized_end = 5861 + _PROVISIONEDDEVICEINFO_WVSECURITYLEVEL._serialized_start = 5782 + _PROVISIONEDDEVICEINFO_WVSECURITYLEVEL._serialized_end = 5861 + _PROVISIONINGOPTIONS._serialized_start = 5863 + _PROVISIONINGOPTIONS._serialized_end = 5884 + _PROVISIONINGREQUEST._serialized_start = 5886 + _PROVISIONINGREQUEST._serialized_end = 5907 + _PROVISIONINGRESPONSE._serialized_start = 5909 + _PROVISIONINGRESPONSE._serialized_end = 5931 + _REMOTEATTESTATION._serialized_start = 5933 + _REMOTEATTESTATION._serialized_end = 6038 + _SESSIONINIT._serialized_start = 6040 + _SESSIONINIT._serialized_end = 6053 + _SESSIONSTATE._serialized_start = 6055 + _SESSIONSTATE._serialized_end = 6069 + _SIGNEDCERTIFICATESTATUSLIST._serialized_start = 6071 + _SIGNEDCERTIFICATESTATUSLIST._serialized_end = 6100 + _SIGNEDDEVICECERTIFICATE._serialized_start = 6103 + _SIGNEDDEVICECERTIFICATE._serialized_end = 6237 + _SIGNEDPROVISIONINGMESSAGE._serialized_start = 6239 + _SIGNEDPROVISIONINGMESSAGE._serialized_end = 6266 + _SIGNEDMESSAGE._serialized_start = 6269 + _SIGNEDMESSAGE._serialized_end = 6552 + _SIGNEDMESSAGE_MESSAGETYPE._serialized_start = 6427 + _SIGNEDMESSAGE_MESSAGETYPE._serialized_end = 6552 + _WIDEVINECENCHEADER._serialized_start = 6555 + _WIDEVINECENCHEADER._serialized_end = 6880 + _WIDEVINECENCHEADER_ALGORITHM._serialized_start = 6840 + _WIDEVINECENCHEADER_ALGORITHM._serialized_end = 6880 + _SIGNEDLICENSEREQUEST._serialized_start = 6883 + _SIGNEDLICENSEREQUEST._serialized_end = 7197 + _SIGNEDLICENSEREQUEST_MESSAGETYPE._serialized_start = 6427 + _SIGNEDLICENSEREQUEST_MESSAGETYPE._serialized_end = 6552 + _SIGNEDLICENSEREQUESTRAW._serialized_start = 7200 + _SIGNEDLICENSEREQUESTRAW._serialized_end = 7523 + _SIGNEDLICENSEREQUESTRAW_MESSAGETYPE._serialized_start = 6427 + _SIGNEDLICENSEREQUESTRAW_MESSAGETYPE._serialized_end = 6552 + _SIGNEDLICENSE._serialized_start = 7526 + _SIGNEDLICENSE._serialized_end = 7819 + _SIGNEDLICENSE_MESSAGETYPE._serialized_start = 6427 + _SIGNEDLICENSE_MESSAGETYPE._serialized_end = 6552 + _SIGNEDSERVICECERTIFICATE._serialized_start = 7822 + _SIGNEDSERVICECERTIFICATE._serialized_end = 8153 + _SIGNEDSERVICECERTIFICATE_MESSAGETYPE._serialized_start = 6427 + _SIGNEDSERVICECERTIFICATE_MESSAGETYPE._serialized_end = 6552 + _FILEHASHES._serialized_start = 8156 + _FILEHASHES._serialized_end = 8337 + _FILEHASHES_SIGNATURE._serialized_start = 8229 + _FILEHASHES_SIGNATURE._serialized_end = 8337 +# @@protoc_insertion_point(module_scope) + +class Session: + def __init__(self, session_id, init_data, device_config, offline): + self.session_id = session_id + self.init_data = init_data + self.offline = offline + self.device_config = device_config + self.device_key = None + self.session_key = None + self.derived_keys = { + 'enc': None, + 'auth_1': None, + 'auth_2': None + } + self.license_request = None + self.license = None + self.service_certificate = None + self.privacy_mode = False + self.keys = [] + +class Key: + def __init__(self, kid, type, key, permissions=[]): + self.kid = kid + self.type = type + self.key = key + self.permissions = permissions + + def __repr__(self): + if self.type == "OPERATOR_SESSION": + return "key(kid={}, type={}, key={}, permissions={})".format(self.kid, self.type, binascii.hexlify(self.key), self.permissions) + else: + return "key(kid={}, type={}, key={})".format(self.kid, self.type, binascii.hexlify(self.key)) + +try: + from google.protobuf.internal.decoder import _DecodeVarint as _di +except ImportError: + def LEB128_decode(buffer, pos, limit=64): + result = 0 + shift = 0 + while True: + b = buffer[pos] + pos += 1 + result |= ((b & 0x7F) << shift) + if not (b & 0x80): + return (result, pos) + shift += 7 + if shift > limit: + raise Exception("integer too large, shift: {}".format(shift)) + _di = LEB128_decode + +class FromFileMixin: + @classmethod + def from_file(cls, filename): + with open(filename, "rb") as f: + return cls(f.read()) + +class VariableReader(FromFileMixin): + def __init__(self, buf): + self.buf = buf + self.pos = 0 + self.size = len(buf) + + def read_int(self): + (val, nextpos) = _di(self.buf, self.pos) + self.pos = nextpos + return val + + def read_bytes_raw(self, size): + b = self.buf[self.pos:self.pos+size] + self.pos += size + return b + + def read_bytes(self): + size = self.read_int() + return self.read_bytes_raw(size) + + def is_end(self): + return (self.size == self.pos) + +class TaggedReader(VariableReader): + def read_tag(self): + return (self.read_int(), self.read_bytes()) + + def read_all_tags(self, max_tag=3): + tags = {} + while (not self.is_end()): + (tag, bytes) = self.read_tag() + if (tag > max_tag): + raise IndexError("tag out of bound: got {}, max {}".format(tag, max_tag)) + tags[tag] = bytes + return tags + +class WideVineSignatureReader(FromFileMixin): + SIGNER_TAG = 1 + SIGNATURE_TAG = 2 + ISMAINEXE_TAG = 3 + + def __init__(self, buf): + reader = TaggedReader(buf) + self.version = reader.read_int() + if (self.version != 0): + raise Exception("Unsupported signature format version {}".format(self.version)) + self.tags = reader.read_all_tags() + + self.signer = self.tags[self.SIGNER_TAG] + self.signature = self.tags[self.SIGNATURE_TAG] + + extra = self.tags[self.ISMAINEXE_TAG] + if (len(extra) != 1 or (extra[0] > 1)): + raise Exception("Unexpected 'ismainexe' field value (not '\\x00' or '\\x01'), please check: {0}".format(extra)) + + self.mainexe = bool(extra[0]) + + @classmethod + def get_tags(cls, filename): + return cls.from_file(filename).tags + +device_android_generic = { + 'name': 'android_generic', + 'description': 'android studio cdm', + 'security_level': 3, + 'session_id_type': 'android', + 'private_key_available': True, + 'vmp': False, + 'send_key_control_nonce': True +} +devices_available = [device_android_generic] + +FILES_FOLDER = 'devices' + +class DeviceConfig: + def __init__(self, device): + self.device_name = device['name'] + self.description = device['description'] + self.security_level = device['security_level'] + self.session_id_type = device['session_id_type'] + self.private_key_available = device['private_key_available'] + self.vmp = device['vmp'] + self.send_key_control_nonce = device['send_key_control_nonce'] + + if 'keybox_filename' in device: + self.keybox_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['keybox_filename']) + else: + self.keybox_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'keybox') + + if 'device_cert_filename' in device: + self.device_cert_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_cert_filename']) + else: + self.device_cert_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_cert') + + if 'device_private_key_filename' in device: + self.device_private_key_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_private_key_filename']) + else: + self.device_private_key_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_private_key') + + if 'device_client_id_blob_filename' in device: + self.device_client_id_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_client_id_blob_filename']) + else: + self.device_client_id_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_client_id_blob') + + if 'device_vmp_blob_filename' in device: + self.device_vmp_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_vmp_blob_filename']) + else: + self.device_vmp_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_vmp_blob') + + def __repr__(self): + return "DeviceConfig(name={}, description={}, security_level={}, session_id_type={}, private_key_available={}, vmp={})".format(self.device_name, self.description, self.security_level, self.session_id_type, self.private_key_available, self.vmp) + +class Cdm: + def __init__(self): + self.logger = logging.getLogger(__name__) + self.sessions = {} + + def open_session(self, init_data_b64, device, raw_init_data = None, offline=False): + if device.session_id_type == 'android': + # format: 16 random hexdigits, 2 digit counter, 14 0s + rand_ascii = ''.join(random.choice('ABCDEF0123456789') for _ in range(16)) + counter = '01' # this resets regularly so its fine to use 01 + rest = '00000000000000' + session_id = rand_ascii + counter + rest + session_id = session_id.encode('ascii') + elif device.session_id_type == 'chrome': + rand_bytes = get_random_bytes(16) + session_id = rand_bytes + else: + # other formats NYI + return 1 + if raw_init_data and isinstance(raw_init_data, (bytes, bytearray)): + # used for NF key exchange, where they don't provide a valid PSSH + init_data = raw_init_data + self.raw_pssh = True + else: + init_data = self._parse_init_data(init_data_b64) + self.raw_pssh = False + + if init_data: + new_session = Session(session_id, init_data, device, offline) + else: + return 1 + self.sessions[session_id] = new_session + return session_id + + def _parse_init_data(self, init_data_b64): + parsed_init_data = WidevineCencHeader() + try: + parsed_init_data.ParseFromString(base64.b64decode(init_data_b64)[32:]) + except DecodeError: + try: + id_bytes = parsed_init_data.ParseFromString(base64.b64decode(init_data_b64)[32:]) + except DecodeError: + return None + return parsed_init_data + + def close_session(self, session_id): + if session_id in self.sessions: + self.sessions.pop(session_id) + return 0 + else: + return 1 + + def set_service_certificate(self, session_id, cert_b64): + + if session_id not in self.sessions: + return 1 + + session = self.sessions[session_id] + + message = SignedMessage() + + try: + message.ParseFromString(base64.b64decode(cert_b64)) + except DecodeError: + self.logger.error("failed to parse cert as SignedMessage") + + service_certificate = SignedDeviceCertificate() + + if message.Type: + try: + service_certificate.ParseFromString(message.Msg) + except DecodeError: + return 1 + else: + try: + service_certificate.ParseFromString(base64.b64decode(cert_b64)) + except DecodeError: + return 1 + + session.service_certificate = service_certificate + session.privacy_mode = True + + return 0 + + def get_license_request(self, session_id): + + if session_id not in self.sessions: + return 1 + + session = self.sessions[session_id] + + # raw pssh will be treated as bytes and not parsed + if self.raw_pssh: + license_request = SignedLicenseRequestRaw() + else: + license_request = SignedLicenseRequest() + client_id = ClientIdentification() + + if not os.path.exists(session.device_config.device_client_id_blob_filename): + return 1 + + with open(session.device_config.device_client_id_blob_filename, "rb") as f: + try: + cid_bytes = client_id.ParseFromString(f.read()) + except DecodeError: + return 1 + + if not self.raw_pssh: + license_request.Type = SignedLicenseRequest.MessageType.Value('LICENSE_REQUEST') + license_request.Msg.ContentId.CencId.Pssh.CopyFrom(session.init_data) + else: + license_request.Type = SignedLicenseRequestRaw.MessageType.Value('LICENSE_REQUEST') + license_request.Msg.ContentId.CencId.Pssh = session.init_data # bytes + + if session.offline: + license_type = LicenseType.Value('OFFLINE') + else: + license_type = LicenseType.Value('DEFAULT') + license_request.Msg.ContentId.CencId.LicenseType = license_type + license_request.Msg.ContentId.CencId.RequestId = session_id + license_request.Msg.Type = LicenseRequest.RequestType.Value('NEW') + license_request.Msg.RequestTime = int(time.time()) + license_request.Msg.ProtocolVersion = ProtocolVersion.Value('CURRENT') + if session.device_config.send_key_control_nonce: + license_request.Msg.KeyControlNonce = random.randrange(1, 2**31) + + if session.privacy_mode: + if session.device_config.vmp: + vmp_hashes = FileHashes() + with open(session.device_config.device_vmp_blob_filename, "rb") as f: + try: + vmp_bytes = vmp_hashes.ParseFromString(f.read()) + except DecodeError: + return 1 + client_id._FileHashes.CopyFrom(vmp_hashes) + cid_aes_key = get_random_bytes(16) + cid_iv = get_random_bytes(16) + + cid_cipher = AES.new(cid_aes_key, AES.MODE_CBC, cid_iv) + + encrypted_client_id = cid_cipher.encrypt(Padding.pad(client_id.SerializeToString(), 16)) + + service_public_key = RSA.importKey(session.service_certificate._DeviceCertificate.PublicKey) + + service_cipher = PKCS1_OAEP.new(service_public_key) + + encrypted_cid_key = service_cipher.encrypt(cid_aes_key) + + encrypted_client_id_proto = EncryptedClientIdentification() + + encrypted_client_id_proto.ServiceId = session.service_certificate._DeviceCertificate.ServiceId + encrypted_client_id_proto.ServiceCertificateSerialNumber = session.service_certificate._DeviceCertificate.SerialNumber + encrypted_client_id_proto.EncryptedClientId = encrypted_client_id + encrypted_client_id_proto.EncryptedClientIdIv = cid_iv + encrypted_client_id_proto.EncryptedPrivacyKey = encrypted_cid_key + + license_request.Msg.EncryptedClientId.CopyFrom(encrypted_client_id_proto) + else: + license_request.Msg.ClientId.CopyFrom(client_id) + + if session.device_config.private_key_available: + key = RSA.importKey(open(session.device_config.device_private_key_filename).read()) + session.device_key = key + else: + return 1 + + + hash = SHA1.new(license_request.Msg.SerializeToString()) + signature = pss.new(key).sign(hash) + + license_request.Signature = signature + + session.license_request = license_request + + return license_request.SerializeToString() + + def provide_license(self, session_id, license_b64): + + if session_id not in self.sessions: + return 1 + + session = self.sessions[session_id] + + if not session.license_request: + return 1 + + license = SignedLicense() + try: + license.ParseFromString(base64.b64decode(license_b64)) + except DecodeError: + self.logger.error("unable to parse license - check protobufs") + return 1 + + session.license = license + + oaep_cipher = PKCS1_OAEP.new(session.device_key) + + session.session_key = oaep_cipher.decrypt(license.SessionKey) + + lic_req_msg = session.license_request.Msg.SerializeToString() + + enc_key_base = b"ENCRYPTION\000" + lic_req_msg + b"\0\0\0\x80" + auth_key_base = b"AUTHENTICATION\0" + lic_req_msg + b"\0\0\2\0" + + enc_key = b"\x01" + enc_key_base + auth_key_1 = b"\x01" + auth_key_base + auth_key_2 = b"\x02" + auth_key_base + auth_key_3 = b"\x03" + auth_key_base + auth_key_4 = b"\x04" + auth_key_base + + cmac_obj = CMAC.new(session.session_key, ciphermod=AES) + cmac_obj.update(enc_key) + + enc_cmac_key = cmac_obj.digest() + + cmac_obj = CMAC.new(session.session_key, ciphermod=AES) + cmac_obj.update(auth_key_1) + auth_cmac_key_1 = cmac_obj.digest() + + cmac_obj = CMAC.new(session.session_key, ciphermod=AES) + cmac_obj.update(auth_key_2) + auth_cmac_key_2 = cmac_obj.digest() + + cmac_obj = CMAC.new(session.session_key, ciphermod=AES) + cmac_obj.update(auth_key_3) + auth_cmac_key_3 = cmac_obj.digest() + + cmac_obj = CMAC.new(session.session_key, ciphermod=AES) + cmac_obj.update(auth_key_4) + auth_cmac_key_4 = cmac_obj.digest() + + auth_cmac_combined_1 = auth_cmac_key_1 + auth_cmac_key_2 + auth_cmac_combined_2 = auth_cmac_key_3 + auth_cmac_key_4 + + session.derived_keys['enc'] = enc_cmac_key + session.derived_keys['auth_1'] = auth_cmac_combined_1 + session.derived_keys['auth_2'] = auth_cmac_combined_2 + + lic_hmac = HMAC.new(session.derived_keys['auth_1'], digestmod=SHA256) + lic_hmac.update(license.Msg.SerializeToString()) + + if lic_hmac.digest() != license.Signature: + with open("original_lic.bin", "wb") as f: + f.write(base64.b64decode(license_b64)) + with open("parsed_lic.bin", "wb") as f: + f.write(license.SerializeToString()) + for key in license.Msg.Key: + if key.Id: + key_id = key.Id + else: + key_id = License.KeyContainer.KeyType.Name(key.Type).encode('utf-8') + encrypted_key = key.Key + iv = key.Iv + type = License.KeyContainer.KeyType.Name(key.Type) + + cipher = AES.new(session.derived_keys['enc'], AES.MODE_CBC, iv=iv) + decrypted_key = cipher.decrypt(encrypted_key) + if type == "OPERATOR_SESSION": + permissions = [] + perms = key._OperatorSessionKeyPermissions + for (descriptor, value) in perms.ListFields(): + if value == 1: + permissions.append(descriptor.name) + print(permissions) + else: + permissions = [] + session.keys.append(Key(key_id, type, Padding.unpad(decrypted_key, 16), permissions)) + return 0 + + def get_keys(self, session_id): + if session_id in self.sessions: + return self.sessions[session_id].keys + +class WvDecrypt(object): + WV_SYSTEM_ID = [ + 237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237] + + def __init__(self, init_data_b64, cert_data_b64, device): + self.init_data_b64 = init_data_b64 + self.cert_data_b64 = cert_data_b64 + self.device = device + self.cdm = Cdm() + + def check_pssh(pssh_b64): + pssh = base64.b64decode(pssh_b64) + if not pssh[12:28] == bytes(self.WV_SYSTEM_ID): + new_pssh = bytearray([0, 0, 0]) + new_pssh.append(32 + len(pssh)) + new_pssh[4:] = bytearray(b'pssh') + new_pssh[8:] = [0, 0, 0, 0] + new_pssh[13:] = self.WV_SYSTEM_ID + new_pssh[29:] = [0, 0, 0, 0] + new_pssh[31] = len(pssh) + new_pssh[32:] = pssh + return base64.b64encode(new_pssh) + else: + return pssh_b64 + + self.session = self.cdm.open_session(check_pssh(self.init_data_b64), DeviceConfig(self.device)) + if self.cert_data_b64: + self.cdm.set_service_certificate(self.session, self.cert_data_b64) + + def log_message(self, msg): + return '{}'.format(msg) + + def start_process(self): + keyswvdecrypt = [] + try: + for key in self.cdm.get_keys(self.session): + if key.type == 'CONTENT': + keyswvdecrypt.append(self.log_message('{}:{}'.format(key.kid.hex(), key.key.hex()))) + + except Exception: + return ( + False, keyswvdecrypt) + else: + return ( + True, keyswvdecrypt) + + def get_challenge(self): + return self.cdm.get_license_request(self.session) + + def update_license(self, license_b64): + self.cdm.provide_license(self.session, license_b64) + return True + +class PsshExtractor: + def __init__(self, response_text): + self.response_text = response_text + + def extract_pssh(self): + pssh_match = re.search(r'.*?(.*?)', self.response_text, re.DOTALL) + + if pssh_match: + return pssh_match.group(1) + else: + cenc_default_kid_match = re.search(r'cenc:default_KID="([^"]+)"', self.response_text) + if cenc_default_kid_match: + kid = cenc_default_kid_match.group(1) + array_of_bytes = bytearray(b'\x00\x00\x002pssh\x00\x00\x00\x00') + array_of_bytes.extend(bytes.fromhex("edef8ba979d64acea3c827dcd51d21ed")) + array_of_bytes.extend(b'\x00\x00\x00\x12\x12\x10') + array_of_bytes.extend(bytes.fromhex(str(kid).replace("-", ""))) + pssh = base64.b64encode(bytes.fromhex(array_of_bytes.hex())).decode("utf-8") + return pssh + else: + return None +class KeyExtractor: + def __init__(self, pssh_value, cert_b64, license_url, headers): + self.pssh_value = pssh_value + self.cert_b64 = cert_b64 + self.license_url = license_url + self.headers = headers + + def get_keys(self): + wvdecrypt = WvDecrypt(init_data_b64=self.pssh_value, cert_data_b64=self.cert_b64, device=device_android_generic) + raw_challenge = wvdecrypt.get_challenge() + data = raw_challenge + + response = requests.post(self.license_url, headers=self.headers, data=data) + license_b64 = b64encode(response.content) + wvdecrypt.update_license(license_b64) + keys = wvdecrypt.start_process() + return keys + +class DataExtractor_DSNP: + def __init__(self, content): + self.content = content + + def extract_base64_by_choice(self, choice): + if self.content: + matches = [(match[0], re.search(r'base64,(.*)', match[1]).group(1)) for match in re.findall(r'KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",KEYFORMATVERSIONS="[^"]+",CHARACTERISTICS="([^"]+)",URI="([^"]+)"', self.content)] + if matches: + if 1 <= choice <= len(matches): + characteristics, base64_data = matches[choice - 1] + return characteristics, base64_data + else: + return None, None + return None, None + + def get_characteristics_list(self): + if self.content: + matches = [(match[0], re.search(r'base64,(.*)', match[1]).group(1)) for match in re.findall(r'KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",KEYFORMATVERSIONS="[^"]+",CHARACTERISTICS="([^"]+)",URI="([^"]+)"', self.content)] + return matches + return [] + +def parse_manifest_ism(manifest_url): + r = requests.get(manifest_url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/72.0.3626.121 Safari/537.36'}) + + if r.status_code != 200: + raise Exception(r.text) + + ism = xmltodict.parse(r.text) + + pssh = ism['SmoothStreamingMedia']['Protection']['ProtectionHeader']['#text'] + + pr_pssh_dec = base64.b64decode(pssh).decode('utf16') + pr_pssh_dec = pr_pssh_dec[pr_pssh_dec.index('<'):] + pr_pssh_xml = xmltodict.parse(pr_pssh_dec) + kid_hex = base64.b64decode(pr_pssh_xml['WRMHEADER']['DATA']['KID']).hex() + + kid = uuid.UUID(kid_hex).bytes_le.hex() + + stream_indices = ism['SmoothStreamingMedia']['StreamIndex'] + + # List to store information for each stream + stream_info_list = [] + + # Iterate over each StreamIndex (as it might be a list) + for stream_info in stream_indices if isinstance(stream_indices, list) else [stream_indices]: + type_info = stream_info['@Type'] + + if type_info in {'video', 'audio'}: + # Handle the case where there can be multiple QualityLevel elements + quality_levels = stream_info.get('QualityLevel', []) + + if not isinstance(quality_levels, list): + quality_levels = [quality_levels] + + for quality_level in quality_levels: + codec = quality_level.get('@FourCC', 'N/A') + bitrate = quality_level.get('@Bitrate', 'N/A') + + # Additional attributes for video streams + if type_info == 'video': + max_width = quality_level.get('@MaxWidth', 'N/A') + max_height = quality_level.get('@MaxHeight', 'N/A') + resolution = f"{max_width}x{max_height}" + else: + resolution = 'N/A' + + # Additional attributes for audio streams + language = stream_info.get('@Language', 'N/A') + track_id = stream_info.get('@AudioTrackId', 'N/A') if type_info == 'audio' else None + + stream_info_list.append({ + 'type': type_info, + 'codec': codec, + 'bitrate': bitrate, + 'resolution': resolution, + 'language': language, + 'track_id': track_id + }) + + # PSSH encoding logic in ism + array_of_bytes = bytearray(b'\x00\x00\x002pssh\x00\x00\x00\x00') + array_of_bytes.extend(bytes.fromhex("edef8ba979d64acea3c827dcd51d21ed")) + array_of_bytes.extend(b'\x00\x00\x00\x12\x12\x10') + array_of_bytes.extend(bytes.fromhex(str(kid).replace("-", ""))) + + encoded_string = base64.b64encode(bytes.fromhex(array_of_bytes.hex())).decode("utf-8") + + return kid, stream_info_list, encoded_string + +def get_keys_license_cdrm_project(license_url, headers_license, pssh_value): + formatted_headers = '\n'.join([f'{key}: "{value}"' for key, value in headers_license.items()]) + + json_data = { + 'license': license_url, + 'headers': formatted_headers, + 'pssh': pssh_value, + 'buildInfo': '', + 'proxy': '', + 'cache': False, + } + + response = requests.post('https://cdrm-project.com/wv', json=json_data) + return response + +def get_keys_cache_cdrm_project(pssh_value): + data = pssh_value + response = requests.post('https://cdrm-project.com/findpssh', data=data) + print_keys_cdrm_project(response) + +def print_keys_cdrm_project(response): + if response.status_code == 200: + soup = BeautifulSoup(response.text, 'html.parser') + li_elements = soup.find('ol').find_all('li') + for li in li_elements: + key = li.get_text(strip=True) + print(f'KEY: {key}') + else: + print(f"Error: {response.status_code}") + +def extract_pssh_m3u8(content): + # Use regular expression to extract the Base64-encoded PSSH value + pssh_match = re.search(r'URI="data:text/plain;base64,([^"]+)"', content) + + if pssh_match: + pssh_base64 = pssh_match.group(1) + return pssh_base64 + + # If the regex match fails, return None or raise an exception as needed + return None + +def get_keys_cdrm_api(headers_license, license_url, pssh_value): + api_url = "https://cdrm-project.com/api" + + r = requests.post(api_url, headers=headers_license, json={"license": license_url, "pssh": pssh_value}) + data = r.json() + + # Extract keys from the response + keys = [key["key"] for key in data.get("keys", [])] + + return keys \ No newline at end of file diff --git a/cdrm.py b/cdrm.py new file mode 100644 index 0000000..cae5878 --- /dev/null +++ b/cdrm.py @@ -0,0 +1,35 @@ +import argparse +import requests +from cdm.wks import PsshExtractor, get_keys_license_cdrm_project, print_keys_cdrm_project + +token = "" + +def main(): + parser = argparse.ArgumentParser(description="Decrypt Widevine content using MPD URL and License URL") + parser.add_argument("-mpd", required=True, help="URL of the MPD manifest") + parser.add_argument("-lic", required=True, help="URL of the license server") + args = parser.parse_args() + + mpd_url = args.mpd + license_url = args.lic + + headers_mpd = { + 'origin': 'https://play.hbomax.com', + 'referer': 'https://play.hbomax.com/', + } + + response = requests.get(mpd_url, headers=headers_mpd) + pssh_extractor = PsshExtractor(response.text) + pssh_value = pssh_extractor.extract_pssh() + + print("PSSH value:", pssh_value) + + headers_license = { + 'authorization': f'Bearer {token}', + } + + response = get_keys_license_cdrm_project(license_url, headers_license, pssh_value) + print_keys_cdrm_project(response) + +if __name__ == "__main__": + main() diff --git a/cdrm_api.py b/cdrm_api.py new file mode 100644 index 0000000..415981d --- /dev/null +++ b/cdrm_api.py @@ -0,0 +1,56 @@ +import requests +from cdm.wks import PsshExtractor, get_keys_cdrm_api +# HBOMAX Test +def parse_command_line_arguments(): + """Parse command line arguments.""" + parser = __import__('argparse').ArgumentParser(description="Decrypt Widevine content using MPD URL and License URL") + parser.add_argument("-mpd", required=True, help="URL of the MPD manifest") + parser.add_argument("-lic", required=True, help="URL of the license server") + return parser.parse_args() + +def get_mpd_response(mpd_url): + """Get MPD manifest response.""" + mpd_headers = { + 'origin': 'https://play.hbomax.com', + 'referer': 'https://play.hbomax.com/', + } + return requests.get(mpd_url, headers=mpd_headers) + +def get_license_headers(token): + """Get headers for the license server request.""" + return { + 'accept': "*/*", # no delet + 'content-length': "316", # no delet + 'Connection': 'keep-alive', # no delet + 'authorization': f'Bearer {token}', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (Ktesttemp, like Gecko) Chrome/90.0.4430.85 Safari/537.36' + } + +def main(): + # Parse command line arguments + args = parse_command_line_arguments() + mpd_url, license_url = args.mpd, args.lic + + # Get MPD response + mpd_response = get_mpd_response(mpd_url) + + # Extract PSSH value from MPD response + pssh_extractor = PsshExtractor(mpd_response.text) + pssh_value = pssh_extractor.extract_pssh() + + print("PSSH value:", pssh_value) + + # Get headers for the license server request + token = "" + # Update with your actual token + license_headers = get_license_headers(token) + + # Call the function in keys.py to get the keys + keys = get_keys_cdrm_api(license_headers, license_url, pssh_value) + + # Process each key + for key in keys: + print(f'KEY: {key}') + +if __name__ == "__main__": + main() diff --git a/cdrm_cache.py b/cdrm_cache.py new file mode 100644 index 0000000..98fb5ad --- /dev/null +++ b/cdrm_cache.py @@ -0,0 +1,35 @@ +import argparse +import requests +from cdm.wks import PsshExtractor, get_keys_cache_cdrm_project + +def extract_pssh_value(mpd_url): + headers_mpd = { + 'origin': 'https://play.hbomax.com', + 'referer': 'https://play.hbomax.com/', + } + + response = requests.get(mpd_url, headers=headers_mpd) + + if response.status_code == 200: + pssh_extractor = PsshExtractor(response.text) + pssh_value = pssh_extractor.extract_pssh() + return pssh_value + else: + raise ValueError(f"Error: Unable to fetch MPD manifest, Status Code: {response.status_code}") + +def main(): + parser = argparse.ArgumentParser(description="Decrypt Widevine content using MPD URL and License URL") + parser.add_argument("-mpd", required=True, help="URL of the MPD manifest") + args = parser.parse_args() + + mpd_url = args.mpd + + try: + pssh_value = extract_pssh_value(mpd_url) + print("PSSH value:", pssh_value) + get_keys_cache_cdrm_project(pssh_value) + except Exception as e: + print(f"An error occurred: {e}") + +if __name__ == "__main__": + main() diff --git a/extractwvd.py b/extractwvd.py new file mode 100644 index 0000000..908a5b5 --- /dev/null +++ b/extractwvd.py @@ -0,0 +1,92 @@ +import argparse +import json +from enum import Enum +from pathlib import Path +from construct import BitStruct, Bytes, Const +from construct import Enum as CEnum +from construct import Flag, If, Int8ub, Int16ub, Optional, Padded, Padding, Struct, this +from Cryptodome.PublicKey import RSA +from cdm.wks import ClientIdentification + +class DeviceTypes(Enum): + CHROME = 1 + ANDROID = 2 + +WidevineDeviceStruct = Struct( + 'signature' / Const(b'WVD'), + 'version' / Int8ub, + 'type' / CEnum( + Int8ub, + **{t.name: t.value for t in DeviceTypes} + ), + 'security_level' / Int8ub, + 'flags' / Padded(1, Optional(BitStruct( + Padding(7), + 'send_key_control_nonce' / Flag + ))), + 'private_key_len' / Int16ub, + 'private_key' / Bytes(this.private_key_len), + 'client_id_len' / Int16ub, + 'client_id' / Bytes(this.client_id_len), + 'vmp_len' / Optional(Int16ub), + 'vmp' / If(this.vmp_len, Optional(Bytes(this.vmp_len))) +) + +WidevineDeviceStructVersion = 1 + +def parse_args(): + parser = argparse.ArgumentParser(description='Widevine Device Information Parser') + parser.add_argument('file', type=Path, help='Path to WVD file') + return parser.parse_args() + +def write_key_and_blob_files(out_dir, device): + private_key_file = out_dir / 'device_private_key' + print(f'\n[INFO] Writing private key to: {private_key_file}') + private_key = RSA.import_key(device.private_key) + private_key_file.write_text(private_key.export_key('PEM').decode()) + + client_id_blob_file = out_dir / 'device_client_id_blob' + print(f'[INFO] Writing client ID blob to: {client_id_blob_file}') + client_id_blob_file.write_bytes(device.client_id) + + if device.vmp: + vmp_blob_file = out_dir / 'device_vmp_blob' + print(f'[INFO] Writing VMP blob to: {vmp_blob_file}') + vmp_blob_file.write_bytes(device.vmp) + +def write_json_file(out_dir, name, client_id, device): + wv_json_file = out_dir / 'wv.json' + description = f'{name} ({client_id.Token._DeviceCertificate.SystemId})' + print(f'[INFO] Writing JSON file to: {wv_json_file}') + wv_json_file.write_text(json.dumps({ + 'name': name, + 'description': description, + 'security_level': device.security_level, + 'session_id_type': device.type.lower(), + 'private_key_available': True, + 'vmp': bool(device.vmp), + 'send_key_control_nonce': device.type == DeviceTypes.ANDROID + }, indent=2)) + +def main(): + args = parse_args() + + name = args.file.with_suffix('').name + out_dir = Path.cwd() / 'cdm' / 'devices' / 'android_generic' + out_dir.mkdir(parents=True, exist_ok=True) + + with args.file.open('rb') as fd: + device = WidevineDeviceStruct.parse_stream(fd) + + print(f'\n[INFO] Starting Widevine Device Information Parsing') + write_key_and_blob_files(out_dir, device) + + client_id = ClientIdentification() + client_id.ParseFromString(device.client_id) + + write_json_file(out_dir, name, client_id, device) + + print('[INFO] Done') + +if __name__ == '__main__': + main() diff --git a/gettoken.py b/gettoken.py new file mode 100644 index 0000000..f636df7 --- /dev/null +++ b/gettoken.py @@ -0,0 +1,83 @@ +import requests + +headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0', + 'Accept': '*/*', + 'Accept-Language': 'en-US,en;q=0.5', + # 'Accept-Encoding': 'gzip, deflate, br', + 'Referer': 'https://npo.nl/start/serie/nos-journaal/seizoen-328/nos-journaal_91491/afspelen', + 'Content-Type': 'application/json', +# 'sentry-trace': 'adf1091e221d423fa031623483d5a75f-b2cd5555b5236103-0', + 'baggage': 'sentry-environment=prod,sentry-release=7-1xQyxcfFi-KAQG1GEsk,sentry-public_key=ff5dbc8fc4e94b9390c3581c962b975a,sentry-trace_id=adf1091e221d423fa031623483d5a75f,sentry-transaction=%2Fserie%2F%5BseriesSlug%5D%2F%5B%5B...seriesParams%5D%5D,sentry-sampled=false', + 'DNT': '1', + 'Connection': 'keep-alive', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'Pragma': 'no-cache', + 'Cache-Control': 'no-cache', + # Requests doesn't support trailers + # 'TE': 'trailers', +} +session = requests.Session() +response = session.get('https://npo.nl/start/api/auth/session', headers=headers) + +response_cookies = session.cookies.get_dict() +# dict: +# { +# '__Host-next-auth.csrf-token' : '73bcedab2973fc2547d6a8f4405da66ac1dbeb432e21ba209ed93dac14dfbe88%7Cf4ed5605a6f0b044e80da012b9705c9d915a6603e2109d38ebff47823d58acc3', +# '__Secure-next-auth.callback-url' : 'https%3A%2F%2Fnpo.nl' +# } + +csrf = response_cookies["__Host-next-auth.csrf-token"] +print(f'csrf: {csrf}') + + + + +cookies2 = { + '__Host-next-auth.csrf-token': csrf, + '__Secure-next-auth.callback-url': 'https%3A%2F%2Fnpo.nl', +# 'CCM_Wrapper_Cache': 'eyJ2ZXIiOiJ2My4yLjgiLCJqc2giOiIiLCJjaWQiOiJWSHNva3FFUkk2TVozbTI1IiwiY29uaWQiOiJXR0ptTCJ9', + 'pa_privacy': '%22optin%22', + '_pcid': '%7B%22browserId%22%3A%22lr5dxgvg8okqb7ru%22%2C%22_t%22%3A%22m6tsuy7i%7Clr5dxgvi%22%7D', + '_pctx': '%7Bu%7DN4IgrgzgpgThIC4B2YA2qA05owMoBcBDfSREQpAeyRCwgEt8oBJAE0RXSwH18yBbAGz4IYAJ4B2AFYAfVDACsrAB4BzAG5SQAXyA', + 'Cookie_Consent': 'false', + 'CCM_ID': 'VHsokqERI6MZ3m25', + 'Cookie_Category_Necessary': 'true', + 'Cookie_Category_Analytics': 'true', +# 'Cookie_Category_Social': '', +# 'bitmovin_analytics_uuid': 'f6cdf35e-618f-4b82-8bec-2a543be32390', +} + +headers2 = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0', + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'en-US,en;q=0.5', + # 'Accept-Encoding': 'gzip, deflate, br', + 'Content-Type': 'application/json', +# 'sentry-trace': 'da0c64681077477196b6c39ce00fe616-b275f8f0de3ad282-0', +# 'baggage': 'sentry-environment=prod,sentry-release=7-1xQyxcfFi-KAQG1GEsk,sentry-public_key=ff5dbc8fc4e94b9390c3581c962b975a,sentry-trace_id=da0c64681077477196b6c39ce00fe616', + 'Origin': 'https://npo.nl', + 'DNT': '1', + 'Connection': 'keep-alive', + 'Referer': 'https://npo.nl/start/serie/nos-journaal/seizoen-328/nos-journaal_91491/afspelen', + # 'Cookie': '__Host-next-auth.csrf-token=e47c6a34c0fe5a4d68821408a2b1271ca16d7ae435f8ca6ddb1f088f00f4ec84%7C8b09f39a2efa3e8229eef01fffb4a41ee08e19996f0e6ca860260e68aac1d682; __Secure-next-auth.callback-url=https%3A%2F%2Fnpo.nl; CCM_Wrapper_Cache=eyJ2ZXIiOiJ2My4yLjgiLCJqc2giOiIiLCJjaWQiOiJWSHNva3FFUkk2TVozbTI1IiwiY29uaWQiOiJXR0ptTCJ9; pa_privacy=%22optin%22; _pcid=%7B%22browserId%22%3A%22lr5dxgvg8okqb7ru%22%2C%22_t%22%3A%22m6tsuy7i%7Clr5dxgvi%22%7D; _pctx=%7Bu%7DN4IgrgzgpgThIC4B2YA2qA05owMoBcBDfSREQpAeyRCwgEt8oBJAE0RXSwH18yBbAGz4IYAJ4B2AFYAfVDACsrAB4BzAG5SQAXyA; Cookie_Consent=false; CCM_ID=VHsokqERI6MZ3m25; Cookie_Category_Necessary=true; Cookie_Category_Analytics=true; Cookie_Category_Social=; bitmovin_analytics_uuid=f6cdf35e-618f-4b82-8bec-2a543be32390', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'Pragma': 'no-cache', + 'Cache-Control': 'no-cache', + # Requests doesn't support trailers + # 'TE': 'trailers', +} + +#json_data = { +# 'productId': 'POW_05759713', +#} + +response2 = requests.post('https://npo.nl/start/api/domain/player-token', cookies=cookies2, headers=headers2) + +#videoplayer token +token = response2.json()["token"] +print(f'token: {token}') diff --git a/init_pssh.py b/init_pssh.py new file mode 100644 index 0000000..d33b44e --- /dev/null +++ b/init_pssh.py @@ -0,0 +1,66 @@ +import base64 +import sys +from pathlib import Path +from google.protobuf.message import DecodeError +from cdm.wks import WidevineCencHeader + +# File path to read the raw data +file_path = "your init" + +# Read the raw data from the file +raw = Path(file_path).read_bytes() + +# Find the offset of 'pssh' in the raw data +pssh_offset = raw.rfind(b'pssh') + +if pssh_offset == -1: + print("[ERROR] 'pssh' not found in the file.") + sys.exit(1) +else: + # Extract the PSSH data based on the offset and length information + _start = max(pssh_offset - 4, 0) + _end = min(pssh_offset - 4 + raw[pssh_offset - 1], len(raw)) + pssh = raw[_start:_end] + + # Display the PSSH data in base64 format + print('\n[INFO] PSSH:', base64.b64encode(pssh).decode('utf-8')) + pssh_b64 = base64.b64encode(pssh) + print("\n[SUCCESS] PSSH extracted successfully.") + +# Check if the PSSH data needs modification +if not pssh[12:28] == bytes([237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237]): + print("[Modifying PSSH data...]") + # Create a new PSSH data with the required modifications + new_pssh = bytearray([0, 0, 0]) + new_pssh.append(32 + len(pssh)) + new_pssh[4:] = bytearray(b'pssh') + new_pssh[8:] = [0, 0, 0, 0] + new_pssh[13:] = [237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237] + new_pssh[29:] = [0, 0, 0, 0] + new_pssh[31] = len(pssh) + new_pssh[32:] = pssh + pssh_b64 = base64.b64encode(new_pssh) + print("[Modified PSSH data:", pssh_b64.decode(), "]") +else: + print("[PSSH data doesn't need modification.]") + +# Parse the modified or original PSSH data using WidevineCencHeader +parsed_init_data = WidevineCencHeader() + +try: + parsed_init_data.ParseFromString(base64.b64decode(pssh_b64)) + print("[PSSH data parsed successfully.]") +except (DecodeError, SystemError) as e: + print("[Error parsing PSSH data:", e, "]") + try: + # Attempt to parse PSSH data from byte offset 32 + id_bytes = parsed_init_data.ParseFromString(base64.b64decode(pssh_b64)[32:]) + print("[PSSH data parsed successfully from byte offset 32.]") + except DecodeError as de: + print("[Error parsing PSSH data from byte offset 32:", de, "]") + sys.exit(1) + +# Convert the parsed key_id to a hexadecimal string +key_id_str = ''.join(['{:02x}'.format(b) for b in parsed_init_data.key_id[0]]) +print("[Key ID in hexadecimal:", key_id_str, "]") +print("\n[SUCCESS] [Done]\n") \ No newline at end of file diff --git a/ism.py b/ism.py new file mode 100644 index 0000000..cb83d5b --- /dev/null +++ b/ism.py @@ -0,0 +1,36 @@ +import argparse +from cdm.wks import parse_manifest_ism + +def main(): + # Create an ArgumentParser object and add the 'urls' argument + parser = argparse.ArgumentParser(description='Script for parsing Smooth Streaming manifest URLs.') + parser.add_argument('urls', + help='The URLs to parse. You may need to wrap the URLs in double quotes if you have issues.', + nargs='+') + + # Parse the arguments + args = parser.parse_args() + + # Iterate over the provided URLs + for manifest_link in args.urls: + kid, stream_info_list, encoded_string = parse_manifest_ism(manifest_link) + + # Print information for each stream + for stream_info in stream_info_list: + type_info = stream_info['type'] + codec = stream_info['codec'] + bitrate = stream_info['bitrate'] + resolution = stream_info['resolution'] + + if type_info == 'video': + print(f'[INFO] VIDEO - Codec: {codec}, Resolution: {resolution}, Bitrate: {bitrate}') + elif type_info == 'audio': + language = stream_info['language'] + track_id = stream_info['track_id'] + print(f'[INFO] AUDIO - Codec: {codec}, Bitrate: {bitrate}, Language: {language}, Track ID: {track_id}') + + # Print PSSH information + print('\n[INFO] PSSH:', encoded_string) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..de61670 --- /dev/null +++ b/main.py @@ -0,0 +1,43 @@ +import argparse +from cdm.wks import WvDecrypt, device_android_generic, PsshExtractor, KeyExtractor +import requests + +def get_keys_license(mpd_url, license_url): + response = requests.get(mpd_url) + pssh_extractor = PsshExtractor(response.text) + pssh_value = pssh_extractor.extract_pssh() + + print("PSSH value:", pssh_value) + + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36', + } + + cert_b64 = None + key_extractor = KeyExtractor(pssh_value, cert_b64, license_url, headers) + keys = key_extractor.get_keys() + wvdecrypt = WvDecrypt(init_data_b64=pssh_value, cert_data_b64=cert_b64, device=device_android_generic) + raw_challenge = wvdecrypt.get_challenge() + data = raw_challenge + + return keys + +def main(): + parser = argparse.ArgumentParser(description="Decrypt Widevine content using MPD URL and License URL") + parser.add_argument("-mpd", required=True, help="URL of the MPD manifest") + parser.add_argument("-lic", required=True, help="URL of the license server") + args = parser.parse_args() + + mpd_url = args.mpd + license_url = args.lic + + keys = get_keys_license(mpd_url, license_url) + + for key in keys: + if isinstance(key, list): + if key: + for key_str in key: + print(f"KEY: {key_str}") + +if __name__ == "__main__": + main() diff --git a/main_dsnp.py b/main_dsnp.py new file mode 100644 index 0000000..1347a36 --- /dev/null +++ b/main_dsnp.py @@ -0,0 +1,58 @@ +import argparse +from cdm.wks import KeyExtractor, DataExtractor_DSNP +import requests +token = "" +def get_keys_license(m3u8_url): + response = requests.get(m3u8_url) + content = response.text if response.status_code == 200 else None + + data_extractor = DataExtractor_DSNP(content) + + if content: + characteristics_list = data_extractor.get_characteristics_list() + + if characteristics_list: + print("Choose CHARACTERISTICS Value:") + for i, (characteristics, _) in enumerate(characteristics_list): + print(f"{i + 1}. {characteristics}") + + choice = int(input("Enter the number of the CHARACTERISTICS you want: ")) + characteristics, base64_data = data_extractor.extract_base64_by_choice(choice) + + if characteristics and base64_data: + print("CHARACTERISTICS Value:", characteristics) + + print("PSSH value (Base64 Data):", base64_data) + + license_url = "https://disney.playback.edge.bamgrid.com/widevine/v1/obtain-license" + + headers = { + 'authorization': f'Bearer {token}', + 'origin': 'https://www.disneyplus.com', + 'referer': 'https://www.disneyplus.com/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36', + } + + cert_b64 = None + key_extractor = KeyExtractor(base64_data, cert_b64, license_url, headers) + keys = key_extractor.get_keys() + + for key in keys: + if isinstance(key, list): + if key: + for key_str in key: + print(f"KEY: {key_str}") + + return base64_data + +def main(): + parser = argparse.ArgumentParser(description="Decrypt Widevine content using M3U8 URL") + parser.add_argument("-m3u8", required=True, help="URL of the M3U8 manifest") + args = parser.parse_args() + + m3u8_url = args.m3u8 + + pssh_value = get_keys_license(m3u8_url) + +if __name__ == "__main__": + main() diff --git a/main_m3u8.py b/main_m3u8.py new file mode 100644 index 0000000..ee14484 --- /dev/null +++ b/main_m3u8.py @@ -0,0 +1,42 @@ +from cdm.wks import WvDecrypt, device_android_generic, extract_pssh_m3u8, KeyExtractor +import argparse +import requests + +def get_keys_license(m3u8_url, license_url): + response = requests.get(m3u8_url) + pssh_value = extract_pssh_m3u8(response.text) + + print("PSSH value:", pssh_value) + + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36', + } + + cert_b64 = None + key_extractor = KeyExtractor(pssh_value, cert_b64, license_url, headers) + keys = key_extractor.get_keys() + wvdecrypt = WvDecrypt(init_data_b64=pssh_value, cert_data_b64=cert_b64, device=device_android_generic) + raw_challenge = wvdecrypt.get_challenge() + data = raw_challenge + + return keys + +def main(): + parser = argparse.ArgumentParser(description="Decrypt Widevine content using M3U8 URL and License URL") + parser.add_argument("-m3u8", required=True, help="URL of the M3U8 manifest") + parser.add_argument("-lic", required=True, help="URL of the license server") + args = parser.parse_args() + + m3u8_url = args.m3u8 + license_url = args.lic + + keys = get_keys_license(m3u8_url, license_url) + + for key in keys: + if isinstance(key, list): + if key: + for key_str in key: + print(f"KEY: {key_str}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/npo all-in-one.py b/npo all-in-one.py new file mode 100644 index 0000000..5bfdb39 --- /dev/null +++ b/npo all-in-one.py @@ -0,0 +1,317 @@ +# Pre-requisites: +# * N_m3u8DL-RE and mp4decrypt in current directory +# * ffmpeg in PATH + +# PIP Requirements: +# * protobuf +# * bs4 +# * xmltodict +# * browser_cookie3 +# * requests +# * pycryptodomex + +import argparse +import requests +import subprocess +import os +from bs4 import BeautifulSoup +import json +import platform # check for windows OS +import shutil # check for ffmpeg in PATH, part of python std +import browser_cookie3 # cookies for premium accs +from cdm.wks import WvDecrypt, device_android_generic, PsshExtractor, KeyExtractor +import concurrent.futures + +headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.3', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Cache-Control': 'no-cache', +} + +license_url = "https://npo-drm-gateway.samgcloud.nepworldwide.nl/authentication" + +if platform.system() == "Windows": + windows_flag = True +else: + windows_flag = False + + +parser = argparse.ArgumentParser(description='PYWKS-NPO') +parser.add_argument('-url', dest='url', required=False, help='NPO Video URL') +parser.add_argument('-file', dest='file', required=False, help='File with NPO Video URLs, one per line') +args = parser.parse_args() + + +def parse_url_file(file_path): + with open(file_path, 'r') as file: + urls = [line.strip() for line in file] + return urls + +if args.file and args.url: + print("ERR: Please specify just one argument.") + print("-url: input NPO video URL") + print("-file: input a file with NPO video URLS, one per line") + exit() +elif args.file: + urls = parse_url_file(args.file) +elif args.url: + urls = [args.url] +else: + print("ERR: Please input your URL.") + print("-url: input NPO video URL") + print("-file: input a file with NPO video URLS, one per line") + exit() + + +def find_cookies(): + print("NPO Plus subscribers are able to download in 1080p instead of 540p.") + print("Are you an NPO Plus subscriber and logged in on your browser? (y/N)") + userinput = input().lower() + if not userinput or userinput.lower() != 'y': + return + + cookies = browser_cookie3.load(domain_name='npo.nl') + return cookies + + +def find_targetId(url): + # Get full HTML and extract productId and episode number + # "future proof" + response_targetId = requests.get(url) + content = response_targetId.content + + try: + url_split = url.split("/") + target_slug = url_split[7] + except: + print("URL invalid.") + print("URL format: https://npo.nl/start/serie/wie-is-de-mol/seizoen-24/wie-is-de-mol_56/afspelen") + print(f"Your URL: {url}") + exit() + + soup = BeautifulSoup(content, 'html.parser') + script_tag = soup.find('script', {'id': '__NEXT_DATA__'}) + + if script_tag: + script_content = script_tag.contents[0] + else: + print("Script tag not found.") + print("Hint: Use the -token argument to supply your own.") + + def search(data, target_slug): + if isinstance(data, list): + for item in data: + result = search(item, target_slug) + if result: + return result + elif isinstance(data, dict): + for key, value in data.items(): + if key == "slug" and value == target_slug: + return data.get("productId"), data.get("programKey") + else: + result = search(value, target_slug) + if result: + return result + return None + + data_dict = json.loads(script_content) + target_product_id = search(data_dict, target_slug) + return target_product_id + + +def find_CSRF(targetId, plus_cookie): + response_CSRF = requests.get('https://npo.nl/start/api/auth/session', headers=headers, cookies=plus_cookie) + response_cookies = response_CSRF.cookies.get_dict() + csrf = response_cookies["__Host-next-auth.csrf-token"] + + csrf_cookies = { + '__Host-next-auth.csrf-token': csrf, + '__Secure-next-auth.callback-url': 'https%3A%2F%2Fnpo.nl', + } + + if not plus_cookie: + plus_cookie = csrf_cookies + + json_productId = { + 'productId': targetId, + } + + response_token = requests.post('https://npo.nl/start/api/domain/player-token', cookies=plus_cookie, headers=headers, json=json_productId) + token = response_token.json()["token"] + return token + + +def find_MPD(token, url, plus_cookie): + headers['Authorization'] = token + + json_auth = { + 'profileName': 'dash', + 'drmType': 'widevine', + 'referrerUrl': url, + } + + response = requests.post('https://prod.npoplayer.nl/stream-link', headers=headers, json=json_auth, cookies=plus_cookie) + response_data = response.json() + stream_data = response_data.get('stream', {}) + + if stream_data.get('streamURL'): + return stream_data + else: + print("NO MPD URL - BAD TOKEN") + print(response_data) + exit() + + +def find_PSSH(mpd): + mpd_url = mpd.get('streamURL') + + response = requests.get(mpd_url, headers=headers) + pssh_extractor = PsshExtractor(response.text) + pssh_value = pssh_extractor.extract_pssh() + return pssh_value, mpd_url + + +def find_key(mpd, pssh): + headers_license = { + 'x-custom-data': mpd.get('drmToken'), + 'origin': 'https://start-player.npo.nl', + 'referer': 'https://start-player.npo.nl/', + } + + cert_b64 = None + key_extractor = KeyExtractor(pssh, cert_b64, license_url, headers_license) + keys = key_extractor.get_keys() + wvdecrypt = WvDecrypt(init_data_b64=pssh, cert_data_b64=cert_b64, device=device_android_generic) + raw_challenge = wvdecrypt.get_challenge() + data = raw_challenge + for key in keys: + if isinstance(key, list): + if key: + for key_str in key: + return key_str + + +def check_prereq(): + if windows_flag == True: + prereq_filelist = ['mp4decrypt.exe', 'N_m3u8DL-RE.exe'] + else: + prereq_filelist = ['mp4decrypt', 'N_m3u8DL-RE'] + + for file in prereq_filelist: + if not os.path.isfile(file): + print(f"ERR: {file} not found!") + print("Please check your directory and try again.") + exit() + if shutil.which("ffmpeg") is None: + print("ffmpeg not found in PATH.") + exit() + + +def create_filename(url, programKey): +# 1 2 3 4 5 6 7 8 (optional) +# create filename based on input URL: https://npo.nl/start/serie /wie-is-de-mol /seizoen-24 /wie-is-de-mol_56 /afspelen +# https://npo.nl/start/serie /de-avondshow-met-arjen-lubach /seizoen-8_1 /de-avondshow-met-arjen-lubach_93 /afspelen + url_split = url.split("/") + title = url_split[7].split("_")[0] + season = url_split[6].split("_")[0] + + filename_enc = title + "_" + season + "_ep-" + programKey + "_encrypted" + filename = filename_enc.replace("_encrypted", "") + return filename_enc, filename + + +def download(mpd_url, filename_enc): +# output: filename.m4a (audio) and filename.mp4 (video) + if windows_flag == True: + subprocess.run(['N_m3u8DL-RE.exe', '--auto-select', '--no-log', '--save-name', filename_enc, mpd_url], stdout=subprocess.DEVNULL) + else: + subprocess.run(['N_m3u8DL-RE', '--auto-select', '--no-log', '--save-name', filename_enc, mpd_url], stdout=subprocess.DEVNULL) + + +def decrypt(key, filename_enc, filename): + if windows_flag == True: + subprocess.run(['mp4decrypt.exe', '--key', key, str(filename_enc + ".mp4"), str(filename + "_video.mp4")], stdout=subprocess.DEVNULL) + subprocess.run(['mp4decrypt.exe', '--key', key, str(filename_enc + ".m4a"), str(filename + "_audio.m4a")], stdout=subprocess.DEVNULL) + else: + subprocess.run(['mp4decrypt', '--key', key, str(filename_enc + ".mp4"), str(filename + "_video.mp4")], stdout=subprocess.DEVNULL) + subprocess.run(['mp4decrypt', '--key', key, str(filename_enc + ".m4a"), str(filename + "_audio.m4a")], stdout=subprocess.DEVNULL) + + +def merge(filename): + ffmpeg_command = [ + 'ffmpeg', '-v', 'quiet', # '-stats', + '-i', filename + "_video.mp4", + '-i', filename + "_audio.m4a", + '-c:v', 'copy', + '-c:a', 'copy', + '-strict', 'experimental', + filename + ".mp4" + ] + + subprocess.run(ffmpeg_command) + + +def clean(filename_enc, filename): + os.remove(filename_enc + ".mp4") + os.remove(filename_enc + ".m4a") + os.remove(filename + "_audio.m4a") + os.remove(filename + "_video.mp4") + + +def check_file(filename): + if not os.path.exists(filename + ".mp4"): + print("File not found. Continue anyway? (y/N)") + userinput = input().lower() + if not userinput or userinput != 'y': + exit() + + +def execute(url, plus_cookie, process_no): + productId, programKey = find_targetId(url) + token = find_CSRF(productId,plus_cookie) + mpd = find_MPD(token, url, plus_cookie) + pssh, mpd_url = find_PSSH(mpd) + key = find_key(mpd, pssh) + check_prereq() + filename_enc, filename = create_filename(url, programKey) + download(mpd_url, filename_enc) + decrypt(key, filename_enc, filename) + merge(filename) + clean(filename_enc, filename) + check_file(filename) + return process_no # keeps track of process index to return x/y videos completed message + + + +plus_cookie = find_cookies() +max_workers = min(os.cpu_count(), len(urls)) + +with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + futures = [executor.submit(execute, url, plus_cookie, i + 1) for i, url in enumerate(urls)] + + completed_videos = 0 + for future in concurrent.futures.as_completed(futures): + result = future.result() + completed_videos += 1 + print(f"{completed_videos}/{len(urls)} video{'s'[:len(urls) != 1]} completed") + + +######### +# NOTES # +######### +# The downloader *should* work across every platform, linux/mac/win. +# It has not been extensively tested on anything but windows. DM me if you need help :D + +# Supported browsers for NPO Plus cookies: +# (https://github.com/borisbabic/browser_cookie3#testing-dates--ddmmyy) +# * Chrome +# * Firefox +# * LibreWolf +# * Opera +# * Opera GX +# * Edge +# * Chromium +# * Brave +# * Vivaldi +# * Safari \ No newline at end of file diff --git a/npo.py b/npo.py new file mode 100644 index 0000000..9e07af2 --- /dev/null +++ b/npo.py @@ -0,0 +1,135 @@ +import argparse +import requests +from bs4 import BeautifulSoup +import json +from cdm.wks import WvDecrypt, device_android_generic, PsshExtractor, KeyExtractor + + +# Parse URL input +parser = argparse.ArgumentParser(description='PYWKS-NPO') +parser.add_argument('-url', dest='url', required=True, help='NPO Video URL') +args = parser.parse_args() + + +# Get HTML and extract productId +headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Connection': 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'same-site', + 'Pragma': 'no-cache', + 'Cache-Control': 'no-cache', +} + +response_targetId = requests.get(args.url, headers=headers) +content = response_targetId.content + +try: + url_split = args.url.split("/") + target_slug = url_split[7] +except: + print("URL invalid.") + print("URL format: https://npo.nl/start/serie/wie-is-de-mol/seizoen-24/wie-is-de-mol_56/afspelen") + print(f"Your URL: {args.url}") + exit() + +soup = BeautifulSoup(content, 'html.parser') +script_tag = soup.find('script', {'id': '__NEXT_DATA__'}) + +if script_tag: + script_content = script_tag.contents[0] +else: + print("Script tag not found.") + +def search(data, target_slug): + if isinstance(data, list): + for item in data: + result = search(item, target_slug) + if result: + return result + elif isinstance(data, dict): + for key, value in data.items(): + if key == "slug" and value == target_slug: + return data.get("productId") + else: + result = search(value, target_slug) + if result: + return result + return None + +data_dict = json.loads(script_content) +target_product_id = search(data_dict, target_slug) + + +# Get CSRF token +response_CSRF = requests.get('https://npo.nl/start/api/auth/session', headers=headers) +response_cookies = response_CSRF.cookies.get_dict() +csrf = response_cookies["__Host-next-auth.csrf-token"] + + +# Get player token +cookies = { + '__Host-next-auth.csrf-token': csrf, + '__Secure-next-auth.callback-url': 'https%3A%2F%2Fnpo.nl', +} + +json_productId = { + 'productId': target_product_id, +} + +response_token = requests.post('https://npo.nl/start/api/domain/player-token', cookies=cookies, headers=headers, json=json_productId) +token = response_token.json()["token"] + + +# Get MPD URL +headers['authorization'] = token + +json_auth = { + 'profileName': 'dash', + 'drmType': 'widevine', + 'referrerUrl': args.url, +} + +response = requests.post('https://prod.npoplayer.nl/stream-link', headers=headers, json=json_auth) +response_data = response.json() +stream_data = response_data.get('stream', {}) + +if stream_data.get('streamURL'): + print('MPD URL:', stream_data.get('streamURL')) +else: + print("NO MPD URL - BAD TOKEN") + exit() + + +# Get PSSH +mpd_url = stream_data.get('streamURL') +license_url = "https://npo-drm-gateway.samgcloud.nepworldwide.nl/authentication" + +response = requests.get(mpd_url, headers=headers) +pssh_extractor = PsshExtractor(response.text) +pssh_value = pssh_extractor.extract_pssh() + +print("PSSH:", pssh_value) +headers_license = { + 'x-custom-data': stream_data.get('drmToken'), + 'origin': 'https://start-player.npo.nl', + 'referer': 'https://start-player.npo.nl/', +} + + +# Get Key +cert_b64 = None +key_extractor = KeyExtractor(pssh_value, cert_b64, license_url, headers_license) +keys = key_extractor.get_keys() +wvdecrypt = WvDecrypt(init_data_b64=pssh_value, cert_data_b64=cert_b64, device=device_android_generic) +raw_challenge = wvdecrypt.get_challenge() +data = raw_challenge +for key in keys: + if isinstance(key, list): + if key: + for key_str in key: + print(f"KEY: {key_str}") \ No newline at end of file diff --git a/npo_new.py b/npo_new.py new file mode 100644 index 0000000..b4dc3fd --- /dev/null +++ b/npo_new.py @@ -0,0 +1,78 @@ +import argparse +import requests +from bs4 import BeautifulSoup +import json +import logging +import coloredlogs +from cdm.wks import WvDecrypt, device_android_generic, PsshExtractor, KeyExtractor + +# def search(data, target_slug): +# if isinstance(data, list): +# for item in data: +# result = search(item, target_slug) +# if result: +# return result +# elif isinstance(data, dict): +# for key, value in data.items(): +# if key == "slug" and value == target_slug: +# return data.get("productId") +# else: +# result = search(value, target_slug) +# if result: +# return result +# return None + +parser = argparse.ArgumentParser(description='PYWKS-NPO') + +parser.add_argument('-url', dest='url', required=True, help='NPO Video URL') +parser.add_argument("-logger", default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], help="Logger level") +args = parser.parse_args() +LOG_FORMAT = "{asctime} [{levelname[0]}] {name} : {message}" +LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" +LOG_STYLE = "{" +coloredlogs.install(level=args.logger, fmt=LOG_FORMAT, datefmt=LOG_DATE_FORMAT, style=LOG_STYLE) +logger = logging.getLogger("NPO") + +response_target_id = requests.get(args.url) +content = response_target_id.content +try: + target_slug = args.url.split("/")[7] +except IndexError: + logger.error("Invalid URL format. Example: https://npo.nl/start/serie/wie-is-de-mol/seizoen-24/wie-is-de-mol_56/afspelen") + exit() +soup = BeautifulSoup(content, 'html.parser') +script_tag = soup.find('script', {'id': '__NEXT_DATA__'}) +script_content = script_tag.contents[0] if script_tag else logger.error("Script tag not found.") +data_dict = json.loads(script_content) +target_product_id = search(data_dict, target_slug) +if not target_product_id: + logger.error("Failed to retrieve target product ID.") + exit() +response_csrf = requests.get('https://npo.nl/start/api/auth/session') +cookies = {'__Host-next-auth.csrf-token': response_csrf.cookies.get_dict()["__Host-next-auth.csrf-token"],'__Secure-next-auth.callback-url': 'https://npo.nl'} +json_product_id = {'productId': target_product_id} +response_token = requests.post('https://npo.nl/start/api/domain/player-token', cookies=cookies, json=json_product_id) +headers = {'authorization': response_token.json()["token"]} +json_auth = {'profileName': 'dash', 'drmType': 'widevine', 'referrerUrl': args.url} +response = requests.post('https://prod.npoplayer.nl/stream-link', headers=headers, json=json_auth) +stream_data = response.json().get('stream', {}) +if not stream_data.get('streamURL'): + logger.error("Failed to retrieve MPD URL. Invalid or expired authentication token.") + exit() +mpd_url = stream_data.get('streamURL') +logger.info(f"MPD URL: {mpd_url}") +license_url = "https://npo-drm-gateway.samgcloud.nepworldwide.nl/authentication" +response = requests.get(mpd_url, headers=headers) +pssh_extractor = PsshExtractor(response.text) +pssh_value = pssh_extractor.extract_pssh() +logger.info(f"PSSH: {pssh_value}") +headers_license = {'x-custom-data': stream_data.get('drmToken'),'origin': 'https://start-player.npo.nl','referer': 'https://start-player.npo.nl/'} +cert_b64 = None +key_extractor = KeyExtractor(pssh_value, cert_b64, license_url, headers_license) +keys = key_extractor.get_keys() +wvdecrypt = WvDecrypt(init_data_b64=pssh_value, cert_data_b64=cert_b64, device=device_android_generic) +raw_challenge = wvdecrypt.get_challenge() +for key in keys: + if isinstance(key, list) and key: + for key_str in key: + logger.info(f"\u251C KEY: {key_str}") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..33d1d93 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +protobuf +bs4 +xmltodict +browser_cookie3 +requests +pycryptodomex \ No newline at end of file