Compare commits

...

42 Commits
main ... main

Author SHA1 Message Date
e9212dfbf9 Merge pull request 'Add manifest URL field, reset keys when manifest changes, organize repo, update to Manifest v3' (#3) from voldemort/CDRM-Extension:main into main
Reviewed-on: tpd94/CDRM-Extension#3
2025-08-19 03:43:15 +00:00
voldemort
2b2dc65b7b bump version 2025-07-24 23:17:15 +07:00
voldemort
d419af0fb9 UI overhaul 2025-07-24 23:04:05 +07:00
voldemort
37334ae389 fix: enhance response handling in RemoteCDMBase and remoteWidevineCDM classes 2025-07-24 17:57:28 +07:00
voldemort
ee9eeb30ea feat: add js minification during file copy in build process 2025-07-22 10:55:02 +07:00
voldemort
0b59c6b0d6 feat: implement resetDRMState to clear state and session on new manifest detection 2025-07-22 10:28:25 +07:00
voldemort
f40e1880d6 add logging for POST request interception in DRM handling 2025-07-22 10:13:17 +07:00
voldemort
8d4cd89a02 consolidate DRM interception handling for fetch and XHR requests 2025-07-22 10:04:17 +07:00
voldemort
a40a6abaf7 extract DRM PSSH detection and storage into a separate function 2025-07-22 09:56:05 +07:00
voldemort
81f44b2f0e concatenate message handling for DRM, injection type, and CDM devices 2025-07-22 09:52:12 +07:00
voldemort
e03ca633de do not show export if keys are not present 2025-07-21 23:29:40 +07:00
voldemort
d0154fd6c1 change MANIFEST_URL_FOUND to MANIFEST_URL 2025-07-21 22:57:28 +07:00
voldemort
867294f7f6 refactor: extract DRM challenge detection and handling for fetch and XHR requests 2025-07-21 20:25:47 +07:00
voldemort
f8712d7726 refactor: consolidate remote cdm initialization into a helper function 2025-07-21 18:28:59 +07:00
voldemort
9e071365e3 use regular prefix instead of adding color. fixes not storing to chrome.storage.local 2025-07-21 17:18:18 +07:00
voldemort
89f66b25be add extension prefix and color for ease of debugging and filtering 2025-07-21 16:05:35 +07:00
voldemort
30e797bc09 use global DRM_SIGNATURES for readability 2025-07-21 11:54:40 +07:00
voldemort
3bd2e0f465 refactor: extract redundant code into helpers (part 1) 2025-07-21 11:41:49 +07:00
voldemort
9cff5b44bd forgot to remove _FOUND in MANIFEST_URL 2025-07-21 11:39:35 +07:00
voldemort
3fae8f296f show "use yt-dlp" in manifest for youtube, use plugin-react-swc 2025-07-20 20:10:47 +07:00
voldemort
6e22837047 Fix Trusted Types violation in manifest interceptor injection, which fixes data not showing for YouTube DRM 2025-07-20 17:43:13 +07:00
voldemort
889a4c63f3 sync version: only update the "version" instead of parsing whole file 2025-07-20 17:21:02 +07:00
voldemort
918269f42e update setting UI, showing current instance in a separate line 2025-07-20 17:16:40 +07:00
voldemort
5cc6834532 add export to json button 2025-07-20 16:56:34 +07:00
voldemort
5949228d4f update to manifest v3 2025-07-20 16:27:13 +07:00
voldemort
5678c9b5da add manifest URL in frontend 2025-07-20 16:00:59 +07:00
voldemort
9c738d8a3e dynamically add version to extension title bar and manifest.json, add readme 2025-07-20 15:03:43 +07:00
voldemort
53aa7d66e3 remove leftover dist folder 2025-07-20 14:24:03 +07:00
voldemort
935d235ad9 organize files and folders, remove unnecessary generated build files, add new script to build extension to extension-release folder 2025-07-20 14:22:04 +07:00
voldemort
c9ff17558d prettier formatting, add manifest url in console.log 2025-07-20 12:18:15 +07:00
bafe525510 Merge pull request 'rewrite' (#2) from rewrite into main
Reviewed-on: tpd94/CDRM-Extension#2
2025-06-29 16:45:50 +00:00
c463d8455f Update frontend 2025-06-29 12:44:09 -04:00
3fea6e54ba Update inject.js 2025-06-29 12:07:18 -04:00
388afaa171 Update inject.js 2025-06-29 02:50:36 -04:00
8a2fc2dfed Update inject.js
logic flow, anchored functions, etc.
2025-06-26 21:45:26 -04:00
046d64bbc5 Backup 2025-06-19 22:42:40 -04:00
b4de9c6d61 Update inject.js 2025-06-16 01:59:26 -04:00
a7b58a0a78 Update inject.js
real happy
2025-06-16 01:21:17 -04:00
c019e73625 Happy so far 2025-06-16 00:24:53 -04:00
75b5d6e8ef Rollback 2025-06-15 18:25:54 -04:00
31fd877bd9 Added better message logic 2025-06-15 13:00:09 -04:00
b3eec07085 Update inject.js 2025-06-09 12:01:39 -04:00
46 changed files with 7608 additions and 5261 deletions

30
.gitignore vendored Normal file
View File

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

3
.prettierignore Normal file
View File

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

8
.prettierrc.json Normal file
View File

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

52
README.md Normal file
View File

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

View File

@ -1,94 +0,0 @@
// Open popout window when the extension icon is clicked
chrome.browserAction.onClicked.addListener(() => {
chrome.windows.create({
url: chrome.runtime.getURL("react/index.html"),
type: "popup", // opens as a floating window
width: 800,
height: 600
});
});
// Listen for messages and store data in chrome.storage.local
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const { type, data } = message;
switch (type) {
case "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.");
}
});
}
});

