Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

46 changed files with 5262 additions and 7609 deletions

30
.gitignore vendored
View File

@ -1,30 +0,0 @@
# 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

View File

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

View File

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

View File

@ -1,52 +0,0 @@
# 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.

94
background.js Normal file
View File

@ -0,0 +1,94 @@
// Open popout window when the extension icon is clicked
chrome.browserAction.onClicked.addListener(() => {
chrome.windows.create({
url: chrome.runtime.getURL("react/index.html"),
type: "popup", // opens as a floating window
width: 800,
height: 600
});
});
// Listen for messages and store data in chrome.storage.local
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const { type, data } = message;
switch (type) {
case "INTERCEPTED_POST":
console.log("Storing POST Request", data);
chrome.storage.local.set({ latestLicenseRequest: data });
break;
case "PSSH_DATA":
console.log("Storing PSSH:", data);
chrome.storage.local.set({ latestPSSH: data });
break;
case "LICENSE_DATA":
console.log("Storing License Response:", data);
chrome.storage.local.set({ latestLicenseResponse: data });
break;
case "CERTIFICATE_DATA":
console.log("Storing Service Certificate:", data);
chrome.storage.local.set({ latestServiceCertificate: data });
break;
case "KEYS_DATA":
console.log("Storing Decryption Keys:", data);
chrome.storage.local.set({ latestKeys: data });
break;
case "DRM_TYPE":
console.log("DRM Type:", data);
chrome.storage.local.set({ drmType: data });
break;
default:
console.warn("Unknown message type received:", type);
}
});
// Set initial config and injection type on install
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === "install") {
chrome.storage.local.set({ valid_config: false }, () => {
if (chrome.runtime.lastError) {
console.error("Error setting valid_config:", chrome.runtime.lastError);
} else {
console.log("valid_config set to false on first install.");
}
});
chrome.storage.local.set({ injection_type: "LICENSE" }, () => {
if (chrome.runtime.lastError) {
console.error("Error setting Injection Type:", chrome.runtime.lastError);
} else {
console.log("Injection type set to LICENSE on first install.");
}
});
chrome.storage.local.set({ 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.");
}
});
}
});

View File

@ -1,119 +0,0 @@
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);
});

76
content.js Normal file
View File

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

View File

@ -1,9 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"useTabs": false,
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"]
}

52
frontend/dist/assets/index-BFZJq4X0.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

13
frontend/dist/index.html vendored Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CDRM Decryption Extension</title>
<script type="module" crossorigin src="./assets/index-CN3ssfBX.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,30 +1,33 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ["dist"] },
{ ignores: ['dist'] },
{
files: ["**/*.{js,jsx}"],
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: "latest",
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: "module",
sourceType: 'module',
},
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
'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 }],
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
];
]

View File

@ -2,8 +2,8 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>CDRM Decryption Extension v%APPVERSION%</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CDRM Decryption Extension</title>
</head>
<body class="min-w-full min-h-full w-full h-full">
<div class="min-w-full min-h-full w-full h-full" id="root"></div>

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "3.0",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
@ -10,26 +10,21 @@
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"@tailwindcss/vite": "^4.1.7",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.7.0",
"sonner": "^2.0.6",
"tailwindcss": "^4.1.11"
"react-router-dom": "^7.6.1",
"tailwindcss": "^4.1.7"
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.7.0",
"@vitejs/plugin-react-swc": "^3.11.0",
"daisyui": "^5.0.46",
"eslint": "^9.31.0",
"@eslint/js": "^9.25.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"vite": "^7.0.5"
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"vite": "^6.3.5"
}
}

View File

