104 lines
4.8 KiB
Python
104 lines
4.8 KiB
Python
|
from uuid import UUID
|
||
|
|
||
|
from Cryptodome.Random import get_random_bytes, random
|
||
|
|
||
|
from vinetrimmer.utils.widevine.device import LocalDevice
|
||
|
from vinetrimmer.utils.widevine.session import Session
|
||
|
|
||
|
import requests
|
||
|
import json
|
||
|
import base64
|
||
|
|
||
|
|
||
|
class Cdm:
|
||
|
#system_id = b"\xed\xef\x8b\xa9\x79\xd6\x4a\xce\xa3\xc8\x27\xdc\xd5\x1d\x21\xed"
|
||
|
system_id = b"\x9a\x04\xf0\x79\x98\x40\x42\x86\xab\x92\xe6\x5b\xe0\x88\x5f\x95"
|
||
|
uuid = UUID(bytes=system_id)
|
||
|
urn = f"urn:uuid:{uuid}"
|
||
|
service_certificate_challenge = b"\x08\x04"
|
||
|
common_privacy_cert = ("CAUSxwUKwQIIAxIQFwW5F8wSBIaLBjM6L3cqjBiCtIKSBSKOAjCCAQoCggEBAJntWzsyfateJO/DtiqVtZhSCtW8y"
|
||
|
"zdQPgZFuBTYdrjfQFEEQa2M462xG7iMTnJaXkqeB5UpHVhYQCOn4a8OOKkSeTkwCGELbxWMh4x+Ib/7/up34QGeHl"
|
||
|
"eB6KRfRiY9FOYOgFioYHrc4E+shFexN6jWfM3rM3BdmDoh+07svUoQykdJDKR+ql1DghjduvHK3jOS8T1v+2RC/TH"
|
||
|
"hv0CwxgTRxLpMlSCkv5fuvWCSmvzu9Vu69WTi0Ods18Vcc6CCuZYSC4NZ7c4kcHCCaA1vZ8bYLErF8xNEkKdO7Dev"
|
||
|
"Sy8BDFnoKEPiWC8La59dsPxebt9k+9MItHEbzxJQAZyfWgkCAwEAAToUbGljZW5zZS53aWRldmluZS5jb20SgAOuN"
|
||
|
"HMUtag1KX8nE4j7e7jLUnfSSYI83dHaMLkzOVEes8y96gS5RLknwSE0bv296snUE5F+bsF2oQQ4RgpQO8GVK5uk5M"
|
||
|
"4PxL/CCpgIqq9L/NGcHc/N9XTMrCjRtBBBbPneiAQwHL2zNMr80NQJeEI6ZC5UYT3wr8+WykqSSdhV5Cs6cD7xdn9"
|
||
|
"qm9Nta/gr52u/DLpP3lnSq8x2/rZCR7hcQx+8pSJmthn8NpeVQ/ypy727+voOGlXnVaPHvOZV+WRvWCq5z3CqCLl5"
|
||
|
"+Gf2Ogsrf9s2LFvE7NVV2FvKqcWTw4PIV9Sdqrd+QLeFHd/SSZiAjjWyWOddeOrAyhb3BHMEwg2T7eTo/xxvF+YkP"
|
||
|
"j89qPwXCYcOxF+6gjomPwzvofcJOxkJkoMmMzcFBDopvab5tDQsyN9UPLGhGC98X/8z8QSQ+spbJTYLdgFenFoGq4"
|
||
|
"7gLwDS6NWYYQSqzE3Udf2W7pzk4ybyG4PHBYV3s4cyzdq8amvtE/sNSdOKReuHpfQ=")
|
||
|
|
||
|
def __init__(self, device):
|
||
|
"""Create a Widevine Content Decryption Module using a specific devices data."""
|
||
|
self.sessions = {}
|
||
|
self.device = device
|
||
|
|
||
|
def open(self, pssh, raw=False, offline=False):
|
||
|
"""
|
||
|
Open a CDM session with the specified pssh box.
|
||
|
Multiple sessions can be active at the same time.
|
||
|
|
||
|
Parameters:
|
||
|
pssh: PSSH Data, either a full WidevineCencHeader or a full mp4 pssh box.
|
||
|
raw: If the PSSH Data is incomplete, e.g. NF Key Exchange, set this to True.
|
||
|
offline: 'OFFLINE' License Type field value.
|
||
|
|
||
|
Returns:
|
||
|
New Session ID.
|
||
|
"""
|
||
|
session_id = self.create_session_id(self.device)
|
||
|
self.sessions[session_id] = Session(session_id, pssh, raw, offline)
|
||
|
return session_id
|
||
|
|
||
|
def close(self, session_id):
|
||
|
"""
|
||
|
Close a CDM session.
|
||
|
:param session_id: Session to close.
|
||
|
:returns: True if Successful.
|
||
|
"""
|
||
|
if self.is_session_open(session_id):
|
||
|
self.sessions.pop(session_id)
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
def is_session_open(self, session_id):
|
||
|
return session_id in self.sessions
|
||
|
|
||
|
def set_service_certificate(self, session_id, certificate):
|
||
|
if not self.is_session_open(session_id):
|
||
|
raise ValueError(f"There's no session with the id [{session_id!r}]...")
|
||
|
return self.device.set_service_certificate(self.sessions[session_id], certificate)
|
||
|
|
||
|
def get_license_challenge(self, session_id):
|
||
|
if not self.is_session_open(session_id):
|
||
|
raise ValueError(f"There's no session with the id [{session_id!r}]...")
|
||
|
return self.device.get_license_challenge(self.sessions[session_id])
|
||
|
|
||
|
def parse_license(self, session_id, license_res):
|
||
|
if not self.is_session_open(session_id):
|
||
|
raise ValueError(f"There's no session with the id [{session_id!r}]...")
|
||
|
return self.device.parse_license(self.sessions[session_id], license_res)
|
||
|
|
||
|
def get_keys(self, session_id, content_only=False):
|
||
|
if not self.is_session_open(session_id):
|
||
|
raise ValueError(f"There's no session with the id [{session_id!r}]...")
|
||
|
keys = self.sessions[session_id].keys
|
||
|
if content_only:
|
||
|
return [x for x in keys if x.type == "CONTENT"]
|
||
|
return keys
|
||
|
|
||
|
@staticmethod
|
||
|
def create_session_id(device):
|
||
|
if device.type == LocalDevice.Types.ANDROID:
|
||
|
session_id = "{hex:16X}{counter}".format(
|
||
|
hex=random.getrandbits(64),
|
||
|
counter="01" # counter, this resets regularly so it's fine to use 01
|
||
|
)
|
||
|
session_id.ljust(32, "0") # pad to 16 bytes (32 chars)
|
||
|
return session_id.encode("ascii")
|
||
|
if device.type == LocalDevice.Types.CHROME:
|
||
|
return get_random_bytes(16)
|
||
|
if device.type == LocalDevice.Types.PLAYREADY:
|
||
|
return get_random_bytes(16)
|
||
|
raise ValueError(f"Device Type {device.type.name} is not implemented")
|