From 5949228d4f9e846ca10dbe586871e0d82a061392 Mon Sep 17 00:00:00 2001 From: voldemort <5692900+yell0wsuit@users.noreply.github.com> Date: Sun, 20 Jul 2025 16:27:13 +0700 Subject: [PATCH] update to manifest v3 --- README.md | 2 + frontend/package.json | 58 +-- mv2/background.js | 89 ++++ mv2/content.js | 96 ++++ mv2/inject.js | 1122 +++++++++++++++++++++++++++++++++++++++++ mv2/manifest.json | 41 ++ src/background.js | 5 +- src/manifest.json | 80 ++- 8 files changed, 1415 insertions(+), 78 deletions(-) create mode 100644 mv2/background.js create mode 100644 mv2/content.js create mode 100644 mv2/inject.js create mode 100644 mv2/manifest.json diff --git a/README.md b/README.md index 16ce2fc..4cb2211 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Keep these extension core files inside `src`: - `inject.js` - `manifest.json` +The `mv2` folder is for Manifest v2 backup for legacy reasons. + Frontend React source stays in `frontend`. The build process will take care of everything into `extension-release`. diff --git a/frontend/package.json b/frontend/package.json index 326d96a..bae1047 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,30 +1,30 @@ { - "name": "frontend", - "private": true, - "version": "2.1.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "@tailwindcss/vite": "^4.1.11", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-router-dom": "^7.7.0", - "tailwindcss": "^4.1.11" - }, - "devDependencies": { - "@eslint/js": "^9.31.0", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.7.0", - "eslint": "^9.31.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^16.3.0", - "vite": "^7.0.5" - } -} \ No newline at end of file + "name": "frontend", + "private": true, + "version": "2.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.11", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.7.0", + "tailwindcss": "^4.1.11" + }, + "devDependencies": { + "@eslint/js": "^9.31.0", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.7.0", + "eslint": "^9.31.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "vite": "^7.0.5" + } +} diff --git a/mv2/background.js b/mv2/background.js new file mode 100644 index 0000000..da9c10f --- /dev/null +++ b/mv2/background.js @@ -0,0 +1,89 @@ +// Open popout window when the extension icon is clicked +chrome.browserAction.onClicked.addListener(() => { + chrome.windows.create({ + url: chrome.runtime.getURL("index.html"), + type: "popup", // opens as a floating window + width: 800, + height: 600, + }); +}); + +// Listen for messages and store data in chrome.storage.local +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + const { type, data } = message; + + switch (type) { + case "DRM_TYPE": + console.log("DRM Type:", data); + chrome.storage.local.set({ drmType: data }); + break; + + case "PSSH_DATA": + console.log("Storing PSSH:", data); + chrome.storage.local.set({ latestPSSH: data }); + break; + + case "KEYS_DATA": + console.log("Storing Decryption Keys:", data); + chrome.storage.local.set({ latestKeys: data }); + break; + + case "LICENSE_URL": + console.log("Storling License URL " + data); + chrome.storage.local.set({ licenseURL: data }); + break; + + case "MANIFEST_URL_FOUND": + console.log("Storing Manifest URL:", data); + chrome.storage.local.set({ manifestURL: data }); + break; + + default: + console.warn("Unknown message type received:", type); + } +}); + +// Set initial config and injection type on install +chrome.runtime.onInstalled.addListener((details) => { + if (details.reason === "install") { + chrome.storage.local.set({ valid_config: false }, () => { + if (chrome.runtime.lastError) { + console.error("Error setting valid_config:", chrome.runtime.lastError); + } else { + console.log("valid_config set to false on first install."); + } + }); + + chrome.storage.local.set({ injection_type: "LICENSE" }, () => { + if (chrome.runtime.lastError) { + console.error("Error setting Injection Type:", chrome.runtime.lastError); + } else { + console.log("Injection type set to LICENSE on first install."); + } + }); + + chrome.storage.local.set({ drm_override: "DISABLED" }, () => { + if (chrome.runtime.lastError) { + console.error("Error setting DRM Override type:", chrome.runtime.lastError); + } else { + console.log("DRM Override type set to DISABLED on first install."); + } + }); + + chrome.storage.local.set({ cdrm_instance: null }, () => { + if (chrome.runtime.lastError) { + console.error("Error setting CDRM instance:", chrome.runtime.lastError); + } else { + console.log("CDRM instance set to null."); + } + }); + + chrome.storage.local.set({ cdrm_api_key: null }, () => { + if (chrome.runtime.lastError) { + console.error("Error setting CDRM API Key:", chrome.runtime.lastError); + } else { + console.log("CDRM API Key set."); + } + }); + } +}); diff --git a/mv2/content.js b/mv2/content.js new file mode 100644 index 0000000..55b7891 --- /dev/null +++ b/mv2/content.js @@ -0,0 +1,96 @@ +// Inject `inject.js` into the page context +(function injectScript() { + const script = document.createElement("script"); + script.src = chrome.runtime.getURL("inject.js"); + script.type = "text/javascript"; + script.onload = () => script.remove(); // Clean up + // Inject directly into or + (document.documentElement || document.head || document.body).appendChild(script); +})(); + +// Listen for messages from the injected script +window.addEventListener("message", function (event) { + if (event.source !== window) return; + + if ( + ["__DRM_TYPE__", "__PSSH_DATA__", "__KEYS_DATA__", "__LICENSE_URL__"].includes( + event.data?.type + ) + ) { + chrome.runtime.sendMessage({ + type: event.data.type.replace("__", "").replace("__", ""), + data: event.data.data, + }); + } + + if (event.data.type === "__GET_CDM_DEVICES__") { + chrome.storage.local.get(["widevine_device", "playready_device"], (result) => { + const widevine_device = result.widevine_device || null; + const playready_device = result.playready_device || null; + + window.postMessage( + { + type: "__CDM_DEVICES__", + widevine_device, + playready_device, + }, + "*" + ); + }); + } + + if (event.data.type === "__GET_INJECTION_TYPE__") { + chrome.storage.local.get("injection_type", (result) => { + const injectionType = result.injection_type || "LICENSE"; + + window.postMessage( + { + type: "__INJECTION_TYPE__", + injectionType, + }, + "*" + ); + }); + } + + if (event.data.type === "__GET_DRM_OVERRIDE__") { + chrome.storage.local.get("drm_override", (result) => { + const drmOverride = result.drm_override || "DISABLED"; + + window.postMessage( + { + type: "__DRM_OVERRIDE__", + drmOverride, + }, + "*" + ); + }); + } + + // Manifest header and URL + + const seenManifestUrls = new Set(); + + if (event.data?.type === "__MANIFEST_URL__") { + const url = event.data.data; + if (seenManifestUrls.has(url)) return; + seenManifestUrls.add(url); + console.log("✅ [Content] Unique manifest URL:", url); + + chrome.runtime.sendMessage({ + type: "MANIFEST_URL_FOUND", + data: url, + }); + } + + if (event.data?.type === "__MANIFEST_HEADERS__") { + const { url, headers } = event.data; + console.log("[Content.js] Manifest Headers:", url, headers); + + chrome.runtime.sendMessage({ + type: "MANIFEST_HEADERS", + url, + headers, + }); + } +}); diff --git a/mv2/inject.js b/mv2/inject.js new file mode 100644 index 0000000..fdcb029 --- /dev/null +++ b/mv2/inject.js @@ -0,0 +1,1122 @@ +let widevineDeviceInfo = null; +let playreadyDeviceInfo = null; +let originalChallenge = null; +let serviceCertFound = false; +let drmType = "NONE"; +let psshFound = false; +let foundWidevinePssh = null; +let foundPlayreadyPssh = null; +let drmDecided = null; +let drmOverride = "DISABLED"; +let interceptType = "DISABLED"; +let remoteCDM = null; +let generateRequestCalled = false; +let remoteListenerMounted = false; +let injectionSuccess = false; +let foundChallengeInBody = false; +let licenseResponseCounter = 0; +let keysRetrieved = false; + +// Post message to content.js to get DRM override +window.postMessage({ type: "__GET_DRM_OVERRIDE__" }, "*"); + +// Add listener for DRM override messages +window.addEventListener("message", function (event) { + if (event.source !== window) return; + if (event.data.type === "__DRM_OVERRIDE__") { + drmOverride = event.data.drmOverride || "DISABLED"; + console.log("DRM Override set to:", drmOverride); + } +}); + +// Post message to content.js to get injection type +window.postMessage({ type: "__GET_INJECTION_TYPE__" }, "*"); + +// Add listener for injection type messages +window.addEventListener("message", function (event) { + if (event.source !== window) return; + + if (event.data.type === "__INJECTION_TYPE__") { + interceptType = event.data.injectionType || "DISABLED"; + console.log("Injection type set to:", interceptType); + } +}); + +// Post message to get CDM devices +window.postMessage({ type: "__GET_CDM_DEVICES__" }, "*"); + +// Add listener for CDM device messages +window.addEventListener("message", function (event) { + if (event.source !== window) return; + + if (event.data.type === "__CDM_DEVICES__") { + const { widevine_device, playready_device } = event.data; + + console.log("Received device info:", widevine_device, playready_device); + + widevineDeviceInfo = widevine_device; + playreadyDeviceInfo = playready_device; + } +}); + +function safeHeaderShellEscape(str) { + return str + .replace(/\\/g, "\\\\") + .replace(/"/g, '\\"') + .replace(/\$/g, "\\$") // escape shell expansion + .replace(/`/g, "\\`") + .replace(/\n/g, ""); // strip newlines +} + +// Intercep network to find manifest +function injectManifestInterceptor() { + const script = document.createElement("script"); + script.textContent = ` + (function() { + function isProbablyManifest(text = "", contentType = "") { + const lowerCT = contentType?.toLowerCase() ?? ""; + const sample = text.slice(0, 2000); + + const isHLSMime = lowerCT.includes("mpegurl"); + const isDASHMime = lowerCT.includes("dash+xml"); + const isSmoothMime = lowerCT.includes("sstr+xml"); + + const isHLSKeyword = sample.includes("#EXTM3U") || sample.includes("#EXT-X-STREAM-INF"); + const isDASHKeyword = sample.includes(" { + headersObj[key] = value; + }); + + const headerFlags = Object.entries(headersObj) + .map(([key, val]) => '--add-headers "' + safeHeaderShellEscape(key) + ': ' + safeHeaderShellEscape(val) + '"') + .join(" "); + + window.postMessage({ + type: "__MANIFEST_HEADERS__", + url, + headers: headerFlags + }, "*"); + } + } catch (e) {} + + return response; + }; + + const originalXHROpen = XMLHttpRequest.prototype.open; + const originalXHRSend = XMLHttpRequest.prototype.send; + + XMLHttpRequest.prototype.open = function(method, url) { + this.__url = url; + return originalXHROpen.apply(this, arguments); + }; + + XMLHttpRequest.prototype.send = function(body) { + this.addEventListener("load", function () { + try { + const contentType = this.getResponseHeader("content-type") || ""; + const text = this.responseText; + + if (isProbablyManifest(text, contentType)) { + window.postMessage({ type: "__MANIFEST_URL__", data: this.__url }, "*"); + console.log("[Manifest][xhr]", this.__url, contentType); + + const xhrHeaders = {}; + const rawHeaders = this.getAllResponseHeaders().trim().split(/\\r?\\n/); + rawHeaders.forEach(line => { + const parts = line.split(": "); + if (parts.length === 2) { + xhrHeaders[parts[0]] = parts[1]; + } + }); + + const headerFlags = Object.entries(xhrHeaders) + .map(([key, val]) => '--add-headers "' + safeHeaderShellEscape(key) + ': ' + safeHeaderShellEscape(val) + '"') + .join(" "); + + window.postMessage({ + type: "__MANIFEST_HEADERS__", + url: this.__url, + headers: headerFlags + }, "*"); + } + } catch (e) {} + }); + return originalXHRSend.apply(this, arguments); + }; + })(); + `; + document.documentElement.appendChild(script); + script.remove(); +} + +injectManifestInterceptor(); + +// PlayReady Remote CDM Class +class remotePlayReadyCDM { + constructor(security_level, host, secret, device_name) { + this.security_level = security_level; + this.host = host; + this.secret = secret; + this.device_name = device_name; + this.session_id = null; + this.challenge = null; + this.keys = null; + } + + // Open PlayReady session + openSession() { + const url = `${this.host}/remotecdm/playready/${this.device_name}/open`; + const xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.send(); + const jsonData = JSON.parse(xhr.responseText); + if (jsonData.data?.session_id) { + this.session_id = jsonData.data.session_id; + console.log("PlayReady session opened:", this.session_id); + } else { + console.error("Failed to open PlayReady session:", jsonData.message); + throw new Error("Failed to open PlayReady session"); + } + } + + // Get PlayReady challenge + getChallenge(init_data) { + const url = `${this.host}/remotecdm/playready/${this.device_name}/get_license_challenge`; + const xhr = new XMLHttpRequest(); + xhr.open("POST", url, false); + xhr.setRequestHeader("Content-Type", "application/json"); + const body = { + session_id: this.session_id, + init_data: init_data, + }; + xhr.send(JSON.stringify(body)); + const jsonData = JSON.parse(xhr.responseText); + if (jsonData.data?.challenge) { + this.challenge = btoa(jsonData.data.challenge); + console.log("PlayReady challenge received:", this.challenge); + } else { + console.error("Failed to get PlayReady challenge:", jsonData.message); + throw new Error("Failed to get PlayReady challenge"); + } + } + + // Parse PlayReady license response + parseLicense(license_message) { + const url = `${this.host}/remotecdm/playready/${this.device_name}/parse_license`; + const xhr = new XMLHttpRequest(); + xhr.open("POST", url, false); + xhr.setRequestHeader("Content-Type", "application/json"); + const body = { + session_id: this.session_id, + license_message: license_message, + }; + xhr.send(JSON.stringify(body)); + const jsonData = JSON.parse(xhr.responseText); + if ( + jsonData.message === "Successfully parsed and loaded the Keys from the License message" + ) { + console.log("PlayReady license response parsed successfully"); + return true; + } else { + console.error("Failed to parse PlayReady license response:", jsonData.message); + throw new Error("Failed to parse PlayReady license response"); + } + } + + // Get PlayReady keys + getKeys() { + const url = `${this.host}/remotecdm/playready/${this.device_name}/get_keys`; + const xhr = new XMLHttpRequest(); + xhr.open("POST", url, false); + xhr.setRequestHeader("Content-Type", "application/json"); + const body = { + session_id: this.session_id, + }; + xhr.send(JSON.stringify(body)); + const jsonData = JSON.parse(xhr.responseText); + if (jsonData.data?.keys) { + this.keys = jsonData.data.keys; + console.log("PlayReady keys received:", this.keys); + } else { + console.error("Failed to get PlayReady keys:", jsonData.message); + throw new Error("Failed to get PlayReady keys"); + } + } + + // Close PlayReady session + closeSession() { + const url = `${this.host}/remotecdm/playready/${this.device_name}/close/${this.session_id}`; + const xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.send(); + const jsonData = JSON.parse(xhr.responseText); + if (jsonData) { + console.log("PlayReady session closed successfully"); + } else { + console.error("Failed to close PlayReady session:", jsonData.message); + throw new Error("Failed to close PlayReady session"); + } + } +} + +// Widevine Remote CDM Class +class remoteWidevineCDM { + constructor(device_type, system_id, security_level, host, secret, device_name) { + this.device_type = device_type; + this.system_id = system_id; + this.security_level = security_level; + this.host = host; + this.secret = secret; + this.device_name = device_name; + this.session_id = null; + this.challenge = null; + this.keys = null; + } + + // Open Widevine session + openSession() { + const url = `${this.host}/remotecdm/widevine/${this.device_name}/open`; + const xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.send(); + const jsonData = JSON.parse(xhr.responseText); + if (jsonData.data?.session_id) { + this.session_id = jsonData.data.session_id; + console.log("Widevine session opened:", this.session_id); + } else { + console.error("Failed to open Widevine session:", jsonData.message); + throw new Error("Failed to open Widevine session"); + } + } + + // Set Widevine service certificate + setServiceCertificate(certificate) { + const url = `${this.host}/remotecdm/widevine/${this.device_name}/set_service_certificate`; + const xhr = new XMLHttpRequest(); + xhr.open("POST", url, false); + xhr.setRequestHeader("Content-Type", "application/json"); + const body = { + session_id: this.session_id, + certificate: certificate ?? null, + }; + xhr.send(JSON.stringify(body)); + const jsonData = JSON.parse(xhr.responseText); + if (jsonData.status === 200) { + console.log("Service certificate set successfully"); + } else { + console.error("Failed to set service certificate:", jsonData.message); + throw new Error("Failed to set service certificate"); + } + } + + // Get Widevine challenge + getChallenge(init_data, license_type = "STREAMING") { + const url = `${this.host}/remotecdm/widevine/${this.device_name}/get_license_challenge/${license_type}`; + const xhr = new XMLHttpRequest(); + xhr.open("POST", url, false); + xhr.setRequestHeader("Content-Type", "application/json"); + const body = { + session_id: this.session_id, + init_data: init_data, + privacy_mode: serviceCertFound, + }; + xhr.send(JSON.stringify(body)); + const jsonData = JSON.parse(xhr.responseText); + if (jsonData.data?.challenge_b64) { + this.challenge = jsonData.data.challenge_b64; + console.log("Widevine challenge received:", this.challenge); + } else { + console.error("Failed to get Widevine challenge:", jsonData.message); + throw new Error("Failed to get Widevine challenge"); + } + } + + // Parse Widevine license response + parseLicense(license_message) { + const url = `${this.host}/remotecdm/widevine/${this.device_name}/parse_license`; + const xhr = new XMLHttpRequest(); + xhr.open("POST", url, false); + xhr.setRequestHeader("Content-Type", "application/json"); + const body = { + session_id: this.session_id, + license_message: license_message, + }; + xhr.send(JSON.stringify(body)); + const jsonData = JSON.parse(xhr.responseText); + if (jsonData.status === 200) { + console.log("Widevine license response parsed successfully"); + return true; + } else { + console.error("Failed to parse Widevine license response:", jsonData.message); + throw new Error("Failed to parse Widevine license response"); + } + } + + // Get Widevine keys + getKeys() { + const url = `${this.host}/remotecdm/widevine/${this.device_name}/get_keys/ALL`; + const xhr = new XMLHttpRequest(); + xhr.open("POST", url, false); + xhr.setRequestHeader("Content-Type", "application/json"); + const body = { + session_id: this.session_id, + }; + xhr.send(JSON.stringify(body)); + const jsonData = JSON.parse(xhr.responseText); + if (jsonData.data?.keys) { + this.keys = jsonData.data.keys; + console.log("Widevine keys received:", this.keys); + } else { + console.error("Failed to get Widevine keys:", jsonData.message); + throw new Error("Failed to get Widevine keys"); + } + } + + // Close Widevine session + closeSession() { + const url = `${this.host}/remotecdm/widevine/${this.device_name}/close/${this.session_id}`; + const xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.send(); + const jsonData = JSON.parse(xhr.responseText); + if (jsonData) { + console.log("Widevine session closed successfully"); + } else { + console.error("Failed to close Widevine session:", jsonData.message); + throw new Error("Failed to close Widevine session"); + } + } +} + +// Utility functions +function hexStrToU8(hexString) { + return Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); +} + +function u8ToHexStr(bytes) { + return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), ""); +} + +function b64ToHexStr(b64) { + return [...atob(b64)].map((c) => c.charCodeAt(0).toString(16).padStart(2, "0")).join``; +} + +function jsonContainsValue(obj, prefix = "CAES") { + if (typeof obj === "string") return obj.startsWith(prefix); + if (Array.isArray(obj)) return obj.some((val) => jsonContainsValue(val, prefix)); + if (typeof obj === "object" && obj !== null) { + return Object.values(obj).some((val) => jsonContainsValue(val, prefix)); + } + return false; +} + +function jsonReplaceValue(obj, newValue) { + if (typeof obj === "string") { + return obj.startsWith("CAES") || obj.startsWith("PD94") ? newValue : obj; + } + + if (Array.isArray(obj)) { + return obj.map((item) => jsonReplaceValue(item, newValue)); + } + + if (typeof obj === "object" && obj !== null) { + const newObj = {}; + for (const key in obj) { + if (Object.hasOwn(obj, key)) { + newObj[key] = jsonReplaceValue(obj[key], newValue); + } + } + return newObj; + } + + return obj; +} + +function isJson(str) { + try { + JSON.parse(str); + return true; + } catch (e) { + return false; + } +} + +function getWidevinePssh(buffer) { + const hex = u8ToHexStr(new Uint8Array(buffer)); + const match = hex.match(/000000(..)?70737368.*/); + if (!match) return null; + + const boxHex = match[0]; + const bytes = hexStrToU8(boxHex); + return window.btoa(String.fromCharCode(...bytes)); +} + +function getPlayReadyPssh(buffer) { + const u8 = new Uint8Array(buffer); + const systemId = "9a04f07998404286ab92e65be0885f95"; + const hex = u8ToHexStr(u8); + const index = hex.indexOf(systemId); + if (index === -1) return null; + const psshBoxStart = hex.lastIndexOf("70737368", index); + if (psshBoxStart === -1) return null; + const lenStart = psshBoxStart - 8; + const boxLen = parseInt(hex.substr(lenStart, 8), 16) * 2; + const psshHex = hex.substr(lenStart, boxLen); + const psshBytes = hexStrToU8(psshHex); + return window.btoa(String.fromCharCode(...psshBytes)); +} + +function getClearkey(response) { + let obj = JSON.parse(new TextDecoder("utf-8").decode(response)); + return obj["keys"].map((o) => ({ + key_id: b64ToHexStr(o["kid"].replace(/-/g, "+").replace(/_/g, "/")), + key: b64ToHexStr(o["k"].replace(/-/g, "+").replace(/_/g, "/")), + })); +} + +function base64ToUint8Array(base64) { + const binaryStr = atob(base64); + const len = binaryStr.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryStr.charCodeAt(i); + } + return bytes; +} + +function arrayBufferToBase64(uint8array) { + let binary = ""; + const len = uint8array.length; + + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(uint8array[i]); + } + + return window.btoa(binary); +} + +// Challenge generator interceptor +const originalGenerateRequest = MediaKeySession.prototype.generateRequest; +MediaKeySession.prototype.generateRequest = function (initDataType, initData) { + const session = this; + let playReadyPssh = getPlayReadyPssh(initData); + if (playReadyPssh) { + console.log("[DRM Detected] PlayReady"); + foundPlayreadyPssh = playReadyPssh; + console.log("[PlayReady PSSH found] " + playReadyPssh); + } + let wideVinePssh = getWidevinePssh(initData); + if (wideVinePssh) { + // Widevine code + console.log("[DRM Detected] Widevine"); + foundWidevinePssh = wideVinePssh; + console.log("[Widevine PSSH found] " + wideVinePssh); + } + // Challenge message interceptor + if (!remoteListenerMounted) { + remoteListenerMounted = true; + session.addEventListener("message", function messageInterceptor(event) { + event.stopImmediatePropagation(); + const uint8Array = new Uint8Array(event.message); + const base64challenge = arrayBufferToBase64(uint8Array); + if (base64challenge === "CAQ=" && interceptType !== "DISABLED" && !serviceCertFound) { + const { device_type, system_id, security_level, host, secret, device_name } = + widevineDeviceInfo; + remoteCDM = new remoteWidevineCDM( + device_type, + system_id, + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + } + if (!injectionSuccess && base64challenge !== "CAQ=" && interceptType !== "DISABLED") { + if (interceptType === "EME") { + injectionSuccess = true; + } + if (!originalChallenge) { + originalChallenge = base64challenge; + } + if (originalChallenge.startsWith("CAES")) { + window.postMessage({ type: "__DRM_TYPE__", data: "Widevine" }, "*"); + window.postMessage({ type: "__PSSH_DATA__", data: foundWidevinePssh }, "*"); + if (interceptType === "EME" && !remoteCDM) { + const { + device_type, + system_id, + security_level, + host, + secret, + device_name, + } = widevineDeviceInfo; + remoteCDM = new remoteWidevineCDM( + device_type, + system_id, + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + remoteCDM.getChallenge(foundWidevinePssh); + } + } + if (!originalChallenge.startsWith("CAES")) { + const buffer = event.message; + const decoder = new TextDecoder("utf-16"); + const decodedText = decoder.decode(buffer); + const match = decodedText.match( + /([^<]+)<\/Challenge>/ + ); + if (match) { + window.postMessage({ type: "__DRM_TYPE__", data: "PlayReady" }, "*"); + window.postMessage( + { type: "__PSSH_DATA__", data: foundPlayreadyPssh }, + "*" + ); + originalChallenge = match[1]; + if (interceptType === "EME" && !remoteCDM) { + const { security_level, host, secret, device_name } = + playreadyDeviceInfo; + remoteCDM = new remotePlayReadyCDM( + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + remoteCDM.getChallenge(foundPlayreadyPssh); + } + } + } + if (interceptType === "EME" && remoteCDM) { + const uint8challenge = base64ToUint8Array(remoteCDM.challenge); + const challengeBuffer = uint8challenge.buffer; + const syntheticEvent = new MessageEvent("message", { + data: event.data, + origin: event.origin, + lastEventId: event.lastEventId, + source: event.source, + ports: event.ports, + }); + Object.defineProperty(syntheticEvent, "message", { + get: () => challengeBuffer, + }); + console.log("Intercepted EME Challenge and injected custom one."); + session.dispatchEvent(syntheticEvent); + } + } + }); + console.log("Message interceptor mounted."); + } + return originalGenerateRequest.call(session, initDataType, initData); +}; + +// Message update interceptors +const originalUpdate = MediaKeySession.prototype.update; +MediaKeySession.prototype.update = function (response) { + const uint8 = response instanceof Uint8Array ? response : new Uint8Array(response); + const base64Response = window.btoa(String.fromCharCode(...uint8)); + if (base64Response.startsWith("CAUS") && foundWidevinePssh && remoteCDM && !serviceCertFound) { + remoteCDM.setServiceCertificate(base64Response); + if (interceptType === "EME" && !remoteCDM.challenge) { + remoteCDM.getChallenge(foundWidevinePssh); + } + window.postMessage({ type: "__DRM_TYPE__", data: "Widevine" }, "*"); + window.postMessage({ type: "__PSSH_DATA__", data: foundWidevinePssh }, "*"); + serviceCertFound = true; + } + if ( + !base64Response.startsWith("CAUS") && + (foundWidevinePssh || foundPlayreadyPssh) && + !keysRetrieved + ) { + if (licenseResponseCounter === 1 || foundChallengeInBody) { + remoteCDM.parseLicense(base64Response); + remoteCDM.getKeys(); + remoteCDM.closeSession(); + keysRetrieved = true; + window.postMessage({ type: "__KEYS_DATA__", data: remoteCDM.keys }, "*"); + } + licenseResponseCounter++; + } + const updatePromise = originalUpdate.call(this, response); + if (!foundPlayreadyPssh && !foundWidevinePssh) { + updatePromise + .then(() => { + let clearKeys = getClearkey(response); + if (clearKeys && clearKeys.length > 0) { + console.log("[CLEARKEY] ", clearKeys); + const drmType = { + type: "__DRM_TYPE__", + data: "ClearKey", + }; + window.postMessage(drmType, "*"); + const keysData = { + type: "__KEYS_DATA__", + data: clearKeys, + }; + window.postMessage(keysData, "*"); + } + }) + .catch((e) => { + console.log("[CLEARKEY] Not found"); + }); + } + + return updatePromise; +}; + +// fetch POST interceptor +(function () { + const originalFetch = window.fetch; + + window.fetch = async function (resource, config = {}) { + const method = (config.method || "GET").toUpperCase(); + + if (method === "POST") { + let body = config.body; + if (body) { + if (body instanceof ArrayBuffer || body instanceof Uint8Array) { + const buffer = body instanceof Uint8Array ? body : new Uint8Array(body); + const base64Body = window.btoa(String.fromCharCode(...buffer)); + if ( + (base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && + (!remoteCDM || + remoteCDM.challenge === null || + base64Body !== remoteCDM.challenge) && + interceptType === "EME" + ) { + foundChallengeInBody = true; + window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*"); + // Block the request + return; + } + if ( + (base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && + interceptType == "LICENSE" && + !foundChallengeInBody + ) { + foundChallengeInBody = true; + window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*"); + if (!remoteCDM) { + if (base64Body.startsWith("CAES")) { + const { + device_type, + system_id, + security_level, + host, + secret, + device_name, + } = widevineDeviceInfo; + remoteCDM = new remoteWidevineCDM( + device_type, + system_id, + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + remoteCDM.getChallenge(foundWidevinePssh); + } + if (base64Body.startsWith("PD94")) { + const { security_level, host, secret, device_name } = + playreadyDeviceInfo; + remoteCDM = new remotePlayReadyCDM( + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + remoteCDM.getChallenge(foundPlayreadyPssh); + } + } + if (remoteCDM && remoteCDM.challenge === null) { + remoteCDM.getChallenge(foundWidevinePssh); + } + const injectedBody = base64ToUint8Array(remoteCDM.challenge); + config.body = injectedBody; + return originalFetch(resource, config); + } + } + if (typeof body === "string" && !isJson(body)) { + const base64EncodedBody = btoa(body); + if ( + (base64EncodedBody.startsWith("CAES") || + base64EncodedBody.startsWith("PD94")) && + (!remoteCDM || + remoteCDM.challenge === null || + base64EncodedBody !== remoteCDM.challenge) && + interceptType === "EME" + ) { + foundChallengeInBody = true; + window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*"); + // Block the request + return; + } + if ( + (base64EncodedBody.startsWith("CAES") || + base64EncodedBody.startsWith("PD94")) && + interceptType == "LICENSE" && + !foundChallengeInBody + ) { + foundChallengeInBody = true; + window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*"); + if (!remoteCDM) { + if (base64EncodedBody.startsWith("CAES")) { + const { + device_type, + system_id, + security_level, + host, + secret, + device_name, + } = widevineDeviceInfo; + remoteCDM = new remoteWidevineCDM( + device_type, + system_id, + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + remoteCDM.getChallenge(foundWidevinePssh); + } + if (base64EncodedBody.startsWith("PD94")) { + const { security_level, host, secret, device_name } = + playreadyDeviceInfo; + remoteCDM = new remotePlayReadyCDM( + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + remoteCDM.getChallenge(foundPlayreadyPssh); + } + } + if (remoteCDM && remoteCDM.challenge === null) { + remoteCDM.getChallenge(foundWidevinePssh); + } + const injectedBody = atob(remoteCDM.challenge); + config.body = injectedBody; + return originalFetch(resource, config); + } + } + if (typeof body === "string" && isJson(body)) { + const jsonBody = JSON.parse(body); + + if ( + (jsonContainsValue(jsonBody, "CAES") || + jsonContainsValue(jsonBody, "PD94")) && + (!remoteCDM || remoteCDM.challenge === null) && + interceptType === "EME" + ) { + foundChallengeInBody = true; + window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*"); + // Block the request + return; + } + + if ( + (jsonContainsValue(jsonBody, "CAES") || + jsonContainsValue(jsonBody, "PD94")) && + interceptType === "LICENSE" && + !foundChallengeInBody + ) { + foundChallengeInBody = true; + window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*"); + if (!remoteCDM) { + if (jsonContainsValue(jsonBody, "CAES")) { + const { + device_type, + system_id, + security_level, + host, + secret, + device_name, + } = widevineDeviceInfo; + remoteCDM = new remoteWidevineCDM( + device_type, + system_id, + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + remoteCDM.getChallenge(foundWidevinePssh); + } + if (jsonContainsValue(jsonBody, "PD94")) { + const { security_level, host, secret, device_name } = + playreadyDeviceInfo; + remoteCDM = new remotePlayReadyCDM( + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + remoteCDM.getChallenge(foundPlayreadyPssh); + } + } + if (remoteCDM && remoteCDM.challenge === null) { + remoteCDM.getChallenge(foundWidevinePssh); + } + const injectedBody = jsonReplaceValue(jsonBody, remoteCDM.challenge); + config.body = JSON.stringify(injectedBody); + } + } + } + } + + return originalFetch(resource, config); + }; +})(); + +// XHR POST interceptor +(function () { + const originalOpen = XMLHttpRequest.prototype.open; + const originalSend = XMLHttpRequest.prototype.send; + + XMLHttpRequest.prototype.open = function (method, url, async, user, password) { + this._method = method; + this._url = url; + return originalOpen.apply(this, arguments); + }; + + XMLHttpRequest.prototype.send = function (body) { + if (this._method && this._method.toUpperCase() === "POST") { + if (body) { + if (body instanceof ArrayBuffer || body instanceof Uint8Array) { + const buffer = body instanceof Uint8Array ? body : new Uint8Array(body); + const base64Body = window.btoa(String.fromCharCode(...buffer)); + if ( + (base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && + (!remoteCDM || + remoteCDM.challenge === null || + base64Body !== remoteCDM.challenge) && + interceptType === "EME" + ) { + foundChallengeInBody = true; + window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*"); + // Block the request + return; + } + if ( + (base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && + interceptType == "LICENSE" && + !foundChallengeInBody + ) { + foundChallengeInBody = true; + window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*"); + if (!remoteCDM) { + if (base64Body.startsWith("CAES")) { + const { + device_type, + system_id, + security_level, + host, + secret, + device_name, + } = widevineDeviceInfo; + remoteCDM = new remoteWidevineCDM( + device_type, + system_id, + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + remoteCDM.getChallenge(foundWidevinePssh); + } + if (base64Body.startsWith("PD94")) { + const { security_level, host, secret, device_name } = + playreadyDeviceInfo; + remoteCDM = new remotePlayReadyCDM( + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + remoteCDM.getChallenge(foundPlayreadyPssh); + } + } + if (remoteCDM && remoteCDM.challenge === null) { + remoteCDM.getChallenge(foundWidevinePssh); + } + const injectedBody = base64ToUint8Array(remoteCDM.challenge); + return originalSend.call(this, injectedBody); + } + } + + if (typeof body === "string" && !isJson(body)) { + const base64EncodedBody = btoa(body); + if ( + (base64EncodedBody.startsWith("CAES") || + base64EncodedBody.startsWith("PD94")) && + (!remoteCDM || + remoteCDM.challenge === null || + base64EncodedBody !== remoteCDM.challenge) && + interceptType === "EME" + ) { + foundChallengeInBody = true; + window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*"); + // Block the request + return; + } + if ( + (base64EncodedBody.startsWith("CAES") || + base64EncodedBody.startsWith("PD94")) && + interceptType == "LICENSE" && + !foundChallengeInBody + ) { + foundChallengeInBody = true; + window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*"); + if (!remoteCDM) { + if (base64EncodedBody.startsWith("CAES")) { + const { + device_type, + system_id, + security_level, + host, + secret, + device_name, + } = widevineDeviceInfo; + remoteCDM = new remoteWidevineCDM( + device_type, + system_id, + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + remoteCDM.getChallenge(foundWidevinePssh); + } + if (base64EncodedBody.startsWith("PD94")) { + const { security_level, host, secret, device_name } = + playreadyDeviceInfo; + remoteCDM = new remotePlayReadyCDM( + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + remoteCDM.getChallenge(foundPlayreadyPssh); + } + } + if (remoteCDM && remoteCDM.challenge === null) { + remoteCDM.getChallenge(foundWidevinePssh); + } + const injectedBody = atob(remoteCDM.challenge); + return originalSend.call(this, injectedBody); + } + } + + if (typeof body === "string" && isJson(body)) { + const jsonBody = JSON.parse(body); + + if ( + (jsonContainsValue(jsonBody, "CAES") || + jsonContainsValue(jsonBody, "PD94")) && + (!remoteCDM || remoteCDM.challenge === null) && + interceptType === "EME" + ) { + foundChallengeInBody = true; + window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*"); + // Block the request + return; + } + + if ( + (jsonContainsValue(jsonBody, "CAES") || + jsonContainsValue(jsonBody, "PD94")) && + interceptType === "LICENSE" && + !foundChallengeInBody + ) { + foundChallengeInBody = true; + window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*"); + if (!remoteCDM) { + if (jsonContainsValue(jsonBody, "CAES")) { + const { + device_type, + system_id, + security_level, + host, + secret, + device_name, + } = widevineDeviceInfo; + remoteCDM = new remoteWidevineCDM( + device_type, + system_id, + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + remoteCDM.getChallenge(foundWidevinePssh); + } + if (jsonContainsValue(jsonBody, "PD94")) { + const { security_level, host, secret, device_name } = + playreadyDeviceInfo; + remoteCDM = new remotePlayReadyCDM( + security_level, + host, + secret, + device_name + ); + remoteCDM.openSession(); + remoteCDM.getChallenge(foundPlayreadyPssh); + } + } + if (remoteCDM && remoteCDM.challenge === null) { + remoteCDM.getChallenge(foundWidevinePssh); + } + const injectedBody = jsonReplaceValue(jsonBody, remoteCDM.challenge); + return originalSend.call(this, JSON.stringify(injectedBody)); + } + } + } + } + return originalSend.apply(this, arguments); + }; +})(); diff --git a/mv2/manifest.json b/mv2/manifest.json new file mode 100644 index 0000000..fb34e80 --- /dev/null +++ b/mv2/manifest.json @@ -0,0 +1,41 @@ +{ + "manifest_version": 2, + "name": "CDRM Extension", + "version": "2.1.0", + "description": "Decrypt DRM protected content", + "permissions": [ + "webRequest", + "webRequestBlocking", + "", + "activeTab", + "storage", + "tabs", + "contextMenus" + ], + "background": { + "scripts": ["background.js"], + "persistent": true + }, + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"], + "run_at": "document_start", + "all_frames": true + } + ], + "web_accessible_resources": ["inject.js"], + "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", + "browser_action": { + "default_icon": { + "16": "icons/icon16.png", + "32": "icons/icon32.png", + "128": "icons/icon128.png" + } + }, + "icons": { + "16": "icons/icon16.png", + "32": "icons/icon32.png", + "128": "icons/icon128.png" + } +} diff --git a/src/background.js b/src/background.js index da9c10f..3c0fc5d 100644 --- a/src/background.js +++ b/src/background.js @@ -1,8 +1,7 @@ -// Open popout window when the extension icon is clicked -chrome.browserAction.onClicked.addListener(() => { +chrome.action.onClicked.addListener(() => { chrome.windows.create({ url: chrome.runtime.getURL("index.html"), - type: "popup", // opens as a floating window + type: "popup", width: 800, height: 600, }); diff --git a/src/manifest.json b/src/manifest.json index 00a5ab7..a2a5d0a 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,49 +1,37 @@ { - "manifest_version": 2, - "name": "CDRM Extension", - "version": "2.1.0", - "description": "Decrypt DRM protected content", - "permissions": [ - "webRequest", - "webRequestBlocking", - "", - "activeTab", - "storage", - "tabs", - "contextMenus" - ], - "background": { - "scripts": [ - "background.js" + "manifest_version": 3, + "name": "CDRM Extension", + "version": "2.1.0", + "description": "Decrypt DRM protected content", + "permissions": ["storage", "activeTab", "contextMenus"], + "host_permissions": [""], + "background": { + "service_worker": "background.js" + }, + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"], + "run_at": "document_start", + "all_frames": true + } ], - "persistent": true - }, - "content_scripts": [ - { - "matches": [ - "" - ], - "js": [ - "content.js" - ], - "run_at": "document_start", - "all_frames": true + "web_accessible_resources": [ + { + "resources": ["inject.js"], + "matches": [""] + } + ], + "action": { + "default_icon": { + "16": "icons/icon16.png", + "32": "icons/icon32.png", + "128": "icons/icon128.png" + } + }, + "icons": { + "16": "icons/icon16.png", + "32": "icons/icon32.png", + "128": "icons/icon128.png" } - ], - "web_accessible_resources": [ - "inject.js" - ], - "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", - "browser_action": { - "default_icon": { - "16": "icons/icon16.png", - "32": "icons/icon32.png", - "128": "icons/icon128.png" - } - }, - "icons": { - "16": "icons/icon16.png", - "32": "icons/icon32.png", - "128": "icons/icon128.png" - } -} \ No newline at end of file +}