diff --git a/lib/streaming_providers/providers/discovery/auth.py b/lib/streaming_providers/providers/discovery/auth.py index 828225d..c2480f1 100644 --- a/lib/streaming_providers/providers/discovery/auth.py +++ b/lib/streaming_providers/providers/discovery/auth.py @@ -5,6 +5,8 @@ Discovery+ Authentication Handles authentication for Discovery+ including anonymous and user credentials. Supports two-step token negotiation with session headers. """ +import base64 +import hashlib import json import time import uuid @@ -571,6 +573,146 @@ class DiscoveryAuthenticator(BaseAuthenticator): except Exception as e: logger.warning(f"Feature flags fetch failed, x-gisdk will be omitted: {e}") + @staticmethod + def _build_bda(user_agent: str) -> str: + """ + Build an Arkose browser data (bda) fingerprint payload. + + Arkose requires a bda field containing AES-encrypted JSON fingerprint + data. The encryption key is: userAgent + str(floor(now / 21600) * 21600) + — i.e. the UA concatenated with the current 6-hour window start. + + The fingerprint mirrors a real Chrome/Linux browser session on + discoveryplus.com so Arkose validates it as a legitimate client. + + Args: + user_agent: Browser User-Agent string (must match FC request header) + + Returns: + Base64-encoded encrypted bda string + """ + import os as _os + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import padding as crypto_padding + + ts_ms = int(time.time() * 1000) + ts_s = ts_ms // 1000 + n_value = base64.b64encode(str(ts_s).encode()).decode() + f_value = hashlib.md5(user_agent.encode()).hexdigest() + + enhanced_fp = [ + {"key": "user_agent_data_brands", "value": "Not(A:Brand,Chromium,Google Chrome"}, + {"key": "user_agent_data_mobile", "value": False}, + {"key": "navigator_connection_downlink", "value": 10}, + {"key": "navigator_connection_downlink_max", "value": None}, + {"key": "network_info_rtt", "value": 50}, + {"key": "network_info_save_data", "value": False}, + {"key": "network_info_rtt_type", "value": None}, + {"key": "screen_pixel_depth", "value": 24}, + {"key": "navigator_device_memory", "value": 8}, + {"key": "navigator_languages", "value": "de-DE"}, + {"key": "window_inner_width", "value": 0}, + {"key": "window_inner_height", "value": 0}, + {"key": "window_outer_width", "value": 1920}, + {"key": "window_outer_height", "value": 1040}, + {"key": "browser_detection_firefox", "value": False}, + {"key": "browser_detection_brave", "value": False}, + {"key": "f58835f", "value": hashlib.md5((user_agent + "f58835f").encode()).hexdigest()}, + {"key": "browser_object_checks", "value": hashlib.md5((user_agent + "boc").encode()).hexdigest()}, + {"key": "29s83ih9", "value": hashlib.md5(b"").hexdigest()}, + {"key": "audio_codecs", "value": "{\"ogg\":\"probably\",\"mp3\":\"probably\",\"wav\":\"probably\",\"m4a\":\"maybe\",\"aac\":\"probably\"}"}, + {"key": "audio_codecs_extended_hash", "value": hashlib.md5(b"audio_codecs_extended").hexdigest()}, + {"key": "video_codecs", "value": "{\"ogg\":\"\",\"h264\":\"probably\",\"webm\":\"probably\",\"mpeg4v\":\"\",\"mpeg4a\":\"\",\"theora\":\"\"}"}, + {"key": "video_codecs_extended_hash", "value": hashlib.md5(b"video_codecs_extended").hexdigest()}, + {"key": "media_query_dark_mode", "value": False}, + {"key": "f9bf2db", "value": "{\"pc\":\"no-preference\",\"ah\":\"hover\",\"ap\":\"fine\",\"p\":\"fine\",\"h\":\"hover\",\"u\":\"fast\",\"prm\":\"no-preference\",\"prt\":\"no-preference\",\"s\":\"enabled\",\"fc\":\"none\"}"}, + {"key": "headless_browser_phantom", "value": False}, + {"key": "headless_browser_selenium", "value": False}, + {"key": "headless_browser_nightmare_js", "value": False}, + {"key": "862f2c1", "value": 4}, + {"key": "1l2l5234ar2", "value": str(ts_ms)}, + {"key": "document__referrer", "value": ""}, + {"key": "window__ancestor_origins", "value": ["https://auth.discoveryplus.com"]}, + {"key": "window__tree_index", "value": [3]}, + {"key": "window__tree_structure", "value": "[[[]],[[]],[],[]]"}, + {"key": "window__location_href", "value": "about:srcdoc"}, + {"key": "client_config__sitedata_location_href", "value": "about:srcdoc"}, + {"key": "client_config__language", "value": "de-DE"}, + {"key": "client_config__surl", "value": "https://a4gds3vfh.discoveryplus.com"}, + {"key": "c8480e29a", "value": hashlib.md5((user_agent + "c8480e29a").encode()).hexdigest()}, + {"key": "client_config__triggered_inline", "value": False}, + {"key": "mobile_sdk__is_sdk", "value": False}, + {"key": "audio_fingerprint", "value": "124.04347527516074"}, + {"key": "navigator_battery_charging", "value": False}, + {"key": "7541c2s", "value": None}, + {"key": "1f220c9", "value": hashlib.md5((user_agent + "1f220c9").encode()).hexdigest()}, + {"key": "math_fingerprint", "value": hashlib.md5(b"math").hexdigest()}, + {"key": "supported_math_functions", "value": hashlib.md5(b"supported_math").hexdigest()}, + {"key": "3f76dd27", "value": "landscape-primary"}, + {"key": "5dd48ca0", "value": 5}, + {"key": "4b4b269e68", "value": "0d8cf6a2-ec3e-48fb-80bf-5f1c3a09af7f"}, + {"key": "6a62b2a558", "value": hashlib.md5((user_agent + "6a62b2a558").encode()).hexdigest()}, + {"key": "is_keyless", "value": False}, + {"key": "wait_for_settings", "value": True}, + {"key": "c2d2015", "value": hashlib.md5((user_agent + "c2d2015").encode()).hexdigest()}, + {"key": "43f2d94", "value": []}, + {"key": "20c15922", "value": False}, + {"key": "4f59ca8", "value": None}, + {"key": "3ea7194", "value": {"supported": True, "formats": ["HDR10", "HLG"], "isHDR": False}}, + {"key": "05d3d24", "value": hashlib.md5((user_agent + "05d3d24").encode()).hexdigest()}, + {"key": "speech_default_voice", "value": "Google Deutsch || de-DE"}, + {"key": "speech_voices_hash", "value": hashlib.md5(b"speech_voices").hexdigest()}, + {"key": "83eb055", "value": hashlib.md5((user_agent + "83eb055").encode()).hexdigest()}, + {"key": "4ca87df3d1", "value": "Ow=="}, + {"key": "867e25e5d4", "value": "Ow=="}, + {"key": "d4a306884c", "value": "Ow=="}, + ] + + fe_list = [ + "DNT:unknown", "L:de-DE", "D:24", "PR:1", "S:1920,1080", + "AS:1920,1040", "TO:-60", "SS:true", "LS:true", "IDB:true", + "B:false", "ODB:false", "CPUC:unknown", "PK:Linux x86_64", + "CFP:false", "FR:false", "FOS:false", "FB:false", + "JSF:Arial,Courier,Courier New,Helvetica,Times,Times New Roman", + "P:Chrome PDF Viewer,Chromium PDF Viewer,Microsoft Edge PDF Viewer,PDF Viewer,WebKit built-in PDF", + "T:0,false,false", "H:4", "SWF:false", + ] + + fingerprint = [ + {"key": "api_type", "value": "js"}, + {"key": "f", "value": f_value}, + {"key": "n", "value": n_value}, + {"key": "wh", "value": f"{hashlib.md5(user_agent.encode()).hexdigest()}|{hashlib.md5((user_agent + 'wh').encode()).hexdigest()}"}, + {"key": "enhanced_fp", "value": enhanced_fp}, + {"key": "fe", "value": fe_list}, + {"key": "ife_hash", "value": hashlib.md5(json.dumps(fe_list[:5]).encode()).hexdigest()}, + {"key": "jsbd", "value": "{\"HL\":5,\"NCE\":true,\"DT\":\"Authentication\",\"NWD\":\"false\",\"DMTO\":1,\"DOTO\":1}"}, + ] + + plaintext = json.dumps(fingerprint, separators=(',', ':')) + key_time = round(ts_s - (ts_s % 21600)) + key_str = user_agent + str(key_time) + + # Encrypt: derive AES-256 key via MD5 chaining (same as Arkose JS client) + salt = _os.urandom(8) + dk = key_str.encode() + salt + arr = [hashlib.md5(dk).hexdigest()] + for x in range(1, 3): + arr.append(hashlib.md5(bytes.fromhex(arr[x - 1]) + dk).hexdigest()) + result = ''.join(arr) + aes_key = bytes.fromhex(result[:64]) + iv = _os.urandom(16) + + padder = crypto_padding.PKCS7(128).padder() + padded = padder.update(plaintext.encode()) + padder.finalize() + cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend()) + enc = cipher.encryptor() + ct = enc.update(padded) + enc.finalize() + + payload = {"ct": base64.b64encode(ct).decode(), "s": salt.hex(), "iv": iv.hex()} + return base64.b64encode(json.dumps(payload).encode()).decode() + def _sign_request(self, method: str, path: str, body: Optional[str] = None) -> str: """ Build x-disco-client-id header value for a specific request. @@ -893,13 +1035,20 @@ class DiscoveryAuthenticator(BaseAuthenticator): """ import math + # Build browser data fingerprint encrypted with UA + 6hr window key + bda = self._build_bda(DISCOVERY_USER_AGENT) + + # rnd = random float in [0, 1) — mirrors what the browser JS sends + import random + rnd = str(random.random()) + # esync value = current time rounded down to the nearest hour esync_value = str(int(math.floor(time.time() / 3600) * 3600)) headers = { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "Origin": DISCOVERY_AUTH_ORIGIN, - "Referer": DISCOVERY_AUTH_REFERER, + "Referer": DISCOVERY_AUTH_REFERER + "/", "User-Agent": DISCOVERY_USER_AGENT, "x-ark-esync-value": esync_value, } @@ -910,6 +1059,8 @@ class DiscoveryAuthenticator(BaseAuthenticator): "capi_version": "3.7.8", "capi_mode": "lightbox", "style_theme": "dplus", + "rnd": rnd, + "bda": bda, "site": "null", "userbrowser": DISCOVERY_USER_AGENT, "language": "de-DE",