@ -1,41 +1,33 @@
import { useEffect, useState } from "react";
import { Navigate, Route, HashRouter as Router, Routes } from "react-router-dom";
import About from "./components/about";
import Container from "./components/container";
import { useState, useEffect } from "react";
import {
HashRouter as Router,
Routes,
Route,
Navigate,
} from "react-router-dom";
import TopNav from "./components/topnav";
import SideNav from "./components/sidenav";
import Results from "./components/results";
import Settings from "./components/settings";
import TabNavigation from "./components/tabnavigation";
const App = () => {
const [validConfig, setValidConfig] = useState(null);
function App() {
const [isSideNavOpen, setIsSideNavOpen] = useState(false);
const [validConfig, setValidConfig] = useState(null); // null = loading
useEffect(() => {
// Fix: Access chrome API properly for browser extensions
if (typeof chrome !== "undefined" && chrome.storage) {
chrome.storage.local.get("valid_config", (result) => {
if (chrome.runtime.lastError) {
console.error("Error reading valid_config:", chrome.runtime.lastError);
setValidConfig(false);
setValidConfig(false); // fallback
} else {
setValidConfig(result.valid_config === true);
}
});
} else {
// Fallback for development/testing
setValidConfig(false);
}
}, []);
const handleConfigSaved = () => {
setValidConfig(true);
// Navigate to main tab after config is saved
window.location.hash = "#/results";
};
if (validConfig === null) {
return (
<div className="flex h-screen items-center justify-center">
<span className="loading loading-spinner loading-md ms-2"></span>
<div className="flex items-center justify-center h-screen bg-black text-white">
Loading...
</div>
);
@ -43,16 +35,20 @@ const App = () => {
return (
<Router>
<div className="flex h-screen flex-col py-4">
<Container>
<TabNavigation validConfig={validConfig} />
<div className="divider"></div>
<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={handleConfigSaved} />}
element={
<Settings onConfigSaved={() => setValidConfig(true)} />
}
/>
<Route path="*" element={<Navigate to="/settings" replace />} />
</>
@ -61,14 +57,21 @@ const App = () => {
<Route path="/" element={<Navigate to="/results" replace />} />
<Route path="/results" element={<Results />} />
<Route path="/settings" element={<Settings />} />
<Route path="/about" element={<About />} />
</>
)}
</Routes>
</Container>
</div>
<div
className={`fixed top-0 left-0 w-full h-full z-50 bg-black transform transition-transform duration-300 ease-in-out ${
isSideNavOpen ? "translate-x-0" : "-translate-x-full"
}`}
>
<SideNav onClose={() => setIsSideNavOpen(false)} />
</div>
</div>
</Router>
);
};
}
export default App;

View File

@ -1,15 +0,0 @@
@font-face {
font-family: Inter;
src: url("./InterVariable.woff2");
font-style: normal;
font-weight: 300 900;
font-display: swap;
}
@font-face {
font-family: Inter;
src: url("./InterVariable-Italic.woff2");
font-style: italic;
font-weight: 300 900;
font-display: swap;
}

View File

@ -1,57 +0,0 @@
import { FaDiscord, FaTelegram } from "react-icons/fa";
import { SiGitea } from "react-icons/si";
const AboutPage = () => {
const socialLinks = [
{
name: "Discord",
icon: <FaDiscord className="text-4xl" />,
url: "https://discord.cdrm-project.com/",
description: "Join our Discord community",
color: "hover:text-indigo-400",
},
{
name: "Telegram",
icon: <FaTelegram className="text-4xl" />,
url: "https://telegram.cdrm-project.com/",
description: "Follow us on Telegram",
color: "hover:text-sky-400",
},
{
name: "Gitea",
icon: <SiGitea className="text-4xl" />,
url: "https://cdm-project.com/tpd94/cdrm-project",
description: "Check out our code",
color: "hover:text-lime-400",
},
];
return (
<div className="flex min-h-full flex-col items-center justify-center p-6">
<div className="mb-8 text-center">
<h2 className="mb-2 text-3xl font-bold">Connect with us</h2>
<p className="text-base-content/70 text-lg">Join our community and stay updated</p>
</div>
<div className="grid w-full max-w-4xl grid-cols-1 gap-6 md:grid-cols-3">
{socialLinks.map((link) => (
<a
key={link.name}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className={`card bg-base-200 shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl ${link.color}`}
>
<div className="card-body items-center text-center">
<div className="mb-2">{link.icon}</div>
<h3 className="card-title text-xl font-semibold">{link.name}</h3>
<p className="text-base-content/70">{link.description}</p>
</div>
</a>
))}
</div>
</div>
);
};
export default AboutPage;

