VideoHelp Forum


Try StreamFab Downloader and download from Netflix, Amazon, Youtube! Or Try DVDFab and copy Blu-rays! or rip iTunes movies!


Try StreamFab Downloader and download streaming video from Youtube, Netflix, Amazon! Download free trial.


+ Reply to Thread
Results 1 to 13 of 13
Thread
  1. Member
    Join Date
    Feb 2022
    Location
    Search the forum first!
    Search PM
    In the process of re-writing and updating Temple of Doom I have two versions of l3 py for use with pywidevine; one when everything is working, and another to include full debugging information so you may see where the fail point lies. For those just starting out it should help.

    Both use a headers.py file, in the same folder, to contain the headers needed for permissive access to the license server. (See curlconverter.com)
    You will also, of course, have a wvd file created from your CDM using pywidevine, the path to which needs editing in both files.

    Normal version l3.py
    Code:
    from pywidevine.cdm import Cdm
    from pywidevine.device import Device
    from pywidevine.pssh import PSSH
    import httpx
    
    ####################
    #BE SURE TO EDIT THIS !!!!
    ####################
    WVD_PATH = "<your path to>/device.wvd"
    
    from headers import headers
    
    def get_key(pssh, license_url):
        device = Device.load(WVD_PATH)
        cdm = Cdm.from_device(device)
        session_id = cdm.open()
    
        challenge = cdm.get_license_challenge(session_id, PSSH(pssh))
        response = httpx.post(license_url, data=challenge, headers=headers)
        cdm.parse_license(session_id, response.content)
    
        keys = []
        for key in cdm.get_keys(session_id):
            if key.type == 'CONTENT':
                keys.append(f"--key {key.kid.hex}:{key.key.hex()}")
    
        cdm.close(session_id)
        return "\n".join(keys)
    
    if __name__ == "__main__":
        pssh_str = input("PSSH? ")
        lic_url = input("License URL? ")
        result = get_key(pssh_str, lic_url)
        print(result)
    Debug Version l3_debug.py
    Code:
     
    import logging
    from pywidevine.cdm import Cdm
    from pywidevine.device import Device
    from pywidevine.pssh import PSSH
    import httpx
    from headers import headers
    
    logging.basicConfig(level=logging.DEBUG)
    logger = logging.getLogger(__name__)
    
    ####################
    #BE SURE TO EDIT THIS!!!!! 
    ####################
    WVD_PATH = "<path to your>/device.wvd"
    
    
    
    def get_key(pssh, license_url):
        logger.debug("Loading device...")
        device = Device.load(WVD_PATH)
        cdm = Cdm.from_device(device)
        session_id = cdm.open()
        logger.debug("Session opened...")
    
        challenge = cdm.get_license_challenge(session_id, PSSH(pssh))
        response = httpx.post(license_url, data=challenge, headers=headers)
        cdm.parse_license(session_id, response.content)
    
        keys = []
        logger.debug("Retrieving keys...")
        for key in cdm.get_keys(session_id):
            logger.debug(f"Key found: {key.kid.hex}:{key.key.hex()}, Type: {key.type}")
            if key.type == 'CONTENT':
                keys.append(f"--key {key.kid.hex}:{key.key.hex()}")
    
        cdm.close(session_id)
        logger.debug("Session closed...")
        return "\n".join(keys)
    
    if __name__ == "__main__":
        pssh_str = input("PSSH? ")
        lic_url = input("License URL? ")
        result = get_key(pssh_str, lic_url)
        logger.debug("Result:")
        print(result)
    Each site needs its own new headers file - headers.py placed in the same folder as l3_debug and l3.py
    A headers.py looks something like this:-
    headers={
    'authority': 'www.itv.com',
    'user-agent': 'Dalvik/2.9.8 (Linux; U; Android 9.9.2; ALE-L94 Build/NJHGGF)',
    'accept': '*/*',
    'accept-language': 'en-GB,en-US;q=0.9,en;q=0.8',
    'Referer': 'https://www.itv.com/',
    }
    But every site needs its own version created by copying cURL of the license into curlconverter.com

    Running l3_debug for tg4.ie produces this_
    PSSH? AAAAVnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADYIARIQka QzESAPRdadcqc8IdLlBRoNd2lkZXZpbmVfdGVzdCIIMTIzNDU2 NzgyB2RlZmF1bHQ=<
    License URL? https://manifest.prod.boltdns.net/license/v1/cenc/widevine/1555966122001/38dfe47f-f6c1...Y0MzY1NA%3D%3D
    DEBUG:__main__:Loading device...
    DEBUG:__main__:Session opened...
    DEBUG:httpx:load_ssl_context verify=True cert=None trust_env=True http2=False
    DEBUG:httpx:load_verify_locations cafile='/home/angela/.local/lib/python3.12/site-packages/certifi/cacert.pem'
    DEBUG:httpcore.connection:connect_tcp.started host='manifest.prod.boltdns.net' port=443 local_address=None timeout=5.0 socket_options=None
    DEBUG:httpcore.connection:connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x7f6f850b02c0>
    DEBUG:httpcore.connectiontart_tls.started ssl_context=<ssl.SSLContext object at 0x7f6f852d1e50> server_hostname='manifest.prod.boltdns.net' timeout=5.0
    DEBUG:httpcore.connectiontart_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x7f6f86fe1e80>
    DEBUG:httpcore.http11end_request_headers.started request=<Request [b'POST']>
    DEBUG:httpcore.http11end_request_headers.complete
    DEBUG:httpcore.http11end_request_body.started request=<Request [b'POST']>
    DEBUG:httpcore.http11end_request_body.complete
    DEBUG:httpcore.http11:receive_response_headers.sta rted request=<Request [b'POST']>
    DEBUG:httpcore.http11:receive_response_headers.com plete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Connection', b'keep-alive'), (b'Content-Length', b'690'), (b'Access-Control-Allow-Origin', b'*'), (b'Access-Control-Expose-Headers', b'*'), (b'Bcov-Request-Id', b'74996227-6345-429a-ac26-5acd6637b1e2'), (b'Cache-Control', b'private, max-age=5988'), (b'Content-Type', b'application/octet-stream'), (b'X-Hosted-On', b'Gantry'), (b'X-Powered-By', b'BC'), (b'X-Powered-From', b'eu-west-1c'), (b'Accept-Ranges', b'bytes'), (b'Date', b'Wed, 05 Jun 2024 10:57:26 GMT'), (b'Via', b'1.1 varnish'), (b'X-Served-By', b'cache-lon420096-LON'), (b'X-Cache', b'MISS'), (b'X-Cache-Hits', b'0'), (b'X-Timer', b'S1717585046.217263,VS0,VE62'), (b'X-Device-Group', b'desktop-firefox')])
    INFO:httpx:HTTP Request: POST https://manifest.prod.boltdns.net/license/v1/cenc/widevine/1555966122001/38dfe47f-f6c1...Y0MzY1NA%3D%3D "HTTP/1.1 200 OK"
    DEBUG:httpcore.http11:receive_response_body.starte d request=<Request [b'POST']>
    DEBUG:httpcore.http11:receive_response_body.comple te
    DEBUG:httpcore.http11:response_closed.started
    DEBUG:httpcore.http11:response_closed.complete
    DEBUG:httpcore.connection:close.started
    DEBUG:httpcore.connection:close.complete
    DEBUG:__main__:Retrieving keys...
    DEBUG:__main__:Key found: 00000000000000000000000000000000:4a5943fb90b54f114 c167e4423dc64c1d4ca4c7462e34fddc6528a2df6e8151eede f8382e0941e7d8c659e16fbc41b9db489c84529b1029b9c5a3 1025b5f99f5, Type: SIGNING
    DEBUG:__main__:Key found: 91a43311200f45d69d72a73c21d2e505:c5ea6154a0aa8e563 59cf15ce5bd51bf, Type: CONTENT
    DEBUG:__main__:Session closed...
    DEBUG:__main__:Result:
    --key 91a43311200f45d69d72a73c21d2e505:c5ea6154a0aa8e563 59cf15ce5bd51bf
    And l3.py tested at bitmovin.com produces this:-
    py l3.py
    PSSH? AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62 dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xq YVNkZmFsa3IzaioCSEQyAA==
    License URL? https://cwip-shaka-proxy.appspot.com/no_auth
    --key ccbf5fb4c2965be7aa130ffb3ba9fd73:9cc0c92044cb1d694 33f5f5839a159df
    --key 9bf0e9cf0d7b55aeb4b289a63bab8610:90f52fd8ca48717b2 1d0c2fed7a12ae1
    --key eb676abbcb345e96bbcf616630f1a3da:100b6c20940f779a4 589152b57d2dacb
    --key 0294b9599d755de2bbf0fdca3fa5eab7:3bda2f40344c7def6 14227b9c0f03e26
    --key 639da80cf23b55f3b8cab3f64cfa5df6:229f5f29b643e2030 04b30c4eaf348f4
    All files are here https://files.videohelp.com/u/301890/L3_PY.zip
    Last edited by A_n_g_e_l_a; 5th Jun 2024 at 06:10.
    Noob Starter Pack. Just download everything DRM Not kidding!.
    https://files.videohelp.com/u/301890/hellyes2.zip
    Quote Quote  
  2. Thanks @A_n_g_e_l_a

    Here are some extra tips for newbies.
    1. Get ready to use CDM from this thread
    2. To generate WVD from CDM - refer this thread
    3. How to use curlconverter.com to generate required headers - refer this post
    Last edited by LittleSoldier; 5th Jun 2024 at 10:00. Reason: fixed broken link of curlconverter.com
    Quote Quote  
  3. A somewhat extended version of Angela's script that can handle more variants of how the challenge is posted to the license server and license data is returned back:

    Code:
    from pywidevine.cdm import Cdm
    from pywidevine.device import Device
    from pywidevine.pssh import PSSH
    import base64
    import httpx
    import re
    import urllib.parse
    
    ####################
    #BE SURE TO EDIT THIS !!!!
    ####################
    WVD_PATH = "<your path to>/device.wvd"
    
    from headers import headers
    try:
        from headers import data
    except:
        data = None
    
    def get_key(pssh, license_url):
        device = Device.load(WVD_PATH)
        cdm = Cdm.from_device(device)
        session_id = cdm.open()
    
        challenge = cdm.get_license_challenge(session_id, PSSH(pssh))
        if data:
            # rte.ie e.g.
            if match := re.search(r'"(CAES.*?)"', data):
                challenge = data.replace(match.group(1), base64.b64encode(challenge).decode())
            elif match := re.search(r'=(CAES.*?)(&.*)?$', data):
                b64challenge = base64.b64encode(challenge).decode()
                quoted = urllib.parse.quote_plus(b64challenge)
                challenge = lic_ctx.data.replace(match.group(1), quoted)
    
        license = httpx.post(url=license_url, data=challenge, headers=headers)
        try:
            license.raise_for_status()
        except requests.HTTPError as e:
            raise e
    
        license_content = license.content
        try:
            # if content is returned as JSON object:
            match = re.search(r'"(CAIS.*?)"', license.content.decode('utf-8'))
            if match:
                license_content = match.group(1)
        except:
            pass
    
        cdm.parse_license(session_id, license_content)
    
        keys = []
        for key in cdm.get_keys(session_id):
            if key.type == 'CONTENT':
                keys.append(f"--key {key.kid.hex}:{key.key.hex()}")
    
        cdm.close(session_id)
        return "\n".join(keys)
    
    if __name__ == "__main__":
        pssh_str = input("PSSH? ")
        lic_url = input("License URL? ")
        result = get_key(pssh_str, lic_url)
        print(result)
    Short explanation of the added code:
    - if the license server expects the challenge as base64 encoded value either in a JSON object or as form data (can be seen, if the data contains "CAES..."), replace the original challenge data from the browser in the expected format by the challenge that our own CDM produced.
    - if the response from the license server contains the license data in base64 format (can be seen, if it contains "CAIS..."), then only the relevant license data is extracted before parsing the license.

    This is usually meant when talking about "you need a custom script".

    rte.ie e.g. needs the challenge to be posted as JSON object and returns the license data as JSON object.
    Last edited by Obo; 5th Jun 2024 at 12:38.
    Quote Quote  
  4. Member
    Join Date
    Feb 2022
    Location
    Search the forum first!
    Search PM
    And moving from Obo's complex solution to the simplest possible; I note that pywidevine itself will fetch keys for sites that need no headers and talk together in bytes.

    Code:
    pywidevine license -t STREAMING <path to >/device.wvd pssh license_url
    Image
    [Attachment 79715 - Click to enlarge]
    Noob Starter Pack. Just download everything DRM Not kidding!.
    https://files.videohelp.com/u/301890/hellyes2.zip
    Quote Quote  
  5. Search, Learn, Download! Karoolus's Avatar
    Join Date
    Oct 2022
    Location
    Belgium
    Search Comp PM
    Originally Posted by A_n_g_e_l_a View Post
    And moving from Obo's complex solution to the simplest possible; I note that pywidevine itself will fetch keys for sites that need no headers and talk together in bytes.

    Code:
    pywidevine license -t STREAMING <path to >/device.wvd pssh license_url
    Image
    [Attachment 79715 - Click to enlarge]

    Now this is new information to me. Thanks for that!
    Quote Quote  
  6. Digging into that - thank you!
    Quote Quote  
  7. Member
    Join Date
    Feb 2022
    Location
    Search the forum first!
    Search PM
    Originally Posted by Obo View Post
    A somewhat extended version of Angela's script that can handle more variants of how the challenge is posted to the license server and license data is returned back:
    All well and good if it works as posted. There two errors
    1. except requests.HTTPError as e:
      I happen to use httpx so requests has not been imported
    2. lic_ctx
      lic_ctx is an unknown variable. So wherever you pasted this from, you didn't catch the initialization.

    This adaptation works as obo intended.
    Code:
    """
        Retrieves the keys from a license server using the provided PSSH and license URL.
        Args:
            pssh (str): The PSSH (Protection System Specific Header) of the content.
            license_url (str): The URL of the license server.
        Returns:
            str: A string containing the keys in the format "--key <kid>:<key>".
        Raises:
            httpx.HTTPStatusError: If the HTTP request to the license server fails.
        Be sure to populate headers.py with a cURL of the license URL converted at curlconverter.com
        """
    
    from pywidevine.cdm import Cdm
    from pywidevine.device import Device
    from pywidevine.pssh import PSSH
    import base64
    import httpx
    import re
    import urllib.parse
    
    ####################
    #BE SURE TO EDIT THIS !!!!
    ####################
    WVD_PATH = "<path to your>/device.wvd"
    global data
    from headers import headers
    try:
        from headers import data
    except: 
        data = None
    
    
    def get_key(pssh, license_url):
        device = Device.load(WVD_PATH)
        cdm = Cdm.from_device(device)
        session_id = cdm.open()
    
        challenge = cdm.get_license_challenge(session_id, PSSH(pssh))
    
        if data:
            # rte.ie e.g.
            if match := re.search(r'"(CAES.*?)"', data):
                challenge = data.replace(match.group(1), base64.b64encode(challenge).decode())
            elif match := re.search(r'=(CAES.*?)(&.*)?$', data):
                b64challenge = base64.b64encode(challenge).decode()
                quoted = urllib.parse.quote_plus(b64challenge)
                challenge = data.replace(match.group(1), quoted)
    
        # Prepare the final payload
        payload = challenge if data is None else challenge
    
        license_response = httpx.post(url=license_url, data=payload, headers=headers)
        try:
            license_response.raise_for_status()
        except httpx.HTTPStatusError as e:
            raise e
    
        license_content = license_response.content
        try:
            # if content is returned as JSON object:
            match = re.search(r'"(CAIS.*?)"', license_response.content.decode('utf-8'))
            if match:
                license_content = base64.b64decode(match.group(1))
        except:
            pass
    
        # Ensure license_content is in the correct format
        if isinstance(license_content, str):
            license_content = base64.b64decode(license_content)
    
        cdm.parse_license(session_id, license_content)
    
        keys = []
        for key in cdm.get_keys(session_id):
            if key.type == 'CONTENT':
                keys.append(f"--key {key.kid.hex}:{key.key.hex()}")
    
        cdm.close(session_id)
        return "\n".join(keys)
    
    if __name__ == "__main__":
        pssh_str = input("PSSH? ")
        lic_url = input("License URL? ")
        result = get_key(pssh_str, lic_url)
        print(result)
    Last edited by A_n_g_e_l_a; 7th Jun 2024 at 09:19. Reason: addded reviced code from Obo
    Noob Starter Pack. Just download everything DRM Not kidding!.
    https://files.videohelp.com/u/301890/hellyes2.zip
    Quote Quote  
  8. Yes thanks, I don't have httpx installed (pywidevine uses "requests" as well) and changed the import to an alias of requests, when I edited the script

    The lic_ctx was a Dataclass that I now have removed as well, didn't test it after the change.

    Locally I've refactored it further: the script now has two functions "prepare_challenge" and "process_license_response" to factor out the base64 stuff. Makes it more readable. Signature of the get_keys function now is (I pass the cdm from outside, because I've put this stuff into a package file):

    Code:
    def get_keys(
            pssh: str | PSSH,
            cdm: Cdm,
            url: str,
            params: dict = None,
            headers: dict = None,
            data: str = None
    ) -> list:
    Sorry for the inconvenience
    Quote Quote  
  9. Member
    Join Date
    Feb 2022
    Location
    Search the forum first!
    Search PM
    I've added the grand-daddy of l3s to UK-FTA but standalone is newly posted here https://forum.videohelp.com/threads/407216-Decryption-The-Dungeon-of-Despair#post2669285 I call it hell3.py
    Noob Starter Pack. Just download everything DRM Not kidding!.
    https://files.videohelp.com/u/301890/hellyes2.zip
    Quote Quote  
  10. Originally Posted by A_n_g_e_l_a View Post
    I've added the grand-daddy of l3s to UK-FTA but standalone is newly posted here https://forum.videohelp.com/threads/407216-Decryption-The-Dungeon-of-Despair#post2669285 I call it hell3.py
    As always excellent work @A_n_g_e_l_a

    P.S. - Please fix the code formatting in that post it's unreadable
    Quote Quote  
  11. Member
    Join Date
    Feb 2022
    Location
    Search the forum first!
    Search PM
    Originally Posted by LittleSoldier View Post

    P.S. - Please fix the code formatting in that post it's unreadable
    Thanks What a mess it was!! I had changed the editor in my profile settings for wysiwyg but now reverted to standard.
    Noob Starter Pack. Just download everything DRM Not kidding!.
    https://files.videohelp.com/u/301890/hellyes2.zip
    Quote Quote  
  12. Originally Posted by A_n_g_e_l_a View Post

    Thanks What a mess it was!! I had changed the editor in my profile settings for wysiwyg but now reverted to standard.
    All good now

    Thanks for considering the suggestion
    Last edited by LittleSoldier; 14th Jun 2024 at 11:52.
    Quote Quote  



Similar Threads

Visit our sponsor! Try DVDFab and backup Blu-rays!