119
buildext.js Normal file
View File

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

View File

@ -1,76 +0,0 @@
// 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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,35 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.7",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.1",
"tailwindcss": "^4.1.7"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"vite": "^6.3.5"
}
"name": "frontend",
"private": true,
"version": "3.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.7.0",
"sonner": "^2.0.6",
"tailwindcss": "^4.1.11"
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.7.0",
"@vitejs/plugin-react-swc": "^3.11.0",
"daisyui": "^5.0.46",
"eslint": "^9.31.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"
}
}

View File

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

Binary file not shown.

Binary file not shown.

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

@ -1,114 +0,0 @@
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,8 +1,35 @@
@import "tailwindcss";
@plugin "daisyui";
html, body, #root {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
@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);
}

View File

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

View File

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

918
inject.js
View File

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

View File

@ -1,41 +0,0 @@
{
"manifest_version": 2,
"name": "CDRM Extension 2.0",
"version": "2.0",
"description": "Decrypt DRM Protected content",
"permissions": [
"webRequest",
"webRequestBlocking",
"<all_urls>",
"activeTab",
"storage",
"tabs",
"contextMenus"
],
"background": {
"scripts": ["background.js"],
"persistent": true
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start",
"all_frames": true
}
],
"web_accessible_resources": ["inject.js"],
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"browser_action": {
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"128": "icons/icon128.png"
}
},
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"128": "icons/icon128.png"
}
}

89
mv2/background.js Normal file
View File

@ -0,0 +1,89 @@
// Open popout window when the extension icon is clicked
chrome.browserAction.onClicked.addListener(() => {
chrome.windows.create({
url: chrome.runtime.getURL("index.html"),
type: "popup", // opens as a floating window
width: 800,
height: 600,
});
});
// Listen for messages and store data in chrome.storage.local
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const { type, data } = message;
switch (type) {
case "DRM_TYPE":
console.log("DRM Type:", data);
chrome.storage.local.set({ drmType: data });
break;
case "PSSH_DATA":
console.log("Storing PSSH:", data);
chrome.storage.local.set({ latestPSSH: data });
break;
case "KEYS_DATA":
console.log("Storing Decryption Keys:", data);
chrome.storage.local.set({ latestKeys: data });
break;
case "LICENSE_URL":
console.log("Storling License URL " + data);
chrome.storage.local.set({ licenseURL: data });
break;
case "MANIFEST_URL_FOUND":
console.log("Storing Manifest URL:", data);
chrome.storage.local.set({ manifestURL: data });
break;
default:
console.warn("Unknown message type received:", type);
}
});
// Set initial config and injection type on install
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === "install") {
chrome.storage.local.set({ valid_config: false }, () => {
if (chrome.runtime.lastError) {
console.error("Error setting valid_config:", chrome.runtime.lastError);
} else {
console.log("valid_config set to false on first install.");
}
});
chrome.storage.local.set({ injection_type: "LICENSE" }, () => {
if (chrome.runtime.lastError) {
console.error("Error setting Injection Type:", chrome.runtime.lastError);
} else {
console.log("Injection type set to LICENSE on first install.");
}
});
chrome.storage.local.set({ drm_override: "DISABLED" }, () => {
if (chrome.runtime.lastError) {
console.error("Error setting DRM Override type:", chrome.runtime.lastError);
} else {
console.log("DRM Override type set to DISABLED on first install.");
}
});
chrome.storage.local.set({ cdrm_instance: null }, () => {
if (chrome.runtime.lastError) {
console.error("Error setting CDRM instance:", chrome.runtime.lastError);
} else {
console.log("CDRM instance set to null.");
}
});
chrome.storage.local.set({ cdrm_api_key: null }, () => {
if (chrome.runtime.lastError) {
console.error("Error setting CDRM API Key:", chrome.runtime.lastError);
} else {
console.log("CDRM API Key set.");
}
});
}
});

