#!/usr/bin/env python3 """Fetch the Widevine CDM from Google's component-update server (Linux). The CloakBrowser binary is built with Widevine support, but the CDM itself is a proprietary Google component we don't redistribute. This pulls it at runtime from the same component server Chrome uses, then drops it where the wrapper's ``CLOAKBROWSER_WIDEVINE_CDM`` resolution (cloakbrowser/widevine.py) expects it: /manifest.json /_platform_specific/linux_/libwidevinecdm.so No curl/jq/unzip needed. Linux x86-64 only (Google doesn't publish the CDM for linux arm64). The Docker entrypoint runs this when CLOAKBROWSER_FETCH_WIDEVINE is set; bare-metal Linux users can run it directly. Integrity: the download is checked against the server-provided SHA-256 (over TLS). When `cryptography` is importable (it is in any pip/Docker install of cloakbrowser), the CRX3 publisher signature is additionally verified and bound to the expected Widevine app id — same trust root Chrome's component updater uses. Standalone runs without `cryptography` fall back to TLS + SHA-256. """ import argparse import hashlib import io import json import os import platform import shutil import struct import sys import tempfile import urllib.request import zipfile # Widevine CDM component id in Chromium's component updater. APP_ID = "oimompecagnajdejgnnjijobebaeigek" UPDATE_URL = "https://update.googleapis.com/service/update2/json" # Deliberately-low installed version so the server always reports an update. INSTALLED_VERSION = "1.4.9.1088" XSSI_PREFIX = ")]}'" def _arch(): """Map the host machine to the Widevine platform suffix (x86-64 only). Google's component server publishes the Linux Widevine CDM for x86-64 only — arm64/aarch64 return no update (verified: the server either reports noupdate or hands back the x86-64 binary), so reject them with a clear message rather than letting the request reach the misleading "no update available" path. """ m = platform.machine().lower() if m in ("x86_64", "amd64", "x64"): return "x64" if m in ("aarch64", "arm64", "arm"): raise SystemExit("the Widevine CDM is not published for linux arm64 (x86-64 only)") raise SystemExit(f"unsupported architecture for Widevine: {platform.machine()!r}") def _read_varint(b, i): shift = result = 0 while True: if i >= len(b): raise ValueError("truncated varint") byte = b[i]; i += 1 result |= (byte & 0x7F) << shift if not byte & 0x80: return result, i shift += 7 if shift > 63: raise ValueError("varint too long") def _parse_pb(b): """Minimal protobuf reader → {field_num: [length-delimited bytes, ...]}.""" out, i, n = {}, 0, len(b) while i < n: tag, i = _read_varint(b, i) field, wire = tag >> 3, tag & 7 if wire == 2: ln, i = _read_varint(b, i) out.setdefault(field, []).append(b[i:i + ln]); i += ln elif wire == 0: _, i = _read_varint(b, i) elif wire == 1: i += 8 elif wire == 5: i += 4 else: raise ValueError(f"unsupported protobuf wire type {wire}") return out def _crx_appid(pubkey_der): """CRX app id = first 16 bytes of SHA-256(pubkey), each nibble mapped a–p.""" digest = hashlib.sha256(pubkey_der).digest()[:16] return "".join(chr(0x61 + (byte >> 4)) + chr(0x61 + (byte & 0xF)) for byte in digest), digest def _verify_crx3(crx_bytes): """Verify the CRX3 RSA publisher signature and bind it to APP_ID. Returns True if verified, False if `cryptography` is unavailable (caller then relies on TLS + the server SHA-256). Raises SystemExit on a real failure. We verify the RSASSA-PKCS1-v1_5 / SHA-256 proof (CRX3 field 2), which is what Google signs the Widevine component with; ECDSA proofs (field 3) are not relied on. The app id is derived from the signing key — the same trust root Chrome verifies — so a non-Widevine publisher key can't satisfy the check. """ try: from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding from cryptography.exceptions import InvalidSignature except ImportError: return False if len(crx_bytes) < 12: raise SystemExit("not a CRX3 file (too short)") if crx_bytes[:4] != b"Cr24": raise SystemExit("not a CRX3 file (bad magic)") version = struct.unpack("