VideoHelp Forum




+ Reply to Thread
Results 1 to 6 of 6
  1. Member
    Join Date
    Jun 2026
    Location
    the world
    Search Comp PM
    As a request from @Ganteng (Ganteng) i checked how is delivered the Clearkey key into the legal and and free for all (with vpn) live television streaming site, no registration needed, "cubmu.com".

    Just checked the liveTV and not sure if is the same for on-demand elements in the page, that's now on you to try, some modifications will be needed.

    Most of other Clearkey sites i checked (I'm not an expert) uses base64 to encode the kid and key on the url or html body.
    Some, for security i guess, uses obfuscated scripts to hide it, but is easy to run the JS script code on page body and get the final decoded information.
    But cubmu uses part of the information (clearKey.iv, clearKey.data) on the body (__NEXT_DATA__) and a separated drmDecryptKey from JS script (live-tv-(letters and numbers).js) that can be found on header of the page, so the information is in two parts and encrypted (AES-256-CBC), not just obfuscated.

    Here is a diagram of how it works:

    Image
    [Attachment 92893 - Click to enlarge]


    Automated the process into a python script:

    Code:
    #!/usr/bin/env python3
    """
    Author: Ernestleft / idento
    
    Fetches the CubMu live-tv page, extracts the player JS to find drmDecryptKey,
    and AES-256-CBC decrypts clearKey.data to get the key.
    
    Usage:
      python decode_cubmu_drm.py --cookie <cookie_file> <url>
    
    Accepted most options of cookies types (auto-detected), i used "Get cookies.txt LOCALLY"
      - JSON:       [{"name":"x","value":"y",...},...] 
      - Netscape:   Standard curl/wget cookie jar format
      - Header:     Raw "name=value; name=value" cookie header string
    """
    import argparse
    import base64
    import datetime
    import json
    import re
    import sys
    import urllib.request
    
    DEFAULT_HEADERS = {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36 Edg/149.0.0.0",
        "sec-ch-ua": '"Microsoft Edge";v="149", "Chromium";v="149", "Not)A;Brand";v="24"',
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": '"Windows"',
    }
    
    
    def load_cookies(path):
        """Load cookies from a file, auto-detecting JSON, Netscape, or header string format."""
        with open(path, 'r', encoding='utf-8') as f:
            text = f.read().strip()
    
        # Try JSON first
        if text.startswith('['):
            try:
                cookies = json.loads(text)
                parts = []
                for c in cookies:
                    parts.append("%s=%s" % (c['name'], c['value']))
                return '; '.join(parts)
            except (json.JSONDecodeError, KeyError):
                pass
    
        # Try Netscape format
        lines = text.splitlines()
        if lines and lines[0].startswith('# Netscape'):
            parts = []
            for line in lines:
                if line.startswith('#') or not line.strip():
                    continue
                fields = line.split('\t')
                if len(fields) >= 7:
                    parts.append("%s=%s" % (fields[5], fields[6]))
            return '; '.join(parts)
    
        # Assume raw header string
        return text
    
    
    def fetch(url, cookies=None, extra_headers=None):
        """Fetch a URL and return the response body as a string."""
        headers = dict(DEFAULT_HEADERS)
        if cookies:
            headers["Cookie"] = cookies
        if extra_headers:
            headers.update(extra_headers)
        req = urllib.request.Request(url, headers=headers)
        with urllib.request.urlopen(req) as resp:
            return resp.read().decode("utf-8")
    
    
    def extract_next_data(html):
        """Extract and parse the __NEXT_DATA__ JSON from the HTML."""
        match = re.search(
            r'<script\s+id="__NEXT_DATA__"\s+type="application/json">(.*?)</script>',
            html, re.DOTALL
        )
        if not match:
            raise ValueError("No __NEXT_DATA__ script tag found in HTML")
        return json.loads(match.group(1))
    
    
    def find_player_js_url(html):
        """Find the live-tv player JS chunk URL from the HTML."""
        match = re.search(
            r'<script\s+src="(/_next/static/chunks/pages/play/live-tv-[^"]+)"',
            html
        )
        if not match:
            raise ValueError("No live-tv player JS chunk found in HTML")
        return "https://www.cubmu.com" + match.group(1)
    
    
    def extract_drm_key(js_text):
        """Extract drmDecryptKey value from the player JS."""
        match = re.search(r'drmDecryptKey\s*:\s*"([^"]+)"', js_text)
        if not match:
            raise ValueError("drmDecryptKey not found in player JS")
        return match.group(1)
    
    
    def safe_b64decode(s):
        """Base64 decode with URL-safe variant support."""
        padded = s.replace('-', '+').replace('_', '/')
        missing = len(padded) % 4
        if missing:
            padded += '=' * (4 - missing)
        return base64.b64decode(padded)
    
    
    def main():
        parser = argparse.ArgumentParser(
            description="Decrypt CubMu ClearKey DRM from live URL."
        )
        parser.add_argument("url", help="CubMu live-tv page URL")
        parser.add_argument("--cookie", "-c", metavar="FILE",
                            help="Cookie file (JSON, Netscape, or header string)")
        args = parser.parse_args()
    
        cookies = load_cookies(args.cookie) if args.cookie else None
    
        # --- Fetch the live-tv HTML page ---
        print("Fetching page: %s" % args.url)
        html = fetch(args.url, cookies=cookies)
        page_data = extract_next_data(html)
        page = page_data['props']['pageProps']
    
        # --- Find and fetch the player JS chunk ---
        js_url = find_player_js_url(html)
        print("Fetching player JS: %s" % js_url)
        js_text = fetch(js_url, extra_headers={"Referer": args.url})
    
        # --- Extract drmDecryptKey from player JS ---
        drm_key = extract_drm_key(js_text)
    
        # --- Extract manifest + clearKey from page data ---
        manifest_url = page.get('manifest', '')
        ck = page.get('clearKey', {})
        ck_data_b64 = ck.get('data', '')
        ck_iv_b64 = ck.get('iv', '')
    
        if not ck_data_b64 or not ck_iv_b64:
            print("Error: No clearKey data or IV found in page.")
            sys.exit(1)
    
        dk_raw = drm_key.encode('utf-8')
        ck_data = safe_b64decode(ck_data_b64)
        iv = safe_b64decode(ck_iv_b64)
    
        # --- Output ---
        print()
        print("[DRM Decrypt Key]")
        print("  String:  %s" % drm_key)
        print("  Hex:     %s" % dk_raw.hex())
        print("  Length:  %d bytes (%d bits)" % (len(dk_raw), len(dk_raw) * 8))
        print()
    
        print("=" * 70)
        print("DECRYPTING clearKey.data")
        print("=" * 70)
        print()
        print("Method: AES-256-CBC")
        print("Key:    drmDecryptKey as raw string bytes (%d bytes)" % len(dk_raw))
        print("IV:     clearKey.iv (%s)" % iv.hex())
        print()
    
        # --- Decrypt ---
        try:
            from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
        except ImportError:
            print("Error: 'cryptography' library not available. Install with:")
            print("  pip install cryptography")
            sys.exit(1)
    
        try:
            cipher = Cipher(algorithms.AES(dk_raw), modes.CBC(iv))
            dec = cipher.decryptor()
            pt = dec.update(ck_data) + dec.finalize()
    
            # Remove PKCS7 padding
            pad_len = pt[-1]
            if 1 <= pad_len <= 16 and all(b == pad_len for b in pt[-pad_len:]):
                pt = pt[:-pad_len]
    
            print("Decrypted ClearKey License (%d bytes):" % len(pt))
            print()
    
            license_json = json.loads(pt.decode('utf-8'))
            print(json.dumps(license_json, indent=2))
            print()
    
            for i, k_entry in enumerate(license_json.get('keys', [])):
                print("--- Content Key #%d ---" % (i + 1))
                print("  kid (Key ID): %s" % k_entry.get('kid', 'N/A'))
                print("  k   (Key):    %s" % k_entry.get('k', 'N/A'))
                print("  kty (Type):   %s" % k_entry.get('kty', 'N/A'))
                if 'k' in k_entry:
                    raw_key = safe_b64decode(k_entry['k'])
                    print("  k decoded:    %s (%d bytes)" % (raw_key.hex(), len(raw_key)))
                if 'kid' in k_entry:
                    raw_kid = safe_b64decode(k_entry['kid'])
                    print("  kid decoded:  %s (%d bytes)" % (raw_kid.hex(), len(raw_kid)))
                print()
    
            if 'exp' in license_json:
                exp_ts = license_json['exp']
                try:
                    exp_dt = datetime.datetime.fromtimestamp(exp_ts)
                    print("Expires: %s (timestamp: %d)" % (exp_dt.isoformat(), exp_ts))
                except Exception:
                    print("Expires: timestamp %d" % exp_ts)
            if 'type' in license_json:
                print("Type: %s" % license_json['type'])
    
        except Exception as e:
            print("AES-256-CBC decryption failed: %s" % e)
            sys.exit(1)
    
        print()
        print("Done.")
        print()
        print("Manifest: %s" % manifest_url)
        print()
    
        # --- kid:key pair ---
        raw_key = safe_b64decode(license_json['keys'][0]['k'])
        raw_kid = safe_b64decode(license_json['keys'][0]['kid'])
        print("%s:%s" % (raw_kid.hex(), raw_key.hex()))
    
    
    if __name__ == '__main__':
        main()
    And used like:

    Code:
    python decode_cubmu_drm.py --cookie www.cubmu.com_cookies_JSON.json "https://www.cubmu.com/play/live-tv?id=250&genre_id=2"
    From my test all the channels used the same kid:key
    Code:
    "7c2f241060c7426c8fa95e8102980eea:3a4079ae796a41c0bda323bce107074f"
    Hope this help other people!

    Greetings from Latin America!
    Last edited by idento; 3rd Jul 2026 at 09:12. Reason: typos, sorry
    Quote Quote  
  2. Thanks a ton for the super detailed explanation, it really cleared things up for me. I’ll take it slow and go through everything step by step so I can really get the hang of it. Appreciate your effort, it helps a lot!
    Quote Quote  
  3. Member
    Join Date
    Jun 2026
    Location
    the world
    Search Comp PM
    Originally Posted by Ganteng View Post
    Thanks a ton for the super detailed explanation, it really cleared things up for me. I’ll take it slow and go through everything step by step so I can really get the hang of it. Appreciate your effort, it helps a lot!
    Good! Let me know if need something to be explained, remember to use cookie for the requests to work as intended.
    Quote Quote  
  4. Originally Posted by idento View Post
    Originally Posted by Ganteng View Post
    Thanks a ton for the super detailed explanation, it really cleared things up for me. I’ll take it slow and go through everything step by step so I can really get the hang of it. Appreciate your effort, it helps a lot!
    Good! Let me know if need something to be explained, remember to use cookie for the requests to work as intended.
    Image
    [Attachment 92896 - Click to enlarge]
    Big thanks for the script and your clear guidance! It worked perfectly, and I really appreciate the help it made things so much easier.
    Quote Quote  
  5. well done idento
    thank you very much
    Quote Quote  
  6. Update:
    The site changed its live TV route, so the old script path stopped working.
    It used to look for
    Code:
    /pages/play/live-tv/...,
    but now the page source points to:

    Code:
    /_next/static/chunks/pages/watch/live-tv/%5Bslug%5D-....js
    I updated the regex in find_player_js_url() to match the new route, and it works again.
    Code:
    def find_player_js_url(html):
        match = re.search(
            r'<script\s+src="(/_next/static/chunks/pages/(?:play|watch)/live-tv/[^"]+\.js)"',
            html
        )
        if not match:
            raise ValueError("No live-tv player JS chunk found in HTML")
        return "https://www.cubmu.com" + match.group(1)
    Big thanks to the original script author @Idento, without their script, I wouldn’t have had a solid base to tinker with and figure this out
    Quote Quote  



Similar Threads

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