Hi everyone,
I'm working on a Python script to retrieve Widevine keys from a VdoCipher stream. The script seems to work well up to a certain point: it successfully parses the player URL, finds the MPD and the PSSH.
However, when I send the POST request to the license server (https://license.vdocipher.com/auth), I consistently get a 403 Forbidden error with the message: {"code":2048,"message":"Authentication failed"}.
I've tried to structure the final JSON payload and headers to mimic what a browser would send, but I must be missing something. I've confirmed that the player URL I'm using as input is active and works correctly when opened in a browser.
Any help in figuring out what's wrong with my license request would be greatly appreciated.
I'm using a real WVD, extracted from a device.
Code:https://player.vdocipher.com/v2/?otp=20160313versUSE32305v64nCQtNBmVl8bAvs0U57QsEOH1IkKIADm3TfM6vJ5FT&playbackInfo=eyJ2aWRlb0lkIjoiMGQ4YzczYmYzODBhNDE5NWEyYTZiMjk3ZWZjNzhjNjEifQ%3D%3D&player=r36ybRfkhOyoRUZa&primaryColor=7a2940
Code:# Arquivo: from pywidevine.py (VERSÃO FINAL COM PAYLOAD CORRETO) from pywidevine.device import Device from pywidevine.pssh import PSSH from pywidevine.cdm import Cdm from bs4 import BeautifulSoup from urllib.parse import urlparse, parse_qs import requests import base64 import json import re import sys # Lembre-se de manter o caminho com a barra inicial wv_CDM = "/content/samsung_sm-a556e_18.0.0@342415000_2305dd0b_34137_l3.wvd" input_url = input('Cole a URL COMPLETA do player (a que funciona no navegador): ') # Strip leading/trailing whitespace from the input URL input_url = input_url.strip() # Replace & with & in the input URL input_url = input_url.replace('&', '&') try: parsed_url = urlparse(input_url) query_params = parse_qs(parsed_url.query) otp = query_params.get('otp', [None])[0] playback_info = query_params.get('playbackInfo', [None])[0] if not otp or not playback_info: raise ValueError("A URL não contém 'otp' ou 'playbackInfo'") except Exception as e: print(f"\n[ERRO FATAL] A URL fornecida é inválida ou está incompleta: {e}") sys.exit() # Add print statements to inspect otp and playback_info print(f"\n[DEBUG] Extracted OTP: {otp}") print(f"[DEBUG] Extracted PlaybackInfo: {playback_info}") iframe_url = input_url headers_player = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'Accept-Language': 'pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3', 'Connection': 'keep-alive', 'Host': 'player.vdocipher.com', 'Referer': iframe_url, 'Sec-Fetch-Dest': 'iframe', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'cross-site', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:100.0) Gecko/20100101 Firefox/100.0', } print(f"\n[INFO] Buscando player: {iframe_url}") try: response = requests.get(iframe_url, headers=headers_player) response.raise_for_status() iframe = response.content.decode() except requests.exceptions.RequestException as e: print(f"\n[ERRO DE REDE] Falha ao acessar a URL do player: {e}") sys.exit() soup = BeautifulSoup(iframe, 'html.parser') script_tag = soup.find('script', {'type': 'application/json'}) if script_tag is None: print("\n[ERRO FATAL] Não foi possível encontrar os metadados do vídeo.") print("Causa Probável: A URL/OTP expirou. Tente com uma nova URL.") sys.exit() mpd_path = json.loads(script_tag.string) mpd_link = mpd_path['dash']['manifest'] try: get_pssh = re.search('<cenc:pssh>(.*)</cenc:pssh>', requests.get(mpd_link).text).group(1) except (AttributeError, requests.exceptions.RequestException): print(f"\n[ERRO FATAL] Não foi possível encontrar o PSSH no MPD: {mpd_link}") sys.exit() print('\n--- INFORMAÇÕES ENCONTRADAS ---') print('PSSH:', get_pssh) print(f'MPD: {mpd_link}\n') # --- DADOS PARA O PEDIDO DE LICENÇA CORRIGIDOS --- # Adicionamos 'href' e 'tech' para uma autorização válida json_data = { "otp": otp, "playbackInfo": playback_info, "href": input_url, "tech": "wv" } # --- FIM DA CORREÇÃO --- pssh = PSSH(get_pssh) device = Device.load(wv_CDM) cdm = Cdm.from_device(device) session_id = cdm.open() challenge = cdm.get_license_challenge(session_id, pssh) json_data["licenseRequest"] = base64.b64encode(challenge).decode("utf-8") # Encode the entire json_data object and put it under the 'token' key - Reverting to this structure based on the "request unwrapping failed" error and re-examining the likely correct structure token_payload = {"token": base64.b64encode(json.dumps(json_data).encode("utf-8")).decode("utf-8")} headers_license = { 'Accept': '*/*', 'Accept-Language': 'pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3', 'content-type': 'application/json', 'Host': 'license.vdocipher.com', 'Origin': 'https://player.vdocipher.com', 'Referer': iframe_url, 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-site', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:100.0) Gecko/100101 Firefox/100.0', # Corrected User-Agent typo 'vdo-sdk': 'VdoWeb/2.5.2' } print("[INFO] Solicitando licença...") # Sending json_data directly as the request body response = requests.post("https://license.vdocipher.com/auth", headers=headers_license, json=token_payload) if not response.ok: print(f'ERRO: {response}\n{response.text}') cdm.close(session_id) sys.exit() print("\n[SUCCESS] Licença recebida! Obtendo chaves...") license = base64.b64decode(response.json()["license"]) cdm.parse_license(session_id, license) for key in cdm.get_keys(session_id): if key.type != 'SIGNING': print(f"✅ [KEY]: {key.kid.hex}:{key.key.hex()}") cdm.close(session_id)
Support our site by donate $5 directly to us Thanks!!!
Try StreamFab Downloader and download streaming video from Netflix, Amazon!
Try StreamFab Downloader and download streaming video from Netflix, Amazon!
+ Reply to Thread
Results 1 to 9 of 9
-
-
Recently, VdoCipher started showing error 2074 (Please open in Android app) to license requests from Android CDMs, even in real browsers with OpenWV or Vineless. Probably makes no sense without something to do about integrity tokens.
-
just compare a license request (their DrmCertificate) from firefox with one from chrome (both on android). That's probably the reason why firefox doesn't work.
Firefox:
Code:type: DEVICE serial_number: "\261\332:\220\02...247]\200T" creation_time_seconds: 1716207363 public_key: "0\202\001\n\002\202\00...2\000\037\002\003\001\000\001" system_id: 8159 algorithm: RSA
Code:type: DEVICE serial_number: "\354\274d|.\200...316\246\214-" creation_time_seconds: 1754775788 public_key: "0\202\001\n\002...251\334\227{\002\003\001\000\001" system_id: 8159 algorithm: RSA rot_id { version: ROOT_OF_TRUST_ID_VERSION_1 key_id: 0 encrypted_unique_id: "\004\3225t\363p\022\...5-{\003" unique_id_hash: "\035\270\343\366\...;\275:\226\364\\U\226" }
Bypass HMACs, One-time-tokens and Lic.Wrapping: https://github.com/DevLARLEY/WidevineProxy2 -
hmm, if you provision Chrome again it stops working even though there's still a RootOfTrust Id present. maybe they're checking the device age?
Bypass HMACs, One-time-tokens and Lic.Wrapping: https://github.com/DevLARLEY/WidevineProxy2 -
I think they are checking the provision date rather than the device age.
It would be good security on their part to block newly provisioned devices, which prevents new dev keyboxes/certificates from being used.discord=notaghost9997 -
that's what I meant actually.
After doing some testing it turns out that they weren't using blocking newly provisioned devices (even though that would've made a lot of sense). Weirdly, the solution was to change my IP after provisioning my device again (by re-installing Chrome).
Take this diff of a challenge generated for a normal DRM site using shaka-player without any special settings and one token from VdoCipher. VdoCipher seems to not have their own License Servers since they're using the license.widevine.com certificate from Google, which means that they should be limited to what the official WV License Server responds with.
My phone with a Xiaomi Mi 9T (aka. Redmi K20) with working L1 support. Chrome seems to be able to use both my L3 Generic CDM (System ID 8159) and the L1 CDM (System ID 13398). I'm still investigating how this is chosen because debugging web pages/installing Add-Ons isn't that easy on Android.
https://www.diffchecker.com/SB7KhtZZ/
Update:
VdoCipher seems to request a video robustness of HW_SECURE_ALL in Chrome, which will use my L1 CDM. This doesn't work in Firefox for some reason (EME Logger shows an error was returned when requesting MediaKeySystemAccess)Last edited by larley; 13th Sep 2025 at 11:30.
Bypass HMACs, One-time-tokens and Lic.Wrapping: https://github.com/DevLARLEY/WidevineProxy2 -
Better report it to the Firefox android app developers.
https://github.com/mozilla-mobile/firefox-android/wiki#upcoming-migration-to-mozilla-central
https://hg-edge.mozilla.org/mozilla-central
But then again it’ll not change much if the video needs L1.discord=notaghost9997 -
Filed a report: https://bugzilla.mozilla.org/show_bug.cgi?id=1988424
Bypass HMACs, One-time-tokens and Lic.Wrapping: https://github.com/DevLARLEY/WidevineProxy2
Similar Threads
-
How to get the widevine pssh from init mp4 JUST using python script?
By ancientbanana in forum Video Streaming DownloadingReplies: 14Last Post: 20th Nov 2024, 11:57 -
Vdocipher decoding tool
By TatsP in forum Video Streaming DownloadingReplies: 5Last Post: 11th May 2024, 14:28 -
script widevine
By teosaurus45 in forum Video Streaming DownloadingReplies: 14Last Post: 26th Sep 2023, 06:14 -
Found an awesome script to download widevine content (mpd) and decrypt it
By royjeon215 in forum Latest Video NewsReplies: 8Last Post: 11th Nov 2021, 15:26 -
Decoding manifest.mpd (Widevine)
By WaitForIt in forum Video Streaming DownloadingReplies: 6Last Post: 7th Jun 2021, 13:44