Compare commits

..

26 Commits
main ... main

Author SHA1 Message Date
voldemort
ee9eeb30ea feat: add js minification during file copy in build process 2025-07-22 10:55:02 +07:00
voldemort
0b59c6b0d6 feat: implement resetDRMState to clear state and session on new manifest detection 2025-07-22 10:28:25 +07:00
voldemort
f40e1880d6 add logging for POST request interception in DRM handling 2025-07-22 10:13:17 +07:00
voldemort
8d4cd89a02 consolidate DRM interception handling for fetch and XHR requests 2025-07-22 10:04:17 +07:00
voldemort
a40a6abaf7 extract DRM PSSH detection and storage into a separate function 2025-07-22 09:56:05 +07:00
voldemort
81f44b2f0e concatenate message handling for DRM, injection type, and CDM devices 2025-07-22 09:52:12 +07:00
voldemort
e03ca633de do not show export if keys are not present 2025-07-21 23:29:40 +07:00
voldemort
d0154fd6c1 change MANIFEST_URL_FOUND to MANIFEST_URL 2025-07-21 22:57:28 +07:00
voldemort
867294f7f6 refactor: extract DRM challenge detection and handling for fetch and XHR requests 2025-07-21 20:25:47 +07:00
voldemort
f8712d7726 refactor: consolidate remote cdm initialization into a helper function 2025-07-21 18:28:59 +07:00
voldemort
9e071365e3 use regular prefix instead of adding color. fixes not storing to chrome.storage.local 2025-07-21 17:18:18 +07:00
voldemort
89f66b25be add extension prefix and color for ease of debugging and filtering 2025-07-21 16:05:35 +07:00
voldemort
30e797bc09 use global DRM_SIGNATURES for readability 2025-07-21 11:54:40 +07:00
voldemort
3bd2e0f465 refactor: extract redundant code into helpers (part 1) 2025-07-21 11:41:49 +07:00
voldemort
9cff5b44bd forgot to remove _FOUND in MANIFEST_URL 2025-07-21 11:39:35 +07:00
voldemort
3fae8f296f show "use yt-dlp" in manifest for youtube, use plugin-react-swc 2025-07-20 20:10:47 +07:00
voldemort
6e22837047 Fix Trusted Types violation in manifest interceptor injection, which fixes data not showing for YouTube DRM 2025-07-20 17:43:13 +07:00
voldemort
889a4c63f3 sync version: only update the "version" instead of parsing whole file 2025-07-20 17:21:02 +07:00
voldemort
918269f42e update setting UI, showing current instance in a separate line 2025-07-20 17:16:40 +07:00
voldemort
5cc6834532 add export to json button 2025-07-20 16:56:34 +07:00
voldemort
5949228d4f update to manifest v3 2025-07-20 16:27:13 +07:00
voldemort
5678c9b5da add manifest URL in frontend 2025-07-20 16:00:59 +07:00
voldemort
9c738d8a3e dynamically add version to extension title bar and manifest.json, add readme 2025-07-20 15:03:43 +07:00
voldemort
53aa7d66e3 remove leftover dist folder 2025-07-20 14:24:03 +07:00
voldemort
935d235ad9 organize files and folders, remove unnecessary generated build files, add new script to build extension to extension-release folder 2025-07-20 14:22:04 +07:00
voldemort
c9ff17558d prettier formatting, add manifest url in console.log 2025-07-20 12:18:15 +07:00
37 changed files with 7229 additions and 5029 deletions

30
.gitignore vendored Normal file
View File

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

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
react/
frontend/dist/
frontend/src/assets/

8
.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"useTabs": false,
"printWidth": 100
}

52
README.md Normal file
View File

@ -0,0 +1,52 @@
# CDRM Extension
An extension to show keys from DRM protected content, which are used to decrypt content.
## Notes
Keep these extension core files inside `src`:
- `background.js`
- `content.js`
- `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`.
To update the version across the entire project, simply change the version number in the root `package.json`. The build script will handle version sync automatically to both the extension's version and the frontend's title bar.
## Build instructions
### Prerequisites
- Node.js v21 or higher. [Download Node.js here](https://nodejs.org/en/download).
### How to build by yourself
- Open terminal at the project root
- Run the build script:
```bash
npm install
npm run buildext
```
This will:
- Sync the version number from the root `package.json` to `src/manifest.json` and `frontend/package.json`
- Install frontend dependencies if needed
- Build the React frontend
- Clean and prepare the `extension-release` folder
- Copy extension files in `src`, built frontend assets, and icons into `extension-release`
### How to load the extension in Google Chrome or Chromium browsers
1. Go to `chrome://extensions/`
2. Enable **Developer mode**
3. Click **Load unpacked** and select the `extension-release` folder
4. Verify the extension is working by clicking its icon or opening the developer console (F12) to check for any logs or errors.

View File

@ -1,84 +0,0 @@
// 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 "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;
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.");
}
});
}
});

119
buildext.js Normal file
View File