View File

@ -1,9 +0,0 @@
const Container = ({ children, className = "", ...props }) => {
return (
<main className={`container mx-auto p-4 mb-5 ${className}`} {...props}>
{children}
</main>
);
};
export default Container;

View File

@ -1,72 +0,0 @@
import { useEffect, useState } from "react";
const InjectionMenu = () => {
const [injectionType, setInjectionType] = useState("LICENSE");
const [drmOverride, setDrmOverride] = useState("DISABLED");
useEffect(() => {
chrome.storage.local.get(["injection_type", "drm_override"], (result) => {
if (result.injection_type !== undefined) {
setInjectionType(result.injection_type);
}
if (result.drm_override !== undefined) {
setDrmOverride(result.drm_override);
}
});
}, []);
const handleInjectionTypeChange = (type) => {
chrome.storage.local.set({ injection_type: type }, () => {
if (chrome.runtime.lastError) {
console.error("Error updating injection_type:", chrome.runtime.lastError);
} else {
setInjectionType(type);
console.log(`Injection type updated to ${type}`);
}
});
};
const handleDrmOverrideChange = (type) => {
chrome.storage.local.set({ drm_override: type }, () => {
if (chrome.runtime.lastError) {
console.error("Error updating drm_override:", chrome.runtime.lastError);
} else {
setDrmOverride(type);
console.log(`DRM Override updated to ${type}`);
}
});
};
return (
<div className="flex flex-row">
<div className="mr-2 ml-auto flex h-full flex-row items-center justify-center">
<p className="mr-2 p-2 text-lg text-nowrap">Injection type:</p>
<div role="tablist" className="tabs tabs-border">
<a
role="tab"
className={`tab ${injectionType === "LICENSE" ? "tab-active font-semibold" : ""}`}
onClick={() => handleInjectionTypeChange("LICENSE")}
>
License
</a>
<a
role="tab"
className={`tab ${injectionType === "EME" ? "tab-active font-semibold" : ""}`}
onClick={() => handleInjectionTypeChange("EME")}
>
EME
</a>
<a
role="tab"
className={`tab ${injectionType === "DISABLED" ? "tab-active font-semibold" : ""}`}
onClick={() => handleInjectionTypeChange("DISABLED")}
>
Disabled
</a>
</div>
</div>
</div>
);
};
export default InjectionMenu;

View File

