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
This commit is contained in:
parent
4b985d7d71
commit
8aa81bddf2
26
cdm/wks.py
26
cdm/wks.py
|
@ -26,6 +26,7 @@ from Cryptodome.PublicKey import RSA
|
||||||
from Cryptodome.Signature import pss
|
from Cryptodome.Signature import pss
|
||||||
from Cryptodome.Util import Padding
|
from Cryptodome.Util import Padding
|
||||||
import logging
|
import logging
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
_sym_db = _symbol_database.Default()
|
_sym_db = _symbol_database.Default()
|
||||||
|
|
||||||
|
@ -856,3 +857,28 @@ def parse_manifest_ism(manifest_url):
|
||||||
encoded_string = base64.b64encode(bytes.fromhex(array_of_bytes.hex())).decode("utf-8")
|
encoded_string = base64.b64encode(bytes.fromhex(array_of_bytes.hex())).decode("utf-8")
|
||||||
|
|
||||||
return kid, stream_info_list, encoded_string
|
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}")
|
||||||
|
|
|
@ -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()
|
|
@ -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()
|
Loading…
Reference in New Issue