96
mv2/content.js Normal file
View File

@ -0,0 +1,96 @@
// Inject `inject.js` into the page context
(function injectScript() {
const script = document.createElement("script");
script.src = chrome.runtime.getURL("inject.js");
script.type = "text/javascript";
script.onload = () => script.remove(); // Clean up
// Inject directly into <html> or <head>
(document.documentElement || document.head || document.body).appendChild(script);
})();
// Listen for messages from the injected script
window.addEventListener("message", function (event) {
if (event.source !== window) return;
if (
["__DRM_TYPE__", "__PSSH_DATA__", "__KEYS_DATA__", "__LICENSE_URL__"].includes(
event.data?.type
)
) {
chrome.runtime.sendMessage({
type: event.data.type.replace("__", "").replace("__", ""),
data: event.data.data,
});
}
if (event.data.type === "__GET_CDM_DEVICES__") {
chrome.storage.local.get(["widevine_device", "playready_device"], (result) => {
const widevine_device = result.widevine_device || null;
const playready_device = result.playready_device || null;
window.postMessage(
{
type: "__CDM_DEVICES__",
widevine_device,
playready_device,
},
"*"
);
});
}
if (event.data.type === "__GET_INJECTION_TYPE__") {
chrome.storage.local.get("injection_type", (result) => {
const injectionType = result.injection_type || "LICENSE";
window.postMessage(
{
type: "__INJECTION_TYPE__",
injectionType,
},
"*"
);
});
}
if (event.data.type === "__GET_DRM_OVERRIDE__") {
chrome.storage.local.get("drm_override", (result) => {
const drmOverride = result.drm_override || "DISABLED";
window.postMessage(
{
type: "__DRM_OVERRIDE__",
drmOverride,
},
"*"
);
});
}
// Manifest header and URL
const seenManifestUrls = new Set();
if (event.data?.type === "__MANIFEST_URL__") {
const url = event.data.data;
if (seenManifestUrls.has(url)) return;
seenManifestUrls.add(url);
console.log("✅ [Content] Unique manifest URL:", url);
chrome.runtime.sendMessage({
type: "MANIFEST_URL_FOUND",
data: url,
});
}
if (event.data?.type === "__MANIFEST_HEADERS__") {
const { url, headers } = event.data;
console.log("[Content.js] Manifest Headers:", url, headers);
chrome.runtime.sendMessage({
type: "MANIFEST_HEADERS",
url,
headers,
});
}
});

1143
mv2/inject.js Normal file

File diff suppressed because it is too large Load Diff

41
mv2/manifest.json Normal file
View File

@ -0,0 +1,41 @@
{
"manifest_version": 2,
"name": "CDRM Extension",
"version": "2.1.0",
"description": "Decrypt DRM protected content",
"permissions": [
"webRequest",
"webRequestBlocking",
"<all_urls>",
"activeTab",
"storage",
"tabs",
"contextMenus"
],
"background": {
"scripts": ["background.js"],
"persistent": true
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start",
"all_frames": true
}
],
"web_accessible_resources": ["inject.js"],
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"browser_action": {
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"128": "icons/icon128.png"
}
},
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"128": "icons/icon128.png"
}
}

136
package-lock.json generated Normal file
View File

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

24
package.json Normal file
View File

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

View File

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

88
src/background.js Normal file
View File