@ -0,0 +1,119 @@
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
import { minify } from "terser";
import url from "url";
import syncVersion from "./syncVersion.js";
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
const frontendDir = path.join(__dirname, "frontend");
const distDir = path.join(frontendDir, "dist");
const srcDir = path.join(__dirname, "src");
const iconDir = path.join(__dirname, "icons");
const releaseDir = path.join(__dirname, "extension-release");
const run = (cmd, cwd) => {
console.log(`🛠️ Running: ${cmd}`);
execSync(cmd, { cwd, stdio: "inherit" });
};
const copyDir = async (src, dest) => {
await fs.promises.mkdir(dest, { recursive: true });
await fs.promises.cp(src, dest, {
recursive: true,
force: true,
filter: (src) => !src.endsWith(".map"),
});
};
const minifyJS = async (jsContent) => {
try {
const result = await minify(jsContent, {
compress: {
drop_console: false, // Keep console logs for debugging
drop_debugger: true,
pure_funcs: ["console.debug"],
},
mangle: {
reserved: ["chrome"], // Don't mangle chrome API
},
});
return result.code;
} catch (error) {
console.warn("⚠️ Minification failed, using original:", error.message);
return jsContent;
}
};
// Copy and minify JavaScript files from src directory
const copyAndMinifySrcFiles = async (src, dest) => {
await fs.promises.mkdir(dest, { recursive: true });
const entries = await fs.promises.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
await copyAndMinifySrcFiles(srcPath, destPath);
} else if (entry.name.endsWith(".js")) {
// Minify JavaScript files
console.log(`🗜️ Minifying ${entry.name}...`);
const content = await fs.promises.readFile(srcPath, "utf8");
const originalSize = Buffer.byteLength(content, "utf8");
const minified = await minifyJS(content, entry.name);
const minifiedSize = Buffer.byteLength(minified, "utf8");
const savings = (((originalSize - minifiedSize) / originalSize) * 100).toFixed(1);
console.log(
` 📊 ${entry.name}: ${originalSize}${minifiedSize} bytes (${savings}% smaller)`
);
await fs.promises.writeFile(destPath, minified, "utf8");
} else {
// Copy other files as-is
await fs.promises.copyFile(srcPath, destPath);
}
}
};
const main = async () => {
await syncVersion();
console.log("🚀 Starting extension build...");
// 1. Install frontend deps if needed
if (!fs.existsSync(path.join(frontendDir, "node_modules"))) {
console.log("📦 node_modules not found. Running npm install...");
run("npm install", frontendDir);
}
// 2. Build frontend
console.log("📦 Building frontend...");
run("npm run build", frontendDir);
// 3. Clean release folder
if (fs.existsSync(releaseDir)) {
console.log("🧹 Cleaning existing extension-release folder...");
await fs.promises.rm(releaseDir, { recursive: true, force: true });
}
await fs.promises.mkdir(releaseDir);
// 4. Copy and minify src files
console.log("📦 Copying and minifying src files...");
await copyAndMinifySrcFiles(srcDir, releaseDir);
// 5. Copy frontend dist files to release (merged at root)
console.log("📦 Copying frontend dist files to extension-release...");
await copyDir(distDir, releaseDir);
// 6. Copy icon directory to release (merged at root)
console.log("📦 Copying icon directory to extension-release...");
await copyDir(iconDir, path.join(releaseDir, "icons"));
console.log("✅ Build complete! extension-release ready.");
};
main().catch((e) => {
console.error("❌ Build failed:", e);
process.exit(1);
});

View File

@ -1,67 +0,0 @@
// 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 <html> or <head>
(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
},
"*"
);
});
}
});

File diff suppressed because one or more lines are too long

View File

@ -1,13 +0,0 @@
<!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-ydPQKJSy.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-UaipKa9p.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>

View File

@ -1,33 +1,30 @@
import js from '@eslint/js' import js from "@eslint/js";
import globals from 'globals' import globals from "globals";
import reactHooks from 'eslint-plugin-react-hooks' import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from 'eslint-plugin-react-refresh' import reactRefresh from "eslint-plugin-react-refresh";
export default [ export default [
{ ignores: ['dist'] }, { ignores: ["dist"] },
{ {
files: ['**/*.{js,jsx}'], files: ["**/*.{js,jsx}"],
languageOptions: { languageOptions: {
ecmaVersion: 2020, ecmaVersion: 2020,
globals: globals.browser, globals: globals.browser,
parserOptions: { parserOptions: {
ecmaVersion: 'latest', ecmaVersion: "latest",
ecmaFeatures: { jsx: true }, ecmaFeatures: { jsx: true },
sourceType: 'module', 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 }],
},
}, },
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 },
],
},
},
]

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,31 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "0.0.0", "version": "2.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.7", "@tailwindcss/vite": "^4.1.11",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router-dom": "^7.6.1", "react-router-dom": "^7.7.0",
"tailwindcss": "^4.1.7" "tailwindcss": "^4.1.11"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.25.0", "@eslint/js": "^9.31.0",
"@types/react": "^19.1.2", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.2", "@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.7.0",
"eslint": "^9.25.0", "@vitejs/plugin-react-swc": "^3.11.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint": "^9.31.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-hooks": "^5.2.0",
"globals": "^16.0.0", "eslint-plugin-react-refresh": "^0.4.20",
"vite": "^6.3.5" "globals": "^16.3.0",
} "vite": "^7.0.5"
}
} }

View File

@ -1,77 +1,72 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { import { HashRouter as Router, Routes, Route, Navigate } from "react-router-dom";
HashRouter as Router,
Routes,
Route,
Navigate,
} from "react-router-dom";
import TopNav from "./components/topnav"; import TopNav from "./components/topnav";
import SideNav from "./components/sidenav"; import SideNav from "./components/sidenav";
import Results from "./components/results"; import Results from "./components/results";
import Settings from "./components/settings"; import Settings from "./components/settings";
function App() { function App() {
const [isSideNavOpen, setIsSideNavOpen] = useState(false); const [isSideNavOpen, setIsSideNavOpen] = useState(false);
const [validConfig, setValidConfig] = useState(null); // null = loading const [validConfig, setValidConfig] = useState(null); // null = loading
useEffect(() => { useEffect(() => {
chrome.storage.local.get("valid_config", (result) => { chrome.storage.local.get("valid_config", (result) => {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
console.error("Error reading valid_config:", chrome.runtime.lastError); console.error("Error reading valid_config:", chrome.runtime.lastError);
setValidConfig(false); // fallback setValidConfig(false); // fallback
} else { } else {
setValidConfig(result.valid_config === true); setValidConfig(result.valid_config === true);
} }
}); });
}, []); }, []);
if (validConfig === null) {
return (
<div className="flex items-center justify-center h-screen bg-black text-white">
Loading...
</div>
);
}
if (validConfig === null) {
return ( return (
<div className="flex items-center justify-center h-screen bg-black text-white"> <Router>
Loading... <div className="min-w-full min-h-full w-full h-full flex flex-grow bg-black/95 flex-col relative">
</div> <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 overflow-x-hidden">
<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>
); );
}
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 overflow-x-hidden">
<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; export default App;

View File

