Initial release
This commit is contained in:
TPD94 2025-06-01 12:40:24 -04:00
commit deea01fd64
28 changed files with 5227 additions and 0 deletions

86
background.js Normal file
View File

@ -0,0 +1,86 @@
// Open popout window when the extension icon is clicked
chrome.browserAction.onClicked.addListener(() => {
chrome.windows.create({
url: chrome.runtime.getURL("react/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 "INTERCEPTED_POST":
console.log("Storing POST Request", data);
chrome.storage.local.set({ latestLicenseRequest: data });
break;
case "PSSH_DATA":
console.log("Storing PSSH:", data);
chrome.storage.local.set({ latestPSSH: data });
break;
case "LICENSE_DATA":
console.log("Storing License Response:", data);
chrome.storage.local.set({ latestLicenseResponse: data });
break;
case "CERTIFICATE_DATA":
console.log("Storing Service Certificate:", data);
chrome.storage.local.set({ latestServiceCertificate: data });
break;
case "KEYS_DATA":
console.log("Storing Decryption Keys:", data);
chrome.storage.local.set({ latestKeys: data });
break;
case "DRM_TYPE":
console.log("DRM Type:", data);
chrome.storage.local.set({ drmType: 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({ 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.");
}
});
}
});

61
content.js Normal file
View File

@ -0,0 +1,61 @@
// Inject `inject.js` into the page context
(function injectScript() {
function append() {
const container = document.head || document.documentElement;
if (!container) {
return requestAnimationFrame(append); // Wait for DOM to exist
}
const script = document.createElement('script');
script.src = chrome.runtime.getURL('inject.js');
script.type = 'text/javascript';
script.onload = () => script.remove(); // Clean up after injecting
container.appendChild(script);
}
append();
})();
// Listen for messages from the injected script
window.addEventListener("message", function(event) {
if (event.source !== window) return;
if (["__INTERCEPTED_POST__", "__PSSH_DATA__", "__LICENSE_DATA__", "__CERTIFICATE_DATA__", "__KEYS_DATA__", "__DRM_TYPE__"].includes(event.data?.type)) {
chrome.runtime.sendMessage({
type: event.data.type.replace("__", "").replace("__", ""),
data: event.data.data
});
}
if (event.data.type === "__GET_CDM_DEVICES__") {
console.log("Received request for 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__") {
console.log("Received request for injection type");
chrome.storage.local.get("injection_type", (result) => {
const injectionType = result.injection_type || "LICENSE";
window.postMessage(
{
type: "__INJECTION_TYPE__",
injectionType
},
"*"
);
});
}
});

24
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

12
frontend/README.md Normal file
View File

@ -0,0 +1,12 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

33
frontend/eslint.config.js Normal file
View File

@ -0,0 +1,33 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

12
frontend/index.html Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CDRM Decryption Extension</title>
</head>
<body class="min-w-full min-h-full w-full h-full">
<div class="min-w-full min-h-full w-full h-full" id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

3411
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
frontend/package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.7",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.1",
"tailwindcss": "^4.1.7"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"vite": "^6.3.5"
}
}

77
frontend/src/App.jsx Normal file
View File

@ -0,0 +1,77 @@
import { useState, useEffect } from "react";
import {
HashRouter as Router,
Routes,
Route,
Navigate,
} from "react-router-dom";
import TopNav from "./components/topnav";
import SideNav from "./components/sidenav";
import Results from "./components/results";
import Settings from "./components/settings";
function App() {
const [isSideNavOpen, setIsSideNavOpen] = useState(false);
const [validConfig, setValidConfig] = useState(null); // null = loading
useEffect(() => {
chrome.storage.local.get("valid_config", (result) => {
if (chrome.runtime.lastError) {
console.error("Error reading valid_config:", chrome.runtime.lastError);
setValidConfig(false); // fallback
} else {
setValidConfig(result.valid_config === true);
}
});
}, []);
if (validConfig === null) {
return (
<div className="flex items-center justify-center h-screen bg-black text-white">
Loading...
</div>
);
}
return (
<Router>
<div className="min-w-full min-h-full w-full h-full flex flex-grow bg-black/95 flex-col relative">
<div className="w-full min-h-16 max-h-16 h-16 shrink-0 flex sticky top-0 z-20 border-b border-b-white bg-black">
<TopNav onMenuClick={() => setIsSideNavOpen(true)} />
</div>
<div id="currentpagecontainer" className="w-full grow overflow-y-auto">
<Routes>
{!validConfig ? (
<>
<Route
path="/settings"
element={
<Settings onConfigSaved={() => setValidConfig(true)} />
}
/>
<Route path="*" element={<Navigate to="/settings" replace />} />
</>
) : (
<>
<Route path="/" element={<Navigate to="/results" replace />} />
<Route path="/results" element={<Results />} />
<Route path="/settings" element={<Settings />} />
</>
)}
</Routes>
</div>
<div
className={`fixed top-0 left-0 w-full h-full z-50 bg-black transform transition-transform duration-300 ease-in-out ${
isSideNavOpen ? "translate-x-0" : "-translate-x-full"
}`}
>
<SideNav onClose={() => setIsSideNavOpen(false)} />
</div>
</div>
</Router>
);
}
export default App;

View File

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <circle cx="12" cy="12" r="10" stroke="#ffffff" stroke-width="1.5"/> <path d="M14.5 9.50002L9.5 14.5M9.49998 9.5L14.5 14.5" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round"/> </g>
</svg>

After

Width:  |  Height:  |  Size: 641 B

View File

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <path d="M20 7L4 7" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round"/> <path d="M20 12L4 12" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round"/> <path d="M20 17L4 17" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round"/> </g>
</svg>

After

Width:  |  Height:  |  Size: 703 B

View File

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M8 0L0 6V8H1V15H4V10H7V15H15V8H16V6L14 4.5V1H11V2.25L8 0ZM9 10H12V13H9V10Z" fill="#ffffff"/> </g>
</svg>

After

Width:  |  Height:  |  Size: 615 B

View File

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" fill="#000000">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <title>settings</title> <desc>Created with Sketch Beta.</desc> <defs> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"> <g id="Icon-Set" sketch:type="MSLayerGroup" transform="translate(-101.000000, -360.000000)" fill="#ffffff"> <path d="M128.52,381.134 L127.528,382.866 C127.254,383.345 126.648,383.508 126.173,383.232 L123.418,381.628 C122.02,383.219 120.129,384.359 117.983,384.799 L117.983,387 C117.983,387.553 117.54,388 116.992,388 L115.008,388 C114.46,388 114.017,387.553 114.017,387 L114.017,384.799 C111.871,384.359 109.98,383.219 108.582,381.628 L105.827,383.232 C105.352,383.508 104.746,383.345 104.472,382.866 L103.48,381.134 C103.206,380.656 103.369,380.044 103.843,379.769 L106.609,378.157 C106.28,377.163 106.083,376.106 106.083,375 C106.083,373.894 106.28,372.838 106.609,371.843 L103.843,370.232 C103.369,369.956 103.206,369.345 103.48,368.866 L104.472,367.134 C104.746,366.656 105.352,366.492 105.827,366.768 L108.582,368.372 C109.98,366.781 111.871,365.641 114.017,365.201 L114.017,363 C114.017,362.447 114.46,362 115.008,362 L116.992,362 C117.54,362 117.983,362.447 117.983,363 L117.983,365.201 C120.129,365.641 122.02,366.781 123.418,368.372 L126.173,366.768 C126.648,366.492 127.254,366.656 127.528,367.134 L128.52,368.866 C128.794,369.345 128.631,369.956 128.157,370.232 L125.391,371.843 C125.72,372.838 125.917,373.894 125.917,375 C125.917,376.106 125.72,377.163 125.391,378.157 L128.157,379.769 C128.631,380.044 128.794,380.656 128.52,381.134 L128.52,381.134 Z M130.008,378.536 L127.685,377.184 C127.815,376.474 127.901,375.749 127.901,375 C127.901,374.252 127.815,373.526 127.685,372.816 L130.008,371.464 C130.957,370.912 131.281,369.688 130.733,368.732 L128.75,365.268 C128.203,364.312 126.989,363.983 126.041,364.536 L123.694,365.901 C122.598,364.961 121.352,364.192 119.967,363.697 L119.967,362 C119.967,360.896 119.079,360 117.983,360 L114.017,360 C112.921,360 112.033,360.896 112.033,362 L112.033,363.697 C110.648,364.192 109.402,364.961 108.306,365.901 L105.959,364.536 C105.011,363.983 103.797,364.312 103.25,365.268 L101.267,368.732 C100.719,369.688 101.044,370.912 101.992,371.464 L104.315,372.816 C104.185,373.526 104.099,374.252 104.099,375 C104.099,375.749 104.185,376.474 104.315,377.184 L101.992,378.536 C101.044,379.088 100.719,380.312 101.267,381.268 L103.25,384.732 C103.797,385.688 105.011,386.017 105.959,385.464 L108.306,384.099 C109.402,385.039 110.648,385.809 112.033,386.303 L112.033,388 C112.033,389.104 112.921,390 114.017,390 L117.983,390 C119.079,390 119.967,389.104 119.967,388 L119.967,386.303 C121.352,385.809 122.598,385.039 123.694,384.099 L126.041,385.464 C126.989,386.017 128.203,385.688 128.75,384.732 L130.733,381.268 C131.281,380.312 130.957,379.088 130.008,378.536 L130.008,378.536 Z M116,378 C114.357,378 113.025,376.657 113.025,375 C113.025,373.344 114.357,372 116,372 C117.643,372 118.975,373.344 118.975,375 C118.975,376.657 117.643,378 116,378 L116,378 Z M116,370 C113.261,370 111.042,372.238 111.042,375 C111.042,377.762 113.261,380 116,380 C118.739,380 120.959,377.762 120.959,375 C120.959,372.238 118.739,370 116,370 L116,370 Z" id="settings" sketch:type="MSShapeGroup"> </path> </g> </g> </g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,144 @@
import React, { useEffect, useState } from "react";
function Results() {
const [drmType, setDrmType] = useState("");
const [pssh, setPssh] = useState("");
const [licenseUrl, setLicenseUrl] = useState("");
const [keys, setKeys] = useState("");
useEffect(() => {
chrome.storage.local.get(
["drmType", "latestPSSH", "latestLicenseRequest", "latestKeys"],
(result) => {
if (result.drmType) setDrmType(result.drmType);
if (result.latestPSSH) setPssh(result.latestPSSH);
if (result.latestLicenseRequest?.url)
setLicenseUrl(result.latestLicenseRequest.url);
if (result.latestKeys) {
try {
const parsed = Array.isArray(result.latestKeys)
? result.latestKeys
: JSON.parse(result.latestKeys);
setKeys(parsed);
} catch (e) {
console.error("Failed to parse keys:", e);
setKeys([]);
}
}
}
);
const handleChange = (changes, area) => {
if (area === "local") {
if (changes.drmType) {
setDrmType(changes.drmType.newValue);
}
if (changes.latestPSSH) {
setPssh(changes.latestPSSH.newValue);
}
if (changes.latestLicenseRequest) {
setLicenseUrl(changes.latestLicenseRequest.newValue.url);
}
if (changes.latestKeys) {
setKeys(changes.latestKeys.newValue);
}
}
};
chrome.storage.onChanged.addListener(handleChange);
return () => chrome.storage.onChanged.removeListener(handleChange);
}, []);
const handleCapture = () => {
// Reset stored values
chrome.storage.local.set({
drmType: "None",
latestPSSH: "None",
latestLicenseRequest: { url: "None" },
latestKeys: [],
});
// Get all normal windows to exclude your popup
chrome.windows.getAll(
{ populate: true, windowTypes: ["normal"] },
(windows) => {
if (!windows || windows.length === 0) {
console.warn("No normal Chrome windows found");
return;
}
// Find the last focused normal window
const lastFocusedWindow = windows.find((w) => w.focused) || windows[0];
if (!lastFocusedWindow) {
console.warn("No focused normal window found");
return;
}
// Find the active tab in that window (that is a regular webpage)
const activeTab = lastFocusedWindow.tabs.find(
(tab) => tab.active && tab.url && /^https?:\/\//.test(tab.url)
);
if (activeTab?.id) {
chrome.tabs.reload(activeTab.id, () => {
if (chrome.runtime.lastError) {
console.error("Failed to reload tab:", chrome.runtime.lastError);
}
});
} else {
console.warn("No active tab found in the last focused normal window");
}
}
);
};
return (
<div className="w-full grow flex h-full overflow-y-auto overflow-x-auto flex-col text-white p-4">
<button
onClick={handleCapture}
className="w-full h-10 bg-sky-500 rounded-md p-2 mt-2 text-white cursor-pointer hover:bg-sky-600"
>
Capture current tab
</button>
<p className="text-2xl mt-5">DRM Type</p>
<input
type="text"
value={drmType}
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white"
placeholder="None"
disabled
/>
<p className="text-2xl mt-5">PSSH</p>
<input
type="text"
value={pssh}
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white"
placeholder="None"
disabled
/>
<p className="text-2xl mt-5">License URL</p>
<input
type="text"
value={licenseUrl}
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white"
placeholder="None"
disabled
/>
<p className="text-2xl mt-5">Keys</p>
<div className="w-full min-h-64 h-64 flex items-center justify-center text-center overflow-y-auto bg-slate-800/50 rounded-md p-2 mt-2 text-white whitespace-pre-line">
{Array.isArray(keys) &&
keys.filter((k) => k.type !== "SIGNING").length > 0 ? (
keys
.filter((k) => k.type !== "SIGNING")
.map((k) => `${k.key_id || k.keyId}:${k.key}`)
.join("\n")
) : (
<span className="text-gray-400">None</span>
)}
</div>
</div>
);
}
export default Results;

View File

@ -0,0 +1,150 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
function Settings({ onConfigSaved }) {
const [instanceUrl, setInstanceUrl] = useState("");
const [storedUrl, setStoredUrl] = useState(null);
const [message, setMessage] = useState(null);
const [messageType, setMessageType] = useState(null);
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
useEffect(() => {
chrome.storage.local.get("cdrm_instance", (result) => {
if (chrome.runtime.lastError) {
console.error(
"Error fetching CDRM instance:",
chrome.runtime.lastError
);
} else if (result.cdrm_instance) {
setStoredUrl(result.cdrm_instance);
}
});
}, []);
const handleSave = async () => {
const trimmedUrl = instanceUrl.trim().replace(/\/+$/, "");
if (!trimmedUrl) {
setMessage("Please enter a valid URL.");
setMessageType("error");
return;
}
const endpoint = trimmedUrl + "/api/extension";
setLoading(true);
setMessage(null);
try {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (data.status === true) {
setMessage("Successfully connected to CDRM Instance.");
setMessageType("success");
const widevineRes = await fetch(
`${trimmedUrl}/remotecdm/widevine/deviceinfo`
);
if (!widevineRes.ok)
throw new Error("Failed to fetch Widevine device info");
const widevineData = await widevineRes.json();
const playreadyRes = await fetch(
`${trimmedUrl}/remotecdm/playready/deviceinfo`
);
if (!playreadyRes.ok)
throw new Error("Failed to fetch PlayReady device info");
const playreadyData = await playreadyRes.json();
chrome.storage.local.set(
{
valid_config: true,
cdrm_instance: trimmedUrl,
widevine_device: {
device_type: widevineData.device_type,
system_id: widevineData.system_id,
security_level: widevineData.security_level,
secret: widevineData.secret,
device_name: widevineData.device_name,
host: trimmedUrl,
},
playready_device: {
security_level: playreadyData.security_level,
secret: playreadyData.secret,
device_name: playreadyData.device_name,
host: trimmedUrl,
},
},
() => {
if (chrome.runtime.lastError) {
console.error(
"Error saving to chrome.storage:",
chrome.runtime.lastError
);
setMessage("Error saving configuration.");
setMessageType("error");
} else {
console.log("Configuration saved.");
setStoredUrl(trimmedUrl);
setInstanceUrl("");
if (onConfigSaved) onConfigSaved();
navigate("/results"); // Automatically redirect after success
}
}
);
} else {
throw new Error("Invalid response from endpoint.");
}
} catch (err) {
console.error("Connection error:", err);
setMessage("Invalid endpoint or device info could not be retrieved.");
setMessageType("error");
} finally {
setLoading(false);
}
};
return (
<div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col p-4">
<input
type="text"
value={instanceUrl}
onChange={(e) => setInstanceUrl(e.target.value)}
placeholder={
storedUrl
? `Current CDRM Instance: ${storedUrl}`
: "CDRM Instance URL (e.g., https://cdrm-project.com/, http://127.0.0.1:5000/)"
}
className="w-full p-4 text-lg bg-gray-800 text-white border border-gray-700 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 mt-4"
/>
<button
onClick={handleSave}
disabled={loading}
className={`mt-4 p-2 ${
loading ? "bg-blue-400" : "bg-blue-600 hover:bg-blue-700"
} text-white rounded-md transition duration-300`}
>
{loading ? "Connecting..." : "Save Settings"}
</button>
{message && (
<p
className={`mt-2 text-sm text-center ${
messageType === "success" ? "text-green-400" : "text-red-400"
}`}
>
{message}
</p>
)}
</div>
);
}
export default Settings;

View File

@ -0,0 +1,51 @@
import { NavLink } from "react-router-dom";
import homeIcon from "../assets/home.svg";
import settingsIcon from "../assets/settings.svg";
import closeIcon from "../assets/close.svg";
function SideNav({ onClose }) {
return (
<div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col bg-black">
<div className="w-full min-h-16 max-h-16 h-16 shrink-0 flex sticky top-0 z-20 border-b border-b-white bg-black">
<button
onClick={onClose}
className="h-full ml-auto p-3 hover:cursor-pointer"
>
<img src={closeIcon} alt="Close" className="h-full" />
</button>
</div>
<div className="w-full h-16 flex items-center justify-center mt-2">
<NavLink
to="/results"
onClick={onClose}
className="text-white text-2xl font-bold flex flex-row items-center border-l-white hover:border-l-1 w-full hover:bg-black/50 transition duration-300 ease-in-out p-2"
>
<img
src={homeIcon}
alt="Home"
className="h-full w-16 p-2 flex items-center cursor-pointer"
/>
Home
</NavLink>
</div>
<div className="w-full h-16 flex items-center justify-center mt-2">
<NavLink
to="/settings"
onClick={onClose}
className="text-white text-2xl font-bold flex flex-row items-center hover:border-l-1 border-l-white w-full hover:bg-black/50 transition duration-300 ease-in-out p-2"
>
<img
src={settingsIcon}
alt="Settings"
className="h-full w-16 p-2 flex items-center cursor-pointer"
/>
Settings
</NavLink>
</div>
</div>
);
}
export default SideNav;

View File

@ -0,0 +1,70 @@
import { useEffect, useState } from "react";
import hamburgerIcon from "../assets/hamburger.svg";
function TopNav({ onMenuClick }) {
const [injectionType, setInjectionType] = useState("LICENSE");
useEffect(() => {
chrome.storage.local.get("injection_type", (result) => {
if (result.injection_type) {
setInjectionType(result.injection_type);
}
});
}, []);
const handleInjectionTypeChange = (type) => {
chrome.storage.local.set({ injection_type: type }, () => {
if (chrome.runtime.lastError) {
console.error(
"Error updating injection_type:",
chrome.runtime.lastError
);
} else {
setInjectionType(type);
console.log(`Injection type updated to ${type}`);
}
});
};
return (
<div className="w-full h-full flex flex-row">
<img
src={hamburgerIcon}
alt="Menu"
className="h-full w-16 p-2 flex items-center cursor-pointer"
onClick={onMenuClick}
/>
<div className="flex flex-row h-full justify-center items-center ml-auto mr-2">
<p className="text-white text-lg p-2 mr-2 border-r-2 border-r-white text-nowrap">
Injection Type:
</p>
<button
onClick={() => handleInjectionTypeChange("LICENSE")}
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
injectionType === "LICENSE" ? "bg-sky-500/70" : "bg-black"
}`}
>
License
</button>
<button
onClick={() => handleInjectionTypeChange("EME")}
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
injectionType === "EME" ? "bg-green-500/70" : "bg-black"
}`}
>
EME
</button>
<button
onClick={() => handleInjectionTypeChange("DISABLED")}
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
injectionType === "DISABLED" ? "bg-red-500/70" : "bg-black"
}`}
>
Disabled
</button>
</div>
</div>
);
}
export default TopNav;

8
frontend/src/index.css Normal file
View File

@ -0,0 +1,8 @@
@import "tailwindcss";
html, body, #root {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}

10
frontend/src/main.jsx Normal file
View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

9
frontend/vite.config.js Normal file
View File

@ -0,0 +1,9 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
base: './',
plugins: [react(), tailwindcss()],
})

BIN
icons/icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
icons/icon16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

BIN
icons/icon32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

908
inject.js Normal file
View File

@ -0,0 +1,908 @@
let customBase64 = "PlaceHolder";
let psshFound = false;
let postRequestFound = false;
let firstValidLicenseResponse = false;
let firstValidServiceCertificate = false;
let remoteCDM = null;
let decryptionKeys = null;
let messageSuppressed = false;
let interceptType = "DISABLED"; // Default to LICENSE, can be changed to 'EME' for EME interception
let originalChallenge = null;
let widevineDeviceInfo = null;
let playreadyDeviceInfo = null;
window.postMessage({ type: "__GET_INJECTION_TYPE__" }, "*");
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);
}
});
window.postMessage({ type: "__GET_CDM_DEVICES__" }, "*");
window.addEventListener("message", function(event) {
if (event.source !== window) return;
if (event.data.type === "__CDM_DEVICES__") {
const { widevine_device, playready_device } = event.data;
// Now you can use widevine_device and playready_device!
console.log("Received device info:", widevine_device, playready_device);
// Store them globally
widevineDeviceInfo = widevine_device;
playreadyDeviceInfo = playready_device;
}
});
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;
}
async openSession() {
const url = `${this.host}/remotecdm/playready/${this.device_name}/open`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const jsonData = await response.json();
if (response.ok && jsonData.data?.session_id) {
this.session_id = jsonData.data.session_id;
return { success: true, session_id: this.session_id };
} else {
return { success: false, error: jsonData.message || 'Unknown error occurred.' };
}
} catch (error) {
return { success: false, error: error.message };
}
}
async getChallenge(init_data) {
const url = `${this.host}/remotecdm/playready/${this.device_name}/get_license_challenge`;
const body = {
session_id: this.session_id,
init_data: init_data,
};
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
const jsonData = await response.json();
if (response.ok && jsonData.data?.challenge) {
return {
success: true,
challenge: jsonData.data.challenge
};
} else {
return {
success: false,
error: jsonData.message || 'Failed to retrieve license challenge.'
};
}
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async parseLicense(license_message) {
const url = `${this.host}/remotecdm/playready/${this.device_name}/parse_license`;
const body = {
session_id: this.session_id,
license_message: license_message
};
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
const jsonData = await response.json();
if (response.ok && jsonData.message === "Successfully parsed and loaded the Keys from the License message") {
return {
success: true,
message: jsonData.message
};
} else {
return {
success: false,
error: jsonData.message || 'Failed to parse license.'
};
}
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async closeSession() {
const url = `${this.host}/remotecdm/playready/${this.device_name}/close/${this.session_id}`;
try {
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
const jsonData = await response.json();
if (response.ok) {
return { success: true, message: jsonData.message };
} else {
return { success: false, error: jsonData.message || 'Failed to close session.' };
}
} catch (error) {
return { success: false, error: error.message };
}
}
async getKeys() {
const url = `${this.host}/remotecdm/playready/${this.device_name}/get_keys`;
const body = {
session_id: this.session_id
};
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const jsonData = await response.json();
if (response.ok && jsonData.data?.keys) {
decryptionKeys = jsonData.data.keys;
// Automatically close the session after key retrieval
await this.closeSession();
return { success: true, keys: decryptionKeys };
} else {
return {
success: false,
error: jsonData.message || 'Failed to retrieve decryption keys.'
};
}
} catch (error) {
return { success: false, error: error.message };
}
}
}
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;
}
async openSession() {
const url = `${this.host}/remotecdm/widevine/${this.device_name}/open`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const jsonData = await response.json();
if (response.ok && jsonData.status === 200 && jsonData.data?.session_id) {
this.session_id = jsonData.data.session_id;
return { success: true, session_id: this.session_id };
} else {
return { success: false, error: jsonData.message || 'Unknown error occurred.' };
}
} catch (error) {
return { success: false, error: error.message };
}
}
async setServiceCertificate(certificate) {
const url = `${this.host}/remotecdm/widevine/${this.device_name}/set_service_certificate`;
const body = {
session_id: this.session_id,
certificate: certificate ?? null
};
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
const jsonData = await response.json();
if (response.ok && jsonData.status === 200) {
return { success: true };
} else {
return { success: false, error: jsonData.message || 'Failed to set service certificate.' };
}
} catch (error) {
return { success: false, error: error.message };
}
}
async getChallenge(init_data, license_type = 'STREAMING', privacy_mode = false) {
const url = `${this.host}/remotecdm/widevine/${this.device_name}/get_license_challenge/${license_type}`;
const body = {
session_id: this.session_id,
init_data: init_data,
privacy_mode: privacy_mode
};
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
const jsonData = await response.json();
if (response.ok && jsonData.status === 200 && jsonData.data?.challenge_b64) {
return {
success: true,
challenge: jsonData.data.challenge_b64
};
} else {
return {
success: false,
error: jsonData.message || 'Failed to retrieve license challenge.'
};
}
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async parseLicense(license_message) {
const url = `${this.host}/remotecdm/widevine/${this.device_name}/parse_license`;
const body = {
session_id: this.session_id,
license_message: license_message
};
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
const jsonData = await response.json();
if (response.ok && jsonData.status === 200) {
return {
success: true,
message: jsonData.message
};
} else {
return {
success: false,
error: jsonData.message || 'Failed to parse license.'
};
}
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async closeSession() {
const url = `${this.host}/remotecdm/widevine/${this.device_name}/close/${this.session_id}`;
try {
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
const jsonData = await response.json();
if (response.ok && jsonData.status === 200) {
return { success: true, message: jsonData.message };
} else {
return { success: false, error: jsonData.message || 'Failed to close session.' };
}
} catch (error) {
return { success: false, error: error.message };
}
}
async getKeys() {
const url = `${this.host}/remotecdm/widevine/${this.device_name}/get_keys/ALL`;
const body = {
session_id: this.session_id
};
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const jsonData = await response.json();
if (response.ok && jsonData.status === 200 && jsonData.data?.keys) {
decryptionKeys = jsonData.data.keys;
// Automatically close the session after key retrieval
await this.closeSession();
return { success: true, keys: decryptionKeys };
} else {
return {
success: false,
error: jsonData.message || 'Failed to retrieve decryption keys.'
};
}
} catch (error) {
return { success: false, error: error.message };
}
}
}
// --- Utility functions ---
const hexStrToU8 = hexString =>
Uint8Array.from(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
const u8ToHexStr = bytes =>
bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
const b64ToHexStr = b64 =>
[...atob(b64)].map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join``;
function jsonContainsValue(obj, target) {
if (typeof obj === "string") return obj === target;
if (Array.isArray(obj)) return obj.some(val => jsonContainsValue(val, target));
if (typeof obj === "object" && obj !== null) {
return Object.values(obj).some(val => jsonContainsValue(val, target));
}
return false;
}
function jsonReplaceValue(obj, target, newValue) {
if (typeof obj === "string") {
return obj === target ? newValue : obj;
}
if (Array.isArray(obj)) {
return obj.map(item => jsonReplaceValue(item, target, newValue));
}
if (typeof obj === "object" && obj !== null) {
const newObj = {};
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
newObj[key] = jsonReplaceValue(obj[key], target, newValue);
}
}
return newObj;
}
return obj;
}
const isJson = (str) => {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
};
// --- Widevine-style PSSH extractor ---
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));
}
// --- PlayReady-style PSSH extractor ---
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));
}
// --- Clearkey Support ---
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, '/')),
}));
}
// --- Convert Base64 to Uint8Array ---
function base64ToUint8Array(base64) {
const binaryStr = atob(base64); // Decode base64 to binary string
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;
// Convert each byte to a character
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(uint8array[i]);
}
// Encode the binary string to Base64
return window.btoa(binary);
}
// --- Intercepting EME Calls ---
const originalGenerateRequest = MediaKeySession.prototype.generateRequest;
MediaKeySession.prototype.generateRequest = async function(initDataType, initData) {
console.log(initData);
const session = this;
let playReadyAttempted = false;
let playReadySucceeded = false;
let playReadyPssh = null;
let widevinePssh = null;
if (!psshFound && !messageSuppressed && (interceptType === 'EME' || interceptType === 'LICENSE')) {
// === Try PlayReady First ===
playReadyPssh = getPlayReadyPssh(initData);
playReadyAttempted = !!playReadyPssh;
if (playReadyPssh) {
console.log("[PlayReady PSSH] Found:", playReadyPssh);
const drmType = {
type: "__DRM_TYPE__",
data: 'PlayReady'
};
window.postMessage(drmType, "*");
try {
const {
security_level, host, secret, device_name
} = playreadyDeviceInfo;
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
const sessionResult = await remoteCDM.openSession();
if (sessionResult.success) {
console.log("PlayReady session opened:", sessionResult.session_id);
const challengeResult = await remoteCDM.getChallenge(playReadyPssh);
if (challengeResult.success) {
customBase64 = btoa(challengeResult.challenge);
playReadySucceeded = true;
psshFound = true;
window.postMessage({ type: "__PSSH_DATA__", data: playReadyPssh }, "*");
} else {
console.warn("PlayReady challenge failed:", challengeResult.error);
}
} else {
console.warn("PlayReady session failed:", sessionResult.error);
}
} catch (err) {
console.error("PlayReady error:", err.message);
}
} else {
console.log("[PlayReady PSSH] Not found.");
}
// === Fallback to Widevine ===
if (!playReadySucceeded) {
widevinePssh = getWidevinePssh(initData);
if (widevinePssh) {
console.log("[Widevine PSSH] Found:", widevinePssh);
const drmType = {
type: "__DRM_TYPE__",
data: 'Widevine'
};
window.postMessage(drmType, "*");
try {
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);
const sessionResult = await remoteCDM.openSession();
if (sessionResult.success) {
console.log("Widevine session opened:", sessionResult.session_id);
const challengeResult = await remoteCDM.getChallenge(widevinePssh);
if (challengeResult.success) {
customBase64 = challengeResult.challenge;
psshFound = true;
window.postMessage({ type: "__PSSH_DATA__", data: widevinePssh }, "*");
} else {
console.warn("Widevine challenge failed:", challengeResult.error);
}
} else {
console.warn("Widevine session failed:", sessionResult.error);
}
} catch (err) {
console.error("Widevine error:", err.message);
}
} else {
console.log("[Widevine PSSH] Not found.");
}
}
// === Intercept License or EME Messages ===
if (!messageSuppressed && interceptType === 'EME') {
session.addEventListener("message", function originalMessageInterceptor(event) {
event.stopImmediatePropagation();
console.log("[Intercepted EME Message] Injecting custom message.");
console.log(event.data);
const uint8 = base64ToUint8Array(customBase64);
const arrayBuffer = uint8.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: () => arrayBuffer
});
console.log(syntheticEvent);
setTimeout(() => session.dispatchEvent(syntheticEvent), 0);
}, { once: true });
messageSuppressed = true;
}
if (!messageSuppressed && interceptType === 'LICENSE') {
session.addEventListener("message", function originalMessageInterceptor(event) {
if (playReadyAttempted && playReadySucceeded) {
const buffer = event.message;
const decoder = new TextDecoder('utf-16');
const decodedText = decoder.decode(buffer);
const match = decodedText.match(/<Challenge encoding="base64encoded">([^<]+)<\/Challenge>/);
if (match) {
originalChallenge = match[1];
console.log("[PlayReady Challenge Extracted]");
messageSuppressed = true;
}
}
if (!playReadySucceeded && widevinePssh && psshFound) {
const uint8Array = new Uint8Array(event.message);
const b64array = arrayBufferToBase64(uint8Array);
if (b64array !== "CAQ=") {
originalChallenge = b64array;
console.log("[Widevine Challenge Extracted]");
messageSuppressed = true;
}
}
}, { once: false });
}
}
// Proceed with original generateRequest
return originalGenerateRequest.call(session, initDataType, initData);
};
// license message handler
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));
// Handle Service Certificate
if (base64Response.startsWith("CAUS") && !firstValidServiceCertificate) {
const base64ServiceCertificateData = {
type: "__CERTIFICATE_DATA__",
data: base64Response
};
window.postMessage(base64ServiceCertificateData, "*");
firstValidServiceCertificate = true;
}
// Handle License Data
if (!base64Response.startsWith("CAUS") && !firstValidLicenseResponse && (interceptType === 'EME' || interceptType === 'LICENSE')) {
firstValidLicenseResponse = true;
// 🔁 Call parseLicense, then getKeys from global remoteCDM
if (remoteCDM !== null && remoteCDM.session_id) {
remoteCDM.parseLicense(base64Response)
.then(result => {
if (result.success) {
console.log("[Base64 Response]", base64Response);
const base64LicenseData = {
type: "__LICENSE_DATA__",
data: base64Response
};
window.postMessage(base64LicenseData, "*");
console.log("[remoteCDM] License parsed successfully");
// 🚀 Now call getKeys after parsing
return remoteCDM.getKeys();
} else {
console.warn("[remoteCDM] License parse failed:", result.error);
}
})
.then(keysResult => {
if (keysResult?.success) {
const keysData = {
type: "__KEYS_DATA__",
data: keysResult.keys
};
window.postMessage(keysData, "*");
console.log("[remoteCDM] Decryption keys retrieved:", keysResult.keys);
} else if (keysResult) {
console.warn("[remoteCDM] Failed to retrieve keys:", keysResult.error);
}
})
.catch(err => {
console.error("[remoteCDM] Unexpected error in license flow:", err);
});
} else {
console.warn("[remoteCDM] Cannot parse license: remoteCDM not initialized or session_id missing.");
}
}
const updatePromise = originalUpdate.call(this, response);
if (!psshFound) {
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;
};
// --- Request Interception ---
(function interceptRequests() {
const sendToBackground = (data) => {
window.postMessage({ type: "__INTERCEPTED_POST__", data }, "*");
};
// Intercept fetch
const originalFetch = window.fetch;
window.fetch = async function(input, init = {}) {
const method = (init.method || 'GET').toUpperCase();
if (method === "POST") {
const url = typeof input === "string" ? input : input.url;
let body = init.body;
// If the body is FormData, convert it to an object (or JSON)
if (body instanceof FormData) {
const formData = {};
body.forEach((value, key) => {
formData[key] = value;
});
body = JSON.stringify(formData); // Convert formData to JSON string
}
const headers = {};
if (init.headers instanceof Headers) {
init.headers.forEach((v, k) => { headers[k] = v; });
} else {
Object.assign(headers, init.headers || {});
}
try {
let modifiedBody = body; // Keep a reference to the original body
// Handle body based on its type
if (typeof body === 'string') {
if (isJson(body)) {
const parsed = JSON.parse(body);
if (jsonContainsValue(parsed, customBase64)) {
sendToBackground({ url, method, headers, body });
}
if (jsonContainsValue(parsed, originalChallenge)) {
newJSONBody = jsonReplaceValue(parsed, originalChallenge, customBase64);
modifiedBody = JSON.stringify(newJSONBody)
sendToBackground({ url, method, headers, modifiedBody });
}
} else if (body === customBase64) {
sendToBackground({ url, method, headers, body });
} else if (btoa(body) == originalChallenge) {
modifiedBody = atob(customBase64);
sendToBackground({ url, method, headers, modifiedBody });
}
}else 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 === customBase64) {
sendToBackground({ url, method, headers, body: base64Body });
}
if (base64Body === originalChallenge) {
modifiedBody = base64ToUint8Array(customBase64); // Modify the body
sendToBackground({ url, method, headers, body: modifiedBody });
}
}
// Ensure the modified body is used and passed to the original fetch call
init.body = modifiedBody;
} catch (e) {
console.warn("Error handling fetch body:", e);
}
}
// Call the original fetch method with the potentially modified body
return originalFetch(input, init);
};
// Intercept XMLHttpRequest
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?.toUpperCase() === "POST") {
const xhr = this;
const headers = {};
const originalSetRequestHeader = xhr.setRequestHeader;
xhr.setRequestHeader = function(header, value) {
headers[header] = value;
return originalSetRequestHeader.apply(this, arguments);
};
setTimeout(() => {
try {
let modifiedBody = body; // Start with the original body
// Check if the body is a string and can be parsed as JSON
if (typeof body === 'string') {
if (isJson(body)) {
const parsed = JSON.parse(body);
if (jsonContainsValue(parsed, customBase64)) {
sendToBackground({ url: xhr._url, method: xhr._method, headers, body });
}
if (jsonContainsValue(parsed, originalChallenge)) {
newJSONBody = jsonReplaceValue(parsed, originalChallenge, customBase64);
modifiedBody = JSON.stringify(newJSONBody);
sendToBackground({ url: xhr._url, method: xhr._method, headers, modifiedBody });
}
} else if (body === originalChallenge) {
modifiedBody = customBase64
sendToBackground({ url: xhr._url, method: xhr._method, headers, body });
} else if (btoa(body) == originalChallenge) {
modifiedBody = atob(customBase64);
sendToBackground({ url: xhr._url, method: xhr._method, headers, body });
}
} else 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 === customBase64) {
sendToBackground({ url: xhr._url, method: xhr._method, headers, body: base64Body });
}
if (base64Body === originalChallenge) {
modifiedBody = base64ToUint8Array(customBase64); // Modify the body
sendToBackground({ url: xhr._url, method: xhr._method, headers, body: modifiedBody });
}
}
// Ensure original send is called only once with the potentially modified body
originalSend.apply(this, [modifiedBody]);
} catch (e) {
console.warn("Error handling XHR body:", e);
}
}, 0);
} else {
// Call the original send for non-POST requests
return originalSend.apply(this, arguments);
}
};
})();

37
manifest.json Normal file
View File

@ -0,0 +1,37 @@
{
"manifest_version": 2,
"name": "CDRM Extension 2.0",
"version": "2.0",
"description": "Decrypt DRM Protected content",
"permissions": [
"storage",
"tabs",
"activeTab",
"webRequest",
"webRequestBlocking",
"<all_urls>"
],
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
],
"web_accessible_resources": ["inject.js"],
"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"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

13
react/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CDRM Decryption Extension</title>
<script type="module" crossorigin src="./assets/index-DwE1Hrng.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DyrP80vq.css">
</head>
<body class="min-w-full min-h-full w-full h-full">
<div class="min-w-full min-h-full w-full h-full" id="root"></div>
</body>
</html>