Compare commits

..

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

37 changed files with 5030 additions and 7230 deletions

30
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

84
background.js Normal file
View File

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

View File

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

67
content.js Normal file
View File

@ -0,0 +1,67 @@
// Inject `inject.js` into the page context
(function injectScript() {
const script = document.createElement('script');
script.src = chrome.runtime.getURL('inject.js');
script.type = 'text/javascript';
script.onload = () => script.remove(); // Clean up
// Inject directly into <html> or <head>
(document.documentElement || document.head || document.body).appendChild(script);
})();
// Listen for messages from the injected script
window.addEventListener("message", function(event) {
if (event.source !== window) return;
if (["__DRM_TYPE__", "__PSSH_DATA__", "__KEYS_DATA__", "__LICENSE_URL__"].includes(event.data?.type)) {
chrome.runtime.sendMessage({
type: event.data.type.replace("__", "").replace("__", ""),
data: event.data.data
});
}
if (event.data.type === "__GET_CDM_DEVICES__") {
chrome.storage.local.get(["widevine_device", "playready_device"], (result) => {
const widevine_device = result.widevine_device || null;
const playready_device = result.playready_device || null;
window.postMessage(
{
type: "__CDM_DEVICES__",
widevine_device,
playready_device
},
"*"
);
});
}
if (event.data.type === "__GET_INJECTION_TYPE__") {
chrome.storage.local.get("injection_type", (result) => {
const injectionType = result.injection_type || "LICENSE";
window.postMessage(
{
type: "__INJECTION_TYPE__",
injectionType
},
"*"
);
});
}
if (event.data.type === "__GET_DRM_OVERRIDE__") {
chrome.storage.local.get("drm_override", (result) => {
const drmOverride = result.drm_override || "DISABLED";
window.postMessage(
{
type: "__DRM_OVERRIDE__",
drmOverride
},
"*"
);
});
}
});

File diff suppressed because one or more lines are too long

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

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

View File

@ -1,30 +1,33 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ["dist"] },
{
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 }],
},
{ 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 },
],
},
},
]

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,30 @@
{
"name": "frontend",
"private": true,
"version": "2.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.7.0",
"tailwindcss": "^4.1.11"
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.7.0",
"@vitejs/plugin-react-swc": "^3.11.0",
"eslint": "^9.31.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"vite": "^7.0.5"
}
"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"
}
}

View File

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

View File

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

View File

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

View File

@ -4,45 +4,48 @@ 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>
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="/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>
);
<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