@ -1,149 +1,250 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
function Results() { function Results() {
const [drmType, setDrmType] = useState(""); const [drmType, setDrmType] = useState("");
const [pssh, setPssh] = useState(""); const [pssh, setPssh] = useState("");
const [licenseUrl, setLicenseUrl] = useState(""); const [licenseUrl, setLicenseUrl] = useState("");
const [keys, setKeys] = useState([]); const [keys, setKeys] = useState([]);
const [manifestUrl, setManifestUrl] = useState("");
const [currentTabUrl, setCurrentTabUrl] = useState("");
useEffect(() => { useEffect(() => {
chrome.storage.local.get( chrome.storage.local.get(
[ [
"drmType", "drmType",
"latestPSSH", "latestPSSH",
"latestLicenseRequest", "latestLicenseRequest",
"latestKeys", "latestKeys",
"licenseURL", "licenseURL",
], "manifestURL",
(result) => { ],
if (result.drmType) setDrmType(result.drmType); (result) => {
if (result.latestPSSH) setPssh(result.latestPSSH); if (result.drmType) setDrmType(result.drmType || "");
if (result.licenseURL) setLicenseUrl(result.licenseURL); if (result.latestPSSH) setPssh(result.latestPSSH || "");
if (result.latestKeys) { if (result.licenseURL) setLicenseUrl(result.licenseURL || "");
try { if (result.manifestURL) setManifestUrl(result.manifestURL || "");
const parsed = Array.isArray(result.latestKeys) if (result.latestKeys) {
? result.latestKeys try {
: JSON.parse(result.latestKeys); const parsed = Array.isArray(result.latestKeys)
setKeys(parsed); ? result.latestKeys
} catch (e) { : JSON.parse(result.latestKeys);
console.error("Failed to parse keys:", e); setKeys(parsed);
setKeys([]); } 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.licenseURL) {
setLicenseUrl(changes.licenseURL.newValue);
}
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",
licenseURL: "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) { // Get current tab URL when component mounts
chrome.tabs.reload(activeTab.id, () => { chrome.windows.getAll({ populate: true, windowTypes: ["normal"] }, (windows) => {
if (chrome.runtime.lastError) { if (windows && windows.length > 0) {
console.error("Failed to reload tab:", chrome.runtime.lastError); const lastFocusedWindow = windows.find((w) => w.focused) || windows[0];
if (lastFocusedWindow) {
const activeTab = lastFocusedWindow.tabs.find(
(tab) => tab.active && tab.url && /^https?:\/\//.test(tab.url)
);
if (activeTab?.url) {
setCurrentTabUrl(activeTab.url);
}
}
} }
}); });
} else {
console.warn("No active tab found in the last focused normal window");
}
}
);
};
return ( const handleChange = (changes, area) => {
<div className="w-full grow flex h-full overflow-y-auto overflow-x-auto flex-col text-white p-4"> if (area === "local") {
<button if (changes.drmType) {
onClick={handleCapture} setDrmType(changes.drmType.newValue || "");
className="w-full h-10 bg-sky-500 rounded-md p-2 mt-2 text-white cursor-pointer hover:bg-sky-600" }
> if (changes.latestPSSH) {
Capture current tab setPssh(changes.latestPSSH.newValue || "");
</button> }
<p className="text-2xl mt-5">DRM Type</p> if (changes.licenseURL) {
<input setLicenseUrl(changes.licenseURL.newValue || "");
type="text" }
value={drmType} if (changes.manifestURL) {
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white" setManifestUrl(changes.manifestURL.newValue || "");
placeholder="None" }
disabled if (changes.latestKeys) {
/> setKeys(changes.latestKeys.newValue || []);
<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" chrome.storage.onChanged.addListener(handleChange);
placeholder="None" return () => chrome.storage.onChanged.removeListener(handleChange);
disabled }, []);
/>
<p className="text-2xl mt-5">License URL</p> const handleCapture = () => {
<input // Reset stored values
type="text" chrome.storage.local.set({
value={licenseUrl} drmType: "",
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white" latestPSSH: "",
placeholder="None" licenseURL: "",
disabled manifestURL: "",
/> latestKeys: [],
<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) && // Get all normal windows to exclude your popup
keys.filter((k) => k.type !== "SIGNING").length > 0 ? ( chrome.windows.getAll({ populate: true, windowTypes: ["normal"] }, (windows) => {
keys if (!windows || windows.length === 0) {
.filter((k) => k.type !== "SIGNING") console.warn("No normal Chrome windows found");
.map((k) => `${k.key_id || k.keyId}:${k.key}`) return;
.join("\n") }
) : (
<span className="text-gray-400">None</span> // Find the last focused normal window
)} const lastFocusedWindow = windows.find((w) => w.focused) || windows[0];
</div>
</div> 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");
}
});
};
// Check if current tab is YouTube
const isYouTube = () => {
return currentTabUrl.includes("youtube.com") || currentTabUrl.includes("youtu.be");
};
// Get manifest URL display value
const getManifestDisplayValue = () => {
if (manifestUrl) {
return manifestUrl;
}
if (isYouTube()) {
return "[Use yt-dlp to download video]";
}
return "";
};
// Get manifest URL placeholder
const getManifestPlaceholder = () => {
if (isYouTube() && !manifestUrl) {
return "[Use yt-dlp to download video]";
}
return "[Not available]";
};
// Export to JSON file
const hasData = () => {
return Array.isArray(keys) && keys.filter((k) => k.type !== "SIGNING").length > 0;
};
const handleExportJSON = () => {
const exportData = {
drmType: drmType || null,
manifestUrl: manifestUrl || null,
pssh: pssh || null,
licenseUrl: licenseUrl || null,
keys:
Array.isArray(keys) && keys.length > 0
? keys
.filter((k) => k.type !== "SIGNING")
.map((k) => `${k.key_id || k.keyId}:${k.key}`)
: null,
exportedAt: new Date().toISOString(),
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `drm-data-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
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 font-bold 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 font-mono"
placeholder="[Not available]"
disabled
/>
<p className="text-2xl mt-5">Manifest URL</p>
<input
type="text"
value={getManifestDisplayValue()}
className={`w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 font-mono ${
isYouTube() && !manifestUrl ? "text-yellow-400" : "text-white"
}`}
placeholder={getManifestPlaceholder()}
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 font-mono"
placeholder="[Not available]"
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 font-mono"
placeholder="[Not available]"
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 font-mono">
{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">[Not available]</span>
)}
</div>
{hasData() && (
<button
onClick={handleExportJSON}
className="w-full h-10 bg-green-500 rounded-md p-2 mt-5 text-white cursor-pointer font-bold hover:bg-green-600"
>
Export as JSON
</button>
)}
</div>
);
} }
export default Results; export default Results;

View File

@ -2,149 +2,143 @@ import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
function Settings({ onConfigSaved }) { function Settings({ onConfigSaved }) {
const [instanceUrl, setInstanceUrl] = useState(""); const [instanceUrl, setInstanceUrl] = useState("");
const [storedUrl, setStoredUrl] = useState(null); const [storedUrl, setStoredUrl] = useState(null);
const [message, setMessage] = useState(null); const [message, setMessage] = useState(null);
const [messageType, setMessageType] = useState(null); const [messageType, setMessageType] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
chrome.storage.local.get("cdrm_instance", (result) => { 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) { if (chrome.runtime.lastError) {
console.error( console.error("Error fetching CDRM instance:", chrome.runtime.lastError);
"Error saving to chrome.storage:", } else if (result.cdrm_instance) {
chrome.runtime.lastError setStoredUrl(result.cdrm_instance);
);
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 ( const handleSave = async () => {
<div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col p-4"> const trimmedUrl = instanceUrl.trim().replace(/\/+$/, "");
<input if (!trimmedUrl) {
type="text" setMessage("Please enter a valid URL.");
value={instanceUrl} setMessageType("error");
onChange={(e) => setInstanceUrl(e.target.value)} return;
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 && ( const endpoint = trimmedUrl + "/api/extension";
<p setLoading(true);
className={`mt-2 text-sm text-center ${ setMessage(null);
messageType === "success" ? "text-green-400" : "text-red-400"
}`} try {
> const response = await fetch(endpoint, {
{message} method: "POST",
</p> headers: {
)} "Content-Type": "application/json",
</div> },
); });
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">
{storedUrl && (
<p className="text-gray-300 mb-2">
Current instance: <span className="text-white font-semibold">{storedUrl}</span>
</p>
)}
<p className="mt-3 text-white">New instance URL:</p>
<input
type="text"
value={instanceUrl}
onChange={(e) => setInstanceUrl(e.target.value)}
placeholder="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 font-mono"
/>
<button
onClick={handleSave}
disabled={loading}
className={`mt-4 p-2 font-bold ${
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; export default Settings;

View File

@ -4,48 +4,45 @@ import settingsIcon from "../assets/settings.svg";
import closeIcon from "../assets/close.svg"; import closeIcon from "../assets/close.svg";
function SideNav({ onClose }) { function SideNav({ onClose }) {
return ( return (
<div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col bg-black"> <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"> <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 <button onClick={onClose} className="h-full ml-auto p-3 hover:cursor-pointer">
onClick={onClose} <img src={closeIcon} alt="Close" className="h-full" />
className="h-full ml-auto p-3 hover:cursor-pointer" </button>
> </div>
<img src={closeIcon} alt="Close" className="h-full" />
</button>
</div>
<div className="w-full h-16 flex items-center justify-center mt-2"> <div className="w-full h-16 flex items-center justify-center mt-2">
<NavLink <NavLink
to="/results" to="/results"
onClick={onClose} 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" 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 <img
src={homeIcon} src={homeIcon}
alt="Home" alt="Home"
className="h-full w-16 p-2 flex items-center cursor-pointer" className="h-full w-16 p-2 flex items-center cursor-pointer"
/> />
Home Home
</NavLink> </NavLink>
</div> </div>
<div className="w-full h-16 flex items-center justify-center mt-2"> <div className="w-full h-16 flex items-center justify-center mt-2">
<NavLink <NavLink
to="/settings" to="/settings"
onClick={onClose} 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" 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 <img
src={settingsIcon} src={settingsIcon}
alt="Settings" alt="Settings"
className="h-full w-16 p-2 flex items-center cursor-pointer" className="h-full w-16 p-2 flex items-center cursor-pointer"
/> />
Settings Settings
</NavLink> </NavLink>
</div> </div>
</div> </div>
); );
} }
export default SideNav; export default SideNav;

View File

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

View File

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

View File

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

View File

@ -1,9 +1,21 @@
import { defineConfig } from 'vite' import tailwindcss from "@tailwindcss/vite";
import react from '@vitejs/plugin-react' import react from "@vitejs/plugin-react-swc";
import tailwindcss from '@tailwindcss/vite' import { readFileSync } from "fs";
import { defineConfig } from "vite";
const packageJson = JSON.parse(readFileSync("./package.json", "utf8"));
const replaceVersionPlugin = () => {
return {
name: "replace-version",
transformIndexHtml(html) {
return html.replace("%APPVERSION%", packageJson.version);
},
};
};
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
base: './', base: "./",
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss(), replaceVersionPlugin()],
}) });

813
inject.js
View File

@ -1,813 +0,0 @@
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;
}
});
// 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 encoding="base64encoded">([^<]+)<\/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);
};
})();

View File

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

89
mv2/background.js Normal file
View File

@ -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.");
}
});
}
});