@ -0,0 +1,88 @@
chrome.action.onClicked.addListener(() => {
chrome.windows.create({
url: chrome.runtime.getURL("index.html"),
type: "popup",
width: 800,
height: 600,
});
});
// Listen for messages and store data in chrome.storage.local
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const { type, data } = message;
switch (type) {
case "DRM_TYPE":
console.log("[CDRM-Extension] DRM Type:", data);
chrome.storage.local.set({ drmType: data });
break;
case "PSSH_DATA":
console.log("[CDRM-Extension] Storing PSSH:", data);
chrome.storage.local.set({ latestPSSH: data });
break;
case "KEYS_DATA":
console.log("[CDRM-Extension] Storing Decryption Keys:", data);
chrome.storage.local.set({ latestKeys: data });
break;
case "LICENSE_URL":
console.log("[CDRM-Extension] Storing License URL " + data);
chrome.storage.local.set({ licenseURL: data });
break;
case "MANIFEST_URL":
console.log("[CDRM-Extension] Storing Manifest URL:", data);
chrome.storage.local.set({ manifestURL: data });
break;
default:
console.warn("[CDRM-Extension] Unknown message type received:", type);
}
});
// Set initial config and injection type on install
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === "install") {
chrome.storage.local.set({ valid_config: false }, () => {
if (chrome.runtime.lastError) {
console.error("[CDRM-Extension] Error setting valid_config:", chrome.runtime.lastError);
} else {
console.log("[CDRM-Extension] valid_config set to false on first install.");
}
});
chrome.storage.local.set({ injection_type: "LICENSE" }, () => {
if (chrome.runtime.lastError) {
console.error("[CDRM-Extension] Error setting Injection Type:", chrome.runtime.lastError);
} else {
console.log("[CDRM-Extension] Injection type set to LICENSE on first install.");
}
});
chrome.storage.local.set({ drm_override: "DISABLED" }, () => {
if (chrome.runtime.lastError) {
console.error("[CDRM-Extension] Error setting DRM Override type:", chrome.runtime.lastError);
} else {
console.log("[CDRM-Extension] DRM Override type set to DISABLED on first install.");
}
});
chrome.storage.local.set({ cdrm_instance: null }, () => {
if (chrome.runtime.lastError) {
console.error("[CDRM-Extension] Error setting CDRM instance:", chrome.runtime.lastError);
} else {
console.log("[CDRM-Extension] CDRM instance set to null.");
}
});
chrome.storage.local.set({ cdrm_api_key: null }, () => {
if (chrome.runtime.lastError) {
console.error("[CDRM-Extension] Error setting CDRM API Key:", chrome.runtime.lastError);
} else {
console.log("[CDRM-Extension] CDRM API Key set.");
}
});
}
});

96
src/content.js Normal file
View File

@ -0,0 +1,96 @@
// Inject `inject.js` into the page context
(function injectScript() {
const script = document.createElement("script");
script.src = chrome.runtime.getURL("inject.js");
script.type = "text/javascript";
script.onload = () => script.remove(); // Clean up
// Inject directly into <html> or <head>
(document.documentElement || document.head || document.body).appendChild(script);
})();
// Listen for messages from the injected script
window.addEventListener("message", function (event) {
if (event.source !== window) return;
if (
["__DRM_TYPE__", "__PSSH_DATA__", "__KEYS_DATA__", "__LICENSE_URL__"].includes(
event.data?.type
)
) {
chrome.runtime.sendMessage({
type: event.data.type.replace("__", "").replace("__", ""),
data: event.data.data,
});
}
if (event.data.type === "__GET_CDM_DEVICES__") {
chrome.storage.local.get(["widevine_device", "playready_device"], (result) => {
const widevine_device = result.widevine_device || null;
const playready_device = result.playready_device || null;
window.postMessage(
{
type: "__CDM_DEVICES__",
widevine_device,
playready_device,
},
"*"
);
});
}
if (event.data.type === "__GET_INJECTION_TYPE__") {
chrome.storage.local.get("injection_type", (result) => {
const injectionType = result.injection_type || "LICENSE";
window.postMessage(
{
type: "__INJECTION_TYPE__",
injectionType,
},
"*"
);
});
}
if (event.data.type === "__GET_DRM_OVERRIDE__") {
chrome.storage.local.get("drm_override", (result) => {
const drmOverride = result.drm_override || "DISABLED";
window.postMessage(
{
type: "__DRM_OVERRIDE__",
drmOverride,
},
"*"
);
});
}
// Manifest header and URL
const seenManifestUrls = new Set();
if (event.data?.type === "__MANIFEST_URL__") {
const url = event.data.data;
if (seenManifestUrls.has(url)) return;
seenManifestUrls.add(url);
console.log("[CDRM-Extension] ✅ [content.js] Unique manifest URL:", url);
chrome.runtime.sendMessage({
type: "MANIFEST_URL",
data: url,
});
}
if (event.data?.type === "__MANIFEST_HEADERS__") {
const { url, headers } = event.data;
console.log("[CDRM-Extension] [content.js] Manifest headers:", url, headers);
chrome.runtime.sendMessage({
type: "MANIFEST_HEADERS",
url,
headers,
});
}
});

930
src/inject.js Normal file
View File

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

37
src/manifest.json Normal file
View File

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

66
syncVersion.js Normal file
View File

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