From a1e7ff5b5631a5c6f8e1c40c5c2ee45e8f93d9ff Mon Sep 17 00:00:00 2001 From: BuildTools Date: Wed, 30 Oct 2024 16:37:11 +0100 Subject: [PATCH] + Added manifest detection + Added exe name and shaka option + Reverted to old design + README update --- README.md | 10 +-- background.js | 67 +++++++++++++++--- content_script.js | 119 +++++++++++++++++++++++++++++++- manifest.json | 5 +- panel/panel.css | 17 +---- panel/panel.html | 7 ++ panel/panel.js | 172 +++++++++++++++++++++++++++++----------------- util.js | 18 +++++ 8 files changed, 321 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index bda6c33..c5bd5d6 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # WidevineProxy2 An extension-based proxy for Widevine EME challenges and license messages. \ -Modifies the challenge before it reaches the web player and obtains the decryption keys from the response. +Modifies the challenge before it reaches the web player and retrieves the decryption keys from the response. ## Features + User-friendly / GUI-based -+ Bypasses one-time tokens, hashes and license wrapping ++ Bypasses one-time tokens, hashes, and license wrapping + JavaScript native Widevine implementation + Supports Widevine Device files + Manifest V3 compliant @@ -46,10 +46,10 @@ Now, open the extension, click `Choose File` and select your Widevine Device fil ### Remote CDM If you don't already have a `remote.json` file, open the API URL in the browser (if provided) and save the response as `remote.json`. \ -Now, open the extension, click `Choose remote.json` and select the json file provided by your API. +Now, open the extension, click `Choose remote.json` and select the JSON file provided by your API. -+ Select the type of device you're using in the top right hand corner ++ Select the type of device you're using in the top right-hand corner + The files are saved in the extension's `chrome.storage.sync` storage and will be synchronized across any browsers into which the user is signed in with their Google account. + The maximum number of Widevine devices is ~25 **OR** ~200 Remote CDMs + Check `Enabled` to activate the message interception and you're done. @@ -62,7 +62,7 @@ Keys are saved: > [!NOTE] > The video will not play when the interception is active, as the Widevine CDM library isn't able to decrypt the Android CDM license. -+ Click the `+` button to expand the section to reveal the PSSH and keys. ++ Click the `+` button to expand the section to reveal the PSSH and keys. ## FAQ > What if I'm unable to get the keys? diff --git a/background.js b/background.js index 5eb18f9..9f78064 100644 --- a/background.js +++ b/background.js @@ -16,9 +16,35 @@ import { RemoteCdm } from "./remote_cdm.js"; const { LicenseType, SignedMessage, LicenseRequest, License } = protobuf.roots.default.license_protocol; +let manifests = new Map(); +let requests = new Map(); let sessions = new Map(); let logs = []; +chrome.webRequest.onBeforeSendHeaders.addListener( + function(details) { + if (details.method === "GET") { + if (!requests.has(details.url)) { + const headers = details.requestHeaders + .filter(item => !( + item.name.startsWith('sec-ch-ua') || + item.name.startsWith('Sec-Fetch') || + item.name.startsWith('Accept-') || + item.name.startsWith('Host') || + item.name === "Connection" + )).reduce((acc, item) => { + acc[item.name] = item.value; + return acc; + }, {}); + console.log(headers); + requests.set(details.url, headers); + } + } + }, + {urls: [""]}, + ['requestHeaders', chrome.webRequest.OnSendHeadersOptions.EXTRA_HEADERS].filter(Boolean) +); + async function parseClearKey(body, sendResponse, tab_url) { const clearkey = JSON.parse(atob(body)); @@ -35,13 +61,14 @@ async function parseClearKey(body, sendResponse, tab_url) { return; } - console.log("[WidevineProxy2]", "CLEARKEY KEYS", formatted_keys); + console.log("[WidevineProxy2]", "CLEARKEY KEYS", formatted_keys, tab_url); const log = { type: "CLEARKEY", pssh_data: pssh_data, keys: formatted_keys, url: tab_url, - timestamp: Math.floor(Date.now() / 1000) + timestamp: Math.floor(Date.now() / 1000), + manifests: manifests.has(tab_url) ? manifests.get(tab_url) : [] } logs.push(log); @@ -95,7 +122,7 @@ async function parseLicense(body, sendResponse, tab_url) { const signed_license_message = SignedMessage.decode(license); if (signed_license_message.type !== SignedMessage.MessageType.LICENSE) { - console.log("[WidevineProxy2]", "INVALID_MESSAGE_TYPE", signed_license_message.type.toString()) + console.log("[WidevineProxy2]", "INVALID_MESSAGE_TYPE", signed_license_message.type.toString()); sendResponse(); return; } @@ -118,7 +145,8 @@ async function parseLicense(body, sendResponse, tab_url) { pssh_data: pssh, keys: keys, url: tab_url, - timestamp: Math.floor(Date.now() / 1000) + timestamp: Math.floor(Date.now() / 1000), + manifests: manifests.has(tab_url) ? manifests.get(tab_url) : [] } logs.push(log); await AsyncLocalStorage.setStorage({[pssh]: log}); @@ -173,7 +201,7 @@ async function parseLicenseRemote(body, sendResponse, tab_url) { const signed_license_message = SignedMessage.decode(license); if (signed_license_message.type !== SignedMessage.MessageType.LICENSE) { - console.log("[WidevineProxy2]", "INVALID_MESSAGE_TYPE", signed_license_message.type.toString()) + console.log("[WidevineProxy2]", "INVALID_MESSAGE_TYPE", signed_license_message.type.toString()); sendResponse(); return; } @@ -200,7 +228,7 @@ async function parseLicenseRemote(body, sendResponse, tab_url) { await remote_cdm.parse_license(session_id.id, body); const returned_keys = await remote_cdm.get_keys(session_id.id, "CONTENT"); await remote_cdm.close(session_id.id); - + if (returned_keys.length === 0) { sendResponse(); return; @@ -214,7 +242,8 @@ async function parseLicenseRemote(body, sendResponse, tab_url) { pssh_data: session_id.pssh, keys: keys, url: tab_url, - timestamp: Math.floor(Date.now() / 1000) + timestamp: Math.floor(Date.now() / 1000), + manifests: manifests.has(tab_url) ? manifests.get(tab_url) : [] } logs.push(log); await AsyncLocalStorage.setStorage({[session_id.pssh]: log}); @@ -225,10 +254,13 @@ async function parseLicenseRemote(body, sendResponse, tab_url) { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { (async () => { + const tab_url = sender.tab ? sender.tab.url : null; + switch (message.type) { case "REQUEST": if (!await SettingsManager.getEnabled()) { sendResponse(message.body); + manifests.clear(); return; } @@ -254,10 +286,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { case "RESPONSE": if (!await SettingsManager.getEnabled()) { sendResponse(message.body); + manifests.clear(); return; } - const tab_url = sender.tab ? sender.tab.url : null; try { await parseClearKey(message.body, sendResponse, tab_url); return; @@ -294,7 +326,26 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { break; case "CLEAR": logs = []; + manifests.clear() break; + case "MANIFEST": + const parsed = JSON.parse(message.body); + const element = { + type: parsed.type, + url: parsed.url, + headers: requests.has(parsed.url) ? requests.get(parsed.url) : [], + }; + + if (!manifests.has(tab_url)) { + manifests.set(tab_url, [element]); + } else { + let elements = manifests.get(tab_url); + if (!elements.some(e => e.url === parsed.url)) { + elements.push(element); + manifests.set(tab_url, elements); + } + } + sendResponse(); } })(); return true; diff --git a/content_script.js b/content_script.js index cf52017..b4f4eb1 100644 --- a/content_script.js +++ b/content_script.js @@ -2,6 +2,10 @@ function uint8ArrayToBase64(uint8array) { return btoa(String.fromCharCode.apply(null, uint8array)); } +function uint8ArrayToString(uint8array) { + return String.fromCharCode.apply(null, uint8array) +} + function base64toUint8Array(base64_string){ return Uint8Array.from(atob(base64_string), c => c.charCodeAt(0)) } @@ -15,7 +19,6 @@ function compareUint8Arrays(arr1, arr2) { function emitAndWaitForResponse(type, data) { return new Promise((resolve) => { const requestId = Math.random().toString(16).substring(2, 9); - const responseHandler = (event) => { const { detail } = event; if (detail.substring(0, 7) === requestId) { @@ -47,6 +50,39 @@ function getEventListeners(type) { return store[type]; } +class Evaluator { + static isDASH(text) { + return text.includes(''); + } + + static isHLS(text) { + return text.includes('#extm3u'); + } + + static isHLSMaster(text) { + return text.includes('#ext-x-stream-inf'); + } + + static isMSS(text) { + return text.includes(''); + } + + static getManifestType(text) { + const lower = text.toLowerCase(); + if (this.isDASH(lower)) { + return "DASH"; + } else if (this.isHLS(lower)) { + if (this.isHLSMaster(lower)) { + return "HLS_MASTER"; + } else { + return "HLS_PLAYLIST"; + } + } else if (this.isMSS(lower)) { + return "MSS"; + } + } +} + (async () => { if (typeof EventTarget !== 'undefined') { proxy(EventTarget.prototype, 'addEventListener', async (_target, _this, _args) => { @@ -147,3 +183,84 @@ function getEventListeners(type) { }); } })(); + +const originalFetch = window.fetch; +window.fetch = function() { + return new Promise(async (resolve, reject) => { + originalFetch.apply(this, arguments).then((response) => { + if (response) { + response.clone().text().then((text) => { + const manifest_type = Evaluator.getManifestType(text); + if (manifest_type) { + if (arguments.length === 1) { + emitAndWaitForResponse("MANIFEST", JSON.stringify({ + "url": arguments[0].url, + "type": manifest_type, + })); + } else if (arguments.length === 2) { + emitAndWaitForResponse("MANIFEST", JSON.stringify({ + "url": arguments[0], + "type": manifest_type, + })); + } + } + resolve(response); + }).catch(() => { + resolve(response); + }) + } else { + resolve(response); + } + }).catch(() => { + resolve(); + }) + }) +} + +const open = XMLHttpRequest.prototype.open; +XMLHttpRequest.prototype.open = function(method, url) { + this._method = method; + return open.apply(this, arguments); +}; + +const send = XMLHttpRequest.prototype.send; +XMLHttpRequest.prototype.send = function(postData) { + this.addEventListener('load', async function() { + if (this._method === "GET") { + let body = void 0; + switch (this.responseType) { + case "": + case "text": + body = this.responseText ?? this.response; + break; + case "json": + // TODO: untested + body = JSON.stringify(this.response); + break; + case "arraybuffer": + // TODO: untested + if (this.response.byteLength) { + const response = new Uint8Array(this.response); + body = uint8ArrayToString(new Uint8Array([...response.slice(0, 2000), ...response.slice(-2000)])); + } + break; + case "document": + // todo + break; + case "blob": + body = await this.response.text(); + break; + } + if (body) { + const manifest_type = Evaluator.getManifestType(body); + if (manifest_type) { + emitAndWaitForResponse("MANIFEST", JSON.stringify({ + "url": this.responseURL, + "type": manifest_type, + })); + } + } + } + }); + return send.apply(this, arguments); +}; diff --git a/manifest.json b/manifest.json index a7fae7b..8c18475 100644 --- a/manifest.json +++ b/manifest.json @@ -1,12 +1,13 @@ { "manifest_version": 3, "name": "WidevineProxy2", - "version": "0.7.4", + "version": "0.8", "permissions": [ "activeTab", "tabs", "storage", - "unlimitedStorage" + "unlimitedStorage", + "webRequest" ], "host_permissions": [ "*://*/*" diff --git a/panel/panel.css b/panel/panel.css index 78fc559..f147dec 100644 --- a/panel/panel.css +++ b/panel/panel.css @@ -4,16 +4,9 @@ body { color: black; width: 400px; } -button:not(.toggleButton), select { - padding: 4px; -} .toggleButton { width: 24px; } -button, select { - background-color: #e1e1e1; - border: none; -} select { max-width: 330px; } @@ -21,11 +14,8 @@ select { outline: none; width: 80%; } -button:hover, select:hover { - background-color: #c3c3c3; -} -button, select, fieldset, .text-box { - border-radius: 7px; +#downloader-name { + width: 160px; } @@ -40,7 +30,6 @@ button, select, fieldset, .text-box { .dark-mode button, .dark-mode select { background-color: #222; color: #D5D5D5; - border-color: #222; } .dark-mode button:hover, .dark-mode select:hover { background-color: #323232; @@ -50,7 +39,6 @@ button, select, fieldset, .text-box { } .dark-mode .text-box { background-color: #323232; - border-color: #535353; color: #D5D5D5; } @@ -134,7 +122,6 @@ input:checked + .slider:before { width: 100%; overflow: hidden; background-color: lightblue; - border-radius: 7px; padding: 0; } .expandableDiv.expanded { diff --git a/panel/panel.html b/panel/panel.html index 22caabe..c9d2d2a 100644 --- a/panel/panel.html +++ b/panel/panel.html @@ -53,6 +53,13 @@ +
+ Command options + +
+ + +
Keys diff --git a/panel/panel.js b/panel/panel.js index cba26ab..01dd618 100644 --- a/panel/panel.js +++ b/panel/panel.js @@ -4,17 +4,18 @@ import { base64toUint8Array, DeviceManager, RemoteCDMManager, SettingsManager } const key_container = document.getElementById('key-container'); +// ================ Main ================ +const enabled = document.getElementById('enabled'); +enabled.addEventListener('change', async function (){ + await SettingsManager.setEnabled(enabled.checked); +}); + const toggle = document.getElementById('darkModeToggle'); toggle.addEventListener('change', async () => { await SettingsManager.setDarkMode(toggle.checked); await SettingsManager.saveDarkMode(toggle.checked); }); -const enabled = document.getElementById('enabled'); -enabled.addEventListener('change', async function (){ - await SettingsManager.setEnabled(enabled.checked); -}); - const wvd_select = document.getElementById('wvd_select'); wvd_select.addEventListener('change', async function (){ if (wvd_select.checked) { @@ -28,15 +29,12 @@ remote_select.addEventListener('change', async function (){ await SettingsManager.saveSelectedDeviceType("REMOTE"); } }); +// ====================================== -const wvd_combobox = document.getElementById('wvd-combobox'); -wvd_combobox.addEventListener('change', async function() { - await DeviceManager.saveSelectedWidevineDevice(wvd_combobox.options[wvd_combobox.selectedIndex].text); -}); - -const remote_combobox = document.getElementById('remote-combobox'); -remote_combobox.addEventListener('change', async function() { - await RemoteCDMManager.saveSelectedRemoteCDM(remote_combobox.options[remote_combobox.selectedIndex].text); +// ================ Widevine Device ================ +document.getElementById('fileInput').addEventListener('click', () => { + chrome.runtime.sendMessage({ type: "OPEN_PICKER_WVD" }); + window.close(); }); const remove = document.getElementById('remove'); @@ -52,6 +50,27 @@ remove.addEventListener('click', async function() { } }); +const download = document.getElementById('download'); +download.addEventListener('click', async function() { + const widevine_device = await DeviceManager.getSelectedWidevineDevice(); + SettingsManager.downloadFile( + base64toUint8Array(await DeviceManager.loadWidevineDevice(widevine_device)), + widevine_device + ".wvd" + ) +}); + +const wvd_combobox = document.getElementById('wvd-combobox'); +wvd_combobox.addEventListener('change', async function() { + await DeviceManager.saveSelectedWidevineDevice(wvd_combobox.options[wvd_combobox.selectedIndex].text); +}); +// ================================================= + +// ================ Remote CDM ================ +document.getElementById('remoteInput').addEventListener('click', () => { + chrome.runtime.sendMessage({ type: "OPEN_PICKER_REMOTE" }); + window.close(); +}); + const remote_remove = document.getElementById('remoteRemove'); remote_remove.addEventListener('click', async function() { await RemoteCDMManager.removeSelectedRemoteCDM(); @@ -63,15 +82,6 @@ remote_remove.addEventListener('click', async function() { } else { await RemoteCDMManager.removeSelectedRemoteCDMKey(); } -}) - -const download = document.getElementById('download'); -download.addEventListener('click', async function() { - const widevine_device = await DeviceManager.getSelectedWidevineDevice(); - SettingsManager.downloadFile( - base64toUint8Array(await DeviceManager.loadWidevineDevice(widevine_device)), - widevine_device + ".wvd" - ) }); const remote_download = document.getElementById('remoteDownload'); @@ -83,28 +93,43 @@ remote_download.addEventListener('click', async function() { ) }); +const remote_combobox = document.getElementById('remote-combobox'); +remote_combobox.addEventListener('change', async function() { + await RemoteCDMManager.saveSelectedRemoteCDM(remote_combobox.options[remote_combobox.selectedIndex].text); +}); +// ============================================ + +// ================ Command Options ================ +const use_shaka = document.getElementById('use-shaka'); +use_shaka.addEventListener('change', async function (){ + await SettingsManager.saveUseShakaPackager(use_shaka.checked); +}); + +const downloader_name = document.getElementById('downloader-name'); +downloader_name.addEventListener('input', async function (event){ + console.log("input change", event); + await SettingsManager.saveExecutableName(downloader_name.value); +}); +// ================================================= + +// ================ Keys ================ const clear = document.getElementById('clear'); clear.addEventListener('click', async function() { chrome.runtime.sendMessage({ type: "CLEAR" }); key_container.innerHTML = ""; }); -document.getElementById('fileInput').addEventListener('click', () => { - chrome.runtime.sendMessage({ type: "OPEN_PICKER_WVD" }); - window.close(); -}); - -document.getElementById('remoteInput').addEventListener('click', () => { - chrome.runtime.sendMessage({ type: "OPEN_PICKER_REMOTE" }); - window.close(); -}); +async function createCommand(json, key_string) { + const metadata = JSON.parse(json); + const header_string = Object.entries(metadata.headers).map(([key, value]) => `-H "${key}: ${value.replace(/"/g, "'")}"`).join(' '); + return `${await SettingsManager.getExecutableName()} "${metadata.url}" ${header_string} ${key_string} ${await SettingsManager.getUseShakaPackager() ? "--use-shaka-packager " : ""}-M format=mkv`; +} -function appendLog(result) { +async function appendLog(result) { const key_string = result.keys.map(key => `--key ${key.kid}:${key.k}`).join(' '); const date = new Date(result.timestamp * 1000); const date_string = date.toLocaleString(); - // Create a container for the log entry const logContainer = document.createElement('div'); logContainer.classList.add('log-container'); logContainer.innerHTML = ` @@ -113,61 +138,79 @@ function appendLog(result) { +