@ -1,31 +1,19 @@
import React, { useEffect, useState } from "react";
import { IoCameraOutline, IoCopyOutline, IoSaveOutline } from "react-icons/io5";
import { toast } from "sonner";
import InjectionMenu from "./injectionmenu";
const Results = () => {
function Results() {
const [drmType, setDrmType] = useState("");
const [pssh, setPssh] = useState("");
const [licenseUrl, setLicenseUrl] = useState("");
const [keys, setKeys] = useState([]);
const [manifestUrl, setManifestUrl] = useState("");
const [currentTabUrl, setCurrentTabUrl] = useState("");
const [keys, setKeys] = useState("");
useEffect(() => {
chrome.storage.local.get(
[
"drmType",
"latestPSSH",
"latestLicenseRequest",
"latestKeys",
"licenseURL",
"manifestURL",
],
["drmType", "latestPSSH", "latestLicenseRequest", "latestKeys"],
(result) => {
if (result.drmType) setDrmType(result.drmType || "");
if (result.latestPSSH) setPssh(result.latestPSSH || "");
if (result.licenseURL) setLicenseUrl(result.licenseURL || "");
if (result.manifestURL) setManifestUrl(result.manifestURL || "");
if (result.drmType) setDrmType(result.drmType);
if (result.latestPSSH) setPssh(result.latestPSSH);
if (result.latestLicenseRequest?.url)
setLicenseUrl(result.latestLicenseRequest.url);
if (result.latestKeys) {
try {
const parsed = Array.isArray(result.latestKeys)
@ -40,37 +28,19 @@ const Results = () => {
}
);
// Get current tab URL when component mounts
chrome.windows.getAll({ populate: true, windowTypes: ["normal"] }, (windows) => {
if (windows && windows.length > 0) {
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);
}
}
}
});
const handleChange = (changes, area) => {
if (area === "local") {
if (changes.drmType) {
setDrmType(changes.drmType.newValue || "");
setDrmType(changes.drmType.newValue);
}
if (changes.latestPSSH) {
setPssh(changes.latestPSSH.newValue || "");
setPssh(changes.latestPSSH.newValue);
}
if (changes.licenseURL) {
setLicenseUrl(changes.licenseURL.newValue || "");
}
if (changes.manifestURL) {
setManifestUrl(changes.manifestURL.newValue || "");
if (changes.latestLicenseRequest) {
setLicenseUrl(changes.latestLicenseRequest.newValue.url);
}
if (changes.latestKeys) {
setKeys(changes.latestKeys.newValue || []);
setKeys(changes.latestKeys.newValue);
}
}
};
@ -82,15 +52,16 @@ const Results = () => {
const handleCapture = () => {
// Reset stored values
chrome.storage.local.set({
drmType: "",
latestPSSH: "",
licenseURL: "",
manifestURL: "",
drmType: "None",
latestPSSH: "None",
latestLicenseRequest: { url: "None" },
latestKeys: [],
});
// Get all normal windows to exclude your popup
chrome.windows.getAll({ populate: true, windowTypes: ["normal"] }, (windows) => {
chrome.windows.getAll(
{ populate: true, windowTypes: ["normal"] },
(windows) => {
if (!windows || windows.length === 0) {
console.warn("No normal Chrome windows found");
return;
@ -118,204 +89,56 @@ const Results = () => {
} else {
console.warn("No active tab found in the last focused normal window");
}
});
};
const handleCopyToClipboard = (text, label) => {
navigator.clipboard.writeText(text);
toast.success(`Copied ${label} to clipboard`);
};
// 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="flex h-full w-full flex-col gap-4">
<InjectionMenu />
<button onClick={handleCapture} className="btn btn-primary">
<IoCameraOutline className="h-5 w-5" />
Capture current tab
</button>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">DRM Type</legend>
<input
type="text"
value={drmType}
className="input w-full font-mono"
placeholder="[Not available]"
disabled
/>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Manifest URL</legend>
<input
type="text"
value={getManifestDisplayValue()}
className={`input w-full font-mono ${
isYouTube() && !manifestUrl ? "text-yellow-400" : ""
}`}
placeholder={getManifestPlaceholder()}
disabled
/>
{!isYouTube() && manifestUrl && (
<p className="label flex justify-end">
<button
className="btn btn-link btn-sm text-info"
onClick={() => handleCopyToClipboard(manifestUrl, "manifest URL")}
>
<IoCopyOutline className="h-5 w-5" />
Copy manifest URL
</button>
</p>
)}
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">PSSH</legend>
<input
type="text"
value={pssh}
className="input w-full font-mono"
placeholder="[Not available]"
disabled
/>
{pssh && (
<p className="label flex justify-end">
<button
className="btn btn-link btn-sm text-info"
onClick={() => handleCopyToClipboard(pssh, "PSSH")}
>
<IoCopyOutline className="h-5 w-5" />
Copy PSSH
</button>
</p>
)}
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">License URL</legend>
<input
type="text"
value={licenseUrl}
className="input w-full font-mono"
placeholder="[Not available]"
disabled
/>
{licenseUrl && (
<p className="label flex justify-end">
<button
className="btn btn-link btn-sm text-info"
onClick={() => handleCopyToClipboard(licenseUrl, "license URL")}
>
<IoCopyOutline className="h-5 w-5" />
Copy license URL
</button>
</p>
)}
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Keys</legend>
<textarea
value={
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")
: "[Not available]"
}
className="textarea w-full font-mono"
disabled
/>
{hasData() &&
Array.isArray(keys) &&
keys.filter((k) => k.type !== "SIGNING").length > 0 && (
<p className="label flex justify-end">
<button
className="btn btn-link btn-sm text-info"
onClick={() =>
handleCopyToClipboard(
keys
.filter((k) => k.type !== "SIGNING")
.map((k) => `${k.key_id || k.keyId}:${k.key}`)
.join("\n"),
"keys"
)
}
>
<IoCopyOutline className="h-5 w-5" />
Copy keys
</button>
</p>
)}
</fieldset>
{hasData() && (
<button onClick={handleExportJSON} className="btn btn-success">
<IoSaveOutline className="h-5 w-5" />
Export as JSON
</button>
)}
</div>
);
};
return (
<div className="w-full grow flex h-full overflow-y-auto overflow-x-auto flex-col text-white p-4">
<button
onClick={handleCapture}
className="w-full h-10 bg-sky-500 rounded-md p-2 mt-2 text-white cursor-pointer hover:bg-sky-600"
>
Capture current tab
</button>
<p className="text-2xl mt-5">DRM Type</p>
<input
type="text"
value={drmType}
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white"
placeholder="None"
disabled
/>
<p className="text-2xl mt-5">PSSH</p>
<input
type="text"
value={pssh}
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white"
placeholder="None"
disabled
/>
<p className="text-2xl mt-5">License URL</p>
<input
type="text"
value={licenseUrl}
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white"
placeholder="None"
disabled
/>
<p className="text-2xl mt-5">Keys</p>
<div className="w-full min-h-64 h-64 flex items-center justify-center text-center overflow-y-auto bg-slate-800/50 rounded-md p-2 mt-2 text-white whitespace-pre-line">
{Array.isArray(keys) &&
keys.filter((k) => k.type !== "SIGNING").length > 0 ? (
keys
.filter((k) => k.type !== "SIGNING")
.map((k) => `${k.key_id || k.keyId}:${k.key}`)
.join("\n")
) : (
<span className="text-gray-400">None</span>
)}
</div>
</div>
);
}
export default Results;

View File

@ -1,11 +1,11 @@
import { useEffect, useState } from "react";
import { IoSaveOutline } from "react-icons/io5";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
const Settings = ({ onConfigSaved }) => {
function Settings({ onConfigSaved }) {
const [instanceUrl, setInstanceUrl] = useState("");
const [storedUrl, setStoredUrl] = useState(null);
const [message, setMessage] = useState(null);
const [messageType, setMessageType] = useState(null);
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
@ -13,8 +13,10 @@ const Settings = ({ onConfigSaved }) => {
useEffect(() => {
chrome.storage.local.get("cdrm_instance", (result) => {
if (chrome.runtime.lastError) {
toast.error("Error fetching CDRM instance:", chrome.runtime.lastError);
console.error("Error fetching CDRM instance:", chrome.runtime.lastError);
console.error(
"Error fetching CDRM instance:",
chrome.runtime.lastError
);
} else if (result.cdrm_instance) {
setStoredUrl(result.cdrm_instance);
}
@ -24,12 +26,14 @@ const Settings = ({ onConfigSaved }) => {
const handleSave = async () => {
const trimmedUrl = instanceUrl.trim().replace(/\/+$/, "");
if (!trimmedUrl) {
toast.error("Please enter a valid URL.");
setMessage("Please enter a valid URL.");
setMessageType("error");
return;
}
const endpoint = trimmedUrl + "/api/extension";
setLoading(true);
setMessage(null);
try {
const response = await fetch(endpoint, {
@ -42,24 +46,21 @@ const Settings = ({ onConfigSaved }) => {
const data = await response.json();
if (data.status === true) {
toast.success("Successfully connected to a CDRM instance");
setMessage("Successfully connected to CDRM Instance.");
setMessageType("success");
const widevineRes = await fetch(`${trimmedUrl}/remotecdm/widevine/deviceinfo`);
if (!widevineRes.ok) {
toast.error(
`Failed to fetch Widevine device info. Reason: ${widevineRes.statusText}`
const widevineRes = await fetch(
`${trimmedUrl}/remotecdm/widevine/deviceinfo`
);
return;
}
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) {
toast.error(
`Failed to fetch PlayReady device info. Reason: ${playreadyRes.statusText}`
const playreadyRes = await fetch(
`${trimmedUrl}/remotecdm/playready/deviceinfo`
);
return;
}
if (!playreadyRes.ok)
throw new Error("Failed to fetch PlayReady device info");
const playreadyData = await playreadyRes.json();
chrome.storage.local.set(
@ -87,11 +88,10 @@ const Settings = ({ onConfigSaved }) => {
"Error saving to chrome.storage:",
chrome.runtime.lastError
);
toast.error(
`Error saving configuration. Reason: ${chrome.runtime.lastError}`
);
setMessage("Error saving configuration.");
setMessageType("error");
} else {
console.log("Configuration saved");
console.log("Configuration saved.");
setStoredUrl(trimmedUrl);
setInstanceUrl("");
if (onConfigSaved) onConfigSaved();
@ -100,56 +100,51 @@ const Settings = ({ onConfigSaved }) => {
}
);
} else {
toast.error("Invalid response from endpoint.");
throw new Error("Invalid response from endpoint.");
}
} catch (err) {
console.error("Connection error:", err);
toast.error(
`Invalid endpoint or device info could not be retrieved. Reason: ${err.message}`
);
setMessage("Invalid endpoint or device info could not be retrieved.");
setMessageType("error");
} finally {
setLoading(false);
}
};
return (
<div className="flex h-full w-full flex-col gap-4">
{storedUrl && (
<p className="mb-2 text-base">
Current instance: <span className="font-mono font-semibold">{storedUrl}</span>
</p>
)}
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">New instance URL</legend>
<div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col p-4">
<input
type="text"
value={instanceUrl}
onChange={(e) => setInstanceUrl(e.target.value)}
placeholder="https://cdrm-project.com/, http://127.0.0.1:5000/"
className="input w-full font-mono"
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"
/>
</fieldset>
<button
type="button"
onClick={handleSave}
disabled={loading}
className="btn btn-primary btn-block"
className={`mt-4 p-2 ${
loading ? "bg-blue-400" : "bg-blue-600 hover:bg-blue-700"
} text-white rounded-md transition duration-300`}
>
{loading ? (
<>
<span className="loading loading-spinner loading-sm"></span> Connecting...
</>
) : (
<>
<IoSaveOutline className="h-5 w-5" />
Save settings
</>
)}
{loading ? "Connecting..." : "Save Settings"}
</button>
{message && (
<p
className={`mt-2 text-sm text-center ${
messageType === "success" ? "text-green-400" : "text-red-400"
}`}
>
{message}
</p>
)}
</div>
);
};
}
export default Settings;

View File

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

View File

@ -1,51 +0,0 @@
import { IoIosInformationCircleOutline } from "react-icons/io";
import { IoHomeOutline, IoSettingsOutline } from "react-icons/io5";
import { NavLink, useLocation } from "react-router-dom";
const TabNavigation = ({ validConfig }) => {
const location = useLocation();
const activeTab =
location.pathname === "/settings"
? "settings"
: location.pathname === "/about"
? "about"
: "main";
return (
<div className="mb-4 flex items-center justify-center">
<div role="tablist" className="tabs tabs-box">
<NavLink
role="tab"
to="/results"
className={`tab ${!validConfig ? "cursor-not-allowed" : activeTab === "main" ? "tab-active font-semibold" : ""}`}
onClick={(e) => {
if (!validConfig) {
e.preventDefault();
}
}}
>
<IoHomeOutline className="mr-1 h-5 w-5" />
Main
</NavLink>
<NavLink
role="tab"
to="/settings"
className={`tab ${activeTab === "settings" ? "tab-active font-semibold" : ""}`}
>
<IoSettingsOutline className="mr-1 h-5 w-5" />
Settings
</NavLink>
<NavLink
role="tab"
to="/about"
className={`tab ${activeTab === "about" ? "tab-active font-semibold" : ""}`}
>
<IoIosInformationCircleOutline className="mr-1 h-5 w-5" />
About
</NavLink>
</div>
</div>
);
};
export default TabNavigation;

View File

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

View File

@ -1,35 +1,8 @@
@import "tailwindcss";
@plugin "daisyui";
@plugin "daisyui/theme" {
name: "dim";
default: true;
prefersdark: true;
color-scheme: "dark";
--radius-selector: 0.5rem;
--radius-field: 0.5rem;
--radius-box: 0.5rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--depth: 0;
--noise: 0;
}
:root {
--font-default-sans:
"Inter", system-ui, -apple-system, Roboto, "Segoe UI", "Helvetica Neue", "Noto Sans",
Oxygen, Ubuntu, Cantarell, "Open Sans", Arial, sans-serif;
font-family: var(--font-default-sans);
}
/* Force Sonner toast to use Inter first */
[data-sonner-toast],
.sonner-toast,
:where([data-sonner-toast]) :where([data-title]) :where([data-description]) {
font-family: var(--font-default-sans) !important;
}
html,
body {
font-family: var(--font-default-sans);
html, body, #root {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}

View File

@ -1,19 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Toaster } from "sonner";
import App from "./App.jsx";
import "./assets/fonts/font-face.css";
import "./index.css";
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById("root")).render(
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
<Toaster
richColors
className="flex justify-center"
position="bottom-center"
duration="7000"
theme="dark"
/>
</StrictMode>
);
</StrictMode>,
)

View File

@ -1,21 +1,9 @@
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react-swc";
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);
},
};
};
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
base: "./",
plugins: [react(), tailwindcss(), replaceVersionPlugin()],
});
base: './',
plugins: [react(), tailwindcss()],
})

918
inject.js Normal file
View File

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

41
manifest.json Normal file
View File

@ -0,0 +1,41 @@
{
"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"
}
}

View File

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

View File

@ -1,96 +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,
},
"*"
);
});
}
// 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,
});
}
});

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +0,0 @@
{
"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
View File

@ -1,136 +0,0 @@
{
"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"
}
}
}
}

View File

@ -1,24 +0,0 @@
{
"name": "cdrm-extension",
"version": "3.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

13
react/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CDRM Decryption Extension</title>
<script type="module" crossorigin src="./assets/index-CN3ssfBX.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,88 +0,0 @@
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.");
}
});
}
});

View File

@ -1,96 +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,
},
"*"
);
});
}
// 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,
});
}
});

View File

@ -1,930 +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;
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 (
xhr.status === 200 ||
jsonData.status === "Success" ||
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 (xhr.status === 200 || jsonData.status === "Success" || 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);
};
})();

View File

@ -1,37 +0,0 @@
{
"manifest_version": 3,
"name": "CDRM Extension",
"version": "3.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"
}
}

View File

@ -1,66 +0,0 @@
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;