From 8aa81bddf2f6f17687c4db49d2cf5876e1f6c709 Mon Sep 17 00:00:00 2001 From: Sasuke-Duck UwU <95662926+SASUKE-DUCK@users.noreply.github.com> Date: Thu, 16 Nov 2023 01:32:18 -0500 Subject: [PATCH] Add files via upload Added a script to extract device_private_key and device_client_id_blob from a WDV File, also added support for https://cdrm-project.com api --- cdm/wks.py | 28 +++++++++++++++- cdrm.py | 35 ++++++++++++++++++++ extractwvd.py | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 cdrm.py create mode 100644 extractwvd.py diff --git a/cdm/wks.py b/cdm/wks.py index 1d27832..60d7a44 100644 --- a/cdm/wks.py +++ b/cdm/wks.py @@ -26,6 +26,7 @@ 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() @@ -855,4 +856,29 @@ def parse_manifest_ism(manifest_url): encoded_string = base64.b64encode(bytes.fromhex(array_of_bytes.hex())).decode("utf-8") - return kid, stream_info_list, encoded_string \ No newline at end of file + 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 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}") diff --git a/cdrm.py b/cdrm.py new file mode 100644 index 0000000..19eabe5 --- /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/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()