96
mv2/content.js Normal file
View File

@ -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 <html> or <head>
(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,
});
}
});

1143
mv2/inject.js Normal file

File diff suppressed because it is too large Load Diff

41
mv2/manifest.json Normal file
View File

@ -0,0 +1,41 @@
{
"manifest_version": 2,
"name": "CDRM Extension",
"version": "2.1.0",
"description": "Decrypt DRM protected content",
"permissions": [
"webRequest",
"webRequestBlocking",
"<all_urls>",
"activeTab",
"storage",
"tabs",
"contextMenus"
],
"background": {
"scripts": ["background.js"],
"persistent": true
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"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"
}
}

136
package-lock.json generated Normal file
View File

@ -0,0 +1,136 @@
{
"name": "cdrm-extension",
"version": "2.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cdrm-extension",
"version": "2.1.0",
"license": "ISC",
"devDependencies": {
"terser": "^5.43.1"
},
"engines": {
"node": ">=21.0.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.12",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.10",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz",
"integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.29",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"license": "MIT"
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/terser": {
"version": "5.43.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.14.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
}
}
}

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "cdrm-extension",
"version": "2.1.0",
"description": "",
"main": "background.js",
"scripts": {
"buildext": "node buildext.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://cdm-project.com/tpd94/CDRM-Extension.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"engines": {
"node": ">=21.0.0"
},
"devDependencies": {
"terser": "^5.43.1"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,13 +0,0 @@
<!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-ydPQKJSy.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-UaipKa9p.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>

88
src/background.js Normal file
View File

@ -0,0 +1,88 @@
chrome.action.onClicked.addListener(() => {
chrome.windows.create({
url: chrome.runtime.getURL("index.html"),
type: "popup",
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("[CDRM-Extension] DRM Type:", data);
chrome.storage.local.set({ drmType: data });
break;
case "PSSH_DATA":
console.log("[CDRM-Extension] Storing PSSH:", data);
chrome.storage.local.set({ latestPSSH: data });
break;
case "KEYS_DATA":
console.log("[CDRM-Extension] Storing Decryption Keys:", data);
chrome.storage.local.set({ latestKeys: data });
break;
case "LICENSE_URL":
console.log("[CDRM-Extension] Storing License URL " + data);
chrome.storage.local.set({ licenseURL: data });
break;
case "MANIFEST_URL":
console.log("[CDRM-Extension] Storing Manifest URL:", data);
chrome.storage.local.set({ manifestURL: data });
break;
default:
console.warn("[CDRM-Extension] 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("[CDRM-Extension] Error setting valid_config:", chrome.runtime.lastError);
} else {
console.log("[CDRM-Extension] valid_config set to false on first install.");
}
});
chrome.storage.local.set({ injection_type: "LICENSE" }, () => {
if (chrome.runtime.lastError) {
console.error("[CDRM-Extension] Error setting Injection Type:", chrome.runtime.lastError);
} else {
console.log("[CDRM-Extension] Injection type set to LICENSE on first install.");
}
});
chrome.storage.local.set({ drm_override: "DISABLED" }, () => {
if (chrome.runtime.lastError) {
console.error("[CDRM-Extension] Error setting DRM Override type:", chrome.runtime.lastError);
} else {
console.log("[CDRM-Extension] DRM Override type set to DISABLED on first install.");
}
});
chrome.storage.local.set({ cdrm_instance: null }, () => {
if (chrome.runtime.lastError) {
console.error("[CDRM-Extension] Error setting CDRM instance:", chrome.runtime.lastError);
} else {
console.log("[CDRM-Extension] CDRM instance set to null.");
}
});
chrome.storage.local.set({ cdrm_api_key: null }, () => {
if (chrome.runtime.lastError) {
console.error("[CDRM-Extension] Error setting CDRM API Key:", chrome.runtime.lastError);
} else {
console.log("[CDRM-Extension] CDRM API Key set.");
}
});
}
});