@ -2,81 +2,84 @@ import { useEffect, useState } from "react";
import hamburgerIcon from "../assets/hamburger.svg";
function TopNav({ onMenuClick }) {
const [injectionType, setInjectionType] = useState("LICENSE");
const [drmOverride, setDrmOverride] = useState("DISABLED");
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);
}
});
}, []);
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 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}`);
}
});
};
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">
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>
);
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">
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,10 +1,8 @@
@import "tailwindcss";
html,
body,
#root {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
html, body, #root {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}

View File

@ -1,10 +1,10 @@
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 './index.css'
import App from './App.jsx'
createRoot(document.getElementById("root")).render(
<StrictMode>
<App />
</StrictMode>
);
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

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

813
inject.js Normal file
View File

@ -0,0 +1,813 @@
let widevineDeviceInfo = null;
let playreadyDeviceInfo = null;
let originalChallenge = null
let serviceCertFound = false;
let drmType = "NONE";
let psshFound = false;
let foundWidevinePssh = null;
let foundPlayreadyPssh = null;
let drmDecided = null;
let drmOverride = "DISABLED";
let interceptType = "DISABLED";
let remoteCDM = null;
let generateRequestCalled = false;
let remoteListenerMounted = false;
let injectionSuccess = false;
let foundChallengeInBody = false;
let licenseResponseCounter = 0;
let keysRetrieved = false;
// Post message to content.js to get DRM override
window.postMessage({ type: "__GET_DRM_OVERRIDE__" }, "*");
// Add listener for DRM override messages
window.addEventListener("message", function(event) {
if (event.source !== window) return;
if (event.data.type === "__DRM_OVERRIDE__") {
drmOverride = event.data.drmOverride || "DISABLED";
console.log("DRM Override set to:", drmOverride);
}
});
// Post message to content.js to get injection type
window.postMessage({ type: "__GET_INJECTION_TYPE__" }, "*");
// Add listener for injection type messages
window.addEventListener("message", function(event) {
if (event.source !== window) return;
if (event.data.type === "__INJECTION_TYPE__") {
interceptType = event.data.injectionType || "DISABLED";
console.log("Injection type set to:", interceptType);
}
});
// Post message to get CDM devices
window.postMessage({ type: "__GET_CDM_DEVICES__" }, "*");
// Add listener for CDM device messages
window.addEventListener("message", function(event) {
if (event.source !== window) return;
if (event.data.type === "__CDM_DEVICES__") {
const { widevine_device, playready_device } = event.data;
console.log("Received device info:", widevine_device, playready_device);
widevineDeviceInfo = widevine_device;
playreadyDeviceInfo = playready_device;
}
});
// PlayReady Remote CDM Class
class remotePlayReadyCDM {
constructor(security_level, host, secret, device_name) {
this.security_level = security_level;
this.host = host;
this.secret = secret;
this.device_name = device_name;
this.session_id = null;
this.challenge = null;
this.keys = null;
}
// Open PlayReady session
openSession() {
const url = `${this.host}/remotecdm/playready/${this.device_name}/open`;
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send();
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.data?.session_id) {
this.session_id = jsonData.data.session_id;
console.log("PlayReady session opened:", this.session_id);
} else {
console.error("Failed to open PlayReady session:", jsonData.message);
throw new Error("Failed to open PlayReady session");
}
}
// Get PlayReady challenge
getChallenge(init_data) {
const url = `${this.host}/remotecdm/playready/${this.device_name}/get_license_challenge`;
const xhr = new XMLHttpRequest();
xhr.open('POST', url, false);
xhr.setRequestHeader('Content-Type', 'application/json');
const body = {
session_id: this.session_id,
init_data: init_data
};
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.data?.challenge) {
this.challenge = btoa(jsonData.data.challenge);
console.log("PlayReady challenge received:", this.challenge);
} else {
console.error("Failed to get PlayReady challenge:", jsonData.message);
throw new Error("Failed to get PlayReady challenge");
}
}
// Parse PlayReady license response
parseLicense(license_message) {
const url = `${this.host}/remotecdm/playready/${this.device_name}/parse_license`;
const xhr = new XMLHttpRequest();
xhr.open('POST', url, false);
xhr.setRequestHeader('Content-Type', 'application/json');
const body = {
session_id: this.session_id,
license_message: license_message
}
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.message === "Successfully parsed and loaded the Keys from the License message")
{
console.log("PlayReady license response parsed successfully");
return true;
} else {
console.error("Failed to parse PlayReady license response:", jsonData.message);
throw new Error("Failed to parse PlayReady license response");
}
}
// Get PlayReady keys
getKeys() {
const url = `${this.host}/remotecdm/playready/${this.device_name}/get_keys`;
const xhr = new XMLHttpRequest();
xhr.open('POST', url, false);
xhr.setRequestHeader('Content-Type', 'application/json');
const body = {
session_id: this.session_id
}
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.data?.keys) {
this.keys = jsonData.data.keys;
console.log("PlayReady keys received:", this.keys);
} else {
console.error("Failed to get PlayReady keys:", jsonData.message);
throw new Error("Failed to get PlayReady keys");
}
}
// Close PlayReady session
closeSession () {
const url = `${this.host}/remotecdm/playready/${this.device_name}/close/${this.session_id}`;
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send();
const jsonData = JSON.parse(xhr.responseText);
if (jsonData) {
console.log("PlayReady session closed successfully");
} else {
console.error("Failed to close PlayReady session:", jsonData.message);
throw new Error("Failed to close PlayReady session");
}
}
}
// Widevine Remote CDM Class
class remoteWidevineCDM {
constructor(device_type, system_id, security_level, host, secret, device_name) {
this.device_type = device_type;
this.system_id = system_id;
this.security_level = security_level;
this.host = host;
this.secret = secret;
this.device_name = device_name;
this.session_id = null;
this.challenge = null;
this.keys = null;
}
// Open Widevine session
openSession () {
const url = `${this.host}/remotecdm/widevine/${this.device_name}/open`;
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send();
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.data?.session_id) {
this.session_id = jsonData.data.session_id;
console.log("Widevine session opened:", this.session_id);
} else {
console.error("Failed to open Widevine session:", jsonData.message);
throw new Error("Failed to open Widevine session");
}
}
// Set Widevine service certificate
setServiceCertificate(certificate) {
const url = `${this.host}/remotecdm/widevine/${this.device_name}/set_service_certificate`;
const xhr = new XMLHttpRequest();
xhr.open('POST', url, false);
xhr.setRequestHeader('Content-Type', 'application/json');
const body = {
session_id: this.session_id,
certificate: certificate ?? null
}
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.status === 200) {
console.log("Service certificate set successfully");
} else {
console.error("Failed to set service certificate:", jsonData.message);
throw new Error("Failed to set service certificate");
}
}
// Get Widevine challenge
getChallenge(init_data, license_type = 'STREAMING') {
const url = `${this.host}/remotecdm/widevine/${this.device_name}/get_license_challenge/${license_type}`;
const xhr = new XMLHttpRequest();
xhr.open('POST', url, false);
xhr.setRequestHeader('Content-Type', 'application/json');
const body = {
session_id: this.session_id,
init_data: init_data,
privacy_mode: serviceCertFound
};
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.data?.challenge_b64) {
this.challenge = jsonData.data.challenge_b64;
console.log("Widevine challenge received:", this.challenge);
} else {
console.error("Failed to get Widevine challenge:", jsonData.message);
throw new Error("Failed to get Widevine challenge");
}
}
// Parse Widevine license response
parseLicense(license_message) {
const url = `${this.host}/remotecdm/widevine/${this.device_name}/parse_license`;
const xhr = new XMLHttpRequest();
xhr.open('POST', url, false);
xhr.setRequestHeader('Content-Type', 'application/json');
const body = {
session_id: this.session_id,
license_message: license_message
};
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.status === 200) {
console.log("Widevine license response parsed successfully");
return true;
} else {
console.error("Failed to parse Widevine license response:", jsonData.message);
throw new Error("Failed to parse Widevine license response");
}
}
// Get Widevine keys
getKeys() {
const url = `${this.host}/remotecdm/widevine/${this.device_name}/get_keys/ALL`;
const xhr = new XMLHttpRequest();
xhr.open('POST', url, false);
xhr.setRequestHeader('Content-Type', 'application/json');
const body = {
session_id: this.session_id
};
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.data?.keys) {
this.keys = jsonData.data.keys;
console.log("Widevine keys received:", this.keys);
} else {
console.error("Failed to get Widevine keys:", jsonData.message);
throw new Error("Failed to get Widevine keys");
}
}
// Close Widevine session
closeSession() {
const url = `${this.host}/remotecdm/widevine/${this.device_name}/close/${this.session_id}`;
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send();
const jsonData = JSON.parse(xhr.responseText);
if (jsonData) {
console.log("Widevine session closed successfully");
} else {
console.error("Failed to close Widevine session:", jsonData.message);
throw new Error("Failed to close Widevine session");
}
}
}
// Utility functions
function hexStrToU8(hexString) {
return Uint8Array.from(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
}
function u8ToHexStr(bytes) {
return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
}
function b64ToHexStr(b64) {
return [...atob(b64)].map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join``;
}
function jsonContainsValue(obj, prefix = "CAES") {
if (typeof obj === "string") return obj.startsWith(prefix);
if (Array.isArray(obj)) return obj.some(val => jsonContainsValue(val, prefix));
if (typeof obj === "object" && obj !== null) {
return Object.values(obj).some(val => jsonContainsValue(val, prefix));
}
return false;
}
function jsonReplaceValue(obj, newValue) {
if (typeof obj === "string") {
return obj.startsWith("CAES") || obj.startsWith("PD94") ? newValue : obj;
}
if (Array.isArray(obj)) {
return obj.map(item => jsonReplaceValue(item, newValue));
}
if (typeof obj === "object" && obj !== null) {
const newObj = {};
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
newObj[key] = jsonReplaceValue(obj[key], newValue);
}
}
return newObj;
}
return obj;
}
function isJson(str) {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
function getWidevinePssh(buffer) {
const hex = u8ToHexStr(new Uint8Array(buffer));
const match = hex.match(/000000(..)?70737368.*/);
if (!match) return null;
const boxHex = match[0];
const bytes = hexStrToU8(boxHex);
return window.btoa(String.fromCharCode(...bytes));
}
function getPlayReadyPssh(buffer) {
const u8 = new Uint8Array(buffer);
const systemId = "9a04f07998404286ab92e65be0885f95";
const hex = u8ToHexStr(u8);
const index = hex.indexOf(systemId);
if (index === -1) return null;
const psshBoxStart = hex.lastIndexOf("70737368", index);
if (psshBoxStart === -1) return null;
const lenStart = psshBoxStart - 8;
const boxLen = parseInt(hex.substr(lenStart, 8), 16) * 2;
const psshHex = hex.substr(lenStart, boxLen);
const psshBytes = hexStrToU8(psshHex);
return window.btoa(String.fromCharCode(...psshBytes));
}
function getClearkey(response) {
let obj = JSON.parse((new TextDecoder("utf-8")).decode(response));
return obj["keys"].map(o => ({
key_id: b64ToHexStr(o["kid"].replace(/-/g, '+').replace(/_/g, '/')),
key: b64ToHexStr(o["k"].replace(/-/g, '+').replace(/_/g, '/')),
}));
}
function base64ToUint8Array(base64) {
const binaryStr = atob(base64);
const len = binaryStr.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryStr.charCodeAt(i);
}
return bytes;
}
function arrayBufferToBase64(uint8array) {
let binary = '';
const len = uint8array.length;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(uint8array[i]);
}
return window.btoa(binary);
}
// Challenge generator interceptor
const originalGenerateRequest = MediaKeySession.prototype.generateRequest;
MediaKeySession.prototype.generateRequest = function(initDataType, initData) {
const session = this;
let playReadyPssh = getPlayReadyPssh(initData);
if (playReadyPssh) {
console.log("[DRM Detected] PlayReady");
foundPlayreadyPssh = playReadyPssh;
console.log("[PlayReady PSSH found] " + playReadyPssh)
}
let wideVinePssh = getWidevinePssh(initData)
if (wideVinePssh) {
// Widevine code
console.log("[DRM Detected] Widevine");
foundWidevinePssh = wideVinePssh;
console.log("[Widevine PSSH found] " + wideVinePssh)
}
// Challenge message interceptor
if (!remoteListenerMounted) {
remoteListenerMounted = true;
session.addEventListener("message", function messageInterceptor(event) {
event.stopImmediatePropagation();
const uint8Array = new Uint8Array(event.message);
const base64challenge = arrayBufferToBase64(uint8Array);
if (base64challenge === "CAQ=" && interceptType !== "DISABLED" && !serviceCertFound) {
const {
device_type, system_id, security_level, host, secret, device_name
} = widevineDeviceInfo;
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
remoteCDM.openSession();
}
if (!injectionSuccess && base64challenge !== "CAQ=" && interceptType !== "DISABLED") {
if (interceptType === "EME") {
injectionSuccess = true;
}
if (!originalChallenge) {
originalChallenge = base64challenge;
}
if (originalChallenge.startsWith("CAES")) {
window.postMessage({ type: "__DRM_TYPE__", data: "Widevine" }, "*");
window.postMessage({ type: "__PSSH_DATA__", data: foundWidevinePssh }, "*");
if (interceptType === "EME" && !remoteCDM) {
const {
device_type, system_id, security_level, host, secret, device_name
} = widevineDeviceInfo;
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
remoteCDM.openSession();
remoteCDM.getChallenge(foundWidevinePssh);
}}
if (!originalChallenge.startsWith("CAES")) {
const buffer = event.message;
const decoder = new TextDecoder('utf-16');
const decodedText = decoder.decode(buffer);
const match = decodedText.match(/<Challenge encoding="base64encoded">([^<]+)<\/Challenge>/);
if (match) {
window.postMessage({ type: "__DRM_TYPE__", data: "PlayReady" }, "*");
window.postMessage({ type: "__PSSH_DATA__", data: foundPlayreadyPssh }, "*");
originalChallenge = match[1];
if (interceptType === "EME" && !remoteCDM) {
const {
security_level, host, secret, device_name
} = playreadyDeviceInfo;
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name)
remoteCDM.openSession();
remoteCDM.getChallenge(foundPlayreadyPssh);
}
}}
if (interceptType === "EME" && remoteCDM) {
const uint8challenge = base64ToUint8Array(remoteCDM.challenge);
const challengeBuffer = uint8challenge.buffer;
const syntheticEvent = new MessageEvent("message", {
data: event.data,
origin: event.origin,
lastEventId: event.lastEventId,
source: event.source,
ports: event.ports
});
Object.defineProperty(syntheticEvent, "message", {
get: () => challengeBuffer
});
console.log("Intercepted EME Challenge and injected custom one.")
session.dispatchEvent(syntheticEvent);
}
}
})
console.log("Message interceptor mounted.");
}
return originalGenerateRequest.call(session, initDataType, initData);
};
// Message update interceptors
const originalUpdate = MediaKeySession.prototype.update;
MediaKeySession.prototype.update = function(response) {
const uint8 = response instanceof Uint8Array ? response : new Uint8Array(response);
const base64Response = window.btoa(String.fromCharCode(...uint8));
if (base64Response.startsWith("CAUS") && foundWidevinePssh && remoteCDM && !serviceCertFound) {
remoteCDM.setServiceCertificate(base64Response);
if (interceptType === "EME" && !remoteCDM.challenge) {
remoteCDM.getChallenge(foundWidevinePssh);
}
window.postMessage({ type: "__DRM_TYPE__", data: "Widevine" }, "*");
window.postMessage({ type: "__PSSH_DATA__", data: foundWidevinePssh }, "*");
serviceCertFound = true;
}
if (!base64Response.startsWith("CAUS") && (foundWidevinePssh || foundPlayreadyPssh) && !keysRetrieved) {
if (licenseResponseCounter === 1 || foundChallengeInBody) {
remoteCDM.parseLicense(base64Response);
remoteCDM.getKeys();
remoteCDM.closeSession();
keysRetrieved = true;
window.postMessage({ type: "__KEYS_DATA__", data: remoteCDM.keys }, "*");
}
licenseResponseCounter++;
}
const updatePromise = originalUpdate.call(this, response);
if (!foundPlayreadyPssh && !foundWidevinePssh) {
updatePromise
.then(() => {
let clearKeys = getClearkey(response);
if (clearKeys && clearKeys.length > 0) {
console.log("[CLEARKEY] ", clearKeys);
const drmType = {
type: "__DRM_TYPE__",
data: 'ClearKey'
};
window.postMessage(drmType, "*");
const keysData = {
type: "__KEYS_DATA__",
data: clearKeys
};
window.postMessage(keysData, "*");
}
})
.catch(e => {
console.log("[CLEARKEY] Not found");
});
}
return updatePromise;
};
// fetch POST interceptor
(function() {
const originalFetch = window.fetch;
window.fetch = async function(resource, config = {}) {
const method = (config.method || 'GET').toUpperCase();
if (method === 'POST') {
let body = config.body;
if (body) {
if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
const buffer = body instanceof Uint8Array ? body : new Uint8Array(body);
const base64Body = window.btoa(String.fromCharCode(...buffer));
if ((base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && (!remoteCDM || remoteCDM.challenge === null || base64Body !== remoteCDM.challenge) && interceptType === "EME") {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
// Block the request
return;
}
if ((base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && interceptType == "LICENSE" &&!foundChallengeInBody) {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
if (!remoteCDM) {
if (base64Body.startsWith("CAES")) {
const {
device_type, system_id, security_level, host, secret, device_name
} = widevineDeviceInfo;
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
remoteCDM.openSession();
remoteCDM.getChallenge(foundWidevinePssh);
}
if (base64Body.startsWith("PD94")) {
const {
security_level, host, secret, device_name
} = playreadyDeviceInfo;
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
remoteCDM.openSession();
remoteCDM.getChallenge(foundPlayreadyPssh);
}
}
if (remoteCDM && remoteCDM.challenge === null) {
remoteCDM.getChallenge(foundWidevinePssh);
}
const injectedBody = base64ToUint8Array(remoteCDM.challenge);
config.body = injectedBody;
return originalFetch(resource, config);
}
}
if (typeof body === 'string' && !isJson(body)) {
const base64EncodedBody = btoa(body);
if ((base64EncodedBody.startsWith("CAES") || base64EncodedBody.startsWith("PD94")) && (!remoteCDM || remoteCDM.challenge === null || base64EncodedBody !== remoteCDM.challenge) && interceptType === "EME") {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
// Block the request
return;
}
if ((base64EncodedBody.startsWith("CAES") || base64EncodedBody.startsWith("PD94")) && interceptType == "LICENSE" && !foundChallengeInBody) {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
if (!remoteCDM) {
if (base64EncodedBody.startsWith("CAES")) {
const {
device_type, system_id, security_level, host, secret, device_name
} = widevineDeviceInfo;
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
remoteCDM.openSession();
remoteCDM.getChallenge(foundWidevinePssh);
}
if (base64EncodedBody.startsWith("PD94")) {
const {
security_level, host, secret, device_name
} = playreadyDeviceInfo;
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
remoteCDM.openSession();
remoteCDM.getChallenge(foundPlayreadyPssh);
}
}
if (remoteCDM && remoteCDM.challenge === null) {
remoteCDM.getChallenge(foundWidevinePssh);
}
const injectedBody = atob(remoteCDM.challenge);
config.body = injectedBody;
return originalFetch(resource, config);
}
}
if (typeof body === 'string' && isJson(body)) {
const jsonBody = JSON.parse(body);
if ((jsonContainsValue(jsonBody, "CAES") || jsonContainsValue(jsonBody, "PD94")) && (!remoteCDM || remoteCDM.challenge === null) && interceptType === "EME") {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
// Block the request
return;
}
if ((jsonContainsValue(jsonBody, "CAES") || jsonContainsValue(jsonBody, "PD94")) && interceptType === "LICENSE" && !foundChallengeInBody) {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
if (!remoteCDM) {
if (jsonContainsValue(jsonBody, "CAES")) {
const {
device_type, system_id, security_level, host, secret, device_name
} = widevineDeviceInfo;
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
remoteCDM.openSession();
remoteCDM.getChallenge(foundWidevinePssh);
}
if (jsonContainsValue(jsonBody, "PD94")) {
const {
security_level, host, secret, device_name
} = playreadyDeviceInfo;
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
remoteCDM.openSession();
remoteCDM.getChallenge(foundPlayreadyPssh);
}
}
if (remoteCDM && remoteCDM.challenge === null) {
remoteCDM.getChallenge(foundWidevinePssh);
}
const injectedBody = jsonReplaceValue(jsonBody, remoteCDM.challenge);
config.body = JSON.stringify(injectedBody);
}
}
}
}
return originalFetch(resource, config);
};
})();
// XHR POST interceptor
(function() {
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
this._method = method;
this._url = url;
return originalOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function(body) {
if (this._method && this._method.toUpperCase() === 'POST') {
if (body) {
if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
const buffer = body instanceof Uint8Array ? body : new Uint8Array(body);
const base64Body = window.btoa(String.fromCharCode(...buffer));
if ((base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && (!remoteCDM || remoteCDM.challenge === null || base64Body !== remoteCDM.challenge) && interceptType === "EME") {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
// Block the request
return;
}
if ((base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && interceptType == "LICENSE" &&!foundChallengeInBody) {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
if (!remoteCDM) {
if (base64Body.startsWith("CAES")) {
const {
device_type, system_id, security_level, host, secret, device_name
} = widevineDeviceInfo;
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
remoteCDM.openSession();
remoteCDM.getChallenge(foundWidevinePssh);
}
if (base64Body.startsWith("PD94")) {
const {
security_level, host, secret, device_name
} = playreadyDeviceInfo;
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
remoteCDM.openSession();
remoteCDM.getChallenge(foundPlayreadyPssh);
}
}
if (remoteCDM && remoteCDM.challenge === null) {
remoteCDM.getChallenge(foundWidevinePssh);
}
const injectedBody = base64ToUint8Array(remoteCDM.challenge);
return originalSend.call(this, injectedBody);
}
}
if (typeof body === 'string' && !isJson(body)) {
const base64EncodedBody = btoa(body);
if ((base64EncodedBody.startsWith("CAES") || base64EncodedBody.startsWith("PD94")) && (!remoteCDM || remoteCDM.challenge === null || base64EncodedBody !== remoteCDM.challenge) && interceptType === "EME") {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
// Block the request
return;
}
if ((base64EncodedBody.startsWith("CAES") || base64EncodedBody.startsWith("PD94")) && interceptType == "LICENSE" && !foundChallengeInBody) {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
if (!remoteCDM) {
if (base64EncodedBody.startsWith("CAES")) {
const {
device_type, system_id, security_level, host, secret, device_name
} = widevineDeviceInfo;
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
remoteCDM.openSession();
remoteCDM.getChallenge(foundWidevinePssh);
}
if (base64EncodedBody.startsWith("PD94")) {
const {
security_level, host, secret, device_name
} = playreadyDeviceInfo;
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
remoteCDM.openSession();
remoteCDM.getChallenge(foundPlayreadyPssh);
}
}
if (remoteCDM && remoteCDM.challenge === null) {
remoteCDM.getChallenge(foundWidevinePssh);
}
const injectedBody = atob(remoteCDM.challenge);
return originalSend.call(this, injectedBody);
}
}
if (typeof body === 'string' && isJson(body)) {
const jsonBody = JSON.parse(body);
if ((jsonContainsValue(jsonBody, "CAES") || jsonContainsValue(jsonBody, "PD94")) && (!remoteCDM || remoteCDM.challenge === null) && interceptType === "EME") {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
// Block the request
return;
}
if ((jsonContainsValue(jsonBody, "CAES") || jsonContainsValue(jsonBody, "PD94")) && interceptType === "LICENSE" && !foundChallengeInBody) {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
if (!remoteCDM) {
if (jsonContainsValue(jsonBody, "CAES")) {
const {
device_type, system_id, security_level, host, secret, device_name
} = widevineDeviceInfo;
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
remoteCDM.openSession();
remoteCDM.getChallenge(foundWidevinePssh);
}
if (jsonContainsValue(jsonBody, "PD94")) {
const {
security_level, host, secret, device_name
} = playreadyDeviceInfo;
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
remoteCDM.openSession();
remoteCDM.getChallenge(foundPlayreadyPssh);
}
}
if (remoteCDM && remoteCDM.challenge === null) {
remoteCDM.getChallenge(foundWidevinePssh);
}
const injectedBody = jsonReplaceValue(jsonBody, remoteCDM.challenge);
return originalSend.call(this, JSON.stringify(injectedBody));
}
}
}
}
return originalSend.apply(this, arguments);
};
})();

41
manifest.json Normal file
View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

136
package-lock.json generated
View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

13
react/index.html Normal file
View File

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

View File

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

View File

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

View File

@ -1,925 +0,0 @@
let widevineDeviceInfo = null;
let playreadyDeviceInfo = null;
let originalChallenge = null;
let serviceCertFound = false;
let drmType = "NONE";
let psshFound = false;
let foundWidevinePssh = null;
let foundPlayreadyPssh = null;
let drmDecided = null;
let drmOverride = "DISABLED";
let interceptType = "DISABLED";
let remoteCDM = null;
let generateRequestCalled = false;
let remoteListenerMounted = false;
let injectionSuccess = false;
let foundChallengeInBody = false;
let licenseResponseCounter = 0;
let keysRetrieved = false;
const DRM_SIGNATURES = {
WIDEVINE: "CAES",
PLAYREADY: "PD94",
SERVICE_CERT: "CAUS",
WIDEVINE_INIT: "CAQ=",
};
const EXTENSION_PREFIX = "[CDRM EXTENSION]";
const PREFIX_COLOR = "black";
const PREFIX_BACKGROUND_COLOR = "yellow";
const logWithPrefix = (...args) => {
const style = `color: ${PREFIX_COLOR}; background: ${PREFIX_BACKGROUND_COLOR}; font-weight: bold; padding: 2px 4px; border-radius: 2px;`;
if (typeof args[0] === "string") {
// If the first arg is a string, prepend the prefix
console.log(`%c${EXTENSION_PREFIX}%c ${args[0]}`, style, "", ...args.slice(1));
} else {
// If not, just log the prefix and the rest
console.log(`%c${EXTENSION_PREFIX}`, style, ...args);
}
};
function resetDRMState() {
logWithPrefix("Resetting DRM state for new manifest...");
// Reset DRM detection state
originalChallenge = null;
serviceCertFound = false;
drmType = "NONE";
psshFound = false;
foundWidevinePssh = null;
foundPlayreadyPssh = null;
drmDecided = null;
// Reset CDM and session state
if (remoteCDM) {
try {
// Try to close the existing session if it exists
if (remoteCDM.session_id) {
remoteCDM.closeSession();
}
} catch (e) {
// Ignore errors when closing session
logWithPrefix("Error closing previous CDM session:", e.message);
}
remoteCDM = null;
}
// Reset interceptor state
generateRequestCalled = false;
remoteListenerMounted = false;
injectionSuccess = false;
foundChallengeInBody = false;
licenseResponseCounter = 0;
keysRetrieved = false;
// Post reset messages to clear UI state
window.postMessage({ type: "__DRM_TYPE__", data: "" }, "*");
window.postMessage({ type: "__PSSH_DATA__", data: "" }, "*");
window.postMessage({ type: "__KEYS_DATA__", data: "" }, "*");
window.postMessage({ type: "__LICENSE_URL__", data: "" }, "*");
}
window.postMessage({ type: "__GET_DRM_OVERRIDE__" }, "*");
window.postMessage({ type: "__GET_INJECTION_TYPE__" }, "*");
window.postMessage({ type: "__GET_CDM_DEVICES__" }, "*");
function createMessageHandler(handlers) {
window.addEventListener("message", function (event) {
if (event.source !== window) return;
const handler = handlers[event.data.type];
if (handler) {
handler(event.data);
}
});
}
createMessageHandler({
__DRM_OVERRIDE__: (data) => {
drmOverride = data.drmOverride || "DISABLED";
logWithPrefix("DRM Override set to:", drmOverride);
},
__INJECTION_TYPE__: (data) => {
interceptType = data.injectionType || "DISABLED";
logWithPrefix("Injection type set to:", interceptType);
},
__CDM_DEVICES__: (data) => {
const { widevine_device, playready_device } = data;
logWithPrefix("Received device info:", widevine_device, playready_device);
widevineDeviceInfo = widevine_device;
playreadyDeviceInfo = playready_device;
},
});
function safeHeaderShellEscape(str) {
return str
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\$/g, "\\$") // escape shell expansion
.replace(/`/g, "\\`")
.replace(/\n/g, ""); // strip newlines
}
function headersToFlags(headersObj) {
return Object.entries(headersObj)
.map(
([key, val]) =>
'--add-headers "' +
safeHeaderShellEscape(key) +
": " +
safeHeaderShellEscape(val) +
'"'
)
.join(" ");
}
function handleManifestDetection(url, headersObj, contentType, source) {
// Reset DRM state when new manifest is detected
resetDRMState();
window.postMessage({ type: "__MANIFEST_URL__", data: url }, "*");
logWithPrefix(`[Manifest][${source}]`, url, contentType);
const headerFlags = headersToFlags(headersObj);
window.postMessage(
{
type: "__MANIFEST_HEADERS__",
url,
headers: headerFlags,
},
"*"
);
}
// Intercep network to find manifest
function injectManifestInterceptor() {
// Execute the interceptor code directly instead of injecting a script
(function () {
function isProbablyManifest(text = "", contentType = "") {
const lowerCT = contentType?.toLowerCase() ?? "";
const sample = text.slice(0, 2000);
const isHLSMime = lowerCT.includes("mpegurl");
const isDASHMime = lowerCT.includes("dash+xml");
const isSmoothMime = lowerCT.includes("sstr+xml");
const isHLSKeyword = sample.includes("#EXTM3U") || sample.includes("#EXT-X-STREAM-INF");
const isDASHKeyword = sample.includes("<MPD") || sample.includes("<AdaptationSet");
const isSmoothKeyword = sample.includes("<SmoothStreamingMedia");
const isJsonManifest = sample.includes('"playlist"') && sample.includes('"segments"');
return (
isHLSMime ||
isDASHMime ||
isSmoothMime ||
isHLSKeyword ||
isDASHKeyword ||
isSmoothKeyword ||
isJsonManifest
);
}
const originalFetch = window.fetch;
window.fetch = async function (input, init) {
const response = await originalFetch.apply(this, arguments);
try {
const clone = response.clone();
const contentType = clone.headers.get("content-type") || "";
const text = await clone.text();
const url = typeof input === "string" ? input : input.url;
if (isProbablyManifest(text, contentType)) {
const headersObj = {};
clone.headers.forEach((value, key) => {
headersObj[key] = value;
});
handleManifestDetection(url, headersObj, contentType, "fetch");
}
} catch (e) {}
return response;
};
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url) {
this.__url = url;
return originalXHROpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {
this.addEventListener("load", function () {
try {
const contentType = this.getResponseHeader("content-type") || "";
const text = this.responseText;
if (isProbablyManifest(text, contentType)) {
const xhrHeaders = {};
const rawHeaders = this.getAllResponseHeaders().trim().split(/\r?\n/);
rawHeaders.forEach((line) => {
const parts = line.split(": ");
if (parts.length === 2) {
xhrHeaders[parts[0]] = parts[1];
}
});
handleManifestDetection(this.__url, xhrHeaders, contentType, "xhr");
}
} catch (e) {}
});
return originalXHRSend.apply(this, arguments);
};
})();
}
injectManifestInterceptor();
class RemoteCDMBase {
constructor({ host, secret, device_name, security_level }) {
this.host = host;
this.secret = secret;
this.device_name = device_name;
this.security_level = security_level;
this.session_id = null;
this.challenge = null;
this.keys = null;
}
openSession(path) {
const url = `${this.host}${path}/open`;
const xhr = new XMLHttpRequest();
xhr.open("GET", url, false);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send();
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.data?.session_id) {
this.session_id = jsonData.data.session_id;
logWithPrefix("Session opened:", this.session_id);
} else {
console.error("Failed to open session:", jsonData.message);
throw new Error("Failed to open session");
}
}
getChallenge(path, body) {
const url = `${this.host}${path}/get_license_challenge`;
const xhr = new XMLHttpRequest();
xhr.open("POST", url, false);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.data?.challenge) {
this.challenge = btoa(jsonData.data.challenge);
logWithPrefix("Challenge received:", this.challenge);
} else if (jsonData.data?.challenge_b64) {
this.challenge = jsonData.data.challenge_b64;
logWithPrefix("Challenge received:", this.challenge);
} else {
console.error("Failed to get challenge:", jsonData.message);
throw new Error("Failed to get challenge");
}
}
parseLicense(path, body) {
const url = `${this.host}${path}/parse_license`;
const xhr = new XMLHttpRequest();
xhr.open("POST", url, false);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.status === 200 || jsonData.message?.includes("parsed and loaded")) {
logWithPrefix("License response parsed successfully");
return true;
} else {
console.error("Failed to parse license response:", jsonData.message);
throw new Error("Failed to parse license response");
}
}
getKeys(path, body, extraPath = "") {
const url = `${this.host}${path}/get_keys${extraPath}`;
const xhr = new XMLHttpRequest();
xhr.open("POST", url, false);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.data?.keys) {
this.keys = jsonData.data.keys;
logWithPrefix("Keys received:", this.keys);
} else {
console.error("Failed to get keys:", jsonData.message);
throw new Error("Failed to get keys");
}
}
closeSession(path) {
const url = `${this.host}${path}/close/${this.session_id}`;
const xhr = new XMLHttpRequest();
xhr.open("GET", url, false);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send();
const jsonData = JSON.parse(xhr.responseText);
if (jsonData) {
logWithPrefix("Session closed successfully");
} else {
console.error("Failed to close session:", jsonData.message);
throw new Error("Failed to close session");
}
}
}
// PlayReady Remote CDM Class
class remotePlayReadyCDM extends RemoteCDMBase {
constructor(security_level, host, secret, device_name) {
super({ host, secret, device_name, security_level });
}
openSession() {
super.openSession(`/remotecdm/playready/${this.device_name}`);
}
getChallenge(init_data) {
super.getChallenge(`/remotecdm/playready/${this.device_name}`, {
session_id: this.session_id,
init_data: init_data,
});
}
parseLicense(license_message) {
return super.parseLicense(`/remotecdm/playready/${this.device_name}`, {
session_id: this.session_id,
license_message: license_message,
});
}
getKeys() {
super.getKeys(`/remotecdm/playready/${this.device_name}`, {
session_id: this.session_id,
});
}
closeSession() {
super.closeSession(`/remotecdm/playready/${this.device_name}`);
}
}
// Widevine Remote CDM Class
class remoteWidevineCDM extends RemoteCDMBase {
constructor(device_type, system_id, security_level, host, secret, device_name) {
super({ host, secret, device_name, security_level });
this.device_type = device_type;
this.system_id = system_id;
}
openSession() {
super.openSession(`/remotecdm/widevine/${this.device_name}`);
}
setServiceCertificate(certificate) {
const url = `${this.host}/remotecdm/widevine/${this.device_name}/set_service_certificate`;
const xhr = new XMLHttpRequest();
xhr.open("POST", url, false);
xhr.setRequestHeader("Content-Type", "application/json");
const body = {
session_id: this.session_id,
certificate: certificate ?? null,
};
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.status === 200) {
logWithPrefix("Service certificate set successfully");
} else {
console.error("Failed to set service certificate:", jsonData.message);
throw new Error("Failed to set service certificate");
}
}
getChallenge(init_data, license_type = "STREAMING") {
const url = `${this.host}/remotecdm/widevine/${this.device_name}/get_license_challenge/${license_type}`;
const xhr = new XMLHttpRequest();
xhr.open("POST", url, false);
xhr.setRequestHeader("Content-Type", "application/json");
const body = {
session_id: this.session_id,
init_data: init_data,
privacy_mode: serviceCertFound,
};
xhr.send(JSON.stringify(body));
const jsonData = JSON.parse(xhr.responseText);
if (jsonData.data?.challenge_b64) {
this.challenge = jsonData.data.challenge_b64;
logWithPrefix("Widevine challenge received:", this.challenge);
} else {
console.error("Failed to get Widevine challenge:", jsonData.message);
throw new Error("Failed to get Widevine challenge");
}
}
parseLicense(license_message) {
return super.parseLicense(`/remotecdm/widevine/${this.device_name}`, {
session_id: this.session_id,
license_message: license_message,
});
}
getKeys() {
super.getKeys(
`/remotecdm/widevine/${this.device_name}`,
{
session_id: this.session_id,
},
"/ALL"
);
}
closeSession() {
super.closeSession(`/remotecdm/widevine/${this.device_name}`);
}
}
// Utility functions
function hexStrToU8(hexString) {
return Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
}
function u8ToHexStr(bytes) {
return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
}
function b64ToHexStr(b64) {
return [...atob(b64)].map((c) => c.charCodeAt(0).toString(16).padStart(2, "0")).join``;
}
function jsonContainsValue(obj, prefix = DRM_SIGNATURES.WIDEVINE) {
if (typeof obj === "string") return obj.startsWith(prefix);
if (Array.isArray(obj)) return obj.some((val) => jsonContainsValue(val, prefix));
if (typeof obj === "object" && obj !== null) {
return Object.values(obj).some((val) => jsonContainsValue(val, prefix));
}
return false;
}
function jsonReplaceValue(obj, newValue) {
if (typeof obj === "string") {
return obj.startsWith(DRM_SIGNATURES.WIDEVINE) || obj.startsWith(DRM_SIGNATURES.PLAYREADY)
? newValue
: obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => jsonReplaceValue(item, newValue));
}
if (typeof obj === "object" && obj !== null) {
const newObj = {};
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
newObj[key] = jsonReplaceValue(obj[key], newValue);
}
}
return newObj;
}
return obj;
}
function isJson(str) {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
function getWidevinePssh(buffer) {
const hex = u8ToHexStr(new Uint8Array(buffer));
const match = hex.match(/000000(..)?70737368.*/);
if (!match) return null;
const boxHex = match[0];
const bytes = hexStrToU8(boxHex);
return window.btoa(String.fromCharCode(...bytes));
}
function getPlayReadyPssh(buffer) {
const u8 = new Uint8Array(buffer);
const systemId = "9a04f07998404286ab92e65be0885f95";
const hex = u8ToHexStr(u8);
const index = hex.indexOf(systemId);
if (index === -1) return null;
const psshBoxStart = hex.lastIndexOf("70737368", index);
if (psshBoxStart === -1) return null;
const lenStart = psshBoxStart - 8;
const boxLen = parseInt(hex.substr(lenStart, 8), 16) * 2;
const psshHex = hex.substr(lenStart, boxLen);
const psshBytes = hexStrToU8(psshHex);
return window.btoa(String.fromCharCode(...psshBytes));
}
function getClearkey(response) {
let obj = JSON.parse(new TextDecoder("utf-8").decode(response));
return obj["keys"].map((o) => ({
key_id: b64ToHexStr(o["kid"].replace(/-/g, "+").replace(/_/g, "/")),
key: b64ToHexStr(o["k"].replace(/-/g, "+").replace(/_/g, "/")),
}));
}
function base64ToUint8Array(base64) {
const binaryStr = atob(base64);
const len = binaryStr.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryStr.charCodeAt(i);
}
return bytes;
}
function arrayBufferToBase64(uint8array) {
let binary = "";
const len = uint8array.length;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(uint8array[i]);
}
return window.btoa(binary);
}
function bufferToBase64(buffer) {
const uint8 = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
return window.btoa(String.fromCharCode(...uint8));
}
// DRM type detection
function isWidevine(base64str) {
return base64str.startsWith(DRM_SIGNATURES.WIDEVINE);
}
function isPlayReady(base64str) {
return base64str.startsWith(DRM_SIGNATURES.PLAYREADY);
}
function isServiceCertificate(base64str) {
return base64str.startsWith(DRM_SIGNATURES.SERVICE_CERT);
}
function postDRMTypeAndPssh(type, pssh) {
window.postMessage({ type: "__DRM_TYPE__", data: type }, "*");
window.postMessage({ type: "__PSSH_DATA__", data: pssh }, "*");
}
function createAndOpenRemoteCDM(type, deviceInfo, pssh) {
let cdm;
if (type === "Widevine") {
const { device_type, system_id, security_level, host, secret, device_name } = deviceInfo;
cdm = new remoteWidevineCDM(
device_type,
system_id,
security_level,
host,
secret,
device_name
);
cdm.openSession();
cdm.getChallenge(pssh);
} else if (type === "PlayReady") {
const { security_level, host, secret, device_name } = deviceInfo;
cdm = new remotePlayReadyCDM(security_level, host, secret, device_name);
cdm.openSession();
cdm.getChallenge(pssh);
}
return cdm;
}
function ensureRemoteCDM(type, deviceInfo, pssh) {
if (!remoteCDM) {
remoteCDM = createAndOpenRemoteCDM(type, deviceInfo, pssh);
}
}
function detectAndStorePssh(initData) {
const detections = [
{
type: "PlayReady",
getter: getPlayReadyPssh,
store: (pssh) => (foundPlayreadyPssh = pssh),
},
{ type: "Widevine", getter: getWidevinePssh, store: (pssh) => (foundWidevinePssh = pssh) },
];
detections.forEach(({ type, getter, store }) => {
const pssh = getter(initData);
if (pssh) {
logWithPrefix(`[DRM Detected] ${type}`);
store(pssh);
logWithPrefix(`[${type} PSSH found] ${pssh}`);
}
});
}
// Challenge generator interceptor
const originalGenerateRequest = MediaKeySession.prototype.generateRequest;
MediaKeySession.prototype.generateRequest = function (initDataType, initData) {
const session = this;
detectAndStorePssh(initData);
// Challenge message interceptor
if (!remoteListenerMounted) {
remoteListenerMounted = true;
session.addEventListener("message", function messageInterceptor(event) {
event.stopImmediatePropagation();
const base64challenge = bufferToBase64(event.message);
if (
base64challenge === DRM_SIGNATURES.WIDEVINE_INIT &&
interceptType !== "DISABLED" &&
!serviceCertFound
) {
remoteCDM = createAndOpenRemoteCDM(
"Widevine",
widevineDeviceInfo,
foundWidevinePssh
);
}
if (
!injectionSuccess &&
base64challenge !== DRM_SIGNATURES.WIDEVINE_INIT &&
interceptType !== "DISABLED"
) {
if (interceptType === "EME") {
injectionSuccess = true;
}
if (!originalChallenge) {
originalChallenge = base64challenge;
}
if (originalChallenge.startsWith(DRM_SIGNATURES.WIDEVINE)) {
postDRMTypeAndPssh("Widevine", foundWidevinePssh);
if (interceptType === "EME") {
ensureRemoteCDM("Widevine", widevineDeviceInfo, foundWidevinePssh);
}
}
if (!originalChallenge.startsWith(DRM_SIGNATURES.WIDEVINE)) {
const buffer = event.message;
const decoder = new TextDecoder("utf-16");
const decodedText = decoder.decode(buffer);
const match = decodedText.match(
/<Challenge encoding="base64encoded">([^<]+)<\/Challenge>/
);
if (match) {
postDRMTypeAndPssh("PlayReady", foundPlayreadyPssh);
originalChallenge = match[1];
if (interceptType === "EME") {
ensureRemoteCDM("PlayReady", playreadyDeviceInfo, foundPlayreadyPssh);
}
}
}
if (interceptType === "EME" && remoteCDM) {
const uint8challenge = base64ToUint8Array(remoteCDM.challenge);
const challengeBuffer = uint8challenge.buffer;
const syntheticEvent = new MessageEvent("message", {
data: event.data,
origin: event.origin,
lastEventId: event.lastEventId,
source: event.source,
ports: event.ports,
});
Object.defineProperty(syntheticEvent, "message", {
get: () => challengeBuffer,
});
logWithPrefix("Intercepted EME Challenge and injected custom one.");
session.dispatchEvent(syntheticEvent);
}
}
});
logWithPrefix("Message interceptor mounted.");
}
return originalGenerateRequest.call(session, initDataType, initData);
};
// Message update interceptors
const originalUpdate = MediaKeySession.prototype.update;
MediaKeySession.prototype.update = function (response) {
const base64Response = bufferToBase64(response);
if (
base64Response.startsWith(DRM_SIGNATURES.SERVICE_CERT) &&
foundWidevinePssh &&
remoteCDM &&
!serviceCertFound
) {
remoteCDM.setServiceCertificate(base64Response);
if (interceptType === "EME" && !remoteCDM.challenge) {
remoteCDM.getChallenge(foundWidevinePssh);
}
window.postMessage({ type: "__DRM_TYPE__", data: "Widevine" }, "*");
window.postMessage({ type: "__PSSH_DATA__", data: foundWidevinePssh }, "*");
serviceCertFound = true;
}
if (
!base64Response.startsWith(DRM_SIGNATURES.SERVICE_CERT) &&
(foundWidevinePssh || foundPlayreadyPssh) &&
!keysRetrieved
) {
if (licenseResponseCounter === 1 || foundChallengeInBody) {
remoteCDM.parseLicense(base64Response);
remoteCDM.getKeys();
remoteCDM.closeSession();
keysRetrieved = true;
window.postMessage({ type: "__KEYS_DATA__", data: remoteCDM.keys }, "*");
}
licenseResponseCounter++;
}
const updatePromise = originalUpdate.call(this, response);
if (!foundPlayreadyPssh && !foundWidevinePssh) {
updatePromise
.then(() => {
let clearKeys = getClearkey(response);
if (clearKeys && clearKeys.length > 0) {
logWithPrefix("[CLEARKEY] ", clearKeys);
const drmType = {
type: "__DRM_TYPE__",
data: "ClearKey",
};
window.postMessage(drmType, "*");
const keysData = {
type: "__KEYS_DATA__",
data: clearKeys,
};
window.postMessage(keysData, "*");
}
})
.catch((e) => {
logWithPrefix("[CLEARKEY] Not found");
});
}
return updatePromise;
};
// Helpers
function detectDRMChallenge(body) {
// Handles ArrayBuffer, Uint8Array, string, and JSON string
// Returns: { type: "Widevine"|"PlayReady"|null, base64: string|null, bodyType: "buffer"|"string"|"json"|null }
if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
const buffer = body instanceof Uint8Array ? body : new Uint8Array(body);
const base64Body = window.btoa(String.fromCharCode(...buffer));
if (base64Body.startsWith(DRM_SIGNATURES.WIDEVINE)) {
return { type: "Widevine", base64: base64Body, bodyType: "buffer" };
}
if (base64Body.startsWith(DRM_SIGNATURES.PLAYREADY)) {
return { type: "PlayReady", base64: base64Body, bodyType: "buffer" };
}
} else if (typeof body === "string" && !isJson(body)) {
const base64EncodedBody = btoa(body);
if (base64EncodedBody.startsWith(DRM_SIGNATURES.WIDEVINE)) {
return { type: "Widevine", base64: base64EncodedBody, bodyType: "string" };
}
if (base64EncodedBody.startsWith(DRM_SIGNATURES.PLAYREADY)) {
return { type: "PlayReady", base64: base64EncodedBody, bodyType: "string" };
}
} else if (typeof body === "string" && isJson(body)) {
const jsonBody = JSON.parse(body);
if (jsonContainsValue(jsonBody, DRM_SIGNATURES.WIDEVINE)) {
return { type: "Widevine", base64: null, bodyType: "json" };
}
if (jsonContainsValue(jsonBody, DRM_SIGNATURES.PLAYREADY)) {
return { type: "PlayReady", base64: null, bodyType: "json" };
}
}
return { type: null, base64: null, bodyType: null };
}
function handleLicenseMode({
drmInfo,
body,
setBody, // function to set the new body (for fetch: (b) => config.body = b, for XHR: (b) => originalSend.call(this, b))
urlOrResource,
getWidevinePssh,
getPlayreadyPssh,
widevineDeviceInfo,
playreadyDeviceInfo,
}) {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: urlOrResource }, "*");
// Create remoteCDM if needed
if (!remoteCDM) {
if (drmInfo.type === "Widevine") {
remoteCDM = createAndOpenRemoteCDM("Widevine", widevineDeviceInfo, getWidevinePssh());
}
if (drmInfo.type === "PlayReady") {
remoteCDM = createAndOpenRemoteCDM(
"PlayReady",
playreadyDeviceInfo,
getPlayreadyPssh()
);
}
}
if (remoteCDM && remoteCDM.challenge === null) {
remoteCDM.getChallenge(getWidevinePssh());
}
// Inject the new challenge into the request body
if (drmInfo.bodyType === "json") {
const jsonBody = JSON.parse(body);
const injectedBody = jsonReplaceValue(jsonBody, remoteCDM.challenge);
setBody(JSON.stringify(injectedBody));
} else if (drmInfo.bodyType === "buffer") {
setBody(base64ToUint8Array(remoteCDM.challenge));
} else {
setBody(atob(remoteCDM.challenge));
}
}
function handleDRMInterception(drmInfo, body, url, setBodyCallback, continueRequestCallback) {
// EME mode: block the request if a DRM challenge is detected
if (
drmInfo.type &&
(!remoteCDM || remoteCDM.challenge === null || drmInfo.base64 !== remoteCDM.challenge) &&
interceptType === "EME"
) {
foundChallengeInBody = true;
window.postMessage({ type: "__LICENSE_URL__", data: url }, "*");
// Block the request
return { shouldBlock: true };
}
// LICENSE mode: replace the challenge in the request
if (drmInfo.type && interceptType === "LICENSE" && !foundChallengeInBody) {
handleLicenseMode({
drmInfo,
body,
setBody: setBodyCallback,
urlOrResource: url,
getWidevinePssh: () => foundWidevinePssh,
getPlayreadyPssh: () => foundPlayreadyPssh,
widevineDeviceInfo,
playreadyDeviceInfo,
});
return { shouldIntercept: true, result: continueRequestCallback() };
}
return { shouldContinue: true };
}
// fetch POST interceptor
(function () {
const originalFetch = window.fetch;
window.fetch = async function (resource, config = {}) {
const method = (config.method || "GET").toUpperCase();
if (method === "POST" && config.body) {
logWithPrefix("[FETCH] Intercepting POST request to:", resource);
const drmInfo = detectDRMChallenge(config.body);
const result = handleDRMInterception(
drmInfo,
config.body,
resource,
(b) => {
config.body = b;
},
() => originalFetch(resource, config)
);
if (result.shouldBlock) return;
if (result.shouldIntercept) return result.result;
}
return originalFetch(resource, config);
};
})();
// XHR POST interceptor
(function () {
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
this._method = method;
this._url = url;
return originalOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {
if (this._method && this._method.toUpperCase() === "POST" && body) {
logWithPrefix("[XHR] Intercepting POST request to:", this._url);
const drmInfo = detectDRMChallenge(body);
const result = handleDRMInterception(
drmInfo,
body,
this._url,
(b) => originalSend.call(this, b),
() => {} // XHR doesn't need continuation callback
);
if (result.shouldBlock) return;
if (result.shouldIntercept) return result.result;
}
return originalSend.apply(this, arguments);
};
})();

View File

@ -1,37 +0,0 @@
{
"manifest_version": 3,
"name": "CDRM Extension",
"version": "2.1.0",
"description": "Decrypt DRM protected content",
"permissions": ["storage", "activeTab", "contextMenus"],
"host_permissions": ["<all_urls>"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start",
"all_frames": true
}
],
"web_accessible_resources": [
{
"resources": ["inject.js"],
"matches": ["<all_urls>"]
}
],
"action": {
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"128": "icons/icon128.png"
}
},
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"128": "icons/icon128.png"
}
}

View File

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