#!/usr/bin/env python3 """ HTTP server on port 8765: - serves static files (drm_test.html etc.) - POST /capture → saves challenge, extracts ClientIdentification as client_id.bin """ import http.server, socketserver, os, sys, urllib.request, urllib.error from pathlib import Path OUT = Path(__file__).parent / 'L3' OUT.mkdir(exist_ok=True) PORT = 8765 class Handler(http.server.SimpleHTTPRequestHandler): def do_POST(self): if self.path == '/proxy': # CORS proxy: forward binary body to TVP license server and return response TVP_LICENSE = 'https://vod.tvp.pl/api/products/2993670/drm/widevine/external?platform=BROWSER&type=MOVIE' length = int(self.headers.get('Content-Length', 0)) body = self.rfile.read(length) print(f' [proxy] {len(body)}b → {TVP_LICENSE[:60]}') try: req = urllib.request.Request(TVP_LICENSE, data=body, headers={'Content-Type': 'application/octet-stream'}, method='POST') with urllib.request.urlopen(req, timeout=10) as resp: resp_body = resp.read() print(f' [proxy] response: {resp.status} {len(resp_body)}b') self.send_response(resp.status) self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Content-Type', 'application/octet-stream') self.send_header('Content-Length', str(len(resp_body))) self.end_headers() self.wfile.write(resp_body) except Exception as e: print(f' [proxy] error: {e}') self.send_response(502) self.send_header('Access-Control-Allow-Origin', '*') self.end_headers() return if self.path == '/capture': length = int(self.headers.get('Content-Length', 0)) data = self.rfile.read(length) print(f'\n[+] Received {len(data)} bytes challenge head={data[:16].hex()}') (OUT / 'challenge.bin').write_bytes(data) print(f' Saved → L3/challenge.bin') # Parse SignedMessage → LicenseRequest → ClientIdentification try: from pywidevine import license_protocol_pb2 as pb sm = pb.SignedMessage() sm.MergeFromString(data) print(f' SignedMessage.type={sm.type} msg={len(sm.msg)}b sig={len(sm.signature)}b') lr = pb.LicenseRequest() lr.MergeFromString(sm.msg) ci = lr.client_id ci_bytes = ci.SerializeToString() print(f' ClientIdentification: type={ci.type} token={len(ci.token)}b total={len(ci_bytes)}b') print(f' CI head: {ci_bytes[:16].hex()}') if len(ci_bytes) > 50: (OUT / 'client_id.bin').write_bytes(ci_bytes) print(f'[+] Saved → L3/client_id.bin') # Try pywidevine create-device import subprocess r = subprocess.run(['pywidevine', 'create-device', '-t', 'android', '-l', '3', '-k', str(OUT / 'private_key.der'), '-c', str(OUT / 'client_id.bin'), '-o', str(OUT)], capture_output=True, text=True) if r.returncode == 0: print('\n[+] pywidevine create-device SUCCESS!') print(r.stdout[-600:]) else: print('\n[!] pywidevine create-device FAILED:') print(r.stderr[-400:]) else: print(f'[!] ClientIdentification too small ({len(ci_bytes)} bytes)') print(f' Full LicenseRequest fields:') for f in lr.DESCRIPTOR.fields: try: val = getattr(lr, f.name) if val: print(f' {f.name}: {str(val)[:80]}') except: pass except Exception as e: import traceback print(f'[!] Parse error: {e}') traceback.print_exc() self.send_response(200) self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Content-Type', 'text/plain') self.end_headers() self.wfile.write(b'OK') else: self.send_response(404) self.end_headers() def do_OPTIONS(self): self.send_response(200) self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') self.send_header('Access-Control-Allow-Headers', 'Content-Type') self.end_headers() def log_message(self, fmt, *args): if '/capture' in str(args): pass # already logged above else: super().log_message(fmt, *args) os.chdir(Path(__file__).parent) print(f'[*] Serving on port {PORT} (static files + POST /capture)') print(f'[*] In emulator, run:') print(f' adb reverse tcp:{PORT} tcp:{PORT}') print(f' Then open http://localhost:{PORT}/drm_test.html\n') with socketserver.TCPServer(('', PORT), Handler) as httpd: try: httpd.serve_forever() except KeyboardInterrupt: print('\n[!] Stopped.')