96
src/content.js Normal file
View File

@ -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 <html> or <head>
(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("[CDRM-Extension] ✅ [content.js] Unique manifest URL:", url);
chrome.runtime.sendMessage({
type: "MANIFEST_URL",
data: url,
});
}
if (event.data?.type === "__MANIFEST_HEADERS__") {
const { url, headers } = event.data;
console.log("[CDRM-Extension] [content.js] Manifest headers:", url, headers);
chrome.runtime.sendMessage({
type: "MANIFEST_HEADERS",
url,
headers,
});
}
});

925
src/inject.js Normal file
View File

@ -0,0 +1,925 @@
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;
const DRM_SIGNATURES = {
WIDEVINE: "CAES",
PLAYREADY: "PD94",
SERVICE_CERT: "CAUS",
WIDEVINE_INIT: "CAQ=",
};
const EXTENSION_PREFIX = "[CDRM EXTENSION]";
const PREFIX_COLOR = "black";
const PREFIX_BACKGROUND_COLOR = "yellow";
const logWithPrefix = (...args) => {
const style = `color: ${PREFIX_COLOR}; background: ${PREFIX_BACKGROUND_COLOR}; font-weight: bold; padding: 2px 4px; border-radius: 2px;`;
if (typeof args[0] === "string") {
// If the first arg is a string, prepend the prefix
console.log(`%c${EXTENSION_PREFIX}%c ${args[0]}`, style, "", ...args.slice(1));
} else {
// If not, just log the prefix and the rest
console.log(`%c${EXTENSION_PREFIX}`, style, ...args);
}
};
function resetDRMState() {
logWithPrefix("Resetting DRM state for new manifest...");
// Reset DRM detection state
originalChallenge = null;
serviceCertFound = false;
drmType = "NONE";
psshFound = false;
foundWidevinePssh = null;
foundPlayreadyPssh = null;
drmDecided = null;
// Reset CDM and session state
if (remoteCDM) {
try {
// Try to close the existing session if it exists
if (remoteCDM.session_id) {
remoteCDM.closeSession();
}
} catch (e) {
// Ignore errors when closing session
logWithPrefix("Error closing previous CDM session:", e.message);
}
remoteCDM = null;
}
// Reset interceptor state
generateRequestCalled = false;
remoteListenerMounted = false;
injectionSuccess = false;
foundChallengeInBody = false;
licenseResponseCounter = 0;
keysRetrieved = false;
// Post reset messages to clear UI state
window.postMessage({ type: "__DRM_TYPE__", data: "" }, "*");
window.postMessage({ type: "__PSSH_DATA__", data: "" }, "*");
window.postMessage({ type: "__KEYS_DATA__", data: "" }, "*");
window.postMessage({ type: "__LICENSE_URL__", data: "" }, "*");
}
window.postMessage({ type: "__GET_DRM_OVERRIDE__" }, "*");
window.postMessage({ type: "__GET_INJECTION_TYPE__" }, "*");
window.postMessage({ type: "__GET_CDM_DEVICES__" }, "*");
function createMessageHandler(handlers) {
window.addEventListener("message", function (event) {
if (event.source !== window) return;
const handler = handlers[event.data.type];
if (handler) {
handler(event.data);
}
});
}
createMessageHandler({
__DRM_OVERRIDE__: (data) => {
drmOverride = data.drmOverride || "DISABLED";
logWithPrefix("DRM Override set to:", drmOverride);
},
__INJECTION_TYPE__: (data) => {
interceptType = data.injectionType || "DISABLED";
logWithPrefix("Injection type set to:", interceptType);
},
__CDM_DEVICES__: (data) => {
const { widevine_device, playready_device } = data;
logWithPrefix("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
}
function headersToFlags(headersObj) {
return Object.entries(headersObj)
.map(
([key, val]) =>
'--add-headers "' +
safeHeaderShellEscape(key) +
": " +
safeHeaderShellEscape(val) +
'"'
)
.join(" ");
}
function handleManifestDetection(url, headersObj, contentType, source) {
// Reset DRM state when new manifest is detected
resetDRMState();
window.postMessage({ type: "__MANIFEST_URL__", data: url }, "*");
logWithPrefix(`[Manifest][${source}]`, url, contentType);
const headerFlags = headersToFlags(headersObj);
window.postMessage(
{
type: "__MANIFEST_HEADERS__",
url,
headers: headerFlags,
},
"*"
);
}
// Intercep network to find manifest
function injectManifestInterceptor() {
// Execute the interceptor code directly instead of injecting a script
(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("<MPD") || sample.includes("<AdaptationSet");
const isSmoothKeyword = sample.includes("<SmoothStreamingMedia");
const isJsonManifest = sample.includes('"playlist"') && sample.includes('"segments"');
return (
isHLSMime ||
isDASHMime ||
isSmoothMime ||
isHLSKeyword ||
isDASHKeyword ||
isSmoothKeyword ||
isJsonManifest
);
}
const originalFetch = window.fetch;
window.fetch = async function (input, init) {
const response = await originalFetch.apply(this, arguments);
try {
const clone = response.clone();
const contentType = clone.headers.get("content-type") || "";
const text = await clone.text();
const url = typeof input === "string" ? input : input.url;
if (isProbablyManifest(text, contentType)) {
const headersObj = {};
clone.headers.forEach((value, key) => {
headersObj[key] = value;
});
handleManifestDetection(url, headersObj, contentType, "fetch");
}
} 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)) {
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];
}
});
handleManifestDetection(this.__url, xhrHeaders, contentType, "xhr");
}
} catch (e) {}
});
return originalXHRSend.apply(this, arguments);
};
})();
}
injectManifestInterceptor();
class RemoteCDMBase {
constructor({ host, secret, device_name, security_level }) {
this.host = host;
this.secret = secret;
this.device_name = device_name;
this.security_level = security_level;
this.session_id = null;
this.challenge = null;
this.keys = null;
}
openSession(path) {
const url = `${this.host}${path}/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;
logWithPrefix("Session opened:", this.session_id);
} else {
console.error("Failed to open session:", jsonData.message);
throw new Error("Failed to open session");
}
}
getChallenge(path, body) {
const url = `${this.host}${path}/get_license_challenge`;
const xhr = new XMLHttpRequest();
xhr.open("POST", url, false);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.data?.challenge) {
this.challenge = btoa(jsonData.data.challenge);
logWithPrefix("Challenge received:", this.challenge);
} else if (jsonData.data?.challenge_b64) {
this.challenge = jsonData.data.challenge_b64;
logWithPrefix("Challenge received:", this.challenge);
} else {
console.error("Failed to get challenge:", jsonData.message);
throw new Error("Failed to get challenge");
}
}
parseLicense(path, body) {
const url = `${this.host}${path}/parse_license`;
const xhr = new XMLHttpRequest();
xhr.open("POST", url, false);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.status === 200 || jsonData.message?.includes("parsed and loaded")) {
logWithPrefix("License response parsed successfully");
return true;
} else {
console.error("Failed to parse license response:", jsonData.message);
throw new Error("Failed to parse license response");
}
}
getKeys(path, body, extraPath = "") {
const url = `${this.host}${path}/get_keys${extraPath}`;
const xhr = new XMLHttpRequest();
xhr.open("POST", url, false);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.data?.keys) {
this.keys = jsonData.data.keys;
logWithPrefix("Keys received:", this.keys);
} else {
console.error("Failed to get keys:", jsonData.message);
throw new Error("Failed to get keys");
}
}
closeSession(path) {
const url = `${this.host}${path}/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) {
logWithPrefix("Session closed successfully");
} else {
console.error("Failed to close session:", jsonData.message);
throw new Error("Failed to close session");
}
}
}
// PlayReady Remote CDM Class
class remotePlayReadyCDM extends RemoteCDMBase {
constructor(security_level, host, secret, device_name) {
super({ host, secret, device_name, security_level });
}
openSession() {
super.openSession(`/remotecdm/playready/${this.device_name}`);
}
getChallenge(init_data) {
super.getChallenge(`/remotecdm/playready/${this.device_name}`, {
session_id: this.session_id,
init_data: init_data,
});
}
parseLicense(license_message) {
return super.parseLicense(`/remotecdm/playready/${this.device_name}`, {
session_id: this.session_id,
license_message: license_message,
});
}
getKeys() {
super.getKeys(`/remotecdm/playready/${this.device_name}`, {
session_id: this.session_id,
});
}
closeSession() {
super.closeSession(`/remotecdm/playready/${this.device_name}`);
}
}
// Widevine Remote CDM Class
class remoteWidevineCDM extends RemoteCDMBase {
constructor(device_type, system_id, security_level, host, secret, device_name) {
super({ host, secret, device_name, security_level });
this.device_type = device_type;
this.system_id = system_id;
}
openSession() {
super.openSession(`/remotecdm/widevine/${this.device_name}`);
}
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) {
logWithPrefix("Service certificate set successfully");
} else {
console.error("Failed to set service certificate:", jsonData.message);
throw new Error("Failed to set service certificate");
}
}
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;
logWithPrefix("Widevine challenge received:", this.challenge);
} else {
console.error("Failed to get Widevine challenge:", jsonData.message);
throw new Error("Failed to get Widevine challenge");
}
}
parseLicense(license_message) {
return super.parseLicense(`/remotecdm/widevine/${this.device_name}`, {
session_id: this.session_id,
license_message: license_message,
});
}
getKeys() {
super.getKeys(
`/remotecdm/widevine/${this.device_name}`,
{
session_id: this.session_id,
},
"/ALL"
);
}
closeSession() {
super.closeSession(`/remotecdm/widevine/${this.device_name}`);
}
}
// 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 = DRM_SIGNATURES.WIDEVINE) {
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(DRM_SIGNATURES.WIDEVINE) || obj.startsWith(DRM_SIGNATURES.PLAYREADY)
? 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);
}
function bufferToBase64(buffer) {
const uint8 = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
return window.btoa(String.fromCharCode(...uint8));
}
// DRM type detection
function isWidevine(base64str) {
return base64str.startsWith(DRM_SIGNATURES.WIDEVINE);
}
function isPlayReady(base64str) {
return base64str.startsWith(DRM_SIGNATURES.PLAYREADY);
}
function isServiceCertificate(base64str) {
return base64str.startsWith(DRM_SIGNATURES.SERVICE_CERT);
}
function postDRMTypeAndPssh(type, pssh) {
window.postMessage({ type: "__DRM_TYPE__", data: type }, "*");
window.postMessage({ type: "__PSSH_DATA__", data: pssh }, "*");
}
function createAndOpenRemoteCDM(type, deviceInfo, pssh) {
let cdm;
if (type === "Widevine") {
const { device_type, system_id, security_level, host, secret, device_name } = deviceInfo;
cdm = new remoteWidevineCDM(
device_type,
system_id,
security_level,
host,
secret,
device_name
);
cdm.openSession();
cdm.getChallenge(pssh);
} else if (type === "PlayReady") {
const { security_level, host, secret, device_name } = deviceInfo;
cdm = new remotePlayReadyCDM(security_level, host, secret, device_name);
cdm.openSession();
cdm.getChallenge(pssh);
}
return cdm;
}
function ensureRemoteCDM(type, deviceInfo, pssh) {
if (!remoteCDM) {
remoteCDM = createAndOpenRemoteCDM(type, deviceInfo, pssh);
}
}
function detectAndStorePssh(initData) {
const detections = [
{
type: "PlayReady",
getter: getPlayReadyPssh,
store: (pssh) => (foundPlayreadyPssh = pssh),
},
{ type: "Widevine", getter: getWidevinePssh, store: (pssh) => (foundWidevinePssh = pssh) },
];
detections.forEach(({ type, getter, store }) => {
const pssh = getter(initData);
if (pssh) {
logWithPrefix(`[DRM Detected] ${type}`);
store(pssh);
logWithPrefix(`[${type} PSSH found] ${pssh}`);
}
});
}
// Challenge generator interceptor
const originalGenerateRequest = MediaKeySession.prototype.generateRequest;
MediaKeySession.prototype.generateRequest = function (initDataType, initData) {
const session = this;
detectAndStorePssh(initData);
// Challenge message interceptor
if (!remoteListenerMounted) {
remoteListenerMounted = true;
session.addEventListener("message", function messageInterceptor(event) {
event.stopImmediatePropagation();
const base64challenge = bufferToBase64(event.message);
if (
base64challenge === DRM_SIGNATURES.WIDEVINE_INIT &&
interceptType !== "DISABLED" &&
!serviceCertFound
) {
remoteCDM = createAndOpenRemoteCDM(
"Widevine",
widevineDeviceInfo,
foundWidevinePssh
);
}
if (
!injectionSuccess &&
base64challenge !== DRM_SIGNATURES.WIDEVINE_INIT &&
interceptType !== "DISABLED"
) {
if (interceptType === "EME") {
injectionSuccess = true;
}
if (!originalChallenge) {
originalChallenge = base64challenge;
}
if (originalChallenge.startsWith(DRM_SIGNATURES.WIDEVINE)) {
postDRMTypeAndPssh("Widevine", foundWidevinePssh);
if (interceptType === "EME") {
ensureRemoteCDM("Widevine", widevineDeviceInfo, foundWidevinePssh);
}
}
if (!originalChallenge.startsWith(DRM_SIGNATURES.WIDEVINE)) {
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) {
postDRMTypeAndPssh("PlayReady", foundPlayreadyPssh);
originalChallenge = match[1];
if (interceptType === "EME") {
ensureRemoteCDM("PlayReady", playreadyDeviceInfo, 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,
});
logWithPrefix("Intercepted EME Challenge and injected custom one.");
session.dispatchEvent(syntheticEvent);
}
}
});
logWithPrefix("Message interceptor mounted.");
}
return originalGenerateRequest.call(session, initDataType, initData);
};
// Message update interceptors
const originalUpdate = MediaKeySession.prototype.update;
MediaKeySession.prototype.update = function (response) {
const base64Response = bufferToBase64(response);
if (
base64Response.startsWith(DRM_SIGNATURES.SERVICE_CERT) &&
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(DRM_SIGNATURES.SERVICE_CERT) &&
(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) {
logWithPrefix("[CLEARKEY] ", clearKeys);
const drmType = {
type: "__DRM_TYPE__",
data: "ClearKey",
};
window.postMessage(drmType, "*");
const keysData = {
type: "__KEYS_DATA__",
data: clearKeys,
};
window.postMessage(keysData, "*");
}
})
.catch((e) => {
logWithPrefix("[CLEARKEY] Not found");
});
}
return updatePromise;
};
// Helpers
function detectDRMChallenge(body) {
// Handles ArrayBuffer, Uint8Array, string, and JSON string
// Returns: { type: "Widevine"|"PlayReady"|null, base64: string|null, bodyType: "buffer"|"string"|"json"|null }
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(DRM_SIGNATURES.WIDEVINE)) {
return { type: "Widevine", base64: base64Body, bodyType: "buffer" };
}
if (base64Body.startsWith(DRM_SIGNATURES.PLAYREADY)) {
return { type: "PlayReady", base64: base64Body, bodyType: "buffer" };
}
} else if (typeof body === "string" && !isJson(body)) {
const base64EncodedBody = btoa(body);
if (base64EncodedBody.startsWith(DRM_SIGNATURES.WIDEVINE)) {
return { type: "Widevine", base64: base64EncodedBody, bodyType: "string" };
}
if (base64EncodedBody.startsWith(DRM_SIGNATURES.PLAYREADY)) {
return { type: "PlayReady", base64: base64EncodedBody, bodyType: "string" };
}
} else if (typeof body === "string" && isJson(body)) {
const jsonBody = JSON.parse(body);
if (jsonContainsValue(jsonBody, DRM_SIGNATURES.WIDEVINE)) {
return { type: "Widevine", base64: null, bodyType: "json" };
}
if (jsonContainsValue(jsonBody, DRM_SIGNATURES.PLAYREADY)) {
return { type: "PlayReady", base64: null, bodyType: "json" };
}
}
return { type: null, base64: null, bodyType: null };
}
function handleLicenseMode({
drmInfo,
body,
setBody, // function to set the new body (for fetch: (b) => config.body = b, for XHR: (b) => originalSend.call(this, b))
urlOrResource,
getWidevinePssh,
getPlayreadyPssh,
widevineDeviceInfo,
playreadyDeviceInfo,
}) {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: urlOrResource }, "*");
// Create remoteCDM if needed
if (!remoteCDM) {
if (drmInfo.type === "Widevine") {
remoteCDM = createAndOpenRemoteCDM("Widevine", widevineDeviceInfo, getWidevinePssh());
}
if (drmInfo.type === "PlayReady") {
remoteCDM = createAndOpenRemoteCDM(
"PlayReady",
playreadyDeviceInfo,
getPlayreadyPssh()
);
}
}
if (remoteCDM && remoteCDM.challenge === null) {
remoteCDM.getChallenge(getWidevinePssh());
}
// Inject the new challenge into the request body
if (drmInfo.bodyType === "json") {
const jsonBody = JSON.parse(body);
const injectedBody = jsonReplaceValue(jsonBody, remoteCDM.challenge);
setBody(JSON.stringify(injectedBody));
} else if (drmInfo.bodyType === "buffer") {
setBody(base64ToUint8Array(remoteCDM.challenge));
} else {
setBody(atob(remoteCDM.challenge));
}
}
function handleDRMInterception(drmInfo, body, url, setBodyCallback, continueRequestCallback) {
// EME mode: block the request if a DRM challenge is detected
if (
drmInfo.type &&
(!remoteCDM || remoteCDM.challenge === null || drmInfo.base64 !== remoteCDM.challenge) &&
interceptType === "EME"
) {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: url }, "*");
// Block the request
return { shouldBlock: true };
}
// LICENSE mode: replace the challenge in the request
if (drmInfo.type && interceptType === "LICENSE" && !foundChallengeInBody) {
handleLicenseMode({
drmInfo,
body,
setBody: setBodyCallback,
urlOrResource: url,
getWidevinePssh: () => foundWidevinePssh,
getPlayreadyPssh: () => foundPlayreadyPssh,
widevineDeviceInfo,
playreadyDeviceInfo,
});
return { shouldIntercept: true, result: continueRequestCallback() };
}
return { shouldContinue: true };
}
// fetch POST interceptor
(function () {
const originalFetch = window.fetch;
window.fetch = async function (resource, config = {}) {
const method = (config.method || "GET").toUpperCase();
if (method === "POST" && config.body) {
logWithPrefix("[FETCH] Intercepting POST request to:", resource);
const drmInfo = detectDRMChallenge(config.body);
const result = handleDRMInterception(
drmInfo,
config.body,
resource,
(b) => {
config.body = b;
},
() => originalFetch(resource, config)
);
if (result.shouldBlock) return;
if (result.shouldIntercept) return result.result;
}
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" && body) {
logWithPrefix("[XHR] Intercepting POST request to:", this._url);
const drmInfo = detectDRMChallenge(body);
const result = handleDRMInterception(
drmInfo,
body,
this._url,
(b) => originalSend.call(this, b),
() => {} // XHR doesn't need continuation callback
);
if (result.shouldBlock) return;
if (result.shouldIntercept) return result.result;
}
return originalSend.apply(this, arguments);
};
})();

37
src/manifest.json Normal file
View File

@ -0,0 +1,37 @@
{
"manifest_version": 3,
"name": "CDRM Extension",
"version": "2.1.0",
"description": "Decrypt DRM protected content",
"permissions": ["storage", "activeTab", "contextMenus"],
"host_permissions": ["<all_urls>"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start",
"all_frames": true
}
],
"web_accessible_resources": [
{
"resources": ["inject.js"],
"matches": ["<all_urls>"]
}
],
"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"
}
}

66
syncVersion.js Normal file
View File

@ -0,0 +1,66 @@
import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const updateVersionWithRegex = async (filePath, newVersion) => {
try {
const content = await fs.readFile(filePath, "utf-8");
// Regex to match "version": "any.version.number"
const versionRegex = /("version"\s*:\s*")([^"]+)(")/;
if (!versionRegex.test(content)) {
console.warn(`⚠️ No version field found in ${filePath}`);
return false;
}
const updatedContent = content.replace(versionRegex, `$1${newVersion}$3`);
if (content !== updatedContent) {
await fs.writeFile(filePath, updatedContent);
return true;
}
return false;
} catch (err) {
console.error(`❌ Failed to update ${filePath}: ${err.message}`);
return false;
}
};
const syncVersion = async () => {
const rootPkgPath = path.join(__dirname, "package.json");
const frontendPkgPath = path.join(__dirname, "frontend", "package.json");
const manifestPath = path.join(__dirname, "src", "manifest.json");
// Read root package.json version
const rootPkgRaw = await fs.readFile(rootPkgPath, "utf-8");
const rootPkg = JSON.parse(rootPkgRaw);
const version = rootPkg.version;
if (!version) {
console.warn("⚠️ No version field found in root package.json, skipping sync.");
return;
}
// Update frontend/package.json using regex
const frontendUpdated = await updateVersionWithRegex(frontendPkgPath, version);
if (frontendUpdated) {
console.log(`🔄 Updated frontend/package.json version to ${version}`);
} else {
console.log(" frontend/package.json not found or no changes needed.");
}
// Update src/manifest.json using regex
const manifestUpdated = await updateVersionWithRegex(manifestPath, version);
if (manifestUpdated) {
console.log(`🔄 Updated src/manifest.json version to ${version}`);
} else {
console.log(" src/manifest.json not found or no changes needed.");
}
};
export default syncVersion;